*/ 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. * Atomic: the read/check/remove runs under a row lock so two concurrent requests * can never both spend the same code (replay). */ public function useRecoveryCode(string $code): bool { $code = trim($code); return DB::transaction(function () use ($code) { $locked = static::whereKey($this->getKey())->lockForUpdate()->first(); $codes = $locked?->recoveryCodes() ?? []; if (! in_array($code, $codes, true)) { return false; } $remaining = array_values(array_filter($codes, fn ($c) => $c !== $code)); $locked->forceFill(['two_factor_recovery_codes' => $remaining])->save(); // Keep the in-memory instance consistent for the rest of the request. $this->setRawAttributes($locked->getAttributes(), true); 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(); } }