155 lines
4.3 KiB
PHP
155 lines
4.3 KiB
PHP
<?php
|
|
/**
|
|
* POST /api/verify-otp
|
|
*
|
|
* Verify an OTP code submitted by the user (Flutter app).
|
|
*
|
|
* Request body:
|
|
* {
|
|
* "phone": "+9627XXXXXXXX",
|
|
* "otp": "4829",
|
|
* "app_key": "SECRET_APP_KEY"
|
|
* }
|
|
*/
|
|
|
|
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');
|
|
|
|
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';
|
|
|
|
// Authenticate — requires app key
|
|
Auth::requireAuth('app');
|
|
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
|
|
if (!$input || !isset($input['phone']) || !isset($input['otp'])) {
|
|
http_response_code(400);
|
|
echo json_encode(['success' => false, 'message' => 'missing_phone_or_otp']);
|
|
RequestLogger::log('verify-otp', 'POST', $input, 400, 'missing_fields');
|
|
exit;
|
|
}
|
|
|
|
$phone = trim($input['phone']);
|
|
$otp = trim($input['otp']);
|
|
|
|
// Validate phone format
|
|
if (!preg_match('/^\+[1-9]\d{6,14}$/', $phone)) {
|
|
http_response_code(400);
|
|
echo json_encode(['success' => false, 'message' => 'invalid_phone_format']);
|
|
RequestLogger::log('verify-otp', 'POST', $input, 400, 'invalid_phone');
|
|
exit;
|
|
}
|
|
|
|
// Validate OTP format (3 or 4 digits)
|
|
if (!preg_match('/^\d{3,4}$/', $otp)) {
|
|
http_response_code(400);
|
|
echo json_encode(['success' => false, 'message' => 'invalid_otp_format']);
|
|
RequestLogger::log('verify-otp', 'POST', $input, 400, 'invalid_otp_format');
|
|
exit;
|
|
}
|
|
|
|
$redis = RedisClient::getInstance();
|
|
$db = Database::getInstance();
|
|
$redisKey = "otp:{$phone}";
|
|
|
|
// Check if OTP exists in Redis
|
|
$stored = $redis->get($redisKey);
|
|
|
|
if ($stored === false || $stored === null) {
|
|
// OTP expired or never existed
|
|
echo json_encode([
|
|
'success' => false,
|
|
'message' => 'expired',
|
|
]);
|
|
RequestLogger::log('verify-otp', 'POST', $input, 200, 'expired');
|
|
exit;
|
|
}
|
|
|
|
// Decode stored data
|
|
if (is_string($stored)) {
|
|
$otpData = json_decode($stored, true);
|
|
} else {
|
|
$otpData = $stored;
|
|
}
|
|
|
|
if (!is_array($otpData) || !isset($otpData['otp'])) {
|
|
// Corrupted data — clean up
|
|
$redis->del($redisKey);
|
|
echo json_encode(['success' => false, 'message' => 'expired']);
|
|
RequestLogger::log('verify-otp', 'POST', $input, 200, 'corrupted_data');
|
|
exit;
|
|
}
|
|
|
|
// Check max attempts
|
|
if (isset($otpData['attempts']) && (int) $otpData['attempts'] >= MAX_OTP_ATTEMPTS) {
|
|
$redis->del($redisKey);
|
|
echo json_encode([
|
|
'success' => false,
|
|
'message' => 'max_attempts',
|
|
]);
|
|
RequestLogger::log('verify-otp', 'POST', $input, 200, 'max_attempts');
|
|
exit;
|
|
}
|
|
|
|
// Increment attempt counter
|
|
$otpData['attempts'] = (int) ($otpData['attempts'] ?? 0) + 1;
|
|
$ttl = $redis->ttl($redisKey);
|
|
if ($ttl > 0) {
|
|
$redis->setex($redisKey, $ttl, json_encode($otpData));
|
|
}
|
|
|
|
// Timing-safe comparison
|
|
if (hash_equals($otpData['otp'], $otp)) {
|
|
// Success — clean up Redis
|
|
$redis->del($redisKey);
|
|
|
|
// Update database
|
|
try {
|
|
$stmt = $db->prepare(
|
|
"UPDATE otp_requests
|
|
SET verified_at = NOW(), status = 'verified'
|
|
WHERE phone = ? AND otp_code = ? AND verified_at IS NULL
|
|
ORDER BY created_at DESC
|
|
LIMIT 1"
|
|
);
|
|
$stmt->execute([$phone, $otp]);
|
|
} catch (\Throwable $e) {
|
|
// Non-critical — OTP is already verified via Redis
|
|
error_log('verify-otp DB update error: ' . $e->getMessage());
|
|
}
|
|
|
|
echo json_encode([
|
|
'success' => true,
|
|
'message' => 'verified',
|
|
]);
|
|
RequestLogger::log('verify-otp', 'POST', $input, 200);
|
|
|
|
} else {
|
|
// Wrong OTP
|
|
$remainingAttempts = MAX_OTP_ATTEMPTS - (int) $otpData['attempts'];
|
|
|
|
echo json_encode([
|
|
'success' => false,
|
|
'message' => 'invalid_otp',
|
|
'remaining_attempts' => max(0, $remainingAttempts),
|
|
]);
|
|
RequestLogger::log('verify-otp', 'POST', $input, 200, 'invalid_otp');
|
|
}
|