Add social quick login and user sync API
Add quick provider login buttons (Auth0 connections), an API to upsert users into Postgres and gate admin via can_manage, plus schema and Node server. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
128
server.js
Normal file
128
server.js
Normal file
@@ -0,0 +1,128 @@
|
||||
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}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user