1efa77bf56
A parking lot is one pool of spaces with a flexible set of entry/exit
points — no "lane". Direction is a property of each RELAY inside an access
controller; readers/cameras bind to a controller relay and inherit it.
Schema:
- drop `lane` from ledger_events, device_events, sessions
- rename lane_devices -> devices (no lane/direction columns)
- access config.relays=[{relay,direction,button?}]; reader/camera
config.controllerId+relay binding
- fresh 0000_baseline migration (history reset; dev data was throwaway)
Signed ledger:
- remove `lane` from canonicalize(); bump signer keyId sw-hmac-v1 -> v2
(v1 events won't verify under v2 — intentional, gated per-event by keyId)
Server:
- new device-resolve.ts (replaces lane-map.ts): relayForButton,
relayForDevice, firstRelayByDirection, devicesByDirection
- entry-flow: button terminal -> its relay; exit/permit: reader's bound
relay; dispatcher resolves the bound relay + inherited direction
- camera snapshots fire by direction site-wide, async, never block open
- DeviceConfig widened to nested JSON for relays[]
Web:
- wizard: no lane selector; add controllers (relay map + entry-button
terminal) first, then bind readers/cameras/printers to a controller relay
Wiki: new entry-exit-points.md (replaces lane-direction); reworked
entry-exit-readers, parking-session, first-run-setup, device-registry,
append-only-event-chain, device-events; removed stale lane/LaneMap mentions.
83 lines
2.9 KiB
TypeScript
83 lines
2.9 KiB
TypeScript
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
|
import { eq, devices, type Db } from "@parking/db";
|
|
import { deviceEvents } from "../device-events.js";
|
|
import { verifyDigest } from "../digest-auth.js";
|
|
|
|
// Inbound device push endpoints. The Dingtian board's "Input Link URL" feature
|
|
// HTTP-calls us when an input (button) fires — no polling. We translate the
|
|
// push into an internal device event; the entry flow decides what to do
|
|
// (print a ticket, then command the relay). See wiki/concepts/device-input-flow.md.
|
|
//
|
|
// AUTH: HTTP Digest (the device can do Digest but not HTTPS-to-self-signed —
|
|
// both tested on hardware). The password is never sent on the wire; the secret
|
|
// is NOT in the URL. Per-device credentials live in lane_devices (written on
|
|
// assign). This is defence-in-depth on a flat network; the signed event log is
|
|
// the real anti-fraud guarantee (an open with no matching signed event is an
|
|
// anomaly). Source-IP is also checked. NOT behind the SPA cookie/CSRF auth
|
|
// (machine call from the device).
|
|
|
|
interface InputParams {
|
|
deviceId: string;
|
|
n: string;
|
|
edge: string;
|
|
}
|
|
|
|
interface DingtianDeviceConfig {
|
|
host?: string;
|
|
pushUser?: string;
|
|
pushPassword?: string;
|
|
}
|
|
|
|
function clientIp(req: FastifyRequest): string {
|
|
return req.ip.replace(/^::ffff:/, "");
|
|
}
|
|
|
|
export async function deviceRoutes(app: FastifyInstance, db: Db): Promise<void> {
|
|
const handle = async (req: FastifyRequest<{ Params: InputParams }>, reply: FastifyReply) => {
|
|
const { deviceId, n, edge } = req.params;
|
|
|
|
const row = await db.select().from(devices).where(eq(devices.id, deviceId)).get();
|
|
const cfg = row?.config as DingtianDeviceConfig | undefined;
|
|
|
|
// Unknown device / not a dingtian / no push creds / wrong source IP → 404.
|
|
if (
|
|
!row ||
|
|
row.driverId !== "dingtian" ||
|
|
!cfg?.pushUser ||
|
|
!cfg.pushPassword ||
|
|
!cfg.host ||
|
|
clientIp(req) !== cfg.host
|
|
) {
|
|
app.log.warn(`rejected device push: device=${deviceId} ip=${clientIp(req)}`);
|
|
return reply.code(404).send({ error: "not found" });
|
|
}
|
|
|
|
// Digest auth — issues a 401 challenge on first hit; the device retries with
|
|
// the hashed response (verifyDigest sends the challenge + returns false).
|
|
if (!verifyDigest(req, reply, { user: cfg.pushUser, password: cfg.pushPassword })) {
|
|
return; // 401 already sent
|
|
}
|
|
|
|
const input = Number(n);
|
|
const ed = edge === "off" ? "off" : "on";
|
|
app.log.info(`[dingtian:${deviceId}] input ${input} ${ed} (push)`);
|
|
deviceEvents.emitInput({
|
|
driverId: "dingtian",
|
|
deviceId,
|
|
input,
|
|
edge: ed,
|
|
at: new Date().toISOString(),
|
|
source: "push",
|
|
});
|
|
return { ok: true };
|
|
};
|
|
|
|
for (const method of ["GET", "POST"] as const) {
|
|
app.route({
|
|
method,
|
|
url: "/api/devices/dingtian/:deviceId/input/:n/:edge",
|
|
handler: handle,
|
|
});
|
|
}
|
|
}
|