Keep a guest community logo on review and on the published rule.

Sign-in was clearing the staged file before it uploaded, and review only read the server URL, so a chosen logo disappeared after the upload step.
This commit is contained in:
adilallo committed 2026-11-05 20:02:40 -07:00
1 parent 2348a9e94f
commit 8212423ec3
19 files changed
+780 -61

No files matched your search

+35 -11
View File
@@ -31,6 +31,17 @@ function isPurpose(x: string): x is CreateFlowUploadPurpose {
return x === "communityAvatar" || x === "customMethodAttachment";
}
function clientIp(request: NextRequest): string {
return (
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ??
request.headers.get("x-real-ip") ??
"unknown"
);
}
/** Same spacing as a signed-in upload, keyed by IP so a guest cannot fill the disk. */
const ANONYMOUS_COMMUNITY_AVATAR_MIN_INTERVAL_MS = 5_000;
function messageForValidationReason(
reason: CreateFlowUploadValidationReason,
): string {
@@ -54,7 +65,19 @@ export const POST = apiRoute("uploads.post", async (request: NextRequest) => {
}
const user = await getSessionUser();
if (!user) {
let formData: FormData | null = null;
try {
formData = await request.formData();
} catch {
formData = null;
}
const purposeRaw = formData?.get("purpose");
// A guest publishes before they have a session. Only the community logo
// can be stored without one; attachments stay signed-in. A body that
// cannot be parsed is not a logo upload.
if (!user && purposeRaw !== "communityAvatar") {
return unauthorized();
}
@@ -64,15 +87,7 @@ export const POST = apiRoute("uploads.post", async (request: NextRequest) => {
);
}
const rl = rateLimitKey(`upload:${user.id}`, 5_000);
if (rl.ok === false) {
return rateLimited(rl.retryAfterMs);
}
let formData: FormData;
try {
formData = await request.formData();
} catch {
if (!formData) {
return errorJson(
"payload_too_large",
"Upload body is too large or malformed.",
@@ -80,9 +95,18 @@ export const POST = apiRoute("uploads.post", async (request: NextRequest) => {
);
}
const purposeRaw = formData.get("purpose");
const file = asUploadedBlob(formData.get("file"));
const rl = user
? rateLimitKey(`upload:${user.id}`, 5_000)
: rateLimitKey(
`upload-anon:${clientIp(request)}`,
ANONYMOUS_COMMUNITY_AVATAR_MIN_INTERVAL_MS,
);
if (rl.ok === false) {
return rateLimited(rl.retryAfterMs);
}
if (typeof purposeRaw !== "string" || !isPurpose(purposeRaw)) {
return errorJson(
"validation_error",