diff --git a/docker-compose.yml b/docker-compose.yml index cfd6164..6c7f70d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,8 +57,12 @@ services: OLLAMA_EMBED_MODEL: nomic-embed-text OLLAMA_LLM_MODEL: qwen3:1.7b NODE_ENV: production - NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000} - NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-dev-secret-change-in-production} + AUTH_SECRET: ${AUTH_SECRET} + AUTH_TRUST_HOST: "true" + AUTH_MICROSOFT_ENTRA_ID_ID: ${AUTH_MICROSOFT_ENTRA_ID_ID} + AUTH_MICROSOFT_ENTRA_ID_SECRET: ${AUTH_MICROSOFT_ENTRA_ID_SECRET} + AUTH_MICROSOFT_ENTRA_ID_TENANT_ID: ${AUTH_MICROSOFT_ENTRA_ID_TENANT_ID} + CRON_SECRET: ${CRON_SECRET:-change-me} volumes: - uploads_data:/data/uploads depends_on: @@ -66,6 +70,25 @@ services: condition: service_healthy ollama: condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/api/health || exit 1"] + interval: 15s + timeout: 5s + retries: 3 + start_period: 30s + profiles: + - production + + # ─── Cloudflare Tunnel (HTTPS access without port forwarding) ── + tunnel: + image: cloudflare/cloudflared:latest + restart: unless-stopped + command: tunnel run + environment: + TUNNEL_TOKEN: ${CLOUDFLARE_TUNNEL_TOKEN} + depends_on: + app: + condition: service_healthy profiles: - production diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..a3e840f --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,88 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + const checks: Record = {}; + let healthy = true; + + // 1. Database connectivity + try { + await prisma.$queryRawUnsafe("SELECT 1"); + checks.database = { status: "ok" }; + } catch (e: any) { + checks.database = { status: "fail", detail: e.message }; + healthy = false; + } + + // 2. pgvector extension + try { + const result: any[] = await prisma.$queryRawUnsafe( + "SELECT extversion FROM pg_extension WHERE extname = 'vector'" + ); + if (result.length > 0) { + checks.pgvector = { status: "ok", detail: `v${result[0].extversion}` }; + } else { + checks.pgvector = { status: "fail", detail: "extension not installed" }; + healthy = false; + } + } catch { + checks.pgvector = { status: "fail", detail: "query failed" }; + healthy = false; + } + + // 3. Organization exists + try { + const orgCount = await prisma.organization.count(); + if (orgCount > 0) { + checks.organization = { status: "ok", detail: `${orgCount} org(s)` }; + } else { + checks.organization = { status: "fail", detail: "no organizations found" }; + healthy = false; + } + } catch { + checks.organization = { status: "fail", detail: "query failed" }; + healthy = false; + } + + // 4. Pipeline templates exist + try { + const templateCount = await prisma.pipelineTemplate.count(); + checks.pipeline_templates = { + status: templateCount > 0 ? "ok" : "fail", + detail: `${templateCount} template(s)`, + }; + if (templateCount === 0) healthy = false; + } catch { + checks.pipeline_templates = { status: "fail", detail: "query failed" }; + healthy = false; + } + + // 5. DEV_BYPASS_AUTH safety + const devBypassActive = + process.env.DEV_BYPASS_AUTH === "true" && process.env.NODE_ENV === "production"; + if (devBypassActive) { + checks.dev_bypass = { status: "fail", detail: "DEV_BYPASS_AUTH=true in production!" }; + healthy = false; + } else { + checks.dev_bypass = { status: "ok" }; + } + + // 6. AUTH_SECRET set (required for next-auth v5) + if (process.env.NODE_ENV === "production" && !process.env.AUTH_SECRET) { + checks.auth_secret = { status: "fail", detail: "AUTH_SECRET not set" }; + healthy = false; + } else { + checks.auth_secret = { status: "ok" }; + } + + return NextResponse.json( + { + status: healthy ? "healthy" : "unhealthy", + checks, + timestamp: new Date().toISOString(), + }, + { status: healthy ? 200 : 503 } + ); +} diff --git a/src/middleware.ts b/src/middleware.ts index 25dbe35..397e871 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -11,9 +11,10 @@ export function middleware(request: NextRequest) { const isAuthPage = pathname.startsWith("/login"); const isPendingPage = pathname.startsWith("/pending"); const isApiAuth = pathname.startsWith("/api/auth"); + const isHealthCheck = pathname === "/api/health"; - // Always allow auth API routes - if (isApiAuth) { + // Always allow auth API routes and health checks + if (isApiAuth || isHealthCheck) { return NextResponse.next(); }