diff --git a/api/auth/sendOTP.cfm b/api/auth/sendOTP.cfm index 46d55bc..700c11a 100644 --- a/api/auth/sendOTP.cfm +++ b/api/auth/sendOTP.cfm @@ -5,15 +5,16 @@ /** - * Send OTP to phone number for signup + * Unified OTP: Send OTP to any phone number (login or signup) * * POST: { "phone": "5551234567" } * - * Returns: { OK: true, UUID: "..." } or { OK: false, ERROR: "..." } + * Returns: { OK: true, UUID: "..." } * - * If phone already has verified account, returns error. - * If phone has unverified account, resends OTP. - * Otherwise creates new user record with OTP. + * Works for any phone number: + * - Existing complete account: updates OTP, user will be logged in after verify + * - Existing incomplete account: updates OTP, user continues to registration after verify + * - New phone: creates user record, user continues to registration after verify */ function apiAbort(required struct payload) { @@ -54,29 +55,11 @@ try { apiAbort({ "OK": false, "ERROR": "invalid_phone", "MESSAGE": "Please enter a valid 10-digit phone number" }); } - // Check if phone already has a COMPLETE account (verified AND has profile info) - // An account is only "complete" if they have a first name (meaning they finished signup) + // Check if phone already has ANY account qExisting = queryTimed(" - SELECT ID, UUID, FirstName + SELECT ID, UUID, FirstName, IsContactVerified, IsActive FROM Users WHERE ContactNumber = :phone - AND IsContactVerified > 0 - AND FirstName IS NOT NULL - AND LENGTH(TRIM(FirstName)) > 0 - LIMIT 1 - ", { phone: { value: phone, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" }); - - if (qExisting.recordCount > 0) { - apiAbort({ "OK": false, "ERROR": "phone_exists", "MESSAGE": "This phone number already has an account. Please login instead." }); - } - - // Check for incomplete account with this phone (verified but no profile, OR unverified) - // These accounts can be reused for signup - qIncomplete = queryTimed(" - SELECT ID, UUID - FROM Users - WHERE ContactNumber = :phone - AND (IsContactVerified = 0 OR FirstName IS NULL OR LENGTH(TRIM(FirstName)) = 0) LIMIT 1 ", { phone: { value: phone, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" }); @@ -87,21 +70,24 @@ try { } userUUID = ""; - if (qIncomplete.recordCount > 0) { - // Update existing incomplete record with new OTP and reset for re-registration - userUUID = qIncomplete.UUID; + if (qExisting.recordCount > 0) { + // Existing account - just update OTP + userUUID = qExisting.UUID; + if (!len(trim(userUUID))) { + userUUID = replace(createUUID(), "-", "", "all"); + } queryTimed(" UPDATE Users SET MobileVerifyCode = :otp, - IsContactVerified = 0, - IsActive = 0 + UUID = :uuid WHERE ID = :userId ", { otp: { value: otp, cfsqltype: "cf_sql_varchar" }, - userId: { value: qIncomplete.ID, cfsqltype: "cf_sql_integer" } + uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" }, + userId: { value: qExisting.ID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" }); } else { - // Create new user record + // New phone - create user record userUUID = replace(createUUID(), "-", "", "all"); queryTimed(" INSERT INTO Users ( @@ -142,9 +128,9 @@ try { try { smsResult = application.twilioObj.sendSMS( recipientNumber: "+1" & phone, - messageBody: "Your Payfrit verification code is: " & otp + messageBody: "Your Payfrit code is: " & otp ); - smsMessage = smsResult.success ? "Verification code sent" : "SMS failed - please try again"; + smsMessage = smsResult.success ? "Code sent" : "SMS failed - please try again"; } catch (any smsErr) { smsMessage = "SMS error: " & smsErr.message; } diff --git a/api/auth/verifyOTP.cfm b/api/auth/verifyOTP.cfm index 5704783..f44f3b5 100644 --- a/api/auth/verifyOTP.cfm +++ b/api/auth/verifyOTP.cfm @@ -5,14 +5,14 @@ /** - * Verify OTP and activate user account + * Unified OTP Verify: Works for both login and signup * * 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. + * - 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) { @@ -40,14 +40,12 @@ try { apiAbort({ "OK": false, "ERROR": "missing_fields", "MESSAGE": "UUID and OTP are required" }); } - // Find unverified user with matching UUID and OTP - // Magic OTP only bypasses Twilio SMS (in sendOTP.cfm), not OTP verification + // Find user with matching UUID and OTP (any verification status) qUser = queryTimed(" - SELECT ID, FirstName, LastName, EmailAddress, IsEmailVerified + SELECT ID, FirstName, LastName, EmailAddress, IsContactVerified, IsEmailVerified, IsActive FROM Users WHERE UUID = :uuid AND MobileVerifyCode = :otp - AND IsContactVerified = 0 LIMIT 1 ", { uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" }, @@ -57,7 +55,7 @@ try { if (qUser.recordCount == 0) { // Check if UUID exists but OTP is wrong qCheck = queryTimed(" - SELECT ID FROM Users WHERE UUID = :uuid AND IsContactVerified = 0 + SELECT ID FROM Users WHERE UUID = :uuid ", { uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" }); if (qCheck.recordCount > 0) { @@ -67,15 +65,29 @@ try { } } - // Clear the OTP code (one-time use) but DON'T mark as verified yet - // Account will be marked verified after profile completion - queryTimed(" - UPDATE Users - SET MobileVerifyCode = '' - WHERE ID = :userId - ", { userId: { value: qUser.ID, cfsqltype: "cf_sql_integer" } }, { datasource: "payfrit" }); + // Check if profile is complete (has first name) + needsProfile = !len(trim(qUser.FirstName)); - // Create auth token (needed for completeProfile call) + // 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) @@ -84,10 +96,6 @@ try { 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,