feature: new dashboard workos auth for dashboard login, dashboard optional in site config, sync server for dashboard, workos and dashboard docs updated

This commit is contained in:
Wayne Sutton
2025-12-29 22:11:52 -08:00
parent f6c9478c9d
commit e8d09fcce2
47 changed files with 15058 additions and 193 deletions

224
scripts/sync-server.ts Normal file
View File

@@ -0,0 +1,224 @@
#!/usr/bin/env npx tsx
/**
* Sync Server
*
* Local HTTP server for executing sync commands from the Dashboard UI.
* Runs on localhost:3001 with optional token authentication.
*
* Usage:
* npm run sync-server
*
* Security:
* - Binds to localhost only (not accessible from network)
* - Optional token auth via SYNC_TOKEN env var
* - Whitelisted commands only
*/
import { spawn } from "child_process";
import http from "http";
import dotenv from "dotenv";
// Load environment variables
dotenv.config({ path: ".env.local" });
dotenv.config();
const PORT = 3001;
const HOST = "127.0.0.1"; // Localhost only for security
// Optional token for authentication (set in .env.local)
const SYNC_TOKEN = process.env.SYNC_TOKEN || "";
// Whitelist of allowed sync commands
const ALLOWED_COMMANDS: Record<string, { script: string; env?: Record<string, string> }> = {
"sync": { script: "sync" },
"sync:prod": { script: "sync:prod" },
"sync:discovery": { script: "sync:discovery" },
"sync:discovery:prod": { script: "sync:discovery:prod" },
"sync:all": { script: "sync:all" },
"sync:all:prod": { script: "sync:all:prod" },
};
// CORS headers for local development
function setCorsHeaders(res: http.ServerResponse) {
res.setHeader("Access-Control-Allow-Origin", "http://localhost:5173");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Sync-Token");
}
// Verify authentication token
function verifyAuth(req: http.IncomingMessage): boolean {
// If no token configured, allow all requests (dev mode)
if (!SYNC_TOKEN) {
return true;
}
const token = req.headers["x-sync-token"];
return token === SYNC_TOKEN;
}
// Parse JSON body from request
async function parseBody(req: http.IncomingMessage): Promise<Record<string, unknown>> {
return new Promise((resolve, reject) => {
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", () => {
try {
resolve(body ? JSON.parse(body) : {});
} catch {
reject(new Error("Invalid JSON"));
}
});
req.on("error", reject);
});
}
// Execute npm script and stream output
function executeScript(
scriptName: string,
res: http.ServerResponse,
): void {
const config = ALLOWED_COMMANDS[scriptName];
if (!config) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: `Unknown command: ${scriptName}` }));
return;
}
// Set headers for streaming response
res.writeHead(200, {
"Content-Type": "text/plain; charset=utf-8",
"Transfer-Encoding": "chunked",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
});
// Write initial message
res.write(`> npm run ${config.script}\n\n`);
// Spawn the npm process
const child = spawn("npm", ["run", config.script], {
cwd: process.cwd(),
env: { ...process.env, ...config.env },
shell: true,
});
// Stream stdout
child.stdout.on("data", (data: Buffer) => {
res.write(data.toString());
});
// Stream stderr
child.stderr.on("data", (data: Buffer) => {
res.write(data.toString());
});
// Handle process completion
child.on("close", (code) => {
if (code === 0) {
res.write(`\n[Done] Command completed successfully.\n`);
} else {
res.write(`\n[Error] Command exited with code ${code}.\n`);
}
res.end();
});
// Handle process errors
child.on("error", (err) => {
res.write(`\n[Error] Failed to execute command: ${err.message}\n`);
res.end();
});
}
// Main request handler
async function handleRequest(
req: http.IncomingMessage,
res: http.ServerResponse,
): Promise<void> {
setCorsHeaders(res);
// Handle preflight OPTIONS request
if (req.method === "OPTIONS") {
res.writeHead(204);
res.end();
return;
}
const url = new URL(req.url || "/", `http://${HOST}:${PORT}`);
const path = url.pathname;
// Health check endpoint
if (path === "/health" && req.method === "GET") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok", commands: Object.keys(ALLOWED_COMMANDS) }));
return;
}
// Execute sync command
if (path === "/api/sync" && req.method === "POST") {
// Verify authentication
if (!verifyAuth(req)) {
res.writeHead(401, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Unauthorized" }));
return;
}
try {
const body = await parseBody(req);
const command = body.command as string;
if (!command || !ALLOWED_COMMANDS[command]) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({
error: "Invalid command",
allowed: Object.keys(ALLOWED_COMMANDS)
}));
return;
}
executeScript(command, res);
} catch (error) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Invalid request body" }));
}
return;
}
// 404 for other routes
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Not found" }));
}
// Create and start server
const server = http.createServer((req, res) => {
handleRequest(req, res).catch((error) => {
console.error("Request error:", error);
if (!res.headersSent) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Internal server error" }));
}
});
});
server.listen(PORT, HOST, () => {
console.log(`\n Sync Server running at http://${HOST}:${PORT}`);
console.log(`\n Available commands:`);
Object.keys(ALLOWED_COMMANDS).forEach((cmd) => {
console.log(` - ${cmd}`);
});
if (SYNC_TOKEN) {
console.log(`\n Token authentication: enabled`);
} else {
console.log(`\n Token authentication: disabled (set SYNC_TOKEN in .env.local to enable)`);
}
console.log(`\n Use with Dashboard at http://localhost:5173/dashboard\n`);
});
// Handle graceful shutdown
process.on("SIGINT", () => {
console.log("\n Shutting down sync server...");
server.close(() => {
process.exit(0);
});
});