Add dev environment configuration and tools
- api/config/environment.cfm: Central config for dev vs prod settings - Verbose errors, debug logging, magic OTP bypass - Rate limiting toggle, email catch-all, token expiry settings - api/dev/: Development-only endpoints - seedData.cfm: Create/reset test users - timeTravel.cfm: Manipulate timestamps for testing - index.cfm: Dev tools index Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8e3bb681e7
commit
05cf73446f
5 changed files with 542 additions and 0 deletions
|
|
@ -31,6 +31,8 @@
|
||||||
datasource="payfrit"
|
datasource="payfrit"
|
||||||
showdebugoutput="false"
|
showdebugoutput="false"
|
||||||
>
|
>
|
||||||
|
<!--- Preserve struct key casing in JSON serialization (Flutter expects mixed-case keys) --->
|
||||||
|
<cfset getApplicationSettings().serialization.preserveCaseForStructKey = true>
|
||||||
|
|
||||||
<!--- Magic OTP bypass for App Store review (set to true to enable 123456 as universal OTP) --->
|
<!--- Magic OTP bypass for App Store review (set to true to enable 123456 as universal OTP) --->
|
||||||
<cfset application.MAGIC_OTP_ENABLED = false>
|
<cfset application.MAGIC_OTP_ENABLED = false>
|
||||||
|
|
@ -49,6 +51,9 @@
|
||||||
<!--- Stripe Configuration (loads from config/stripe.cfm) --->
|
<!--- Stripe Configuration (loads from config/stripe.cfm) --->
|
||||||
<cfinclude template="config/stripe.cfm">
|
<cfinclude template="config/stripe.cfm">
|
||||||
|
|
||||||
|
<!--- Environment Configuration (dev vs prod settings) --->
|
||||||
|
<cfinclude template="config/environment.cfm">
|
||||||
|
|
||||||
<cfscript>
|
<cfscript>
|
||||||
function apiAbort(payload) {
|
function apiAbort(payload) {
|
||||||
writeOutput(serializeJSON(payload));
|
writeOutput(serializeJSON(payload));
|
||||||
|
|
@ -145,6 +150,15 @@ if (len(request._api_path)) {
|
||||||
if (findNoCase("/api/tasks/callserver", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/tasks/callserver", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/tasks/createChat.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/tasks/createChat.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/tasks/expirestalechats.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/tasks/expirestalechats.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/tasks/listCategories.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/tasks/saveCategory.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/tasks/deleteCategory.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/tasks/seedCategories.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/tasks/listAllTypes.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/tasks/listTypes.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/tasks/saveType.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/tasks/deleteType.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/tasks/reorderTypes.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
||||||
// Chat endpoints
|
// Chat endpoints
|
||||||
if (findNoCase("/api/chat/getMessages.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/chat/getMessages.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
@ -240,6 +254,10 @@ if (len(request._api_path)) {
|
||||||
// Stations endpoints
|
// Stations endpoints
|
||||||
if (findNoCase("/api/stations/list.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/stations/list.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
||||||
|
// Ratings endpoints (setup + token-based submission)
|
||||||
|
if (findNoCase("/api/ratings/setup.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
if (findNoCase("/api/ratings/submit.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
||||||
// Stripe endpoints
|
// Stripe endpoints
|
||||||
if (findNoCase("/api/stripe/onboard.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/stripe/onboard.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
if (findNoCase("/api/stripe/status.cfm", request._api_path)) request._api_isPublic = true;
|
if (findNoCase("/api/stripe/status.cfm", request._api_path)) request._api_isPublic = true;
|
||||||
|
|
|
||||||
124
api/config/environment.cfm
Normal file
124
api/config/environment.cfm
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Environment Configuration
|
||||||
|
*
|
||||||
|
* Controls dev vs production behavior.
|
||||||
|
* This file should have DIFFERENT values on dev vs biz servers.
|
||||||
|
*
|
||||||
|
* dev.payfrit.com: isDevEnvironment = true
|
||||||
|
* biz.payfrit.com: isDevEnvironment = false
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ENVIRONMENT FLAG - CHANGE PER SERVER
|
||||||
|
// ============================================
|
||||||
|
isDevEnvironment = true; // Set to FALSE on biz.payfrit.com
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// ERROR HANDLING
|
||||||
|
// ============================================
|
||||||
|
// Dev: Show full stack traces
|
||||||
|
// Prod: Show generic "server error" message
|
||||||
|
application.showDetailedErrors = isDevEnvironment;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// DEBUG LOGGING
|
||||||
|
// ============================================
|
||||||
|
// Dev: Log all API requests/responses
|
||||||
|
// Prod: Minimal logging
|
||||||
|
application.debugLogging = isDevEnvironment;
|
||||||
|
application.logDirectory = expandPath("/api/logs/");
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// RATE LIMITING
|
||||||
|
// ============================================
|
||||||
|
// Dev: No rate limits
|
||||||
|
// Prod: Enforce rate limits
|
||||||
|
application.enableRateLimiting = !isDevEnvironment;
|
||||||
|
application.rateLimitPerMinute = 60; // requests per minute per IP
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// EMAIL HANDLING
|
||||||
|
// ============================================
|
||||||
|
// Dev: Send all emails to catch-all address
|
||||||
|
// Prod: Send to real recipients
|
||||||
|
application.emailCatchAll = isDevEnvironment ? "dev-emails@payfrit.com" : "";
|
||||||
|
application.emailEnabled = !isDevEnvironment ? true : false; // Disable emails on dev entirely
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// MAGIC OTP (for testing)
|
||||||
|
// ============================================
|
||||||
|
// Dev: Allow magic phone number to bypass OTP
|
||||||
|
// Prod: Disabled
|
||||||
|
application.MAGIC_OTP_ENABLED = isDevEnvironment;
|
||||||
|
application.MAGIC_OTP_CODE = "123456";
|
||||||
|
application.MAGIC_PHONE_NUMBERS = ["5555555555", "0000000000"];
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// STRIPE MODE
|
||||||
|
// ============================================
|
||||||
|
// Already handled in stripe.cfm, but good to have reference
|
||||||
|
application.stripeTestMode = isDevEnvironment;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// API RESPONSE EXTRAS
|
||||||
|
// ============================================
|
||||||
|
// Dev: Include debug info in API responses (timing, queries, etc)
|
||||||
|
// Prod: Minimal responses
|
||||||
|
application.includeDebugInResponse = isDevEnvironment;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// SESSION/TOKEN SETTINGS
|
||||||
|
// ============================================
|
||||||
|
// Dev: Longer token expiry for easier testing
|
||||||
|
// Prod: Normal expiry
|
||||||
|
application.tokenExpiryHours = isDevEnvironment ? 720 : 24; // 30 days vs 1 day
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// HELPER FUNCTIONS
|
||||||
|
// ============================================
|
||||||
|
function isDev() {
|
||||||
|
return structKeyExists(application, "isDevEnvironment") && application.isDevEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
function logDebug(message, data = {}) {
|
||||||
|
if (!application.debugLogging) return;
|
||||||
|
|
||||||
|
var logFile = application.logDirectory & "debug_" & dateFormat(now(), "yyyy-mm-dd") & ".log";
|
||||||
|
var logLine = "[" & timeFormat(now(), "HH:mm:ss") & "] " & message;
|
||||||
|
|
||||||
|
if (!structIsEmpty(data)) {
|
||||||
|
logLine &= " | " & serializeJSON(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!directoryExists(application.logDirectory)) {
|
||||||
|
directoryCreate(application.logDirectory);
|
||||||
|
}
|
||||||
|
fileAppend(logFile, logLine & chr(10));
|
||||||
|
} catch (any e) {
|
||||||
|
// Silent fail - don't break app if logging fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function apiError(message, detail = "", statusCode = 500) {
|
||||||
|
var response = {
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": application.showDetailedErrors ? message : "An error occurred"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (application.showDetailedErrors && len(detail)) {
|
||||||
|
response["DETAIL"] = detail;
|
||||||
|
response["STACK"] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store in application scope
|
||||||
|
application.isDevEnvironment = isDevEnvironment;
|
||||||
|
application.isDev = isDev;
|
||||||
|
application.logDebug = logDebug;
|
||||||
|
application.apiError = apiError;
|
||||||
|
</cfscript>
|
||||||
45
api/dev/index.cfm
Normal file
45
api/dev/index.cfm
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Dev Tools Index - DEV ONLY
|
||||||
|
* Lists available development endpoints
|
||||||
|
*/
|
||||||
|
|
||||||
|
// SAFETY: Only allow on dev environment
|
||||||
|
if (!structKeyExists(application, "isDevEnvironment") || !application.isDevEnvironment) {
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "forbidden",
|
||||||
|
"MESSAGE": "Dev tools are disabled on this server"
|
||||||
|
}));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"environment": "development",
|
||||||
|
"endpoints": {
|
||||||
|
"seedData": {
|
||||||
|
"url": "/api/dev/seedData.cfm",
|
||||||
|
"methods": ["GET", "POST"],
|
||||||
|
"description": "Create and manage test users",
|
||||||
|
"actions": ["seed", "reset"]
|
||||||
|
},
|
||||||
|
"timeTravel": {
|
||||||
|
"url": "/api/dev/timeTravel.cfm",
|
||||||
|
"methods": ["POST"],
|
||||||
|
"description": "Manipulate timestamps for testing",
|
||||||
|
"actions": ["expireTokens", "setUserCreated", "clearOTPs", "resetUser"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"magicOTP": application.MAGIC_OTP_CODE,
|
||||||
|
"magicPhones": application.MAGIC_PHONE_NUMBERS,
|
||||||
|
"debugLogging": application.debugLogging,
|
||||||
|
"showDetailedErrors": application.showDetailedErrors,
|
||||||
|
"tokenExpiryHours": application.tokenExpiryHours
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
</cfscript>
|
||||||
209
api/dev/seedData.cfm
Normal file
209
api/dev/seedData.cfm
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Seed Data Script - DEV ONLY
|
||||||
|
*
|
||||||
|
* Creates test users and data for development testing.
|
||||||
|
* This endpoint is disabled on production.
|
||||||
|
*
|
||||||
|
* GET: Returns current seed data info
|
||||||
|
* POST: { "action": "seed" } - Creates seed data
|
||||||
|
* POST: { "action": "reset" } - Clears all test data and re-seeds
|
||||||
|
*/
|
||||||
|
|
||||||
|
function apiAbort(required struct payload) {
|
||||||
|
writeOutput(serializeJSON(payload));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: Only allow on dev environment
|
||||||
|
if (!structKeyExists(application, "isDevEnvironment") || !application.isDevEnvironment) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "forbidden",
|
||||||
|
"MESSAGE": "This endpoint is only available in development"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 createTestUser(phone, firstName, lastName, email, isVerified = true) {
|
||||||
|
var userUUID = replace(createUUID(), "-", "", "all");
|
||||||
|
|
||||||
|
queryExecute("
|
||||||
|
INSERT INTO Users (
|
||||||
|
UserContactNumber,
|
||||||
|
UserUUID,
|
||||||
|
UserFirstName,
|
||||||
|
UserLastName,
|
||||||
|
UserEmail,
|
||||||
|
UserIsContactVerified,
|
||||||
|
UserIsEmailVerified,
|
||||||
|
UserIsActive,
|
||||||
|
UserAddedOn,
|
||||||
|
UserPassword,
|
||||||
|
UserPromoCode,
|
||||||
|
UserMobileVerifyCode
|
||||||
|
) VALUES (
|
||||||
|
:phone,
|
||||||
|
:uuid,
|
||||||
|
:firstName,
|
||||||
|
:lastName,
|
||||||
|
:email,
|
||||||
|
:isVerified,
|
||||||
|
0,
|
||||||
|
:isVerified,
|
||||||
|
:addedOn,
|
||||||
|
'',
|
||||||
|
:promoCode,
|
||||||
|
'123456'
|
||||||
|
)
|
||||||
|
", {
|
||||||
|
phone: { value: phone, cfsqltype: "cf_sql_varchar" },
|
||||||
|
uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" },
|
||||||
|
firstName: { value: firstName, cfsqltype: "cf_sql_varchar" },
|
||||||
|
lastName: { value: lastName, cfsqltype: "cf_sql_varchar" },
|
||||||
|
email: { value: email, cfsqltype: "cf_sql_varchar" },
|
||||||
|
isVerified: { value: isVerified ? 1 : 0, cfsqltype: "cf_sql_integer" },
|
||||||
|
addedOn: { value: now(), cfsqltype: "cf_sql_timestamp" },
|
||||||
|
promoCode: { value: randRange(1000000, 9999999), cfsqltype: "cf_sql_varchar" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
return userUUID;
|
||||||
|
}
|
||||||
|
|
||||||
|
function seedTestData() {
|
||||||
|
var created = [];
|
||||||
|
|
||||||
|
// Test User 1: Magic phone (always works with OTP 123456)
|
||||||
|
try {
|
||||||
|
createTestUser("5555555555", "Magic", "User", "magic@test.payfrit.com", true);
|
||||||
|
arrayAppend(created, "Magic User (5555555555)");
|
||||||
|
} catch (any e) {
|
||||||
|
if (e.message contains "Duplicate") {
|
||||||
|
arrayAppend(created, "Magic User already exists");
|
||||||
|
} else {
|
||||||
|
arrayAppend(created, "Magic User failed: " & e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test User 2: Regular verified user
|
||||||
|
try {
|
||||||
|
createTestUser("5551234567", "Test", "Customer", "test@test.payfrit.com", true);
|
||||||
|
arrayAppend(created, "Test Customer (5551234567)");
|
||||||
|
} catch (any e) {
|
||||||
|
if (e.message contains "Duplicate") {
|
||||||
|
arrayAppend(created, "Test Customer already exists");
|
||||||
|
} else {
|
||||||
|
arrayAppend(created, "Test Customer failed: " & e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test User 3: Unverified user (for testing signup flow)
|
||||||
|
try {
|
||||||
|
createTestUser("5559876543", "", "", "", false);
|
||||||
|
arrayAppend(created, "Unverified User (5559876543)");
|
||||||
|
} catch (any e) {
|
||||||
|
if (e.message contains "Duplicate") {
|
||||||
|
arrayAppend(created, "Unverified User already exists");
|
||||||
|
} else {
|
||||||
|
arrayAppend(created, "Unverified User failed: " & e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetTestData() {
|
||||||
|
// Delete test users (by phone prefix 555)
|
||||||
|
queryExecute("
|
||||||
|
DELETE FROM Users
|
||||||
|
WHERE UserContactNumber LIKE '555%'
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
return seedTestData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTestDataInfo() {
|
||||||
|
var qUsers = queryExecute("
|
||||||
|
SELECT UserID, UserContactNumber, UserFirstName, UserLastName,
|
||||||
|
UserIsContactVerified, UserUUID
|
||||||
|
FROM Users
|
||||||
|
WHERE UserContactNumber LIKE '555%'
|
||||||
|
ORDER BY UserContactNumber
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
var users = [];
|
||||||
|
for (var row in qUsers) {
|
||||||
|
arrayAppend(users, {
|
||||||
|
"phone": row.UserContactNumber,
|
||||||
|
"name": trim(row.UserFirstName & " " & row.UserLastName),
|
||||||
|
"verified": row.UserIsContactVerified == 1,
|
||||||
|
"uuid": row.UserUUID
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
requestMethod = cgi.request_method;
|
||||||
|
|
||||||
|
if (requestMethod == "GET") {
|
||||||
|
// Return current test data info
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"testUsers": getTestDataInfo(),
|
||||||
|
"magicOTP": application.MAGIC_OTP_CODE,
|
||||||
|
"magicPhones": application.MAGIC_PHONE_NUMBERS
|
||||||
|
}));
|
||||||
|
} else if (requestMethod == "POST") {
|
||||||
|
data = readJsonBody();
|
||||||
|
action = structKeyExists(data, "action") ? lcase(data.action) : "seed";
|
||||||
|
|
||||||
|
if (action == "reset") {
|
||||||
|
created = resetTestData();
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"action": "reset",
|
||||||
|
"created": created,
|
||||||
|
"testUsers": getTestDataInfo()
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
created = seedTestData();
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"action": "seed",
|
||||||
|
"created": created,
|
||||||
|
"testUsers": getTestDataInfo()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "method_not_allowed",
|
||||||
|
"MESSAGE": "Use GET to view or POST to seed"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": application.showDetailedErrors ? e.message : "An error occurred",
|
||||||
|
"DETAIL": application.showDetailedErrors ? e.detail : ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
146
api/dev/timeTravel.cfm
Normal file
146
api/dev/timeTravel.cfm
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
<cfsetting showdebugoutput="false">
|
||||||
|
<cfsetting enablecfoutputonly="true">
|
||||||
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||||
|
|
||||||
|
<cfscript>
|
||||||
|
/**
|
||||||
|
* Time Travel Endpoint - DEV ONLY
|
||||||
|
*
|
||||||
|
* Allows manipulation of timestamps for testing time-based features.
|
||||||
|
* Examples: token expiry, trial periods, scheduled events.
|
||||||
|
*
|
||||||
|
* POST: { "action": "expireTokens", "userId": 123 }
|
||||||
|
* POST: { "action": "setUserCreated", "userId": 123, "daysAgo": 30 }
|
||||||
|
*/
|
||||||
|
|
||||||
|
function apiAbort(required struct payload) {
|
||||||
|
writeOutput(serializeJSON(payload));
|
||||||
|
abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: Only allow on dev environment
|
||||||
|
if (!structKeyExists(application, "isDevEnvironment") || !application.isDevEnvironment) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "forbidden",
|
||||||
|
"MESSAGE": "This endpoint is only available in development"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if (cgi.request_method != "POST") {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "method_not_allowed",
|
||||||
|
"MESSAGE": "POST required"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
data = readJsonBody();
|
||||||
|
action = structKeyExists(data, "action") ? lcase(data.action) : "";
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "expiretokens":
|
||||||
|
// Expire all tokens for a user (simulate session timeout)
|
||||||
|
if (!structKeyExists(data, "userId")) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_userId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
queryExecute("
|
||||||
|
UPDATE UserTokens
|
||||||
|
SET CreatedAt = DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||||||
|
WHERE UserID = :userId
|
||||||
|
", {
|
||||||
|
userId: { value: data.userId, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"message": "Tokens expired for user " & data.userId
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "setusercreated":
|
||||||
|
// Backdate user creation (for testing trial periods, etc)
|
||||||
|
if (!structKeyExists(data, "userId") || !structKeyExists(data, "daysAgo")) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_userId_or_daysAgo" });
|
||||||
|
}
|
||||||
|
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Users
|
||||||
|
SET UserAddedOn = DATE_SUB(NOW(), INTERVAL :days DAY)
|
||||||
|
WHERE UserID = :userId
|
||||||
|
", {
|
||||||
|
userId: { value: data.userId, cfsqltype: "cf_sql_integer" },
|
||||||
|
days: { value: data.daysAgo, cfsqltype: "cf_sql_integer" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"message": "User " & data.userId & " created date set to " & data.daysAgo & " days ago"
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "clearotps":
|
||||||
|
// Clear all OTPs (force re-request)
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Users SET UserMobileVerifyCode = ''
|
||||||
|
", {}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"message": "All OTPs cleared"
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "resetuser":
|
||||||
|
// Reset a user to unverified state (for retesting signup)
|
||||||
|
if (!structKeyExists(data, "phone")) {
|
||||||
|
apiAbort({ "OK": false, "ERROR": "missing_phone" });
|
||||||
|
}
|
||||||
|
|
||||||
|
queryExecute("
|
||||||
|
UPDATE Users
|
||||||
|
SET UserIsContactVerified = 0,
|
||||||
|
UserIsActive = 0,
|
||||||
|
UserFirstName = NULL,
|
||||||
|
UserLastName = NULL,
|
||||||
|
UserMobileVerifyCode = '123456'
|
||||||
|
WHERE UserContactNumber = :phone
|
||||||
|
", {
|
||||||
|
phone: { value: data.phone, cfsqltype: "cf_sql_varchar" }
|
||||||
|
}, { datasource: "payfrit" });
|
||||||
|
|
||||||
|
writeOutput(serializeJSON({
|
||||||
|
"OK": true,
|
||||||
|
"message": "User with phone " & data.phone & " reset to unverified"
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "unknown_action",
|
||||||
|
"MESSAGE": "Valid actions: expireTokens, setUserCreated, clearOTPs, resetUser"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (any e) {
|
||||||
|
apiAbort({
|
||||||
|
"OK": false,
|
||||||
|
"ERROR": "server_error",
|
||||||
|
"MESSAGE": application.showDetailedErrors ? e.message : "An error occurred"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</cfscript>
|
||||||
Reference in a new issue