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

@@ -1,4 +1,5 @@
import { GpsFixType, LocateMode, UtilityType } from '@prisma/client';
import { ApiPropertyOptional, ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
ArrayMaxSize,
@@ -18,98 +19,120 @@ import {
ValidateNested,
} from 'class-validator';
// Field names here are terse (lat/lng/alt/hAcc/...) to keep MQTT payloads
// small; this same shape is reused as the sim tool's REST request body.
export class MqttPointDto {
@ApiProperty({ minimum: -90, maximum: 90 })
@IsNumber()
@Min(-90)
@Max(90)
lat: number;
@ApiProperty({ minimum: -180, maximum: 180 })
@IsNumber()
@Min(-180)
@Max(180)
lng: number;
@ApiPropertyOptional({ description: 'Altitude, meters' })
@IsOptional()
@IsNumber()
alt?: number;
@ApiPropertyOptional({ enum: GpsFixType })
@IsOptional()
@IsEnum(GpsFixType)
fix?: GpsFixType;
@ApiPropertyOptional({ description: 'Horizontal accuracy, meters', minimum: 0 })
@IsOptional()
@IsNumber()
@Min(0)
hAcc?: number;
@ApiPropertyOptional({ description: 'Meters below grade', minimum: 0 })
@IsOptional()
@IsNumber()
@Min(0)
depth?: number;
@ApiPropertyOptional({ enum: UtilityType })
@IsOptional()
@IsEnum(UtilityType)
utility?: UtilityType;
@ApiPropertyOptional({ description: 'Ordering within a locate run' })
@IsOptional()
@IsInt()
seq?: number;
// GPS quality
@ApiPropertyOptional({ description: 'Vertical accuracy, meters', minimum: 0 })
@IsOptional()
@IsNumber()
@Min(0)
vAcc?: number;
@ApiPropertyOptional({ minimum: 0 })
@IsOptional()
@IsInt()
@Min(0)
sats?: number;
@ApiPropertyOptional({ minimum: 0 })
@IsOptional()
@IsNumber()
@Min(0)
hdop?: number;
// Locator receiver telemetry
@ApiPropertyOptional({ description: 'Locate frequency, Hz', minimum: 0 })
@IsOptional()
@IsInt()
@Min(0)
freqHz?: number;
@ApiPropertyOptional({ description: 'Signal current on the line, mA', minimum: 0 })
@IsOptional()
@IsNumber()
@Min(0)
currentMa?: number;
@ApiPropertyOptional({ description: 'Signal strength, dB' })
@IsOptional()
@IsNumber()
signalDb?: number;
@ApiPropertyOptional({ description: 'Receiver gain, dB' })
@IsOptional()
@IsNumber()
gainDb?: number;
@ApiPropertyOptional({ enum: LocateMode })
@IsOptional()
@IsEnum(LocateMode)
mode?: LocateMode;
@ApiPropertyOptional({ description: 'Degrees' })
@IsOptional()
@IsNumber()
phaseDeg?: number;
@ApiPropertyOptional({ description: 'Line direction, 0-360', minimum: 0, maximum: 360 })
@IsOptional()
@IsNumber()
@Min(0)
@Max(360)
compassDeg?: number;
@ApiPropertyOptional({ minimum: 0, maximum: 100 })
@IsOptional()
@IsNumber()
@Min(0)
@Max(100)
distortionPct?: number;
@ApiProperty({ format: 'date-time', description: 'Device GPS timestamp' })
@IsDateString()
ts: string;
}
@@ -149,9 +172,11 @@ export type MqttLogMessageType = (typeof MQTT_LOG_MESSAGE_TYPES)[number];
// what happens to it: "log" persists a LocatePoint; "status" is an ephemeral
// current-position update, broadcast live but never written to the DB.
export class MqttLogMessageDto extends MqttPointDto {
@ApiProperty({ enum: MQTT_LOG_MESSAGE_TYPES, description: '"log" persists a point; "status" is live-only' })
@IsIn(MQTT_LOG_MESSAGE_TYPES)
type: MqttLogMessageType;
@ApiProperty({ description: 'Job this reading belongs to; also supplies the organization' })
@IsString()
@IsNotEmpty()
jobId: string;