Add device-certificate mTLS auth, live position tracking, and API docs

Introduces a CA/PKI module so field devices can authenticate to Mosquitto
over TLS (8883) with per-device client certificates (CN = serial number)
instead of a shared password, with matching Devices/MQTT-Certs UI. Adds
live transmitter position tracking alongside logged points, an MQTTS
transport option in the simulator for exercising the real cert-auth path,
and Swagger API docs at /api/docs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ulhub
2026-07-18 01:40:12 +00:00
parent f1c94e9279
commit 842cb23e1f
57 changed files with 2283 additions and 67 deletions

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "devices" ADD COLUMN "lastPosition" JSONB,
ADD COLUMN "lastPositionAt" TIMESTAMPTZ(6);

View File

@@ -0,0 +1,20 @@
-- CreateTable
CREATE TABLE "device_certificates" (
"id" TEXT NOT NULL,
"deviceId" TEXT NOT NULL,
"serialNumber" TEXT NOT NULL,
"commonName" TEXT NOT NULL,
"certificatePem" TEXT NOT NULL,
"privateKeyPem" TEXT NOT NULL,
"fingerprint" TEXT NOT NULL,
"issuedAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expiresAt" TIMESTAMPTZ(6) NOT NULL,
CONSTRAINT "device_certificates_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "device_certificates_deviceId_key" ON "device_certificates"("deviceId");
-- AddForeignKey
ALTER TABLE "device_certificates" ADD CONSTRAINT "device_certificates_deviceId_fkey" FOREIGN KEY ("deviceId") REFERENCES "devices"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -142,15 +142,45 @@ model Device {
// fetches this via GET /api/devices/:serial/status to show on its own screen.
disabledReason String?
lastSeenAt DateTime? @db.Timestamptz(6)
// Most recent known position, from either a "status" ping (live-only, never
// persisted as a LocatePoint) or a "log" point — whichever is newest. Lets
// the devices page show current position without waiting for a log write.
lastPosition Json?
lastPositionAt DateTime? @db.Timestamptz(6)
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
points LocatePoint[]
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
points LocatePoint[]
certificate DeviceCertificate?
@@map("devices")
}
// A client certificate issued to a device for mTLS auth on the broker's 8883
// listener; the cert's CN (= serialNumber) becomes the MQTT username. The CA
// key/server key never touch the DB (see PkiService) — only device
// certs/keys are stored here, mirroring how the reference implementation
// (MQTT_DEVICE_AUTH.md) does it. One active cert per device; issuing a new
// one requires deleting this row first. No revokedAt: deleting the row is
// the only "revoke" action there is, and it isn't broker-enforced either way
// (no CRL/OCSP), so a soft-delete flag would misleadingly imply otherwise.
model DeviceCertificate {
id String @id @default(cuid())
deviceId String @unique
serialNumber String
commonName String
certificatePem String @db.Text
privateKeyPem String @db.Text
fingerprint String
issuedAt DateTime @default(now()) @db.Timestamptz(6)
expiresAt DateTime @db.Timestamptz(6)
device Device @relation(fields: [deviceId], references: [id], onDelete: Cascade)
@@map("device_certificates")
}
model LocatePoint {
id BigInt @id @default(autoincrement())
jobId String