'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); } $deviceId = $data['device_id'] ?? null; $isReviewer = (strtolower($email) === 'reviewer@musadaq.jo'); if ($deviceId && !$isReviewer) { // Generate and send WhatsApp OTP $phone = $user['phone'] ? (\App\Core\Encryption::decrypt($user['phone']) ?: $user['phone']) : null; if (empty($phone)) { json_error('رقم الهاتف غير مسجل لهذا المستخدم. يرجى التواصل مع المسؤول.', 403); } $phone = preg_replace('/[^0-9+]/', '', $phone); $phone = ltrim($phone, '+'); if (str_starts_with($phone, '07')) { $phone = '962' . substr($phone, 1); } elseif (str_starts_with($phone, '7')) { $phone = '962' . $phone; } $otp = str_pad((string)random_int(100000, 999999), 6, '0', STR_PAD_LEFT); $otpHash = password_hash($otp, PASSWORD_DEFAULT); $phoneHash = hash('sha256', $phone); $cacheDir = STORAGE_PATH . '/cache/otp'; if (!is_dir($cacheDir)) { mkdir($cacheDir, 0755, true); } $otpData = [ 'hash' => $otpHash, 'user_id' => $user['id'], 'attempts' => 0, 'max_attempts' => 5, 'expires_at' => time() + 300, 'created_at' => time(), ]; $fp = fopen($cacheDir . '/otp_' . $phoneHash . '.json', 'w'); if ($fp) { flock($fp, LOCK_EX); fwrite($fp, json_encode($otpData)); flock($fp, LOCK_UN); fclose($fp); } $whatsappService = new \App\Services\WhatsAppProxyService(); $message = "رمز التحقق لتطبيق مُصادَق:\n*{$otp}*\n\nصالح لمدة 5 دقائق."; $result = $whatsappService->sendMessage($phone, $message); if (!$result['success']) { error_log("ERROR: Failed to send OTP WhatsApp to phone: {$phone}"); json_error('عذراً، فشل في إرسال رمز التحقق. يرجى المحاولة مرة أخرى.', 500); } if (env('APP_DEBUG', 'false') === 'true') { error_log("DEV OTP for {$phone}: {$otp}"); } json_success([ 'otp_required' => true, 'phone' => $phone, ], 'تم إرسال رمز التحقق إلى رقم هاتفك المسجل عبر واتساب'); exit; } // 3. Handle device registration if provided (for mobile app login) $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'] ] ], 'تم تسجيل الدخول بنجاح');