*/ use HasFactory, Notifiable; protected function casts(): array { return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', 'two_factor_secret' => 'encrypted', 'two_factor_recovery_codes' => 'encrypted:array', 'two_factor_confirmed_at' => 'datetime', 'must_change_password' => 'boolean', ]; } /** @return array The 8 freshly generated one-time codes (also persisted). */ public function replaceRecoveryCodes(): array { $codes = array_map( fn () => Str::random(10).'-'.Str::random(10), range(1, 8), ); $this->forceFill(['two_factor_recovery_codes' => $codes])->save(); return $codes; } /** @return array */ public function recoveryCodes(): array { return $this->two_factor_recovery_codes ?? []; } public function hasRecoveryCodes(): bool { return count($this->recoveryCodes()) > 0; } /** Consume one recovery code (one-time). Returns true if it was valid + removed. */ public function useRecoveryCode(string $code): bool { $code = trim($code); $codes = $this->recoveryCodes(); if (! in_array($code, $codes, true)) { return false; } $this->forceFill([ 'two_factor_recovery_codes' => array_values(array_filter($codes, fn ($c) => $c !== $code)), ])->save(); return true; } public function hasTwoFactorEnabled(): bool { return ! is_null($this->two_factor_confirmed_at) && ! is_null($this->two_factor_secret); } /** * Completed the security onboarding (rotated the seeded password + enrolled 2FA) — * the same gate EnsureSecurityOnboarded enforces before the panel is reachable. * Authorization (e.g. broadcast channels) must require this, not mere authentication. */ public function securityOnboarded(): bool { return ! $this->must_change_password && $this->hasTwoFactorEnabled(); } }