Files
2026-09-16 23:20:08 +07:00

104 lines
3.8 KiB
PHP

<?php
namespace App\Models;
use App\Helpers\Argon2Hasher;
/**
* Class User
* User Model with Argon2id Password Guard & 2FA TOTP Support
*
* @package App\Models
*/
class User extends Model
{
protected string $table = 'users';
/**
* Find user by National ID (13 digits)
*/
public function findByNationalId(string $nationalId): ?array
{
return $this->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,
]);
}
}