diff --git a/.env.example b/.env.example index 51a79b8..f36cfd4 100644 --- a/.env.example +++ b/.env.example @@ -28,17 +28,29 @@ JITSI_INTERNAL_AUTH_PASSWORD=changeme KEYDB_PASSWORD= MEILISEARCH_API_KEY=changeme TYPESENSE_API_KEY=changeme -# Cloudflare Tunnel — dev local exposé publiquement (npm run expose) +# Cloudflare Tunnel — dev local exposé publiquement (./deploy/expose.sh) # CLOUDFLARE_TUNNEL_TOKEN= -# CLOUDFLARE_TUNNEL_PUBLIC_URL=https://dev.ultispace.fr +# CLOUDFLARE_TUNNEL_PUBLIC_URL dérivé de SUITE_ORIGIN (voir section Public suite URL) + +# ----------------------------------------------------------------------------- +# Public suite URL — modifier PUBLIC_HOST + SECURE pour changer de domaine +# ----------------------------------------------------------------------------- +# Nom d'hôte public uniquement (sans schéma) : dev.example.com ou localhost +PUBLIC_HOST=localhost +# "s" → https/wss (tunnel Cloudflare, prod) ; vide → http/ws (nginx local :80) +SECURE= +# Alias nginx / compose (expansion {{PUBLIC_HOST}}) +DOMAIN={{PUBLIC_HOST}} +# Origine publique dérivée (http:// ou https:// + hôte) +SUITE_ORIGIN=http{{SECURE}}://{{PUBLIC_HOST}} +CLOUDFLARE_TUNNEL_PUBLIC_URL={{SUITE_ORIGIN}} # ----------------------------------------------------------------------------- # General # ----------------------------------------------------------------------------- -DOMAIN=localhost ULTID_PORT=8080 # Origines navigateur autorisees (web app sur autre port/origine que l'API). -# Vide = auto : localhost/127.0.0.1/LAN prive en dev ; http(s)://${DOMAIN} en prod. +# Vide = auto : localhost/127.0.0.1/LAN prive en dev ; SUITE_ORIGIN en prod. # Exemple dev explicite : ULTID_CORS_ALLOWED_ORIGINS=http://localhost:3004,http://127.0.0.1:3004 # ULTID_CORS_ALLOWED_ORIGINS= @@ -108,7 +120,9 @@ AUTHENTIK_POSTGRESQL__NAME=authentik AUTHENTIK_REDIS__HOST=keydb AUTHENTIK_WEB__PATH=/auth/ # URL publique affichee dans les redirects OIDC (navigateur) -AUTHENTIK_HOST=http://{{DOMAIN}} +AUTHENTIK_HOST=http{{SECURE}}://{{PUBLIC_HOST}} +# Authentik provisioning : true quand SECURE=s (sinon déduit de AUTHENTIK_HOST) +# AUTHENTIK_PUBLIC_HTTPS=true # API interne (ultid → authentik-server) pour provisionner les apps OIDC au demarrage AUTHENTIK_API_URL=http://authentik-server:9000 # Token admin Authentik (Flows & Stages → Tokens) — active le provisioning API ultid @@ -122,8 +136,8 @@ AUTHENTIK_API_URL=http://authentik-server:9000 # URL interne (ultid → nginx Nextcloud, racine WebDAV) NEXTCLOUD_URL=http://nextcloud:80 # URL publique UI (edge nginx /cloud/) — aussi base Destination WebDAV MOVE/COPY -NC_PUBLIC_URL=http://{{DOMAIN}}/cloud -NC_OVERWRITE_PROTOCOL=http +NC_PUBLIC_URL=http{{SECURE}}://{{PUBLIC_HOST}}/cloud +NC_OVERWRITE_PROTOCOL=http{{SECURE}} NC_ADMIN_USER=admin # NC_ADMIN_PASSWORD — defini dans la section Secrets # Exemple externe : @@ -150,7 +164,7 @@ NC_S3_SSL=false # ----------------------------------------------------------------------------- ONLYOFFICE_ENABLED=false ONLYOFFICE_URL=http://onlyoffice -ONLYOFFICE_PUBLIC_URL=http://{{DOMAIN}}/office +ONLYOFFICE_PUBLIC_URL=http{{SECURE}}://{{PUBLIC_HOST}}/office # URL ultid joignable depuis le conteneur OnlyOffice (fetch doc + callback) ONLYOFFICE_API_INTERNAL_URL=http://ultid:8080 # URL Nextcloud joignable depuis OnlyOffice (host nginx Docker). Nginx route /index.php/* → Nextcloud. @@ -159,15 +173,15 @@ ONLYOFFICE_API_INTERNAL_URL=http://ultid:8080 ONLYOFFICE_JWT_SECRET=changeme-onlyoffice-jwt ONLYOFFICE_OIDC_CLIENT_ID=ulti-onlyoffice # ONLYOFFICE_OIDC_CLIENT_SECRET — defini dans la section Secrets -ULTID_PUBLIC_URL=http://{{DOMAIN}} +ULTID_PUBLIC_URL=http{{SECURE}}://{{PUBLIC_HOST}} # Base URL for public share links (default: {ULTID_PUBLIC_URL}/drive → /drive/s/{token}) -# DRIVE_PUBLIC_URL=http://{{DOMAIN}}/drive +# DRIVE_PUBLIC_URL=http{{SECURE}}://{{PUBLIC_HOST}}/drive # ----------------------------------------------------------------------------- # Rich text editor (TipTap + Hocuspocus) # ----------------------------------------------------------------------------- RICHTEXT_ENABLED=true -HOCUSPOCUS_PUBLIC_URL=ws://{{DOMAIN}}/collab +HOCUSPOCUS_PUBLIC_URL=ws{{SECURE}}://{{PUBLIC_HOST}}/collab HOCUSPOCUS_SECRET=changeme-hocuspocus-secret RICHTEXT_STORAGE_MODE=sidecar # RICHTEXT_EXPORT_MIRROR=docx @@ -183,6 +197,7 @@ AI_GATEWAY_API_KEY=ulti-gateway BYPASS_MODEL_ACCESS_CONTROL=true WEBUI_NAME=UltiAI AI_ASSISTANT_PUBLIC_PATH=/ai +OPENWEBUI_PUBLIC_URL=http{{SECURE}}://{{PUBLIC_HOST}}/ai ULTIMAIL_MCP_URL=http://ultimail-mcp:3100 # Public MCP endpoint (nginx → ultid → ultimail-mcp): /api/v1/ai/mcp # OpenWebUI uses AI_GATEWAY_API_KEY + forwarded X-OpenWebUI-User-* headers for per-user tokens. @@ -202,7 +217,7 @@ JITSI_ENABLED=true JITSI_DOMAIN=meet.jitsi JITSI_APP_ID=ulti # JITSI_APP_SECRET — defini dans la section Secrets -JITSI_PUBLIC_URL=https://{{DOMAIN}}/meet +JITSI_PUBLIC_URL=http{{SECURE}}://{{PUBLIC_HOST}}/meet # Secret partagé avec Jigasi pour POST /api/v1/meet/transcripts MEET_TRANSCRIPT_WEBHOOK_SECRET=changeme-meet-transcript-secret # Modèle Faster Whisper (Skynet) : tiny, base, small… @@ -238,7 +253,7 @@ IMMICH_ML_URL=http://immich-ml:3003 HEALTH_NEXTCLOUD_URL={{NEXTCLOUD_URL}}/status.php HEALTH_IMMICH_URL={{IMMICH_API_URL}}/server-info/ping # Stack Docker locale : probe interne jitsi-web -# Déploiement externe : https://{{DOMAIN}}/about/health (ou JITSI_PUBLIC_URL sans /meet + /about/health) +# Déploiement externe : {{SUITE_ORIGIN}}/about/health (ou JITSI_PUBLIC_URL sans /meet + /about/health) HEALTH_JITSI_URL=http://jitsi-web:80/about/health HEALTH_HTTP_TIMEOUT=3s # Grafana local (monitoring) @@ -274,7 +289,7 @@ MAIL_MICROSOFT_OAUTH_CLIENT_ID= MAIL_MICROSOFT_OAUTH_CLIENT_SECRET= MAIL_MICROSOFT_OAUTH_TENANT=common MAIL_OAUTH_REDIRECT_URL= -MAIL_APP_URL=http://localhost/mail +MAIL_APP_URL=http{{SECURE}}://{{PUBLIC_HOST}}/mail # Cible nginx → suite frontend unifié mail+drive (dev: Next sur l'hôte :3004 ; prod: suite-frontend:3000) # ----------------------------------------------------------------------------- diff --git a/deploy/authentik/README.md b/deploy/authentik/README.md index 8d8ea31..a7c9ea8 100644 --- a/deploy/authentik/README.md +++ b/deploy/authentik/README.md @@ -81,12 +81,17 @@ Si `AUTHENTIK_API_TOKEN` est défini, **ultid** provisionne au démarrage les ap Sans token : les blueprints ci-dessus restent la source (worker Authentik au boot). +Les blueprints OIDC (`*-oidc.yaml`) sont **générés** depuis `*.yaml.template` au lancement de `./deploy/compose-up.sh`, avec les redirect URIs dérivés de `SUITE_ORIGIN` (`PUBLIC_HOST` + `SECURE`). Éditer les `.template`, pas les `.yaml` générés. + ## Appliquer / vérifier ```bash -# Re-appliquer après modification -docker exec deploy-authentik-server-1 ak apply_blueprint /blueprints/custom/01-ulti-enrollment.yaml -docker exec deploy-authentik-server-1 ak apply_blueprint /blueprints/custom/02-ulti-brand.yaml +# Re-render + re-appliquer après changement de domaine +./deploy/compose-up.sh restart authentik-worker + +# Ou manuellement : +./deploy/authentik/render-blueprints.sh # source .env.resolved d'abord +docker exec deploy-authentik-server-1 ak apply_blueprint /blueprints/custom/ulti-oidc.yaml ./deploy/compose-up.sh restart authentik-worker # Vérifier OIDC Ultimail diff --git a/deploy/authentik/blueprints/nextcloud-oidc.yaml b/deploy/authentik/blueprints/nextcloud-oidc.yaml index 85b86b1..58b569f 100644 --- a/deploy/authentik/blueprints/nextcloud-oidc.yaml +++ b/deploy/authentik/blueprints/nextcloud-oidc.yaml @@ -1,4 +1,5 @@ # Authentik blueprint — Nextcloud OIDC (when NEXTCLOUD_ENABLED=true) +# Generated from nextcloud-oidc.yaml.template # Client secret must match NC_OIDC_CLIENT_SECRET in .env version: 1 metadata: @@ -22,6 +23,8 @@ entries: client_id: ulti-nextcloud client_secret: changeme redirect_uris: + - matching_mode: strict + url: https://dev.ultispace.fr/cloud/apps/user_oidc/code - matching_mode: strict url: http://localhost/cloud/apps/user_oidc/code - matching_mode: strict @@ -36,5 +39,5 @@ entries: slug: nextcloud group: Ulti Suite provider: !KeyOf nc-oauth-provider - meta_launch_url: http://localhost/cloud/ + meta_launch_url: https://dev.ultispace.fr/cloud/ policy_engine_mode: any diff --git a/deploy/authentik/blueprints/nextcloud-oidc.yaml.template b/deploy/authentik/blueprints/nextcloud-oidc.yaml.template new file mode 100644 index 0000000..4289c11 --- /dev/null +++ b/deploy/authentik/blueprints/nextcloud-oidc.yaml.template @@ -0,0 +1,43 @@ +# Authentik blueprint — Nextcloud OIDC (when NEXTCLOUD_ENABLED=true) +# Generated from nextcloud-oidc.yaml.template +# Client secret must match NC_OIDC_CLIENT_SECRET in .env +version: 1 +metadata: + name: Nextcloud OIDC + labels: + blueprints.goauthentik.io/instantiate: "true" +entries: + - model: authentik_providers_oauth2.oauth2provider + id: nc-oauth-provider + identifiers: + name: ulti-nextcloud-provider + attrs: + name: ulti-nextcloud-provider + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + client_type: confidential + client_id: ulti-nextcloud + client_secret: changeme + redirect_uris: + - matching_mode: strict + url: {{SUITE_ORIGIN}}/cloud/apps/user_oidc/code + - matching_mode: strict + url: http://localhost/cloud/apps/user_oidc/code + - matching_mode: strict + url: http://127.0.0.1/cloud/apps/user_oidc/code + signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] + + - model: authentik_core.application + identifiers: + slug: nextcloud + attrs: + name: Nextcloud + slug: nextcloud + group: Ulti Suite + provider: !KeyOf nc-oauth-provider + meta_launch_url: {{SUITE_ORIGIN}}/cloud/ + policy_engine_mode: any diff --git a/deploy/authentik/blueprints/onlyoffice-oidc.yaml b/deploy/authentik/blueprints/onlyoffice-oidc.yaml index 72c85fe..0163ba7 100644 --- a/deploy/authentik/blueprints/onlyoffice-oidc.yaml +++ b/deploy/authentik/blueprints/onlyoffice-oidc.yaml @@ -1,4 +1,5 @@ # Authentik blueprint — OnlyOffice OIDC (when ONLYOFFICE_ENABLED=true) +# Generated from onlyoffice-oidc.yaml.template # Client secret must match ONLYOFFICE_OIDC_CLIENT_SECRET in .env version: 1 metadata: @@ -22,6 +23,10 @@ entries: client_id: ulti-onlyoffice client_secret: changeme redirect_uris: + - matching_mode: strict + url: https://dev.ultispace.fr/office/ + - matching_mode: strict + url: https://dev.ultispace.fr/office/oauth2/callback - matching_mode: strict url: http://localhost/office/ - matching_mode: strict @@ -36,5 +41,5 @@ entries: slug: onlyoffice group: Ulti Suite provider: !KeyOf oo-oauth-provider - meta_launch_url: http://localhost/office + meta_launch_url: https://dev.ultispace.fr/office policy_engine_mode: any diff --git a/deploy/authentik/blueprints/onlyoffice-oidc.yaml.template b/deploy/authentik/blueprints/onlyoffice-oidc.yaml.template new file mode 100644 index 0000000..bbed5b3 --- /dev/null +++ b/deploy/authentik/blueprints/onlyoffice-oidc.yaml.template @@ -0,0 +1,45 @@ +# Authentik blueprint — OnlyOffice OIDC (when ONLYOFFICE_ENABLED=true) +# Generated from onlyoffice-oidc.yaml.template +# Client secret must match ONLYOFFICE_OIDC_CLIENT_SECRET in .env +version: 1 +metadata: + name: OnlyOffice OIDC + labels: + blueprints.goauthentik.io/instantiate: "true" +entries: + - model: authentik_providers_oauth2.oauth2provider + id: oo-oauth-provider + identifiers: + name: ulti-onlyoffice-provider + attrs: + name: ulti-onlyoffice-provider + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + client_type: confidential + client_id: ulti-onlyoffice + client_secret: changeme + redirect_uris: + - matching_mode: strict + url: {{SUITE_ORIGIN}}/office/ + - matching_mode: strict + url: {{SUITE_ORIGIN}}/office/oauth2/callback + - matching_mode: strict + url: http://localhost/office/ + - matching_mode: strict + url: http://localhost/office/oauth2/callback + signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] + + - model: authentik_core.application + identifiers: + slug: onlyoffice + attrs: + name: OnlyOffice + slug: onlyoffice + group: Ulti Suite + provider: !KeyOf oo-oauth-provider + meta_launch_url: {{SUITE_ORIGIN}}/office + policy_engine_mode: any diff --git a/deploy/authentik/blueprints/ulti-oidc.yaml b/deploy/authentik/blueprints/ulti-oidc.yaml index fb725ec..4757a88 100644 --- a/deploy/authentik/blueprints/ulti-oidc.yaml +++ b/deploy/authentik/blueprints/ulti-oidc.yaml @@ -1,4 +1,5 @@ # Authentik blueprint — Ultimail OIDC (auto-applied on worker startup) +# Generated from ulti-oidc.yaml.template — edit template, not this file directly. # Client secret must match ULTID_OIDC_CLIENT_SECRET in .env version: 1 metadata: @@ -25,6 +26,8 @@ entries: client_id: ulti-backend client_secret: changeme redirect_uris: + - matching_mode: strict + url: https://dev.ultispace.fr/api/auth/callback - matching_mode: strict url: http://localhost/api/auth/callback - matching_mode: strict @@ -43,5 +46,5 @@ entries: slug: ulti group: Ulti Suite provider: !KeyOf ulti-oauth-provider - meta_launch_url: http://localhost/mail/inbox + meta_launch_url: https://dev.ultispace.fr/mail/inbox policy_engine_mode: any diff --git a/deploy/authentik/blueprints/ulti-oidc.yaml.template b/deploy/authentik/blueprints/ulti-oidc.yaml.template new file mode 100644 index 0000000..9348f73 --- /dev/null +++ b/deploy/authentik/blueprints/ulti-oidc.yaml.template @@ -0,0 +1,50 @@ +# Authentik blueprint — Ultimail OIDC (auto-applied on worker startup) +# Generated from ulti-oidc.yaml.template — edit template, not this file directly. +# Client secret must match ULTID_OIDC_CLIENT_SECRET in .env +version: 1 +metadata: + name: Ultimail OIDC + labels: + blueprints.goauthentik.io/instantiate: "true" +entries: + - model: authentik_providers_oauth2.oauth2provider + id: ulti-oauth-provider + identifiers: + name: ulti-backend-provider + attrs: + name: ulti-backend-provider + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + access_token_validity: "hours=1" + refresh_token_validity: "days=365" + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, offline_access]] + client_type: confidential + client_id: ulti-backend + client_secret: changeme + redirect_uris: + - matching_mode: strict + url: {{SUITE_ORIGIN}}/api/auth/callback + - matching_mode: strict + url: http://localhost/api/auth/callback + - matching_mode: strict + url: http://127.0.0.1/api/auth/callback + - matching_mode: strict + url: http://localhost:3004/api/auth/callback + - matching_mode: strict + url: http://127.0.0.1:3004/api/auth/callback + signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] + + - model: authentik_core.application + identifiers: + slug: ulti + attrs: + name: Ultimail + slug: ulti + group: Ulti Suite + provider: !KeyOf ulti-oauth-provider + meta_launch_url: {{SUITE_ORIGIN}}/mail/inbox + policy_engine_mode: any diff --git a/deploy/authentik/render-blueprints.sh b/deploy/authentik/render-blueprints.sh new file mode 100755 index 0000000..127db3b --- /dev/null +++ b/deploy/authentik/render-blueprints.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Render Authentik blueprint templates using PUBLIC_HOST / SECURE / SUITE_ORIGIN from .env.resolved. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +BP_DIR="$ROOT/deploy/authentik/blueprints" + +if [[ -z "${SUITE_ORIGIN:-}" || -z "${PUBLIC_HOST:-}" ]]; then + echo "render-blueprints: SUITE_ORIGIN and PUBLIC_HOST must be set (source .env.resolved first)" >&2 + exit 1 +fi + +render_one() { + local tpl="$1" + local out="${tpl%.template}" + sed \ + -e "s|{{SUITE_ORIGIN}}|${SUITE_ORIGIN}|g" \ + -e "s|{{PUBLIC_HOST}}|${PUBLIC_HOST}|g" \ + -e "s|{{SECURE}}|${SECURE:-}|g" \ + "$tpl" > "$out" + echo "render-blueprints: ${out##*/}" +} + +shopt -s nullglob +for tpl in "$BP_DIR"/*.yaml.template; do + render_one "$tpl" +done diff --git a/deploy/compose-up.sh b/deploy/compose-up.sh index 18b282c..6ca2d90 100755 --- a/deploy/compose-up.sh +++ b/deploy/compose-up.sh @@ -34,6 +34,8 @@ set -a source .env.resolved set +a +"$ROOT/deploy/authentik/render-blueprints.sh" + compose_files=( "-f" "deploy/docker-compose.yml" ) diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index ce601eb..a1c2b90 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -105,7 +105,7 @@ services: AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_POSTGRESQL__NAME:-authentik} AUTHENTIK_REDIS__HOST: ${AUTHENTIK_REDIS__HOST:-keydb} AUTHENTIK_WEB__PATH: /auth/ - AUTHENTIK_HOST: http://${DOMAIN:-localhost} + AUTHENTIK_HOST: ${AUTHENTIK_HOST:-http://localhost} env_file: ../.env.resolved volumes: - ./authentik/blueprints:/blueprints/custom:ro @@ -140,7 +140,7 @@ services: AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_POSTGRESQL__NAME:-authentik} AUTHENTIK_REDIS__HOST: ${AUTHENTIK_REDIS__HOST:-keydb} AUTHENTIK_WEB__PATH: /auth/ - AUTHENTIK_HOST: http://${DOMAIN:-localhost} + AUTHENTIK_HOST: ${AUTHENTIK_HOST:-http://localhost} env_file: ../.env.resolved volumes: - ./authentik/blueprints:/blueprints/custom:ro @@ -207,15 +207,18 @@ services: build: context: ../../gmail-interface-clone dockerfile: Dockerfile + args: + PUBLIC_HOST: ${PUBLIC_HOST:-localhost} + SECURE: ${SECURE:-} restart: unless-stopped environment: - ULTI_PROXY_ORIGIN=http://nginx - NEXT_PUBLIC_API_URL=/api/v1 - - NEXT_PUBLIC_APP_URL=http://${DOMAIN:-localhost} + - PUBLIC_HOST=${PUBLIC_HOST:-localhost} + - SECURE=${SECURE:-} - OIDC_CLIENT_SECRET=${ULTID_OIDC_CLIENT_SECRET:-changeme} - - NEXT_PUBLIC_OIDC_ISSUER=http://${DOMAIN:-localhost}/auth/application/o/ulti/ - NEXT_PUBLIC_OIDC_CLIENT_ID=${ULTID_OIDC_CLIENT_ID:-ulti-backend} - - NEXT_PUBLIC_ONLYOFFICE_URL=http://${DOMAIN:-localhost}/office + - NEXT_PUBLIC_AI_PUBLIC_PATH=/ai networks: - ulti-net depends_on: diff --git a/deploy/expose.sh b/deploy/expose.sh index d238d8b..53c4e34 100755 --- a/deploy/expose.sh +++ b/deploy/expose.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Expose local nginx (:80) via Cloudflare Tunnel (e.g. https://dev.ultispace.fr). +# Expose local nginx (:80) via Cloudflare Tunnel (public URL from SUITE_ORIGIN in .env). set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" @@ -10,9 +10,23 @@ if [[ ! -f .env ]]; then exit 1 fi +RESOLVED="$(mktemp)" +trap 'rm -f "$RESOLVED"' EXIT + +if command -v go >/dev/null 2>&1; then + go run ./cmd/envexpand -in .env -out "$RESOLVED" +else + if [[ -x ./bin/envexpand ]]; then + ./bin/envexpand -in .env -out "$RESOLVED" + else + echo "Go not found — install Go or build: go build -o bin/envexpand ./cmd/envexpand" >&2 + exit 1 + fi +fi + set -a # shellcheck disable=SC1091 -source .env +source "$RESOLVED" set +a if [[ -z "${CLOUDFLARE_TUNNEL_TOKEN:-}" ]]; then @@ -20,7 +34,7 @@ if [[ -z "${CLOUDFLARE_TUNNEL_TOKEN:-}" ]]; then exit 1 fi -PUBLIC_URL="${CLOUDFLARE_TUNNEL_PUBLIC_URL:-https://dev.ultispace.fr}" +PUBLIC_URL="${CLOUDFLARE_TUNNEL_PUBLIC_URL:-${SUITE_ORIGIN:-http://localhost}}" echo "Cloudflare tunnel → local stack (nginx :80)" echo "Public URL: ${PUBLIC_URL}" echo "Start stack first: ./deploy/compose-up.sh up -d" diff --git a/deploy/jitsi/docker-compose.jitsi.yml b/deploy/jitsi/docker-compose.jitsi.yml index 951e7f9..ea83100 100644 --- a/deploy/jitsi/docker-compose.jitsi.yml +++ b/deploy/jitsi/docker-compose.jitsi.yml @@ -18,7 +18,7 @@ services: AUTH_TYPE: jwt JWT_ACCEPTED_ISSUERS: ulti JWT_ACCEPTED_AUDIENCES: ulti - PUBLIC_URL: https://${DOMAIN:-localhost}/meet + PUBLIC_URL: ${JITSI_PUBLIC_URL:-http://localhost/meet} XMPP_DOMAIN: meet.jitsi XMPP_MUC_DOMAIN: muc.meet.jitsi XMPP_BOSH_URL_BASE: http://jitsi-prosody:5280 @@ -70,7 +70,7 @@ services: XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi JVB_PORT: "10000" JVB_STUN_SERVERS: stun.l.google.com:19302 - PUBLIC_URL: https://${DOMAIN:-localhost}/meet + PUBLIC_URL: ${JITSI_PUBLIC_URL:-http://localhost/meet} networks: - ulti-net depends_on: diff --git a/deploy/openwebui/docker-compose.openwebui.yml b/deploy/openwebui/docker-compose.openwebui.yml index 10d46df..51e24af 100644 --- a/deploy/openwebui/docker-compose.openwebui.yml +++ b/deploy/openwebui/docker-compose.openwebui.yml @@ -17,7 +17,7 @@ services: WEBUI_NAME: UltiAI OPENAI_API_BASE_URL: http://ultid:8080/api/v1/ai OPENAI_API_KEY: ${AI_GATEWAY_API_KEY:-ulti-gateway} - WEBUI_URL: http://${DOMAIN:-localhost}/ai + WEBUI_URL: ${OPENWEBUI_PUBLIC_URL:-http://localhost/ai} WEBUI_SECRET_KEY: ${WEBUI_SECRET_KEY:-changeme-openwebui-dev-secret} DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/openwebui USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED: "false" diff --git a/internal/authentik/provision.go b/internal/authentik/provision.go index e76e091..bf4780f 100644 --- a/internal/authentik/provision.go +++ b/internal/authentik/provision.go @@ -91,12 +91,20 @@ func EnsureSuiteApplications(ctx context.Context, pool *pgxpool.Pool, cfg *confi launchURL = spec.LaunchURL(cfg) } + // Sync redirect URIs for blueprint- or API-provisioned providers (domain/https changes). + if prov, found, err := client.FindOAuth2ProviderByName(ctx, spec.ProviderName); err != nil { + slog.Warn("authentik provider lookup failed", "app", spec.Key, "error", err) + } else if found && prov.PK != 0 { + if err := client.UpdateOAuth2ProviderRedirects(ctx, prov.PK, redirects); err != nil { + slog.Warn("authentik redirect sync failed", "app", spec.Key, "error", err) + } else { + slog.Info("authentik redirect URIs synced", "app", spec.Key, "count", len(redirects)) + } + } + if provisioned, err := IsProvisioned(ctx, pool, spec.Key); err != nil { return err } else if provisioned { - if err := syncRedirects(ctx, pool, client, spec.Key, redirects); err != nil { - slog.Warn("authentik redirect sync failed", "app", spec.Key, "error", err) - } continue } @@ -122,17 +130,6 @@ func EnsureSuiteApplications(ctx context.Context, pool *pgxpool.Pool, cfg *confi return nil } -func syncRedirects(ctx context.Context, pool *pgxpool.Pool, client *Client, appKey string, redirects []string) error { - rec, err := GetProvisioned(ctx, pool, appKey) - if err != nil { - return err - } - if rec.ProviderID == nil || *rec.ProviderID == 0 { - return nil - } - return client.UpdateOAuth2ProviderRedirects(ctx, *rec.ProviderID, redirects) -} - func ensureApp( ctx context.Context, client *Client, diff --git a/internal/config/config.go b/internal/config/config.go index 90e9410..32cb554 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -487,6 +487,9 @@ func authentikPublicHTTPS() bool { if envBool("AUTHENTIK_PUBLIC_HTTPS", false) { return true } + if strings.TrimSpace(os.Getenv("SECURE")) == "s" { + return true + } host := strings.TrimSpace(os.Getenv("AUTHENTIK_HOST")) return strings.HasPrefix(strings.ToLower(host), "https://") }