phase 2 and 3

This commit is contained in:
jason
2026-04-21 08:56:51 -05:00
parent b98837a72c
commit d79aaf6ef8
42 changed files with 4962 additions and 19 deletions
+33
View File
@@ -0,0 +1,33 @@
import { NextResponse, type NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
import { errorResponse, ApiError } from "@/lib/api";
import { getSessionUser } from "@/lib/session";
import { mimeForKind, readFileBytes, type FileKind } from "@/lib/files";
// Any signed-in user can download; mime/extension derived from the asset.
export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
try {
const user = await getSessionUser();
if (!user) throw new ApiError(401, "unauthenticated", "Sign in required");
const { id } = await ctx.params;
const file = await prisma.fileAsset.findUnique({ where: { id } });
if (!file) throw new ApiError(404, "not_found", "File not found");
const bytes = await readFileBytes(file.path);
const mime = mimeForKind(file.kind as FileKind, file.mimeType);
const body = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
return new NextResponse(body as ArrayBuffer, {
status: 200,
headers: {
"content-type": mime,
"content-length": String(bytes.byteLength),
"content-disposition": `inline; filename="${encodeURIComponent(file.originalName)}"`,
"cache-control": "private, max-age=3600",
},
});
} catch (err) {
return errorResponse(err);
}
}
+64
View File
@@ -0,0 +1,64 @@
import { type NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
import { ok, errorResponse, requireRole, ApiError } from "@/lib/api";
import { deleteFileFromDisk } from "@/lib/files";
import { audit } from "@/lib/audit";
import { clientIp } from "@/lib/request";
export async function GET(_req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
try {
await requireRole("admin");
const { id } = await ctx.params;
const file = await prisma.fileAsset.findUnique({ where: { id } });
if (!file) throw new ApiError(404, "not_found", "File not found");
return ok({ file });
} catch (err) {
return errorResponse(err);
}
}
export async function DELETE(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
try {
const user = await requireRole("admin");
const { id } = await ctx.params;
const file = await prisma.fileAsset.findUnique({
where: { id },
include: {
_count: {
select: { partStep: true, partDrawing: true, partCut: true, poPdfs: true },
},
},
});
if (!file) throw new ApiError(404, "not_found", "File not found");
const refs =
file._count.partStep +
file._count.partDrawing +
file._count.partCut +
file._count.poPdfs;
if (refs > 0) {
throw new ApiError(
409,
"file_in_use",
`File is referenced by ${refs} record(s). Detach it first.`,
);
}
await prisma.fileAsset.delete({ where: { id } });
await deleteFileFromDisk(file.path);
await audit({
actorId: user.id,
action: "delete",
entity: "FileAsset",
entityId: id,
before: { sha256: file.sha256, originalName: file.originalName },
ipAddress: clientIp(req),
});
return ok({ ok: true });
} catch (err) {
return errorResponse(err);
}
}