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:
@@ -1,10 +1,16 @@
|
||||
import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { toPointDto } from '../points/points.service';
|
||||
import { RealtimeService } from '../realtime/realtime.service';
|
||||
import { DevicePositionSnapshot } from './device-position';
|
||||
import { CreateDeviceDto, UpdateDeviceDto } from './dto/devices.dto';
|
||||
|
||||
@Injectable()
|
||||
export class DevicesService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly realtime: RealtimeService,
|
||||
) {}
|
||||
|
||||
list(orgId: string) {
|
||||
return this.prisma.device.findMany({
|
||||
@@ -55,7 +61,7 @@ export class DevicesService {
|
||||
async update(orgId: string, deviceId: string, dto: UpdateDeviceDto) {
|
||||
await this.get(orgId, deviceId);
|
||||
const { disabledReason, ...rest } = dto;
|
||||
return this.prisma.device.update({
|
||||
const device = await this.prisma.device.update({
|
||||
where: { id: deviceId },
|
||||
data: {
|
||||
...rest,
|
||||
@@ -64,6 +70,14 @@ export class DevicesService {
|
||||
...(dto.isActive === false && { disabledReason: disabledReason ?? null }),
|
||||
},
|
||||
});
|
||||
this.realtime.publish(`org:${orgId}:devices`, {
|
||||
type: 'device',
|
||||
orgId,
|
||||
deviceId,
|
||||
isActive: device.isActive,
|
||||
disabledReason: device.disabledReason,
|
||||
});
|
||||
return device;
|
||||
}
|
||||
|
||||
async remove(orgId: string, deviceId: string) {
|
||||
@@ -72,6 +86,54 @@ export class DevicesService {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async getLocation(orgId: string, deviceId: string) {
|
||||
const device = await this.get(orgId, deviceId);
|
||||
const point = await this.prisma.locatePoint.findFirst({
|
||||
where: { deviceId },
|
||||
orderBy: { recordedAt: 'desc' },
|
||||
include: { job: { select: { id: true, ticketNumber: true, title: true } } },
|
||||
});
|
||||
|
||||
// A live "status" ping is never persisted as a LocatePoint, so the most
|
||||
// recent position may only exist on the device's lastPosition snapshot —
|
||||
// compare timestamps and use whichever source is actually newer.
|
||||
const live = device.lastPosition as unknown as DevicePositionSnapshot | null;
|
||||
if (live && device.lastPositionAt && (!point || device.lastPositionAt > point.recordedAt)) {
|
||||
return {
|
||||
point: {
|
||||
id: `live-${deviceId}`,
|
||||
lat: live.lat,
|
||||
lng: live.lng,
|
||||
altitude: live.altitude,
|
||||
utilityType: live.utilityType,
|
||||
fixType: live.fixType,
|
||||
sequence: null,
|
||||
recordedAt: live.recordedAt,
|
||||
hAccuracy: live.hAccuracy,
|
||||
vAccuracy: live.vAccuracy,
|
||||
satellites: live.satellites,
|
||||
hdop: live.hdop,
|
||||
depth: live.depth,
|
||||
frequencyHz: live.frequencyHz,
|
||||
currentMa: live.currentMa,
|
||||
signalDb: live.signalDb,
|
||||
gainDb: live.gainDb,
|
||||
locateMode: live.locateMode,
|
||||
phaseDeg: live.phaseDeg,
|
||||
compassDeg: live.compassDeg,
|
||||
distortionPct: live.distortionPct,
|
||||
},
|
||||
job: { id: live.jobId, ticketNumber: live.jobTicketNumber, title: live.jobTitle },
|
||||
};
|
||||
}
|
||||
|
||||
if (!point) {
|
||||
return { point: null, job: null };
|
||||
}
|
||||
const { job, ...rest } = point;
|
||||
return { point: toPointDto(rest), job };
|
||||
}
|
||||
|
||||
private async get(orgId: string, deviceId: string) {
|
||||
const device = await this.prisma.device.findFirst({ where: { id: deviceId, orgId } });
|
||||
if (!device) {
|
||||
|
||||
Reference in New Issue
Block a user