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:
2026-06-14 07:59:46 +02:00
parent 7de5c74500
commit 72ba4099ea
24 changed files with 1138 additions and 71 deletions
+40
View File
@@ -0,0 +1,40 @@
import type { Role } from "@parking/shared";
// Local JWT auth helpers — fully local, no external identity provider
// (offline-first). See wiki/entities/local-jwt-auth.md.
declare module "@fastify/jwt" {
interface FastifyJWT {
payload: { sub: string; username: string; role: Role };
user: { sub: string; username: string; role: Role };
}
}
/**
* 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.
*/
export 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;
}
/**
* 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 });
}
};
}