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

@@ -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