firstWhere(['national_id' => $nationalId]); } /** * Verify credentials (National ID + Argon2id Password) * * @param string $nationalId * @param string $password * @return array ['success' => bool, 'user' => ?array, 'error' => ?string] */ public function verifyCredentials(string $nationalId, string $password): array { $user = $this->findByNationalId($nationalId); if (!$user) { return ['success' => false, 'user' => null, 'error' => 'ไม่พบผู้ใช้งานด้วยเลขบัตรประชาชนนี้']; } if ((int)$user['is_active'] !== 1) { return ['success' => false, 'user' => null, 'error' => 'บัญชีนี้ถูกระงับการใช้งาน กรุณาติดต่อผู้ดูแลระบบ']; } if (!empty($user['locked_until']) && strtotime($user['locked_until']) > time()) { $lockMin = ceil((strtotime($user['locked_until']) - time()) / 60); return ['success' => false, 'user' => null, 'error' => "บัญชีถูกระงับชั่วคราวเนื่องจากกรอกรหัสผ่านผิดเกินกำหนด กรุณารออีก {$lockMin} นาที"]; } // Verify with Argon2id if (!Argon2Hasher::verify($password, $user['password_hash'])) { // Record failed attempt $this->incrementFailedAttempts((int)$user['id'], (int)$user['failed_login_attempts']); return ['success' => false, 'user' => null, 'error' => 'รหัสผ่านไม่ถูกต้อง (Argon2id Check Failed)']; } // Reset failed attempts & update login IP $this->recordSuccessfulLogin((int)$user['id']); // Remove password_hash from returned array for security unset($user['password_hash']); return ['success' => true, 'user' => $user, 'error' => null]; } public function incrementFailedAttempts(int $userId, int $currentAttempts): void { $newAttempts = $currentAttempts + 1; $sql = "UPDATE `{$this->table}` SET `failed_login_attempts` = :att WHERE `id` = :id"; $stmt = self::getDB()->prepare($sql); $stmt->execute(['att' => $newAttempts, 'id' => $userId]); } public function recordSuccessfulLogin(int $userId): void { $ip = \App\Helpers\Security::getClientIp(); $sql = "UPDATE `{$this->table}` SET `failed_login_attempts` = 0, `locked_until` = NULL, `last_login_at` = NOW(), `last_login_ip` = :ip WHERE `id` = :id"; $stmt = self::getDB()->prepare($sql); $stmt->execute(['ip' => $ip, 'id' => $userId]); } /** * Enable 2FA Google Authenticator */ public function enableTwoFactor(int $userId, string $secret, array $recoveryCodes): bool { return $this->update($userId, [ 'two_factor_secret' => $secret, 'two_factor_enabled' => 1, 'two_factor_recovery_codes' => json_encode($recoveryCodes), ]); } /** * Disable / Reset 2FA by Admin */ public function resetTwoFactor(int $userId): bool { return $this->update($userId, [ 'two_factor_secret' => null, 'two_factor_enabled' => 0, 'two_factor_recovery_codes' => null, 'remember_token' => null, ]); } }