This repository has been archived on 2026-03-21. You can view files and clone it, but cannot push or open issues or pull requests.
payfrit-biz/api/auth/verifyOTP.cfm
John Mizerek bc88f28e60 Fix UserID column references in auth endpoints after schema normalization
Users table primary key was renamed from UserID to ID but these
endpoints still referenced the old column name, causing server_error
on login/signup OTP flow.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 17:15:46 -08:00

125 lines
4.2 KiB
Text

<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfheader name="Cache-Control" value="no-store">
<cfscript>
/**
* Verify OTP and activate user account
*
* POST: { "uuid": "...", "otp": "123456" }
*
* Returns: { OK: true, UserID: 123, Token: "...", NeedsProfile: true/false }
*
* On success, marks phone as verified and returns auth token.
* NeedsProfile indicates if user still needs to provide name/email.
*/
function apiAbort(required struct payload) {
writeOutput(serializeJSON(payload));
abort;
}
function readJsonBody() {
var raw = getHttpRequestData().content;
if (isNull(raw)) raw = "";
if (!len(trim(raw))) return {};
try {
var data = deserializeJSON(raw);
if (isStruct(data)) return data;
} catch (any e) {}
return {};
}
try {
data = readJsonBody();
userUUID = structKeyExists(data, "uuid") ? trim(data.uuid) : "";
otp = structKeyExists(data, "otp") ? trim(data.otp) : "";
if (!len(userUUID) || !len(otp)) {
apiAbort({ "OK": false, "ERROR": "missing_fields", "MESSAGE": "UUID and OTP are required" });
}
// Check for magic OTP bypass (for App Store review)
isMagicOTP = structKeyExists(application, "MAGIC_OTP_ENABLED")
&& application.MAGIC_OTP_ENABLED
&& structKeyExists(application, "MAGIC_OTP_CODE")
&& otp == application.MAGIC_OTP_CODE;
// Find unverified user with matching UUID and OTP (or magic OTP)
if (isMagicOTP) {
qUser = queryExecute("
SELECT ID, FirstName, LastName, EmailAddress, IsEmailVerified
FROM Users
WHERE UUID = :uuid
AND IsContactVerified = 0
LIMIT 1
", {
uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" }
}, { datasource: "payfrit" });
} else {
qUser = queryExecute("
SELECT ID, FirstName, LastName, EmailAddress, IsEmailVerified
FROM Users
WHERE UUID = :uuid
AND MobileVerifyCode = :otp
AND IsContactVerified = 0
LIMIT 1
", {
uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" },
otp: { value: otp, cfsqltype: "cf_sql_varchar" }
}, { datasource: "payfrit" });
}
if (qUser.recordCount == 0) {
// Check if UUID exists but OTP is wrong
qCheck = queryExecute("
SELECT ID FROM Users WHERE UUID = :uuid AND IsContactVerified = 0
", { uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" });
if (qCheck.recordCount > 0) {
apiAbort({ "OK": false, "ERROR": "invalid_otp", "MESSAGE": "Invalid verification code. Please try again." });
} else {
apiAbort({ "OK": false, "ERROR": "expired", "MESSAGE": "Verification expired. Please request a new code." });
}
}
// Clear the OTP code (one-time use) but DON'T mark as verified yet
// Account will be marked verified after profile completion
queryExecute("
UPDATE Users
SET MobileVerifyCode = ''
WHERE ID = :userId
", { userId: { value: qUser.ID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" });
// Create auth token (needed for completeProfile call)
token = replace(createUUID(), "-", "", "all");
queryExecute("
INSERT INTO UserTokens (UserID, Token) VALUES (:userId, :token)
", {
userId: { value: qUser.ID, cfsqltype: "cf_sql_integer" },
token: { value: token, cfsqltype: "cf_sql_varchar" }
}, { datasource: "payfrit" });
// Check if profile is complete (has first name)
// For new signups, this will always be true
needsProfile = !len(trim(qUser.FirstName));
try{logPerf(0);}catch(any e){}
writeOutput(serializeJSON({
"OK": true,
"UserID": qUser.ID,
"Token": token,
"NeedsProfile": needsProfile,
"FirstName": qUser.FirstName ?: "",
"IsEmailVerified": qUser.IsEmailVerified == 1
}));
} catch (any e) {
apiAbort({
"OK": false,
"ERROR": "server_error",
"MESSAGE": e.message
});
}
</cfscript>