'datetime', 'last_login_at' => 'datetime', 'email_authentication_at' => 'datetime', 'password' => 'hashed', 'app_authentication_secret' => 'encrypted', 'app_authentication_recovery_codes' => 'encrypted:array', ]; } public function canAccessPanel(Panel $panel): bool { return $panel->getId() === 'tenant' && $this->status === 'active'; } public function isAdmin(): bool { return $this->role === 'admin' || $this->role === 'owner' || $this->hasAnyRole(['admin', 'owner']); } public function isOwner(): bool { return $this->role === 'owner' || $this->hasRole('owner'); } public function isAccountant(): bool { return $this->role === 'accountant' || $this->hasRole('accountant'); } public function isMechanic(): bool { return in_array($this->role, ['mechanic', 'master'], true) || $this->hasAnyRole(['mechanic']); } public function permissionOverrides(): HasMany { return $this->hasMany(UserPermissionOverride::class); } /** * Permission check honoring (in order): * 1. Active deny-override → false * 2. Active grant-override → true * 3. Admin/owner bypass → true * 4. Standard role-based check */ public function canDo(string $permission): bool { $override = $this->activeOverrideFor($permission); if ($override) { if ($override->mode === 'deny') { $this->logDeniedIfSensitive($permission); return false; } if ($override->mode === 'grant') return true; } // Owner + admin bypass for permissions without explicit deny. if ($this->isAdmin()) return true; try { $allowed = $this->can($permission); if (! $allowed) $this->logDeniedIfSensitive($permission); return $allowed; } catch (\Throwable $e) { return false; } } private function activeOverrideFor(string $permissionSlug): ?UserPermissionOverride { return $this->permissionOverrides() ->whereHas('permission', fn ($q) => $q->where('name', $permissionSlug)) ->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now())) ->first(); } /** Sensitive permissions whose deny we should record for audit. */ private const AUDITED_DENIALS = [ 'admin.users.manage', 'admin.roles.manage', 'admin.settings.edit', 'admin.backup.download', 'finance.delete_payment', 'finance.view_pl', 'salaries.mark_paid', 'salaries.view_all', 'work_orders.delete', 'work_orders.approve_discount_any', ]; private function logDeniedIfSensitive(string $permission): void { if (! in_array($permission, self::AUDITED_DENIALS, true)) return; try { activity('permissions') ->causedBy($this) ->withProperties(['permission' => $permission]) ->event('permission_denied') ->log("permission denied: $permission for user #{$this->id}"); } catch (\Throwable $e) { // activity-log may be misconfigured in some contexts — never let auth fail because of it. } } /** Has 2FA app authentication enabled (Filament native). */ public function hasTwoFactorEnabled(): bool { return $this->app_authentication_secret !== null; } public function hasEmailAuthentication(): bool { return $this->email_authentication_at !== null; } public function toggleEmailAuthentication(bool $condition): void { $this->forceFill([ 'email_authentication_at' => $condition ? now() : null, ])->saveQuietly(); } public function getAppAuthenticationSecret(): ?string { return $this->app_authentication_secret; } public function saveAppAuthenticationSecret(?string $secret): void { $this->forceFill(['app_authentication_secret' => $secret])->saveQuietly(); } public function getAppAuthenticationHolderName(): string { return $this->email; } public function getAppAuthenticationRecoveryCodes(): ?array { return $this->app_authentication_recovery_codes; } public function saveAppAuthenticationRecoveryCodes(?array $codes): void { $this->forceFill(['app_authentication_recovery_codes' => $codes])->saveQuietly(); } }