mirror of
https://github.com/Chewbaccalakis/rfid-database.git
synced 2026-09-09 16:01:56 -07:00
Add Next.js app with SQLite/Drizzle storage for sites and LF/HF tag dumps, multi-user Auth.js (credentials + optional OIDC), personal API tokens in Settings, versioned /api/v1 for UI and future CLI use, MCT/Proxmark/JSON import-export, PWA offline shell, Vitest tests, and GitHub Actions CI.
90 lines
2.9 KiB
TypeScript
90 lines
2.9 KiB
TypeScript
import { NextRequest } from "next/server";
|
|
import { count, eq } from "drizzle-orm";
|
|
import { requireApiUser, jsonError, jsonOk } from "@/lib/api/auth-guard";
|
|
import { getDb } from "@/db/client";
|
|
import { sites, tagRecords } from "@/db/schema";
|
|
import { siteUpdateSchema } from "@/lib/validation/rfid";
|
|
|
|
type Ctx = { params: Promise<{ id: string }> };
|
|
|
|
export async function GET(request: NextRequest, context: Ctx) {
|
|
const userOrRes = await requireApiUser(request);
|
|
if (userOrRes instanceof Response) return userOrRes;
|
|
|
|
const { id } = await context.params;
|
|
const db = getDb();
|
|
const site = db.select().from(sites).where(eq(sites.id, id)).get();
|
|
if (!site) return jsonError(404, "NOT_FOUND", "Site not found");
|
|
|
|
const tagCount =
|
|
db
|
|
.select({ c: count() })
|
|
.from(tagRecords)
|
|
.where(eq(tagRecords.siteId, id))
|
|
.get()?.c ?? 0;
|
|
|
|
return jsonOk({
|
|
...site,
|
|
createdAt: site.createdAt.toISOString(),
|
|
tagCount: Number(tagCount),
|
|
});
|
|
}
|
|
|
|
export async function PATCH(request: NextRequest, context: Ctx) {
|
|
const userOrRes = await requireApiUser(request);
|
|
if (userOrRes instanceof Response) return userOrRes;
|
|
|
|
const { id } = await context.params;
|
|
let body: unknown;
|
|
try {
|
|
body = await request.json();
|
|
} catch {
|
|
return jsonError(400, "INVALID_JSON", "Request body must be JSON");
|
|
}
|
|
|
|
const parsed = siteUpdateSchema.safeParse(body);
|
|
if (!parsed.success) {
|
|
return jsonError(400, "VALIDATION_ERROR", parsed.error.message);
|
|
}
|
|
|
|
const db = getDb();
|
|
const existing = db.select().from(sites).where(eq(sites.id, id)).get();
|
|
if (!existing) return jsonError(404, "NOT_FOUND", "Site not found");
|
|
|
|
const updates: Partial<typeof sites.$inferInsert> = {};
|
|
if (parsed.data.name !== undefined) updates.name = parsed.data.name;
|
|
if (parsed.data.code !== undefined) updates.code = parsed.data.code.toUpperCase();
|
|
if (parsed.data.notes !== undefined) updates.notes = parsed.data.notes;
|
|
|
|
if (updates.code && updates.code !== existing.code) {
|
|
const clash = db
|
|
.select()
|
|
.from(sites)
|
|
.where(eq(sites.code, updates.code))
|
|
.get();
|
|
if (clash) {
|
|
return jsonError(409, "CONFLICT", `Site code ${updates.code} already exists`);
|
|
}
|
|
}
|
|
|
|
db.update(sites).set(updates).where(eq(sites.id, id)).run();
|
|
const updated = db.select().from(sites).where(eq(sites.id, id)).get()!;
|
|
return jsonOk({
|
|
...updated,
|
|
createdAt: updated.createdAt.toISOString(),
|
|
});
|
|
}
|
|
|
|
export async function DELETE(request: NextRequest, context: Ctx) {
|
|
const userOrRes = await requireApiUser(request);
|
|
if (userOrRes instanceof Response) return userOrRes;
|
|
|
|
const { id } = await context.params;
|
|
const db = getDb();
|
|
const existing = db.select().from(sites).where(eq(sites.id, id)).get();
|
|
if (!existing) return jsonError(404, "NOT_FOUND", "Site not found");
|
|
|
|
db.delete(sites).where(eq(sites.id, id)).run();
|
|
return jsonOk({ ok: true });
|
|
}
|