phase 2 and 3
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { type NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { ok, errorResponse, requireRole, ApiError } from "@/lib/api";
|
||||
import { saveUploadedFile } from "@/lib/files";
|
||||
import { audit } from "@/lib/audit";
|
||||
import { clientIp } from "@/lib/request";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
await requireRole("admin");
|
||||
const files = await prisma.fileAsset.findMany({
|
||||
orderBy: { uploadedAt: "desc" },
|
||||
take: 200,
|
||||
});
|
||||
return ok({ files });
|
||||
} catch (err) {
|
||||
return errorResponse(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const user = await requireRole("admin");
|
||||
|
||||
const form = await req.formData().catch(() => null);
|
||||
if (!form) throw new ApiError(400, "invalid_form", "Expected multipart/form-data");
|
||||
|
||||
const file = form.get("file");
|
||||
if (!(file instanceof File)) {
|
||||
throw new ApiError(400, "missing_file", "Missing 'file' field");
|
||||
}
|
||||
|
||||
const saved = await saveUploadedFile(file, user.id);
|
||||
|
||||
if (!saved.deduped) {
|
||||
await audit({
|
||||
actorId: user.id,
|
||||
action: "create",
|
||||
entity: "FileAsset",
|
||||
entityId: saved.id,
|
||||
after: { sha256: saved.sha256, kind: saved.kind, originalName: saved.originalName },
|
||||
ipAddress: clientIp(req),
|
||||
});
|
||||
}
|
||||
|
||||
return ok({ file: saved }, { status: saved.deduped ? 200 : 201 });
|
||||
} catch (err) {
|
||||
if (err instanceof Error && /Empty file|File too large/.test(err.message)) {
|
||||
return errorResponse(new ApiError(413, "file_rejected", err.message));
|
||||
}
|
||||
return errorResponse(err);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user