feat : add audit logs
This commit is contained in:
+51
-34
@@ -9,6 +9,7 @@ import {
|
||||
deleteGlossaryAlias,
|
||||
} from "@/lib/glossary-repository";
|
||||
import { requireAdmin } from "@/lib/auth/server";
|
||||
import { auditActor } from "@/lib/audit-log";
|
||||
import { publicGuideSections } from "@/lib/guides/public-sections";
|
||||
import { publicationIssues } from "@/lib/guides/publication";
|
||||
import { getAdminGuideContentById } from "@/lib/guides/queries";
|
||||
@@ -47,10 +48,11 @@ type GuideActionResult =
|
||||
type CreateGuideActionResult = Exclude<GuideActionResult, { status: "conflict" }>;
|
||||
type SaveIntent = { overwrite?: boolean };
|
||||
|
||||
function overwriteDetails(admin: Awaited<ReturnType<typeof requireAdmin>>, intent?: SaveIntent) {
|
||||
return intent?.overwrite
|
||||
? { operation: "overwrite" as const, actorName: (admin.user.name?.trim() || admin.user.email).slice(0, 100) }
|
||||
: undefined;
|
||||
function mutationContext(
|
||||
admin: Awaited<ReturnType<typeof requireAdmin>>,
|
||||
intent?: SaveIntent,
|
||||
) {
|
||||
return { actor: auditActor(admin.user), overwrite: intent?.overwrite };
|
||||
}
|
||||
|
||||
async function runGuideAction(action: () => Promise<{ version?: number }>): Promise<GuideActionResult> {
|
||||
@@ -69,9 +71,9 @@ async function runGuideAction(action: () => Promise<{ version?: number }>): Prom
|
||||
}
|
||||
|
||||
export async function createStructuredGuide(input: unknown): Promise<CreateGuideActionResult> {
|
||||
await requireAdmin();
|
||||
const admin = await requireAdmin();
|
||||
try {
|
||||
const guide = await createGuide(input);
|
||||
const guide = await createGuide(input, mutationContext(admin));
|
||||
return { status: "saved", version: guide.version, location: `/admin/${guide.characterKey}` };
|
||||
} catch (cause) {
|
||||
return { status: "error", message: cause instanceof Error ? cause.message : "สร้าง Guide ไม่สำเร็จ" };
|
||||
@@ -80,27 +82,27 @@ export async function createStructuredGuide(input: unknown): Promise<CreateGuide
|
||||
|
||||
export async function saveWeaponSection(guideId: string, input: unknown, intent?: SaveIntent) {
|
||||
const admin = await requireAdmin();
|
||||
return runGuideAction(() => saveWeapons(guideId, input, overwriteDetails(admin, intent)));
|
||||
return runGuideAction(() => saveWeapons(guideId, input, mutationContext(admin, intent)));
|
||||
}
|
||||
|
||||
export async function saveGuideOverview(guideId: string, input: unknown, intent?: SaveIntent) {
|
||||
const admin = await requireAdmin();
|
||||
return runGuideAction(() => updateGuideBasics(guideId, input, overwriteDetails(admin, intent)));
|
||||
return runGuideAction(() => updateGuideBasics(guideId, input, mutationContext(admin, intent)));
|
||||
}
|
||||
|
||||
export async function saveArtifactSection(guideId: string, input: unknown, intent?: SaveIntent) {
|
||||
const admin = await requireAdmin();
|
||||
return runGuideAction(() => saveArtifacts(guideId, input, overwriteDetails(admin, intent)));
|
||||
return runGuideAction(() => saveArtifacts(guideId, input, mutationContext(admin, intent)));
|
||||
}
|
||||
|
||||
export async function saveConstellationSection(guideId: string, input: unknown, intent?: SaveIntent) {
|
||||
const admin = await requireAdmin();
|
||||
return runGuideAction(() => saveConstellations(guideId, input, overwriteDetails(admin, intent)));
|
||||
return runGuideAction(() => saveConstellations(guideId, input, mutationContext(admin, intent)));
|
||||
}
|
||||
|
||||
export async function saveTeamSection(guideId: string, input: unknown, intent?: SaveIntent) {
|
||||
const admin = await requireAdmin();
|
||||
return runGuideAction(() => saveTeams(guideId, input, overwriteDetails(admin, intent)));
|
||||
return runGuideAction(() => saveTeams(guideId, input, mutationContext(admin, intent)));
|
||||
}
|
||||
|
||||
export async function listTeamImportGuides(currentGuideId: string) {
|
||||
@@ -129,9 +131,13 @@ export async function loadTeamImportGuide(guideId: string) {
|
||||
}
|
||||
|
||||
export async function createGuideExtra(guideId: string, input: unknown) {
|
||||
await requireAdmin();
|
||||
const admin = await requireAdmin();
|
||||
try {
|
||||
const section = await createExtraSection(z.string().uuid().parse(guideId), input);
|
||||
const section = await createExtraSection(
|
||||
z.string().uuid().parse(guideId),
|
||||
input,
|
||||
mutationContext(admin),
|
||||
);
|
||||
updateTag(pageCacheTag(guideId));
|
||||
return {
|
||||
status: "saved" as const,
|
||||
@@ -160,18 +166,19 @@ export async function saveGuideExtra(
|
||||
const parsedGuideId = z.string().uuid().parse(guideId);
|
||||
const parsedSectionId = z.string().uuid().parse(sectionId);
|
||||
const result = await runGuideAction(() =>
|
||||
saveExtraSection(parsedGuideId, parsedSectionId, input, overwriteDetails(admin, intent)),
|
||||
saveExtraSection(parsedGuideId, parsedSectionId, input, mutationContext(admin, intent)),
|
||||
);
|
||||
if (result.status === "saved") updateTag(pageCacheTag(guideId));
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function deleteGuideExtra(guideId: string, sectionId: string) {
|
||||
await requireAdmin();
|
||||
const admin = await requireAdmin();
|
||||
const result = await runGuideAction(() =>
|
||||
deleteExtraSection(
|
||||
z.string().uuid().parse(guideId),
|
||||
z.string().uuid().parse(sectionId),
|
||||
mutationContext(admin),
|
||||
),
|
||||
);
|
||||
if (result.status === "saved") updateTag(pageCacheTag(guideId));
|
||||
@@ -179,36 +186,40 @@ export async function deleteGuideExtra(guideId: string, sectionId: string) {
|
||||
}
|
||||
|
||||
export async function reorderGuideExtras(guideId: string, input: unknown) {
|
||||
await requireAdmin();
|
||||
const admin = await requireAdmin();
|
||||
const result = await runGuideAction(() =>
|
||||
reorderExtraSections(z.string().uuid().parse(guideId), input),
|
||||
reorderExtraSections(
|
||||
z.string().uuid().parse(guideId),
|
||||
input,
|
||||
mutationContext(admin),
|
||||
),
|
||||
);
|
||||
if (result.status === "saved") updateTag(pageCacheTag(guideId));
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function moveGuideToTrash(guideId: string) {
|
||||
await requireAdmin();
|
||||
return runGuideAction(() => trashGuide(guideId));
|
||||
const admin = await requireAdmin();
|
||||
return runGuideAction(() => trashGuide(guideId, mutationContext(admin)));
|
||||
}
|
||||
|
||||
export async function restoreStructuredGuide(guideId: string) {
|
||||
await requireAdmin();
|
||||
return runGuideAction(() => restoreGuide(guideId));
|
||||
const admin = await requireAdmin();
|
||||
return runGuideAction(() => restoreGuide(guideId, mutationContext(admin)));
|
||||
}
|
||||
|
||||
export async function setStructuredGuideVisibility(
|
||||
guideId: string,
|
||||
input: unknown,
|
||||
) {
|
||||
await requireAdmin();
|
||||
const admin = await requireAdmin();
|
||||
const parsed = z.strictObject({
|
||||
isPublic: z.boolean(),
|
||||
expectedVersion: z.number().int().positive(),
|
||||
}).safeParse(input);
|
||||
if (!parsed.success) return { status: "error" as const, message: "ข้อมูลการเผยแพร่ไม่ถูกต้อง" };
|
||||
if (!parsed.data.isPublic) {
|
||||
return runGuideAction(() => setGuideVisibility(guideId, parsed.data));
|
||||
return runGuideAction(() => setGuideVisibility(guideId, parsed.data, mutationContext(admin)));
|
||||
}
|
||||
|
||||
const guide = await getAdminGuideContentById(guideId);
|
||||
@@ -223,18 +234,21 @@ export async function setStructuredGuideVisibility(
|
||||
if (issues.length) {
|
||||
return { status: "error" as const, message: issues.join(" • "), issues };
|
||||
}
|
||||
return runGuideAction(() => setGuideVisibility(guideId, parsed.data));
|
||||
return runGuideAction(() => setGuideVisibility(guideId, parsed.data, mutationContext(admin)));
|
||||
}
|
||||
|
||||
export async function deleteStructuredGuide(guideId: string) {
|
||||
await requireAdmin();
|
||||
return runGuideAction(() => permanentlyDeleteGuide(guideId));
|
||||
const admin = await requireAdmin();
|
||||
return runGuideAction(() => permanentlyDeleteGuide(guideId, mutationContext(admin)));
|
||||
}
|
||||
|
||||
export async function startCatalogSync() {
|
||||
await requireAdmin();
|
||||
const admin = await requireAdmin();
|
||||
try {
|
||||
return { status: "started" as const, job: await startCatalogSyncJob() };
|
||||
return {
|
||||
status: "started" as const,
|
||||
job: await startCatalogSyncJob(auditActor(admin.user)),
|
||||
};
|
||||
} catch (cause) {
|
||||
return { status: "error" as const, message: cause instanceof Error ? cause.message : "เริ่ม Sync ไม่สำเร็จ" };
|
||||
}
|
||||
@@ -246,14 +260,14 @@ export async function getCatalogSyncProgress(jobId: string) {
|
||||
}
|
||||
|
||||
export async function cancelCatalogSync(jobId: string) {
|
||||
await requireAdmin();
|
||||
return cancelCatalogSyncJob(jobId);
|
||||
const admin = await requireAdmin();
|
||||
return cancelCatalogSyncJob(jobId, auditActor(admin.user));
|
||||
}
|
||||
|
||||
export async function addGlossaryAlias(input: unknown) {
|
||||
await requireAdmin();
|
||||
const admin = await requireAdmin();
|
||||
try {
|
||||
const alias = await createGlossaryAlias(input);
|
||||
const alias = await createGlossaryAlias(input, auditActor(admin.user));
|
||||
updateTag(GLOSSARY_CACHE_TAG);
|
||||
return { status: "saved" as const, alias };
|
||||
} catch (cause) {
|
||||
@@ -269,9 +283,12 @@ export async function addGlossaryAlias(input: unknown) {
|
||||
}
|
||||
|
||||
export async function removeGlossaryAlias(id: string) {
|
||||
await requireAdmin();
|
||||
const admin = await requireAdmin();
|
||||
try {
|
||||
await deleteGlossaryAlias(z.string().uuid().parse(id));
|
||||
await deleteGlossaryAlias(
|
||||
z.string().uuid().parse(id),
|
||||
auditActor(admin.user),
|
||||
);
|
||||
updateTag(GLOSSARY_CACHE_TAG);
|
||||
return { status: "saved" as const };
|
||||
} catch (cause) {
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { connection } from "next/server";
|
||||
import { ClipboardListIcon } from "lucide-react";
|
||||
|
||||
import { AdminHeader } from "@/components/admin/admin-header";
|
||||
import { AuditLogFilters } from "@/components/admin/audit-log-filters";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Empty,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/components/ui/empty";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
AUDIT_ACTION_LABELS,
|
||||
AUDIT_TARGET_LABELS,
|
||||
AUDIT_TARGET_TYPES,
|
||||
isAuditTargetType,
|
||||
listAuditActors,
|
||||
listAuditLogs,
|
||||
type AuditAction,
|
||||
} from "@/lib/audit-log";
|
||||
import { getAdminSession } from "@/lib/auth/server";
|
||||
|
||||
function pageHref(
|
||||
page: number,
|
||||
actorId?: string,
|
||||
targetType?: string,
|
||||
) {
|
||||
const query = new URLSearchParams({ page: String(page) });
|
||||
if (actorId) query.set("actor", actorId);
|
||||
if (targetType) query.set("type", targetType);
|
||||
return `/admin/activity?${query}`;
|
||||
}
|
||||
|
||||
function eventSummary(metadata: Record<string, string | number | boolean | null>) {
|
||||
const label = metadata.sectionTitle
|
||||
?? metadata.guideName
|
||||
?? metadata.alias
|
||||
?? metadata.fileName
|
||||
?? metadata.accountLabel;
|
||||
if (typeof label === "string") return label;
|
||||
if (typeof metadata.error === "string") return metadata.error;
|
||||
return null;
|
||||
}
|
||||
|
||||
export default async function AuditLogPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||
}) {
|
||||
await connection();
|
||||
if (!(await getAdminSession())) redirect("/admin/login");
|
||||
const query = await searchParams;
|
||||
const rawPage = Array.isArray(query.page) ? query.page[0] : query.page;
|
||||
const parsedPage = Number(rawPage);
|
||||
const page = Number.isSafeInteger(parsedPage) && parsedPage > 0
|
||||
? parsedPage
|
||||
: 1;
|
||||
const rawActorId = Array.isArray(query.actor) ? query.actor[0] : query.actor;
|
||||
const actorId = rawActorId && rawActorId.length <= 128
|
||||
? rawActorId
|
||||
: undefined;
|
||||
const rawTargetType = Array.isArray(query.type) ? query.type[0] : query.type;
|
||||
const targetType = isAuditTargetType(rawTargetType)
|
||||
? rawTargetType
|
||||
: undefined;
|
||||
const [{ events, pageCount, total }, actors] = await Promise.all([
|
||||
listAuditLogs({ page, pageSize: 50, actorId, targetType }),
|
||||
listAuditActors(),
|
||||
]);
|
||||
const dateTime = new Intl.DateTimeFormat("th-TH", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "medium",
|
||||
timeZone: "Asia/Bangkok",
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-svh">
|
||||
<AdminHeader />
|
||||
<main className="mx-auto flex w-full max-w-7xl flex-col gap-6 p-4 sm:p-6 lg:p-8">
|
||||
<header>
|
||||
<p className="text-sm font-medium text-primary">Admin audit trail</p>
|
||||
<h1 className="font-heading text-3xl font-semibold">Activity</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
ประวัติการเปลี่ยนแปลงที่บันทึกสำเร็จทั้งหมด {total.toLocaleString("th-TH")} รายการ
|
||||
</p>
|
||||
</header>
|
||||
<AuditLogFilters
|
||||
key={`${actorId ?? "all"}:${targetType ?? "all"}`}
|
||||
actors={actors}
|
||||
targetTypes={AUDIT_TARGET_TYPES.map((value) => ({
|
||||
value,
|
||||
label: AUDIT_TARGET_LABELS[value],
|
||||
}))}
|
||||
initialActorId={actorId}
|
||||
initialTargetType={targetType}
|
||||
/>
|
||||
{events.length ? (
|
||||
<>
|
||||
<div className="rounded-xl border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>เวลา</TableHead>
|
||||
<TableHead>ผู้ดูแล</TableHead>
|
||||
<TableHead>การทำงาน</TableHead>
|
||||
<TableHead>เป้าหมาย</TableHead>
|
||||
<TableHead>รายละเอียด</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{events.map((event) => {
|
||||
const action = event.action as AuditAction;
|
||||
const summary = eventSummary(event.metadata);
|
||||
return (
|
||||
<TableRow key={event.id}>
|
||||
<TableCell>{dateTime.format(event.createdAt)}</TableCell>
|
||||
<TableCell>{event.actorLabel}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{AUDIT_ACTION_LABELS[action] ?? event.action}</span>
|
||||
{event.metadata.overwrite === true ? (
|
||||
<Badge variant="outline">Overwrite</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-1">
|
||||
<Badge variant="secondary">
|
||||
{isAuditTargetType(event.targetType)
|
||||
? AUDIT_TARGET_LABELS[event.targetType]
|
||||
: event.targetType}
|
||||
</Badge>
|
||||
<span className="max-w-48 truncate font-mono text-xs text-muted-foreground">
|
||||
{event.targetId}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex max-w-80 flex-col gap-1">
|
||||
{summary ? <span className="truncate">{summary}</span> : null}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{event.scope ? `${event.scope} · ` : ""}
|
||||
{event.resultingVersion
|
||||
? `v${event.resultingVersion}`
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{pageCount > 1 ? (
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
{page > 1 ? (
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
href={pageHref(page - 1, actorId, targetType)}
|
||||
text="ก่อนหน้า"
|
||||
/>
|
||||
</PaginationItem>
|
||||
) : null}
|
||||
{page < pageCount ? (
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
href={pageHref(page + 1, actorId, targetType)}
|
||||
text="ถัดไป"
|
||||
/>
|
||||
</PaginationItem>
|
||||
) : null}
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<Empty className="border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<ClipboardListIcon />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>ยังไม่มีกิจกรรม</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
ลองเปลี่ยนตัวกรอง หรือกลับมาหลังจากมีการแก้ไขข้อมูล
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,10 @@ import { z } from "zod";
|
||||
import { getDb } from "@/db";
|
||||
import { users } from "@/db/schema";
|
||||
import { getAuth, requireAdmin } from "@/lib/auth/server";
|
||||
import { securityLog } from "@/lib/security/http";
|
||||
import {
|
||||
auditActor,
|
||||
writeAuditLogBestEffort,
|
||||
} from "@/lib/audit-log";
|
||||
|
||||
export interface CreateAccountState {
|
||||
status: "idle" | "error" | "success";
|
||||
@@ -45,7 +48,7 @@ export async function createAccount(
|
||||
}
|
||||
|
||||
try {
|
||||
await getAuth().api.createUser({
|
||||
const created = await getAuth().api.createUser({
|
||||
body: {
|
||||
name: parsed.data.name,
|
||||
email: parsed.data.email,
|
||||
@@ -55,12 +58,19 @@ export async function createAccount(
|
||||
},
|
||||
headers: await headers(),
|
||||
});
|
||||
await writeAuditLogBestEffort(auditActor(admin.user), {
|
||||
action: "admin_account.created",
|
||||
targetType: "admin_account",
|
||||
targetId: created.user.id,
|
||||
metadata: {
|
||||
accountLabel: created.user.name?.trim() || created.user.email,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return { status: "error", message: "สร้างบัญชีไม่สำเร็จ โปรดลองอีกครั้ง" };
|
||||
}
|
||||
|
||||
revalidatePath("/admin/register");
|
||||
securityLog("account-created", { actorId: admin.user.id });
|
||||
return { status: "success", message: `สร้างบัญชี ${parsed.data.email} แล้ว` };
|
||||
}
|
||||
|
||||
@@ -75,16 +85,30 @@ export async function removeAccount(
|
||||
return { status: "error", message: "ไม่สามารถลบบัญชีที่กำลังใช้งานอยู่" };
|
||||
}
|
||||
|
||||
const [target] = await getDb()
|
||||
.select({ name: users.name, email: users.email })
|
||||
.from(users)
|
||||
.where(eq(users.id, userId))
|
||||
.limit(1);
|
||||
if (!target) return { status: "error", message: "ไม่พบบัญชีนี้" };
|
||||
|
||||
try {
|
||||
await getAuth().api.removeUser({
|
||||
body: { userId },
|
||||
headers: await headers(),
|
||||
});
|
||||
await writeAuditLogBestEffort(auditActor(session.user), {
|
||||
action: "admin_account.removed",
|
||||
targetType: "admin_account",
|
||||
targetId: userId,
|
||||
metadata: {
|
||||
accountLabel: target.name.trim() || target.email,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return { status: "error", message: "ลบบัญชีไม่สำเร็จ โปรดลองอีกครั้ง" };
|
||||
}
|
||||
|
||||
revalidatePath("/admin/register");
|
||||
securityLog("account-removed", { actorId: session.user.id, targetId: userId });
|
||||
return { status: "success" };
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as z from "zod";
|
||||
|
||||
import { requireAdmin } from "@/lib/auth/server";
|
||||
import { completePendingMedia } from "@/lib/media/repository";
|
||||
import { auditActor } from "@/lib/audit-log";
|
||||
|
||||
export async function POST(
|
||||
request: Request,
|
||||
@@ -17,7 +18,9 @@ export async function POST(
|
||||
if (!z.string().uuid().safeParse(id).success) {
|
||||
return Response.json({ error: "invalid-id" }, { status: 400 });
|
||||
}
|
||||
const result = await withUploadSlot(() => completePendingMedia(id));
|
||||
const result = await withUploadSlot(() =>
|
||||
completePendingMedia(id, auditActor(admin.user)),
|
||||
);
|
||||
if (result.status === "ready") {
|
||||
return Response.json({
|
||||
media: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as z from "zod";
|
||||
|
||||
import { requireAdmin } from "@/lib/auth/server";
|
||||
import { discardUnreferencedMedia } from "@/lib/media/repository";
|
||||
import { auditActor } from "@/lib/audit-log";
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
@@ -17,7 +18,7 @@ export async function DELETE(
|
||||
if (!z.string().uuid().safeParse(id).success) {
|
||||
return Response.json({ error: "invalid-id" }, { status: 400 });
|
||||
}
|
||||
const deleted = await discardUnreferencedMedia(id);
|
||||
const deleted = await discardUnreferencedMedia(id, auditActor(admin.user));
|
||||
return deleted
|
||||
? new Response(null, { status: 204 })
|
||||
: Response.json({ error: "media-is-referenced-or-missing" }, { status: 409 });
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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 { boundedBody, errorResponse, HttpError, requireSameOrigin, withUploadSlot } from "@/lib/security/http";
|
||||
import { limitRequest } from "@/lib/security/rate-limit";
|
||||
import { auditActor } from "@/lib/audit-log";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
@@ -26,8 +27,7 @@ export async function POST(request: Request) {
|
||||
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 });
|
||||
const result = await uploadPublicMedia(file, auditActor(admin.user));
|
||||
return Response.json(result, { status: 201, headers: { "Cache-Control": "no-store" } });
|
||||
});
|
||||
} catch (cause) {
|
||||
|
||||
Reference in New Issue
Block a user