import { FormEvent, useCallback, useEffect, useState } from 'react'; import Layout from '../../components/Layout'; import { api } from '../../lib/api'; import { useRequireAuth } from '../../lib/auth-context'; interface CaStatus { initialized: boolean; fingerprint?: string; expiresAt?: string; } async function downloadText(path: string, filename: string) { const res = await fetch(path); if (!res.ok) { throw new Error(`Download failed (${res.status})`); } const text = await res.text(); const blob = new Blob([text], { type: 'application/x-pem-file' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } export default function MqttCertsPage() { const { user, activeOrg, loading } = useRequireAuth(); const isAdmin = activeOrg?.role === 'ORG_ADMIN'; const [status, setStatus] = useState(null); const [hostname, setHostname] = useState(''); const [error, setError] = useState(null); const [notice, setNotice] = useState(null); const reload = useCallback(() => { if (!isAdmin) { return; } api .get('/api/certificates/ca') .then((s) => { setStatus(s); setError(null); }) .catch((err) => setError(err.message)); }, [isAdmin]); useEffect(reload, [reload]); const initCa = async () => { try { await api.post('/api/certificates/ca/init', {}); setNotice('CA initialized. Restart the mosquitto container to enable the 8883 listener if this is the first setup.'); reload(); } catch (err: any) { setError(err.message); } }; const provision = async (e: FormEvent) => { e.preventDefault(); try { await api.post('/api/certificates/mqtt/provision', { hostname }); setNotice('Broker certificate provisioned. Run `docker compose restart mosquitto` to pick it up.'); setHostname(''); } catch (err: any) { setError(err.message); } }; if (loading || !user) { return null; } if (!isAdmin) { return (

MQTT Certificates

Only organization admins can manage the device certificate authority.

); } return (

MQTT Certificates

Field devices authenticate to the MQTT broker with a client certificate (port 8883). The certificate's serial number becomes its MQTT identity, scoping it to devices/<serial>/#. Issue device certificates from the Devices page once the CA below is set up.

{error &&

{error}

} {notice &&

{notice}

}

Certificate authority

{!status &&

Loading…

} {status && !status.initialized && ( <>

No CA has been initialized yet.

)} {status && status.initialized && ( <>

Fingerprint: {status.fingerprint}

Expires: {status.expiresAt ? new Date(status.expiresAt).toLocaleString() : '—'}

)}

Broker server certificate

Issues (or re-issues) the server certificate the broker presents on port 8883, signed by the CA above.

setHostname(e.target.value)} required style={{ padding: '0.4rem', width: 260 }} />

Mosquitto doesn't hot-reload its config or certificate files — a manual docker compose restart mosquitto is required after initializing the CA or re-provisioning the broker certificate. Revoking a device certificate deletes its database record only; it isn't broker-enforced (no CRL/OCSP), so a revoked certificate still authenticates until it expires.

); }