first commit
This commit is contained in:
154
backend/api/verify-otp.php
Normal file
154
backend/api/verify-otp.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?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 (4 digits)
|
||||
if (!preg_match('/^\d{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');
|
||||
}
|
||||
Reference in New Issue
Block a user