diff --git a/deploy.sh b/deploy.sh index 39ef614..2ced385 100755 --- a/deploy.sh +++ b/deploy.sh @@ -346,7 +346,8 @@ check_backend_health() { check_frontend_health() { local i for ((i = 1; i <= 30; i++)); do - if remote "curl -sf http://127.0.0.1:3000/ >/dev/null"; then + # Allow 200 OK or 302/303/307 Redirect (to login) + if remote "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/ | grep -qE '^(200|302|303|307)$'"; then log "Frontend health check passed" return 0 fi diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index ea1089c..eb14a1d 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -94,117 +94,82 @@ chown -R innercontext:innercontext /opt/innercontext cat > /opt/innercontext/shared/backend/.env <<'EOF' DATABASE_URL=postgresql+psycopg://innercontext:change-me@/innercontext GEMINI_API_KEY=your-key + +# OIDC Configuration +OIDC_ISSUER=https://auth.example.com +OIDC_CLIENT_ID=innercontext-backend +OIDC_DISCOVERY_URL=https://auth.example.com/.well-known/openid-configuration +OIDC_ADMIN_GROUPS=admins +OIDC_MEMBER_GROUPS=members + +# Bootstrap Admin (Optional, used for initial setup) +# BOOTSTRAP_ADMIN_OIDC_ISSUER=https://auth.example.com +# BOOTSTRAP_ADMIN_OIDC_SUB=user-sub-from-authelia +# BOOTSTRAP_ADMIN_EMAIL=admin@example.com +# BOOTSTRAP_ADMIN_NAME="Admin User" +# BOOTSTRAP_HOUSEHOLD_NAME="My Household" EOF cat > /opt/innercontext/shared/frontend/.env.production <<'EOF' PUBLIC_API_BASE=http://127.0.0.1:8000 ORIGIN=http://innercontext.lan + +# Session and OIDC +SESSION_SECRET=generate-a-long-random-string +OIDC_ISSUER=https://auth.example.com +OIDC_CLIENT_ID=innercontext-frontend +OIDC_DISCOVERY_URL=https://auth.example.com/.well-known/openid-configuration EOF - -chmod 600 /opt/innercontext/shared/backend/.env -chmod 600 /opt/innercontext/shared/frontend/.env.production -chown innercontext:innercontext /opt/innercontext/shared/backend/.env -chown innercontext:innercontext /opt/innercontext/shared/frontend/.env.production ``` -### 4) Grant deploy sudo permissions +## OIDC Setup (Authelia) -```bash -cat > /etc/sudoers.d/innercontext-deploy << 'EOF' -innercontext ALL=(root) NOPASSWD: \ - /usr/bin/systemctl restart innercontext, \ - /usr/bin/systemctl restart innercontext-node, \ - /usr/bin/systemctl restart innercontext-pricing-worker, \ - /usr/bin/systemctl is-active innercontext, \ - /usr/bin/systemctl is-active innercontext-node, \ - /usr/bin/systemctl is-active innercontext-pricing-worker -EOF +This project uses OIDC for authentication. You need an OIDC provider like Authelia. -chmod 440 /etc/sudoers.d/innercontext-deploy -visudo -c -f /etc/sudoers.d/innercontext-deploy +### Authelia Client Configuration -# Must work without password or TTY prompt: -sudo -u innercontext sudo -n -l +Add the following to your Authelia `configuration.yml`: + +```yaml +identity_providers: + oidc: + clients: + - id: innercontext-frontend + description: InnerContext Frontend + secret: '$pbkdf2-sha512$...' # Not used for public client, but Authelia may require it + public: true + authorization_policy: one_factor + redirect_uris: + - http://innercontext.lan/auth/callback + scopes: + - openid + - profile + - email + - groups + userinfo_signed_response_alg: none + + - id: innercontext-backend + description: InnerContext Backend + secret: '$pbkdf2-sha512$...' + public: false + authorization_policy: one_factor + redirect_uris: [] + scopes: + - openid + - profile + - email + - groups + userinfo_signed_response_alg: none ``` -If `sudo -n -l` fails, deployments will fail during restart/rollback with: -`sudo: a terminal is required` or `sudo: a password is required`. +### Bootstrap Admin -### 5) Install systemd and nginx configs - -After first deploy (or after copying repo content to `/opt/innercontext/current`), install configs: - -```bash -cp /opt/innercontext/current/systemd/innercontext.service /etc/systemd/system/ -cp /opt/innercontext/current/systemd/innercontext-node.service /etc/systemd/system/ -cp /opt/innercontext/current/systemd/innercontext-pricing-worker.service /etc/systemd/system/ -systemctl daemon-reload -systemctl enable innercontext -systemctl enable innercontext-node -systemctl enable innercontext-pricing-worker - -cp /opt/innercontext/current/nginx/innercontext.conf /etc/nginx/sites-available/innercontext -ln -sf /etc/nginx/sites-available/innercontext /etc/nginx/sites-enabled/innercontext -rm -f /etc/nginx/sites-enabled/default -nginx -t && systemctl reload nginx -``` - -## Local Machine Setup - -`~/.ssh/config`: - -``` -Host innercontext - HostName - User innercontext -``` - -Ensure your public key is in `/home/innercontext/.ssh/authorized_keys`. - -## Deploy Commands - -From repository root on external machine: - -```bash -./deploy.sh # full deploy (default = all) -./deploy.sh all -./deploy.sh backend -./deploy.sh frontend -./deploy.sh list -./deploy.sh rollback -``` - -Optional overrides: - -```bash -DEPLOY_SERVER=innercontext ./deploy.sh all -DEPLOY_ROOT=/opt/innercontext ./deploy.sh backend -DEPLOY_ALLOW_DIRTY=1 ./deploy.sh frontend -``` - -## What `deploy.sh` Does - -For `backend` / `frontend` / `all`: - -1. Local checks (strict, fail-fast) -2. Acquire `/opt/innercontext/.deploy.lock` -3. Create `` release directory -4. Upload selected component(s) -5. Link shared env files in the release directory -6. `uv sync` + `alembic upgrade head` (backend scope) -7. Upload `scripts/`, `systemd/`, `nginx/` -8. Switch `current` to the prepared release -9. Restart affected services -10. Run health checks -11. Remove old releases (keep last 5) -12. Write deploy entry to `/opt/innercontext/deploy.log` - -If anything fails after promotion, script auto-rolls back to previous release. +To create the first user and household, set the `BOOTSTRAP_ADMIN_*` environment variables in the backend `.env` file and restart the backend. The backend will automatically create the user and household on startup if they don't exist. After the first successful login, you can remove these variables. ## Health Checks -- Backend: `http://127.0.0.1:8000/health-check` -- Frontend: `http://127.0.0.1:3000/` +- Backend: `http://127.0.0.1:8000/health-check` (returns 200) +- Frontend: `http://127.0.0.1:3000/` (returns 200 or 302 redirect to login) - Worker: `systemctl is-active innercontext-pricing-worker` Manual checks: diff --git a/nginx/innercontext.conf b/nginx/innercontext.conf index d75cb5d..5e9e68e 100644 --- a/nginx/innercontext.conf +++ b/nginx/innercontext.conf @@ -10,6 +10,8 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; } # SvelteKit Node server @@ -19,5 +21,7 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; } } diff --git a/scripts/healthcheck.sh b/scripts/healthcheck.sh index 9d370ff..21ddca0 100755 --- a/scripts/healthcheck.sh +++ b/scripts/healthcheck.sh @@ -25,13 +25,27 @@ log() { check_service() { local service_name=$1 local url=$2 + local allow_redirect=${3:-false} if systemctl is-active --quiet "$service_name"; then - if curl -sf --max-time "$TIMEOUT" "$url" > /dev/null 2>&1; then + local curl_opts="-s --max-time $TIMEOUT" + if [ "$allow_redirect" = false ]; then + curl_opts="$curl_opts -f" + fi + + if curl $curl_opts "$url" > /dev/null 2>&1; then log "${GREEN}✓${NC} $service_name is healthy" return 0 else - log "${YELLOW}⚠${NC} $service_name is running but not responding at $url" + # If allow_redirect is true, we check if it's a 302 + if [ "$allow_redirect" = true ]; then + local status=$(curl -s -o /dev/null -w "%{http_code}" --max-time "$TIMEOUT" "$url") + if [ "$status" = "302" ] || [ "$status" = "303" ] || [ "$status" = "307" ] || [ "$status" = "200" ]; then + log "${GREEN}✓${NC} $service_name is healthy (status $status)" + return 0 + fi + fi + log "${YELLOW}⚠${NC} $service_name is running but not responding correctly at $url" return 1 fi else @@ -45,8 +59,10 @@ backend_ok=0 frontend_ok=0 worker_ok=0 +# Backend health-check is public and should return 200 check_service "innercontext" "$BACKEND_URL" || backend_ok=1 -check_service "innercontext-node" "$FRONTEND_URL" || frontend_ok=1 +# Frontend root may redirect to login (302) +check_service "innercontext-node" "$FRONTEND_URL" true || frontend_ok=1 # Worker doesn't have HTTP endpoint, just check if it's running if systemctl is-active --quiet "innercontext-pricing-worker"; then diff --git a/scripts/validate-env.sh b/scripts/validate-env.sh index 3ca90a4..d442fc6 100755 --- a/scripts/validate-env.sh +++ b/scripts/validate-env.sh @@ -25,7 +25,7 @@ warnings=0 log_error() { echo -e "${RED}✗${NC} $1" - ((errors++)) + errors=$((errors + 1)) } log_success() { @@ -34,7 +34,7 @@ log_success() { log_warning() { echo -e "${YELLOW}⚠${NC} $1" - ((warnings++)) + warnings=$((warnings + 1)) } check_symlink() { @@ -131,6 +131,21 @@ if [ -f "$SHARED_BACKEND_ENV" ]; then check_var "$SHARED_BACKEND_ENV" "GEMINI_API_KEY" check_var "$SHARED_BACKEND_ENV" "LOG_LEVEL" true check_var "$SHARED_BACKEND_ENV" "CORS_ORIGINS" true + + # OIDC Configuration + check_var "$SHARED_BACKEND_ENV" "OIDC_ISSUER" + check_var "$SHARED_BACKEND_ENV" "OIDC_CLIENT_ID" + check_var "$SHARED_BACKEND_ENV" "OIDC_DISCOVERY_URL" + check_var "$SHARED_BACKEND_ENV" "OIDC_ADMIN_GROUPS" + check_var "$SHARED_BACKEND_ENV" "OIDC_MEMBER_GROUPS" + check_var "$SHARED_BACKEND_ENV" "OIDC_JWKS_CACHE_TTL_SECONDS" true + + # Bootstrap Admin (Optional, used for initial setup) + check_var "$SHARED_BACKEND_ENV" "BOOTSTRAP_ADMIN_OIDC_ISSUER" true + check_var "$SHARED_BACKEND_ENV" "BOOTSTRAP_ADMIN_OIDC_SUB" true + check_var "$SHARED_BACKEND_ENV" "BOOTSTRAP_ADMIN_EMAIL" true + check_var "$SHARED_BACKEND_ENV" "BOOTSTRAP_ADMIN_NAME" true + check_var "$SHARED_BACKEND_ENV" "BOOTSTRAP_HOUSEHOLD_NAME" true fi echo "" @@ -138,6 +153,12 @@ echo "=== Validating Frontend Environment Variables ===" if [ -f "$SHARED_FRONTEND_ENV" ]; then check_var "$SHARED_FRONTEND_ENV" "PUBLIC_API_BASE" check_var "$SHARED_FRONTEND_ENV" "ORIGIN" + + # Session and OIDC + check_var "$SHARED_FRONTEND_ENV" "SESSION_SECRET" + check_var "$SHARED_FRONTEND_ENV" "OIDC_ISSUER" + check_var "$SHARED_FRONTEND_ENV" "OIDC_CLIENT_ID" + check_var "$SHARED_FRONTEND_ENV" "OIDC_DISCOVERY_URL" fi echo "" diff --git a/systemd/innercontext-node.service b/systemd/innercontext-node.service index a052333..2293910 100644 --- a/systemd/innercontext-node.service +++ b/systemd/innercontext-node.service @@ -1,5 +1,8 @@ [Unit] Description=innercontext SvelteKit Node frontend +# Required env vars in .env.production: +# PUBLIC_API_BASE, ORIGIN, SESSION_SECRET +# OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_DISCOVERY_URL After=network.target [Service] diff --git a/systemd/innercontext.service b/systemd/innercontext.service index 89545cb..d27138e 100644 --- a/systemd/innercontext.service +++ b/systemd/innercontext.service @@ -1,5 +1,10 @@ [Unit] Description=innercontext FastAPI backend +# Required env vars in .env: +# DATABASE_URL, GEMINI_API_KEY +# OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_DISCOVERY_URL +# OIDC_ADMIN_GROUPS, OIDC_MEMBER_GROUPS +# (Optional) BOOTSTRAP_ADMIN_OIDC_ISSUER, BOOTSTRAP_ADMIN_OIDC_SUB, etc. After=network.target [Service]