remove usernames

This commit is contained in:
2023-05-08 10:40:48 +10:00
parent 7a4f72378d
commit ac2dd23ad7
43 changed files with 372 additions and 864 deletions

View File

@@ -23,7 +23,7 @@ class UserConductor extends Conductor
{
$user = auth()->user();
if ($user === null || $user->hasPermission('admin/users') === false) {
return ['id', 'username'];
return ['id', 'display_name'];
}
return parent::fields($model);
@@ -41,7 +41,7 @@ class UserConductor extends Conductor
$data = $model->toArray();
if ($user === null || ($user->hasPermission('admin/users') === false && strcasecmp($user->id, $model->id) !== 0)) {
$fields = ['id', 'username', 'display_name'];
$fields = ['id', 'display_name'];
$data = arrayLimitKeys($data, $fields);
} else {
$data['permissions'] = $user->permissions;

View File

@@ -47,18 +47,18 @@ class AuthController extends ApiController
*/
public function login(AuthLoginRequest $request)
{
$user = User::where('username', '=', $request->input('username'))->first();
$user = User::where('email', '=', $request->input('email'))->first();
if ($user !== null && Hash::check($request->input('password'), $user->password) === true) {
if ($user->email_verified_at === null) {
return $this->respondWithErrors([
'username' => 'Email address has not been verified.'
'email' => 'Email address has not been verified.'
]);
}
if ($user->disabled === true) {
return $this->respondWithErrors([
'username' => 'Account has been disabled.'
'email' => 'Account has been disabled.'
]);
}
@@ -78,8 +78,8 @@ class AuthController extends ApiController
}//end if
return $this->respondWithErrors([
'username' => 'Invalid username or password',
'password' => 'Invalid username or password',
'email' => 'Invalid email or password',
'password' => 'Invalid email or password',
]);
}

View File

@@ -5,7 +5,6 @@ namespace App\Http\Controllers\Api;
use App\Enum\HttpResponseCodes;
use App\Http\Requests\UserRequest;
use App\Http\Requests\UserForgotPasswordRequest;
use App\Http\Requests\UserForgotUsernameRequest;
use App\Http\Requests\UserRegisterRequest;
use App\Http\Requests\UserResendVerifyEmailRequest;
use App\Http\Requests\UserResetPasswordRequest;
@@ -14,7 +13,6 @@ use App\Jobs\SendEmailJob;
use App\Mail\ChangedEmail;
use App\Mail\ChangedPassword;
use App\Mail\ChangeEmailVerify;
use App\Mail\ForgotUsername;
use App\Mail\ForgotPassword;
use App\Mail\EmailVerify;
use App\Models\User;
@@ -37,7 +35,6 @@ class UserController extends ApiController
'register',
'exists',
'forgotPassword',
'forgotUsername',
'resetPassword',
'verifyEmail',
'resendVerifyEmailCode'
@@ -105,7 +102,7 @@ class UserController extends ApiController
{
if (UserConductor::updatable($user) === true) {
$input = [];
$updatable = ['username', 'first_name', 'last_name', 'email', 'phone', 'password', 'display_name'];
$updatable = ['first_name', 'last_name', 'email', 'phone', 'password', 'display_name'];
if ($request->user()->hasPermission('admin/user') === true) {
$updatable = array_merge($updatable, ['email_verified_at']);
@@ -149,15 +146,28 @@ class UserController extends ApiController
public function register(UserRegisterRequest $request)
{
try {
$user = User::create([
'first_name' => $request->input('first_name'),
'last_name' => $request->input('last_name'),
'username' => $request->input('username'),
'email' => $request->input('email'),
'phone' => $request->input('phone', ''),
'password' => Hash::make($request->input('password')),
'display_name' => $request->input('display_name', $request->input('username')),
]);
$user = User::where('email', $request->input('email'))
->whereNull('password')
->first();
if ($user === null) {
$user = User::create([
'first_name' => $request->input('first_name'),
'last_name' => $request->input('last_name'),
'email' => $request->input('email'),
'phone' => $request->input('phone', ''),
'password' => Hash::make($request->input('password')),
'display_name' => $request->input('display_name'),
]);
} else {
$user->update([
'first_name' => $request->input('first_name'),
'last_name' => $request->input('last_name'),
'phone' => $request->input('phone', ''),
'password' => Hash::make($request->input('password')),
'display_name' => $request->input('display_name'),
]);
}//end if
$code = $user->codes()->create([
'action' => 'verify-email',
@@ -175,26 +185,6 @@ class UserController extends ApiController
}//end try
}
/**
* Sends an email with all the usernames registered at that address
*
* @param \App\Http\Requests\UserForgotUsernameRequest $request The forgot username request.
* @return \Illuminate\Http\Response
*/
public function forgotUsername(UserForgotUsernameRequest $request)
{
$users = User::where('email', $request->input('email'))->whereNotNull('email_verified_at')->get();
if ($users->count() > 0) {
dispatch((new SendEmailJob(
$users->first()->email,
new ForgotUsername($users->pluck('username')->toArray())
)))->onQueue('mail');
return $this->respondNoContent();
}
return $this->respondJson(['message' => 'Username send to the email address if registered']);
}
/**
* Generates a new reset password code
*
@@ -203,7 +193,7 @@ class UserController extends ApiController
*/
public function forgotPassword(UserForgotPasswordRequest $request)
{
$user = User::where('username', $request->input('username'))->first();
$user = User::where('email', $request->input('email'))->first();
if ($user !== null) {
$user->codes()->where('action', 'reset-password')->delete();
$code = $user->codes()->create([
@@ -299,7 +289,7 @@ class UserController extends ApiController
{
UserCode::clearExpired();
$user = User::where('username', $request->input('username'))->first();
$user = User::where('email', $request->input('email'))->first();
if ($user !== null) {
$code = $user->codes()->where('action', 'verify-email')->first();
$code->regenerate();
@@ -324,7 +314,7 @@ class UserController extends ApiController
*/
public function resendVerifyEmailCode(UserResendVerifyEmailRequest $request)
{
$user = User::where('username', $request->input('username'))->first();
$user = User::where('email', $request->input('email'))->first();
if ($user !== null) {
$user->codes()->where('action', 'verify-email')->delete();

View File

@@ -14,7 +14,7 @@ class AuthLoginRequest extends FormRequest
public function rules()
{
return [
'username' => 'required|string|min:6|max:255',
'email' => 'required|string|min:6|max:255',
'password' => 'required|string|min:6',
];
}

View File

@@ -15,7 +15,7 @@ class UserForgotPasswordRequest extends FormRequest
public function rules()
{
return [
'username' => 'required|exists:users,username',
'email' => 'required|exists:users,email',
// 'captcha_token' => [new Recaptcha()],
];
}

View File

@@ -1,22 +0,0 @@
<?php
namespace App\Http\Requests;
use App\Rules\Recaptcha;
use Illuminate\Foundation\Http\FormRequest;
class UserForgotUsernameRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'email' => 'required|email|max:255',
// 'captcha_token' => [new Recaptcha()],
];
}
}

View File

@@ -16,9 +16,8 @@ class UserRegisterRequest extends FormRequest
return [
'first_name' => 'required|string|max:255',
'last_name' => 'required|string|max:255',
'display_name' => 'required|string|max:255',
'email' => 'required|string|email|max:255',
'username' => 'required|string|min:4|max:255|unique:users',
'display_name' => 'required|string|max:255|uniqueish:users',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8',
];
}

View File

@@ -3,9 +3,18 @@
namespace App\Http\Requests;
use Illuminate\Validation\Rule;
use App\Rules\Uniqueish;
class UserRequest extends BaseRequest
{
/**
* Fields that are required unless all are null.
*
* @var string[]
*/
protected $required_with_all = ['first_name','last_name','display_name','phone'];
/**
* Apply the additional POST base rules to this request
*
@@ -14,11 +23,10 @@ class UserRequest extends BaseRequest
public function postRules()
{
return [
'username' => 'required|string|max:255|min:4|unique:users',
'first_name' => 'required|string|max:255|min:2',
'last_name' => 'required|string|max:255|min:2',
'display_name' => 'required|string|max:255',
'email' => 'required|string|email|max:255',
'display_name' => 'required|string|max:255|uniqueish:users',
'email' => 'required|string|email|max:255|unique:users',
'phone' => ['string', 'regex:/^(\+|00)?[0-9][0-9 \-\(\)\.]{7,32}$/'],
'email_verified_at' => 'date'
];
@@ -33,24 +41,32 @@ class UserRequest extends BaseRequest
{
$user = $this->route('user');
$required_with_all = count($this->required_with_all) > 0 ? 'required_with_all:' . implode(',', $this->required_with_all) : '';
return [
'username' => [
'first_name' => "nullable|string|required_if_any:users,last_name,display_name,phone,password|between:2,255",
'last_name' => "nullable|required_if_any:users,first_name,display_name,phone,password|string|max:255|min:2",
'display_name' => [
'nullable',
'required_if_any:users,first_name,last_name,phone,password',
'string',
'max:255',
'min:4',
'min:2',
(new Uniqueish('users', 'display_name'))->ignore($user->id),
],
'email' => [
'string',
'email',
'max:255',
Rule::unique('users')->ignore($user->id)->when(
$this->username !== $user->username,
$this->email !== $user->email,
function ($query) {
return $query->where('username', $this->username);
return $query->where('email', $this->email);
}
),
],
'first_name' => 'string|max:255|min:2',
'last_name' => 'string|max:255|min:2',
'display_name' => 'string|max:255|min:2',
'email' => 'string|email|max:255',
'phone' => ['nullable','regex:/^(\+|00)?[0-9][0-9 \-\(\)\.]{7,32}$/'],
'password' => 'string|min:8'
'phone' => ['nullable', 'regex:/^(\+|00)?[0-9][0-9 \-\(\)\.]{7,32}$/'],
'password' => "nullable|{$required_with_all}|string|min:8"
];
}
}

View File

@@ -15,7 +15,7 @@ class UserResendVerifyEmailRequest extends FormRequest
public function rules()
{
return [
'username' => 'required|exists:users,username',
'email' => 'required|exists:users,email',
// 'captcha_token' => [new Recaptcha()],
];
}

View File

@@ -1,60 +0,0 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ForgotUsername extends Mailable
{
use Queueable;
use SerializesModels;
/**
* The list of usernames
*
* @var string[]
*/
public $usernames;
/**
* Create a new message instance.
*
* @param array $usernames The usernames.
* @return void
*/
public function __construct(array $usernames)
{
$this->usernames = $usernames;
}
/**
* Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope()
{
return new Envelope(
subject: '🤦 Forgot your username?',
);
}
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content()
{
return new Content(
view: 'emails.user.forgot_username',
text: 'emails.user.forgot_username_plain',
);
}
}

View File

@@ -25,7 +25,6 @@ class User extends Authenticatable implements Auditable
* @var array<int, string>
*/
protected $fillable = [
'username',
'first_name',
'last_name',
'email',

View File

@@ -20,14 +20,12 @@ class UserFactory extends Factory
$faker = \Faker\Factory::create();
$faker->addProvider(new \Faker\Provider\CustomInternetProvider($faker));
$username = $faker->unique()->userNameWithMinLength(6);
$first_name = $faker->firstName();
$last_name = $faker->lastName();
$display_name = $faker->randomElement([$username, $first_name . ' ' . $last_name]);
$display_name = $first_name . ' ' . $last_name;
return [
'username' => $username,
'first_name' => $first_name,
'last_name' => $last_name,
'email' => $faker->safeEmail(),

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('username');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->string('username')->unique();
});
DB::table('users')->update(['username' => DB::raw('display_name')]);
}
};

View File

@@ -20,7 +20,7 @@ class DatabaseSeeder extends Seeder
\App\Models\User::factory(40)->create();
\App\Models\User::factory()->create([
'username' => 'nomadjimbob',
'display_name' => 'James Collins',
'first_name' => 'James',
'last_name' => 'Collins',
'email' => 'james@stemmechanics.com.au',

View File

@@ -288,6 +288,16 @@ export const routes = [
component: () =>
import("@/views/dashboard/UserList.vue"),
},
{
path: "create",
name: "dashboard-user-create",
meta: {
title: "Create User",
middleware: "authenticated",
},
component: () =>
import("@/views/dashboard/UserEdit.vue"),
},
{
path: ":id",
name: "dashboard-user-edit",

View File

@@ -8,7 +8,7 @@
</h1>
<SMToolbar>
<div>
<div class="author">By {{ article.user.username }}</div>
<div class="author">By {{ article.user.display_name }}</div>
<div class="date">{{ formattedDate(article.publish_at) }}</div>
</div>
<SMButton
@@ -47,7 +47,7 @@ const applicationStore = useApplicationStore();
*/
let article: Ref<Article> = ref({
title: "",
user: { username: "" },
user: { display_name: "" },
});
/**

View File

@@ -5,11 +5,10 @@
<template v-if="!formDone">
<h1>Forgot Password</h1>
<p>
Enter your username below to receive a password reset
link to your email address.
Enter your email below to receive a password reset link.
</p>
<SMForm v-model="form" @submit="handleSubmit">
<SMInput control="username" />
<SMInput control="email" />
<SMButtonRow>
<template #left>
<div class="small">
@@ -31,9 +30,9 @@
<template v-else>
<h1>Email Sent!</h1>
<p class="text-center">
If that username has been registered, you will receive
an email with a reset password link in the next few
minutes.
If that email address has been registered, you will
receive an email with a reset password link in the next
few minutes.
</p>
<SMRow class="pb-2">
<SMColumn class="justify-content-center">
@@ -56,13 +55,13 @@ import SMButtonRow from "../components/SMButtonRow.vue";
import SMInput from "../components/SMInput.vue";
import { api } from "../helpers/api";
import { Form, FormControl } from "../helpers/form";
import { And, Min, Required } from "../helpers/validate";
import { And, Email, Required } from "../helpers/validate";
// const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const formDone = ref(false);
let form = reactive(
Form({
username: FormControl("", And([Required(), Min(4)])),
email: FormControl("", And([Required(), Email()])),
})
);
@@ -76,7 +75,7 @@ const handleSubmit = async () => {
await api.post({
url: "/users/forgotPassword",
body: {
username: form.controls.username.value,
email: form.controls.email.value,
// captcha_token: captcha,
},
});

View File

@@ -1,90 +0,0 @@
<template>
<SMPage>
<SMRow>
<SMFormCard class="mt-5">
<template v-if="!formDone">
<h1>Forgot Username</h1>
<p>
Enter your email address, and if an account exists, we
will email you your username.
</p>
<SMForm v-model="form" @submit="handleSubmit">
<SMInput control="email" />
<SMButtonRow>
<template #left>
<div class="small">
<span class="pr-1">Remember?</span
><router-link :to="{ name: 'login' }"
>Log in</router-link
>
</div>
</template>
<template #right>
<SMButton
type="submit"
label="Send"
icon="arrow-forward-outline" />
</template>
</SMButtonRow>
</SMForm>
</template>
<template v-else>
<h1>Email Sent!</h1>
<p class="text-center">
If that email has a registered account, you should
receive it shortly.
</p>
<SMRow class="pb-2">
<SMColumn class="justify-content-center">
<SMButton :to="{ name: 'home' }" label="Home" />
</SMColumn>
</SMRow>
</template>
</SMFormCard>
</SMRow>
</SMPage>
</template>
<script setup lang="ts">
import { reactive, ref } from "vue";
// import { useReCaptcha } from "vue-recaptcha-v3";
import SMButton from "../components/SMButton.vue";
import SMFormCard from "../components/SMFormCard.vue";
import SMForm from "../components/SMForm.vue";
import SMButtonRow from "../components/SMButtonRow.vue";
import SMInput from "../components/SMInput.vue";
import { api } from "../helpers/api";
import { Form, FormControl } from "../helpers/form";
import { And, Email, Required } from "../helpers/validate";
// const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const formDone = ref(false);
let form = reactive(
Form({
email: FormControl("", And([Required(), Email()])),
})
);
const handleSubmit = async () => {
form.loading(true);
try {
// await recaptchaLoaded();
// const captcha = await executeRecaptcha("submit");
await api.post({
url: "/users/forgotUsername",
body: {
email: form.controls.email.value,
// captcha_token: captcha,
},
});
formDone.value = true;
} catch (error) {
form.apiErrors(error);
}
form.loading(false);
};
</script>

View File

@@ -9,11 +9,7 @@
</p>
</template>
<template #body>
<SMInput control="username" autofocus>
<router-link to="/forgot-username"
>Forgot username?</router-link
>
</SMInput>
<SMInput control="email" autofocus> </SMInput>
<SMInput control="password" type="password">
<router-link to="/forgot-password"
>Forgot password?</router-link
@@ -54,7 +50,7 @@ import SMInput from "../components/SMInput.vue";
import { api } from "../helpers/api";
import { LoginResponse } from "../helpers/api.types";
import { Form, FormControl } from "../helpers/form";
import { And, Min, Required } from "../helpers/validate";
import { And, Email, Required } from "../helpers/validate";
import { useUserStore } from "../store/UserStore";
import SMButtonRow from "../components/SMButtonRow.vue";
@@ -63,7 +59,7 @@ const userStore = useUserStore();
const router = useRouter();
let form = reactive(
Form({
username: FormControl("", And([Required(), Min(4)])),
email: FormControl("", And([Required(), Email()])),
password: FormControl("", Required()),
})
);
@@ -81,7 +77,7 @@ const handleSubmit = async () => {
let result = await api.post({
url: "/login",
body: {
username: form.controls.username.value,
email: form.controls.email.value,
password: form.controls.password.value,
},
});

View File

@@ -1,7 +1,7 @@
<template>
<SMContainer :center="true">
<SMForm v-if="!userRegistered" v-model="form" @submit="handleSubmit">
<SMFormCard full>
<SMFormCard>
<template #header>
<h2>Register</h2>
<p>
@@ -10,36 +10,9 @@
</p>
</template>
<template #body>
<SMRow>
<SMColumn>
<SMInput control="username" autofocus />
</SMColumn>
<SMColumn>
<SMInput
control="password"
type="password"></SMInput>
</SMColumn>
</SMRow>
<SMRow>
<SMColumn>
<SMInput control="first_name" />
</SMColumn>
<SMColumn>
<SMInput control="last_name" />
</SMColumn>
</SMRow>
<SMRow>
<SMColumn>
<SMInput control="email" />
</SMColumn>
<SMColumn>
<SMInput control="phone"
><template #help
>This field is optional</template
>
</SMInput>
</SMColumn>
</SMRow>
<SMInput control="email" autofocus />
<SMInput control="password" type="password" />
<SMInput control="display_name" label="Display Name" />
</template>
<template #footer-space-between>
<div class="small">
@@ -125,7 +98,6 @@ let form = reactive(
first_name: FormControl("", Required()),
last_name: FormControl("", Required()),
email: FormControl("", And([Required(), Email()])),
phone: FormControl("", Phone()),
username: FormControl("", And([Min(4), Custom(checkUsername)])),
password: FormControl("", And([Required(), Password()])),
})
@@ -135,20 +107,14 @@ const handleSubmit = async () => {
form.loading(true);
try {
// await recaptchaLoaded();
// const captcha = await executeRecaptcha("submit");
await api.post({
url: "/register",
body: {
first_name: form.controls.first_name.value,
last_name: form.controls.last_name.value,
email: form.controls.email.value,
phone: form.controls.phone.value,
username: form.controls.username.value,
password: form.controls.password.value,
display_name: form.controls.username.value,
// captcha_token: captcha,
display_name: form.controls.display_name.value,
},
});

View File

@@ -5,7 +5,7 @@
<template v-if="!formDone">
<h1>Resend Verify Email</h1>
<SMForm v-model="form" @submit="handleSubmit">
<SMInput control="username" />
<SMInput control="email" />
<SMButtonRow>
<template #left>
<div class="small">
@@ -27,9 +27,9 @@
<template v-else>
<h1>Email Sent!</h1>
<p class="text-center">
If that username has been registered, and you still need
to verify your email, you will receive an email with a
new verify code.
If that email address has been registered, and you still
need to verify your email, you will receive an email
with a new verify code.
</p>
<SMButtonRow>
<template #right>
@@ -52,13 +52,13 @@ import SMButtonRow from "../components/SMButtonRow.vue";
import SMInput from "../components/SMInput.vue";
import { api } from "../helpers/api";
import { Form, FormControl } from "../helpers/form";
import { Required } from "../helpers/validate";
import { And, Email, Required } from "../helpers/validate";
// const { executeRecaptcha, recaptchaLoaded } = useReCaptcha();
const formDone = ref(false);
let form = reactive(
Form({
username: FormControl("", Required()),
email: FormControl("", And([Required(), Email()])),
})
);
@@ -72,7 +72,7 @@ const handleSubmit = async () => {
await api.post({
url: "/users/resendVerifyEmailCode",
body: {
username: form.controls.username.value,
email: form.controls.email.value,
// captcha_token: captcha,
},
});

View File

@@ -301,7 +301,7 @@ const loadOptionsAuthors = async () => {
api.get({
url: "/users",
params: {
fields: "id,username,first_name,last_name",
fields: "id,display_name",
limit: 100,
},
})
@@ -312,7 +312,7 @@ const loadOptionsAuthors = async () => {
authors.value = {};
data.users.forEach((item) => {
authors.value[item.id] = `${item.username}`;
authors.value[item.id] = `${item.display_name}`;
});
}
})

View File

@@ -2,15 +2,18 @@
<SMMastHead
:title="pageHeading"
:back-link="
route.params.id
route.params.id || isCreatingUser
? { name: 'dashboard-user-list' }
: { name: 'dashboard' }
"
:back-title="route.params.id ? 'Back to Users' : 'Back to Dashboard'" />
:back-title="
route.params.id || isCreatingUser
? 'Back to Users'
: 'Back to Dashboard'
" />
<SMContainer>
<SMForm :model-value="form" @submit="handleSubmit">
<SMRow>
<SMColumn><SMInput control="username" disabled /></SMColumn>
<SMColumn><SMInput control="display_name" /></SMColumn>
</SMRow>
<SMRow>
@@ -90,9 +93,10 @@ const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const isCreatingUser = route.path.endsWith("/create");
let form = reactive(
Form({
username: FormControl("", And([Required()])),
display_name: FormControl("", And([Required()])),
first_name: FormControl("", And([Required()])),
last_name: FormControl("", And([Required()])),
@@ -122,7 +126,6 @@ const loadData = async () => {
const data = result.data as UserResponse;
if (data && data.user) {
form.controls.username.value = data.user.username;
form.controls.first_name.value = data.user.first_name;
form.controls.last_name.value = data.user.last_name;
form.controls.display_name.value = data.user.display_name;
@@ -134,8 +137,7 @@ const loadData = async () => {
} finally {
form.loading(false);
}
} else {
form.controls.username.value = userStore.username;
} else if (isCreatingUser == false) {
form.controls.first_name.value = userStore.firstName;
form.controls.last_name.value = userStore.lastName;
form.controls.display_name.value = userStore.displayName;
@@ -150,34 +152,56 @@ const loadData = async () => {
const handleSubmit = async () => {
try {
form.loading(true);
const result = await api.put({
url: "/users/{id}",
params: {
id: userStore.id,
},
body: {
first_name: form.controls.first_name.value,
last_name: form.controls.last_name.value,
display_name: form.controls.display_name.value,
email: form.controls.email.value,
phone: form.controls.phone.value,
},
});
const id = route.params.id ? route.params.id : userStore.id;
const data = result.data as UserResponse;
if (isCreatingUser == false) {
const result = await api.put({
url: "/users/{id}",
params: {
id: id,
},
body: {
first_name: form.controls.first_name.value,
last_name: form.controls.last_name.value,
display_name: form.controls.display_name.value,
email: form.controls.email.value,
phone: form.controls.phone.value,
},
});
if (data && data.user) {
userStore.setUserDetails(data.user);
const data = result.data as UserResponse;
if (route.params.id && data && data.user) {
userStore.setUserDetails(data.user);
}
useToastStore().addToast({
title: "Details Updated",
content: "The user has been updated.",
type: "success",
});
} else {
await api.post({
url: "/users",
params: {
id: id,
},
body: {
first_name: form.controls.first_name.value,
last_name: form.controls.last_name.value,
display_name: form.controls.display_name.value,
email: form.controls.email.value,
phone: form.controls.phone.value,
},
});
useToastStore().addToast({
title: "User Created",
content: "The user has been created.",
type: "success",
});
}
useToastStore().addToast({
title: route.params.id ? "Details Updated" : "User Created",
content: route.params.id
? "The user has been updated."
: "The user has been created.",
type: "success",
});
router.push({ name: "dashboard" });
} catch (err) {
form.apiErrors(err);

View File

@@ -1,91 +1,154 @@
<template>
<SMPage permission="admin/users" :page-error="pageError">
<SMPage permission="admin/users">
<SMMastHead
title="Users"
:back-link="{ name: 'dashboard' }"
back-title="Return to Dashboard" />
<SMContainer>
<SMTable
:headers="headers"
:items="items"
@row-click="handleRowClick">
<template #item-actions="item">
<SMButton
label="Edit"
:dropdown="{
download: 'Download',
delete: 'Delete',
}"
size="medium" />
</template>
</SMTable>
<SMContainer class="flex-grow-1">
<SMToolbar>
<SMButton
:to="{ name: 'dashboard-user-create' }"
type="primary"
label="Create User" />
<SMInput
v-model="itemSearch"
label="Search"
class="toolbar-search"
@keyup.enter="handleSearch">
<template #append>
<SMButton
type="primary"
label="Search"
icon="search-outline"
@click="handleSearch" />
</template>
</SMInput>
</SMToolbar>
<SMLoading large v-if="itemsLoading" />
<template v-else>
<SMPagination
v-if="items.length < itemsTotal"
v-model="itemsPage"
:total="itemsTotal"
:per-page="itemsPerPage" />
<SMNoItems v-if="items.length == 0" text="No Media Found" />
<SMTable
:headers="headers"
:items="items"
@row-click="handleEdit">
<template #item-actions="item">
<SMButton
label="Edit"
:dropdown="{
delete: 'Delete',
}"
size="medium"
@click="handleActionButton(item, $event)" />
</template>
</SMTable>
</template>
</SMContainer>
</SMPage>
</template>
<script setup lang="ts">
import { reactive, ref, watch } from "vue";
import { useRouter } from "vue-router";
import { ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { openDialog } from "../../components/SMDialog";
import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
import { api } from "../../helpers/api";
import { api, getApiResultData } from "../../helpers/api";
import { SMDate } from "../../helpers/datetime";
import SMTable from "../../components/SMTable.vue";
import SMMastHead from "../../components/SMMastHead.vue";
import { useToastStore } from "../../store/ToastStore";
import SMNoItems from "../../components/SMNoItems.vue";
import SMButton from "../../components/SMButton.vue";
import SMInput from "../../components/SMInput.vue";
import SMToolbar from "../../components/SMToolbar.vue";
import { updateRouterParams } from "../../helpers/url";
import { User, UserCollection } from "../../helpers/api.types";
import SMLoading from "../../components/SMLoading.vue";
import SMPagination from "../../components/SMPagination.vue";
const route = useRoute();
const router = useRouter();
const searchValue = ref("");
const pageError = ref(0);
const items = ref([]);
const itemsLoading = ref(false);
const itemSearch = ref((route.query.search as string) || "");
const itemsTotal = ref(0);
const itemsPerPage = 25;
const itemsPage = ref(parseInt((route.query.page as string) || "1"));
const headers = [
{ text: "Username", value: "username", sortable: true },
{ text: "Display name", value: "display_name", sortable: true },
{ text: "First name", value: "first_name", sortable: true },
{ text: "Last name", value: "last_name", sortable: true },
{ text: "Email", value: "email", sortable: true },
{ text: "Phone", value: "phone", sortable: true },
{ text: "Joined", value: "created_at", sortable: true },
// { text: "Last logged in", value: "lastAttended", width: 200},
{ text: "Actions", value: "actions" },
];
const items = ref([]);
const formLoading = ref(false);
const serverItemsLength = ref(0);
const serverOptions = ref({
page: 1,
rowsPerPage: 25,
sortBy: null,
sortType: null,
/**
* Watch if page number changes.
*/
watch(itemsPage, () => {
handleLoad();
});
const handleRowClick = (item) => {
router.push({ name: "dashboard-user-edit", params: { id: item.id } });
/**
* Handle searching for item.
*/
const handleSearch = () => {
itemsPage.value = 1;
handleLoad();
};
const loadFromServer = async () => {
formLoading.value = true;
/**
* Handle user selecting option in action button.
*
* @param {Event} item The event item.
* @param option
*/
const handleActionButton = (item: Event, option: string): void => {
if (option.length == 0) {
handleEdit(item);
} else if (option.toLowerCase() == "delete") {
handleDelete(item);
}
};
/**
* Handle loading the page and list
*/
const handleLoad = async () => {
itemsLoading.value = true;
items.value = [];
itemsTotal.value = 0;
updateRouterParams(router, {
search: itemSearch.value,
page: itemsPage.value == 1 ? "" : itemsPage.value.toString(),
});
try {
let params = {};
if (serverOptions.value.sortBy) {
params["sort"] = serverOptions.value.sortBy;
if (
serverOptions.value.sortType &&
serverOptions.value.sortType === "desc"
) {
params["sort"] = "-" + params["sort"];
}
let params = {
page: itemsPage.value,
limit: itemsPerPage,
};
if (itemSearch.value.length > 0) {
params[
"filter"
] = `title:${itemSearch.value},OR,content:${itemSearch.value}`;
}
params["page"] = serverOptions.value.page;
params["limit"] = serverOptions.value.rowsPerPage;
let res = await api.get({
let result = await api.get({
url: "/users",
params: params,
});
items.value = res.data.users;
const userCollection = getApiResultData<UserCollection>(result);
items.value = userCollection.users;
items.value.forEach((row) => {
if (row.created_at !== "undefined") {
@@ -96,44 +159,22 @@ const loadFromServer = async () => {
}
});
serverItemsLength.value = res.data.total;
itemsTotal.value = userCollection.total;
} catch (err) {
/* empty */
}
formLoading.value = false;
itemsLoading.value = false;
};
loadFromServer();
watch(
serverOptions,
() => {
loadFromServer();
},
{ deep: true }
);
const headerItemClassNameFunction = (header) => {
if (["position", "actions"].includes(header.value))
return "easy-data-table-cell-center";
return "";
};
const bodyItemClassNameFunction = (column) => {
if (["position", "actions"].includes(column))
return "easy-data-table-cell-center";
return "";
};
const handleEdit = (user) => {
const handleEdit = (user: User) => {
router.push({ name: "dashboard-user-edit", params: { id: user.id } });
};
const handleDelete = async (user) => {
const handleDelete = async (user: User) => {
let result = await openDialog(DialogConfirm, {
title: "Delete User?",
text: `Are you sure you want to delete the user <strong>${user.username}</strong>?`,
text: `Are you sure you want to delete the user <strong>${user.display_name}</strong>?`,
cancel: {
type: "secondary",
label: "Cancel",
@@ -147,7 +188,7 @@ const handleDelete = async (user) => {
if (result == true) {
try {
await api.delete(`users${user.id}`);
loadFromServer();
handleLoad();
useToastStore().addToast({
title: "User Deleted",
@@ -163,6 +204,33 @@ const handleDelete = async (user) => {
}
}
};
handleLoad();
</script>
<style lang="scss"></style>
<style lang="scss">
.page-dashboard-user-list {
.toolbar-search {
max-width: 350px;
}
// .table tr {
// td:first-of-type,
// td:nth-of-type(2) {
// word-break: break-all;
// }
// td:not(:first-of-type) {
// white-space: nowrap;
// }
// }
}
@media only screen and (max-width: 768px) {
.page-dashboard-user-list {
.toolbar-search {
max-width: none;
}
}
}
</style>

View File

@@ -57,7 +57,7 @@
</td>
</tr>
<tr>
<td><h2>Hey {{ $user?->username }},</h2></td>
<td><h2>Hey {{ $user?->display_name }},</h2></td>
</tr>
<tr>
<td>

View File

@@ -1,4 +1,4 @@
Hey {{ $user?->username }},
Hey {{ $user?->display_name }},
We just need to confirm that this is your new email address.

View File

@@ -57,7 +57,7 @@
</td>
</tr>
<tr>
<td><h2>Yo {{ $user?->username }}</h2></td>
<td><h2>Yo {{ $user?->display_name }}</h2></td>
</tr>
<tr>
<td style="padding-bottom: 2rem">

View File

@@ -1,4 +1,4 @@
Yo {{ $user?->username }}
Yo {{ $user?->display_name }}
Just a quick word that your email has been changed to {{ $new_email }}.

View File

@@ -57,7 +57,7 @@
</td>
</tr>
<tr>
<td><h2>Yo {{ $user?->username }}</h2></td>
<td><h2>Yo {{ $user?->display_name }}</h2></td>
</tr>
<tr>
<td style="padding-bottom: 2rem">

View File

@@ -1,4 +1,4 @@
Yo {{ $user?->username }}
Yo {{ $user?->display_name }}
Just a quick word that your password has been changed.

View File

@@ -57,7 +57,7 @@
</td>
</tr>
<tr>
<td><h2>Welcome {{ $user?->username }},</h2></td>
<td><h2>Welcome {{ $user?->display_name }},</h2></td>
</tr>
<tr>
<td>

View File

@@ -1,4 +1,4 @@
Welcome {{ $user?->username }},
Welcome {{ $user?->display_name }},
We've heard you would like to try out our workshops and courses!
Before we can let you loose on our website, we need to make sure you are a real person and not a pesky robot or cat.

View File

@@ -57,7 +57,7 @@
</td>
</tr>
<tr>
<td><h2>Yo {{ $user?->username }}</h2></td>
<td><h2>Yo {{ $user?->display_name }}</h2></td>
</tr>
<tr>
<td>

View File

@@ -1,4 +1,4 @@
Yo {{ $user?->username }}
Yo {{ $user?->display_name }}
We all forget things sometimes! But you can reset your password typing the following into your browser https://www.stemmechanics.com.au/reset-password and entering the following code:

View File

@@ -1,131 +0,0 @@
<!DOCTYPE html>
<html
lang="{{ str_replace('_', '-', app()->getLocale()) }}"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office"
>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>STEMMechanics - Forgot Password</title>
<link
rel="noopener"
target="_blank"
href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800&display=swap"
rel="stylesheet"
/>
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<style>
@import url("https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800&display=swap");
</style>
</head>
<body>
<table
cellspacing="0"
cellpadding="0"
border="0"
role="presentation"
style="
width: 100%;
padding: 2rem;
font-size: 1.1rem;
color: #000000;
font-family: Nunito, Arial, Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
"
>
<tr>
<td>
<a href="https://www.stemmechanics.com.au/">
<img
alt="STEMMechanics Logo"
src="{{ $message->embed(public_path('assets').'/logo.webp') }}"
width="400"
height="62"
/>
</a>
</td>
</tr>
<tr>
<td>
@if (count($usernames) > 2)
<h2>Yo {{ $usernames[0] }}, {{ $usernames[1] }}, or is it {{ $usernames[count($usernames)-1] }}?</h2>
@elseif (count($usernames) > 1)
<h2>Yo {{ $usernames[0] }}, or is it {{ $usernames[1] }}?</h2>
@else
<h2>Yo {{ $usernames[0] }},</h2>
@endif
</td>
</tr>
<tr>
<td style="padding-bottom: 2rem;">
@if (count($usernames) == 1)
Guess what, your username is <strong>{{ $usernames[0] }}</strong>.
@else
We have the following usernames registered to this email address:
</td>
</tr>
<tr>
<td style="padding-bottom: 2rem;">
<ul>
@foreach($usernames as $username)
<li>{{ $username }}</li>
@endforeach
</ul>
@endif
</td>
</tr>
<tr>
<td
align="center"
style="
font-size: 90%;
text-align: center;
padding-top: 2rem;
padding-bottom: 2rem;
border-top: 1px solid #ddd;
"
>
Need help or got feedback?
<a href="https://www.stemmechanics.com.au/contact"
>Contact us</a
>
or touch base at
<a href="https://twitter.com/stemmechanics"
>@stemmechanics</a
>.
</td>
</tr>
<tr>
<td
align="center"
style="
font-size: 80%;
text-align: center;
padding-top: 1rem;
padding-bottom: 2rem;
"
>
Sent by STEMMechanics &middot;
<a href="https://www.stemmechanics.com.au/"
>Visit our Website</a
>
&middot;
<a href="https://twitter.com/stemmechanics"
>@stemmechanics</a
><br />PO Box 36, Edmonton, QLD 4869, Australia
</td>
</tr>
</table>
</body>
</html>

View File

@@ -1,24 +0,0 @@
@if (count($usernames) > 2)
Yo {{ $usernames[0] }}, {{ $usernames[1] }}, or is it {{ $usernames[count($usernames)-1] }}?
@elseif (count($usernames) > 1)
Yo {{ $usernames[0] }} or is it {{ $usernames[1] }}?
@else
Yo {{ $usernames[0] }},
@endif
@if (count($usernames) == 1)
Guess what, your username is {{ $usernames[0] }}.
@else
We have the following usernames registered to this email address:
@foreach($usernames as $username)
- {{ $username }}
@endforeach
@endif
Need help or got feedback? Contact us at https://www.stemmechanics.com.au/contact or touch base on twitter at @stemmechanics
--
Sent by STEMMechanics
https://www.stemmechanics.com.au/
PO Box 36, Edmonton, QLD 4869, Australia

View File

@@ -1,116 +0,0 @@
<!DOCTYPE html>
<html
lang="{{ str_replace('_', '-', app()->getLocale()) }}"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office"
>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>STEMMechanics - Forgot Password</title>
<link
rel="noopener"
target="_blank"
href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800&display=swap"
rel="stylesheet"
/>
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<style>
@import url("https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800&display=swap");
</style>
</head>
<body>
<table
cellspacing="0"
cellpadding="0"
border="0"
role="presentation"
style="
width: 100%;
padding: 2rem;
font-size: 1.1rem;
color: #000000;
font-family: Nunito, Arial, Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
"
>
<tr>
<td>
<a href="https://www.stemmechanics.com.au/">
<img
alt="STEMMechanics Logo"
src="{{ $message->embed(public_path('assets').'/logo.webp') }}"
width="400"
height="62"
/>
</a>
</td>
</tr>
<tr>
<p></p>
<td><h2>Howdy there,</h2></td>
</tr>
<tr>
<td style="padding-bottom: 2rem">
At your request, you are now subscribed to our newsletter giving you tips, tricks and letting you know when new workshops are scheduled.
</td>
</tr>
<tr>
<td style="padding-bottom: 2rem">
If this wasn't you, you can unsubscribe by visiting <a href="https://www.stemmechanics.com.au/unsubscribe?email={{ $email }}">stemmechanics.com.au/unsubscribe</a>
</td>
</tr>
<tr>
<td
align="center"
style="
font-size: 90%;
text-align: center;
padding-top: 2rem;
padding-bottom: 2rem;
border-top: 1px solid #ddd;
"
>
Need help or got feedback?
<a href="https://www.stemmechanics.com.au/contact"
>Contact us</a
>
or touch base at
<a href="https://twitter.com/stemmechanics"
>@stemmechanics</a
>.
</td>
</tr>
<tr>
<td
align="center"
style="
font-size: 80%;
text-align: center;
padding-top: 1rem;
padding-bottom: 2rem;
"
>
Sent by STEMMechanics &middot;
<a href="https://www.stemmechanics.com.au/"
>Visit our Website</a
>
&middot;
<a href="https://twitter.com/stemmechanics"
>@stemmechanics</a
><br />PO Box 36, Edmonton, QLD 4869, Australia
</td>
</tr>
</table>
</body>
</html>

View File

@@ -1,14 +0,0 @@
Howdy there,
At your request, you are now subscribed to our newsletter giving you tips, tricks and letting you know when new workshops are scheduled.
If this wasn't you, you can unsubscribe by visiting the following URL in your browser:
https://www.stemmechanics.com.au/unsubscribe
Need help or got feedback? Contact us at https://www.stemmechanics.com.au/contact or touch base on twitter at @stemmechanics
--
Sent by STEMMechanics
https://www.stemmechanics.com.au/
PO Box 36, Edmonton, QLD 4869, Australia

View File

@@ -1,111 +0,0 @@
<!DOCTYPE html>
<html
lang="{{ str_replace('_', '-', app()->getLocale()) }}"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office"
>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>STEMMechanics - Forgot Password</title>
<link
rel="noopener"
target="_blank"
href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800&display=swap"
rel="stylesheet"
/>
<!--[if gte mso 9]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<style>
@import url("https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800&display=swap");
</style>
</head>
<body>
<table
cellspacing="0"
cellpadding="0"
border="0"
role="presentation"
style="
width: 100%;
padding: 2rem;
font-size: 1.1rem;
color: #000000;
font-family: Nunito, Arial, Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
"
>
<tr>
<td>
<a href="https://www.stemmechanics.com.au/">
<img
alt="STEMMechanics Logo"
src="{{ $message->embed(public_path('assets').'/logo.webp') }}"
width="400"
height="62"
/>
</a>
</td>
</tr>
<tr>
<p></p>
<td><h2>Howdy there,</h2></td>
</tr>
<tr>
<td style="padding-bottom: 2rem">
At your request, you are now unsubscribed from our newsletter.
</td>
</tr>
<tr>
<td
align="center"
style="
font-size: 90%;
text-align: center;
padding-top: 2rem;
padding-bottom: 2rem;
border-top: 1px solid #ddd;
"
>
Need help or got feedback?
<a href="https://www.stemmechanics.com.au/contact"
>Contact us</a
>
or touch base at
<a href="https://twitter.com/stemmechanics"
>@stemmechanics</a
>.
</td>
</tr>
<tr>
<td
align="center"
style="
font-size: 80%;
text-align: center;
padding-top: 1rem;
padding-bottom: 2rem;
"
>
Sent by STEMMechanics &middot;
<a href="https://www.stemmechanics.com.au/"
>Visit our Website</a
>
&middot;
<a href="https://twitter.com/stemmechanics"
>@stemmechanics</a
><br />PO Box 36, Edmonton, QLD 4869, Australia
</td>
</tr>
</table>
</body>
</html>

View File

@@ -1,10 +0,0 @@
Howdy there,
At your request, you have been unsubscribed from our newsletter.
Need help or got feedback? Contact us at https://www.stemmechanics.com.au/contact or touch base on twitter at @stemmechanics
--
Sent by STEMMechanics
https://www.stemmechanics.com.au/
PO Box 36, Edmonton, QLD 4869, Australia

View File

@@ -30,7 +30,6 @@ Route::get('/analytics', [AnalyticsController::class, 'index']);
Route::post('/analytics', [AnalyticsController::class, 'store']);
Route::apiResource('users', UserController::class);
Route::post('/users/forgotUsername', [UserController::class, 'forgotUsername']);
Route::post('/users/forgotPassword', [UserController::class, 'forgotPassword']);
Route::post('/users/resetPassword', [UserController::class, 'resetPassword']);
Route::post('/users/resendVerifyEmailCode', [UserController::class, 'resendVerifyEmailCode']);
@@ -45,9 +44,6 @@ Route::apiAttachmentResource('articles', ArticleController::class);
Route::apiResource('events', EventController::class);
Route::apiAttachmentResource('events', EventController::class);
Route::apiResource('subscriptions', SubscriptionController::class);
Route::delete('subscriptions', [SubscriptionController::class, 'destroyByEmail']);
Route::post('/contact', [ContactController::class, 'send']);
Route::get('/logs/{name}', [LogController::class, 'show']);

View File

@@ -1,4 +1,5 @@
<?php
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Models\User;
@@ -7,6 +8,7 @@ class AuthApiTest extends TestCase
{
use RefreshDatabase;
public function testLogin()
{
$user = User::factory()->create([
@@ -15,7 +17,7 @@ class AuthApiTest extends TestCase
// Test successful login
$response = $this->postJson('/api/login', [
'username' => $user->username,
'email' => $user->email,
'password' => 'password',
]);
$response->assertStatus(200);
@@ -32,7 +34,7 @@ class AuthApiTest extends TestCase
$response->assertJson([
'user' => [
'id' => $user->id,
'username' => $user->username,
'email' => $user->email,
]
]);
@@ -44,7 +46,7 @@ class AuthApiTest extends TestCase
// Test failed login
$response = $this->postJson('/api/login', [
'username' => $user->username,
'email' => $user->email,
'password' => 'wrongpassword',
]);
$response->assertStatus(422);

View File

@@ -1,4 +1,5 @@
<?php
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
@@ -8,6 +9,7 @@ class UsersApiTest extends TestCase
{
use RefreshDatabase;
public function testNonAdminUsersCanOnlyViewBasicUserInfo()
{
// create a non-admin user
@@ -25,7 +27,7 @@ class UsersApiTest extends TestCase
'users' => [
'*' => [
'id',
'username'
'display_name'
]
],
'total'
@@ -41,7 +43,7 @@ class UsersApiTest extends TestCase
]);
$response->assertJsonFragment([
'id' => $nonAdminUser->id,
'username' => $nonAdminUser->username
'email' => $nonAdminUser->email
]);
// ensure the admin user can access the endpoint and see additional user info
@@ -51,7 +53,6 @@ class UsersApiTest extends TestCase
'users' => [
'*' => [
'id',
'username',
'email'
]
],
@@ -66,14 +67,13 @@ class UsersApiTest extends TestCase
]);
$response->assertJsonFragment([
'id' => $nonAdminUser->id,
'username' => $nonAdminUser->username
'email' => $nonAdminUser->email
]);
}
public function testGuestCannotCreateUser()
{
$userData = [
'username' => 'johndoe',
'email' => 'johndoe@example.com',
'password' => 'password',
];
@@ -81,7 +81,6 @@ class UsersApiTest extends TestCase
$response = $this->postJson('/api/users', $userData);
$response->assertStatus(401);
$this->assertDatabaseMissing('users', [
'username' => $userData['username'],
'email' => $userData['email'],
]);
}
@@ -91,7 +90,6 @@ class UsersApiTest extends TestCase
$userData = [
'first_name' => 'John',
'last_name' => 'Doe',
'username' => 'johndoe',
'display_name' => 'jackdoe',
'email' => 'johndoe@example.com',
'password' => 'password',
@@ -100,18 +98,16 @@ class UsersApiTest extends TestCase
$response = $this->postJson('/api/register', $userData);
$response->assertStatus(200);
$this->assertDatabaseHas('users', [
'username' => $userData['username'],
'email' => $userData['email'],
]);
}
public function testCannotCreateDuplicateUsername()
public function testCannotCreateDuplicateEmailOrDisplayName()
{
$userData = [
'display_name' => 'JackDoe',
'first_name' => 'Jack',
'last_name' => 'Doe',
'username' => 'jackdoe',
'display_name' => 'jackdoe',
'email' => 'jackdoe@example.com',
'password' => 'password',
];
@@ -120,14 +116,13 @@ class UsersApiTest extends TestCase
$response = $this->postJson('/api/register', $userData);
$response->assertStatus(200);
$this->assertDatabaseHas('users', [
'username' => 'jackdoe',
'email' => 'jackdoe@example.com',
]);
// Test creating duplicate user
$response = $this->postJson('/api/register', $userData);
$response->assertStatus(422);
$response->assertJsonValidationErrors('username');
$response->assertJsonValidationErrors(['display_name', 'email']);
}
public function testUserCanOnlyUpdateOwnUser()
@@ -135,7 +130,6 @@ class UsersApiTest extends TestCase
$user = User::factory()->create();
$userData = [
'username' => 'raffi',
'email' => 'raffi@example.com',
'password' => 'password',
];
@@ -145,14 +139,12 @@ class UsersApiTest extends TestCase
$response->assertStatus(200);
$this->assertDatabaseHas('users', [
'id' => $user->id,
'username' => 'raffi',
'email' => 'raffi@example.com',
]);
// Test updating another user
$otherUser = User::factory()->create();
$otherUserData = [
'username' => 'otherraffi',
'email' => 'otherraffi@example.com',
'password' => 'password',
];
@@ -185,7 +177,6 @@ class UsersApiTest extends TestCase
$user = User::factory()->create();
$userData = [
'username' => 'Todd Doe',
'email' => 'todddoe@example.com',
'password' => 'password',
];
@@ -195,14 +186,12 @@ class UsersApiTest extends TestCase
$response->assertStatus(200);
$this->assertDatabaseHas('users', [
'id' => $user->id,
'username' => 'Todd Doe',
'email' => 'todddoe@example.com'
]);
// Test updating another user
$otherUser = User::factory()->create();
$otherUserData = [
'username' => 'Kim Doe',
'email' => 'kimdoe@example.com',
'password' => 'password',
];
@@ -211,7 +200,6 @@ class UsersApiTest extends TestCase
$response->assertStatus(200);
$this->assertDatabaseHas('users', [
'id' => $otherUser->id,
'username' => 'Kim Doe',
'email' => 'kimdoe@example.com',
]);
}