import http from 'node:http'; import crypto from 'node:crypto'; import { Pool } from 'pg'; const PORT = Number(process.env.PORT || 3000); const APP_BASE_URL = (process.env.APP_BASE_URL || '').replace(/\/$/, ''); const ISSUER = (process.env.KEYCLOAK_ISSUER || '').replace(/\/$/, ''); const CLIENT_ID = process.env.KEYCLOAK_CLIENT_ID || ''; const CLIENT_SECRET = process.env.KEYCLOAK_CLIENT_SECRET || ''; const DATABASE_URL = process.env.DATABASE_URL || ''; for (const [name, value] of Object.entries({ APP_BASE_URL, ISSUER, CLIENT_ID, CLIENT_SECRET, DATABASE_URL })) { if (!value) throw new Error(`Missing required environment variable: ${name}`); } const pool = new Pool({ connectionString: DATABASE_URL, max: 5 }); const sessions = new Map(); const PRIORITIES = new Set(['low', 'normal', 'high']); function esc(value) { return String(value ?? '').replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[ch]); } function parseCookies(req) { const out = {}; for (const part of (req.headers.cookie || '').split(';')) { const i = part.indexOf('='); if (i < 0) continue; out[part.slice(0, i).trim()] = decodeURIComponent(part.slice(i + 1)); } return out; } function setCookie(res, name, value, maxAge = 3600) { const cookie = `${name}=${encodeURIComponent(value)}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`; const existing = res.getHeader('Set-Cookie'); res.setHeader('Set-Cookie', existing ? [...(Array.isArray(existing) ? existing : [existing]), cookie] : cookie); } function redirect(res, location) { res.writeHead(302, { Location: location }); res.end(); } function send(res, status, body, type = 'text/html; charset=utf-8') { res.writeHead(status, { 'content-type': type, 'cache-control': 'no-store' }); res.end(body); } async function readForm(req) { let raw = ''; for await (const chunk of req) { raw += chunk; if (raw.length > 1024 * 1024) throw new Error('Request too large'); } return Object.fromEntries(new URLSearchParams(raw)); } function sessionFor(req) { const sid = parseCookies(req).crud_session; if (!sid) return null; const session = sessions.get(sid); if (!session || session.expiresAt < Date.now()) { if (sid) sessions.delete(sid); return null; } return session; } function normalisePriority(value) { const priority = String(value || 'normal').toLowerCase(); return PRIORITIES.has(priority) ? priority : 'normal'; } function priorityOptions(current = 'normal') { return ['low', 'normal', 'high'].map(value => ``).join(''); } function layout(user, content) { return ` Arrowstack Tasks

Arrowstack Tasks schema v2

Keycloak + dedicated PostgreSQL CRUD demo
${esc(user.preferred_username || user.email || 'user')}   Logout
${content}
`; } async function initDb() { await pool.query(` CREATE TABLE IF NOT EXISTS tasks ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL CHECK (char_length(title) BETWEEN 1 AND 200), completed BOOLEAN NOT NULL DEFAULT FALSE, priority TEXT NOT NULL DEFAULT 'normal' CHECK (priority IN ('low', 'normal', 'high')), created_by TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `); await pool.query(` ALTER TABLE tasks ADD COLUMN IF NOT EXISTS priority TEXT NOT NULL DEFAULT 'normal' CHECK (priority IN ('low', 'normal', 'high')) `); await pool.query(`CREATE INDEX IF NOT EXISTS idx_tasks_created_by_created_at ON tasks (created_by, created_at DESC)`); await pool.query(`CREATE INDEX IF NOT EXISTS idx_tasks_created_by_priority_created_at ON tasks (created_by, priority, created_at DESC)`); } await initDb(); console.log('database schema v2 ready'); const server = http.createServer(async (req, res) => { try { const url = new URL(req.url, APP_BASE_URL); if (url.pathname === '/health') { await pool.query('SELECT priority FROM tasks LIMIT 1'); return send(res, 200, JSON.stringify({ ok: true, database: 'connected', schema: 2 }), 'application/json; charset=utf-8'); } if (url.pathname === '/login') { const state = crypto.randomBytes(24).toString('base64url'); setCookie(res, 'oidc_state', state, 300); const auth = new URL(`${ISSUER}/protocol/openid-connect/auth`); auth.searchParams.set('client_id', CLIENT_ID); auth.searchParams.set('redirect_uri', `${APP_BASE_URL}/callback`); auth.searchParams.set('response_type', 'code'); auth.searchParams.set('scope', 'openid profile email'); auth.searchParams.set('state', state); return redirect(res, auth.toString()); } if (url.pathname === '/callback') { const cookies = parseCookies(req); const state = url.searchParams.get('state') || ''; const code = url.searchParams.get('code') || ''; if (!code || !state || cookies.oidc_state !== state) return send(res, 400, 'Invalid OAuth callback state', 'text/plain; charset=utf-8'); const tokenRes = await fetch(`${ISSUER}/protocol/openid-connect/token`, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: `${APP_BASE_URL}/callback`, client_id: CLIENT_ID, client_secret: CLIENT_SECRET }) }); const token = await tokenRes.json(); if (!tokenRes.ok || !token.access_token) return send(res, 502, `Keycloak token exchange failed (${tokenRes.status})`, 'text/plain; charset=utf-8'); const userRes = await fetch(`${ISSUER}/protocol/openid-connect/userinfo`, { headers: { Authorization: `Bearer ${token.access_token}` } }); const user = await userRes.json(); if (!userRes.ok || !user.sub) return send(res, 502, 'Keycloak userinfo failed', 'text/plain; charset=utf-8'); const sid = crypto.randomBytes(32).toString('base64url'); sessions.set(sid, { user, expiresAt: Date.now() + Math.min(Number(token.expires_in || 900), 3600) * 1000 }); setCookie(res, 'crud_session', sid, Math.min(Number(token.expires_in || 900), 3600)); setCookie(res, 'oidc_state', '', 0); return redirect(res, '/'); } if (url.pathname === '/logout') { const sid = parseCookies(req).crud_session; if (sid) sessions.delete(sid); setCookie(res, 'crud_session', '', 0); return redirect(res, '/login'); } const session = sessionFor(req); if (!session) return redirect(res, '/login'); const userKey = session.user.sub; if (req.method === 'POST' && url.pathname === '/tasks') { const form = await readForm(req); const title = String(form.title || '').trim().slice(0, 200); const priority = normalisePriority(form.priority); if (title) await pool.query('INSERT INTO tasks (title, priority, created_by) VALUES ($1, $2, $3)', [title, priority, userKey]); return redirect(res, '/'); } const match = url.pathname.match(/^\/tasks\/(\d+)\/(toggle|update|delete)$/); if (req.method === 'POST' && match) { const id = Number(match[1]); const action = match[2]; if (action === 'toggle') { await pool.query('UPDATE tasks SET completed = NOT completed, updated_at = NOW() WHERE id = $1 AND created_by = $2', [id, userKey]); } else if (action === 'update') { const form = await readForm(req); const title = String(form.title || '').trim().slice(0, 200); const priority = normalisePriority(form.priority); if (title) await pool.query('UPDATE tasks SET title = $1, priority = $2, updated_at = NOW() WHERE id = $3 AND created_by = $4', [title, priority, id, userKey]); } else if (action === 'delete') { await pool.query('DELETE FROM tasks WHERE id = $1 AND created_by = $2', [id, userKey]); } return redirect(res, '/'); } if (req.method === 'GET' && url.pathname === '/') { const result = await pool.query('SELECT id, title, completed, priority, created_at FROM tasks WHERE created_by = $1 ORDER BY created_at DESC', [userKey]); const rows = result.rows.map(task => `
${esc(task.priority)}
Created ${esc(new Date(task.created_at).toLocaleString())}
`).join(''); return send(res, 200, layout(session.user, `
${rows || '
No tasks yet. Add one above.
'}
`)); } return send(res, 404, 'Not found', 'text/plain; charset=utf-8'); } catch (err) { console.error(err); return send(res, 500, 'Internal server error', 'text/plain; charset=utf-8'); } }); server.listen(PORT, '0.0.0.0', () => console.log(`arrowstack-crud-demo listening on :${PORT}`));