'required|email', 'password' => 'required' ]); if ($errors) { json_error('Validation Failed', 422, $errors); } $email = $data['email']; $password = $data['password']; // 2. DB Check (Using hash for lookup since email is encrypted) $db = Database::getInstance(); $emailHash = hash('sha256', strtolower($email)); $stmt = $db->prepare("SELECT * FROM users WHERE email_hash = ? LIMIT 1"); $stmt->execute([$emailHash]); $user = $stmt->fetch(); if (!$user || !password_verify($password, $user['password_hash'])) { json_error('بيانات الدخول غير صحيحة', 401); } // 3. Handle device registration if provided (for mobile app login) $deviceId = $data['device_id'] ?? null; $deviceName = $data['device_name'] ?? 'Web Browser'; $deviceSecret = null; if ($deviceId) { $deviceSecret = hash('sha256', $user['id'] . $deviceId . bin2hex(random_bytes(16))); $stmt = $db->prepare(" INSERT INTO user_devices (id, user_id, device_fingerprint, device_name, platform, app_version, device_secret, is_trusted, last_seen_at) VALUES (UUID(), ?, ?, ?, ?, ?, ?, TRUE, NOW()) ON DUPLICATE KEY UPDATE device_name = VALUES(device_name), platform = VALUES(platform), app_version = VALUES(app_version), device_secret = VALUES(device_secret), is_trusted = TRUE, last_seen_at = NOW(), updated_at = NOW() "); $stmt->execute([ $user['id'], $deviceId, $deviceName, $data['platform'] ?? 'web', $data['app_version'] ?? '1.0.0', password_hash($deviceSecret, PASSWORD_DEFAULT), ]); } // 4. Issue Token $secret = env('JWT_SECRET'); if (!$secret || strlen($secret) < 32) { error_log('FATAL: JWT_SECRET is missing or too short in .env'); json_error('Server configuration error', 500); } // Longer expiry for mobile (30 days), short for web (15 mins) $expiry = $deviceId ? (30 * 24 * 3600) : (15 * 60); $payload = [ 'user_id' => $user['id'], 'tenant_id' => $user['tenant_id'], 'role' => $user['role'], 'device_id' => $deviceId, 'source' => $deviceId ? 'mobile' : 'web', 'exp' => time() + $expiry ]; $token = JWT::encode($payload, $secret); // 5. Update Refresh Token (Hashed before storage for security) $refreshToken = bin2hex(random_bytes(32)); $refreshTokenHash = hash('sha256', $refreshToken); $stmt = $db->prepare("UPDATE users SET refresh_token_hash = ?, last_login_at = NOW() WHERE id = ?"); $stmt->execute([$refreshTokenHash, $user['id']]); // 6. Secure Refresh Token delivery via HttpOnly Cookie (for web) if (!$deviceId) { setcookie('refresh_token', $refreshToken, [ 'expires' => time() + (7 * 24 * 60 * 60), // 7 days 'path' => '/api/v1/auth/refresh', 'secure' => true, 'httponly' => true, 'samesite' => 'Strict', ]); } json_success([ 'access_token' => $token, 'refresh_token' => $refreshToken, 'device_secret' => $deviceSecret, 'user' => [ 'id' => $user['id'], 'name' => (App\Core\Encryption::decrypt($user['name']) ?: $user['name']), 'email' => (App\Core\Encryption::decrypt($user['email']) ?: $user['email']), 'role' => $user['role'], 'tenant_id' => $user['tenant_id'] ] ], 'تم تسجيل الدخول بنجاح');