Add button and custom modal flow implemented
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { NextResponse } from "next/server";
|
||||
import { apiRoute } from "../../../../lib/server/apiRoute";
|
||||
import {
|
||||
notFound,
|
||||
serverMisconfigured,
|
||||
} from "../../../../lib/server/responses";
|
||||
import { resolveUploadedFileById } from "../../../../lib/server/uploads/resolveUploadedFile";
|
||||
import { getUploadRootFromEnv } from "../../../../lib/server/uploads/uploadRoot";
|
||||
|
||||
type RouteContext = { params: Promise<{ id: string }> };
|
||||
|
||||
/**
|
||||
* Public read for opaque upload ids (no auth). Unguessable UUID stem;
|
||||
* do not use for sensitive documents without revisiting policy.
|
||||
*/
|
||||
export const GET = apiRoute<RouteContext>(
|
||||
"uploads.byId",
|
||||
async (_request, context) => {
|
||||
if (!getUploadRootFromEnv()) {
|
||||
return serverMisconfigured(
|
||||
"File uploads are not configured (UPLOAD_ROOT is unset).",
|
||||
);
|
||||
}
|
||||
|
||||
const { id } = await context.params;
|
||||
const resolved = await resolveUploadedFileById(id);
|
||||
if (!resolved) {
|
||||
return notFound("Upload not found");
|
||||
}
|
||||
|
||||
const body = await readFile(resolved.absolutePath);
|
||||
return new NextResponse(new Uint8Array(body), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": resolved.contentType,
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,111 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { isDatabaseConfigured } from "../../../lib/server/env";
|
||||
import {
|
||||
dbUnavailable,
|
||||
errorJson,
|
||||
serverMisconfigured,
|
||||
unauthorized,
|
||||
rateLimited,
|
||||
} from "../../../lib/server/responses";
|
||||
import { getSessionUser } from "../../../lib/server/session";
|
||||
import { apiRoute } from "../../../lib/server/apiRoute";
|
||||
import { rateLimitKey } from "../../../lib/server/rateLimit";
|
||||
import { saveCreateFlowUpload } from "../../../lib/server/uploads/saveCreateFlowUpload";
|
||||
import { getUploadRootFromEnv } from "../../../lib/server/uploads/uploadRoot";
|
||||
import {
|
||||
CREATE_FLOW_UPLOAD_MAX_BYTES,
|
||||
type CreateFlowUploadPurpose,
|
||||
} from "../../../lib/server/uploads/uploadConstants";
|
||||
|
||||
function isPurpose(x: string): x is CreateFlowUploadPurpose {
|
||||
return x === "communityAvatar" || x === "customMethodAttachment";
|
||||
}
|
||||
|
||||
export const POST = apiRoute("uploads.post", async (request: NextRequest) => {
|
||||
if (!isDatabaseConfigured()) {
|
||||
return dbUnavailable();
|
||||
}
|
||||
|
||||
const user = await getSessionUser();
|
||||
if (!user) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
if (!getUploadRootFromEnv()) {
|
||||
return serverMisconfigured(
|
||||
"File uploads are not configured (UPLOAD_ROOT is unset).",
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
return errorJson(
|
||||
"payload_too_large",
|
||||
"Upload body is too large or malformed.",
|
||||
413,
|
||||
);
|
||||
}
|
||||
|
||||
const purposeRaw = formData.get("purpose");
|
||||
const file = formData.get("file");
|
||||
|
||||
if (typeof purposeRaw !== "string" || !isPurpose(purposeRaw)) {
|
||||
return errorJson(
|
||||
"validation_error",
|
||||
"Invalid or missing `purpose` (expected communityAvatar | customMethodAttachment).",
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
if (!(file instanceof File)) {
|
||||
return errorJson(
|
||||
"validation_error",
|
||||
"Missing `file` field (multipart file).",
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
if (file.size > CREATE_FLOW_UPLOAD_MAX_BYTES) {
|
||||
return errorJson(
|
||||
"payload_too_large",
|
||||
`File exceeds maximum allowed size (${CREATE_FLOW_UPLOAD_MAX_BYTES} bytes).`,
|
||||
413,
|
||||
);
|
||||
}
|
||||
|
||||
const buf = Buffer.from(await file.arrayBuffer());
|
||||
const mimeType = file.type || "application/octet-stream";
|
||||
|
||||
const saved = await saveCreateFlowUpload({
|
||||
purpose: purposeRaw,
|
||||
buffer: buf,
|
||||
mimeType,
|
||||
});
|
||||
|
||||
if ("error" in saved) {
|
||||
if (saved.error === "misconfigured") {
|
||||
return serverMisconfigured(
|
||||
"File uploads are not configured (UPLOAD_ROOT is unset).",
|
||||
);
|
||||
}
|
||||
return errorJson(
|
||||
"validation_error",
|
||||
"File type or size is not allowed for this upload purpose.",
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
url: saved.urlPath,
|
||||
id: saved.id,
|
||||
mimeType: saved.mimeType,
|
||||
byteLength: saved.byteLength,
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user