UNPKG

longcelot-sheet-db

Version:

Google Sheets-backed staging database adapter for Node.js with schema-first design

112 lines 4.36 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createAuthRouter = createAuthRouter; const oauth_1 = require("./oauth"); function signJwt(payload, secret) { // Minimal HS256 JWT without a third-party dep — uses Node's built-in crypto const crypto = require('crypto'); const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url'); const body = Buffer.from(JSON.stringify({ ...payload, iat: Math.floor(Date.now() / 1000) })).toString('base64url'); const sig = crypto .createHmac('sha256', secret) .update(`${header}.${body}`) .digest('base64url'); return `${header}.${body}.${sig}`; } /** * Creates an Express-compatible auth router that wires up two routes: * * GET {basePath}/auth/google — redirects to Google OAuth consent screen * GET {basePath}/auth/callback — exchanges code, verifies identity, issues JWT * * @example * ```typescript * import express from 'express' * import { createAuthRouter } from 'longcelot-sheet-db' * * const auth = createAuthRouter({ * adapter, * jwtSecret: process.env.JWT_SECRET!, * frontendUrl: process.env.FRONTEND_URL!, * registrationPolicy: 'login-only', // admin-only — no self-signup * async onUser(profile, adapter) { * const ctx = adapter.withContext({ userId: 'auth', actor: 'admin', actorSheetId: process.env.ADMIN_SHEET_ID! }) * return await ctx.table('users').findOne({ where: { email: profile.email } }) * }, * }) * * app.use(auth.handler) * ``` */ function createAuthRouter(options) { const { adapter, jwtSecret, frontendUrl, onUser, registrationPolicy = 'open', basePath = '', } = options; const oauthCfg = options.oauthConfig ?? { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, redirectUri: process.env.GOOGLE_REDIRECT_URI, }; const oauth = (0, oauth_1.createLoginOAuthManager)(oauthCfg); const loginPath = `${basePath}/auth/google`; const callbackPath = `${basePath}/auth/callback`; const handler = async (req, res, next) => { const url = req.path ?? req.url ?? ''; const pathname = url.split('?')[0]; if (pathname === loginPath) { const authUrl = oauth.getAuthUrl(); res.redirect(authUrl); return; } if (pathname === callbackPath) { const code = req.query['code']; if (!code) { res.status(400).json({ error: 'Missing OAuth code' }); return; } let tokens; try { tokens = (await oauth.getTokens(code)); } catch { res.status(401).json({ error: 'Failed to exchange OAuth code' }); return; } let profile; try { const idToken = tokens['id_token']; if (!idToken) throw new Error('No id_token in OAuth response — ensure openid scope is requested'); profile = (await oauth.verifyToken(idToken)); } catch (err) { res.status(401).json({ error: `Token verification failed: ${err}` }); return; } let user; try { user = await onUser(profile, adapter); } catch (err) { res.status(500).json({ error: `onUser callback threw: ${err}` }); return; } if (user === null) { if (registrationPolicy === 'login-only') { res .status(401) .json({ error: `Access denied: '${profile.email}' is not an authorised user. Contact an admin.` }); return; } // 'open' policy — fall through with minimal profile as user payload user = { email: profile.email, name: profile.name, sub: profile.sub }; } const jwt = signJwt(user, jwtSecret); res.redirect(`${frontendUrl}?token=${jwt}`); return; } next(); }; return { handler, loginPath, callbackPath }; } //# sourceMappingURL=router.js.map