109 lines
3.3 KiB
JavaScript
109 lines
3.3 KiB
JavaScript
#!/usr/bin/env node
|
|
import { spawnSync } from "node:child_process";
|
|
import "dotenv/config";
|
|
|
|
const args = process.argv.slice(2);
|
|
|
|
function getArg(name) {
|
|
const index = args.indexOf(name);
|
|
return index >= 0 ? args[index + 1] : undefined;
|
|
}
|
|
|
|
function hasFlag(name) {
|
|
return args.includes(name);
|
|
}
|
|
|
|
function usage() {
|
|
console.log(`Usage:
|
|
node scripts/provision-mqtt-device.mjs --device-id <device-id> [--base-url http://127.0.0.1:3000] [--apply-local]
|
|
|
|
Options:
|
|
--device-id Device UUID from the platform.
|
|
--base-url Running backend URL. Default: BASE_URL env or http://127.0.0.1:3000.
|
|
--apply-local Run mosquitto_passwd locally. Use only on the broker host.
|
|
|
|
Environment:
|
|
ADMIN_TOKEN Admin bearer token. Default: admin-dev-token.
|
|
MOSQUITTO_PASSWD_FILE Default: /etc/mosquitto/passwd.
|
|
MOSQUITTO_ACL_FILE Default: /etc/mosquitto/acl.
|
|
`);
|
|
}
|
|
|
|
const deviceId = getArg("--device-id");
|
|
const baseUrl = getArg("--base-url") || process.env.BASE_URL || "http://127.0.0.1:3000";
|
|
const adminToken = process.env.ADMIN_TOKEN || "admin-dev-token";
|
|
const passwdFile = process.env.MOSQUITTO_PASSWD_FILE || "/etc/mosquitto/passwd";
|
|
const aclFile = process.env.MOSQUITTO_ACL_FILE || "/etc/mosquitto/acl";
|
|
const applyLocal = hasFlag("--apply-local");
|
|
|
|
if (!deviceId || hasFlag("--help") || hasFlag("-h")) {
|
|
usage();
|
|
process.exit(deviceId ? 0 : 1);
|
|
}
|
|
|
|
function shellQuote(value) {
|
|
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
}
|
|
|
|
async function rotateCredential() {
|
|
const response = await fetch(`${baseUrl}/admin/devices/${deviceId}/credentials/rotate`, {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${adminToken}`,
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: "{}"
|
|
});
|
|
|
|
const payload = await response.json().catch(() => ({}));
|
|
if (!response.ok) {
|
|
throw new Error(payload?.message || payload?.code || `rotate failed with status ${response.status}`);
|
|
}
|
|
|
|
return payload.data;
|
|
}
|
|
|
|
function applyMosquittoPassword(username, password) {
|
|
const result = spawnSync("sudo", ["mosquitto_passwd", "-b", passwdFile, username, password], {
|
|
stdio: "inherit"
|
|
});
|
|
if (result.status !== 0) {
|
|
throw new Error(`mosquitto_passwd failed with status ${result.status}`);
|
|
}
|
|
}
|
|
|
|
const data = await rotateCredential();
|
|
const username = data.credential.mqtt_username;
|
|
const password = data.credential.mqtt_password;
|
|
|
|
if (applyLocal) {
|
|
applyMosquittoPassword(username, password);
|
|
}
|
|
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
device_id: data.device.id,
|
|
mqtt_username: username,
|
|
mqtt_password: password,
|
|
one_time_secret: true,
|
|
applied_local: applyLocal,
|
|
mosquitto_commands: [
|
|
`sudo mosquitto_passwd -b ${shellQuote(passwdFile)} ${shellQuote(username)} ${shellQuote(password)}`,
|
|
`node scripts/check-mqtt-acl.mjs --file ${shellQuote(aclFile)} || node scripts/check-mqtt-acl.mjs --print-template`,
|
|
"sudo chown root:mosquitto /etc/mosquitto/passwd",
|
|
"sudo chmod 640 /etc/mosquitto/passwd",
|
|
"sudo systemctl reload mosquitto"
|
|
],
|
|
topic_scope: [
|
|
`device publish/readwrite: devices/${username}/uplink/#`,
|
|
`device subscribe/read: devices/${username}/downlink/#`,
|
|
"backend read: devices/+/uplink/#",
|
|
"backend write: devices/+/downlink/#"
|
|
]
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
);
|