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,82 @@
import { Body, Controller, Delete, Get, Header, Param, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/auth.guard';
import { OrgRolesGuard } from '../auth/guards/org-roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { CertificatesService } from './certificates.service';
import { ProvisionMqttCertDto } from './dto/certificates.dto';
import { RequireAnyOrgAdminGuard } from './guards/require-any-org-admin.guard';
// Global broker/CA management — not org-nested, since the CA is broker-wide
// (mirrors serialNumber already being globally unique across orgs). JWT-only,
// same precedent as api-keys: credential-minting endpoints never accept an
// API key.
@ApiTags('certificates')
@ApiBearerAuth('jwt')
@Controller('certificates')
@UseGuards(JwtAuthGuard, RequireAnyOrgAdminGuard)
export class CertificatesController {
constructor(private readonly certificatesService: CertificatesService) {}
@Get('ca')
caStatus() {
return this.certificatesService.getCaStatus();
}
@Post('ca/init')
initCa() {
return this.certificatesService.initCa();
}
@Post('mqtt/provision')
provisionMqttCert(@Body() dto: ProvisionMqttCertDto) {
return this.certificatesService.provisionMqttCert(dto);
}
@Get('ca/download')
@Header('Content-Type', 'application/x-pem-file')
@Header('Content-Disposition', 'attachment; filename="ca.crt"')
downloadCa() {
return this.certificatesService.downloadCaCert();
}
}
// Per-device certificate issuance, nested under the org's devices like the
// rest of the domain. Same guard stack as devices create/disable.
@ApiTags('certificates')
@ApiBearerAuth('jwt')
@Controller('orgs/:orgId/devices/:deviceId/certificate')
@UseGuards(JwtAuthGuard, OrgRolesGuard)
@Roles('ORG_ADMIN')
export class DeviceCertificatesController {
constructor(private readonly certificatesService: CertificatesService) {}
@Post()
issue(@Param('orgId') orgId: string, @Param('deviceId') deviceId: string) {
return this.certificatesService.issueDeviceCertificate(orgId, deviceId);
}
@Get()
get(@Param('orgId') orgId: string, @Param('deviceId') deviceId: string) {
return this.certificatesService.getDeviceCertificate(orgId, deviceId);
}
@Get('cert')
@Header('Content-Type', 'application/x-pem-file')
@Header('Content-Disposition', 'attachment; filename="device.crt"')
downloadCert(@Param('orgId') orgId: string, @Param('deviceId') deviceId: string) {
return this.certificatesService.downloadDeviceCert(orgId, deviceId);
}
@Get('key')
@Header('Content-Type', 'application/x-pem-file')
@Header('Content-Disposition', 'attachment; filename="device.key"')
downloadKey(@Param('orgId') orgId: string, @Param('deviceId') deviceId: string) {
return this.certificatesService.downloadDeviceKey(orgId, deviceId);
}
@Delete()
revoke(@Param('orgId') orgId: string, @Param('deviceId') deviceId: string) {
return this.certificatesService.revokeDeviceCertificate(orgId, deviceId);
}
}