Issue dashboard SSL per tenant instead of full cert sync.

Per-row Issue SSL now runs ssl-issue-tenant.sh for business/customer hosts only, with nginx cert map support, so one broken tenant cannot block others.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-26 01:20:03 +03:30
co-authored by Cursor
parent 202ac7d4bd
commit b7bce38131
5 changed files with 226 additions and 70 deletions
+90 -59
View File
@@ -1,16 +1,19 @@
#!/usr/bin/env node
/**
* Tiny HTTP agent on the dashboards VPS.
* Super Admin → Nest API → POST here → runs ssl-sync.sh
* Super Admin → Nest API → POST here → runs ssl-sync.sh or ssl-issue-tenant.sh
*
* Env (/etc/meshkee/ssl-sync-agent.env):
* SSL_SYNC_AGENT_TOKEN=...
* SSL_SYNC_SCRIPT=/opt/meshkee/dashboards/deploy/ssl-sync.sh
* SSL_TENANT_SCRIPT=/opt/meshkee/dashboards/deploy/ssl-issue-tenant.sh
* PORT=9051
* BIND=0.0.0.0
*
* POST /ssl-sync
* Body (optional JSON): { "wait": true }
* Body (optional JSON):
* { "wait": true } — full dashboard cert sync (all tenants, cron / Sync SSL button)
* { "wait": true, "tenantApex": "example.com" } — only business./customer. for one domain
* wait=false (default): accept and run in background → 202
* wait=true: run script and wait for exit → 200 / 500
*/
@@ -21,6 +24,11 @@ import { accessSync, constants } from 'node:fs'
const TOKEN = (process.env.SSL_SYNC_AGENT_TOKEN || '').trim()
const SCRIPT =
(process.env.SSL_SYNC_SCRIPT || '/opt/meshkee/dashboards/deploy/ssl-sync.sh').trim()
const TENANT_SCRIPT =
(
process.env.SSL_TENANT_SCRIPT ||
'/opt/meshkee/dashboards/deploy/ssl-issue-tenant.sh'
).trim()
const PORT = Number(process.env.PORT || 9051)
const BIND = (process.env.BIND || '0.0.0.0').trim()
@@ -29,11 +37,13 @@ if (!TOKEN) {
process.exit(1)
}
try {
accessSync(SCRIPT, constants.X_OK)
} catch {
console.error(`SSL sync script missing or not executable: ${SCRIPT}`)
process.exit(1)
for (const path of [SCRIPT, TENANT_SCRIPT]) {
try {
accessSync(path, constants.X_OK)
} catch {
console.error(`SSL script missing or not executable: ${path}`)
process.exit(1)
}
}
let running = false
@@ -64,12 +74,58 @@ function readJson(req) {
})
}
function startSyncDetached() {
function spawnSync(body, wait) {
const tenantApex =
typeof body.tenantApex === 'string' ? body.tenantApex.trim().toLowerCase() : ''
const script = tenantApex ? TENANT_SCRIPT : SCRIPT
const env = { ...process.env }
if (tenantApex) {
env.SSL_TENANT_APEX = tenantApex
}
if (wait) {
return new Promise((resolve) => {
running = true
const child = spawn(script, [], {
stdio: ['ignore', 'pipe', 'pipe'],
env,
})
let stdout = ''
let stderr = ''
child.stdout.on('data', (d) => {
stdout += d.toString()
})
child.stderr.on('data', (d) => {
stderr += d.toString()
})
child.on('error', (err) => {
running = false
resolve({
ok: false,
code: 1,
log: err.message,
})
})
child.on('exit', (code, signal) => {
running = false
const log = `${stdout}${stderr}`.trim().slice(-4000)
console.log(
`${new Date().toISOString()} ssl-sync waited script=${script} code=${code} signal=${signal ?? ''}`,
)
resolve({
ok: code === 0,
code: code ?? 1,
log,
})
})
})
}
running = true
const child = spawn(SCRIPT, [], {
const child = spawn(script, [], {
detached: true,
stdio: 'ignore',
env: process.env,
env,
})
child.on('error', (err) => {
console.error(`${new Date().toISOString()} spawn error:`, err.message)
@@ -77,49 +133,12 @@ function startSyncDetached() {
})
child.on('exit', (code, signal) => {
console.log(
`${new Date().toISOString()} ssl-sync finished code=${code} signal=${signal ?? ''}`,
`${new Date().toISOString()} ssl-sync finished script=${script} code=${code} signal=${signal ?? ''}`,
)
running = false
})
child.unref()
}
function runSyncAndWait() {
return new Promise((resolve) => {
running = true
const child = spawn(SCRIPT, [], {
stdio: ['ignore', 'pipe', 'pipe'],
env: process.env,
})
let stdout = ''
let stderr = ''
child.stdout.on('data', (d) => {
stdout += d.toString()
})
child.stderr.on('data', (d) => {
stderr += d.toString()
})
child.on('error', (err) => {
running = false
resolve({
ok: false,
code: 1,
log: err.message,
})
})
child.on('exit', (code, signal) => {
running = false
const log = `${stdout}${stderr}`.trim().slice(-4000)
console.log(
`${new Date().toISOString()} ssl-sync waited code=${code} signal=${signal ?? ''}`,
)
resolve({
ok: code === 0,
code: code ?? 1,
log,
})
})
})
return null
}
const server = createServer(async (req, res) => {
@@ -148,30 +167,42 @@ const server = createServer(async (req, res) => {
}
const wait = body && body.wait === true
const tenantApex =
typeof body.tenantApex === 'string' ? body.tenantApex.trim().toLowerCase() : ''
if (wait) {
console.log(`${new Date().toISOString()} ssl-sync wait start`)
const result = await runSyncAndWait()
if (!result.ok) {
console.log(
`${new Date().toISOString()} ssl-sync wait start tenantApex=${tenantApex || '(all)'}`,
)
const result = await spawnSync(body, true)
if (!result?.ok) {
return json(res, 500, {
status: 'failed',
message: 'SSL sync script failed',
code: result.code,
log: result.log,
message: tenantApex
? 'Tenant dashboard SSL issue failed'
: 'SSL sync script failed',
code: result?.code ?? 1,
log: result?.log ?? '',
})
}
return json(res, 200, {
status: 'ok',
message: 'SSL sync completed',
message: tenantApex
? `Tenant dashboard SSL issued for ${tenantApex}`
: 'SSL sync completed',
log: result.log,
})
}
startSyncDetached()
console.log(`${new Date().toISOString()} ssl-sync accepted`)
spawnSync(body, false)
console.log(
`${new Date().toISOString()} ssl-sync accepted tenantApex=${tenantApex || '(all)'}`,
)
return json(res, 202, {
status: 'accepted',
message: 'SSL sync started',
message: tenantApex
? `Tenant dashboard SSL started for ${tenantApex}`
: 'SSL sync started',
})
})