/** * Unified OTP Verify: Works for both login and signup * * POST: { "uuid": "...", "otp": "123456" } * * Returns: { OK: true, UserID: 123, Token: "...", NeedsProfile: true/false } * * - If account is complete (has FirstName) → NeedsProfile: false, user is logged in * - If account is incomplete → NeedsProfile: true, user continues to registration */ 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" }); } // Find user with matching UUID and OTP (any verification status) qUser = queryTimed(" SELECT ID, FirstName, LastName, EmailAddress, IsContactVerified, IsEmailVerified, IsActive FROM Users WHERE UUID = :uuid AND MobileVerifyCode = :otp 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 = queryTimed(" SELECT ID FROM Users WHERE UUID = :uuid ", { 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." }); } } // Check if profile is complete (has first name) needsProfile = !len(trim(qUser.FirstName)); // Clear the OTP code (one-time use) // If profile is complete, mark as verified and active (login case) // If profile incomplete, leave unverified until profile completion if (needsProfile) { queryTimed(" UPDATE Users SET MobileVerifyCode = '' WHERE ID = :userId ", { userId: { value: qUser.ID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" }); } else { queryTimed(" UPDATE Users SET MobileVerifyCode = '', IsContactVerified = 1, IsActive = 1 WHERE ID = :userId ", { userId: { value: qUser.ID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" }); } // Create auth token token = replace(createUUID(), "-", "", "all"); queryTimed(" 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" }); 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 }); }