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

@@ -18,9 +18,11 @@ interface SentEntry {
lat: number;
lng: number;
depth: number;
transport: Transport;
}
type MessageType = 'log' | 'status';
type Transport = 'relay' | 'mqtts';
const UTILITIES = ['GAS', 'WATER', 'ELECTRIC', 'SEWER', 'TELECOM', 'CATV', 'FIBER', 'STEAM', 'UNKNOWN'];
const FIX_TYPES = ['FIXED_RTK', 'FLOAT_RTK', 'DGPS', 'AUTONOMOUS', 'NONE'];
@@ -51,6 +53,7 @@ export default function SimulatorPage() {
const [newTitle, setNewTitle] = useState('');
const [messageType, setMessageType] = useState<MessageType>('log');
const [transport, setTransport] = useState<Transport>('relay');
const [serial, setSerial] = useState('');
const [utility, setUtility] = useState('GAS');
const [fixType, setFixType] = useState('FIXED_RTK');
@@ -137,6 +140,7 @@ export default function SimulatorPage() {
await api.post(`/api/orgs/${orgId}/sim/publish`, {
type: messageType,
serial,
transport,
jobId,
lat: pos.lat,
lng: pos.lng,
@@ -160,7 +164,15 @@ export default function SimulatorPage() {
setCount(seq);
setSent((prev) =>
[
{ at: new Date().toLocaleTimeString(), seq, type: messageType, lat: pos.lat, lng: pos.lng, depth: pointDepth },
{
at: new Date().toLocaleTimeString(),
seq,
type: messageType,
lat: pos.lat,
lng: pos.lng,
depth: pointDepth,
transport,
},
...prev,
].slice(0, 25),
);
@@ -216,7 +228,7 @@ export default function SimulatorPage() {
<main style={{ maxWidth: 720, margin: '0 auto', padding: '1.5rem' }}>
<p style={{ color: '#666' }}>
Simulates a locator receiver publishing GPS + telemetry to the MQTT broker at{' '}
<code>devices/{serial || '<serial>'}/log</code>, exactly as a real device would.
<code>devices/{serial || '<serial>'}/log</code>.
</p>
{error && <p style={{ color: '#c62828' }}>{error}</p>}
@@ -232,6 +244,24 @@ export default function SimulatorPage() {
</label>
</fieldset>
<fieldset style={{ marginBottom: '1rem', border: '1px solid #ddd', borderRadius: 8, padding: '1rem' }}>
<legend>Connection</legend>
<label style={{ marginRight: '1.5rem' }}>
<input type="radio" checked={transport === 'relay'} onChange={() => setTransport('relay')} /> HTTP relay{' '}
<span style={{ color: '#888' }}> backend forwards it on the broker connection it already holds</span>
</label>
<label>
<input type="radio" checked={transport === 'mqtts'} onChange={() => setTransport('mqtts')} /> MQTTS (device
certificate) <span style={{ color: '#888' }}> connects to port 8883 and authenticates as this serial's own client cert</span>
</label>
{transport === 'mqtts' && (
<p style={{ color: '#999', fontSize: '0.85rem', margin: '0.5rem 0 0' }}>
Requires a device with this exact serial number to already exist in this org and have a certificate
issued from <a href="/settings/devices">Devices</a>.
</p>
)}
</fieldset>
<fieldset style={{ marginBottom: '1rem', border: '1px solid #ddd', borderRadius: 8, padding: '1rem' }}>
<legend>Job</legend>
{!creatingJob ? (
@@ -424,6 +454,7 @@ export default function SimulatorPage() {
<th>Lat</th>
<th>Lng</th>
<th>Depth</th>
<th>Via</th>
</tr>
</thead>
<tbody>
@@ -435,11 +466,12 @@ export default function SimulatorPage() {
<td>{s.lat.toFixed(7)}</td>
<td>{s.lng.toFixed(7)}</td>
<td>{s.depth} m</td>
<td style={{ color: s.transport === 'mqtts' ? '#2e7d32' : '#888' }}>{s.transport}</td>
</tr>
))}
{sent.length === 0 && (
<tr>
<td colSpan={6} style={{ padding: '1.5rem', textAlign: 'center', color: '#888' }}>
<td colSpan={7} style={{ padding: '1.5rem', textAlign: 'center', color: '#888' }}>
No points sent yet.
</td>
</tr>