import "dotenv/config"; import express from "express"; import helmet from "helmet"; import path from "node:path"; import { fileURLToPath } from "node:url"; import pg from "pg"; import { createRemoteJWKSet, jwtVerify } from "jose"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); function env(name, fallback = "") { return (process.env[name] ?? fallback).toString(); } function must(name) { const v = env(name).trim(); if (!v) throw new Error(`Missing env: ${name}`); return v; } function safeIdent(s) { const v = String(s || "").trim(); if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(v)) throw new Error("Invalid TABLE identifier"); return v; } const PORT = Number(env("PORT", "8000")) || 8000; const DB_HOST = must("DB_HOST"); const DB_PORT = Number(env("DB_PORT", "5432")) || 5432; const DB_NAME = must("DB_NAME"); const DB_USER = must("DB_USER"); const DB_PASSWORD = must("DB_PASSWORD"); const TABLE = safeIdent(env("TABLE", "ncue_user") || "ncue_user"); const pool = new pg.Pool({ host: DB_HOST, port: DB_PORT, database: DB_NAME, user: DB_USER, password: DB_PASSWORD, ssl: false, max: 10, }); const app = express(); app.use( helmet({ contentSecurityPolicy: false, // keep simple for static + Auth0 }) ); app.use(express.json({ limit: "256kb" })); app.get("/healthz", async (_req, res) => { try { await pool.query("select 1 as ok"); res.json({ ok: true }); } catch (e) { res.status(500).json({ ok: false }); } }); function getBearer(req) { const h = req.headers.authorization || ""; const m = /^Bearer\s+(.+)$/i.exec(h); return m ? m[1].trim() : ""; } async function verifyIdToken(idToken, { issuer, audience }) { const jwks = createRemoteJWKSet(new URL(`${issuer}.well-known/jwks.json`)); const { payload } = await jwtVerify(idToken, jwks, { issuer, audience, }); return payload; } app.post("/api/auth/sync", async (req, res) => { try { const idToken = getBearer(req); if (!idToken) return res.status(401).json({ ok: false, error: "missing_token" }); const issuer = String(req.headers["x-auth0-issuer"] || "").trim(); const audience = String(req.headers["x-auth0-clientid"] || "").trim(); if (!issuer || !audience) return res.status(400).json({ ok: false, error: "missing_auth0_headers" }); const payload = await verifyIdToken(idToken, { issuer, audience }); const sub = String(payload.sub || "").trim(); const email = payload.email ? String(payload.email).trim().toLowerCase() : null; const name = payload.name ? String(payload.name).trim() : null; const picture = payload.picture ? String(payload.picture).trim() : null; const provider = sub.includes("|") ? sub.split("|", 1)[0] : null; if (!sub) return res.status(400).json({ ok: false, error: "missing_sub" }); const q = ` insert into public.${TABLE} (sub, email, name, picture, provider, last_login_at, updated_at) values ($1, $2, $3, $4, $5, now(), now()) on conflict (sub) do update set email = excluded.email, name = excluded.name, picture = excluded.picture, provider = excluded.provider, last_login_at = now(), updated_at = now() returning can_manage `; const r = await pool.query(q, [sub, email, name, picture, provider]); const canManage = Boolean(r.rows?.[0]?.can_manage); res.json({ ok: true, canManage }); } catch (e) { res.status(401).json({ ok: false, error: "verify_failed" }); } }); // Serve static site app.use(express.static(__dirname, { extensions: ["html"] })); app.listen(PORT, () => { // eslint-disable-next-line no-console console.log(`listening on http://localhost:${PORT}`); });