longcelot-sheet-db
Version:
Google Sheets-backed staging database adapter for Node.js with schema-first design
112 lines • 4.36 kB
JavaScript
;
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