- sendLoginOTP.cfm: Accept Email, Phone, or Identifier field Sends OTP via SMS for phone, email for email addresses - verifyEmailOTP.cfm: Accept phone numbers for verification Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
154 lines
3.8 KiB
Text
154 lines
3.8 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 Code for Portal Login (supports email or phone)
|
|
POST: { "Email": "user@example.com", "Code": "123456" }
|
|
or { "Phone": "3105551234", "Code": "123456" }
|
|
or { "Identifier": "email or phone", "Code": "123456" }
|
|
Returns: { OK: true, UserID, FirstName, Token } or { OK: false, ERROR: "invalid_code" }
|
|
*/
|
|
|
|
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 {};
|
|
}
|
|
|
|
function normalizePhone(phone) {
|
|
var digits = reReplace(phone, "[^0-9]", "", "all");
|
|
if (len(digits) == 11 && left(digits, 1) == "1") {
|
|
digits = right(digits, 10);
|
|
}
|
|
return digits;
|
|
}
|
|
|
|
function isPhoneNumber(input) {
|
|
var digits = reReplace(input, "[^0-9]", "", "all");
|
|
return len(digits) >= 10 && len(digits) <= 11;
|
|
}
|
|
|
|
data = readJsonBody();
|
|
identifier = trim(data.Identifier ?: data.Email ?: data.Phone ?: "");
|
|
code = trim(data.Code ?: "");
|
|
|
|
if (!len(identifier) || !len(code)) {
|
|
apiAbort({ "OK": false, "ERROR": "missing_fields", "MESSAGE": "Email/phone and code are required" });
|
|
}
|
|
|
|
// Determine if this is email or phone
|
|
isPhone = isPhoneNumber(identifier);
|
|
email = "";
|
|
phone = "";
|
|
|
|
if (isPhone) {
|
|
phone = normalizePhone(identifier);
|
|
} else {
|
|
email = identifier;
|
|
}
|
|
|
|
try {
|
|
// Look up user by email or phone
|
|
if (len(email)) {
|
|
qUser = queryTimed("
|
|
SELECT ID, FirstName
|
|
FROM Users
|
|
WHERE EmailAddress = :email
|
|
AND IsActive = 1
|
|
LIMIT 1
|
|
", {
|
|
email: { value: email, cfsqltype: "cf_sql_varchar" }
|
|
}, { datasource: "payfrit" });
|
|
} else {
|
|
qUser = queryTimed("
|
|
SELECT ID, FirstName
|
|
FROM Users
|
|
WHERE ContactNumber = :phone
|
|
AND IsActive = 1
|
|
LIMIT 1
|
|
", {
|
|
phone: { value: phone, cfsqltype: "cf_sql_varchar" }
|
|
}, { datasource: "payfrit" });
|
|
}
|
|
|
|
if (qUser.recordCount == 0) {
|
|
apiAbort({ "OK": false, "ERROR": "invalid_code", "MESSAGE": "Invalid or expired code" });
|
|
}
|
|
|
|
userId = qUser.ID;
|
|
|
|
// 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" });
|
|
|
|
// Create auth token (same as login.cfm)
|
|
token = replace(createUUID(), "-", "", "all");
|
|
|
|
queryTimed(
|
|
"INSERT INTO UserTokens (UserID, Token) VALUES (?, ?)",
|
|
[
|
|
{ value: userId, cfsqltype: "cf_sql_integer" },
|
|
{ value: token, cfsqltype: "cf_sql_varchar" }
|
|
],
|
|
{ datasource: "payfrit" }
|
|
);
|
|
|
|
// Set session
|
|
lock timeout="15" throwontimeout="yes" type="exclusive" scope="session" {
|
|
session.UserID = userId;
|
|
}
|
|
request.UserID = userId;
|
|
|
|
writeOutput(serializeJSON({
|
|
"OK": true,
|
|
"ERROR": "",
|
|
"UserID": userId,
|
|
"FirstName": qUser.FirstName,
|
|
"Token": token
|
|
}));
|
|
|
|
} catch (any e) {
|
|
apiAbort({
|
|
"OK": false,
|
|
"ERROR": "server_error",
|
|
"MESSAGE": e.message
|
|
});
|
|
}
|
|
</cfscript>
|