Wire auth middleware into all products, add docker-compose and init-db script

- Auth middleware (JWT + API key + RBAC) copied into P3/P4/P5/P6
- All server entry points now register auth hooks + auth routes
- Webhook and Slack endpoints skip JWT auth (use HMAC/signature)
- docker-compose.yml: shared Postgres + Redis + Meilisearch, all 4 Node products as services
- init-db.sh: creates per-product databases and runs migrations
- P1 (Rust) and P2 (Go agent) run standalone, not in compose
This commit is contained in:
2026-03-01 03:10:35 +00:00
parent 762e2db9df
commit f2e0a32cc7
10 changed files with 677 additions and 2 deletions

View File

@@ -0,0 +1,122 @@
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<string, number> = { 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,
};
});
}

View File

@@ -3,6 +3,8 @@ import cors from '@fastify/cors';
import helmet from '@fastify/helmet';
import pino from 'pino';
import { config } from './config/index.js';
import { pool } from './data/db.js';
import { registerAuth, registerAuthRoutes } from './auth/middleware.js';
import { registerWebhookRoutes } from './api/webhooks.js';
import { registerIncidentRoutes } from './api/incidents.js';
import { registerNotificationRoutes } from './api/notifications.js';
@@ -14,8 +16,11 @@ const app = Fastify({ logger: true });
await app.register(cors, { origin: config.CORS_ORIGIN });
await app.register(helmet);
registerAuth(app, config.JWT_SECRET, pool);
app.get('/health', async () => ({ status: 'ok', service: 'dd0c-alert' }));
registerAuthRoutes(app, config.JWT_SECRET, pool);
registerWebhookRoutes(app);
registerIncidentRoutes(app);
registerNotificationRoutes(app);