operator > viewer; new accounts * default to `operator` — least privilege). Restricted to admins (`manage-users`). The creator * may set a password directly, or leave it blank — then a strong initial password is generated, the * account is flagged `must_change_password`, and the clear-text password is revealed ONCE in the modal * (never persisted or logged). An operator-set password is the user's own, so no forced rotation. */ class CreateUser extends ModalComponent { public string $name = ''; public string $email = ''; /** New account role (admin > operator > viewer). Defaults to operator — least privilege. */ public string $role = 'operator'; /** Optional operator-set password; blank = generate an initial password and reveal it once. */ public string $password = ''; /** Clear-text temp password, held in component state only to reveal it once. */ public ?string $tempPassword = null; /** Name of the account just created, used in the reveal copy. */ public string $createdName = ''; public function mount(): void { abort_unless(auth()->user()?->can('manage-users'), 403); } public static function modalMaxWidth(): string { return 'lg'; } /** * @return array> */ protected function rules(): array { return [ 'name' => ['required', 'string', 'max:120'], 'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users', 'email')], // Optional — when set it must meet the same strength as a self-chosen password. 'password' => ['nullable', 'string', Password::min(6)], 'role' => ['required', Rule::enum(Role::class)], ]; } /** * @return array */ protected function validationAttributes(): array { return [ 'name' => __('accounts.name_label'), 'email' => __('accounts.email_label'), 'password' => __('accounts.password_label'), 'role' => __('accounts.role_label'), ]; } public function save(): void { abort_unless(auth()->user()?->can('manage-users'), 403); $data = $this->validate(); // Re-check uniqueness server-side regardless of the live validator state. $email = mb_strtolower(trim($data['email'])); if (User::where('email', $email)->exists()) { $this->addError('email', __('validation.unique', ['attribute' => __('accounts.email_label')])); return; } // Operator-set password = the user's own login (no forced rotation). Blank = generate a strong // initial password that the user is prompted to rotate, and reveal it once below. $operatorSet = $this->password !== ''; $password = $operatorSet ? $this->password : Str::password(16); $user = User::create([ 'name' => trim($data['name']), 'email' => $email, 'password' => Hash::make($password), 'must_change_password' => ! $operatorSet, 'role' => $data['role'], ]); AuditEvent::create([ 'user_id' => Auth::id(), 'actor' => Auth::user()?->name ?? 'system', 'action' => 'user.create', 'target' => $user->email, 'ip' => request()->ip(), ]); // Operator already knows the password they set → no reveal; just confirm and close. if ($operatorSet) { $this->dispatch('usersChanged'); $this->dispatch('notify', message: __('accounts.created_notify', ['name' => $user->name])); $this->closeModal(); return; } // Reveal the generated initial password ONCE — swaps the form for the reveal panel. $this->createdName = $user->name; $this->tempPassword = $password; } /** "Fertig": reload the list and close. */ public function finish(): void { $this->dispatch('usersChanged'); $this->closeModal(); } public function render() { return view('livewire.modals.create-user'); } }