name = auth()->user()->name; $this->email = auth()->user()->email; } public function saveAccount(): void { $user = auth()->user(); $data = $this->validate([ 'name' => 'required|string|max:255', 'email' => 'required|email|max:255|unique:users,email,'.$user->id, ]); // An operator email must never collide with a customer's — that would // block the customer from ever obtaining a portal login (ensureUser). if (Customer::query()->where('email', $data['email'])->exists()) { $this->addError('email', __('admin_settings.is_customer')); return; } $user->update($data); $this->dispatch('notify', message: __('admin_settings.account_saved')); } public function inviteStaff(): void { $this->authorize('staff.manage'); $data = $this->validate([ 'staffName' => 'required|string|max:255', 'staffEmail' => 'required|email|max:255|unique:users,email', 'staffRole' => 'required|in:Owner,Admin,Support,Billing,Read-only', ]); // Only an Owner may create another Owner. if ($data['staffRole'] === 'Owner' && ! auth()->user()->hasRole('Owner')) { $this->addError('staffRole', __('admin_settings.owner_only')); return; } // Never turn a customer's portal login into an operator. if (Customer::query()->where('email', $data['staffEmail'])->exists()) { $this->addError('staffEmail', __('admin_settings.is_customer')); return; } $user = User::create([ 'name' => $data['staffName'], 'email' => $data['staffEmail'], 'password' => Hash::make(Str::random(40)), // invite/reset delivery is mocked 'is_admin' => true, ]); $user->assignRole($data['staffRole']); $this->reset('staffName', 'staffEmail'); $this->staffRole = 'Support'; $this->dispatch('notify', message: __('admin_settings.staff_invited')); } public function setStaffRole(int $id, string $role): void { $this->authorize('staff.manage'); if (! in_array($role, User::OPERATOR_ROLES, true)) { return; } $result = DB::transaction(function () use ($id, $role) { // Serialize on the Owner role so the global owner count can't be // raced to zero by concurrent demotions of different owners. Role::query()->where('name', 'Owner')->lockForUpdate()->first(); $target = User::query()->whereKey($id)->lockForUpdate()->first(); if ($target === null) { return 'gone'; } if ($target->id === auth()->id()) { return 'self'; } // Only existing operators may be re-roled — never escalate a customer // (or any non-staff) user into the console via a tampered id. if (! $target->isOperator() || Customer::query()->where('email', $target->email)->exists()) { return 'not_staff'; } // Granting or revoking Owner is Owner-only. if (($role === 'Owner' || $target->hasRole('Owner')) && ! auth()->user()->hasRole('Owner')) { return 'owner_only'; } // Never demote the last Owner. if ($target->hasRole('Owner') && $role !== 'Owner' && $this->ownerCount() <= 1) { return 'last_owner'; } $target->syncRoles([$role]); return 'ok'; }); $this->flash($result); } public function revokeStaff(int $id): void { $this->authorize('staff.manage'); $result = DB::transaction(function () use ($id) { Role::query()->where('name', 'Owner')->lockForUpdate()->first(); $target = User::query()->whereKey($id)->lockForUpdate()->first(); if ($target === null || ! $target->isOperator()) { return 'gone'; } if ($target->id === auth()->id()) { return 'self'; } if ($target->hasRole('Owner') && $this->ownerCount() <= 1) { return 'last_owner'; } $target->syncRoles([]); $target->update(['is_admin' => false]); return 'revoked'; }); $this->flash($result); } private function flash(string $result): void { $msg = match ($result) { 'ok' => __('admin_settings.role_updated'), 'revoked' => __('admin_settings.staff_revoked'), 'self' => __('admin_settings.not_self'), 'owner_only' => __('admin_settings.owner_only'), 'last_owner' => __('admin_settings.last_owner'), 'not_staff' => __('admin_settings.not_staff'), default => null, }; if ($msg !== null) { $this->dispatch('notify', message: $msg); } } private function ownerCount(): int { return User::query()->whereHas('roles', fn ($q) => $q->where('name', 'Owner'))->count(); } public function render() { $staff = User::query() ->whereHas('roles', fn ($q) => $q->whereIn('name', User::OPERATOR_ROLES)) ->with('roles') ->orderBy('name') ->get() ->map(fn (User $u) => [ 'id' => $u->id, 'name' => $u->name, 'email' => $u->email, 'role' => $u->roles->first()?->name ?? '—', 'self' => $u->id === auth()->id(), ]); return view('livewire.admin.settings', [ 'staff' => $staff, 'roles' => User::OPERATOR_ROLES, 'canManageStaff' => auth()->user()->can('staff.manage'), 'isOwner' => auth()->user()->hasRole('Owner'), ]); } }