Skip to content

feat(share): public share-link surface for playlists (Phase 1.g.1)#25

Merged
InstaZDLL merged 1 commit into
mainfrom
feat/1-g-1-share-token
Jun 3, 2026
Merged

feat(share): public share-link surface for playlists (Phase 1.g.1)#25
InstaZDLL merged 1 commit into
mainfrom
feat/1-g-1-share-token

Conversation

@InstaZDLL
Copy link
Copy Markdown
Owner

@InstaZDLL InstaZDLL commented Jun 3, 2026

Summary

Phase 1.g.1 — the server side of the playlist sharing loop. Mints opaque share tokens, exposes a public read endpoint, and instant-revoke. Mirror the auth/public split the streaming surface uses.

What lands

  • Migration 20260603000000_playlist_share_token.sql — adds `share_token TEXT` to `playlist` + a partial UNIQUE index (`WHERE share_token IS NOT NULL`) so private playlists don't pay index bloat.
  • `db::share` — 3 helpers (`mint_or_get_token`, `revoke_token`, `fetch_public_by_token`) with tenant ownership baked into the SQL via JOIN on `profile.user_id`. `mint_or_get_token` uses `COALESCE(share_token, $candidate)` to make the race between two concurrent mints atomic — no SELECT-then-INSERT window.
  • `api/share` — three routes:
    • `POST /api/v1/profiles/{profile_id}/playlists/{playlist_id}/share` (JWT) → `{ token }`. Idempotent.
    • `DELETE /api/v1/profiles/{profile_id}/playlists/{playlist_id}/share` (JWT) → 204.
    • `GET /api/v1/share/playlists/{token}` (no auth) → public preview DTO.
  • Wire DTO `PublicPlaylistResponse` — name + description + cover + brand tokens + timestamps + empty `tracks: []`. Tenant-identifying fields (profile_id, etc.) are not exposed.
  • 7 integration tests covering mint idempotency, revoke + re-mint produces fresh token, foreign-playlist 404, public 404 on unknown token, 401 gate, and the public payload's no-leak property.

Scope choices

  • Tracks are empty in the public DTO today. Server doesn't materialise `playlist_track` yet; the field is present so 1.g.2 can populate without a wire break.
  • Token in body, not URL in the mint response. Server doesn't know the web origin — clients combine the token with their persisted `waveflow_web_url` to build the shareable URL.
  • Idempotent mint, not rotating — two calls return the same token. Rotation requires explicit revoke + re-mint.
  • Revoke instant — partial UNIQUE allows re-using the prior value if the user re-mints later (no reservation).

Test plan

  • `cargo test --test share` (needs DATABASE_URL set to a live Postgres) — 7 tests
  • `cargo clippy --all-targets -- -D warnings` green
  • Smoke: mint, hit `/api/v1/share/playlists/{token}` from a fresh curl, revoke, confirm 404

Next: web (1.g.2) → `/p/$token` SSR route + OG tags. Then desktop (1.g.3) → `ShareModal` with QR code.

Summary by CodeRabbit

Notes de version

  • Nouvelles fonctionnalités

    • Ajout de la fonctionnalité de partage de playlists : les utilisateurs peuvent désormais générer des tokens de partage pour leurs playlists, permettant l'accès public en lecture seule via un lien.
    • Possibilité de révoquer l'accès partagé aux playlists à tout moment.
  • Tests

    • Suite de tests complète pour la fonctionnalité de partage de playlists incluant les cas d'idempotence et de sécurité.

Adds three endpoints split across the auth-vs-public boundary the
streaming module already uses:

- POST /api/v1/profiles/{p}/playlists/{pl}/share (JWT) — mints (or
  echoes) an opaque 32-char URL-safe token. Idempotent via
  COALESCE(share_token, $candidate) so a race between two parallel
  mints lands a single value and both callers receive it.
- DELETE /api/v1/profiles/{p}/playlists/{pl}/share (JWT) — clears
  the token, instantly closing the public URL.
- GET /api/v1/share/playlists/{token} (no auth) — public preview
  with name + description + cover + brand tokens. tracks: [] until
  server-side playlist_track materialisation lands in 1.g.2.

Tenant ownership is verified inline in every helper (user_id ->
profile_id -> playlist) so a proxy attack against a foreign
playlist surfaces as 404 with no existence leak.

Partial UNIQUE on share_token allows the vast majority of private
playlists to skip the index pages. 7 integration tests cover the
mint/revoke/public-get matrix incl. proxy attack + 401 gate.

Signed-off-by: InstaZDLL <github.105mh@8shield.net>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5db11c80-c596-4e28-8f94-b0e0566ec062

📥 Commits

Reviewing files that changed from the base of the PR and between 56cc36b and 85e5d18.

📒 Files selected for processing (6)
  • Cargo.toml
  • migrations/20260603000000_playlist_share_token.sql
  • src/api/mod.rs
  • src/api/share.rs
  • src/db.rs
  • tests/share.rs

📝 Walkthrough

Résumé

Cette PR ajoute une API complète de partage public de playlists via des jetons opaques. Les playlists peuvent être partagées (mint idempotent d'un jeton), révoquées immédiatement, et lues publiquement sans authentification JWT. Le jeton sert d'authentification implicite pour la lecture anonyme. Tous les endpoints sont testés end-to-end incluant isolation multi-tenant, contrôles d'authentification, et validations de non-fuite de données tenant.

Modifications

Partage public de playlists

Couche / Fichier(s) Résumé
Fondation : schéma et dépendance runtime
Cargo.toml, migrations/20260603000000_playlist_share_token.sql, src/db.rs
Migration créant playlist.share_token avec index unique partiel. Ajout de rand = "0.8" en dépendance runtime pour la génération de jetons. Module db::share exposant TOKEN_LEN = 32.
Opérations SQL : mint, revoke, fetch public
src/db.rs
mint_or_get_token génère ou réutilise un jeton via UPDATE atomique avec COALESCE, vérification chaîne possession. revoke_token supprime le token. fetch_public_by_token récupère les métadonnées playlist sans contrôle d'auth, retourne Option<...> pour gérer token absent/révoqué.
Contrats API et DTOs
src/api/share.rs
Types publics : MintResponse { token }, PublicPlaylistResponse (métadonnées + tracks: Vec<> vide), PublicTrack (titre, artist, durée). Documentation module décrivant frontières auth/public.
Handlers HTTP et routers OpenAPI
src/api/share.rs
POST mint (200/404/500), DELETE revoke (204/404/500), GET public (200/404/500). Deux routers séparés : auth_router (mint/revoke sous JWT), public_router (GET sans JWT). Journalisation tracing::error!.
Intégration aux routers principaux
src/api/mod.rs
Déclaration module share. Montage share_mint_router derrière auth_layer JWT, share_public_router en surface publique. Fusion dans OpenApiRouter global.
Suite de tests end-to-end
tests/share.rs
Huit tests couvrant mint + lecture publique, idempotence, revoke, revoke + re-mint, isolation multi-tenant (404 sans fuite), token inconnu, absence d'authentification (401). Assertions vérifient que profile_id ne fuit pas en réponse publique.

Diagrammes de séquence

sequenceDiagram
  participant User
  participant API
  participant DB
  User->>API: POST /profiles/{profile_id}/playlists/{playlist_id}/share (JWT)
  API->>DB: mint_or_get_token(user_id, profile_id, playlist_id)
  DB-->>API: Ok(Some(token)) ou Ok(None) si accès refusé
  API-->>User: 200 {token} ou 404
  User->>API: DELETE /profiles/{profile_id}/playlists/{playlist_id}/share (JWT)
  API->>DB: revoke_token(user_id, profile_id, playlist_id)
  DB-->>API: Ok(true) ou Ok(false)
  API-->>User: 204 ou 404
  User->>API: GET /api/v1/share/playlists/{token} (sans JWT)
  API->>DB: fetch_public_by_token(token)
  DB-->>API: Ok(Some(metadata)) ou Ok(None)
  API-->>User: 200 {playlist} ou 404
Loading

Estimation d'effort de revue

🎯 3 (Modéré) | ⏱️ ~25 minutes

PRs potentiellement liées

Poème

🎵 Des jetons opaques, générés par le hasard,
Partagent les playlists sans besoin de gard',
Mint idempotent, revoke immédiat,
Lecture publique sans JWT, quel pas !
Multi-tenant isolé, tests rigoureux,
Une API de partage enfin joyeux. 🎶

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Le titre décrit précisément la fonctionnalité principale : implémentation d'une surface de partage public pour les playlists avec un token opaque (Phase 1.g.1). Clair, spécifique et parfaitement aligné avec les changements du PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1-g-1-share-token

Comment @coderabbitai help to get the list of available commands and usage tips.

@InstaZDLL InstaZDLL self-assigned this Jun 3, 2026
@InstaZDLL InstaZDLL merged commit 9bab9d0 into main Jun 3, 2026
8 checks passed
@InstaZDLL InstaZDLL deleted the feat/1-g-1-share-token branch June 3, 2026 22:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant