37 lines
1.7 KiB
TypeScript
37 lines
1.7 KiB
TypeScript
import { requireAdmin } from "@/lib/auth/server";
|
|
import { uploadPublicMedia } from "@/lib/media/repository";
|
|
import { MAX_MEDIA_BYTES, mediaUploadSchema } from "@/lib/media/validation";
|
|
import { boundedBody, errorResponse, HttpError, requireSameOrigin, securityLog, withUploadSlot } from "@/lib/security/http";
|
|
import { limitRequest } from "@/lib/security/rate-limit";
|
|
|
|
export async function POST(request: Request) {
|
|
try {
|
|
requireSameOrigin(request);
|
|
const admin = await requireAdmin();
|
|
await limitRequest("media", admin.user.id, 20);
|
|
if (!request.headers.get("content-type")?.toLowerCase().startsWith("multipart/form-data;")) {
|
|
throw new HttpError(415, "expected-multipart");
|
|
}
|
|
return await withUploadSlot(async () => {
|
|
let form: FormData;
|
|
try {
|
|
form = await boundedBody(request, MAX_MEDIA_BYTES + 64 * 1024).formData();
|
|
} catch (cause) {
|
|
if (cause instanceof HttpError) throw cause;
|
|
throw new HttpError(400, "invalid-form");
|
|
}
|
|
const file = form.get("file");
|
|
if (!(file instanceof File)) throw new HttpError(400, "missing-image");
|
|
if (file.size > MAX_MEDIA_BYTES) throw new HttpError(413, "image-too-large");
|
|
if (!mediaUploadSchema.safeParse({ fileName: file.name, mimeType: file.type, byteSize: file.size }).success) {
|
|
throw new HttpError(415, "invalid-image");
|
|
}
|
|
const result = await uploadPublicMedia(file, admin.user.id);
|
|
securityLog("media-uploaded", { actorId: admin.user.id, targetId: result.id });
|
|
return Response.json(result, { status: 201, headers: { "Cache-Control": "no-store" } });
|
|
});
|
|
} catch (cause) {
|
|
return errorResponse(cause);
|
|
}
|
|
}
|