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
+64
View File
@@ -0,0 +1,64 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { signOut, useSession } from "next-auth/react";
export function AppHeader() {
const { data: session } = useSession();
const pathname = usePathname();
if (pathname === "/login") return null;
return (
<header
style={{
borderBottom: "1px solid var(--border)",
background: "rgba(15, 20, 25, 0.85)",
backdropFilter: "blur(8px)",
position: "sticky",
top: 0,
zIndex: 20,
}}
>
<div
className="container"
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "0.75rem 0",
gap: "1rem",
}}
>
<Link href="/" style={{ display: "flex", alignItems: "baseline", gap: "0.5rem" }}>
<span style={{ fontWeight: 800, letterSpacing: "-0.03em", fontSize: "1.1rem" }}>
RFID DB
</span>
<span className="muted" style={{ fontSize: "0.75rem" }}>
tag dumps
</span>
</Link>
<nav className="row" style={{ gap: "0.25rem" }}>
<Link className="btn btn-ghost" href="/">
Sites
</Link>
<Link className="btn btn-ghost" href="/tags/new">
New tag
</Link>
<Link className="btn btn-ghost" href="/settings">
Settings
</Link>
{session?.user && (
<button
type="button"
className="btn btn-ghost"
onClick={() => signOut({ callbackUrl: "/login" })}
>
Sign out
</button>
)}
</nav>
</div>
</header>
);
}
+19
View File
@@ -0,0 +1,19 @@
"use client";
import { useState } from "react";
export function CopyButton({ value, label = "Copy" }: { value: string; label?: string }) {
const [copied, setCopied] = useState(false);
async function onCopy() {
await navigator.clipboard.writeText(value);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}
return (
<button type="button" className="btn btn-secondary" onClick={onCopy} style={{ padding: "0.3rem 0.6rem", fontSize: "0.8rem" }}>
{copied ? "Copied" : label}
</button>
);
}
+7
View File
@@ -0,0 +1,7 @@
"use client";
import { SessionProvider } from "next-auth/react";
export function Providers({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}
+13
View File
@@ -0,0 +1,13 @@
"use client";
import { useEffect } from "react";
export function ServiceWorkerRegister() {
useEffect(() => {
if (typeof window === "undefined" || !("serviceWorker" in navigator)) return;
navigator.serviceWorker.register("/sw.js").catch(() => {
// ignore registration failures in dev
});
}, []);
return null;
}