From d13e91931070d1fe9f0477fb8c820f9865339fec Mon Sep 17 00:00:00 2001 From: Koda Date: Fri, 22 May 2026 12:55:44 +0000 Subject: [PATCH] Add admin-managed user creation --- .../Admin/UserManagementController.php | 21 +++ .../Settings/SecurityController.php | 3 + app/Http/Middleware/RequirePasswordChange.php | 38 +++++ .../Admin/StoreManagedUserRequest.php | 36 ++++ app/Http/Resources/UserResource.php | 1 + app/Models/User.php | 3 +- bootstrap/app.php | 2 + database/factories/UserFactory.php | 1 + ..._password_on_next_login_to_users_table.php | 30 ++++ resources/js/pages/admin/users/Create.vue | 155 ++++++++++++++++++ resources/js/pages/admin/users/Index.vue | 31 +++- resources/js/pages/settings/Security.vue | 15 ++ resources/js/types/models/user-model.ts | 1 + routes/web/admin.php | 2 + .../Admin/UserManagementControllerTest.php | 62 +++++++ tests/Feature/Auth/AuthenticationTest.php | 18 ++ tests/Feature/Settings/SecurityTest.php | 31 ++++ 17 files changed, 442 insertions(+), 8 deletions(-) create mode 100644 app/Http/Middleware/RequirePasswordChange.php create mode 100644 app/Http/Requests/Admin/StoreManagedUserRequest.php create mode 100644 database/migrations/2026_05_22_124700_add_must_change_password_on_next_login_to_users_table.php create mode 100644 resources/js/pages/admin/users/Create.vue diff --git a/app/Http/Controllers/Admin/UserManagementController.php b/app/Http/Controllers/Admin/UserManagementController.php index 87204ef..6e17615 100644 --- a/app/Http/Controllers/Admin/UserManagementController.php +++ b/app/Http/Controllers/Admin/UserManagementController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Admin; use App\Actions\Permissions\PermissionService; use App\Http\Controllers\Controller; +use App\Http\Requests\Admin\StoreManagedUserRequest; use App\Http\Requests\Admin\SyncUserGlobalPermissionsRequest; use App\Http\Resources\PermissionOptionResource; use App\Http\Resources\UserResource; @@ -35,6 +36,26 @@ class UserManagementController extends Controller ]); } + public function create(): Response + { + return Inertia::render('admin/users/Create'); + } + + public function store(StoreManagedUserRequest $request): RedirectResponse + { + $validated = $request->validated(); + + User::query()->create([ + 'name' => Arr::get($validated, 'name'), + 'email' => Arr::get($validated, 'email'), + 'password' => Arr::get($validated, 'password'), + 'must_change_password_on_next_login' => (bool) Arr::get($validated, 'must_change_password_on_next_login', true), + ]); + + return to_route('admin.users.index') + ->with('status', 'User was created.'); + } + public function update(SyncUserGlobalPermissionsRequest $request, User $user): RedirectResponse { $validated = $request->validated(); diff --git a/app/Http/Controllers/Settings/SecurityController.php b/app/Http/Controllers/Settings/SecurityController.php index 4e47782..053ac89 100644 --- a/app/Http/Controllers/Settings/SecurityController.php +++ b/app/Http/Controllers/Settings/SecurityController.php @@ -33,6 +33,8 @@ class SecurityController extends Controller implements HasMiddleware $user = $request->user(); $props = [ 'canManageTwoFactor' => Features::canManageTwoFactorAuthentication(), + 'mustChangePassword' => (bool) $user->must_change_password_on_next_login, + 'status' => session('status'), ]; if (Features::canManageTwoFactorAuthentication()) { @@ -53,6 +55,7 @@ class SecurityController extends Controller implements HasMiddleware $user = $request->user(); $user->update([ + 'must_change_password_on_next_login' => false, 'password' => $request->password, ]); diff --git a/app/Http/Middleware/RequirePasswordChange.php b/app/Http/Middleware/RequirePasswordChange.php new file mode 100644 index 0000000..2bf7963 --- /dev/null +++ b/app/Http/Middleware/RequirePasswordChange.php @@ -0,0 +1,38 @@ +user(); + + if ($user === null || ! $user->must_change_password_on_next_login) { + return $next($request); + } + + if ($request->routeIs([ + 'logout', + 'password.confirm', + 'password.confirm.*', + 'security.edit', + 'user-password.update', + ])) { + return $next($request); + } + + return redirect() + ->route('security.edit') + ->with('status', 'You must change your password before continuing.'); + } +} diff --git a/app/Http/Requests/Admin/StoreManagedUserRequest.php b/app/Http/Requests/Admin/StoreManagedUserRequest.php new file mode 100644 index 0000000..6304b5b --- /dev/null +++ b/app/Http/Requests/Admin/StoreManagedUserRequest.php @@ -0,0 +1,36 @@ +user()?->can('access-admin') ?? false; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return [ + ...$this->profileRules(), + 'must_change_password_on_next_login' => ['required', 'boolean'], + 'password' => $this->passwordRules(), + ]; + } +} diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index 317fc44..353d589 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -19,6 +19,7 @@ class UserResource extends JsonResource 'name' => $this->name, 'email' => $this->email, 'email_verified_at' => $this->email_verified_at?->toISOString(), + 'must_change_password_on_next_login' => (bool) $this->must_change_password_on_next_login, 'created_at' => $this->created_at?->toISOString(), 'updated_at' => $this->updated_at?->toISOString(), ]; diff --git a/app/Models/User.php b/app/Models/User.php index b3dc0ba..9ff2172 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -17,7 +17,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Fortify\TwoFactorAuthenticatable; -#[Fillable(['name', 'email', 'password'])] +#[Fillable(['name', 'email', 'password', 'must_change_password_on_next_login'])] #[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])] class User extends Authenticatable { @@ -40,6 +40,7 @@ class User extends Authenticatable { return [ 'email_verified_at' => 'datetime', + 'must_change_password_on_next_login' => 'bool', 'password' => 'hashed', 'two_factor_confirmed_at' => 'datetime', ]; diff --git a/bootstrap/app.php b/bootstrap/app.php index 001ee1c..0e344cc 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -3,6 +3,7 @@ use App\Http\Middleware\AuthenticateApiApplicationKey; use App\Http\Middleware\HandleAppearance; use App\Http\Middleware\HandleInertiaRequests; +use App\Http\Middleware\RequirePasswordChange; use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; @@ -21,6 +22,7 @@ return Application::configure(basePath: dirname(__DIR__)) $middleware->web(append: [ HandleAppearance::class, HandleInertiaRequests::class, + RequirePasswordChange::class, AddLinkHeadersForPreloadedAssets::class, ]); diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index de5d2ba..1c93e9b 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -29,6 +29,7 @@ class UserFactory extends Factory 'name' => fake()->name(), 'email' => fake()->unique()->safeEmail(), 'email_verified_at' => now(), + 'must_change_password_on_next_login' => false, 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), 'two_factor_secret' => null, diff --git a/database/migrations/2026_05_22_124700_add_must_change_password_on_next_login_to_users_table.php b/database/migrations/2026_05_22_124700_add_must_change_password_on_next_login_to_users_table.php new file mode 100644 index 0000000..fd68301 --- /dev/null +++ b/database/migrations/2026_05_22_124700_add_must_change_password_on_next_login_to_users_table.php @@ -0,0 +1,30 @@ +boolean('must_change_password_on_next_login') + ->default(false) + ->after('password'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table): void { + $table->dropColumn('must_change_password_on_next_login'); + }); + } +}; diff --git a/resources/js/pages/admin/users/Create.vue b/resources/js/pages/admin/users/Create.vue new file mode 100644 index 0000000..59a28bb --- /dev/null +++ b/resources/js/pages/admin/users/Create.vue @@ -0,0 +1,155 @@ + + + diff --git a/resources/js/pages/admin/users/Index.vue b/resources/js/pages/admin/users/Index.vue index aaf2775..cbf1efc 100644 --- a/resources/js/pages/admin/users/Index.vue +++ b/resources/js/pages/admin/users/Index.vue @@ -1,6 +1,6 @@