Add @fontsource-variable (manrope/sora/inter), @lucide/svelte and @types/node, mark @lucide/svelte as noExternal for SSR, and add the dev-server launch config.
Move authenticated pages under an (app) group with a Sidebar shell, add /auth/login and page-based /login, drop the old explore page and login +server endpoint, and add the server upload helper.
Expand the player store, split player controls (transport, seek, volume, track info), add Cover/PlaylistArt/NowPlaying UI primitives and a cover image proxy route.
- Add data-sveltekit-reload to the /login links so the browser performs a full
navigation (the client router intercepted them and aborted on the external
Zitadel redirect)
- Default post-login returnTo to / (home) since /explore is not built yet
- 'Empezar gratis' now starts the register flow (/login?mode=register)
The 'latest' Zitadel image requires the Login UI v2, which runs as a separate
'login' container this stack doesn't deploy, so OIDC redirected to /ui/v2/login
and returned {code:5, Not Found}. provision.ps1 now disables the loginV2
requirement via the feature API so OIDC uses the built-in /ui/login. Also
applied to the running instance.
Add Authorization Code + PKCE login/logout against Zitadel, handled entirely
server-side with an httpOnly signed session cookie:
- $lib/server/auth: openid-client discovery (config cached), jose-signed
session encode/decode, cookie/name constants
- routes: /login (builds the authorize URL with PKCE/state/nonce, ?mode=register
-> prompt=create), /auth/callback (code exchange + session cookie),
/logout (clears session + Zitadel end_session)
- hooks.server populates locals.user/accessToken from the session
- +layout.server exposes the user; Navbar shows the signed-in user + Salir,
or Iniciar sesión / Registrarse
- switch to adapter-node; env-driven config (.env gitignored, .env.example
committed) with issuer http://host.docker.internal:8080
Verified: svelte-check + build clean; dev smoke test shows the landing in the
logged-out state and /login issuing a 302 to Zitadel's authorize endpoint with
PKCE/state/nonce (discovery reachable). Real client id/secret still needed to
complete the token exchange.
New SvelteKit app in frontend/ (minimal template, TypeScript, TailwindCSS v4).
Adds the app shell (dark music theme navbar + footer) and a landing page with
hero and feature cards. CTAs point to /login and /explore (built in later parts).
Verified: production build succeeds and the page renders with Tailwind styles
applied (checked via preview).
- Remove the no-op Worker BackgroundService and its registration; the process
is kept alive and does its work through the MassTransit bus and the upload
intent jobs (their own hosted services)
- Fix the gateway middleware guard to reject on string.IsNullOrEmpty(prefix)
instead of 'is null', which no longer matched TicketValidator returning
string.Empty for invalid tickets (invalid tickets skipped the 401 and an
empty object key could slip through)
Build: 0 warnings, 0 errors.
Tighten the stream-ticket authorization in the gateway middleware:
- Enforce a path-segment boundary on the prefix check instead of a bare
StartsWith, so a ticket for 'users/12' no longer grants 'users/1234/...'
(exact-match still allowed for single-object grants)
- Reject object keys containing '..'/'.' segments before proxying, closing a
path-traversal bypass that SeaweedFS would otherwise resolve outside the
authorized prefix
Also simplify TicketValidator.ValidateTicketAsync from ErrorOr<string> to
string? (null = invalid) since it has a single failure mode; drop the now
unused ErrorOr package reference from the gateway.
Verified: build clean; the authorization predicate passes a table of cases
covering both the escalation and traversal vulnerabilities.
Build: 0 warnings, 0 errors.
- Document every endpoint's responses (Produces<T>/status codes/validation)
so the generated OpenAPI/Scalar docs carry response schemas and status codes
instead of empty responses
- Cache recently-synced user ids (IMemoryCache, 15 min) in the JWT
TokenValidated handler so user provisioning no longer hits the DB on every
authenticated request
- Add ProblemDetails + UseExceptionHandler for consistent RFC7807 errors
- Add ToCreatedResult ErrorOr helper and use it in the create endpoints
- Normalize the playlists route to lowercase (/playlists)
Verified: booted the Api against RabbitMQ + Postgres in Docker and inspected
/openapi/v1.json — operations now expose response schemas/status codes and the
route is /playlists.
Build: 0 warnings, 0 errors.
Single docker-compose stack for local development (Postgres, Zitadel,
RabbitMQ, SeaweedFS + bucket init, Jaeger) plus an optional overlay to run
the .NET apps in Docker. up.ps1 orchestrates the full bootstrap: stack up,
stream-ticket keys, EF Core migrations and Zitadel provisioning.
- Zitadel is provisioned automatically (service-account PAT -> project +
OIDC app with JWT access token); the generated ClientId is wired into
Musify.Api user-secrets and .env. No manual console steps.
- ZITADEL_EXTERNAL_DOMAIN=host.docker.internal so token validation works
both with local apps and full-docker.
- App Dockerfiles copy AppSettings*.json to lowercase so config loads on
case-sensitive Linux; Worker image ships ffmpeg.
- Scripts split by responsibility (common/stream-keys/migrate/provision);
apps overlay de-duped via YAML anchors.
- Remove the superseded standalone seaweedfs compose; point docs/development
at the unified stack.
Register AuditableEntityInterceptor as a singleton and attach it through the
AddDbContext options callback instead of newing it up inside
Database.OnConfiguring. The interceptor is stateless, so a single shared
instance is reused across contexts and its wiring lives in the DI layer.
The design-time factory keeps working (interceptor is runtime-only, irrelevant
for migrations).
Verified: build + ef migrations list (design-time context) OK; the Worker host
builds and validates the full DI graph (reaches endpoint configuration) with no
captive-dependency or interceptor error.
Build: 0 warnings, 0 errors.
- Register IAmazonS3 as a singleton instead of scoped: the AWS SDK client is
thread-safe and manages its own connection pool, so one instance per DI scope
(per request / per job iteration) was wasteful and risked socket exhaustion
- AudioTranscoderService: start reading stdout/stderr before writing stdin to
remove the latent pipe-buffer deadlock shape
- Dispose the resized-picture MemoryStream in ResizePictureActivity
- Background jobs use PeriodicTimer instead of while + Task.Delay
Verified: Worker boots against RabbitMQ + Postgres in Docker with no captive-
dependency error (dev scope validation) and both jobs start.
Build: 0 warnings, 0 errors.