104 lines
3.8 KiB
PHP
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,
|
|
]);
|
|
}
|
|
}
|