🚀 مُصادَق: تحديث برمجي جديد 2026-05-03 15:51

This commit is contained in:
Hamza-Ayed
2026-05-03 15:51:53 +03:00
parent e182faad1d
commit 81a3e5188e
12 changed files with 415 additions and 6060 deletions

View File

@@ -36,20 +36,15 @@ final class AuthController
return;
}
// Set refresh token in HttpOnly cookie
setcookie('refresh_token', $result['refresh_token'], [
'expires' => time() + (60 * 60 * 24 * 7),
'path' => '/api/v1/auth/refresh',
'httponly' => true,
'samesite' => 'Strict',
'secure' => true
]);
$this->setAuthCookies($result);
unset($result['refresh_token']);
// Backward compatibility for existing non-browser clients
$responseData = $result;
unset($responseData['refresh_token']);
Response::json([
'success' => true,
'data' => $result,
'data' => $responseData,
'message' => 'تم تسجيل الدخول بنجاح'
]);
} catch (Throwable $e) {
@@ -88,14 +83,10 @@ final class AuthController
}
}
// Clear refresh token cookie
setcookie('refresh_token', '', [
'expires' => time() - 3600,
'path' => '/api/v1/auth/refresh',
'httponly' => true,
'samesite' => 'Strict',
'secure' => true
]);
// Clear auth cookies
setcookie('refresh_token', '', ['expires' => time() - 3600, 'path' => '/api/v1/auth/refresh', 'httponly' => true, 'samesite' => 'Strict', 'secure' => true]);
setcookie('access_token', '', ['expires' => time() - 3600, 'path' => '/', 'httponly' => true, 'samesite' => 'Strict', 'secure' => true]);
setcookie('csrf_token', '', ['expires' => time() - 3600, 'path' => '/', 'httponly' => false, 'samesite' => 'Strict', 'secure' => true]);
Response::json([
'success' => true,
@@ -115,20 +106,15 @@ final class AuthController
try {
$result = $this->authService->refresh($refreshToken);
// Set new refresh token in HttpOnly cookie
setcookie('refresh_token', $result['refresh_token'], [
'expires' => time() + (60 * 60 * 24 * 7),
'path' => '/api/v1/auth/refresh',
'httponly' => true,
'samesite' => 'Strict',
'secure' => true
]);
$this->setAuthCookies($result);
unset($result['refresh_token']);
// Backward compatibility
$responseData = $result;
unset($responseData['refresh_token']);
Response::json([
'success' => true,
'data' => $result,
'data' => $responseData,
'message' => 'تم تجديد الجلسة بنجاح'
]);
} catch (Throwable $e) {
@@ -140,20 +126,15 @@ final class AuthController
try {
$result = $this->authService->register($request->getBody());
// Set refresh token in HttpOnly cookie
setcookie('refresh_token', $result['refresh_token'], [
'expires' => time() + (60 * 60 * 24 * 7),
'path' => '/api/v1/auth/refresh',
'httponly' => true,
'samesite' => 'Strict',
'secure' => true
]);
$this->setAuthCookies($result);
unset($result['refresh_token']);
// Backward compatibility
$responseData = $result;
unset($responseData['refresh_token']);
Response::json([
'success' => true,
'data' => $result,
'data' => $responseData,
'message' => 'تم إنشاء الحساب وتسجيل الدخول بنجاح'
]);
} catch (Throwable $e) {
@@ -226,19 +207,24 @@ final class AuthController
return;
}
// Re-issue a full login session after successful 2FA.
$stmt = $db->prepare("SELECT email FROM users WHERE id = ?");
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$userId]);
$email = $stmt->fetchColumn();
if (!$email) {
$user = $stmt->fetch();
if (!$user) {
Response::error('المستخدم غير موجود', 'NOT_FOUND', 404);
return;
}
$result = $this->authService->createSession($user);
$this->setAuthCookies($result);
$responseData = $result;
unset($responseData['refresh_token']);
Response::json([
'success' => true,
'data' => ['user_id' => $userId, 'email' => $email],
'message' => 'تم التحقق بنجاح'
'data' => $responseData,
'message' => 'تم التحقق وتأسيس الجلسة بنجاح'
]);
}
@@ -265,4 +251,41 @@ final class AuthController
Response::json(['success' => true, 'message' => 'تم تعطيل التحقق الثنائي']);
}
private function setAuthCookies(array $result): void
{
$cookieDomain = $_SERVER['HTTP_HOST'] ?? '';
$secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on';
// 1. Refresh Token (HttpOnly, scoped to /refresh)
setcookie('refresh_token', $result['refresh_token'] ?? '', [
'expires' => time() + (60 * 60 * 24 * 7), // 7 days
'path' => '/api/v1/auth/refresh',
'domain' => $cookieDomain,
'httponly' => true,
'samesite' => 'Strict',
'secure' => $secure
]);
// 2. Access Token (HttpOnly, scoped to /)
setcookie('access_token', $result['access_token'] ?? '', [
'expires' => time() + (60 * 15), // 15 mins
'path' => '/',
'domain' => $cookieDomain,
'httponly' => true,
'samesite' => 'Strict',
'secure' => $secure
]);
// 3. CSRF Token (Readable by JS)
$csrfToken = bin2hex(random_bytes(32));
setcookie('csrf_token', $csrfToken, [
'expires' => time() + (60 * 15),
'path' => '/',
'domain' => $cookieDomain,
'httponly' => false,
'samesite' => 'Strict',
'secure' => $secure
]);
}
}

View File

@@ -24,12 +24,36 @@ final class AuthService
{
$user = $this->userModel->findByEmail($email);
if ($user) {
if ($user['locked_until'] && strtotime($user['locked_until']) > time()) {
throw new \App\Core\Exceptions\HttpException("الحساب مقفل مؤقتاً لعدة محاولات فاشلة، حاول مجدداً لاحقاً", "ACCOUNT_LOCKED", 429);
}
}
if (!$user || !password_verify($password, $user['password_hash'])) {
throw new Exception("البريد الإلكتروني أو كلمة المرور غير صحيحة");
if ($user) {
$failedCount = (int)($user['failed_login_count'] ?? 0) + 1;
$lockedUntil = null;
if ($failedCount >= 5) {
$lockedUntil = date('Y-m-d H:i:s', strtotime('+15 minutes'));
error_log("[SECURITY] Account locked due to brute force: {$email}");
}
$this->userModel->update($user['id'], [
'failed_login_count' => $failedCount,
'locked_until' => $lockedUntil
]);
}
error_log("[SECURITY] Failed login attempt for email: {$email}");
throw new \App\Core\Exceptions\HttpException("البريد الإلكتروني أو كلمة المرور غير صحيحة", "INVALID_CREDENTIALS", 401);
}
if (!$user['is_active']) {
throw new Exception("هذا الحساب معطل حالياً");
throw new \App\Core\Exceptions\HttpException("هذا الحساب معطل حالياً", "ACCOUNT_DISABLED", 403);
}
// Reset failed login count on successful login
if ($user['failed_login_count'] > 0) {
$this->userModel->update($user['id'], ['failed_login_count' => 0, 'locked_until' => null]);
}
$accessToken = $this->jwtService->issueAccessToken([
@@ -51,13 +75,7 @@ final class AuthService
return [
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
'user' => [
'id' => $user['id'],
'name' => $user['name'],
'email' => $user['email'],
'role' => $user['role'],
'assigned_company_id' => $user['assigned_company_id']
]
'user' => $user
];
}
@@ -79,6 +97,11 @@ final class AuthService
throw new Exception("جلسة العمل منتهية، يرجى تسجيل الدخول مرة أخرى");
}
return $this->createSession($user);
}
public function createSession(array $user): array
{
$accessToken = $this->jwtService->issueAccessToken([
'user_id' => $user['id'],
'tenant_id' => $user['tenant_id'],
@@ -100,7 +123,8 @@ final class AuthService
'name' => $user['name'],
'email' => $user['email'],
'role' => $user['role'],
'assigned_company_id' => $user['assigned_company_id']
'assigned_company_id' => $user['assigned_company_id'],
'totp_enabled' => (bool)($user['totp_enabled'] ?? false)
]
];
}
@@ -112,37 +136,46 @@ final class AuthService
throw new Exception("هذا البريد الإلكتروني مسجل مسبقاً");
}
$tenantId = Uuid::uuid4()->toString();
$userId = Uuid::uuid4()->toString();
$db = \App\Core\Database::getInstance();
try {
$db->beginTransaction();
// 2. Create Tenant
$this->tenantModel->create([
'id' => $tenantId,
'name' => $data['tenant_name'],
'email' => $data['email'],
'status' => 'trial',
'trial_ends_at' => date('Y-m-d H:i:s', strtotime('+14 days'))
]);
$tenantId = Uuid::uuid4()->toString();
$userId = Uuid::uuid4()->toString();
// 3. Create Subscription
$this->subscriptionModel->create([
'tenant_id' => $tenantId,
'plan' => 'basic',
'status' => 'trial'
]);
// 2. Create Tenant
$this->tenantModel->create([
'id' => $tenantId,
'name' => $data['name'] ?? 'مساحة عمل جديدة',
'email' => $data['email'],
'status' => 'trial',
'trial_ends_at' => date('Y-m-d H:i:s', strtotime('+14 days'))
]);
// 4. Create User
$this->userModel->create([
'id' => $userId,
'tenant_id' => $tenantId,
'name' => $data['user_name'],
'email' => $data['email'],
'password_hash' => password_hash($data['password'], PASSWORD_ARGON2ID),
'role' => 'admin',
'is_active' => 1
]);
// 3. Create Subscription
$this->subscriptionModel->create([
'tenant_id' => $tenantId,
'plan' => 'basic',
'status' => 'trial'
]);
return $this->login($data['email'], $data['password']);
// 4. Create User
$this->userModel->create([
'id' => $userId,
'tenant_id' => $tenantId,
'name' => $data['name'] ?? 'مسؤول النظام',
'email' => $data['email'],
'password_hash' => password_hash($data['password'], PASSWORD_ARGON2ID),
'role' => 'admin',
'is_active' => 1
]);
$db->commit();
return $this->login($data['email'], $data['password']);
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
}
public function logout(string $jti, int $remaining): void
{