158 lines
5.2 KiB
PHP
158 lines
5.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Auth;
|
|
|
|
use App\Modules\Users\UserModel;
|
|
use App\Modules\Tenants\TenantModel;
|
|
use App\Modules\Subscriptions\SubscriptionModel;
|
|
use App\Services\Security\JwtService;
|
|
use Ramsey\Uuid\Uuid;
|
|
use Exception;
|
|
|
|
final class AuthService
|
|
{
|
|
public function __construct(
|
|
private readonly UserModel $userModel,
|
|
private readonly JwtService $jwtService,
|
|
private readonly TenantModel $tenantModel,
|
|
private readonly SubscriptionModel $subscriptionModel
|
|
) {}
|
|
|
|
public function login(string $email, string $password): array
|
|
{
|
|
$user = $this->userModel->findByEmail($email);
|
|
|
|
if (!$user || !password_verify($password, $user['password_hash'])) {
|
|
throw new Exception("البريد الإلكتروني أو كلمة المرور غير صحيحة");
|
|
}
|
|
|
|
if (!$user['is_active']) {
|
|
throw new Exception("هذا الحساب معطل حالياً");
|
|
}
|
|
|
|
$accessToken = $this->jwtService->issueAccessToken([
|
|
'user_id' => $user['id'],
|
|
'tenant_id' => $user['tenant_id'],
|
|
'role' => $user['role'],
|
|
'assigned_company_id' => $user['assigned_company_id']
|
|
]);
|
|
|
|
$refreshToken = $this->jwtService->issueRefreshToken($user['id']);
|
|
|
|
// Update refresh token hash in DB
|
|
$this->userModel->update($user['id'], [
|
|
'refresh_token_hash' => password_hash($refreshToken, PASSWORD_BCRYPT),
|
|
'last_login_at' => date('Y-m-d H:i:s'),
|
|
'last_login_ip' => $_SERVER['REMOTE_ADDR'] ?? null
|
|
]);
|
|
|
|
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']
|
|
]
|
|
];
|
|
}
|
|
|
|
public function refresh(string $refreshToken): array
|
|
{
|
|
$parts = explode('.', $refreshToken);
|
|
if (count($parts) !== 2) {
|
|
throw new Exception("رمز التجديد غير صالحة");
|
|
}
|
|
|
|
[$userId, $random] = $parts;
|
|
$user = $this->userModel->find($userId);
|
|
|
|
if (!$user || !$user['is_active']) {
|
|
throw new Exception("المستخدم غير موجود أو معطل");
|
|
}
|
|
|
|
if (!$user['refresh_token_hash'] || !password_verify($refreshToken, $user['refresh_token_hash'])) {
|
|
throw new Exception("جلسة العمل منتهية، يرجى تسجيل الدخول مرة أخرى");
|
|
}
|
|
|
|
$accessToken = $this->jwtService->issueAccessToken([
|
|
'user_id' => $user['id'],
|
|
'tenant_id' => $user['tenant_id'],
|
|
'role' => $user['role'],
|
|
'assigned_company_id' => $user['assigned_company_id']
|
|
]);
|
|
|
|
$newRefreshToken = $this->jwtService->issueRefreshToken($user['id']);
|
|
|
|
$this->userModel->update($user['id'], [
|
|
'refresh_token_hash' => password_hash($newRefreshToken, PASSWORD_BCRYPT)
|
|
]);
|
|
|
|
return [
|
|
'access_token' => $accessToken,
|
|
'refresh_token' => $newRefreshToken,
|
|
'user' => [
|
|
'id' => $user['id'],
|
|
'name' => $user['name'],
|
|
'email' => $user['email'],
|
|
'role' => $user['role'],
|
|
'assigned_company_id' => $user['assigned_company_id']
|
|
]
|
|
];
|
|
}
|
|
|
|
public function register(array $data): array
|
|
{
|
|
// 1. Check if tenant already exists
|
|
if ($this->tenantModel->findByEmail($data['email'])) {
|
|
throw new Exception("هذا البريد الإلكتروني مسجل مسبقاً");
|
|
}
|
|
|
|
$tenantId = Uuid::uuid4()->toString();
|
|
$userId = Uuid::uuid4()->toString();
|
|
|
|
// 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'))
|
|
]);
|
|
|
|
// 3. Create Subscription
|
|
$this->subscriptionModel->create([
|
|
'tenant_id' => $tenantId,
|
|
'plan' => 'basic',
|
|
'status' => 'trial'
|
|
]);
|
|
|
|
// 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
|
|
]);
|
|
|
|
return $this->login($data['email'], $data['password']);
|
|
}
|
|
public function logout(string $jti, int $remaining): void
|
|
{
|
|
// Blacklist the JTI for its remaining lifetime
|
|
try {
|
|
$redis = \App\Core\Redis::getInstance();
|
|
$redis->setex('jwt_blacklist:' . $jti, max($remaining, 1), '1');
|
|
} catch (\Throwable $e) {
|
|
error_log('[AUTH] Could not blacklist JTI: ' . $e->getMessage());
|
|
}
|
|
}
|
|
}
|