import { z } from 'zod'; import jwt from 'jsonwebtoken'; import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; import pino from 'pino'; const logger = pino({ name: 'auth' }); export interface AuthPayload { tenantId: string; userId: string; email: string; role: 'owner' | 'admin' | 'member' | 'viewer'; } /** * JWT auth middleware. Extracts tenant context from Bearer token. * Also supports API key auth via `X-API-Key` header (dd0c_ prefix). */ export function registerAuth(app: FastifyInstance, jwtSecret: string, pool: any) { app.decorateRequest('tenantId', ''); app.decorateRequest('userId', ''); app.decorateRequest('userRole', 'viewer'); app.addHook('onRequest', async (req: FastifyRequest, reply: FastifyReply) => { // Skip health check if (req.url === '/health') return; // Skip webhook endpoints (they use HMAC auth) if (req.url.startsWith('/webhooks/')) return; // Skip Slack endpoints (they use Slack signature) if (req.url.startsWith('/slack/')) return; const apiKey = req.headers['x-api-key'] as string | undefined; const authHeader = req.headers['authorization']; if (apiKey) { // API key auth: dd0c_ prefix + 32 hex chars if (!apiKey.startsWith('dd0c_') || apiKey.length !== 37) { return reply.status(401).send({ error: 'Invalid API key format' }); } const prefix = apiKey.slice(0, 13); // dd0c_ + 8 hex chars for fast lookup // TODO: Look up by prefix, bcrypt compare full key // For now, reject — real implementation needs DB lookup return reply.status(401).send({ error: 'API key auth not yet implemented' }); } if (authHeader?.startsWith('Bearer ')) { const token = authHeader.slice(7); try { const payload = jwt.verify(token, jwtSecret) as AuthPayload; (req as any).tenantId = payload.tenantId; (req as any).userId = payload.userId; (req as any).userRole = payload.role; return; } catch (err) { return reply.status(401).send({ error: 'Invalid or expired token' }); } } return reply.status(401).send({ error: 'Missing authentication' }); }); } /** * Role-based access control check. * Use in route handlers: requireRole(req, reply, 'admin') */ export function requireRole(req: FastifyRequest, reply: FastifyReply, minRole: AuthPayload['role']): boolean { const hierarchy: Record = { owner: 4, admin: 3, member: 2, viewer: 1 }; const userLevel = hierarchy[(req as any).userRole] ?? 0; const requiredLevel = hierarchy[minRole] ?? 0; if (userLevel < requiredLevel) { reply.status(403).send({ error: `Requires ${minRole} role or higher` }); return false; } return true; } /** * Generate a JWT token (for login/signup endpoints). */ export function signToken(payload: AuthPayload, secret: string, expiresIn = '24h'): string { return jwt.sign(payload, secret, { expiresIn }); } /** * Login endpoint registration. */ const loginSchema = z.object({ email: z.string().email(), password: z.string().min(8), }); const signupSchema = z.object({ email: z.string().email(), password: z.string().min(8), tenant_name: z.string().min(1).max(100), }); export function registerAuthRoutes(app: FastifyInstance, jwtSecret: string, pool: any) { app.post('/api/v1/auth/login', async (req, reply) => { const body = loginSchema.parse(req.body); // TODO: Look up user by email, bcrypt compare password // For now, return placeholder return reply.status(501).send({ error: 'Login not yet implemented' }); }); app.post('/api/v1/auth/signup', async (req, reply) => { const body = signupSchema.parse(req.body); // TODO: Create tenant + user, hash password with bcrypt return reply.status(501).send({ error: 'Signup not yet implemented' }); }); app.get('/api/v1/auth/me', async (req, reply) => { return { tenant_id: (req as any).tenantId, user_id: (req as any).userId, role: (req as any).userRole, }; }); }