184 lines
5.8 KiB
PHP
184 lines
5.8 KiB
PHP
<?php
|
|
/**
|
|
* Auth Login Endpoint
|
|
*/
|
|
|
|
use App\Core\Database;
|
|
use App\Core\JWT;
|
|
use App\Core\Validator;
|
|
|
|
use App\Middleware\RateLimitMiddleware;
|
|
use App\Core\Security;
|
|
|
|
// 0. Rate Limiting (5 attempts per minute per IP)
|
|
RateLimitMiddleware::check(5, 60);
|
|
|
|
$data = Security::sanitize(input());
|
|
|
|
// 1. Validation
|
|
$errors = Validator::validate($data, [
|
|
'email' => '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']
|
|
]
|
|
], 'تم تسجيل الدخول بنجاح');
|