5 Commits
Author SHA1 Message Date
Admin 35d6314ce8 Point the IDE dev settings at the shared Infrastructure stack
CI/CD / BackEnd (push) Successful in 1m18s
CI/CD / WebPlayer (push) Successful in 41s
CI/CD / Deploy (push) Skipped
appsettings.Development.json still targeted Musify's old own Postgres
(port 59000) with the superuser, the default RabbitMQ vhost as guest and
the admin S3 keys. They now use Infrastructure's dev ports and Musify's
own dev credentials (role, vhost and S3 identity), the same defaults as
deploy/.env.example.

The API's non-secret Zitadel settings (issuer, metadata, endpoints) move
there too, so user-secrets only need the per-machine client id.
2026-09-24 17:11:50 +02:00
Admin f99f74f4a1 Use Musify's own credentials on the shared services
Infrastructure now gives each project its own Postgres role, RabbitMQ
user and vhost, S3 identity limited to its bucket and a Zitadel service
account that owns only its project, instead of handing out the admin
accounts.

- Postgres, RabbitMQ and S3 settings default to Musify's own (musify).
- MassTransit connects to the `musify` vhost (RABBITMQ_VHOST, taken from
  the address path); RABBITMQ_DEFAULT_USER/PASS become RABBITMQ_USER and
  RABBITMQ_PASSWORD.
- zitadel-init mounts ZITADEL_PAT_DIR (Infrastructure's
  zitadel/.output/musify) and uses that PAT. It no longer creates the
  Zitadel project, which that account can't do: Infrastructure does.
2026-09-24 17:05:23 +02:00
Admin f2765e2e7b Call the API by container name from the web player
In prod every project shares infra-net, where the `api` service name
can resolve to another project's container as well.
2026-09-24 15:06:25 +02:00
Admin 7b8f3f1222 Use a reverse-domain redirect scheme for the native app
- Android/iOS now return to cat.ikerdemo.musify:/callback instead of
  musify://callback: a private-use scheme based on a domain we own
  (RFC 8252 section 7.1), so another app is unlikely to claim it.
- The Musify Native app no longer needs devMode: loopback and custom
  scheme redirects are both valid for a native app.
- zitadel-init also updates an existing app's config, so the change
  reaches instances created before it (the client id is kept).
- iOS takes the scheme from everything before the first ':', since the
  new URI has a single slash.
2026-09-24 15:06:25 +02:00
Admin c33b0e8bfd Stop disabling Zitadel Login V2 from zitadel-init
The shared Infrastructure stack now runs the Login V2 container, and
turning the feature off here would switch every project on the shared
instance back to the old login.
2026-09-24 15:06:25 +02:00
14 changed files with 121 additions and 88 deletions
@@ -6,21 +6,26 @@
} }
}, },
"Authentication": { "Authentication": {
"MetadataAddress": "http://host.docker.internal:8080/.well-known/openid-configuration",
"IssuerAddress": "http://host.docker.internal:8080",
"AuthorizationEndpoint": "http://host.docker.internal:8080/oauth/v2/authorize?prompt=login",
"TokenEndpoint": "http://host.docker.internal:8080/oauth/v2/token",
"ScalarRedirectUri": "http://localhost:5111/scalar/",
"Scopes": [ "openid", "profile", "email" ], "Scopes": [ "openid", "profile", "email" ],
"RequireHttpsMetadata": false "RequireHttpsMetadata": false
}, },
"Database": { "Database": {
"ConnectionString": "Host=localhost;Port=59000;Database=musify_db;Username=postgres;Password=postgres" "ConnectionString": "Host=localhost;Port=5432;Database=musify_db;Username=musify;Password=musify"
}, },
"MassTransit": { "MassTransit": {
"Address": "rabbitmq://localhost:5672", "Address": "rabbitmq://localhost:5672/musify",
"Username": "guest", "Username": "musify",
"Password": "guest" "Password": "musify"
}, },
"InfrastructureStorage": { "InfrastructureStorage": {
"Address": "http://localhost:8333", "Address": "http://localhost:8333",
"AccessKey": "admin_access_key", "AccessKey": "musify_access_key",
"SecretAccessKey": "admin_secret_key", "SecretAccessKey": "musify_secret_key",
"ForcePathStyle": true, "ForcePathStyle": true,
"UseHttp": true "UseHttp": true
}, },
@@ -6,20 +6,20 @@
} }
}, },
"Database": { "Database": {
"ConnectionString": "Host=localhost;Port=59000;Database=musify_db;Username=postgres;Password=postgres" "ConnectionString": "Host=localhost;Port=5432;Database=musify_db;Username=musify;Password=musify"
}, },
"MassTransit": { "MassTransit": {
"Address": "rabbitmq://localhost:5672", "Address": "rabbitmq://localhost:5672/musify",
"Username": "guest", "Username": "musify",
"Password": "guest" "Password": "musify"
}, },
"OpenTelemetry": { "OpenTelemetry": {
"OtlpEndpoint": "http://localhost:4317" "OtlpEndpoint": "http://localhost:4317"
}, },
"InfrastructureStorage": { "InfrastructureStorage": {
"Address": "http://localhost:8333", "Address": "http://localhost:8333",
"AccessKey": "admin_access_key", "AccessKey": "musify_access_key",
"SecretAccessKey": "admin_secret_key", "SecretAccessKey": "musify_secret_key",
"ForcePathStyle": true, "ForcePathStyle": true,
"UseHttp": true "UseHttp": true
}, },
+21 -15
View File
@@ -5,8 +5,10 @@
# #
# Requires ../../../Infrastructure running first (Postgres, Zitadel, # Requires ../../../Infrastructure running first (Postgres, Zitadel,
# SeaweedFS, RabbitMQ, Jaeger). See ../../../Infrastructure/README.md. # SeaweedFS, RabbitMQ, Jaeger). See ../../../Infrastructure/README.md.
# Everything marked "must match Infrastructure's .env" below has to be the # Musify has its own credentials on every shared service (role, vhost, S3
# same value as the one in that file. # identity, Zitadel service account), provisioned by Infrastructure. Every
# value marked "= Infrastructure's MUSIFY_*" below has to be the same as that
# setting in Infrastructure's .env.
DOTNET_ENVIRONMENT=Development DOTNET_ENVIRONMENT=Development
@@ -21,17 +23,18 @@ PUBLIC_STREAM_URL=http://localhost:8081
PUBLIC_AUTH_URL=http://host.docker.internal:8080 PUBLIC_AUTH_URL=http://host.docker.internal:8080
# --- PostgreSQL (Infrastructure's shared instance, Musify's own database) --- # --- PostgreSQL (Infrastructure's shared instance, Musify's own database) ---
# POSTGRES_USER / POSTGRES_PASSWORD must match Infrastructure's .env. # Musify's own role, owner of musify_db only.
POSTGRES_HOST=host.docker.internal POSTGRES_HOST=host.docker.internal
POSTGRES_USER=postgres POSTGRES_USER=musify
POSTGRES_PASSWORD=postgres # = Infrastructure's MUSIFY_DB_PASSWORD
POSTGRES_PASSWORD=musify
POSTGRES_DB=musify_db POSTGRES_DB=musify_db
# --- Zitadel (OIDC, shared instance) ----------------------------------------- # --- Zitadel (OIDC, shared instance) -----------------------------------------
# Path to the admin service-account PAT Infrastructure's Zitadel writes on # Folder with Musify's own Zitadel PAT (a service account that owns the
# init. Adjust if your Infrastructure checkout lives somewhere else relative # Musify project only), written by Infrastructure's `provision`. Adjust if
# to this one. # your Infrastructure checkout lives somewhere else relative to this one.
ZITADEL_ADMIN_PAT_DIR=../../../Infrastructure/zitadel/.output ZITADEL_PAT_DIR=../../../Infrastructure/zitadel/.output/musify
# Allows the OIDC apps this project creates to use plain-http redirect URIs # Allows the OIDC apps this project creates to use plain-http redirect URIs
# (dev only). # (dev only).
ZITADEL_DEV_MODE=true ZITADEL_DEV_MODE=true
@@ -43,16 +46,19 @@ WEB_CLIENT_ID=
WEB_CLIENT_SECRET= WEB_CLIENT_SECRET=
# --- RabbitMQ (shared instance, MassTransit) -------------------------------- # --- RabbitMQ (shared instance, MassTransit) --------------------------------
# Must match Infrastructure's .env. # Musify's own user, only on its own vhost.
RABBITMQ_HOST=host.docker.internal RABBITMQ_HOST=host.docker.internal
RABBITMQ_DEFAULT_USER=guest RABBITMQ_VHOST=musify
RABBITMQ_DEFAULT_PASS=guest RABBITMQ_USER=musify
# = Infrastructure's MUSIFY_RABBITMQ_PASSWORD
RABBITMQ_PASSWORD=musify
# --- SeaweedFS (S3, shared instance, Musify's own bucket) -------------------- # --- SeaweedFS (S3, shared instance, Musify's own bucket) --------------------
# S3_ACCESS_KEY / S3_SECRET_KEY must match Infrastructure's .env. # Musify's own identity, limited to S3_BUCKET.
SEAWEEDFS_HOST=host.docker.internal SEAWEEDFS_HOST=host.docker.internal
S3_ACCESS_KEY=admin_access_key # = Infrastructure's MUSIFY_S3_ACCESS_KEY / MUSIFY_S3_SECRET_KEY
S3_SECRET_KEY=admin_secret_key S3_ACCESS_KEY=musify_access_key
S3_SECRET_KEY=musify_secret_key
S3_BUCKET=webapi-storage S3_BUCKET=webapi-storage
S3_PUBLIC_URL=http://host.docker.internal:8333 S3_PUBLIC_URL=http://host.docker.internal:8333
S3_INTERNAL_URL=http://host.docker.internal:8333 S3_INTERNAL_URL=http://host.docker.internal:8333
+10 -5
View File
@@ -28,14 +28,16 @@ PUBLIC_AUTH_URL=https://auth.example.com
S3_PUBLIC_URL=https://s3.example.com S3_PUBLIC_URL=https://s3.example.com
# --- PostgreSQL (Infrastructure's shared instance, Musify's own database) --- # --- PostgreSQL (Infrastructure's shared instance, Musify's own database) ---
# POSTGRES_USER / POSTGRES_PASSWORD must match Infrastructure's .env.prod. # Musify's own role, owner of musify_db only.
POSTGRES_HOST=postgres POSTGRES_HOST=postgres
POSTGRES_USER=postgres POSTGRES_USER=musify
# = Infrastructure's MUSIFY_DB_PASSWORD
POSTGRES_PASSWORD=CHANGE_ME POSTGRES_PASSWORD=CHANGE_ME
POSTGRES_DB=musify_db POSTGRES_DB=musify_db
# --- Zitadel (OIDC, shared instance) ----------------------------------------- # --- Zitadel (OIDC, shared instance) -----------------------------------------
ZITADEL_ADMIN_PAT_DIR=../../../Infrastructure/zitadel/.output # Folder with Musify's own Zitadel PAT, written by Infrastructure's `provision`.
ZITADEL_PAT_DIR=../../../Infrastructure/zitadel/.output/musify
ZITADEL_DEV_MODE=false ZITADEL_DEV_MODE=false
# OIDC clients: the zitadel-init one-shot service (see compose.yml) fills # OIDC clients: the zitadel-init one-shot service (see compose.yml) fills
@@ -46,11 +48,14 @@ WEB_CLIENT_SECRET=
# --- RabbitMQ (shared instance, MassTransit) -------------------------------- # --- RabbitMQ (shared instance, MassTransit) --------------------------------
RABBITMQ_HOST=rabbitmq RABBITMQ_HOST=rabbitmq
RABBITMQ_DEFAULT_USER=musify RABBITMQ_VHOST=musify
RABBITMQ_DEFAULT_PASS=CHANGE_ME RABBITMQ_USER=musify
# = Infrastructure's MUSIFY_RABBITMQ_PASSWORD
RABBITMQ_PASSWORD=CHANGE_ME
# --- SeaweedFS (S3, shared instance, Musify's own bucket) -------------------- # --- SeaweedFS (S3, shared instance, Musify's own bucket) --------------------
SEAWEEDFS_HOST=seaweedfs SEAWEEDFS_HOST=seaweedfs
# = Infrastructure's MUSIFY_S3_ACCESS_KEY / MUSIFY_S3_SECRET_KEY
S3_ACCESS_KEY=CHANGE_ME S3_ACCESS_KEY=CHANGE_ME
S3_SECRET_KEY=CHANGE_ME S3_SECRET_KEY=CHANGE_ME
S3_BUCKET=webapi-storage S3_BUCKET=webapi-storage
+11 -7
View File
@@ -68,13 +68,17 @@ dotnet run --project backend/Hosts/Musify.Worker # background
npm --prefix web-player run dev # :5173 npm --prefix web-player run dev # :5173
``` ```
When running the apps from the IDE, `Musify.Api` reads `Authentication:*` When running the apps from the IDE, neither the .NET hosts nor `web-player`
from user-secrets and `web-player` reads its own `web-player/.env`, neither look at `deploy/.env`. The hosts' `appsettings.Development.json` already
looks at `deploy/.env`. Copy the four values `zitadel-init` wrote point at Infrastructure's dev stack with Musify's own dev credentials (the
(`AUTH_CLIENT_ID`, `WEB_CLIENT_ID`, `WEB_CLIENT_SECRET`, `NATIVE_CLIENT_ID`) defaults of its `MUSIFY_*` settings) and at the shared Zitadel. Only the
into `dotnet user-secrets set Authentication:ClientId <value> --project OIDC client ids are per-machine: copy the values `zitadel-init` wrote once,
backend/Hosts/Musify.Api` and into `web-player/.env` once. They stay valid `AUTH_CLIENT_ID` into both `Authentication:ClientId` and
until Infrastructure's Zitadel volume is wiped. `Authentication:AudienceAddress` (`dotnet user-secrets set <key> <value>
--project backend/Hosts/Musify.Api`), `WEB_CLIENT_ID` and
`WEB_CLIENT_SECRET` into `web-player/.env`, and `NATIVE_CLIENT_ID` into
`mobile/gradle.properties`. They stay valid until Infrastructure's Zitadel
volume is wiped.
### Ports ### Ports
+46 -37
View File
@@ -26,9 +26,10 @@
# already do. # already do.
x-backend-env: &backend-env x-backend-env: &backend-env
Database__ConnectionString: "Host=${POSTGRES_HOST};Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}" Database__ConnectionString: "Host=${POSTGRES_HOST};Port=5432;Database=${POSTGRES_DB};Username=${POSTGRES_USER};Password=${POSTGRES_PASSWORD}"
MassTransit__Address: "rabbitmq://${RABBITMQ_HOST}:5672" # Musify's own vhost and user on the shared broker (the path is the vhost).
MassTransit__Username: ${RABBITMQ_DEFAULT_USER} MassTransit__Address: "rabbitmq://${RABBITMQ_HOST}:5672/${RABBITMQ_VHOST}"
MassTransit__Password: ${RABBITMQ_DEFAULT_PASS} MassTransit__Username: ${RABBITMQ_USER}
MassTransit__Password: ${RABBITMQ_PASSWORD}
# The public S3 URL, not the internal one: the same address is used to sign # The public S3 URL, not the internal one: the same address is used to sign
# the presigned upload/download URLs the browser has to be able to reach. # the presigned upload/download URLs the browser has to be able to reach.
InfrastructureStorage__Address: ${S3_PUBLIC_URL} InfrastructureStorage__Address: ${S3_PUBLIC_URL}
@@ -135,7 +136,7 @@ services:
PUBLIC_WEB_URL: ${PUBLIC_WEB_URL} PUBLIC_WEB_URL: ${PUBLIC_WEB_URL}
ZITADEL_DEV_MODE: ${ZITADEL_DEV_MODE:-false} ZITADEL_DEV_MODE: ${ZITADEL_DEV_MODE:-false}
volumes: volumes:
- ${ZITADEL_ADMIN_PAT_DIR}:/output:ro - ${ZITADEL_PAT_DIR}:/output:ro
entrypoint: ["/bin/sh", "-c"] entrypoint: ["/bin/sh", "-c"]
command: command:
- | - |
@@ -163,13 +164,15 @@ services:
fi fi
} }
echo "==> waiting for the Zitadel service-account PAT" # Musify's own service account, owner of the Musify project only
# (created by Infrastructure's `provision`, see its README).
echo "==> waiting for Musify's Zitadel PAT"
deadline=$$(( $$(date +%s) + 180 )) deadline=$$(( $$(date +%s) + 180 ))
while [ ! -f /output/admin-sa.pat ]; do while [ ! -s /output/zitadel.pat ]; do
[ "$$(date +%s)" -lt "$$deadline" ] || { echo "PAT not found under /output. Is Infrastructure's Zitadel running? Check 'docker logs infra-zitadel'." >&2; exit 1; } [ "$$(date +%s)" -lt "$$deadline" ] || { echo "no zitadel.pat in ZITADEL_PAT_DIR. Is Infrastructure up, and does ZITADEL_PAT_DIR point at its zitadel/.output/musify? Check 'docker logs infra-provision'." >&2; exit 1; }
sleep 3 sleep 3
done done
PAT=$$(tr -d '\r\n' </output/admin-sa.pat) PAT=$$(tr -d '\r\n' </output/zitadel.pat)
MGMT="$$PUBLIC_AUTH_URL/management/v1" MGMT="$$PUBLIC_AUTH_URL/management/v1"
echo "==> waiting for Zitadel to answer at $$PUBLIC_AUTH_URL" echo "==> waiting for Zitadel to answer at $$PUBLIC_AUTH_URL"
@@ -188,21 +191,13 @@ services:
} }
name_query() { printf '{"queries":[{"nameQuery":{"name":"%s","method":"TEXT_QUERY_METHOD_EQUALS"}}]}' "$$1"; } name_query() { printf '{"queries":[{"nameQuery":{"name":"%s","method":"TEXT_QUERY_METHOD_EQUALS"}}]}' "$$1"; }
# `latest` images require the standalone Login UI v2 container, which
# this stack does not run; opting out keeps the built-in /ui/login.
curl -fsS -X PUT "$$PUBLIC_AUTH_URL/v2/features/instance" \
-H "Authorization: Bearer $$PAT" -H "Content-Type: application/json" \
-d '{"loginV2":{"required":false}}' >/dev/null 2>&1 || echo "warning: could not disable Login UI v2"
echo "==> project" echo "==> project"
PROJECT_NAME=Musify PROJECT_NAME=Musify
# Created by Infrastructure's `provision`: this account can't create
# projects, only manage this one.
PROJECT_ID=$$(api_call POST /projects/_search "$$(name_query "$$PROJECT_NAME")" | jq -r --arg n "$$PROJECT_NAME" '.result // [] | map(select(.name==$$n)) | first | .id // empty') PROJECT_ID=$$(api_call POST /projects/_search "$$(name_query "$$PROJECT_NAME")" | jq -r --arg n "$$PROJECT_NAME" '.result // [] | map(select(.name==$$n)) | first | .id // empty')
if [ -z "$$PROJECT_ID" ]; then [ -n "$$PROJECT_ID" ] || { echo "Zitadel project $$PROJECT_NAME not found. Is musify in Infrastructure's x-projects?" >&2; exit 1; }
PROJECT_ID=$$(api_call POST /projects "$$(printf '{"name":"%s"}' "$$PROJECT_NAME")" | jq -r '.id') echo " found (id $$PROJECT_ID)"
echo " created (id $$PROJECT_ID)"
else
echo " reused (id $$PROJECT_ID)"
fi
find_app() { api_call POST "/projects/$$PROJECT_ID/apps/_search" "$$(name_query "$$1")" | jq -r --arg n "$$1" '.result // [] | map(select(.name==$$n)) | first | .id // empty'; } find_app() { api_call POST "/projects/$$PROJECT_ID/apps/_search" "$$(name_query "$$1")" | jq -r --arg n "$$1" '.result // [] | map(select(.name==$$n)) | first | .id // empty'; }
@@ -265,27 +260,38 @@ services:
fi fi
echo "==> native app (KMP client: desktop loopback + mobile custom scheme)" echo "==> native app (KMP client: desktop loopback + mobile custom scheme)"
# Loopback (desktop) and a reverse-domain custom scheme (Android/iOS,
# RFC 8252 §7.1) are both valid for a NATIVE app without devMode, so
# dev and prod share the same config. Unlike the other apps, an
# existing one is also updated, so a change here reaches instances
# created before it.
NATIVE_APP_NAME="Musify Native" NATIVE_APP_NAME="Musify Native"
NATIVE_CONFIG='{
"redirectUris": ["http://127.0.0.1:17453/callback", "cat.ikerdemo.musify:/callback"],
"postLogoutRedirectUris": ["http://127.0.0.1:17453/logout", "cat.ikerdemo.musify:/logout"],
"responseTypes": ["OIDC_RESPONSE_TYPE_CODE"],
"grantTypes": ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE", "OIDC_GRANT_TYPE_REFRESH_TOKEN"],
"appType": "OIDC_APP_TYPE_NATIVE",
"authMethodType": "OIDC_AUTH_METHOD_TYPE_NONE",
"devMode": false,
"accessTokenType": "OIDC_TOKEN_TYPE_JWT",
"accessTokenRoleAssertion": true,
"idTokenRoleAssertion": true,
"idTokenUserinfoAssertion": true
}'
NATIVE_APP_ID=$$(find_app "$$NATIVE_APP_NAME") NATIVE_APP_ID=$$(find_app "$$NATIVE_APP_NAME")
if [ -n "$$NATIVE_APP_ID" ]; then if [ -n "$$NATIVE_APP_ID" ]; then
NATIVE_CLIENT_ID=$$(api_call GET "/projects/$$PROJECT_ID/apps/$$NATIVE_APP_ID" | jq -r '.app.oidcConfig.clientId') NATIVE_CLIENT_ID=$$(api_call GET "/projects/$$PROJECT_ID/apps/$$NATIVE_APP_ID" | jq -r '.app.oidcConfig.clientId')
echo " reused (clientId $$NATIVE_CLIENT_ID)" # Zitadel answers 400 "No changes" when the config already matches.
RESULT=$$(curl -sS -X PUT "$$MGMT/projects/$$PROJECT_ID/apps/$$NATIVE_APP_ID/oidc_config" \
-H "Authorization: Bearer $$PAT" -H "Content-Type: application/json" -d "$$NATIVE_CONFIG")
case "$$RESULT" in
*'"details"'*'"changeDate"'*) echo " reused (clientId $$NATIVE_CLIENT_ID, config updated)" ;;
*COMMAND-1m88i*) echo " reused (clientId $$NATIVE_CLIENT_ID)" ;;
*) echo "could not update the native app config: $$RESULT" >&2; exit 1 ;;
esac
else else
NATIVE_CLIENT_ID=$$(api_call POST "/projects/$$PROJECT_ID/apps/oidc" '{ NATIVE_CLIENT_ID=$$(api_call POST "/projects/$$PROJECT_ID/apps/oidc" "$$(echo "$$NATIVE_CONFIG" | jq --arg name "$$NATIVE_APP_NAME" '. + {name: $$name, version: "OIDC_VERSION_1_0"}')" | jq -r '.clientId')
"name": "Musify Native",
"redirectUris": ["http://127.0.0.1:17453/callback", "musify://callback"],
"postLogoutRedirectUris": ["http://127.0.0.1:17453/logout", "musify://logout"],
"responseTypes": ["OIDC_RESPONSE_TYPE_CODE"],
"grantTypes": ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE", "OIDC_GRANT_TYPE_REFRESH_TOKEN"],
"appType": "OIDC_APP_TYPE_NATIVE",
"authMethodType": "OIDC_AUTH_METHOD_TYPE_NONE",
"version": "OIDC_VERSION_1_0",
"devMode": true,
"accessTokenType": "OIDC_TOKEN_TYPE_JWT",
"accessTokenRoleAssertion": true,
"idTokenRoleAssertion": true,
"idTokenUserinfoAssertion": true
}' | jq -r '.clientId')
echo " created (clientId $$NATIVE_CLIENT_ID)" echo " created (clientId $$NATIVE_CLIENT_ID)"
fi fi
@@ -393,7 +399,10 @@ services:
AUTH_POST_LOGOUT_URI: "${PUBLIC_WEB_URL}/" AUTH_POST_LOGOUT_URI: "${PUBLIC_WEB_URL}/"
AUTH_ALLOW_INSECURE_HTTP: ${AUTH_ALLOW_INSECURE_HTTP} AUTH_ALLOW_INSECURE_HTTP: ${AUTH_ALLOW_INSECURE_HTTP}
SESSION_SECRET: ${SESSION_SECRET} SESSION_SECRET: ${SESSION_SECRET}
API_BASE_URL: http://api:5111 # The container name, not the `api` service name: in prod every project
# shares infra-net, where a generic name can resolve to another
# project's container.
API_BASE_URL: http://musify-api:5111
# Presigned PUTs are signed for the public S3 host; the container sends # Presigned PUTs are signed for the public S3 host; the container sends
# them straight to SeaweedFS instead of going back out through the edge. # them straight to SeaweedFS instead of going back out through the edge.
S3_UPLOAD_ENDPOINT: ${S3_INTERNAL_URL} S3_UPLOAD_ENDPOINT: ${S3_INTERNAL_URL}
+3 -3
View File
@@ -54,7 +54,7 @@ al patrón que ya usa Scalar para probar la API — ver `deploy/compose.yml` (`z
Redirect URIs registrados: Redirect URIs registrados:
- Desktop: `http://127.0.0.1:17453/callback` (RFC 8252, loopback con puerto fijo). - Desktop: `http://127.0.0.1:17453/callback` (RFC 8252, loopback con puerto fijo).
- Android / iOS: `musify://callback` (custom scheme). - Android / iOS: `cat.ikerdemo.musify:/callback` (custom scheme con dominio invertido, RFC 8252 §7.1).
`mobile/gradle.properties` controla a qué backend apunta Android/Desktop: `mobile/gradle.properties` controla a qué backend apunta Android/Desktop:
@@ -125,13 +125,13 @@ Kotlin/Native (`iosMain/`) sigue las APIs estándar de interop (`AVFoundation`,
1. Genera un proyecto Xcode con el asistente "Kotlin Multiplatform" de Android Studio, o crea uno vacío 1. Genera un proyecto Xcode con el asistente "Kotlin Multiplatform" de Android Studio, o crea uno vacío
e importa `composeApp` como framework (`./gradlew :composeApp:embedAndSignAppleFrameworkForXcode`). e importa `composeApp` como framework (`./gradlew :composeApp:embedAndSignAppleFrameworkForXcode`).
2. Registra el scheme `musify` en `Info.plist`: 2. Registra el scheme `cat.ikerdemo.musify` en `Info.plist`:
```xml ```xml
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
<array> <array>
<dict> <dict>
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array><string>musify</string></array> <array><string>cat.ikerdemo.musify</string></array>
</dict> </dict>
</array> </array>
``` ```
@@ -18,7 +18,9 @@
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Catches the `musify://callback` redirect Zitadel sends after login/register. <!-- Catches the `cat.ikerdemo.musify:/callback` redirect Zitadel sends after
login/register. The URI has no host, so the filter matches on the scheme
alone.
See BrowserAuthLauncher.android.kt and AppConfig.nativeRedirectUri. --> See BrowserAuthLauncher.android.kt and AppConfig.nativeRedirectUri. -->
<activity <activity
android:name=".auth.AuthCallbackActivity" android:name=".auth.AuthCallbackActivity"
@@ -29,7 +31,7 @@
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="musify" android:host="callback" /> <data android:scheme="cat.ikerdemo.musify" />
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>
@@ -9,5 +9,5 @@ actual object AppConfig {
// Matches the AuthCallbackActivity intent-filter in AndroidManifest.xml and // Matches the AuthCallbackActivity intent-filter in AndroidManifest.xml and
// the redirect URI registered for the "Musify Native" app in Zitadel. // the redirect URI registered for the "Musify Native" app in Zitadel.
actual val nativeRedirectUri: String = "musify://callback" actual val nativeRedirectUri: String = "cat.ikerdemo.musify:/callback"
} }
@@ -5,7 +5,7 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
/** /**
* Declared in AndroidManifest.xml with an intent-filter for `musify://callback`. * Declared in AndroidManifest.xml with an intent-filter for `cat.ikerdemo.musify:/callback`.
* Chrome Custom Tabs redirects here once Zitadel finishes the login/register * Chrome Custom Tabs redirects here once Zitadel finishes the login/register
* flow; this activity just forwards the URL to [AndroidAuthBridge] and closes. * flow; this activity just forwards the URL to [AndroidAuthBridge] and closes.
*/ */
@@ -9,7 +9,8 @@ package com.musify.app
* `nativeRedirectUri` is the OAuth redirect registered for the "Musify * `nativeRedirectUri` is the OAuth redirect registered for the "Musify
* Native" Zitadel app (see deploy/compose.yml, zitadel-init): a fixed * Native" Zitadel app (see deploy/compose.yml, zitadel-init): a fixed
* loopback port on Desktop (RFC 8252 native-app loopback flow), and the * loopback port on Desktop (RFC 8252 native-app loopback flow), and the
* `musify://callback` custom scheme on Android/iOS. * `cat.ikerdemo.musify:/callback` reverse-domain custom scheme on Android/iOS
* (RFC 8252 §7.1: based on a domain we own, so no other app plausibly claims it).
*/ */
expect object AppConfig { expect object AppConfig {
val apiBaseUrl: String val apiBaseUrl: String
@@ -12,7 +12,7 @@ package com.musify.app.auth
* - Desktop: a one-shot loopback HTTP server on the fixed port registered in * - Desktop: a one-shot loopback HTTP server on the fixed port registered in
* Zitadel (RFC 8252 §7.3), opened via the OS default browser. * Zitadel (RFC 8252 §7.3), opened via the OS default browser.
* - Android: Chrome Custom Tabs; the redirect is caught by * - Android: Chrome Custom Tabs; the redirect is caught by
* [AuthCallbackActivity] via the `musify://callback` intent filter. * [AuthCallbackActivity] via the `cat.ikerdemo.musify:/callback` intent filter.
* - iOS: `ASWebAuthenticationSession`, which owns the whole round trip. * - iOS: `ASWebAuthenticationSession`, which owns the whole round trip.
*/ */
expect class BrowserAuthLauncher() { expect class BrowserAuthLauncher() {
@@ -12,5 +12,5 @@ actual object AppConfig {
// Matches CFBundleURLTypes in Info.plist and the redirect URI registered // Matches CFBundleURLTypes in Info.plist and the redirect URI registered
// for the "Musify Native" app in Zitadel. // for the "Musify Native" app in Zitadel.
actual val nativeRedirectUri: String = "musify://callback" actual val nativeRedirectUri: String = "cat.ikerdemo.musify:/callback"
} }
@@ -23,14 +23,15 @@ private class PresentationContextProvider : NSObject(), ASWebAuthenticationPrese
/** /**
* `ASWebAuthenticationSession` owns the whole round trip: it opens a system * `ASWebAuthenticationSession` owns the whole round trip: it opens a system
* browser sheet at [authorizeUrl] (Zitadel's hosted login, with its * browser sheet at [authorizeUrl] (Zitadel's hosted login, with its
* "Regístrate" link) and intercepts the `musify://` redirect itself, no app * "Regístrate" link) and intercepts the `cat.ikerdemo.musify:` redirect itself, no app
* delegate / `onOpenURL` wiring required. * delegate / `onOpenURL` wiring required.
*/ */
@OptIn(ExperimentalForeignApi::class) @OptIn(ExperimentalForeignApi::class)
actual class BrowserAuthLauncher actual constructor() { actual class BrowserAuthLauncher actual constructor() {
actual suspend fun launch(authorizeUrl: String, redirectUri: String): String = actual suspend fun launch(authorizeUrl: String, redirectUri: String): String =
suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
val scheme = redirectUri.substringBefore("://") // `scheme:/path` (RFC 8252 private-use scheme): everything before the first ':'.
val scheme = redirectUri.substringBefore(":")
val contextProvider = PresentationContextProvider() val contextProvider = PresentationContextProvider()
val session = ASWebAuthenticationSession( val session = ASWebAuthenticationSession(