/** * 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 UserID, UserFirstName, UserLastName, UserEmailAddress, UserIsEmailVerified FROM Users WHERE UserUUID = :uuid AND UserIsContactVerified = 0 LIMIT 1 ", { uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" }); } else { qUser = queryExecute(" SELECT UserID, UserFirstName, UserLastName, UserEmailAddress, UserIsEmailVerified FROM Users WHERE UserUUID = :uuid AND UserMobileVerifyCode = :otp AND UserIsContactVerified = 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 UserID FROM Users WHERE UserUUID = :uuid AND UserIsContactVerified = 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 UserMobileVerifyCode = '' WHERE UserID = :userId ", { userId: { value: qUser.UserID, 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.UserID, 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.UserFirstName)); writeOutput(serializeJSON({ "OK": true, "UserID": qUser.UserID, "Token": token, "NeedsProfile": needsProfile, "UserFirstName": qUser.UserFirstName ?: "", "IsEmailVerified": qUser.UserIsEmailVerified == 1 })); } catch (any e) { apiAbort({ "OK": false, "ERROR": "server_error", "MESSAGE": e.message }); }