249 lines
8.3 KiB
PHP
249 lines
8.3 KiB
PHP
<?php
|
|
/**
|
|
* POST /api/request-otp
|
|
*
|
|
* Request an OTP to be delivered via flash call (Android) or SMS (iOS).
|
|
*
|
|
* Request body:
|
|
* {
|
|
* "phone": "+9627XXXXXXXX",
|
|
* "app_key": "SECRET_APP_KEY",
|
|
* "device_type": "android" | "ios" (optional, defaults to "android")
|
|
* }
|
|
*/
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Access-Control-Allow-Origin: *');
|
|
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
|
header('Access-Control-Allow-Headers: Content-Type, X-App-Key');
|
|
|
|
// Handle CORS preflight
|
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|
http_response_code(204);
|
|
exit;
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
http_response_code(405);
|
|
echo json_encode(['success' => false, 'message' => 'method_not_allowed']);
|
|
exit;
|
|
}
|
|
|
|
require_once __DIR__ . '/../includes/Database.php';
|
|
require_once __DIR__ . '/../includes/Redis.php';
|
|
require_once __DIR__ . '/../includes/RateLimit.php';
|
|
require_once __DIR__ . '/../includes/Auth.php';
|
|
require_once __DIR__ . '/../includes/Logger.php';
|
|
require_once __DIR__ . '/../includes/WhatsApp.php';
|
|
|
|
// Authenticate — requires app key (Flutter app)
|
|
Auth::requireAuth('app');
|
|
|
|
// Parse request body
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
|
|
if (!$input || !isset($input['phone'])) {
|
|
http_response_code(400);
|
|
echo json_encode(['success' => false, 'message' => 'missing_phone']);
|
|
RequestLogger::log('request-otp', 'POST', $input, 400, 'missing_phone');
|
|
exit;
|
|
}
|
|
|
|
$phone = trim($input['phone']);
|
|
$deviceType = isset($input['device_type']) ? strtolower(trim($input['device_type'])) : 'android';
|
|
|
|
// Validate device_type
|
|
if (!in_array($deviceType, ['android', 'ios'], true)) {
|
|
http_response_code(400);
|
|
echo json_encode(['success' => false, 'message' => 'invalid_device_type']);
|
|
RequestLogger::log('request-otp', 'POST', $input, 400, 'invalid_device_type');
|
|
exit;
|
|
}
|
|
|
|
// Validate phone format (E.164)
|
|
if (!preg_match('/^\+[1-9]\d{6,14}$/', $phone)) {
|
|
http_response_code(400);
|
|
echo json_encode([
|
|
'success' => false,
|
|
'message' => 'invalid_phone_format',
|
|
'hint' => 'Phone must be in E.164 format, e.g. +9627XXXXXXXX',
|
|
]);
|
|
RequestLogger::log('request-otp', 'POST', $input, 400, 'invalid_phone_format');
|
|
exit;
|
|
}
|
|
|
|
// Rate limit check: max 3 requests per phone per 10 minutes
|
|
$rateLimit = new RateLimit();
|
|
if (!$rateLimit->check("otp:{$phone}")) {
|
|
$remaining = $rateLimit->remaining("otp:{$phone}");
|
|
$ttl = $rateLimit->ttl("otp:{$phone}");
|
|
http_response_code(429);
|
|
echo json_encode([
|
|
'success' => false,
|
|
'message' => 'rate_limit_exceeded',
|
|
'retry_after' => $ttl,
|
|
'remaining' => $remaining,
|
|
]);
|
|
RequestLogger::log('request-otp', 'POST', $input, 429, 'rate_limit_exceeded');
|
|
exit;
|
|
}
|
|
|
|
// IP-based rate limiting
|
|
$clientIp = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
|
if (!$rateLimit->checkIp($clientIp, 'request-otp', 30, 60)) {
|
|
http_response_code(429);
|
|
echo json_encode(['success' => false, 'message' => 'ip_rate_limit_exceeded']);
|
|
RequestLogger::log('request-otp', 'POST', $input, 429, 'ip_rate_limit');
|
|
exit;
|
|
}
|
|
|
|
// Generate 3-digit OTP (cryptographically secure, always between 100 and 999)
|
|
// If a custom code is provided in the input, use it. Otherwise generate one.
|
|
$customCode = isset($input['code']) ? trim((string)$input['code']) : null;
|
|
$otpCode = $customCode ?: (string) random_int(100, 999);
|
|
|
|
// Determine delivery method
|
|
$method = 'flash_call'; // Default fallback
|
|
$methodInput = isset($input['method']) ? strtolower(trim($input['method'])) : null;
|
|
|
|
if ($methodInput && in_array($methodInput, ['flash_call', 'sms', 'whatsapp', 'telegram'], true)) {
|
|
$method = $methodInput;
|
|
} else {
|
|
$whatsappAvailable = false;
|
|
try {
|
|
$whatsappAvailable = WhatsAppClient::isAvailable($phone);
|
|
} catch (\Throwable $e) {
|
|
error_log('WhatsApp check failed: ' . $e->getMessage());
|
|
}
|
|
|
|
if ($whatsappAvailable) {
|
|
$method = 'whatsapp';
|
|
} else {
|
|
$method = ($deviceType === 'ios') ? 'sms' : 'flash_call';
|
|
}
|
|
}
|
|
|
|
$db = Database::getInstance();
|
|
$redis = RedisClient::getInstance();
|
|
|
|
try {
|
|
$db->beginTransaction();
|
|
|
|
if ($method === 'flash_call') {
|
|
// Find available caller device (round-robin)
|
|
$stmt = $db->prepare(
|
|
"SELECT device_id, phone_number, sim_slot
|
|
FROM caller_devices
|
|
WHERE is_active = 1
|
|
ORDER BY calls_today ASC, last_seen DESC
|
|
LIMIT 1"
|
|
);
|
|
$stmt->execute();
|
|
$device = $stmt->fetch();
|
|
|
|
if (!$device) {
|
|
$db->rollBack();
|
|
http_response_code(503);
|
|
echo json_encode([
|
|
'success' => false,
|
|
'message' => 'no_caller_devices_available',
|
|
]);
|
|
RequestLogger::log('request-otp', 'POST', $input, 503, 'no_caller_devices');
|
|
exit;
|
|
}
|
|
|
|
// Build caller_id: +96279XX{OTP}
|
|
$randomDigits = str_pad((string) random_int(0, 99), 2, '0', STR_PAD_LEFT);
|
|
$callerId = CALLER_ID_PREFIX . $randomDigits . $otpCode;
|
|
|
|
// Insert OTP request
|
|
$expiresAt = date('Y-m-d H:i:s', time() + OTP_EXPIRE_SECONDS);
|
|
$stmt = $db->prepare(
|
|
"INSERT INTO otp_requests (phone, otp_code, caller_id, status, device_id, method, expires_at)
|
|
VALUES (?, ?, ?, 'pending', ?, 'flash_call', ?)"
|
|
);
|
|
$stmt->execute([$phone, $otpCode, $callerId, $device['device_id'], $expiresAt]);
|
|
} else if ($method === 'whatsapp') {
|
|
// WhatsApp delivery
|
|
$expiresAt = date('Y-m-d H:i:s', time() + OTP_EXPIRE_SECONDS);
|
|
$stmt = $db->prepare(
|
|
"INSERT INTO otp_requests (phone, otp_code, caller_id, status, method, expires_at)
|
|
VALUES (?, ?, '', 'pending_whatsapp', 'whatsapp', ?)"
|
|
);
|
|
$stmt->execute([$phone, $otpCode, $expiresAt]);
|
|
$otpId = $db->lastInsertId();
|
|
|
|
// Try to generate premium dynamic base64 OTP image
|
|
$imagePngBase64 = null;
|
|
try {
|
|
$imagePngBase64 = WhatsAppClient::generateOtpImageBase64($otpCode);
|
|
} catch (\Throwable $e) {
|
|
error_log('Failed to generate OTP image: ' . $e->getMessage());
|
|
}
|
|
|
|
// Message caption / body
|
|
$messageText = "رمز التحقق الخاص بك هو: " . $otpCode;
|
|
|
|
$sent = false;
|
|
try {
|
|
if ($imagePngBase64) {
|
|
// Send premium image message with NO caption
|
|
$sent = WhatsAppClient::sendMessage($phone, "", $imagePngBase64);
|
|
} else {
|
|
// Fallback to text message
|
|
$sent = WhatsAppClient::sendMessage($phone, $messageText);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
error_log('WhatsApp sendMessage error: ' . $e->getMessage());
|
|
}
|
|
|
|
if (!$sent) {
|
|
throw new \Exception('Failed to send OTP via WhatsApp');
|
|
}
|
|
} else {
|
|
// SMS delivery — no specific caller_id needed for the OTP request
|
|
$expiresAt = date('Y-m-d H:i:s', time() + OTP_EXPIRE_SECONDS);
|
|
$stmt = $db->prepare(
|
|
"INSERT INTO otp_requests (phone, otp_code, caller_id, status, method, expires_at)
|
|
VALUES (?, ?, '', 'pending_sms', 'sms', ?)"
|
|
);
|
|
$stmt->execute([$phone, $otpCode, $expiresAt]);
|
|
}
|
|
|
|
$otpId = $db->lastInsertId();
|
|
$db->commit();
|
|
|
|
// Store OTP in Redis with TTL
|
|
$redisKey = "otp:{$phone}";
|
|
$redis->setex($redisKey, OTP_EXPIRE_SECONDS, json_encode([
|
|
'otp' => $otpCode,
|
|
'method' => $method,
|
|
'attempts' => 0,
|
|
'created' => time(),
|
|
]));
|
|
|
|
// Response
|
|
$response = [
|
|
'success' => true,
|
|
'otp_id' => $otpId,
|
|
'otp' => $otpCode,
|
|
'expires_in' => OTP_EXPIRE_SECONDS,
|
|
'method' => $method,
|
|
];
|
|
|
|
if ($method === 'flash_call' && isset($device)) {
|
|
$response['caller_device_id'] = $device['device_id'];
|
|
}
|
|
|
|
echo json_encode($response);
|
|
|
|
RequestLogger::log('request-otp', 'POST', $input, 200);
|
|
|
|
} catch (\Throwable $e) {
|
|
$db->rollBack();
|
|
error_log('request-otp error: ' . $e->getMessage());
|
|
http_response_code(500);
|
|
echo json_encode(['success' => false, 'message' => 'internal_error']);
|
|
RequestLogger::log('request-otp', 'POST', $input, 500, $e->getMessage());
|
|
}
|