Handling Uploads
User avatars, team logos, image cropping, and client upload flows.
Avatar upload UI lives in apps/web/modules/saas/settings/components/AvatarUpload.tsx and CropImageDialog.tsx. The chooser accepts an image, and the canvas dialog lets the user crop and zoom before saving.
- The client calls
uploads.signedUploadUrlwithbucketandpath. - The server permits only the configured
NEXT_PUBLIC_AVATARS_BUCKET_NAMEand a path belonging to the user or a team they own. It records the upload contract and requests a signed PNG upload URL; the S3 URL expires after 60 seconds. - The browser sends the cropped file to that URL with
PUT. - The client calls
uploads.finalizeUploadwith the path; the server fetches the stored bytes and image-validates them (PNG/JPEG/WebP, 4 MiB, 16-megapixel cap). - Only after finalization succeeds does the account or team form persist the path through
auth.updateorteam.update. Both records useavatarUrl.
The generic storage helper does not perform the caller's authorization checks. Reuse the existing procedures instead of exposing arbitrary bucket and path signing to the browser.
Feedback attachments
uploads.signedFeedbackUploadUrl validates supported image types and matching extensions, records a server-owned upload contract (one-hour expiry, per-actor daily quota), and returns { uploadUrl, fileUrl, path, uploadId }. The fileUrl is a signed reference, not an unrestricted public object URL. After PUT, the client calls uploads.finalizeUpload with the uploadId; only VERIFIED uploads can be attached, exactly once, via feedback.create({ uploadId }). Expired or already-claimed ids are rejected, and abandoned records are swept by scripts/prune-uploads.mjs. Feedback attachment routes enforce access before issuing downloads; keep the underlying bucket private.
The server record keeps the issuing user or team, provider and bucket, expected content type, verification result, dimensions, byte size, expiry, and the feedback claim. Reissuing an avatar upload resets its verification state only when the same owner requests the same approved path. Clients cannot change that ownership or attach another actor's upload by replacing a path or ID.
Development mock storage writes temporary local files and supports signed reads, with one-hour retention and a 100-entry cap. Its HTTP routes reject production-mode requests. Use a real local S3-compatible service to verify provider writes, CORS and subsequent reads.