diff --git a/products/03-alert-intelligence/src/auth/middleware.ts b/products/03-alert-intelligence/src/auth/middleware.ts new file mode 100644 index 0000000..3329232 --- /dev/null +++ b/products/03-alert-intelligence/src/auth/middleware.ts @@ -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 = { 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, + }; + }); +} diff --git a/products/03-alert-intelligence/src/index.ts b/products/03-alert-intelligence/src/index.ts index 5bc1a72..61f2b49 100644 --- a/products/03-alert-intelligence/src/index.ts +++ b/products/03-alert-intelligence/src/index.ts @@ -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); diff --git a/products/04-lightweight-idp/src/auth/middleware.ts b/products/04-lightweight-idp/src/auth/middleware.ts new file mode 100644 index 0000000..3329232 --- /dev/null +++ b/products/04-lightweight-idp/src/auth/middleware.ts @@ -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 = { 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, + }; + }); +} diff --git a/products/04-lightweight-idp/src/index.ts b/products/04-lightweight-idp/src/index.ts index 3a28ce4..89f68cb 100644 --- a/products/04-lightweight-idp/src/index.ts +++ b/products/04-lightweight-idp/src/index.ts @@ -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 { registerServiceRoutes } from './api/services.js'; import { registerDiscoveryRoutes } from './api/discovery.js'; import { registerSearchRoutes } from './api/search.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-portal' })); +registerAuthRoutes(app, config.JWT_SECRET, pool); registerServiceRoutes(app); registerDiscoveryRoutes(app); registerSearchRoutes(app); diff --git a/products/05-aws-cost-anomaly/src/auth/middleware.ts b/products/05-aws-cost-anomaly/src/auth/middleware.ts new file mode 100644 index 0000000..3329232 --- /dev/null +++ b/products/05-aws-cost-anomaly/src/auth/middleware.ts @@ -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 = { 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, + }; + }); +} diff --git a/products/05-aws-cost-anomaly/src/index.ts b/products/05-aws-cost-anomaly/src/index.ts index 92d9ba4..917247e 100644 --- a/products/05-aws-cost-anomaly/src/index.ts +++ b/products/05-aws-cost-anomaly/src/index.ts @@ -2,6 +2,8 @@ import Fastify from 'fastify'; import cors from '@fastify/cors'; import pino from 'pino'; import { config } from './config/index.js'; +import { pool } from './data/db.js'; +import { registerAuth, registerAuthRoutes } from './auth/middleware.js'; import { registerAnomalyRoutes } from './api/anomalies.js'; import { registerBaselineRoutes } from './api/baselines.js'; import { registerGovernanceRoutes } from './api/governance.js'; @@ -13,8 +15,11 @@ const app = Fastify({ logger: true }); await app.register(cors, { origin: config.CORS_ORIGIN }); +registerAuth(app, config.JWT_SECRET, pool); + app.get('/health', async () => ({ status: 'ok', service: 'dd0c-cost' })); +registerAuthRoutes(app, config.JWT_SECRET, pool); registerIngestionRoutes(app); registerAnomalyRoutes(app); registerBaselineRoutes(app); diff --git a/products/06-runbook-automation/saas/src/auth/middleware.ts b/products/06-runbook-automation/saas/src/auth/middleware.ts new file mode 100644 index 0000000..3329232 --- /dev/null +++ b/products/06-runbook-automation/saas/src/auth/middleware.ts @@ -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 = { 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, + }; + }); +} diff --git a/products/06-runbook-automation/saas/src/index.ts b/products/06-runbook-automation/saas/src/index.ts index 7a09eea..bc9af8e 100644 --- a/products/06-runbook-automation/saas/src/index.ts +++ b/products/06-runbook-automation/saas/src/index.ts @@ -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 { registerRunbookRoutes } from './api/runbooks.js'; import { registerApprovalRoutes } from './api/approvals.js'; import { registerSlackRoutes } from './slackbot/handler.js'; @@ -14,10 +16,11 @@ const app = Fastify({ logger: true }); await app.register(cors, { origin: config.CORS_ORIGIN }); await app.register(helmet); -// Health check +registerAuth(app, config.JWT_SECRET, pool); + app.get('/health', async () => ({ status: 'ok', service: 'dd0c-run' })); -// API routes +registerAuthRoutes(app, config.JWT_SECRET, pool); registerRunbookRoutes(app); registerApprovalRoutes(app); registerSlackRoutes(app); diff --git a/products/docker-compose.yml b/products/docker-compose.yml new file mode 100644 index 0000000..9e4efc7 --- /dev/null +++ b/products/docker-compose.yml @@ -0,0 +1,123 @@ +# dd0c Local Development Stack +# +# Usage: docker compose up -d +# All services share one Postgres and one Redis instance. +# Caddy handles TLS and routing for *.dd0c.localhost + +services: + # --- Shared Infrastructure --- + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: dd0c + POSTGRES_PASSWORD: dd0c-dev + ports: + - "5432:5432" + volumes: + - pg_data:/var/lib/postgresql/data + - ./products/01-llm-cost-router/migrations:/docker-entrypoint-initdb.d/01-route:ro + - ./products/02-iac-drift-detection/saas/migrations:/docker-entrypoint-initdb.d/02-drift:ro + - ./products/03-alert-intelligence/migrations:/docker-entrypoint-initdb.d/03-alert:ro + - ./products/04-lightweight-idp/migrations:/docker-entrypoint-initdb.d/04-portal:ro + - ./products/05-aws-cost-anomaly/migrations:/docker-entrypoint-initdb.d/05-cost:ro + - ./products/06-runbook-automation/saas/migrations:/docker-entrypoint-initdb.d/06-run:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U dd0c"] + interval: 5s + timeout: 3s + retries: 5 + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + + meilisearch: + image: getmeili/meilisearch:v1.8 + environment: + MEILI_ENV: development + ports: + - "7700:7700" + volumes: + - meili_data:/meili_data + + # --- dd0c Products --- + # P3: Alert Intelligence + alert: + build: + context: ./products/03-alert-intelligence + dockerfile: Dockerfile + ports: + - "3003:3000" + environment: + PORT: "3000" + DATABASE_URL: postgresql://dd0c:dd0c-dev@postgres:5432/dd0c_alert + REDIS_URL: redis://redis:6379 + JWT_SECRET: dev-secret-change-me-in-production!! + LOG_LEVEL: info + depends_on: + postgres: { condition: service_healthy } + redis: { condition: service_healthy } + + # P4: Lightweight IDP / Service Catalog + portal: + build: + context: ./products/04-lightweight-idp + dockerfile: Dockerfile + ports: + - "3004:3000" + environment: + PORT: "3000" + DATABASE_URL: postgresql://dd0c:dd0c-dev@postgres:5432/dd0c_portal + REDIS_URL: redis://redis:6379 + MEILI_URL: http://meilisearch:7700 + JWT_SECRET: dev-secret-change-me-in-production!! + LOG_LEVEL: info + depends_on: + postgres: { condition: service_healthy } + redis: { condition: service_healthy } + meilisearch: { condition: service_started } + + # P5: AWS Cost Anomaly Detection + cost: + build: + context: ./products/05-aws-cost-anomaly + dockerfile: Dockerfile + ports: + - "3005:3000" + environment: + PORT: "3000" + DATABASE_URL: postgresql://dd0c:dd0c-dev@postgres:5432/dd0c_cost + REDIS_URL: redis://redis:6379 + JWT_SECRET: dev-secret-change-me-in-production!! + ANOMALY_THRESHOLD: "50" + LOG_LEVEL: info + depends_on: + postgres: { condition: service_healthy } + redis: { condition: service_healthy } + + # P6: Runbook Automation (SaaS) + run: + build: + context: ./products/06-runbook-automation/saas + dockerfile: Dockerfile + ports: + - "3006:3000" + environment: + PORT: "3000" + DATABASE_URL: postgresql://dd0c:dd0c-dev@postgres:5432/dd0c_run + REDIS_URL: redis://redis:6379 + JWT_SECRET: dev-secret-change-me-in-production!! + LOG_LEVEL: info + depends_on: + postgres: { condition: service_healthy } + redis: { condition: service_healthy } + +volumes: + pg_data: + meili_data: diff --git a/products/init-db.sh b/products/init-db.sh new file mode 100755 index 0000000..85cea41 --- /dev/null +++ b/products/init-db.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -euo pipefail + +# dd0c init-db: creates per-product databases and runs migrations +# Usage: ./init-db.sh [postgres-url] + +PG_URL="${1:-postgresql://dd0c:dd0c-dev@localhost:5432}" + +DATABASES=(dd0c_route dd0c_drift dd0c_alert dd0c_portal dd0c_cost dd0c_run) + +echo "Creating databases..." +for db in "${DATABASES[@]}"; do + psql "$PG_URL/postgres" -tc "SELECT 1 FROM pg_database WHERE datname = '$db'" | grep -q 1 \ + || psql "$PG_URL/postgres" -c "CREATE DATABASE $db" 2>/dev/null + echo " ✓ $db" +done + +echo "" +echo "Running migrations..." + +PRODUCTS=( + "01-llm-cost-router:dd0c_route" + "02-iac-drift-detection/saas:dd0c_drift" + "03-alert-intelligence:dd0c_alert" + "04-lightweight-idp:dd0c_portal" + "05-aws-cost-anomaly:dd0c_cost" + "06-runbook-automation/saas:dd0c_run" +) + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +for entry in "${PRODUCTS[@]}"; do + product="${entry%%:*}" + db="${entry##*:}" + migration_dir="$SCRIPT_DIR/$product/migrations" + + if [ -d "$migration_dir" ]; then + for sql in "$migration_dir"/*.sql; do + echo " $db ← $(basename "$sql")" + psql "$PG_URL/$db" -f "$sql" 2>/dev/null || echo " (already applied or error)" + done + fi +done + +echo "" +echo "Done. All databases ready."