feat : 6 astra improve it
CI / Verify and audit (push) Successful in 2m33s
CI / Build, scan and deploy immutable images (push) Failing after 1m29s

This commit is contained in:
2026-09-22 18:28:18 +07:00 Unverified
parent 88afa8e947
commit 87e6bcd96f
56 changed files with 1351 additions and 511 deletions
+27 -19
View File
@@ -1,29 +1,37 @@
import { errorResponse, requireSameOrigin, withUploadSlot } from "@/lib/security/http";
import { limitRequest } from "@/lib/security/rate-limit";
import * as z from "zod";
import { requireAdmin } from "@/lib/auth/server";
import { completePendingMedia } from "@/lib/media/repository";
export async function POST(
_request: Request,
request: Request,
context: { params: Promise<{ id: string }> },
) {
await requireAdmin();
const { id } = await context.params;
if (!z.string().uuid().safeParse(id).success) {
return Response.json({ error: "invalid-id" }, { status: 400 });
try {
requireSameOrigin(request);
const admin = await requireAdmin();
await limitRequest("media", admin.user.id, 20);
const { id } = await context.params;
if (!z.string().uuid().safeParse(id).success) {
return Response.json({ error: "invalid-id" }, { status: 400 });
}
const result = await withUploadSlot(() => completePendingMedia(id));
if (result.status === "ready") {
return Response.json({
media: {
id: result.media.id,
fileName: result.media.fileName,
mimeType: result.media.mimeType,
byteSize: result.media.byteSize,
url: `/media/${result.media.id}`,
},
});
}
const status = result.status === "not-found" ? 404 : result.status === "missing" ? 409 : 422;
return Response.json({ error: result.status }, { status });
} catch (cause) {
return errorResponse(cause);
}
const result = await completePendingMedia(id);
if (result.status === "ready") {
return Response.json({
media: {
id: result.media.id,
fileName: result.media.fileName,
mimeType: result.media.mimeType,
byteSize: result.media.byteSize,
url: `/media/${result.media.id}`,
},
});
}
const status = result.status === "not-found" ? 404 : result.status === "missing" ? 409 : 422;
return Response.json({ error: result.status }, { status });
}
+17 -9
View File
@@ -1,19 +1,27 @@
import { errorResponse, requireSameOrigin } from "@/lib/security/http";
import { limitRequest } from "@/lib/security/rate-limit";
import * as z from "zod";
import { requireAdmin } from "@/lib/auth/server";
import { discardUnreferencedMedia } from "@/lib/media/repository";
export async function DELETE(
_request: Request,
request: Request,
context: { params: Promise<{ id: string }> },
) {
await requireAdmin();
const { id } = await context.params;
if (!z.string().uuid().safeParse(id).success) {
return Response.json({ error: "invalid-id" }, { status: 400 });
try {
requireSameOrigin(request);
const admin = await requireAdmin();
await limitRequest("media", admin.user.id, 20);
const { id } = await context.params;
if (!z.string().uuid().safeParse(id).success) {
return Response.json({ error: "invalid-id" }, { status: 400 });
}
const deleted = await discardUnreferencedMedia(id);
return deleted
? new Response(null, { status: 204 })
: Response.json({ error: "media-is-referenced-or-missing" }, { status: 409 });
} catch (cause) {
return errorResponse(cause);
}
const deleted = await discardUnreferencedMedia(id);
return deleted
? new Response(null, { status: 204 })
: Response.json({ error: "media-is-referenced-or-missing" }, { status: 409 });
}
+27 -24
View File
@@ -1,32 +1,35 @@
import { errorResponse, requireSameOrigin, readJson } from "@/lib/security/http";
import { limitRequest } from "@/lib/security/rate-limit";
import { requireAdmin } from "@/lib/auth/server";
import { createPendingMedia } from "@/lib/media/repository";
import { mediaUploadSchema } from "@/lib/media/validation";
export async function POST(request: Request) {
const admin = await requireAdmin();
let input: unknown;
try {
input = await request.json();
} catch {
return Response.json({ error: "invalid-json" }, { status: 400 });
}
const parsed = mediaUploadSchema.safeParse(input);
if (!parsed.success) {
return Response.json(
{ error: "invalid-media", issues: parsed.error.issues.map((issue) => issue.message) },
{ status: 400 },
);
}
try {
const upload = await createPendingMedia(parsed.data, admin.user.id);
return Response.json({
mediaId: upload.id,
uploadUrl: upload.uploadUrl,
method: "PUT",
headers: { "Content-Type": parsed.data.mimeType },
expiresIn: upload.expiresIn,
});
} catch {
return Response.json({ error: "storage-unavailable" }, { status: 503 });
requireSameOrigin(request);
const admin = await requireAdmin();
await limitRequest("media", admin.user.id, 20);
const input = await readJson(request);
const parsed = mediaUploadSchema.safeParse(input);
if (!parsed.success) {
return Response.json(
{ error: "invalid-media", issues: parsed.error.issues.map((issue) => issue.message) },
{ status: 400 },
);
}
try {
const upload = await createPendingMedia(parsed.data, admin.user.id);
return Response.json({
mediaId: upload.id,
uploadUrl: upload.uploadUrl,
method: "PUT",
headers: { "Content-Type": parsed.data.mimeType },
expiresIn: upload.expiresIn,
});
} catch {
return Response.json({ error: "storage-unavailable" }, { status: 503 });
}
} catch (cause) {
return errorResponse(cause);
}
}
+28 -13
View File
@@ -1,21 +1,36 @@
import { NextResponse } from "next/server";
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) {
const admin = await requireAdmin();
const form = await request.formData();
const file = form.get("file");
if (!(file instanceof File)) {
return NextResponse.json({ error: "กรุณาเลือกไฟล์รูปภาพ" }, { status: 400 });
}
try {
return NextResponse.json(await uploadPublicMedia(file, admin.user.id), { status: 201 });
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 NextResponse.json(
{ error: cause instanceof Error ? cause.message : "อัปโหลดไม่สำเร็จ" },
{ status: 400 },
);
return errorResponse(cause);
}
}