false, 'ERROR' => 'missing_fields', 'MESSAGE' => 'Email/phone and code are required']); } $isPhone = isPhoneNumber($identifier); $email = ''; $phone = ''; if ($isPhone) { $phone = normalizePhone($identifier); } else { $email = $identifier; } if (!empty($email)) { $user = queryOne( "SELECT ID, FirstName FROM Users WHERE EmailAddress = ? AND IsActive = 1 LIMIT 1", [$email] ); } else { $user = queryOne( "SELECT ID, FirstName FROM Users WHERE ContactNumber = ? AND IsActive = 1 LIMIT 1", [$phone] ); } if (!$user) { apiAbort(['OK' => false, 'ERROR' => 'invalid_code', 'MESSAGE' => 'Invalid or expired code']); } $uid = (int) $user['ID']; // Check for valid OTP in OTPCodes table $otpRow = queryOne( "SELECT ID FROM OTPCodes WHERE UserID = ? AND Code = ? AND ExpiresAt > NOW() AND UsedAt IS NULL ORDER BY CreatedAt DESC LIMIT 1", [$uid, $code] ); if (!$otpRow) { apiAbort(['OK' => false, 'ERROR' => 'invalid_code', 'MESSAGE' => 'Invalid or expired code']); } // Mark OTP as used queryTimed("UPDATE OTPCodes SET UsedAt = NOW() WHERE ID = ?", [$otpRow['ID']]); // Create auth token $token = generateSecureToken(); queryTimed( "INSERT INTO UserTokens (UserID, Token) VALUES (?, ?)", [$uid, $token] ); jsonResponse([ 'OK' => true, 'ERROR' => '', 'USERID' => $uid, 'FIRSTNAME' => $user['FirstName'], 'TOKEN' => $token, ]);