Code 123456 will work for any phone number to facilitate App Store review testing. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
265 lines
9.1 KiB
Text
265 lines
9.1 KiB
Text
<cfscript>
|
|
/**
|
|
* Environment Configuration
|
|
*
|
|
* Auto-detects dev vs production based on server hostname.
|
|
* No manual flag to change — safe to deploy via git pull to either server.
|
|
*/
|
|
|
|
// ============================================
|
|
// ENVIRONMENT FLAG - AUTO-DETECTED
|
|
// ============================================
|
|
isDevEnvironment = (createObject("java", "java.net.InetAddress").getLocalHost().getHostName() != "biz");
|
|
|
|
// ============================================
|
|
// 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 / App Store review)
|
|
// ============================================
|
|
// Enabled on both dev and production for App Store review
|
|
// Code 123456 will work for any phone number
|
|
application.MAGIC_OTP_ENABLED = true;
|
|
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;
|
|
}
|
|
|
|
// ============================================
|
|
// PERFORMANCE PROFILING
|
|
// ============================================
|
|
application.perfEnabled = true;
|
|
|
|
if (!structKeyExists(application, "perfBuffer")) {
|
|
application.perfBuffer = [];
|
|
}
|
|
|
|
/**
|
|
* Drop-in replacement for queryExecute() that tracks query count and time.
|
|
* Opt-in: use in endpoints where you want accurate DB time breakdown.
|
|
*/
|
|
function queryTimed(required string sql, any params = [], struct options = {}) {
|
|
if (!structKeyExists(request, "_perf_queryCount")) {
|
|
request._perf_queryCount = 0;
|
|
request._perf_queryTimeMs = 0;
|
|
}
|
|
|
|
var t = getTickCount();
|
|
var result = queryExecute(arguments.sql, arguments.params, arguments.options);
|
|
var elapsed = getTickCount() - t;
|
|
|
|
request._perf_queryCount++;
|
|
request._perf_queryTimeMs += elapsed;
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Flush the in-memory perf buffer to the ApiPerfLogs MySQL table.
|
|
* Thread-safe: duplicates buffer under lock, then inserts outside lock.
|
|
*/
|
|
function flushPerfBuffer() {
|
|
// Auto-create table if it doesn't exist
|
|
if (!structKeyExists(application, "_perfTableChecked")) {
|
|
try {
|
|
queryExecute("
|
|
CREATE TABLE IF NOT EXISTS ApiPerfLogs (
|
|
ID INT AUTO_INCREMENT PRIMARY KEY,
|
|
Endpoint VARCHAR(255) NOT NULL,
|
|
TotalMs INT NOT NULL DEFAULT 0,
|
|
DbMs INT NOT NULL DEFAULT 0,
|
|
AppMs INT NOT NULL DEFAULT 0,
|
|
QueryCount INT NOT NULL DEFAULT 0,
|
|
ResponseBytes INT NOT NULL DEFAULT 0,
|
|
BusinessID INT NOT NULL DEFAULT 0,
|
|
UserID INT NOT NULL DEFAULT 0,
|
|
LoggedAt DATETIME NOT NULL,
|
|
INDEX idx_loggedat (LoggedAt),
|
|
INDEX idx_endpoint (Endpoint)
|
|
) ENGINE=InnoDB
|
|
", {}, { datasource: "payfrit" });
|
|
application._perfTableChecked = true;
|
|
} catch (any e) {}
|
|
}
|
|
|
|
var batch = [];
|
|
|
|
lock name="payfrit_perfBuffer" timeout="2" type="exclusive" {
|
|
if (structKeyExists(application, "perfBuffer")) {
|
|
batch = duplicate(application.perfBuffer);
|
|
application.perfBuffer = [];
|
|
}
|
|
}
|
|
|
|
if (arrayLen(batch) == 0) return;
|
|
|
|
try {
|
|
|
|
var sql = "INSERT INTO ApiPerfLogs (Endpoint, TotalMs, DbMs, AppMs, QueryCount, ResponseBytes, BusinessID, UserID, LoggedAt) VALUES ";
|
|
var rows = [];
|
|
|
|
for (var m in batch) {
|
|
arrayAppend(rows,
|
|
"('" & replace(m.endpoint, "'", "''", "all") & "',"
|
|
& val(m.totalMs) & "," & val(m.dbMs) & "," & val(m.appMs) & ","
|
|
& val(m.queryCount) & "," & val(m.responseBytes) & ","
|
|
& val(m.businessId) & "," & val(m.userId) & ",'"
|
|
& m.loggedAt & "')"
|
|
);
|
|
}
|
|
|
|
sql &= arrayToList(rows, ",");
|
|
queryExecute(sql, {}, { datasource: "payfrit" });
|
|
|
|
// Cleanup old data (1% chance per flush)
|
|
if (randRange(1, 100) == 1) {
|
|
queryExecute(
|
|
"DELETE FROM ApiPerfLogs WHERE LoggedAt < DATE_SUB(NOW(), INTERVAL 30 DAY)",
|
|
{}, { datasource: "payfrit" }
|
|
);
|
|
}
|
|
} catch (any e) {
|
|
// Silent fail - never break the app for profiling
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log performance metrics for the current request.
|
|
* Called automatically from apiAbort().
|
|
*/
|
|
function logPerf(numeric responseBytes = 0) {
|
|
if (!structKeyExists(application, "perfEnabled") || !application.perfEnabled) return;
|
|
if (!structKeyExists(request, "_perf_start")) return;
|
|
|
|
// Safety valve: don't let buffer grow unbounded if flush is failing
|
|
if (structKeyExists(application, "perfBuffer") && arrayLen(application.perfBuffer) > 1000) return;
|
|
|
|
var totalMs = getTickCount() - request._perf_start;
|
|
var dbMs = structKeyExists(request, "_perf_queryTimeMs") ? request._perf_queryTimeMs : 0;
|
|
|
|
var metric = {
|
|
endpoint: structKeyExists(request, "_api_path") ? request._api_path : "unknown",
|
|
totalMs: totalMs,
|
|
dbMs: dbMs,
|
|
appMs: totalMs - dbMs,
|
|
queryCount: structKeyExists(request, "_perf_queryCount") ? request._perf_queryCount : 0,
|
|
responseBytes: arguments.responseBytes,
|
|
businessId: structKeyExists(request, "BusinessID") ? val(request.BusinessID) : 0,
|
|
userId: structKeyExists(request, "UserID") ? val(request.UserID) : 0,
|
|
loggedAt: dateTimeFormat(now(), "yyyy-mm-dd HH:nn:ss")
|
|
};
|
|
|
|
var shouldFlush = false;
|
|
|
|
lock name="payfrit_perfBuffer" timeout="1" type="exclusive" {
|
|
arrayAppend(application.perfBuffer, metric);
|
|
if (arrayLen(application.perfBuffer) >= 100) {
|
|
shouldFlush = true;
|
|
}
|
|
}
|
|
|
|
if (shouldFlush) {
|
|
thread name="perfFlush_#createUUID()#" {
|
|
flushPerfBuffer();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Store in application scope
|
|
application.isDevEnvironment = isDevEnvironment;
|
|
application.baseUrl = isDevEnvironment ? "https://dev.payfrit.com" : "https://biz.payfrit.com";
|
|
application.isDev = isDev;
|
|
application.logDebug = logDebug;
|
|
application.apiError = apiError;
|
|
application.queryTimed = queryTimed;
|
|
application.logPerf = logPerf;
|
|
application.flushPerfBuffer = flushPerfBuffer;
|
|
</cfscript>
|