Implement RFID tag dump PWA with REST API, auth, and CI (#1)

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.

Co-authored-by: Cursor Agent <[email protected]>
This commit is contained in:
Nick Trochalakis
2026-08-23 15:24:12 -07:00
committed by GitHub
co-authored by Cursor Agent
parent 5029945cc2
commit 8500d2f673
65 changed files with 13689 additions and 1 deletions
+62
View File
@@ -0,0 +1,62 @@
import type { NextRequest } from "next/server";
import { eq } from "drizzle-orm";
import { getDb } from "@/db/client";
import { apiTokens, users, type User } from "@/db/schema";
import { auth } from "@/lib/auth/auth";
import { hashToken, isApiTokenFormat } from "@/lib/auth/tokens";
import { jsonError, jsonOk } from "@/lib/api/errors";
export type AuthUser = Pick<User, "id" | "email" | "name">;
export { jsonError, jsonOk };
export async function requireApiUser(
request: NextRequest
): Promise<AuthUser | Response> {
const header = request.headers.get("authorization");
if (header?.toLowerCase().startsWith("bearer ")) {
const token = header.slice(7).trim();
if (!isApiTokenFormat(token)) {
return jsonError(401, "UNAUTHORIZED", "Invalid API token");
}
const db = getDb();
const tokenHash = hashToken(token);
const row = db
.select({
tokenId: apiTokens.id,
expiresAt: apiTokens.expiresAt,
userId: users.id,
email: users.email,
name: users.name,
})
.from(apiTokens)
.innerJoin(users, eq(apiTokens.userId, users.id))
.where(eq(apiTokens.tokenHash, tokenHash))
.get();
if (!row) {
return jsonError(401, "UNAUTHORIZED", "Invalid API token");
}
if (row.expiresAt && row.expiresAt.getTime() < Date.now()) {
return jsonError(401, "UNAUTHORIZED", "API token expired");
}
db.update(apiTokens)
.set({ lastUsedAt: new Date() })
.where(eq(apiTokens.id, row.tokenId))
.run();
return { id: row.userId, email: row.email, name: row.name };
}
const session = await auth();
if (!session?.user?.id) {
return jsonError(401, "UNAUTHORIZED", "Authentication required");
}
return {
id: session.user.id,
email: session.user.email ?? "",
name: session.user.name ?? "",
};
}
+11
View File
@@ -0,0 +1,11 @@
export function jsonError(
status: number,
code: string,
message: string
): Response {
return Response.json({ error: { code, message } }, { status });
}
export function jsonOk<T>(data: T, status = 200): Response {
return Response.json(data, { status });
}