Passa al contenuto principale

Identity Link con l'App Playroll

L'identity link collega l'utente authenticated dell'app Playroll all'identità del contributor attivata dal Contributor Portal.

playroll_supa_new.playroll_users.user_id <-> playroll_contributor_portal.contributors.id

Questo collegamento è il prerequisito per mostrare nell'app Playroll la verifica del contributor, lo stato di payout-ready, le ore validate, le ricompense e lo stato dei pagamenti. Non è il ledger delle ricompense e non calcola gli importi pagabili.

Due flussi

Ci sono due modi in cui il collegamento può essere stabilito. Condividono le stesse tabelle del database e lo stesso contratto di proiezione, cambia solo il bootstrap.

App-firstPortal-first
TriggerL'app genera un JWT intent firmato e apre il portaleIl contributor genera sul portale un codice di attivazione di 8 caratteri e lo incolla nell'app
Canale di bridgeParametro URL ?link_intent=<JWT>Codice monouso conservato come hash SHA-256 sul portale, inviato nel body della POST di redeem
Edge function di ingressoactivate-contributor-app-link (progetto portale)mint-activation-code (progetto portale) + redeem-activation-code (progetto app)
Quando usarloIl contributor è già loggato nell'app Playroll e vuole collegarsiIl contributor ha completato prima l'onboarding sul portale (es. landing page, marketing) e installa l'app solo in un secondo momento

Entrambi i flussi terminano nello stesso esito: contributor_app_links.status = 'active' sul portale e contributor_status_projection.link_status = 'verified' sull'app.

Repository

playroll-cpp
supabase/functions/create-contributor-link-intent/index.ts (app-first: app side)
supabase/functions/redeem-activation-code/index.ts (portal-first: app side)
supabase/migrations/20260511160000_add_contributor_status_projection.sql

playroll-contributor-portal
supabase/functions/activate-contributor-app-link/index.ts (app-first: portal side)
supabase/functions/mint-activation-code/index.ts (portal-first: portal side)
supabase/migrations/20260511161000_add_contributor_app_links.sql
supabase/migrations/20260515180000_add_recorder_activation_codes.sql
src/features/portal/usePortalFlow.ts
src/features/rewards/RecorderCodeBanner.tsx
src/lib/portalApi.ts
src/lib/portalUrl.ts

playroll-ui
src/main/ipc/handlers.ts
src/renderer/pages/Dashboard.tsx
src/renderer/features/launcher/Launcher.tsx
src/renderer/features/launcher/views/HomeView.tsx
src/renderer/features/launcher/views/SettingsView.tsx (portal-first: app side UI)

Progetti Supabase

Playroll app project:
name: playroll_supa_new
ref: vtyolotvuvlbvqbgbzde
url: https://vtyolotvuvlbvqbgbzde.supabase.co

Contributor portal project:
name: playroll_contributor_portal
ref: xlsjwlmsqonllvtyplml
url: https://xlsjwlmsqonllvtyplml.supabase.co

Il portale scrive il source del link nel proprio progetto e fa upsert di un read model minimale nel progetto dell'app. L'app legge soltanto la proiezione.

Source of Truth

Tabella source del portale:

playroll_contributor_portal.public.contributor_app_links

Stati:

pending_activation
active
revoked

Regole:

  • un solo link attivo per contributor
  • un solo link attivo per utente dell'app Playroll
  • le righe pending catturano solo il contesto dell'app
  • l'attivazione richiede il gate del portale
  • i link attivi sincronizzano una riga di proiezione nel progetto Supabase dell'app Playroll

App read model:

playroll_supa_new.public.contributor_status_projection

La regola dell'app è volutamente semplice:

projection row exists for current app user -> Verified
projection row missing -> Not verified

La proiezione non deve esporre documenti legali, IBAN, dati di payout completi o altre PII del portale.

Gate di Attivazione

L'app non può attivare un contributor da sola. L'app dimostra l'identità dell'utente dell'app Playroll; il portale dimostra la readiness del contributor.

Il portale attiva soltanto quando sono complete le accettazioni dei documenti richiesti:

onboarding_submissions.accepted_documents.incarico = true
onboarding_submissions.accepted_documents.privacy = true
onboarding_submissions.accepted_documents.compensation = true
onboarding_submissions.accepted_documents.isee_notice = true

ed esiste un receiving-method SEPA IBAN confermato:

payout_receiving_methods.method_type = sepa_iban
payout_receiving_methods.status = confirmed
payout_receiving_methods.email_confirmation_status = confirmed

manual_review non è sufficiente per l'attivazione automatica in questo MVP.

Flusso App-first

Comportamento importante:

  • l'intent firmato è a vita breve e non porta con sé credenziali del portale
  • il portale accetta sia link_intent sia il parametro di query intent_id legacy-compatibile
  • il portale memorizza l'attivazione come pending se l'utente è loggato ma non ha ancora completato il gate
  • la stessa riga pending viene promossa a active dopo che il gate viene superato

Flusso Portal-first

Se un contributor completa l'onboarding sul portale prima di aver mai aperto l'app Playroll, il bridge JWT app-first non può partire (non c'è ancora una sessione Steam-authenticated). Il flusso portal-first usa al suo posto un codice di attivazione a vita breve.

Spec del codice:

alphabet: A-H J K M N P-Z 2-9 (excludes 0, 1, I, L, O for legibility)
length: 8 chars
entropy: ~38 bits (single-use + 15 min TTL keeps brute force impractical)
storage: SHA-256 hex of normalised code; raw is shown once and never persisted
TTL: 15 minutes
single-use: status flips 'issued' -> 'redeemed' in one atomic UPDATE

Tutele lato server su mint-activation-code:

  • Stesso controllo di gate di activate-contributor-app-link: accettazioni dei documenti richieste + IBAN SEPA confermato.
  • Materializza la riga contributors tramite ensure_current_contributor se manca. Gli utenti portal-first arrivano al mint prima di qualsiasi app-link, quindi la riga potrebbe non esistere ancora.
  • Rate limit: massimo 5 codici emessi per contributor all'ora.
  • Qualsiasi altro codice issued per lo stesso contributor viene revocato prima che il nuovo venga emesso (l'indice unique parziale recorder_activation_codes_one_issued_per_contributor garantisce al massimo un codice attivo).

Tutele lato server su redeem-activation-code:

  • L'identità Steam deve esistere su playroll_users per l'auth user chiamante.
  • Lookup SHA-256 atomico; codici di errore distinti per code_not_found, code_expired, already_redeemed, code_revoked.
  • Rate limit in-memory: massimo 10 tentativi di redeem per app_user_id all'ora.
  • Lo step 5 (scrittura di contributor_app_links come active) usa un update-or-insert manuale invece di UPSERT, perché gli indici unique su quella tabella sono parziali (WHERE status = ...) e Postgres rifiuta ON CONFLICT su indici parziali.

Il sistema continua a non usare il matching via email come bridge. Entrambi i flussi richiedono un utente app authenticated e una prova lato portale della readiness del contributor; il portal-first si limita a disaccoppiare l'ordine in cui le due cose avvengono.

Observability

Le risposte delle Edge Function includono request_id per la correlazione. Durante i test e2e, usalo per unire i log dell'app, i log del browser del portale e i log delle function Supabase.

Log delle function lato app:

Contributor link intent created
Contributor link intent rejected: missing Steam identity
Failed to create contributor link intent

Log delle function del portale:

Contributor app link intent received
Contributor app link captured pending activation
Contributor app link still pending activation
Contributor app link verified
Contributor app link conflict
Failed to check contributor activation gate
Failed to ensure contributor
Failed to activate contributor app link

Log del browser del portale (app-first):

[ContributorLink] sync started
[ContributorLink] sync failed
[ContributorLink] sync completed

Eventi di telemetria portal-first (telemetry_events sul progetto portale):

recorder_code.requested portal browser, click on "Genera codice"
recorder_code.minted mint-activation-code success, attrs: contributor_id, expires_at
recorder_code.mint_failed mint refused (gate, rate, contributor_create, ...)
recorder_code.displayed portal UI rendered the raw code to the user
recorder_code.copy_clicked portal UI copy button hit
recorder_code.expired_in_ui countdown reached zero before redeem
recorder_code.redeemed redeem-activation-code success
recorder_code.redeem_failed redeem refused (not_found, expired, already, revoked, ...)

Eventi di telemetria dell'app Playroll:

contributor_link.intent_opened
contributor_link.intent_failed
contributor_link.status_verified
contributor_link.status_missing
contributor_link.status_fetch_failed
contributor_link.redeem_success
contributor_link.redeem_failed

Accettazione E2E

Happy path:

App user is authenticated
App creates link intent
Portal receives link_intent
Portal user completes required document acceptances
Portal user confirms SEPA IBAN
Portal activates link
Portal writes contributor_status_projection
App refreshes and shows Verified

Controlli negativi (app-first):

missing app auth -> create intent returns 401
missing Steam identity -> create intent returns 409
missing document acceptance -> portal returns pending_activation or activation_required
missing confirmed SEPA IBAN -> portal returns pending_activation or activation_required
manual_review receiving method -> not sufficient for activation
same app user linked to another contributor -> conflict
same contributor linked to another app user -> conflict

Happy path (portal-first):

Contributor signs up on portal, completes documents, confirms SEPA IBAN
Contributor clicks "Genera codice" in Rewards section
Portal shows 8-char code (e.g. ZJ929AEY) with 15-min countdown
Contributor opens Playroll app, signs in with Steam
Contributor pastes the code in Settings -> Account contributor -> Riscatta
contributor_app_links flips to active, contributor_status_projection upserts to verified
App refreshes within ~1s and shows VERIFIED badge

Controlli negativi (portal-first):

mint without completed gate -> 409 activation_gate_incomplete
mint hit 6th time within an hour -> 429 rate_limited
redeem unknown code -> 404 code_not_found
redeem code older than 15 min -> 410 code_expired
redeem code already used by another device -> 409 already_redeemed (with redeemed_app_user_id)
redeem code that mint revoked -> 409 code_revoked
redeem without Steam identity on playroll_users -> 409 steam_identity_required