diff --git a/apps/portal/Dockerfile b/apps/portal/Dockerfile index 2b5278e9..d66b5c2a 100644 --- a/apps/portal/Dockerfile +++ b/apps/portal/Dockerfile @@ -98,7 +98,7 @@ ENV NODE_ENV=production \ # Health check for container orchestration HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ - CMD node -e "fetch('http://localhost:3000/api/health').then(r=>r.ok||process.exit(1)).catch(()=>process.exit(1))" + CMD node -e "fetch('http://localhost:3000/_health').then(r=>r.ok||process.exit(1)).catch(()=>process.exit(1))" ENTRYPOINT ["dumb-init", "--"] CMD ["node", "apps/portal/server.js"] diff --git a/apps/portal/src/app/api/health/route.ts b/apps/portal/src/app/_health/route.ts similarity index 100% rename from apps/portal/src/app/api/health/route.ts rename to apps/portal/src/app/_health/route.ts diff --git a/apps/portal/src/proxy.ts b/apps/portal/src/proxy.ts index 1757af4c..1f74c8d5 100644 --- a/apps/portal/src/proxy.ts +++ b/apps/portal/src/proxy.ts @@ -84,10 +84,10 @@ export const config = { * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico, sitemap.xml, robots.txt (metadata files) - * - api/health (health check endpoint) + * - _health (health check endpoint) */ { - source: "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|api/health).*)", + source: "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|_health).*)", missing: [ { type: "header", key: "next-router-prefetch" }, { type: "header", key: "purpose", value: "prefetch" }, diff --git a/apps/portal/tsconfig.json b/apps/portal/tsconfig.json index 6827fc99..35bece79 100644 --- a/apps/portal/tsconfig.json +++ b/apps/portal/tsconfig.json @@ -9,6 +9,6 @@ "@/*": ["./src/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules", ".next"] } diff --git a/docker/Prod - Portainer/README.md b/docker/Prod - Portainer/README.md index fb78db25..aec73a6c 100644 --- a/docker/Prod - Portainer/README.md +++ b/docker/Prod - Portainer/README.md @@ -23,7 +23,7 @@ docker images | grep portal 2. Name: `customer-portal` 3. Paste contents of `docker-compose.yml` 4. Scroll down to **Environment Variables** -5. Copy variables from `stack.env.example` and fill in your production values +5. Copy variables from `env.example` and fill in your production values ### 3. Generate Secrets @@ -65,11 +65,11 @@ This setup uses `network_mode: bridge` with Docker `links` to avoid the iptables ### Service URLs (internal) -| Service | Internal URL | -|----------|-------------------------| -| Backend | http://backend:4000 | +| Service | Internal URL | +| -------- | -------------------------- | +| Backend | http://backend:4000 | | Database | postgresql://database:5432 | -| Redis | redis://cache:6379 | +| Redis | redis://cache:6379 | ### Nginx/Plesk Proxy @@ -83,6 +83,7 @@ Configure your domain to proxy to: ## Updating the Stack 1. Load new images: + ```bash docker load < portal-frontend-latest.tar docker load < portal-backend-latest.tar diff --git a/docker/Prod - Portainer/docker-compose.yml b/docker/Prod - Portainer/docker-compose.yml index aa42ad71..18442be8 100644 --- a/docker/Prod - Portainer/docker-compose.yml +++ b/docker/Prod - Portainer/docker-compose.yml @@ -25,7 +25,13 @@ services: links: - backend healthcheck: - test: ["CMD", "node", "-e", "fetch('http://localhost:3000/api/health').then(r=>r.ok||process.exit(1)).catch(()=>process.exit(1))"] + test: + [ + "CMD", + "node", + "-e", + "fetch('http://localhost:3000/_health').then(r=>r.ok||process.exit(1)).catch(()=>process.exit(1))", + ] interval: 30s timeout: 10s start_period: 40s @@ -46,36 +52,40 @@ services: - APP_BASE_URL=${APP_BASE_URL} - BFF_PORT=4000 - PORT=4000 - + # Database - use "database" as host (via links) - DATABASE_URL=postgresql://${POSTGRES_USER:-portal}:${POSTGRES_PASSWORD}@database:5432/${POSTGRES_DB:-portal_prod}?schema=public - + # Redis - use "cache" as host (via links) - REDIS_URL=redis://cache:6379/0 - + # Security - JWT_SECRET=${JWT_SECRET} - JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-7d} + - JWT_SECRET_PREVIOUS=${JWT_SECRET_PREVIOUS} + - JWT_ISSUER=${JWT_ISSUER} + - JWT_AUDIENCE=${JWT_AUDIENCE} - BCRYPT_ROUNDS=${BCRYPT_ROUNDS:-12} - CORS_ORIGIN=${CORS_ORIGIN} - TRUST_PROXY=true - CSRF_SECRET_KEY=${CSRF_SECRET_KEY} - + # Auth - AUTH_ALLOW_REDIS_TOKEN_FAILOPEN=${AUTH_ALLOW_REDIS_TOKEN_FAILOPEN:-false} - AUTH_REQUIRE_REDIS_FOR_TOKENS=${AUTH_REQUIRE_REDIS_FOR_TOKENS:-false} + - AUTH_BLACKLIST_FAIL_CLOSED=${AUTH_BLACKLIST_FAIL_CLOSED:-false} - AUTH_MAINTENANCE_MODE=${AUTH_MAINTENANCE_MODE:-false} - + # Rate Limiting - RATE_LIMIT_TTL=${RATE_LIMIT_TTL:-60} - RATE_LIMIT_LIMIT=${RATE_LIMIT_LIMIT:-100} - EXPOSE_VALIDATION_ERRORS=false - + # WHMCS - WHMCS_BASE_URL=${WHMCS_BASE_URL} - WHMCS_API_IDENTIFIER=${WHMCS_API_IDENTIFIER} - WHMCS_API_SECRET=${WHMCS_API_SECRET} - + # Salesforce - SF_LOGIN_URL=${SF_LOGIN_URL} - SF_CLIENT_ID=${SF_CLIENT_ID} @@ -83,25 +93,25 @@ services: - SF_EVENTS_ENABLED=${SF_EVENTS_ENABLED:-true} - SF_PRIVATE_KEY_BASE64=${SF_PRIVATE_KEY_BASE64} - SF_PRIVATE_KEY_PATH=/app/secrets/sf-private.key - + # Freebit - FREEBIT_BASE_URL=${FREEBIT_BASE_URL:-https://i1.mvno.net/emptool/api} - FREEBIT_OEM_ID=${FREEBIT_OEM_ID:-PASI} - FREEBIT_OEM_KEY=${FREEBIT_OEM_KEY} - + # Email - EMAIL_ENABLED=${EMAIL_ENABLED:-true} - EMAIL_FROM=${EMAIL_FROM:-no-reply@asolutions.jp} - EMAIL_FROM_NAME=${EMAIL_FROM_NAME:-Assist Solutions} - SENDGRID_API_KEY=${SENDGRID_API_KEY} - + # Portal - PORTAL_PRICEBOOK_ID=${PORTAL_PRICEBOOK_ID} - PORTAL_PRICEBOOK_NAME=${PORTAL_PRICEBOOK_NAME:-Portal} - + # Logging - LOG_LEVEL=${LOG_LEVEL:-info} - + # Enable automatic database migrations on startup - RUN_MIGRATIONS=true restart: unless-stopped @@ -117,7 +127,13 @@ services: # - Database migration when RUN_MIGRATIONS=true # - Waiting for dependencies (nc checks in entrypoint) healthcheck: - test: ["CMD", "node", "-e", "fetch('http://localhost:4000/health').then(r=>r.ok||process.exit(1)).catch(()=>process.exit(1))"] + test: + [ + "CMD", + "node", + "-e", + "fetch('http://localhost:4000/health').then(r=>r.ok||process.exit(1)).catch(()=>process.exit(1))", + ] interval: 30s timeout: 10s start_period: 60s @@ -151,7 +167,19 @@ services: cache: image: redis:7-alpine container_name: portal-cache - command: ["redis-server", "--save", "60", "1", "--loglevel", "warning", "--maxmemory", "256mb", "--maxmemory-policy", "noeviction"] + command: + [ + "redis-server", + "--save", + "60", + "1", + "--loglevel", + "warning", + "--maxmemory", + "256mb", + "--maxmemory-policy", + "noeviction", + ] volumes: - redis_data:/data restart: unless-stopped diff --git a/docker/Prod - Portainer/env.example b/docker/Prod - Portainer/env.example index 8bfbbf0b..dec25d57 100644 --- a/docker/Prod - Portainer/env.example +++ b/docker/Prod - Portainer/env.example @@ -33,13 +33,17 @@ POSTGRES_PASSWORD= # ----------------------------------------------------------------------------- # Generate with: openssl rand -base64 32 JWT_SECRET= +JWT_SECRET_PREVIOUS= JWT_EXPIRES_IN=7d +JWT_ISSUER=customer-portal +JWT_AUDIENCE=portal BCRYPT_ROUNDS=12 CSRF_SECRET_KEY= # Auth Settings AUTH_ALLOW_REDIS_TOKEN_FAILOPEN=false AUTH_REQUIRE_REDIS_FOR_TOKENS=false +AUTH_BLACKLIST_FAIL_CLOSED=false AUTH_MAINTENANCE_MODE=false # Rate Limiting diff --git a/docker/Prod - Portainer/update-stack.sh b/docker/Prod - Portainer/update-stack.sh index ac8dc0e8..40fb8e18 100755 --- a/docker/Prod - Portainer/update-stack.sh +++ b/docker/Prod - Portainer/update-stack.sh @@ -34,6 +34,8 @@ echo "" # Look for image files FRONTEND_TAR="${IMAGES_DIR}/portal-frontend-${TAG}.tar" BACKEND_TAR="${IMAGES_DIR}/portal-backend-${TAG}.tar" +FRONTEND_TARGZ="${IMAGES_DIR}/portal-frontend-${TAG}.tar.gz" +BACKEND_TARGZ="${IMAGES_DIR}/portal-backend-${TAG}.tar.gz" # Also check alternative naming if [[ ! -f "$FRONTEND_TAR" ]]; then @@ -43,20 +45,33 @@ if [[ ! -f "$BACKEND_TAR" ]]; then BACKEND_TAR="${IMAGES_DIR}/portal-backend.${TAG}.tar" fi +if [[ ! -f "$FRONTEND_TARGZ" ]]; then + FRONTEND_TARGZ="${IMAGES_DIR}/portal-frontend.${TAG}.tar.gz" +fi +if [[ ! -f "$BACKEND_TARGZ" ]]; then + BACKEND_TARGZ="${IMAGES_DIR}/portal-backend.${TAG}.tar.gz" +fi + # Load frontend -if [[ -f "$FRONTEND_TAR" ]]; then +if [[ -f "$FRONTEND_TARGZ" ]]; then + log "Loading frontend image from: $FRONTEND_TARGZ" + gunzip -c "$FRONTEND_TARGZ" | docker load +elif [[ -f "$FRONTEND_TAR" ]]; then log "Loading frontend image from: $FRONTEND_TAR" docker load -i "$FRONTEND_TAR" else - warn "Frontend tarball not found: $FRONTEND_TAR" + warn "Frontend tarball not found: $FRONTEND_TAR or $FRONTEND_TARGZ" fi # Load backend -if [[ -f "$BACKEND_TAR" ]]; then +if [[ -f "$BACKEND_TARGZ" ]]; then + log "Loading backend image from: $BACKEND_TARGZ" + gunzip -c "$BACKEND_TARGZ" | docker load +elif [[ -f "$BACKEND_TAR" ]]; then log "Loading backend image from: $BACKEND_TAR" docker load -i "$BACKEND_TAR" else - warn "Backend tarball not found: $BACKEND_TAR" + warn "Backend tarball not found: $BACKEND_TAR or $BACKEND_TARGZ" fi echo "" diff --git a/scripts/plesk-deploy.sh b/scripts/plesk-deploy.sh index c8608c82..a2e3cedd 100755 --- a/scripts/plesk-deploy.sh +++ b/scripts/plesk-deploy.sh @@ -16,7 +16,7 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # Default paths (override via env vars) REPO_PATH="${REPO_PATH:-/var/www/vhosts/yourdomain.com/git/customer-portal}" -COMPOSE_FILE="${COMPOSE_FILE:-$PROJECT_ROOT/docker/portainer/docker-compose.yml}" +COMPOSE_FILE="${COMPOSE_FILE:-$PROJECT_ROOT/docker/Prod - Portainer/docker-compose.yml}" ENV_FILE="${ENV_FILE:-$PROJECT_ROOT/.env}" # Image settings