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:
@@ -1,4 +1,10 @@
|
||||
import type { CreateFlowUploadPurpose } from "./createFlowUploadPurpose";
|
||||
import {
|
||||
CreateFlowUploadValidationError,
|
||||
messageKeyForCreateFlowUploadReason,
|
||||
validateCreateFlowUploadFile,
|
||||
type CreateFlowUploadValidationReason,
|
||||
} from "./createFlowUploadValidation";
|
||||
|
||||
export type UploadToServerResult = {
|
||||
url: string;
|
||||
@@ -7,6 +13,20 @@ export type UploadToServerResult = {
|
||||
byteLength: number;
|
||||
};
|
||||
|
||||
const VALIDATION_REASONS = new Set<CreateFlowUploadValidationReason>([
|
||||
"empty",
|
||||
"tooLarge",
|
||||
"svg",
|
||||
"invalidType",
|
||||
"undecodable",
|
||||
]);
|
||||
|
||||
function reasonFromUnknown(value: unknown): CreateFlowUploadValidationReason | null {
|
||||
return typeof value === "string" && VALIDATION_REASONS.has(value as CreateFlowUploadValidationReason)
|
||||
? (value as CreateFlowUploadValidationReason)
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticated multipart upload to `POST /api/uploads`.
|
||||
* Caller must have a session cookie (same-origin fetch).
|
||||
@@ -15,6 +35,11 @@ export async function uploadCreateFlowFile(
|
||||
file: File,
|
||||
purpose: CreateFlowUploadPurpose,
|
||||
): Promise<UploadToServerResult> {
|
||||
const validated = await validateCreateFlowUploadFile(file, purpose);
|
||||
if (validated.ok === false) {
|
||||
throw new CreateFlowUploadValidationError(validated.reason);
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("purpose", purpose);
|
||||
formData.append("file", file);
|
||||
@@ -36,14 +61,24 @@ export async function uploadCreateFlowFile(
|
||||
if (body && typeof body === "object" && "error" in body) {
|
||||
const e = (body as {
|
||||
error?: { message?: string; code?: string };
|
||||
details?: { reason?: unknown };
|
||||
}).error;
|
||||
if (!e) return { message: null as string | null, code: null as string | null };
|
||||
const details = (body as { details?: { reason?: unknown } }).details;
|
||||
const reason = reasonFromUnknown(details?.reason);
|
||||
if (!e) {
|
||||
return {
|
||||
message: null as string | null,
|
||||
code: null as string | null,
|
||||
reason,
|
||||
};
|
||||
}
|
||||
return {
|
||||
message: typeof e.message === "string" ? e.message : null,
|
||||
code: typeof e.code === "string" ? e.code : null,
|
||||
reason,
|
||||
};
|
||||
}
|
||||
return { message: null, code: null };
|
||||
return { message: null, code: null, reason: null };
|
||||
})();
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -54,7 +89,7 @@ export async function uploadCreateFlowFile(
|
||||
? "UNAUTHORIZED"
|
||||
: "UPLOAD_FAILED";
|
||||
const code = errParts.code ?? errParts.message ?? fallback;
|
||||
throw new UploadToServerError(res.status, code);
|
||||
throw new UploadToServerError(res.status, code, errParts.reason);
|
||||
}
|
||||
|
||||
const data = body as {
|
||||
@@ -83,11 +118,34 @@ export async function uploadCreateFlowFile(
|
||||
export class UploadToServerError extends Error {
|
||||
readonly status: number;
|
||||
readonly code: string;
|
||||
readonly reason: CreateFlowUploadValidationReason | null;
|
||||
|
||||
constructor(status: number, code: string) {
|
||||
constructor(
|
||||
status: number,
|
||||
code: string,
|
||||
reason: CreateFlowUploadValidationReason | null = null,
|
||||
) {
|
||||
super(code);
|
||||
this.name = "UploadToServerError";
|
||||
this.status = status;
|
||||
this.code = code;
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
export function createFlowUploadFailureMessageKey(err: unknown): string {
|
||||
if (err instanceof CreateFlowUploadValidationError) {
|
||||
return messageKeyForCreateFlowUploadReason(err.reason);
|
||||
}
|
||||
if (err instanceof UploadToServerError) {
|
||||
if (err.reason) return messageKeyForCreateFlowUploadReason(err.reason);
|
||||
if (err.status === 413) return "errors.tooLarge";
|
||||
if (err.status === 401) return "errors.unauthorized";
|
||||
if (err.code === "server_misconfigured") {
|
||||
return "errors.misconfigured";
|
||||
}
|
||||
}
|
||||
return "errors.generic";
|
||||
}
|
||||
|
||||
export { CreateFlowUploadValidationError };
|
||||
|
||||
Reference in New Issue
Block a user