|
21 | 21 |
|
22 | 22 | import { createServer } from "node:http"; |
23 | 23 | import { execSync } from "node:child_process"; |
24 | | -import { readFileSync, existsSync, readdirSync } from "node:fs"; |
| 24 | +import { readFileSync, readlinkSync, existsSync, readdirSync } from "node:fs"; |
25 | 25 | import { timingSafeEqual } from "node:crypto"; |
26 | 26 | import { join } from "node:path"; |
27 | 27 | import express from "express"; |
@@ -254,6 +254,81 @@ app.get("/config", (_req, res) => { |
254 | 254 | res.json(config); |
255 | 255 | }); |
256 | 256 |
|
| 257 | +// ββ Actions βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| 258 | + |
| 259 | +/** Require auth token for all POST actions (even if GET routes are open). */ |
| 260 | +function requireToken(req, res) { |
| 261 | + if (!TOKEN) { |
| 262 | + res.status(403).json({ error: "No BAUDBOT_CP_TOKEN configured β POST actions disabled" }); |
| 263 | + return false; |
| 264 | + } |
| 265 | + const auth = req.headers.authorization || ""; |
| 266 | + const match = auth.match(/^Bearer\s+(.+)$/i); |
| 267 | + if (!match || !tokenMatches(match[1])) { |
| 268 | + res.status(401).json({ error: "unauthorized" }); |
| 269 | + return false; |
| 270 | + } |
| 271 | + return true; |
| 272 | +} |
| 273 | + |
| 274 | +app.post("/restart-bridge", (req, res) => { |
| 275 | + if (!requireToken(req, res)) return; |
| 276 | + try { |
| 277 | + // Kill existing bridge |
| 278 | + run(`sudo -u ${AGENT_USER} env TMUX_TMPDIR=${AGENT_HOME}/.tmux-sock tmux kill-session -t slack-bridge 2>/dev/null`); |
| 279 | + |
| 280 | + // Find the control-agent socket UUID |
| 281 | + const aliasPath = join(AGENT_HOME, ".pi", "session-control", "control-agent.alias"); |
| 282 | + let uuid = null; |
| 283 | + try { |
| 284 | + const target = readlinkSync(aliasPath); |
| 285 | + uuid = target.replace(".sock", ""); |
| 286 | + } catch {} |
| 287 | + |
| 288 | + if (!uuid) { |
| 289 | + return res.json({ ok: false, error: "Could not find control-agent socket UUID" }); |
| 290 | + } |
| 291 | + |
| 292 | + // Start bridge in tmux |
| 293 | + const cmd = `sudo -u ${AGENT_USER} env TMUX_TMPDIR=${AGENT_HOME}/.tmux-sock tmux new-session -d -s slack-bridge "export PATH=${AGENT_HOME}/.varlock/bin:${AGENT_HOME}/opt/node-v22.14.0-linux-x64/bin:\\$PATH && export PI_SESSION_ID=${uuid} && cd ${AGENT_HOME}/runtime/slack-bridge && exec varlock run --path ${AGENT_HOME}/.config/ -- node bridge.mjs"`; |
| 294 | + run(cmd, 10000); |
| 295 | + res.json({ ok: true, action: "restart-bridge", uuid }); |
| 296 | + } catch (err) { |
| 297 | + res.status(500).json({ ok: false, error: err.message }); |
| 298 | + } |
| 299 | +}); |
| 300 | + |
| 301 | +app.post("/kill-session/:name", (req, res) => { |
| 302 | + if (!requireToken(req, res)) return; |
| 303 | + const name = req.params.name; |
| 304 | + // Validate session name (alphanumeric, hyphens, underscores only) |
| 305 | + if (!/^[a-zA-Z0-9_-]+$/.test(name)) { |
| 306 | + return res.status(400).json({ error: "Invalid session name" }); |
| 307 | + } |
| 308 | + try { |
| 309 | + const result = run(`sudo -u ${AGENT_USER} env TMUX_TMPDIR=${AGENT_HOME}/.tmux-sock tmux kill-session -t ${name} 2>&1`); |
| 310 | + res.json({ ok: true, action: "kill-session", session: name, output: result }); |
| 311 | + } catch (err) { |
| 312 | + res.status(500).json({ ok: false, error: err.message }); |
| 313 | + } |
| 314 | +}); |
| 315 | + |
| 316 | +app.post("/deploy", (req, res) => { |
| 317 | + if (!requireToken(req, res)) return; |
| 318 | + try { |
| 319 | + // deploy.sh runs as the current (admin) user |
| 320 | + const srcDir = run("dirname $(dirname $(readlink -f /proc/self/exe || echo /usr/local/bin/baudbot))") || join(process.env.HOME || "/root", "baudbot"); |
| 321 | + const deployScript = join(srcDir, "bin", "deploy.sh"); |
| 322 | + if (!existsSync(deployScript)) { |
| 323 | + return res.status(404).json({ ok: false, error: `deploy.sh not found at ${deployScript}` }); |
| 324 | + } |
| 325 | + const output = run(`bash ${deployScript} 2>&1`, 30000); |
| 326 | + res.json({ ok: true, action: "deploy", output }); |
| 327 | + } catch (err) { |
| 328 | + res.status(500).json({ ok: false, error: err.message }); |
| 329 | + } |
| 330 | +}); |
| 331 | + |
257 | 332 | // ββ Dashboard βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
258 | 333 |
|
259 | 334 | app.get("/", (_req, res) => res.redirect("/dashboard")); |
|
0 commit comments