Commit Graph
331 Commits
Author SHA1 Message Date
Admin 5905f56e84 feat(frontend): show recently played from listening history on home 2026-07-14 13:26:08 +02:00
Admin 5fe0f6ff32 fix(backend): order listening history by recency and cap to 15 tracks 2026-07-14 13:26:04 +02:00
Admin 2abca7f08d fix(frontend): apply display font to page titles missing it 2026-07-14 13:23:56 +02:00
Admin b372028e0a refactor(frontend): drop downloads/import nav, promote library above upload 2026-07-14 13:23:51 +02:00
Admin e4830ad73f feat(frontend): drive seek bar with rAF for smooth playback progress 2026-07-14 13:23:44 +02:00
Admin 9b866cb5f1 docs: add project README and frontend pages notes 2026-07-14 04:55:44 +02:00
Admin 0f776078f3 refactor(frontend): tidy Navbar, Footer and document shell
Align Navbar and Footer with the new design tokens and enable stable scrollbar gutter on the document.
2026-07-14 04:55:44 +02:00
Admin dd41d23144 chore(frontend): add font/icon deps and dev config
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.
2026-07-14 04:55:39 +02:00
Admin c544a6c78c feat(frontend): rework home page with recent, latest and playlists
Add SSR data loading for the home page and rebuild its layout (greeting hero, recently played, new releases, user playlists).
2026-07-14 04:55:12 +02:00
Admin 0b4e61b3ab feat(frontend): add (app) route group and rework auth routing
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.
2026-07-14 04:55:04 +02:00
Admin 1f531ea70e feat(frontend): rework audio player and shared UI components
Expand the player store, split player controls (transport, seek, volume, track info), add Cover/PlaylistArt/NowPlaying UI primitives and a cover image proxy route.
2026-07-14 04:54:51 +02:00
Admin 458c20335e feat(frontend): add theme tokens and dynamic accent palette
Introduce theme.css design tokens, color/palette helpers, and global layout styles (scrollbars, animations, home gradient).
2026-07-14 04:54:45 +02:00
Admin 54484d9fd4 feat(tracks): add track cover endpoint
Serve cover images via GET /tracks/{id}/cover with a size parameter, streamed from storage with long-lived immutable cache headers.
2026-07-14 04:54:40 +02:00
Admin 5c1edc0dc7 feat(tracks): add user listening history
Record a listening-history entry when a stream ticket is issued and expose GET /users/{id}/listening-history to read it back.
2026-07-14 04:54:35 +02:00
Admin bbc115ca84 feat(frontend): play tracks from the explore page 2026-07-06 02:40:44 +02:00
Admin c04d79beb0 feat(frontend): music player component with separated controls and stream proxy 2026-07-06 02:40:44 +02:00
Admin e34347e450 refactor(application): drop redundant default values on query records 2026-07-06 02:23:35 +02:00
Admin 7aa5350484 fix(playlists): reject empty/whitespace name and description on update 2026-07-06 02:20:47 +02:00
Admin 1769df12d5 refactor(search): treat empty search strings as no filter (string.IsNullOrEmpty) 2026-07-06 02:12:58 +02:00
Admin 7f439b168b feat(frontend): add /explore page with track listing and name search 2026-07-06 02:05:47 +02:00
Admin 5cca9a80dd chore(frontend): regenerate api types with track name search 2026-07-06 02:04:04 +02:00
Admin fabd26e42f feat(tracks): support search by name in GetTracks 2026-07-06 02:04:01 +02:00
Admin 047d5da319 chore(frontend): generate backend API types from live OpenAPI spec 2026-07-06 01:54:28 +02:00
Admin 72a7d13f6d feat(frontend): add typed backend API client (openapi-fetch) with codegen script 2026-07-06 01:52:25 +02:00
Admin dadeac3571 refactor(frontend): require refresh/id token in Session, validate at callback boundary 2026-07-05 23:57:19 +02:00
Admin 757c74b899 refactor(frontend): restrict insecure HTTP issuer to dev / explicit opt-in 2026-07-05 23:56:35 +02:00
Admin b9d14412e6 refactor(frontend): make SessionUser fields non-null and normalize claims at boundary 2026-07-05 23:45:18 +02:00
Admin 539fad1d22 refactor(frontend): centralize configurable auth/session values in config.ts 2026-07-05 23:43:50 +02:00
Admin 46c7d5dbc7 feat(frontend): encrypt session cookie (JWE) and auto-refresh access token 2026-07-05 23:41:49 +02:00
Admin e11db5263b chore(frontend): remove scaffold comments and add conventions doc 2026-07-05 23:41:42 +02:00
Admin fe8c2c4244 fix(frontend): full-page nav for auth links; land on home after login
- 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)
2026-07-05 23:16:25 +02:00
Admin 5b2f402a9d fix(deploy): use Zitadel built-in login v1 (disable required Login UI v2)
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.
2026-07-05 23:08:27 +02:00
Admin b28ea023d2 feat(frontend): make Zitadel client secret optional (support PKCE apps)
Configure openid-client with None auth (public client + PKCE) when no secret is
provided, so both PKCE and confidential (Code) Zitadel web apps work.
2026-07-05 23:01:44 +02:00
Admin e7e854959f feat(frontend): server-side Zitadel OIDC auth (part 2)
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.
2026-07-05 22:58:47 +02:00
Admin e13bbd4782 feat(frontend): scaffold SvelteKit + Tailwind with landing (part 1)
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).
2026-07-05 22:47:29 +02:00
Admin 3653a350a7 cleanup(worker): drop empty Worker BackgroundService; fix gateway ticket guard
- 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.
2026-07-05 22:13:20 +02:00
Admin d8127230a7 Formatting 2026-07-05 22:07:43 +02:00
Admin a9789deecb Empty string 2026-07-05 22:06:50 +02:00
Admin f6bab8c0d8 refactor(gateway): simplify prefix extraction in TicketValidator
Read the prefix claim via ClaimsIdentity.FindFirst instead of a three-part
ternary over the claims dictionary, and drop the XML summary.
2026-07-05 21:59:55 +02:00
Admin 3e03fbf078 harden(gateway): prefix-boundary + path-traversal checks; simplify ticket validation
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.
2026-07-05 21:50:43 +02:00
Admin 695f4378c6 refactor(api): OpenAPI response metadata, cached user sync, ProblemDetails
- 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.
2026-07-05 21:04:15 +02:00
Admin c4f5e6754c feat(deploy): consolidated dev stack with auto-provisioned Zitadel
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.
2026-07-05 20:32:40 +02:00
Admin a37dcc1b45 Formatting 2026-07-05 16:04:53 +02:00
Admin b1010b0bb5 refactor(infra): register auditable interceptor via DI
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.
2026-07-05 15:14:03 +02:00
Admin 6e4b8fad56 refactor(infra): S3 client singleton, ffmpeg pipe ordering, tidy streams/jobs
- 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.
2026-07-05 15:07:49 +02:00
Admin 93ee8be0cd feat(infra): playlist processing saga + generalize fault handling (phase 3)
Adds the PlayList picture-processing lifecycle saga (correlated by PlayListId)
mirroring the track one, and generalizes the failure plumbing:
- Rename the track fault translator to ProcessingSlipFaultConsumer and use a
  generic Workflow.SubjectId variable + ProcessKind; it now emits typed
  Track{Picture,Audio}/PlayListPicture ProcessingFailed events for all slips
- PlayListProcessingStateMachine (+ EF-persisted PlayListProcessingState,
  migration): starts on CreatePlayListResourcesEvent, finalizes on
  PlayListPictureProcessed, and on failure marks the playlist Failed and
  removes the orphaned final picture (PlayListProcessingFailedConsumer +
  MarkPlayListAsFailedActivity)
- UpdatePlayListPictureActivity publishes PlayListPictureProcessed on success
- Picture slip carries SubjectId + PlayListPicture kind + fault subscription

Verified end-to-end in Docker (RabbitMQ + migrated Postgres): both saga
endpoints, the fault translator and cleanup consumers configure, and an
addressing cross-check confirms all 21 activity execute queues the builders
address exist (caught + fixed a kebab mismatch on mark-play-list-as-failed).

Build: 0 warnings, 0 errors.
2026-07-05 14:55:40 +02:00
Admin 6e5755039f feat(infra): track saga failure handling + cleanup (phase 2)
On definitive failure of the picture or audio slip, the saga now reaches a
Failed state and recovers end-to-end:
- Picture/audio track slips carry TrackId + ProcessKind variables and a
  subscription to a fault translator (TrackProcessingSlipFaultConsumer) that
  turns RoutingSlipFaulted into a typed Track{Picture,Audio}ProcessingFailed
- The saga stores the final object keys from the start event and, on failure,
  transitions to Failed and publishes TrackProcessingFailed
- TrackProcessingFailedConsumer runs a cleanup slip that removes the orphaned
  final picture/audio copies and marks the track LifeCycleStatus=Failed
  (MarkTrackAsFailedActivity) — the track stays as a visible, recoverable record
- Migration for the new saga columns

Verified: Worker boots against RabbitMQ + Postgres in Docker; the new
endpoints (fault translator, failed consumer, mark-track-as-failed) configure
and the bus starts clean.

Build: 0 warnings, 0 errors.
2026-07-05 14:49:33 +02:00
Admin 97855d9094 feat(infra): add Track processing saga (state machine, phase 1)
Introduce a MassTransit saga state machine that correlates by TrackId and
tracks the two parallel processing slips (picture + audio):
- TrackProcessingStateMachine + EF-persisted TrackProcessingState
  (pessimistic concurrency, EntityFrameworkRepository), migration added
- Starts on CreateTrackResourcesEvent; UpdateTrack{Picture,Audio}Activity now
  publish TrackPictureProcessed / TrackAudioProcessed on success; when both
  arrive the saga finalizes (Ready). Failure transitions to a Failed state
  (cleanup wired in the next phase)
- Add LifeCycleStatus.Failed and the four outcome events

Verified: booted the Worker against RabbitMQ + migrated Postgres in Docker;
the saga endpoint 'track-processing-state' is configured and the bus starts
clean.

Build: 0 warnings, 0 errors.
2026-07-05 14:41:19 +02:00
Admin ee3e457dde refactor(infra): auto-configure MassTransit endpoints via ConfigureEndpoints
Replace ~240 lines of hand-written per-endpoint wiring with MassTransit's
ConfigureEndpoints + KebabCaseEndpointNameFormatter, and apply retry + EF
outbox globally through AddConfigureEndpointsCallback. Adding a consumer or
activity is now a single AddConsumer/AddActivity line.

- Normalize the 4 mismatched activity endpoint names to kebab-case so the
  routing-slip addressing keeps resolving (update-play-list-picture,
  delete/mark/publish play-list variants); cleanup consumer queue is now
  routing-slip-clean-up
- Drop the now-unused QueueName consts from event consumers and trim
  EndpointHelper / delete the manual endpoint-config partial

Verified end-to-end: booted the Worker against RabbitMQ in Docker and
confirmed all 38 endpoints are configured with the exact names the builders
address (19 execute queues + compensations + consumers + routing-slip-clean-up).

Build: 0 warnings, 0 errors.
2026-07-05 14:22:26 +02:00
Admin b3ff1b6516 fix(infra): repair TransferFiles compensation + tidy MassTransit consistency
Fix (recoverability bug):
- TransferFilesToBucketActivity returned Completed() with no log, so its
  compensation could never delete the transferred files (orphaned objects on
  fault). IStorageService.TransferFilesAsync now returns the real uploaded
  keys; the activity logs them so Compensate can remove them. Also drops the
  Path.Combine key recomputation (produced backslash keys on Windows).

Consistency / DRY:
- Normalize all courier activities to internal + folder-based namespaces
  (several were public and in the flat .Activities namespace)
- Extract the duplicated exponential retry into UseStandardRetry
- Unify endpoint-name logic into a single EndpointHelper (remove the parallel
  MassTransitEndpointNames helper)

Build: 0 warnings, 0 errors.
2026-07-05 04:58:27 +02:00