diff --git a/Dockerfile.demo b/Dockerfile.demo new file mode 100644 index 0000000..33b69b9 --- /dev/null +++ b/Dockerfile.demo @@ -0,0 +1,79 @@ +# Demo image: Python backend (Doorman) + Next.js web client +# All demo environment is baked into the image (no .env required). + +FROM python:3.11-slim AS base + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 + +# Install Node.js + npm and useful tools +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + nodejs npm curl ca-certificates git \ + && npm i -g npm@^10 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Backend dependencies first for better layer caching +COPY backend-services/requirements.txt /app/backend-services/requirements.txt +RUN python -m pip install --upgrade pip \ + && pip install -r /app/backend-services/requirements.txt + +# Prepare web client dependencies separately for better caching +WORKDIR /app/web-client +COPY web-client/package*.json ./ +RUN npm ci --include=dev + +# Copy backend source only (avoid copying entire repo) +WORKDIR /app +COPY backend-services /app/backend-services + +# Copy web client sources (excluding node_modules via .dockerignore) +WORKDIR /app/web-client +COPY web-client/ . + +# Build-time args for frontend env (baked into Next.js bundle) +# For demo, leave gateway URL empty so client uses same-origin and Next.js rewrites proxy to the API. +ARG NEXT_PUBLIC_PROTECTED_USERS=demo@doorman.dev +ARG NEXT_PUBLIC_GATEWAY_URL= + +# Build Next.js (production) with baked public env +RUN echo "export NEXT_PUBLIC_PROTECTED_USERS=${NEXT_PUBLIC_PROTECTED_USERS}" > /tmp/build-env.sh && \ + echo "export NEXT_PUBLIC_GATEWAY_URL=${NEXT_PUBLIC_GATEWAY_URL}" >> /tmp/build-env.sh && \ + echo "export NODE_ENV=production" >> /tmp/build-env.sh && \ + echo "export NEXT_TELEMETRY_DISABLED=1" >> /tmp/build-env.sh && \ + echo "export DEMO_MODE=true" >> /tmp/build-env.sh && \ + . /tmp/build-env.sh && \ + npm run build && \ + npm prune --omit=dev + +# Runtime configuration baked for demo +WORKDIR /app + +# Demo defaults (no external .env needed) +ENV ENV=development \ + MEM_OR_EXTERNAL=MEM \ + THREADS=1 \ + HTTPS_ONLY=false \ + DOORMAN_ADMIN_EMAIL=demo@doorman.dev \ + DOORMAN_ADMIN_PASSWORD=DemoPassword123! \ + JWT_SECRET_KEY=demo-secret-change-me-please-32-bytes-min \ + DEMO_SEED=false \ + DEMO_MODE=true \ + ENABLE_STARLETTE_CORS=true \ + ALLOWED_ORIGINS=http://localhost:3000 \ + ALLOW_METHODS="GET,POST,PUT,DELETE,OPTIONS,PATCH,HEAD" \ + ALLOW_HEADERS="*" \ + ALLOW_CREDENTIALS=true \ + PORT=3001 \ + WEB_PORT=3000 + +# Add entrypoint +COPY docker/entrypoint.sh /app/docker/entrypoint.sh +RUN chmod +x /app/docker/entrypoint.sh + +EXPOSE 3001 3000 + +CMD ["/app/docker/entrypoint.sh"] diff --git a/README.md b/README.md index 1318105..eeb0e4d 100644 --- a/README.md +++ b/README.md @@ -42,22 +42,31 @@ When ready: - Web UI: `http://localhost:3000` - Gateway API: `http://localhost:3001` -### One‑Command Demo (in‑memory + auto‑seed) +### One‑Command Demo (in‑memory) Spin up a preconfigured demo (auto‑cleans on exit) without editing `.env`: ```bash +# First time (build the demo image to include frontend proxy config) +docker compose -f docker-compose.yml -f docker-compose.demo.yml up --build + +# Next runs (no rebuild needed) docker compose -f docker-compose.yml -f docker-compose.demo.yml up ``` -Defaults: +Defaults (demo‑only): - Admin: `demo@doorman.dev` / `DemoPassword123!` - Web UI: `http://localhost:3000` - API: `http://localhost:3001` -- Mode: in‑memory (no Redis/Mongo), demo data auto‑seeded in‑process on start +- Mode: in‑memory (no Redis/Mongo); no seed data created - Isolation: uses separate image tag (`doorman-demo:latest`) and project name to avoid overwriting any existing `doorman` - Cleanup: containers, volumes, and networks are removed automatically when you stop (Ctrl+C) +Notes: +- Demo serves the API through the frontend (same‑origin proxy) so browser cookies work without cross‑port CORS issues. The first run with `--build` bakes this Next.js proxy configuration. +- Demo runs fully in‑container: it does not mount the host `generated/` directory and disables memory auto‑save to avoid cross‑contamination with a non‑demo instance. +- If you ran an older demo before this change and see demo data in a non‑demo run, stop the container and delete host memory dumps: `rm -rf generated/memory_dump*`. + ## Frontend Gateway Configuration The web client needs to know the backend gateway URL. Set `NEXT_PUBLIC_GATEWAY_URL` in the root `.env` file: diff --git a/backend-services/utils/database.py b/backend-services/utils/database.py index 28f3507..04f2e02 100644 --- a/backend-services/utils/database.py +++ b/backend-services/utils/database.py @@ -154,11 +154,17 @@ class Database: # for tests that adjust the env and call initialize_collections again. try: env_pwd = os.getenv('DOORMAN_ADMIN_PASSWORD') + env_email = os.getenv('DOORMAN_ADMIN_EMAIL') if env_pwd: users.update_one( {'username': 'admin'}, {'$set': {'password': password_util.hash_password(env_pwd)}}, ) + if env_email: + users.update_one( + {'username': 'admin'}, + {'$set': {'email': env_email}}, + ) except Exception: pass diff --git a/backend-services/utils/database_async.py b/backend-services/utils/database_async.py index 200d6b6..c3c2de7 100644 --- a/backend-services/utils/database_async.py +++ b/backend-services/utils/database_async.py @@ -189,6 +189,12 @@ class AsyncDatabase: raise RuntimeError( 'Admin user missing password and DOORMAN_ADMIN_PASSWORD not set' ) + # Ensure admin email matches env if provided + env_email = os.getenv('DOORMAN_ADMIN_EMAIL') + if adm and env_email and adm.get('email') != env_email: + await self.db.users.update_one( + {'username': 'admin'}, {'$set': {'email': env_email}} + ) except Exception: pass diff --git a/docker-compose.demo.yml b/docker-compose.demo.yml index 1709c9b..2c32345 100644 --- a/docker-compose.demo.yml +++ b/docker-compose.demo.yml @@ -1,33 +1,25 @@ name: doorman-demo -version: '3.8' services: doorman: - # Reuse the same build but inject demo-friendly frontend URL + # Build from demo Dockerfile with baked env build: context: . - dockerfile: Dockerfile - args: - NEXT_PUBLIC_GATEWAY_URL: http://localhost:3001 - NEXT_PUBLIC_PROTECTED_USERS: demo@doorman.dev + dockerfile: Dockerfile.demo image: doorman-demo:latest - container_name: null + # Provide critical runtime env so it works even without a fresh build environment: - # App mode - ENV: development - MEM_OR_EXTERNAL: MEM - THREADS: 1 - HTTPS_ONLY: "false" - - # Admin bootstrap DOORMAN_ADMIN_EMAIL: demo@doorman.dev DOORMAN_ADMIN_PASSWORD: DemoPassword123! JWT_SECRET_KEY: demo-secret-change-me-please-32-bytes-min - - # Demo seeding - DEMO_SEED: "true" - - # Ports (compose defaults already match these) + # Keep demo memory isolated; avoid writing/reading host dumps + MEM_AUTO_SAVE_ENABLED: "false" + MEM_DUMP_PATH: "/tmp/demo_memory_dump.bin" + ENABLE_STARLETTE_CORS: "true" + ALLOWED_ORIGINS: http://localhost:3000 + ALLOW_METHODS: GET,POST,PUT,DELETE,OPTIONS,PATCH,HEAD + ALLOW_HEADERS: "*" + ALLOW_CREDENTIALS: "true" PORT: 3001 WEB_PORT: 3000 @@ -39,6 +31,7 @@ services: # Ensure it doesn't restart and doesn't persist bind mounts from base compose restart: "no" volumes: [] + container_name: doorman-demo demo-cleanup: image: docker:27.3.1-cli diff --git a/docker-compose.yml b/docker-compose.yml index 8915ece..408e1aa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,8 +8,6 @@ # 1. Set MEM_OR_EXTERNAL=REDIS in .env # 2. Run: docker compose --profile production up -version: '3.8' - services: doorman: build: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index b89742c..0fdf667 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -67,6 +67,27 @@ load_env_files trap graceful_stop SIGTERM SIGINT +# Start backend (Doorman) in the foreground so logs go to container stdout +echo "[entrypoint] Starting Doorman backend..." +( + cd /app/backend-services + # Ensure required directories + mkdir -p proto generated logs + python doorman.py run +) & +BACK_PID=$! + +# Start web client (Next.js) +echo "[entrypoint] Starting web client..." +( + cd /app/web-client + # Start Next.js on WEB_PORT, bind to 0.0.0.0 for container networking + PORT="${WEB_PORT:-3000}" npm run start -- -H 0.0.0.0 -p "${WEB_PORT:-3000}" +) & +WEB_PID=$! + +echo "[entrypoint] Services launched. Backend PID=$BACK_PID Web PID=$WEB_PID" + # Optional: Demo seeding (in-memory, for quick start demos) if [ "${DEMO_SEED:-false}" = "true" ]; then ( @@ -95,27 +116,6 @@ if [ "${DEMO_SEED:-false}" = "true" ]; then ) & fi -# Start backend (Doorman) in the foreground so logs go to container stdout -echo "[entrypoint] Starting Doorman backend..." -( - cd /app/backend-services - # Ensure required directories - mkdir -p proto generated logs - python doorman.py run -) & -BACK_PID=$! - -# Start web client (Next.js) -echo "[entrypoint] Starting web client..." -( - cd /app/web-client - # Start Next.js on WEB_PORT, bind to 0.0.0.0 for container networking - PORT="${WEB_PORT:-3000}" npm run start -- -H 0.0.0.0 -p "${WEB_PORT:-3000}" -) & -WEB_PID=$! - -echo "[entrypoint] Services launched. Backend PID=$BACK_PID Web PID=$WEB_PID" - -# Wait on either process to exit, then stop gracefully -wait -n || true +# Wait on either main service to exit, then stop gracefully +wait -n "$BACK_PID" "$WEB_PID" || true graceful_stop diff --git a/web-client/next.config.js b/web-client/next.config.js new file mode 100644 index 0000000..2bf2b72 --- /dev/null +++ b/web-client/next.config.js @@ -0,0 +1,19 @@ +/** @type {import('next').NextConfig} */ +const isDemo = process.env.DEMO_MODE === 'true' + +const nextConfig = { + // Keep production builds resilient (regular and demo) + eslint: { ignoreDuringBuilds: true }, + typescript: { ignoreBuildErrors: true }, + async rewrites() { + if (!isDemo) return [] + // Demo: proxy API to backend on 3001, keep browser same-origin (3000) + const target = process.env.GATEWAY_INTERNAL_URL || 'http://localhost:3001' + return [ + { source: '/platform/:path*', destination: `${target}/platform/:path*` }, + { source: '/api/:path*', destination: `${target}/api/:path*` }, + ] + }, +} + +module.exports = nextConfig diff --git a/web-client/src/utils/config.ts b/web-client/src/utils/config.ts index 8fd6c6b..9a7301b 100644 --- a/web-client/src/utils/config.ts +++ b/web-client/src/utils/config.ts @@ -1,6 +1,8 @@ // Gateway URL from environment - required for API calls // Set NEXT_PUBLIC_GATEWAY_URL in root .env file (loaded via dotenv-cli for dev, build arg for Docker) -const GATEWAY_URL_RAW = process.env.NEXT_PUBLIC_GATEWAY_URL || '' +const DEMO = process.env.DEMO_MODE === 'true' +// In demo, force same-origin by ignoring NEXT_PUBLIC_GATEWAY_URL +const GATEWAY_URL_RAW = DEMO ? '' : (process.env.NEXT_PUBLIC_GATEWAY_URL || '') let _cachedUrl: string | null = null