payfrit-works/api/auth/avatar.cfm
John Mizerek d8d7efe056 Add user account APIs and fix Lucee header handling
- Add avatar.cfm: GET/POST for user profile photos with multi-extension support
- Add profile.cfm: GET/POST for user profile (name, email, phone)
- Add history.cfm: Order history endpoint with pagination
- Add addresses/list.cfm and add.cfm: Delivery address management
- Add setOrderType.cfm: Set delivery/takeaway type on orders
- Add checkToken.cfm: Debug endpoint for token validation
- Fix headerValue() in Application.cfm to use servlet request object
  (Lucee CGI scope doesn't expose custom HTTP headers like X-User-Token)
- Update public allowlist for new endpoints
- Add privacy.html page

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 20:01:07 -08:00

182 lines
5.2 KiB
Text

<cfsetting showdebugoutput="false">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">
<cfheader name="Cache-Control" value="no-store">
<!---
Avatar Upload/Get API
GET: Returns avatar URL for authenticated user
POST: Uploads new avatar image (multipart form data)
Stores images as: /uploads/users/{UserID}.jpg
--->
<cfscript>
function apiAbort(required struct payload) {
writeOutput(serializeJSON(payload));
abort;
}
// Helper to get header value - use servlet request object (CGI scope doesn't expose custom HTTP headers in Lucee)
function getHeader(name) {
try {
req = getPageContext().getRequest();
val = req.getHeader(arguments.name);
if (!isNull(val)) return trim(val);
} catch (any e) {
// Fall back to CGI scope
k = "HTTP_" & ucase(reReplace(arguments.name, "[^A-Za-z0-9]", "_", "all"));
if (structKeyExists(cgi, k)) return trim(cgi[k]);
}
return "";
}
// Get authenticated user ID - try request scope first, then do our own token lookup
userId = 0;
debugInfo = {
"requestUserID": structKeyExists(request, "UserID") ? request.UserID : "not_set",
"headerToken": "",
"tokenLookupResult": "not_attempted"
};
if (structKeyExists(request, "UserID") && isNumeric(request.UserID) && request.UserID > 0) {
userId = request.UserID;
debugInfo.source = "request_scope";
} else {
// Do our own token lookup for multipart requests
userToken = getHeader("X-User-Token");
debugInfo.headerToken = len(userToken) ? left(userToken, 8) & "..." : "empty";
if (len(userToken)) {
try {
qTok = queryExecute(
"SELECT UserID FROM UserTokens WHERE Token = ? LIMIT 1",
[ { value = userToken, cfsqltype = "cf_sql_varchar" } ],
{ datasource = "payfrit" }
);
debugInfo.tokenLookupResult = "found_#qTok.recordCount#_records";
if (qTok.recordCount EQ 1) {
userId = qTok.UserID;
debugInfo.source = "token_lookup";
}
} catch (any e) {
debugInfo.tokenLookupResult = "error: " & e.message;
}
}
}
if (userId <= 0) {
apiAbort({ "OK": false, "ERROR": "not_logged_in", "MESSAGE": "Authentication required", "DEBUG": debugInfo });
}
// Use absolute path from web root
uploadsPath = expandPath("/uploads/users/");
// Check for avatar with various extensions (case-insensitive)
function findAvatarFile(basePath, userId) {
extensions = ["jpg", "jpeg", "png", "gif", "webp", "JPG", "JPEG", "PNG", "GIF", "WEBP"];
for (ext in extensions) {
testPath = basePath & userId & "." & ext;
if (fileExists(testPath)) {
return { "exists": true, "path": testPath, "filename": userId & "." & ext };
}
}
return { "exists": false, "path": basePath & userId & ".jpg", "filename": userId & ".jpg" };
}
avatarInfo = findAvatarFile(uploadsPath, userId);
avatarPath = avatarInfo.path;
avatarFilename = avatarInfo.filename;
avatarUrl = "https://biz.payfrit.com/uploads/users/" & avatarFilename;
// Handle GET - return current avatar URL
if (cgi.REQUEST_METHOD == "GET") {
hasAvatar = avatarInfo.exists;
writeOutput(serializeJSON({
"OK": true,
"HAS_AVATAR": hasAvatar,
"AVATAR_URL": hasAvatar ? (avatarUrl & "?t=" & getTickCount()) : ""
}));
abort;
}
// Handle POST - upload new avatar
if (cgi.REQUEST_METHOD == "POST") {
try {
// Check if file was uploaded
if (!structKeyExists(form, "avatar") || !len(form.avatar)) {
apiAbort({ "OK": false, "ERROR": "missing_file", "MESSAGE": "No avatar file provided" });
}
// Get uploaded file info
uploadedFile = form.avatar;
// Ensure uploads directory exists
if (!directoryExists(uploadsPath)) {
directoryCreate(uploadsPath);
}
// Process the upload
uploadResult = fileUpload(
destination = uploadsPath,
fileField = "avatar",
nameConflict = "overwrite",
accept = "image/jpeg,image/png,image/gif,image/webp"
);
// Rename to userId.jpg (convert if needed)
uploadedPath = uploadsPath & uploadResult.serverFile;
// Read the image and save as JPEG
try {
img = imageRead(uploadedPath);
// Resize if too large (max 500x500 for avatars)
if (img.width > 500 || img.height > 500) {
if (img.width > img.height) {
imageScaleToFit(img, 500, "");
} else {
imageScaleToFit(img, "", 500);
}
}
// Save as JPEG
imageWrite(img, avatarPath, 0.85);
// Delete original if different from target
if (uploadedPath != avatarPath && fileExists(uploadedPath)) {
fileDelete(uploadedPath);
}
} catch (any imgErr) {
// If image processing fails, just rename the file
if (uploadedPath != avatarPath) {
if (fileExists(avatarPath)) {
fileDelete(avatarPath);
}
fileMove(uploadedPath, avatarPath);
}
}
writeOutput(serializeJSON({
"OK": true,
"MESSAGE": "Avatar uploaded successfully",
"AVATAR_URL": avatarUrl & "?t=" & getTickCount()
}));
abort;
} catch (any e) {
apiAbort({
"OK": false,
"ERROR": "upload_error",
"MESSAGE": "Failed to upload avatar",
"DETAIL": e.message
});
}
}
// Unknown method
apiAbort({ "OK": false, "ERROR": "bad_method", "MESSAGE": "Use GET or POST" });
</cfscript>