import { getConfig } from "../common/config.js"; import { getPool } from "../db/index.js"; import { verifyKey } from "../modules/core/apiKey.js"; import crypto from 'crypto'; // Bearer-token auth for the central /api/v1/* API. On success it sets // req.session.user_id (which the core router scopes queries by). It does NOT // populate req.user — the browser session routes (passport) own that — so the // session-authed dashboard/key-management routes remain login-only by design. export async function apiMiddleware(req, res, next) { let success = false; try { const apiKey = req.headers['authorization']?.split(' ')[1]; if (apiKey) { // Preferred: the hashed multi-key store (with a short in-memory cache). const found = await verifyKey(apiKey); if (found) { req.session.user_id = found.user_id; success = true; } else { // Fallback: the legacy single plaintext key in `auth` (issued at login). const client = await getPool(); try { const r = await client.query( `SELECT au.user_id FROM public.auth au WHERE au.api_key = $1`, [apiKey], ); if (r.rowCount > 0) { req.session.user_id = r.rows[0].user_id; success = true; } } finally { await client.release(); } } } } catch (e) { console.error(e); } if (!success) return res.status(401).json({ error: 'API key is invalid' }); next(); } export function getNewKey(user) { const seed = `${user.issuer}:${user.identifier}:${user.id}:${crypto.randomBytes(16).toString('hex')}`; const hash = crypto.createHash('sha256').update(seed).digest(); const apiKey = hash.toString('hex'); return apiKey; } async function createApiKeys(client, user, issuer) { const config = getConfig(); for (const type in config.appModules) { // Internal modules (e.g. dashboard) have no `app` row and mint no keys. if (config.appModules[type]?.internal) continue; const apiKey = issuer !== 'https://single' ? getNewKey(user) : config.authModules.single[type] await client.query(` INSERT INTO public.auth ( user_id, app_id, api_key ) VALUES ( $1, (SELECT id FROM public.app WHERE type = $2), $3 ) ON CONFLICT DO NOTHING; `, [ user.id, type, apiKey, ]); } } async function checkIfUserExists(client, issuer, identifier) { return await client.query(` SELECT u.id, u.photo, u.username FROM public.user u WHERE u.identifier = $1 AND u.issuer = $2 `, [ identifier, issuer, ]); } export async function getUser(client, issuer, identifier) { const res = await client.query(` SELECT u.id, u.username, u.photo, u.issuer, u.identifier, COALESCE(ut.can_view_json, FALSE) AS "canViewJson", json_agg( jsonb_build_object( 'id', a.id, 'type', a.type, 'apiKey', au.api_key ) ) AS apps FROM public.user u INNER JOIN public.auth au ON u.id = au.user_id INNER JOIN public.app a ON au.app_id = a.id LEFT JOIN public.user_type ut ON ut.id = u.user_type_id WHERE u.identifier = $1 AND u.issuer = $2 GROUP BY u.id, u.username, u.photo, u.issuer, u.identifier, ut.can_view_json `, [ identifier, issuer, ]); if (res.rowCount > 0) { const user = res.rows[0]; return user; } return null; } export async function checkUser(type, issuer, identifier, username, photo) { const client = await getPool(); let userExists = await checkIfUserExists(client, issuer, identifier); if (userExists.rowCount === 0) { if (!username || username === '') { username = identifier; } // New users default to the least-privileged role; an administrator // promotes them. Existing users keep whatever the DB already holds // (see the 000019 backfill) — checkUser never overwrites an existing type. const res = await client.query(` INSERT INTO public.user ( username, issuer, identifier, photo, type, user_type_id ) VALUES ( $1, $2, $3, $4, $5, (SELECT id FROM public.user_type WHERE value = 'standard') ) RETURNING id; `, [ username, issuer, identifier, photo, type, ]); if (res.rowCount === 0) { return null; } await createApiKeys(client, res.rows[0], issuer); } else { if (photo !== userExists.rows[0].photo || username != userExists.rows[0].username) { await client.query(` UPDATE public.user SET photo = $1, username = $2, updated_ts = NOW() WHERE id = $3 `, [ userExists.rows[0].photo, userExists.rows[0].username, userExists.rows[0].id, ]); } await createApiKeys(client, userExists.rows[0], issuer); } const user = await getUser(client, issuer, identifier); return user; } export async function initModules(app, passport) { const config = getConfig(); for (const authModule of Object.keys(config.authModules)) { const authModulePath = `../modules/auth/${authModule}.js`; const { initModule } = await import(authModulePath); await initModule(app, passport); } }