Device-agnostic driver registry + first-run setup
Make the device-adapter pattern selectable so the admin chooses hardware at install — per lane, from a catalog of supported drivers. Adding a device = registering one more driver; no business-logic change. packages/devices: - interfaces.ts: AccessControlDevice / ReaderDevice / CameraDevice / PrinterDevice (adds CameraDevice for entry/exit snapshot-on-event; access relay stays intent-only per "a barrier is not a door"). - registry.ts: driver catalog with per-driver config fields + factory, config validation, and a catalog payload for the setup UI. - drivers/: stub adapters — access (zkteco, esp32-relay), reader (wiegand, tcp-ip), camera (hikvision, dahua). Real vendor protocols TBD. packages/db: - lane_devices + setup_state tables (migration 0001); re-export query helpers. apps/server: - routes/setup.ts: GET /api/setup/catalog (public schema), and admin-only /assign, /state, /complete with registry validation before persisting. - extract auth.ts (requireJwtSecret, requireRole, JWT type aug). apps/web: - SetupWizard scaffold + api client: pick a driver per category for a lane, render its config fields. wiki: device-registry + first-run-setup concept pages; cross-link from device-adapter-pattern; index + log updated. Verified: full turbo build (5/5); catalog lists all drivers; admin assign persists; missing-config and no-token requests are rejected.
This commit is contained in:
+13
-38
@@ -1,39 +1,24 @@
|
||||
import jwt from "@fastify/jwt";
|
||||
import Fastify, { type FastifyInstance } from "fastify";
|
||||
import type { Role } from "@parking/shared";
|
||||
import { createDb, type Db } from "@parking/db";
|
||||
import { requireJwtSecret } from "./auth.js";
|
||||
import { setupRoutes } from "./routes/setup.js";
|
||||
|
||||
// The backend is Fastify (Node). Hardware drivers live as isolated Fastify
|
||||
// plugins emitting onto a shared internal event bus; auth is fully local
|
||||
// (offline-first). See wiki/entities/fastify.md and local-jwt-auth.md.
|
||||
|
||||
declare module "@fastify/jwt" {
|
||||
interface FastifyJWT {
|
||||
payload: { sub: string; username: string; role: Role };
|
||||
user: { sub: string; username: string; role: Role };
|
||||
}
|
||||
export interface BuildOptions {
|
||||
db?: Db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the JWT signing secret, refusing to start without a strong one.
|
||||
* There is deliberately no fallback default — a missing, short, or placeholder
|
||||
* secret throws so the server never runs with forgeable tokens.
|
||||
*/
|
||||
function requireJwtSecret(): string {
|
||||
const secret = process.env.JWT_SECRET;
|
||||
if (!secret || secret.length < 32 || /change.?me|insecure|dev-only/i.test(secret)) {
|
||||
throw new Error(
|
||||
"JWT_SECRET must be set to a strong random value (>=32 chars). " +
|
||||
"Generate one with: openssl rand -hex 32",
|
||||
);
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
export async function buildServer(): Promise<FastifyInstance> {
|
||||
export async function buildServer(opts: BuildOptions = {}): Promise<FastifyInstance> {
|
||||
const app = Fastify({
|
||||
logger: { level: process.env.LOG_LEVEL ?? "info" },
|
||||
});
|
||||
|
||||
const db = opts.db ?? createDb();
|
||||
|
||||
// Local JWT signing with a local secret — no external identity provider.
|
||||
// Fail fast rather than fall back to a known default: a booth machine started
|
||||
// without a real secret would sign tokens anyone could forge (incl. an admin
|
||||
@@ -45,21 +30,11 @@ export async function buildServer(): Promise<FastifyInstance> {
|
||||
|
||||
app.get("/health", async () => ({ status: "ok" }));
|
||||
|
||||
// TODO: register device-driver plugins (packages/devices adapters),
|
||||
// the append-only event-log routes, and the role-guarded admin API.
|
||||
// Device-agnostic setup: the admin selects devices per lane from the driver
|
||||
// catalog at first-run. See wiki/concepts/first-run-setup.md.
|
||||
await setupRoutes(app, db);
|
||||
|
||||
// TODO: device-driver runtime plugins, append-only event-log routes, login.
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
/**
|
||||
* preHandler role guard. Authorization is a simple per-route role check — no
|
||||
* Casbin/RBAC engine needed at this scale. See wiki/entities/local-jwt-auth.md.
|
||||
*/
|
||||
export function requireRole(...allowed: Role[]) {
|
||||
return async (req: { jwtVerify: () => Promise<void>; user?: { role: Role } }) => {
|
||||
await req.jwtVerify();
|
||||
if (!req.user || !allowed.includes(req.user.role)) {
|
||||
throw Object.assign(new Error("forbidden"), { statusCode: 403 });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user