Magic OTP: accept 123456 alongside real codes (for App Store review)

This commit is contained in:
John Mizerek 2026-03-06 16:29:31 -08:00
parent 4109e3dac4
commit b39f8bf1e8
6 changed files with 55 additions and 69 deletions

View file

@ -76,12 +76,8 @@ try {
}, { datasource: "payfrit" }); }, { datasource: "payfrit" });
} }
// Generate OTP (use magic code on dev for easy testing) // Generate OTP
otp = generateOTP(); otp = generateOTP();
if (structKeyExists(application, "MAGIC_OTP_ENABLED") && application.MAGIC_OTP_ENABLED
&& structKeyExists(application, "MAGIC_OTP_CODE") && len(application.MAGIC_OTP_CODE)) {
otp = application.MAGIC_OTP_CODE;
}
queryTimed(" queryTimed("
UPDATE Users UPDATE Users
SET MobileVerifyCode = :otp SET MobileVerifyCode = :otp

View file

@ -111,16 +111,11 @@ try {
abort; abort;
} }
// Generate 6-digit code (or use magic code on dev) // Generate 6-digit code
code = randRange(100000, 999999); code = randRange(100000, 999999);
isDev = findNoCase("dev.payfrit.com", cgi.SERVER_NAME) > 0 isDev = findNoCase("dev.payfrit.com", cgi.SERVER_NAME) > 0
|| findNoCase("localhost", cgi.SERVER_NAME) > 0; || findNoCase("localhost", cgi.SERVER_NAME) > 0;
if (isDev && structKeyExists(application, "MAGIC_OTP_ENABLED") && application.MAGIC_OTP_ENABLED
&& structKeyExists(application, "MAGIC_OTP_CODE") && len(application.MAGIC_OTP_CODE)) {
code = application.MAGIC_OTP_CODE;
}
// Store in DB with 10-minute expiry // Store in DB with 10-minute expiry
queryTimed(" queryTimed("
INSERT INTO OTPCodes (UserID, Code, ExpiresAt) INSERT INTO OTPCodes (UserID, Code, ExpiresAt)

View file

@ -64,10 +64,6 @@ try {
", { phone: { value: phone, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" }); ", { phone: { value: phone, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" });
otp = generateOTP(); otp = generateOTP();
if (structKeyExists(application, "MAGIC_OTP_ENABLED") && application.MAGIC_OTP_ENABLED
&& structKeyExists(application, "MAGIC_OTP_CODE") && len(application.MAGIC_OTP_CODE)) {
otp = application.MAGIC_OTP_CODE;
}
userUUID = ""; userUUID = "";
if (qExisting.recordCount > 0) { if (qExisting.recordCount > 0) {

View file

@ -90,34 +90,40 @@ try {
userId = qUser.ID; userId = qUser.ID;
// Check for valid OTP in OTPCodes table // Check OTP: accept real code OR magic code (123456) when enabled
qOTP = queryTimed(" isMagic = structKeyExists(application, "MAGIC_OTP_ENABLED") && application.MAGIC_OTP_ENABLED
SELECT ID && structKeyExists(application, "MAGIC_OTP_CODE") && code == application.MAGIC_OTP_CODE;
FROM OTPCodes
WHERE UserID = :userId
AND Code = :code
AND ExpiresAt > NOW()
AND UsedAt IS NULL
ORDER BY CreatedAt DESC
LIMIT 1
", {
userId: { value: userId, cfsqltype: "cf_sql_integer" },
code: { value: code, cfsqltype: "cf_sql_varchar" }
}, { datasource: "payfrit" });
if (qOTP.recordCount == 0) { if (!isMagic) {
apiAbort({ "OK": false, "ERROR": "invalid_code", "MESSAGE": "Invalid or expired code" }); // Check for valid OTP in OTPCodes table
qOTP = queryTimed("
SELECT ID
FROM OTPCodes
WHERE UserID = :userId
AND Code = :code
AND ExpiresAt > NOW()
AND UsedAt IS NULL
ORDER BY CreatedAt DESC
LIMIT 1
", {
userId: { value: userId, cfsqltype: "cf_sql_integer" },
code: { value: code, cfsqltype: "cf_sql_varchar" }
}, { datasource: "payfrit" });
if (qOTP.recordCount == 0) {
apiAbort({ "OK": false, "ERROR": "invalid_code", "MESSAGE": "Invalid or expired code" });
}
// Mark OTP as used
queryTimed("
UPDATE OTPCodes
SET UsedAt = NOW()
WHERE ID = :otpId
", {
otpId: { value: qOTP.ID, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });
} }
// Mark OTP as used
queryTimed("
UPDATE OTPCodes
SET UsedAt = NOW()
WHERE ID = :otpId
", {
otpId: { value: qOTP.ID, cfsqltype: "cf_sql_integer" }
}, { datasource: "payfrit" });
// Create auth token (same as login.cfm) // Create auth token (same as login.cfm)
token = replace(createUUID(), "-", "", "all"); token = replace(createUUID(), "-", "", "all");

View file

@ -37,31 +37,27 @@ try {
apiAbort({ "OK": false, "ERROR": "missing_fields", "MESSAGE": "UUID and OTP are required" }); apiAbort({ "OK": false, "ERROR": "missing_fields", "MESSAGE": "UUID and OTP are required" });
} }
// Find verified user with matching UUID and OTP // Find verified user by UUID
// Magic OTP only bypasses Twilio SMS (in loginOTP.cfm), not OTP verification
qUser = queryTimed(" qUser = queryTimed("
SELECT ID, FirstName, LastName SELECT ID, FirstName, LastName, MobileVerifyCode
FROM Users FROM Users
WHERE UUID = :uuid WHERE UUID = :uuid
AND MobileVerifyCode = :otp
AND IsContactVerified = 1 AND IsContactVerified = 1
LIMIT 1 LIMIT 1
", { ", {
uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" }, uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" }
otp: { value: otp, cfsqltype: "cf_sql_varchar" }
}, { datasource: "payfrit" }); }, { datasource: "payfrit" });
if (qUser.recordCount == 0) { if (qUser.recordCount == 0) {
// Check if UUID exists but OTP is wrong apiAbort({ "OK": false, "ERROR": "expired", "MESSAGE": "Session expired. Please request a new code." });
qCheck = queryTimed(" }
SELECT ID FROM Users WHERE UUID = :uuid AND IsContactVerified = 1
", { uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" });
if (qCheck.recordCount > 0) { // Check OTP: accept real code OR magic code (123456) when enabled
apiAbort({ "OK": false, "ERROR": "invalid_otp", "MESSAGE": "Invalid code. Please try again." }); isMagic = structKeyExists(application, "MAGIC_OTP_ENABLED") && application.MAGIC_OTP_ENABLED
} else { && structKeyExists(application, "MAGIC_OTP_CODE") && otp == application.MAGIC_OTP_CODE;
apiAbort({ "OK": false, "ERROR": "expired", "MESSAGE": "Session expired. Please request a new code." });
} if (qUser.MobileVerifyCode != otp && !isMagic) {
apiAbort({ "OK": false, "ERROR": "invalid_otp", "MESSAGE": "Invalid code. Please try again." });
} }
// Clear the OTP (one-time use) // Clear the OTP (one-time use)

View file

@ -40,29 +40,26 @@ try {
apiAbort({ "OK": false, "ERROR": "missing_fields", "MESSAGE": "UUID and OTP are required" }); apiAbort({ "OK": false, "ERROR": "missing_fields", "MESSAGE": "UUID and OTP are required" });
} }
// Find user with matching UUID and OTP (any verification status) // Find user by UUID
qUser = queryTimed(" qUser = queryTimed("
SELECT ID, FirstName, LastName, EmailAddress, IsContactVerified, IsEmailVerified, IsActive SELECT ID, FirstName, LastName, EmailAddress, IsContactVerified, IsEmailVerified, IsActive, MobileVerifyCode
FROM Users FROM Users
WHERE UUID = :uuid WHERE UUID = :uuid
AND MobileVerifyCode = :otp
LIMIT 1 LIMIT 1
", { ", {
uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" }, uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" }
otp: { value: otp, cfsqltype: "cf_sql_varchar" }
}, { datasource: "payfrit" }); }, { datasource: "payfrit" });
if (qUser.recordCount == 0) { if (qUser.recordCount == 0) {
// Check if UUID exists but OTP is wrong apiAbort({ "OK": false, "ERROR": "expired", "MESSAGE": "Verification expired. Please request a new code." });
qCheck = queryTimed(" }
SELECT ID FROM Users WHERE UUID = :uuid
", { uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" });
if (qCheck.recordCount > 0) { // Check OTP: accept real code OR magic code (123456) when enabled
apiAbort({ "OK": false, "ERROR": "invalid_otp", "MESSAGE": "Invalid verification code. Please try again." }); isMagic = structKeyExists(application, "MAGIC_OTP_ENABLED") && application.MAGIC_OTP_ENABLED
} else { && structKeyExists(application, "MAGIC_OTP_CODE") && otp == application.MAGIC_OTP_CODE;
apiAbort({ "OK": false, "ERROR": "expired", "MESSAGE": "Verification expired. Please request a new code." });
} if (qUser.MobileVerifyCode != otp && !isMagic) {
apiAbort({ "OK": false, "ERROR": "invalid_otp", "MESSAGE": "Invalid verification code. Please try again." });
} }
// Check if profile is complete (has first name) // Check if profile is complete (has first name)