Restore the community photo after reload and reject empty, oversized, SVG, and spoofed uploads.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-09-10 15:50:32 -06:00
co-authored by Cursor
parent 6ccc1e8c8e
commit 234f3998ad
18 changed files with 1072 additions and 112 deletions
@@ -16,14 +16,24 @@ import { CREATE_FLOW_MD_UP_COLUMN_MAX_CLASS } from "../../components/createFlowL
import { fetchAuthSession } from "../../../../../lib/create/api";
import { ASSETS, getAssetPath } from "../../../../../lib/assetUtils";
import {
UploadToServerError,
createFlowUploadFailureMessageKey,
uploadCreateFlowFile,
} from "../../../../../lib/create/uploadToServer";
import {
COMMUNITY_AVATAR_ACCEPT,
messageKeyForCreateFlowUploadReason,
validateCreateFlowUploadFile,
} from "../../../../../lib/create/createFlowUploadValidation";
import {
clearPendingCommunityAvatarFile,
readPendingCommunityAvatarFile,
storePendingCommunityAvatarFile,
} from "../../../../../lib/create/pendingCommunityAvatarUpload";
function hasCommunityAvatarUrl(url: string | undefined): boolean {
return typeof url === "string" && url.trim().length > 0;
}
/** Create Community — Figma Flow — Upload `20094:41524`. */
export function CommunityUploadScreen() {
const m = useMessages();
@@ -54,17 +64,57 @@ export function CommunityUploadScreen() {
[localPreviewUrl],
);
const resolveUploadError = useCallback(
(err: unknown) => {
if (err instanceof UploadToServerError) {
if (err.status === 413) return tUpload("errors.tooLarge");
if (err.status === 401) return tUpload("errors.unauthorized");
if (err.code === "server_misconfigured") {
return tUpload("errors.misconfigured");
const serverAvatarUrl = hasCommunityAvatarUrl(state.communityAvatarUrl)
? state.communityAvatarUrl!.trim()
: null;
const serverAvatarUrlRef = useRef(serverAvatarUrl);
serverAvatarUrlRef.current = serverAvatarUrl;
useEffect(() => {
if (serverAvatarUrl) {
setLocalPreviewUrl((prev) => {
if (prev) URL.revokeObjectURL(prev);
return null;
});
}
}, [serverAvatarUrl]);
useEffect(() => {
let cancelled = false;
void (async () => {
try {
const file = await readPendingCommunityAvatarFile();
if (cancelled || !file) return;
const validated = await validateCreateFlowUploadFile(
file,
"communityAvatar",
);
if (validated.ok === false) {
await clearPendingCommunityAvatarFile();
return;
}
if (cancelled) return;
if (serverAvatarUrlRef.current) return;
const objectUrl = URL.createObjectURL(file);
if (cancelled) {
URL.revokeObjectURL(objectUrl);
return;
}
setLocalPreviewUrl((prev) => {
if (prev) URL.revokeObjectURL(prev);
return objectUrl;
});
} catch {
// Missing IndexedDB / quota: leave the picker empty.
}
return tUpload("errors.generic");
},
})();
return () => {
cancelled = true;
};
}, []);
const resolveUploadError = useCallback(
(err: unknown) => tUpload(createFlowUploadFailureMessageKey(err)),
[tUpload],
);
@@ -94,6 +144,16 @@ export function CommunityUploadScreen() {
}
if (signedIn === false) {
const validated = await validateCreateFlowUploadFile(
file,
"communityAvatar",
);
if (validated.ok === false) {
setErrorMessage(
tUpload(messageKeyForCreateFlowUploadReason(validated.reason)),
);
return;
}
try {
await storePendingCommunityAvatarFile(file);
setLocalPreviewUrl((prev) => {
@@ -121,24 +181,16 @@ export function CommunityUploadScreen() {
if (prev) URL.revokeObjectURL(prev);
return null;
});
if (
typeof state.communityAvatarUrl === "string" &&
state.communityAvatarUrl.trim().length > 0
) {
if (hasCommunityAvatarUrl(state.communityAvatarUrl)) {
updateState({ communityAvatarUrl: undefined });
}
// Clear any anonymous staged blob so the post-sign-in flush won't resurrect it.
void clearPendingCommunityAvatarFile();
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}, [markCreateFlowInteraction, state.communityAvatarUrl, updateState]);
const displaySrc =
typeof state.communityAvatarUrl === "string" &&
state.communityAvatarUrl.trim().length > 0
? state.communityAvatarUrl.trim()
: localPreviewUrl;
const displaySrc = serverAvatarUrl ?? localPreviewUrl;
const hasPreview = typeof displaySrc === "string" && displaySrc.length > 0;
return (
@@ -161,7 +213,7 @@ export function CommunityUploadScreen() {
type="file"
className="sr-only"
tabIndex={-1}
accept="image/jpeg,image/png,image/webp,image/gif"
accept={COMMUNITY_AVATAR_ACCEPT}
aria-label={u.hintText}
onChange={handleFileChange}
/>