Magic OTP: accept 123456 alongside real codes (for App Store review)
This commit is contained in:
parent
4109e3dac4
commit
b39f8bf1e8
6 changed files with 55 additions and 69 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Reference in a new issue