Skip to main content
Petanque Life
← Back to all features
02

Identity & Access Management

199 features · 7 subsystems

User identity, authentication, authorization, and role management across the entire federation hierarchy. Built on a comprehensive auth stack and capability-based access control.

User Registration & Profile

F02.01
Shipped

> **Härdning (D-HARDEN · `2fd8abe9.m-1` t-2):** admin-ytorna **Medlemsregister** (`/medlemmar`) och

How it works
  • F02.01.01 Shipped

    Self-registration with email/phone verification

    ✅ PL-F0201a (OTP via craft_easy.core.auth.otp, best-effort delivery sink) , ♻️ Rebuild fa798ce7.m-3.t-2 (D-LOCALE FR-3/FR-6 — *identitetens språk sätts nu, inte gissas*: migration 00146 flyttar auth_identity.locale-defaulten 'sv'→'en' (inga UPDATE av befintliga rader) och varje skapandeväg skriver ett explicit härlett värde. Anonyma självregistreringar (OTP/magic-verify, OIDC Branch 3) härleder ur den skapande requestens Accept-Language (RFC 9110, kanoniserad mot språkregistryt; ingen match ⇒ en); tenant-bundna vägar (sys admin-invite, tier-flödets first_admin, onboarding-provisionering, wizard-invite) härleder ur tenantens default_locale — aktörens header används aldrig. En befintlig identitets lagrade locale röres aldrig av en inloggning. Se specs/api/endpoints/dusers-signup.md + docs/engineering/i18n-strategy.md §Server-side locale)
  • F02.01.02 Shipped

    OAuth2 social login (Google, Microsoft, GitHub)

    ✅ PL-1801 (gateway), ✅ PL-1802/03/04 (providers), ✅ PL-1805 (app + admin SSO-knappar), ✅ PL-1806 (flera identiteter per user, Connected Accounts-vy, fresh-auth-fönster, last_auth_method-policy, upstream-revokation)
  • F02.01.03 Shipped

    Player profile (name, DOB, nationality, photo, playing position)

    ✅ PL-F0201a (base model PL-105, per-tenant matrix SE/FR/DE), ✅ PL-T311 (premium app-shell — cover-gradient + −48 px overlap-avatar + sticky <SegmentedControl> Översikt/Stats/Lic/Settings, mobil 1-kol / desktop 2-kol, gateadepad bakom pl_premium_profile_v1)
  • F02.01.04 Shipped

    Official profile (umpire grade, certifications, languages)

    ✅ PL-F0201a (umpire grade L1–L5, certification expiry validation, language list)
  • F02.01.05 Shipped

    Administrator profile (federation, role, mandate period)

    ✅ PL-F0201a (role enum, mandate range validator, currently-serving filter)
  • F02.01.06 Shipped

    Multi-identity support (same person = player + umpire + coach)

    ✅ PL-F0201a (GET /users/{user_id}/profiles aggregates player/official/coach/administrator)
  • F02.01.07 Shipped

    Profile photo upload and management

    ✅ PL-F0201b (multipart upload, PNG/JPEG/WebP, max 5 MB, magic-byte + Pillow-validering, SHA-256 ETag, en bild per profil över alla fyra profil-typer)
  • F02.01.08 Shipped

    Profile completion tracking and prompts

    ✅ PL-F0201a (completion_pct + currently_serving on administrator), ✅ PL-F0201b (GET /{profile}/prompts med aktionsbara meddelanden och severity high/medium/low)
  • F02.01.09 Shipped

    GDPR-compliant data export and deletion

    ✅ PL-F0201b (profile_photos + check_in_records inkluderade i /me/data-export; foton raderas och administrator-profiler anonymiseras i raderingsflödet)
  • F02.01.10 Shipped

    Player ID card generation (digital + printable)

    ✅ PL-F0201b (GET /player-profiles/{id}/card?format=json|pdf|png, ISO/IEC 7810 ID-1 PDF, 1050×660 PNG, stabil JSON + base64-url QR-payload), ✅ PL-T311 (app <LicensePassCard> — bottle-green gradient + embossed QR + holografisk sheen + multi-tenant pager, mountad på (tabs)/license-routen och i profilens License-tab), ♻️ Rebuild 525ece23.m-3.t-1 (rena /v1-kontraktet GET /v1/player-profiles/{id}/card?format=json|png|pdf|pkpass i Go: json/png MUST, pdf/pkpass SHOULD, 415 på okänt format; pkpass-pass (Wallet) med live giltighet/spärr; se specs/api/endpoints/card-and-checkin.md)
  • F02.01.11 Shipped

    QR code identification for check-in at events

    ✅ PL-F0201b (HMAC-SHA256 v1-token, SVG/JSON QR, POST /major-events/{id}/check-in med replay-, expiry- och tampering-försvar, audit-log via CheckInRecord), ♻️ Rebuild 525ece23.m-3.t-1 (Go: GET /v1/me/check-in-token HMAC-signerad self-contained token + POST /v1/events/{id}/check-in append-only check_in_record med token_jti UNIQUE-replay-grind; felkontrakt check_in_token_replayed/_expired/_invalid; offline-verifierbar för Wallet/dålig täckning)
  • F02.01.12 Shipped

    Player stats dashboard (ELO trend, win-rate, partners, opponents, history)

    ✅ PL-T311 (GET /me/stats?period=30d|90d|365d|all aggregator + app <StatsTab> med <EloTrendChart>, <WinRateRing>, top-3 partners/opponents, last-20-matches; Cache-Control: private, no-store)
  • F02.01.13 Planned

    ICE/nödkontakt + skyddad medicinsk basinfo (art.9) med break-glass

    ♻️ Rebuild 525ece23.m-5.t-1 (Go: additiv ALTER player_profile (emergency_contact/medical_flags jsonb); GET/PUT /v1/me/emergency-medical — samtyckesgrindad (ConsentResolver-port, fail-closed) + minderårig-parental_consent + optimistisk låsning + audit; maskerad i alla icke-ägar-läsvägar (FR-3); break-glass GET /v1/events/{id}/participants/{aid}/emergency-medical capability-gatead (incident:emergency-read) + incheckad + pågående event + obligatoriskt audit-skäl; felkontrakt consent_required/parental_consent_required. Samtyckes-/retention-modellen ägs av D-GDPR; se specs/api/endpoints/dusers-emergency-medical.md + docs/domain/emergency-medical.md)
  • F02.01.14 Planned

    Egen ICS-kalender (SHOULD) — personlig prenumerationsfeed över alla tenants

    ♻️ Rebuild 525ece23.m-6 (Go t-1: GET /v1/me/calendar.ics?token= publik, token-autentiserad RFC 5545-feed som aggregerar personens egna poster över alla tenants (BR06.08) via en CalendarSource-port — D-VENUE-banbokningar wirade, D-COMP/D-EVENTS som seams (noll poster, aldrig fel, §5d/NFR-9); revokerbar, endast hashad feed-token; GET /v1/me/calendar-subscription + POST /v1/me/calendar.ics:rotate-token; stabil UID per post. app-UI t-2: Inställningar → "Min kalender & favoriter" visar/kopierar feed-URL:en + roterar/spärrar (app/app/kalender-favoriter.tsx); se specs/api/endpoints/me-calendar-favorites.md + specs/app/views/kalender-favoriter.md)
  • F02.01.15 Planned

    Spara favoriter (NICE) — favoritklubb/-venue/-spelare/-tävling

    ♻️ Rebuild 525ece23.m-6 (Go t-1: favorite-tabell (polymorf, tenant_id nullable, CHECK-enum favorite_type, UNIQUE (auth_identity_id, favorite_type, target_ref), ingen version) + GET/POST/DELETE /v1/me/favorites — self-skopad (IDOR-skydd), 409 favorite_exists, 422 på okänd typ/icke-UUID, 404-icke-läckage; server-side tenant-skopning (player → tenant_id=NULL); bokmärkes-semantik, ingen cross-domän-validering. app-UI t-2: favorit-snabblista (lägg till/ta bort) i app/app/kalender-favoriter.tsx; se specs/api/endpoints/me-calendar-favorites.md + specs/app/views/kalender-favoriter.md)
  • F02.01.16 Planned

    Tenant-medlemsregister — admin listar den aktiva tenantens medlemmar och öppnar en LIVE profil-detalj

    ♻️ Rebuild be2f9ef1.m-1.t-1 (Go: GET /v1/tenant/members — den tenant-skopade läsytan admin-registret (admin/lib/members.tsx, useMemberRegister) konsumerar, tidigare 404. Capability-gatead member:read (ingen ny capability), aktiv tenant ur kontexten (requireTenant/reqctx, aldrig path/query/body/header), {members:[MemberRow]}-kuvert där varje rad bär ett konkret profile_id (= <type>_profile.id, inte auth_identity_id) som löser GET /v1/{profile_type}-profiles/{id}. Riktig data: UNION ALL över player/official/coach/administrator_profile JOIN auth_identity, tenant-filter på <type>_profile.tenant_id (NFR-1 hård cross-tenant-isolation), stabil ordning, NULL display_name → "". Read-only, ingen migration/audit. Stängde den sista hårda frontend↔backend-kontraktsluckan på webbplattformen. Se specs/api/endpoints/tenant-members.md + specs/admin/views/member-register.md)
  • F02.01.17 Planned

    Eget språkval på kontot — användaren byter UI-språk och valet följer kontot över ytor och enheter

    ♻️ Rebuild fa798ce7.m-4.t-1 (D-LOCALE FR-2/FR-4, kontraktsdelen: ny self-endpoint PATCH /v1/me (GuardActorBearer) som uppdaterar enbart auth_identity.locale för bärarens egen identitet — inga user_id/tenant_id i kontraktet, alltså IDOR-fri by construction. Alias lagras kanoniskt (no⇒nb, zh-TW⇒zh-Hant), okänd tagg ⇒ 422 med fältfelet locale före varje skrivning, svaret är identiteten i samma form som GET /v1/me med ny ETag, och en omskrivning med samma värde är idempotent (ingen version-bump). Gratisnivå: varken licens, medlemskap, betalning eller valt tenant-medlemskap krävs. Delade kontrakt i packages/shared: upplösningskedjan resolveUiLocale (lagrad locale → enhetsspråk → tenantens default_locale → en, en kodväg för web+app) och formaterings-helpern formatNumber/formatDate/formatCurrency/formatIsoDate (valutakoden ur DATA, presentationen ur UI-localen, maskinformat aldrig lokaliserat). Ytwiringen (provider, växlare, microcopy) levereras av fa798ce7.m-4.t-2. Se specs/api/endpoints/me-locale.md + docs/engineering/i18n-strategy.md §Produktytornas språkkedja), ♻️ Rebuild fa798ce7.m-4.t-2 (D-LOCALE FR-2, ytleveransen — kedjan är WIRAD, inte bara byggd: I18nProvider monteras med resolvad locale + makeBundleFetcher mot GET /v1/i18n/bundles/{app}/{locale} (endpointen fanns men hade aldrig anropats av någon klient) i både web (WebI18nProvider, innanför ytans enda ApiClient) och app (AppI18nProvider i rotlayouten); 404/nätfel degraderar tyst till den statiska kaskaden. Språkväxlare i webs skal (web-language-picker, inloggad) och appens Inställningar (app-language-picker, mockupens Language-fält): setLocale direkt utan omladdning + PATCH /v1/me, skrivfel backar UI:t ärligt, anonym kan inte välja (ingen anonym persistensmekanism). Kontosynken bevisad: appen läser lagrad locale via GET /v1/me vid sessionsetablering — samma identitet ser samma språk i web och app utan delade cookies. <html lang> kommer ur tenantens default_locale (literalen "sv" borta), skaltexten (siteChrome.*, appNav.*) ur microcopy i en/sv/fr/de, och all formatering följer UI-språket (maskinformat via formatIsoDate). Låst av vakten tools/gate/guards.d/dlocale-format-guard.mjs (G1/G2/G3) + L4-drivrutinen 740-dlocale-m4 mot riktig stack. Se specs/web/views/site-chrome.md §Chromens SPRÅK + specs/app/views/installningar.md)

Authentication

F02.02
Shipped
How it works
  • F02.02.01 Shipped

    Email + OTP login

    ✅ PL-1901 (admin console), ✅ PL-F0202a (per-tenant-matris Sverige/France/Deutschland, enumeration-skydd, abuse-limits), ✅ PL-T310 (premium passwordless flow i app: split-screen desktop / glass-mobil <AuthShell>, refined 6-digit <OtpInput> med auto-advance + paste-spread + monospace, 30 s resend-cooldown, 10 min expiry-countdown, <AuthSuccess>-anim, reduced-motion-fallback, aria-live error-announce) , ♻️ Rebuild fa798ce7.m-3.t-2 (mottagarlocalen resolvas nu i flödet — lagrad identitets-locale → requestens Accept-Language → en — och förs genom auth.Mailer-sömmen via den valfria utökningen auth.LocaleAwareMailer (inget interface breddas, ingen fake knäcks). Dev-sinken lagrar den och GET /auth/dev/last-challenge returnerar det additiva fältet locale. Inget lokaliserat mejlinnehåll fabriceras — mallkatalogen ägs av D-COMM/D-I18N; 202-enumeration-formerna är byte-identiska)
  • F02.02.02 Shipped

    OAuth2 (Google, Microsoft, GitHub) via SSO-gateway with per-app routing

    ✅ PL-1801 (infra), ✅ PL-1802 (Microsoft/Entra ID), ✅ PL-1803 (Google), ✅ PL-1804 (GitHub), ✅ PL-1805 (frontend SSO-knappar app + admin, /auth/complete + /auth/native-complete landings, shared gateway-kontrakt), ✅ PL-1806 (Connected Accounts-vy i app + admin, GET /auth/me, GET/DELETE /auth/me/oauth-identities, POST /auth/oauth/{provider}/link/{app} med 5-min fresh-auth-fönster), ✅ PL-F0202a (surface-test Google+Microsoft i providers-listan), ✅ PL-T310 (refined <FederatedRow> icon-buttons med brand-tinted ring + per-provider a11y-label i app-login), ✅ Go-backend 4848185c.m-2 (F1-3 m-2): ren OIDC relying party (Google/Microsoft/Apple) — discovery + JWKS-cache (kid-miss-refetch), PKCE-S256-obligatorisk authorize, one-shot state + binding-cookie, fullverifierad ID-token, 4 provisioneringsgrenar med per-provider compute_email_verified, web/native-leverans utan token-i-URL, ?error=-/problem+json-felkoder, anti-abuse-rate-limits + lokal Keycloak-IdP-harness (t-1/t-2/t-3; api/internal/http/oidc.go, api/internal/auth/oidc/)
  • F02.02.03 Shipped

    TOTP/2FA (authenticator app)

    ✅ PL-F0202a (/auth/2fa/setup, /enable, /verify, /status, /disable, recovery-koder engångsbruk, PreAuthToken → UserToken-upgrade, petanque-override av den tidigare require_auth-buggen)
  • F02.02.04 Shipped

    WebAuthn passkeys

    ✅ PL-F0202a (/webauthn/register/options, /register, /credentials list+delete, petanque-override av an upstream user_id-typbug, clientDataJSON/challenge/origin-verifiering, duplicate-guard)
  • F02.02.05 Shipped

    JWT ES512 token-based sessions

    ✅ PL-1801 (OAuth-gateway), ✅ PL-F0202b (GET /auth/session/me med explicit algorithm-fält, ES512 verifierat via JWT-headerinspektion)
  • F02.02.06 Shipped

    Refresh token rotation

    ✅ PL-F0202b (RefreshTokenFamily/RefreshToken med SHA-256-hash, family-wide theft detection, POST /auth/session/issue + /refresh + /logout, plrt_-prefix, 32-byte entropi)
  • F02.02.07 Shipped

    Rate limiting and abuse detection

    ✅ PL-1801 (slowapi på /authorize, /callback, /exchange), ✅ PL-F0202b (AuthAbuseMonitor sliding-window per IP + subject, 60/min slowapi-gräns på /session/refresh, /session/logout, /m2m/token, failure-bucket 10/5 min → 429 med Retry-After), ✅ Go-backend 4848185c.m-1 (F1-3 m-1, t-3): disposable-domän-block + DB-backad per-mottagare-/global-kvot på /auth/{otp,magic}/start + per-IP exponentiell verify-backoff/CAPTCHA-fäste på /auth/otp/verify, enumeration-säkert, 429 + Retry-After via RFC 9457 (api/internal/http/auth_antiabuse.go), ✅ Go-backend 4848185c.m-2 (F1-3 m-2, t-3): per-IP/min OIDC-budgetar ovanpå den globala limitern — authorize 10/min, callback 20/min, exchange 20/min — 429 rate_limited + Retry-After + RateLimit-* (in-memory/per-instans, api/internal/http/oidc_antiabuse.go)
  • F02.02.08 Shipped

    M2M OAuth2 client credentials for integrations

    ✅ PL-F0202b (POST /auth/m2m/token RFC 6749 §4.4, M2MClient-modell med capability-scope, plm2m_-prefix, admin-CRUD på /auth/m2m/clients, rotate-secret, revoke, IP-allowlist, RFC 6749 §5.2-felform, Cache-Control: no-store), ✅ Go-backend 4848185c.m-5 (F1-3 m-5, t-1): rena client_credentials-modellen — m2m_client (secret_hash bytea hash-only, scopes text[], ip_allowlist cidr[], opakt nullable tenant_id utan FK), token bär client:true → decode klassar actor_kind=m2m och binder aldrig som mänsklig session (NFR-5), enumeration-säker invalid_client, sys-skopad CRUD (sys-allowlist + fresh-auth), per-IP-rate-limit, audit (api/internal/http/auth_m2m.go, api/internal/store/m2m.go, 00007_m2m.sql)
  • F02.02.09 Shipped

    Service-account M2M-tokens med admin-scopes

    ✅ PL-T293 (ServiceAccount-dokument, ApiToken.service_account_id, admin-scopes tenants:write / federations:write / cms:write / admin_invitations:create / users:write / seed:run, /sys/service-accounts CRUD + token-mint/revoke, agent-driven prod-bootstrap utan WebAuthn-step-up, 90-d default TTL, governance + system audit). 🔁 Go-backend 4848185c.m-5 (F1-3 m-5, t-2) — agent-aktör (ren ny modell, ej portning): agent_token_issuance-revocation-list (token_id pk, parent_actor, impersonated_identity_id FK, inget token-värde lagras); claims agent:true + token_id + impersonerad sub (ingen fid); POST /auth/sys/agent-tokens/issue (sys + fresh, TTL ≤ 24 h, batch ≤ 20) + revoke-all; decode-revocation-check; mutation-scope-block (403 agent_action_blocked, reason=scope_block); env-gatad POST /auth/dev/login-as (local/test + allowlist, omonterad i prod → 404) mintar mänsklig session genom UI:t (PL-T347-mekanismen); audit agent_token_issued/agent_tokens_revoked_all/agent_action_blocked/dev_login_as. Riktig sys_security-roll + capability-enforcement + JIT-Tier-2 är F1-4/framtid (api/internal/http/auth_agent.go, auth_devlogin.go, api/internal/store/agent.go, 00008_agent.sql)
  • F02.02.10 Shipped

    Anti-abuse signup-pipeline

    ✅ PL-T301 — client_ip_cidr24 + time_to_fill_ms + honeypot_triggered + turnstile_score på SignupRequest, fraud-signal-aggregator (ip-cluster/email-repeat/honeypot/fast-fill/stripe-decline-rate) cachat 15 min, sys_security flag-fraud + email-domain-blocklist applicerad direkt vid POST /public/signup, audit-events signup.fraud_signal_triggered + signup.flagged_fraud + signup.email_domain_blocked, auto-SEV3-incident vid alert-severity. Sys-konsol: /customer-base/signups.

Role & Permission Management

F02.03
Shipped

### Predefined Role Templates

How it works
  • F02.03.01 Shipped

    Capability-based access control (deny-by-default)

    ✅ PL-F0203a, 🔁 Go-backend f07177c7.m-1 (F1-4 AuthZ m-1) — register + persistens: capability-registret materialiserat som tunt internt Go-paket api/internal/authz/ — capabilities-referenstabell (00009_authz.sql) + deklarativ in-process-katalog (catalog.go) med namnrymds-grammatik (<namespace>.<…> / <resurs>:<verb>, validerad av ValidateName) och is_sensitive_field-markör; idempotent seed-reconcile av katalogen. ✅ Go-backend f07177c7.m-2 (F1-4 AuthZ m-2) — enforcement-spine: beslutsoraklet authz.Decide (rent, fail-closed, en beslutskälla); route-markerande primitiv (RouteRegistry.Public/SelfGuarded/Require) + deny-by-default-boot-gate (CheckRoutes → serve-vägran + api authz-check-subkommando i CI — en omarkerad route = boot-fel, ej runtime-läcka); gaten fyller F1-1:s reserverade middleware.Authz()-slot; tenant-resolution + X-Tenant-Id-fallback + spoof (JWT-claim auktoritativ, fail-closed, 403 tenant_spoof); agent-scope-block på beslutsnivå (finance.* nekas agent-aktörer, 403 agent_action_blocked); audit på varje deny/spoof/agent-block; tunn orakel-HTTP-yta (GET /me/capabilities, POST /authz/capability-check(-batch)); felkod capability_denied. ✅ Go-backend f07177c7.m-3 (F1-4 AuthZ m-3) — data-lager-isolering + fält-maskering: tree-aware orakel + authz.ScopeTree-subträdsexpansion (fixtur-impl tills F1-9), ScopeFilter (fail-closed list-filtrering till caller:s synliga subträd), ScopedLookup (enforce:at 404-ej-403 på cross-scope/cross-tenant-miss + intern scope-miss-audit), scope-medveten RequireCapability (RequireScoped, beslut i resursens scope) och MaskSensitive (generisk DTO-agnostisk fält-maskering — authz:"sensitive=<cap>"-fält utelämnas för caller utan <cap>, 200 på resten, aldrig 403 på hela posten; VisibilityPolicy-krok för privacy_settings). Roll-/sys-CRUD och UI är senare F1-4-milstolpar; den riktiga org_nodes-hierarkin ägs av F1-9 (plugg:as i ScopeTree-seamen utan signaturändring). ✅ Go-backend f07177c7.m-4 (F1-4 AuthZ m-4) — sys cross-tenant-roller + agent-block-härdning: deterministisk sys-roll → capability-mappning (sysroles.go) + fail-closed sys-capability-resolver (sysresolver.go, cross-tenant); /sys/roles-ytan (GET /sys/roles, GET/POST /sys/users/{email}/roles, DELETE /sys/users/{email}/roles/{role}) med four-eyes self-grant-block (400 self_grant_blocked), fresh-auth (401 reauth_required) och idempotent grant; sys-roll-backad gate ersätter env-allowlisten (allowlist = A4-bootstrap); agent-scope-block härdat — konfigurerbar finans-lista (PL_AGENT_BLOCKED_NAMESPACES) + garantin att en agent som impersonerar sys_finance ändå nekas finans (agent_scope_block); audit sys.role.grant/sys.role.revoke. Kopierbart mönster i api/README.md → "AuthZ fundament (F1-4)" §6–§7
  • F02.03.02 Shipped

    Role definition with capability sets

    ✅ PL-F0203a, 🔁 Go-backend f07177c7.m-1 (F1-4 m-1): roll-modellen (roles, role_capabilities) + de sju seedade plattforms-mall-rollerna (player, umpire, coach, clubadmin, district_admin, federation_admin, support_member — tenant_id=NULL, is_system=true, deterministisk dokumenterad capability-uppsättning per roll); additiva roller (api/internal/authz/seed.go)
  • F02.03.03 Shipped

    Role inheritance (e.g., national-admin inherits regional-admin)

    ✅ PL-F0203a
  • F02.03.04 Shipped

    Role assignment per user per tenant (+ OrgNode scope for regional/club roles)

    ✅ PL-F0203a, 🔁 Go-backend f07177c7.m-1 (F1-4 m-1): role_assignments (principal IN A SCOPE — opakt tenant_id/scope_id/principal_id utan FK, polymorf principal, idempotent UNIQUE-tuple) + den rena fail-closed datalager-resolvern (EffectiveCapabilities — additiv union över aktiva, icke-utgångna tilldelningar, scope-bevarande, tom mängd vid saknad tenant = F-AT-03-skydd; ingen trädexpansion/Allow-Deny — senare milstolpe)
  • F02.03.05 Shipped

    Wildcard capabilities (e.g., competitions:*)

    ✅ PL-F0203a
  • F02.03.06 Shipped

    Sensitive field restrictions

    ✅ PL-F0203b (tabelldriven SENSITIVE_FIELD_SPECS, filter_document/filter_documents, assert_write_allowed, profile:read_sensitive-override, GET /sensitive-fields, POST /sensitive-fields/check), 🔁 Go-backend f07177c7.m-3 (F1-4 AuthZ m-3): MaskSensitive (api/internal/http/authz_mask.go) — generiskt, DTO-agnostiskt primitiv som vid serialisering utelämnar varje fält taggat authz:"sensitive=<capability>" när caller saknar <capability> i resursens scope (prövat via samma orakel), 200 på resten av posten, aldrig 403 på hela; field-level-capabilities privacy.personal_data.read/privacy.medical.read/privacy.safeguarding.read/finance.account.read (is_sensitive_field=true) idempotent seedade; VisibilityPolicy-krok för privacy_settings (full modell ägs av D-MEMBER/D-GDPR)
  • F02.03.07 Shipped

    Petanque-specific role templates (see below)

    ✅ PL-F0203b (category/suggested_scope/is_temporal/is_petanque_specific/requires_approval metadata på RoleTemplate, GET /role-templates?category=...&scope=...)
  • F02.03.08 Shipped

    Access debugging tools

    ✅ PL-F0203b (GET /access-debug/role-tree/{name} BFS inkl. cycle/missing-markering, GET /access-debug/users/{id}?at=... active/inactive split, POST /access-debug/check med granting_roles)
  • F02.03.09 Shipped

    Temporary role grants (e.g., tournament director for one event)

    ✅ PL-F0203b (POST /role-grants/temporary, GET /role-grants/temporary?active_only=..., DELETE /role-grants/{id}, exkluderar permanenta grants automatiskt)
  • F02.03.10 Shipped

    Role request and approval workflow

    ✅ PL-F0203b (RoleRequest-dokument, POST /role-requests med dubblettguard 409, /approve med scope-narrowing, /deny, /cancel med requester-only guard 403)

Organization Hierarchy Access

F02.04
Shipped

Two-layer access model: **Standalone tenants** for hard isolation (every federation is independent), **OrgNodes** (districts → clubs) for scoping within national tenants.

How it works
  • F02.04.01 Shipped

    Tenant-scoped data isolation (every federation is a standalone tenant)

    ✅ PL-F0204a (TENANT_SCOPED_MODEL_REGISTRY, describe_tenant_isolation, assert_tenant_match + structlog-audit hierarchy_access.cross_tenant_violation, GET /hierarchy-access/tenant-isolation)
  • F02.04.02 Shipped

    Public APIs for cross-tenant interactions (license verification, ITC, squad submission)

    ✅ PL-F0204a (CROSS_TENANT_PUBLIC_APIS-registret med 8 ytor: /public/licenses/verify, /public/itc/{request,release,complete}, /public/squads/{submit,{ref}}, /public/federations/directory; GET /hierarchy-access/public-apis)
  • F02.04.03 Shipped

    Linked player identity across tenants (same auth, separate profiles)

    ✅ PL-F0204a (resolve_linked_identities bypassar tenant-filter via en direkt fråga i datalagret, grupperar player/official/coach/administrator per tenant, GET /hierarchy-access/linked-identities/{user_id})
  • F02.04.04 Shipped

    District-based scope filtering (district admin sees clubs/players in their OrgNode subtree)

    ✅ PL-F0204a (scope_district_id-query + X-District-Scope-header på GET /player-profiles, BFS-traversal via resolve_district_subtree, GET /hierarchy-access/district-scope/{district_id} med audit-log), 🔁 Go-backend f07177c7.m-3 (F1-4 AuthZ m-3): ScopeFilter + tree-aware orakel (authz.ScopeTree.Subtree) filtrerar automatiskt list-endpoints till distriktsadmins synliga subträd (syskondistrikt aldrig synliga), ScopedLookup ger 404-ej-403 på cross-scope/cross-tenant-miss; fail-closed (olöst tenant → 0 rader, F-AT-03); expansionsriktning enligt docs/domain/publication-scopes.md; riktig org_nodes-hierarki ägs av F1-9 (ScopeTree-seam)
  • F02.04.05 Shipped

    Delegation of authority (federation admin delegates to assistant)

    ✅ PL-F0204b (DelegationOfAuthority-modell med scope_type tenant/district/club, POST /hierarchy-access/delegations skapa, GET lista/hämta, POST .../revoke med audit-log, duplicate-guard 409, tid-begränsade eller permanenta delegationer)
  • F02.04.06 Shipped

    Club OrgNode management within national tenant

    ✅ PL-F0204b (GET /hierarchy-access/manageable-clubs scopad lista baserat på roll/delegation, GET /manageable-districts, POST /check-club-access med granting_scope-svar, federation-admin→alla, distriktsadmin→subtree, klubbordförande→egen klubb)
  • F02.04.07 Shipped

    Role assignment scoped to OrgNode (club president only manages their club)

    ✅ PL-F0204b (GET /hierarchy-access/scopes/{user_id} aggregerar rolltilldelningar + delegationer, POST /check-org-node-access scope-enforcement, assert_can_manage_club i service-layer)
  • F02.04.08 Shipped

    Role assignment scoped to district OrgNode (district admin)

    ✅ PL-F0204b (POST /hierarchy-access/check-district-access med subtree-traversal, assert_can_manage_district verifierar direkt scope, parent-subtree eller tenant-nivå, distriktsadmin ser alla underliggande distrikt + klubbar)

Privacy & Consent

F02.05
Shipped
How it works
  • F02.05.01 Shipped

    GDPR data subject rights — GET /me/data-export, POST /me/data-deletion-request with anonymisation (PL-208, PL-F0205a). 🔁 Go-backend 525ece23.m-4 (D-USERS m-4 t-1): rena GET /v1/me/data-export — portabelt aggregat av den data D-USERS äger (identitet med maskerad e-post, profiler i alla tenants + privacy, foto-metadata, check_in_record utan token, samtycke=unavailable tills D-GDPR finns); ren /me-ägar-väg, inga secrets (api/internal/http/dusers_gdpr.go, specs/api/endpoints/me-data-export.md). POST /v1/me/data-deletion-request + /v1/me/consents graderas till m-4b (gated på D-GDPR:s erasure-request_type resp. samtyckessubstrat, §5d.1). app-UI 525ece23.m-4 t-2: Inställningar → Integritet & data → "Ladda ner min data" konsumerar GET /v1/me/data-export (app/app/integritet-och-data.tsx, specs/app/views/integritet-och-data.md); samtycke/radering listas som "kommande", wiras ej. 🔁 D-GDPR m-2 (742fe3db.m-2 t-2 + t-3): den FORMELLA DSAR-exporten (Art. 15/20) ligger BREDVID D-USERS snabb-JSON och ersätter den inte — POST /v1/me/data-export (202 ny · 200 idempotent replay) → F1-7-jobbet gdpr_data_export → signerat zip-arkiv (manifest + HMAC-signatur + README + tio kategorier som .json OCH .csv) bakom en engångstoken som går ut efter 7 dygn; fail-hard (ett fel i någon aggregator ⇒ failed, inget arkiv, ingen token — aldrig en delleverans). app-UI t-3: sektionen FULL EXPORT med skärm-kontraktets tre tillstånd (kö · klart med "länken går ut om N dagar" + *Ladda ner signerat arkiv* · fel-kortet som säger att vi inte levererar en ofullständig fil). 🔁 D-GDPR m-3 (742fe3db.m-3): radering (Art. 17) är byggd — migration 00128 (data_deletion_request, data_retention_policy, erasure_legal_hold), det normativa RetentionGuard/OnErasure-kontraktet (api/internal/retention, specs/api/endpoints/retention-guard.md) och sju domänmoduler i api/erasure_registry.go. Rutter: POST /v1/me/data-deletion-request (self, 409 deletion_request_in_progress som databasinvariant), GET /v1/me/data-deletion-requests, POST /v1/me/data-deletion-request/{id}/cancel (409 deletion_request_not_cancellable) samt förbundets läsvyer GET /v1/data-deletion-requests[/{id}[/proof]] (capability gdpr:erasure.manage). Beslutet ägs av F1-6 (approve/reject/withdraw på de generiska rutterna) — aldrig auto-beviljat, och SelfDecisionGuard gör att den sökande aldrig kan besluta sin egen radering; m-3 bygger medvetet ingen egen /process-FSM. 30 dygns ångerfrist räknad från begäran; jobben gdpr_data_erasure + gdpr_erasure_maintenance kör anonymiseringen i EN transaktion och skriver ett signerat deletion_proof vars fullständighet prövas mot täckningsmanifestet (annars proof_incomplete:<tabell>). app-UI t-3: sektionen Radering i Integritet & data (persistent "Raderas om N dagar"-banner + *Avbryt raderingen*, formulär med förbundsval/skäl/bekräftelse, listorna DETTA RADERAS / DETTA BEHÅLLS). admin-UI t-3: GDPR-konsolens flik Raderingar ([raderad] för slutförda, bevis-nedladdning, "Öppna ärendet →" till F1-6). Bevisat mot körande stack av tools/gate/dgdpr-m3-app-e2e.mjs + tools/gate/dgdpr-m3-admin-e2e.mjs.

    ✅ PL-F0205a
  • F02.05.02 Shipped

    Data retention policies — configurable per tenant via DataRetentionPolicy model (PL-208, PL-F0205a)

    ✅ PL-F0205a
  • F02.05.03 Shipped

    Consent management — ConsentRecord model with marketing, photo_publication, data_sharing, analytics types (PL-208, PL-F0205a)

    ✅ PL-F0205a
  • F02.05.04 Shipped

    Minor/youth data protection — ParentalConsent model with guardian verification, blocks consent for minors without parental approval (PL-208, PL-F0205a)

    ✅ PL-F0205a | 🔁 Go-backend 742fe3db.m-1 (D-GDPR m-1 t-1): art. 8-grinden läser tenantens gdpr_tenant_config.consent_age (GDPR art. 8(1): 13–16 per land — SE 13, FR 15, DE/NL 16 — default 16 = EU-takvärdet), aldrig hårdkodat 18; den avtalsrättsliga 18-årsmyndigheten (medlemskap/licens) ägs av D-MEMBER och blandas aldrig in. Ett barn under tröskeln kräver parental_consent med status='verified' OCH scopet i scope_keys — pending räcker aldrig (403 parental_consent_required). Grinden går inte att självbetjäna: en POST /v1/me/parental-consent från den minderåriga själv som breddar scope_keys på en redan verifierad rad (eller byter målsman/metod) återställer raden till pending och nollar verified_at — inget nytt scope öppnas förrän en consent:manage-aktör verifierat på nytt (parental_consent.reopened i audit). Verifieringsvägen: POST /v1/parental-consents/{id}/verify (capability consent:manage); konfigytan GET/PUT /v1/gdpr/config (capability gdpr:manage, 422 consent_age_out_of_range utanför 13–16). Se specs/api/endpoints/gdpr-config.md. app-UI 742fe3db.m-1 t-2: guardian-grinden i Integritet & data — ett 403 parental_consent_required möts av ett varsamt, klarspråkligt kort (aldrig en rå felkod) som skickar målsmans-godkännandet som pending och säger uttryckligen att det gäller först när förbundet verifierat det (app/components/Samtycken.tsx, specs/app/views/integritet-och-data.md). admin-UI 742fe3db.m-1 t-2: /gdpr fliken Samtyckesålder — 13–16 med optimistisk låsning, referenstabell per nation och callouten "Två trösklar — blandas aldrig" (specs/admin/views/gdpr.md).
  • F02.05.05 Shipped

    Privacy settings per user — PrivacySettings embedded in PlayerProfile (profile visibility, results, ranking, club, DOB) — GET/PUT /player-profiles/{id}/privacy-settings subresource, owner-only update, hidden profiles return 404 to non-owners, public search respects visibility

    ✅ PL-F0205b | 🔁 Go-backend: player_profile.privacy (jsonb: profile_public/results_public/ranking_public/club_public/dob_public) ägs av D-USERS (GET/PUT /v1/player-profiles/{id}/privacy-settings, ägar-yta, dold profil ⇒ 404); D-GDPR m-2 t-1 läser blocket in i aggregatet GET /v1/me/privacy-overview men äger det inte.
  • F02.05.06 Shipped

    Audit log of all data access — DataAccessAuditLog append-only model, record_access() best-effort service, GET /data-access-audit admin listing, GET /data-access-audit/me GDPR Art. 15 right-of-access, audit rows on profile reads/lists/exports

    ✅ PL-F0205b | 🔁 Go-backend 742fe3db.m-2 (D-GDPR m-2 t-1): ren tabell data_access_audit (migration 00127) — append-only via DB-trigger (UPDATE/DELETE RAISE:ar) och hash-kedjad per tenant (chain_seq + chain_hash = sha256(prev ‖ row_digest), serialiserad med pg_advisory_xact_lock, unique (tenant_id, chain_seq)); target_identity_id bär MEDVETET ingen FK så raden överlever art. 17-raderingen i m-3. Instrumenteringen är ett deklarativt registry (rutt → action/target_type/syfte): profilläsningar av någon annan än ägaren (player/official/coach/administrator), GET /v1/memberships (en rad per registrerad på sidan, i EN sats) och player-search (en rad per träff, aldrig söksträngen i reason); ägarens egen läsning skriver noll rader. Fail-closed (NFR-8): en läsning som inte kunde loggas levereras inte — problem+json utan ett fält ur profilen (bevisas med sömmen PL_GDPR_ACCESS_LOG_FAIL=1, inert utanför local/test). Den registrerades ytor: GET /v1/me/privacy/access (art. 15, keyset-paginerad, aktörsprojektion med namn/rolletikett + masked-krok för D-SYS/D-SAFE-källor som inte fejkas), GET /v1/me/privacy-overview (ETT anrop för hela skärmen) och POST /v1/me/privacy/flag-access (data_access_flag open/high + gdpr.access_flag.created; främmande och okänd id ⇒ samma 422 access_flag_invalid_reference). Privacy-officer-vyn GET /v1/data-access-audit (+ /flags) bakom nya capabilityn gdpr:access_audit.read bär ip_address/user_agent/route/chain_seq — aldrig innehållet i det som lästes. Kedjan verifieras med api gdpr-verify-access-chain <tenant_id> (exit 0 hel / 1 bruten, pekar ut exakt chain_seq). UI 742fe3db.m-2 t-3: app visar åtkomstraderna tidsordnat i aktivitetsloggen (merge av /v1/me/activity-timeline + /v1/me/privacy/access) med ikon och text och en *Flagga*-åtgärd som kvitterar varsamt utan att lova utfall; admin/gdpr har fliken Åtkomstlogg (tabell med aktör/åtgärd/rutt/IP/chain_seq, filter-chips, datumintervall, "endast flaggade", keyset-paginering och flagg-panelen) bakom gdpr:access_audit.read — en roll utan capabilityn möts av ett ärligt behörighetsbesked, aldrig av en tom tabell. Bevisat mot körande stack av tools/gate/dgdpr-m2-app-e2e.mjs + tools/gate/dgdpr-m2-admin-e2e.mjs. Se specs/api/endpoints/privacy-settings.md + docs/product/app/privacy-dashboard.md + specs/admin/views/gdpr.md.
  • F02.05.07 Shipped

    Cookie consent and tracking preferences — CookieConsentRecord model with 4 canonical categories, GET /cookie-consent/categories, POST /cookie-consent upsert, GET /cookie-consent/me, GET /cookie-consent/anonymous/{id}, PATCH /cookie-consent/{id}, POST /cookie-consent/{id}/withdraw, dual subject identification authenticated+anonymous, GET /me/privacy-overview combined snapshot

    ✅ PL-F0205b
  • F02.05.08 Shipped

    Support-access consent — user-side portal at /me/support/access-grants for consent over sys-operator impersonation. Approve / deny / revoke grants that reference a SysSupportTicket; every transition is audited. Full contract in docs/engineering/security/support-access-consent.md; operator-side in F21.02a.

    ✅ PL-T141
  • F02.05.09 Shipped

    User support portal — /me/support landing aggregate in both app/ (mobile-first) and admin/ (desktop-first) with three columns (pending requests, open tickets, recent history). Surfaces consent decisions inline without navigating away.

    ✅ PL-T143
  • F02.05.10 Shipped

    Access-history drill-in — GET /me/support/access-grants/{id}/activity-summary aggregates sys-audit rows by impersonation_id so the target user can see exactly how many reads / writes the operator performed and when.

    ✅ PL-T143
  • F02.05.11 Shipped

    Real-time pending-request notifier — SSE stream GET /me/support/access-grants/stream pushes new grant requests to the in-app banner; 2-second polling loop in the service, heartbeat every 15 s, 10-minute client-reconnect cap.

    ✅ PL-T143
  • F02.05.12 Shipped

    User-scoped support tickets — GET/POST /me/support/tickets, GET /me/support/tickets/{id}, POST …/reply, POST …/close. Reporter-only; operator-internal notes hidden from the thread; reuses the T130 intake_ticket service.

    ✅ PL-T143
  • F02.05.13 Shipped

    Per-channel notification preferences — GET/PUT /me/support/notifications/preferences toggles email / push / in-app. Break-glass transitions bypass opt-outs by compliance rule.

    ✅ PL-T143
  • F02.05.14 Shipped

    User activity log (self-view) — GET /me/activity-timeline?user_email=…&kinds=… returns the user-facing subset of the sys-timeline (PL-T145). Login, billing, notification, support-grant, incident and legal events flow through unchanged; sys_action rows are redacted to "Support agent viewed your account" and operator identities may be masked per tenant impersonation_actor_visibility. Surfaces: app/app/settings/activity-log.tsx (mobile) + admin/app/(dashboard)/privacy/activity-log.tsx (federation admins). Read-only; scoped automatically to the caller by email. 🔁 Go-backend 525ece23.m-4 (D-USERS m-4 t-1): rena GET /v1/me/activity-timeline — självvänd projektion av audit_log (F1-5, ingen ny tabell), självet som aktör∨subjekt i aktiv tenant (ur claim, aldrig body), användarvänd action-allowlist (auth./login./session./consent./parental_consent./profile./checkin./support./impersonation.), keyset/ListEnvelope, maskering bevarad verbatim, stödåtkomst-grants med aktör+tid (api/internal/http/dusers_gdpr.go, specs/api/endpoints/me-activity-timeline.md). app-UI 525ece23.m-4 t-2: Inställningar → Integritet & data visar aktivitetsloggen (ikon+text per typ, "Visa fler"-paginering, stödåtkomst med vem+när) via GET /v1/me/activity-timeline (app/app/integritet-och-data.tsx, specs/app/views/integritet-och-data.md).

    ✅ PL-T145
  • F02.05.15 Shipped

    Granular consent scope versioning — ConsentScopeDefinition per (tenant_id, scope_key, version) with monotonic version-bump and auto-superseding. Categories: marketing / analytics / operational / subprocessor / photo / research. Existing user consents stay attributable to the exact text the subject saw — no auto-revoke on republish. Endpoints: GET/POST /dpo/consent-scopes, GET /dpo/consent-scopes/{key}/versions, POST /dpo/consent-scopes/{key}/retire. Audit-log category gdpr_consent_versioning.

    ✅ PL-T221 | 🔁 Go-backend 742fe3db.m-1 (D-GDPR m-1 t-1): rena tabeller consent_scope_definition/consent_record/parental_consent/gdpr_tenant_config (migration 00126), atomisk publish v(n+1) + superseded_by_version i SAMMA transaktion, ingen auto-revoke (beslutet mot v(n) består och flaggas stale för re-consent), idempotent retire, hård invariant "tvingande grund (legal_obligation/public_interest) måste vara revocable=false" (422). Rutter: GET/POST /v1/dpo/consent-scopes, POST /v1/dpo/consent-scopes/{scope_key}/retire (capability consent:manage) och den registrerades yta GET /v1/me/consents, POST /v1/me/consents/{scope_key}, POST /v1/me/consents/{scope_key}/revoke, POST /v1/me/parental-consent (self). En audit_log-rad per mutation i samma tx (consent.granted/consent.revoked/consent.scope.published/consent.scope.retired). Se specs/api/endpoints/consent.md. app-UI 742fe3db.m-1 t-2: Samtycken-sektionen i Integritet & data — kort per aktuellt ändamål (kategori, syftestext ur purpose_text_i18n, rättslig-grund-chip, status-pill i ord), re-consent-stepper när beslutet är stale (steg 1 visar den NYA texten, steg 2 tar beslutet) och låstext utan knappar för revocable=false; förbundsväljare eftersom samtycken är per tenant (app/lib/samtycken.tsx). admin-UI 742fe3db.m-1 t-2: /gdpr fliken Samtyckes-scopes — kategorifilter, versionshistorik, publicera-ny-version med invarianten "tvingande grund ⇒ ej återkallbar" speglad i formuläret, retire. Bevisat mot körande stack av tools/gate/dgdpr-m1-app-e2e.mjs + tools/gate/dgdpr-m1-admin-e2e.mjs.
  • F02.05.16 Shipped

    DPO-on-behalf DSAR — DpoAccessRequest model with status FSM (draft → submitted → in_review → fulfilled / rejected / cancelled). DPO files data_export / rectification / erasure / restriction / portability with mandatory legal_ground. Inline append-only audit_trail mirrored to global AuditLog (category=gdpr_dpo). Endpoints: GET/POST /dpo/access-requests, GET /dpo/access-requests/{id}, POST .../submit, POST .../cancel. Cancel blocked once fulfilment_started_at is set.

    ✅ PL-T221
  • F02.05.17 Shipped

    DSAR-job proxy for DPO portal — GET /dpo/data-export-jobs lists GdprPortabilityExport jobs initiated through the DPO console; GET /dpo/data-export-jobs/{id} returns status + signed download URL + expires_at. Reuses F16-DSAR pipeline — PL-T221 only initiates and links.

    ✅ PL-T221
  • F02.05.18 Shipped

    Cross-tenant DPO overview — POST /dpo/cross-tenant-overview for international DPOs (CEP/FIPJP) holding gdpr:dpo:read in 1–20 tenants. Returns per-tenant {tenant_id, open_requests, active_scopes}. In-memory parallel fan-out — no automatic cross-tenant DB join. Operator's own tenant always allowed; system-scope tokens get full body.tenants.

    ✅ PL-T221
  • F02.05.19 Shipped

    DPO capability gates — new capabilities gdpr:dpo:read, gdpr:dpo:write. Capability-deny logged to AuditLog with action=capability.deny so federation security teams can chase unauthorised attempts. System-scope tokens implicitly hold all capabilities.

    ✅ PL-T221
  • F02.05.20 Shipped

    DPO admin console — /(dashboard)/dpo (overview), /dpo/consent-scopes (publish + version history + retire), /dpo/access-requests (kanban with 6 columns + detail with shared AuditTrail component), /dpo/cross-tenant (per-tenant tile with setActiveTenant deep-link). Hidden for affiliate / venue / individual tenant types.

    ✅ PL-T221

Federated Login (OAuth Gateway)

F02.06
Shipped
How it works
  • F02.06.01 Shipped

    OAuth gateway — single redirect URI per provider, per-app routing via {app} path parameter, PKCE S256, one-shot state with SHA-256 binding hash, web (cookie) and native (exchange code) flows

    ✅ PL-1801 (infra), ✅ PL-F0206a (Apple provider + account linking)
  • F02.06.02 Shipped

    Sign in with Microsoft (Entra ID + personal MS account) — multitenant common endpoint, id_token JWKS-verifiering, email priority chain (Graph mail → id_token email → preferred_username → UPN), personal tenant → email_verified=False

    ✅ PL-1802, ✅ PL-F0206a
  • F02.06.03 Shipped

    Sign in with Google — id_token JWKS-verifiering mot accounts.google.com, sub som provider_user_id, email_verified trustas från id_token claim

    ✅ PL-1803, ✅ PL-F0206a
  • F02.06.04 Shipped

    Sign in with GitHub — read:user user:email scope, access token-validering via GET /user, X-OAuth-Scopes-enforcement, GET /user/emails primary+verified-policy, str(user["id"]) som provider_user_id

    ✅ PL-1804, ✅ PL-F0206a
  • F02.06.05 Shipped

    Apple Sign In för iOS — ES256 JWT client_secret (team_id+key_id+private_key), id_token-verifiering mot Apple JWKS, POST /auth/oauth/apple/native för ASAuthorizationController-flödet, email_verified=True (Apple-policy), private relay email-stöd

    ✅ PL-F0206a
  • F02.06.06 Shipped

    Account linking — flera identiteter per user, POST /auth/oauth/{provider}/link/{app} med 5-min fresh-auth-fönster (auth_time claim), GET /auth/me session-introspection, GET /auth/me/oauth-identities (safe fields only), DELETE /auth/me/oauth-identities/{provider} med last_auth_method-policy och upstream token-revokation

    ✅ PL-1806, ✅ PL-F0206a, ✅ Go-backend 4848185c.m-4 (F1-3 m-4, t-2): ren *inloggad self-service* — GET /auth/me/identities (säkra fält, IDOR-säker), POST /auth/oidc/{provider}/link/{application} (länk-läge ovanpå m-2:s OIDC-callback, fresh-auth, PKCE-S256 + binding-cookie, ingen ny session mintas), DELETE /auth/me/identities/{provider}; ?error=/problem+json-felkoder (reauth_required/link_conflict/email_unverified_conflict/last_auth_method); audit + anti-abuse (api/internal/http/auth_connected.go)
  • F02.06.07 Shipped

    Säker avlänkning med last-auth-method-skydd — DELETE /auth/me/oauth-identities/{provider} räknar email + phone + OAuth-identiteter, blockerar med 409 last_auth_method om borttagning skulle lämna noll metoder, upstream token-revokation (Google/GitHub/Apple)

    ✅ PL-F0206b, ✅ Go-backend 4848185c.m-4 (F1-3 m-4, t-2): DELETE /auth/me/identities/{provider} med atomär last-auth-method-räkning i WithTx (icke-revokerade OIDC-länkar + 1 om verifierad primary_email; andrafaktorer räknas ej) → 409 last_auth_method, plus best-effort upstream-revokations-hook (PL_OIDC_<NAME>_REVOCATION_URL, nere endpoint failar aldrig avlänk, A5) + upstream_revocation_attempted-audit
  • F02.06.08 Shipped

    Connected accounts UI i app och admin — admin/app/(dashboard)/connected-accounts.tsx (desktop-first, slate/indigo), app/app/settings/connected-accounts.tsx (mobile-first, i18n), fresh-auth-preflight, unlink-bekräftelse, provider-discovery via GET /auth/oauth/providers

    ✅ PL-F0206b
  • F02.06.09 Shipped

    Fresh re-auth-krav (≤5 min) vid linking — is_auth_time_fresh() med FRESH_AUTH_WINDOW_SECONDS=300, auth_time-claim i JWT, POST /link/{app} returnerar 401 reauth_required vid stale session, GET /auth/me exponerar auth_time_fresh-flagga

    ✅ PL-F0206b
  • F02.06.10 Shipped

    HTTP-only cookie session på .petanque.life — pl_access_token/pl_refresh_token med Secure; HttpOnly; SameSite=Lax; Domain=.petanque.life; Path=/, pl_oauth_binding-cookie med SHA-256-hash binding, cookie-cleanup efter callback

    ✅ PL-F0206b
  • F02.06.11 Shipped

    Native exchange code-flöde för mobil — OAuthExchangeCode med 64-byte base64url, 60s TTL, POST /auth/oauth/exchange med find_one_and_delete (single-use), POST /auth/oauth/apple/native för iOS ASAuthorizationController, tokens aldrig i URL

    ✅ PL-F0206b
  • F02.06.12 Shipped

    Per-provider email verification policy — compute_email_verified() med Google (trust claim), Microsoft (work=trust, personal=false), GitHub (primary+verified from /user/emails), Apple (alltid true), konservativ default false

    ✅ PL-F0206b
  • F02.06.13 Shipped

    Per-tenant OAuth-provider-overrides — TenantConfig.oauth_providers: list[OAuthProviderConfig] (provider+enabled+applications-tuple), ?application=app

    admin|web-query på GET /auth/oauth/providers filtrerar listan mot aktiv tenant så en federation kan stänga av specifika providers per surface utan globalt avhopp | ✅ PL-T206
  • F02.06.14 Shipped

    Sign in with Apple på web — Service ID + private-key + JWKS-verifiering återanvänds från native-flödet; /.well-known/apple-developer-domain-association(.txt) serveras från Settings.APPLE_DOMAIN_ASSOCIATION så ägaren kan rotera filen utan deploy

    ✅ PL-T206
  • F02.06.15 Shipped

    OAuth-knappar i tenant-CMS-login (/account/login) — web/src/app/account/login/page.tsx + AccountLoginClient.tsx använder shared fetchOAuthProviders med application=web, renderar PROVIDER_META-färgade knappar inom tenantens BookingThemeStyle-brand, hanterar ?error= via auth.error.<code>-i18n med fallback till auth.error.generic

    ✅ PL-T206
  • F02.06.16 Shipped

    Apple på admin-konsolen — apple i ADMIN_PROVIDER_WHITELIST + ADMIN_PROVIDER_DEFAULTS; admin/src/lib/oauth.ts.fetchProviders() skickar ?application=admin så per-tenant-overrides respekteras

    ✅ PL-T206
  • F02.06.17 Shipped

    Multi-tenant user sessions / tenant-kontextbyte — en auth_identity är cross-tenant och kan byta aktiv tenant utan re-auth: GET /auth/me returnerar tillgängliga tenants + vald tenant_id, POST /auth/tenant/select { tenant_id } mintar en tenant-scopad access-token (sätter tenant_id-claim) efter verifierat medlemskap, auth_time bevaras, kontext överlever refresh (refresh_token_family.tenant_id), web-cookie/native-body (aldrig token i URL), icke-medlem → 403 tenant_forbidden; en tenant-scopad token bär bara kontext (AuthZ är F1-4)

    ✅ Go-backend 4848185c.m-4 (F1-3 m-4, t-3): api/internal/http/tenant.go + me-utökning i auth.go; medlemskaps-läsmodellen auth_identity_tenant är en dokumenterad D-USERS/F1-9-hook (list_available_tenants/has_tenant_membership fulltestade, källan seedad nu, F1-9-tenants/D-USERS-profiler senare); audit tenant_selected/tenant_select_forbidden; ingen tenants-tabell/hierarki byggd (F1-9)

Multi-factor Authentication & Advanced Security

F02.07
Shipped

> **Go-backend `4848185c.m-3` (F1-3 m-3) — MFA-orkestrering (ny ren Go-stack):**

How it works
  • F02.07.01 Shipped

    TOTP authenticator app (Google Authenticator, 1Password, Authy) — enrollment detection via GET /auth/mfa/methods

    ✅ PL-F0207a
  • F02.07.02 Shipped

    SMS-OTP via 46elks — POST /auth/mfa/sms/challenge, POST /auth/mfa/sms/verify, SHA-256-hashad kodlagring, PII-maskering av telefonnummer, single-use med 5-försökstak

    ✅ PL-F0207a
  • F02.07.03 Shipped

    Email-OTP fallback — POST /auth/mfa/email/challenge, POST /auth/mfa/email/verify, break-glass recovery för tappad SIM/authenticator

    ✅ PL-F0207a
  • F02.07.04 Shipped

    WebAuthn / Passkeys — enrollment detection via GET /auth/mfa/methods, WebAuthnCredential-count

    ✅ PL-F0207a
  • F02.07.05 Shipped

    MFA enforcement per roll — GET /auth/mfa/enforcement med policy-besked (required/enrolled/grace_period), GET /auth/mfa/policy, PUT /auth/mfa/policy admin-upsert, per-tenant konfigurerbara required_roles/allowed_methods/grace_period_days/fresh_window_seconds

    ✅ PL-F0207a
  • F02.07.06 Shipped

    Recovery codes — 10 engångskoder per användare, POST /auth/mfa/recovery-codes/generate, GET /auth/mfa/recovery-codes/count, POST /auth/mfa/recovery-codes/verify med token-upgrade, SHA-256-hashad lagring, regenerering invaliderar gamla koder

    ✅ PL-F0207b
  • F02.07.07 Shipped

    Trusted devices med 30-dagars skip — POST /auth/mfa/trusted-devices registrering efter MFA, GET /auth/mfa/trusted-devices/check fingerprint-kontroll, GET /auth/mfa/trusted-devices lista, DELETE /auth/mfa/trusted-devices/{id} revokering, SHA-256-hashade fingerprints

    ✅ PL-F0207b
  • F02.07.08 Shipped

    Session management — GET /auth/sessions lista aktiva sessioner via RefreshTokenFamily, DELETE /auth/sessions/{id} revokera enskild session, POST /auth/sessions/revoke-all revokera alla utom aktuell

    ✅ PL-F0207b
  • F02.07.09 Shipped

    Brute-force-skydd med exponentiell backoff — BruteForceRecord-modell, 1s→2s→4s→8s→16s→30s (cap) delay per konsekutivt misslyckande, automatisk reset vid lyckad inloggning, applicerat på recovery-code-verify

    ✅ PL-F0207b
  • F02.07.10 Shipped

    Säkerhetsmail vid nya inloggningar och MFA-ändringar — SecurityEvent-modell med append-only audit, SecurityNotificationService dispatchar email vid new_login, mfa_enrolled, mfa_disabled, recovery_code_used, trusted_device_added/removed, session_revoked, GET /auth/security-events lista

    ✅ PL-F0207b
  • F02.08.01 Shipped

    individual_context — IndividualContext datamodell med user_id (unikt), display_name, country, language, birth_year, preferred_position (pointer/shooter/middle), handicap_hint, privacy_public_profile, subscription_tier (FREE/PREMIUM), premium_valid_until, Stripe-fält; 30-sekunders signup via POST /public/signup/individual; profil via GET/PATCH /me/individual-context

    ✅ PL-T005
  • F02.08.02 Shipped

    casual_match_p2p — IndividualCasualMatch venue-lös match mellan två individual-kontext; POST /me/casual-matches med FREE-tier-gating (50/månad), POST /me/casual-matches/{id}/verify motpartsverifiering, GET /me/casual-matches historik

    ✅ PL-T005
  • F02.08.03 Shipped

    friend_graph — IndividualFriend med status PENDING/ACCEPTED/BLOCKED; POST /me/friends/request, POST /me/friends/{id}/accept, POST /me/friends/{id}/block, GET /me/friends filtrering

    ✅ PL-T005
  • F02.08.04 Shipped

    subscription_tier_gate — individual_tier.py service med is_premium_active() (inkl. grace-period), check_casual_match_limit() (50/månad FREE), require_premium(), 402 Payment Required med error_code="premium_required"; upgrade-to-premium dev-stub, downgrade med bevarad premium_valid_until

    ✅ PL-T005
  • F02.09.01 Shipped

    chain_tenant — TenantType.CHAIN med ChainMembership (chain↔venue-länk), super-admin-onboarding via POST /admin/tenants/chain, venue-länkning via POST /chain/{id}/add-venue + POST /venue/{id}/accept-chain/{id}, bryt-länk via DELETE /chain/{id}/venues/{id} (soft-deactivate)

    ✅ PL-T016
  • F02.09.02 Shipped

    cross_venue_league — ChainLeague + ChainLeagueEntry; POST /chain/{id}/leagues med venue-multiselect, GET /chain/{id}/leagues/{id}/leaderboard aggregerat cross-venue-rankning, season_start/end-validering

    ✅ PL-T016
  • F02.09.03 Shipped

    chain_passport — ChainPassport loyalty-tracker med BRONZE (5+)/SILVER (20+)/GOLD (50+) tier-progression baserat på rolling visits; GET /chain/{id}/passport/{user_id}, POST /chain/{id}/passport/{user_id}/visit QR-check-in

    ✅ PL-T016
  • F02.09.04 Shipped

    corporate_event_module — CorporateEvent med INQUIRY→QUOTED→BOOKED→COMPLETED→CANCELLED pipeline; POST/GET/PATCH /chain/{id}/corporate-events, host_venue_tenant_id validerad mot aktiv ChainMembership

    ✅ PL-T016
  • F02.09.05 Shipped

    white_label_app — ChainWhiteLabelConfig med app_name, colors, logos, custom_domain, hide_petanque_life_branding; PUT/GET /chain/{id}/white-label, ChainBrandingProvider i app för dynamisk re-branding

    ✅ PL-T016
  • F02.09.06 Shipped

    multi_currency_chain_invoice — ChainInvoiceBatch med per-country ChainInvoiceLineItem (local_currency, ecb_rate_used, ecb_rate_date); kvartalsvis generering via tools/generate_chain_invoice_batch.py med ECB-fixing

    ✅ PL-T016
  • F02.09.07 Shipped

    chain_passport_v2 — ChainPassport19 med points-baserad tier-progression (REGULAR/GOLD/PLATINUM), passport_id format {CHAIN_CODE}-PASS-NNNNNN, unique index (tenant_id, auth_identity_id), ChainVenueVisit per-visit records; POST /chain/passports (create, 409 duplicate), GET /chain/passports/me, GET /chain/passports/{id}, PUT /chain/passports/{id} (profile + tier override + ban)

    ✅ PL-T019
  • F02.09.08 Shipped

    chain_checkin_points — POST /chain/passports/{id}/check-in (QR-scan, creates visit, increments total_games, 403 if banned), POST /chain/passports/{id}/award-points (manual points by venue staff, creates visit with points_earned, updates total_points)

    ✅ PL-T019
  • F02.09.09 Shipped

    chain_leaderboard — GET /chain/leaderboard med ?venue_id= (venue-filtrerat via aggregation), ?period=month

    year|alltime (tidsfiltrerat), top-50 paginering; alltime query direkt på total_points, period-baserat via an aggregation query på ChainVenueVisit | ✅ PL-T019
  • F02.09.10 Shipped

    chain_visit_history — GET /chain/passports/{id}/visits paginerad venue-besökshistorik sorterad senast-först; composite index (chain_passport_id, visited_at DESC) för performance

    ✅ PL-T019
  • F02.09.11 Shipped

    chain_passport_admin_ui — PassportSearch admin-vy med passport-ID-sökning, detail panel (namn, tier badge, status badge, stats, visit timeline), tier override (confirmation dialog), ban/unban actions; LeaderboardView med period tabs (All Time/This Year/This Month), venue filter dropdown, auto-refresh 60s, rank badges (#1 gold, #2 silver, #3 bronze)

    ✅ PL-T019
  • F02.10.01 Shipped

    tenant_tier_matrix — TENANT_TIER_MATRIX i services/tenant_tier.py mappar varje TenantType till included_domains + excluded_subsystems + all_access-flagga; FEDERATION/CONTINENTAL/FIPJP/SANDBOX = all_access, CLUB/AFFILIATE/VENUE/CHAIN/EVENT/INDIVIDUAL = matrix-baserat; byte-equivalent med www/src/data/tenant-tiers.ts (verifieras av services/tenant_tier_matrix_sync.py + test_pl225_tier_matrix_sync.py)

    ✅ PL-T225
  • F02.10.02 Shipped

    requires_subsystem — API-dependency i security/tenant_tier.py som gatear endpoints på subsystem-nivå (F<NN>.<MM>-format); andra gating-lagret efter capability-RBAC; 401 unauthenticated_tenant_context när tenant saknas, 403 tenant_type_not_eligible med subsystem + tenant_type i body när matrix/addon ej täcker

    ✅ PL-T225
  • F02.10.03 Shipped

    tenant_tier_resolver — TenantTierResolver med Redis 5-min cache (tenant_tier:{tenant_id}-nycklar), graceful fallback till per-request uppslag i datalagret när Redis saknas; is_subsystem_allowed() kollar all_access → matrix → aktiva addons; invalidate_cache(tenant_id) anropas av alla mutationer (grant/revoke/change_type)

    ✅ PL-T225
  • F02.10.04 Shipped

    tenant_tier_addon — TenantTierAddon datamodell med unique index (tenant_id, subsystem_id), optional expires_at (null = permanent), billing_reference, required reason (3–500 chars), granted_at/granted_by-audit; addons låser upp enskilda subsystems utan att flippa tenant_type

    ✅ PL-T225
  • F02.10.05 Shipped

    sys_tenant_tier_admin — routes/sys_tenant_tier.py med GET /sys/tenants/{id}/tier (read, support/finance/security), POST /sys/tenants/{id}/tier/addons (grant, finance/security), DELETE /sys/tenants/{id}/tier/addons/{addon_id} (revoke, finance/security), PATCH /sys/tenants/{id}/tier/type (security only, fresh-auth-required); alla emitterar tenant.tier.*-platform events + AuditLog-rad

    ✅ PL-T225
  • F02.10.06 Shipped

    me_tenant_tier — GET /me/tenant-tier returnerar tenant-snapshot (tenant_type, all_access, allowed_subsystems, addons-trim, included_domains, excluded_subsystems) för app/admin client-side hide; ["*"] när all_access så frontend slipper enumerera hela katalogen; useAllowedSubsystems()-hook i admin cachar 5 min med tenant-switch-bump

    ✅ PL-T225
  • F02.10.07 Shipped

    tier_aware_admin_ui — useAllowedSubsystems() filtrerar visibleSectionsFor() (sektioner med subsystem-mapping döljs i sidebar/CommandPalette när delsystemet ej är allowed); TenantTierUpgradeModal öppnas från modul-event-bus när någon endpoint returnerar 403 tenant_type_not_eligible; modal visar subsystem-id + tenant_type + kontaktuppgifter

    ✅ PL-T225
  • F02.10.08 Shipped

    sys_tenant_tier_view — sys/app/(dashboard)/tenants/[id]/tier.tsx är operator-arbetsytan: tier-summary-card (tenant_type-chip, included domains, excluded subsystems), addons-card (active addons med revoke), grant-addon-form (subsystem_id-regex, expiry, billing_reference, reason), change-tenant-type-card (chip-väljare för 9 TenantType-värden, fresh-auth-prompt)

    ✅ PL-T225
  • F02.10.09 Shipped

    tier_jobs — jobs/tenant_tier_jobs.py med tenant-tier-addon-expiry-tick (15 min, hård-raderar TenantTierAddon där expires_at <= now, behåller permanenta) + tenant-tier-matrix-audit-tick (1 h, jämför API- och www-matrix byte-by-byte, emitterar tenant.tier.matrix_drift på diff för operator-larm)

    ✅ PL-T225
  • F02.11.01 Shipped

    admin_session_devlogin — app-lokal session/token-provider (admin.auth.token) + AuthzClient-fabrik + inline "Dev login"-affordans på /login (dead-stripped i prod-bundle via NEXT_PUBLIC_ENVIRONMENT); auth-redirect till /login när session saknas

    ✅ f07177c7.m-6.t-2
  • F02.11.02 Shipped

    admin_roller_lista — roll-lista (GET /roles) + CRUD (POST/PATCH/DELETE /roles); systemmallar (is_system) visuellt åtskilda (ikon + text) utan redigera/radera-affordans

    ✅ f07177c7.m-6.t-2
  • F02.11.03 Shipped

    admin_capability_matris — grid roll × capability grupperad per namnrymd (kollapsbar progressive disclosure), GET/PUT /roles/{id}/capabilities (REPLACE), GET /capabilities; systemmall skrivskyddad

    ✅ f07177c7.m-6.t-2
  • F02.11.04 Shipped

    admin_rolltilldelning — tilldela roll till principal i scope (GET/POST/DELETE /role-assignments); tenant-scope fullt, org_node väljs i picker ur GET /v1/org-nodes (nodnamn, ärlig fallback vid läsfel), resurs via id-fält (ingen katalog)

    ✅ f07177c7.m-6.t-2, ✅ D-HARDEN 2fd8abe9.m-1.t-5 (rå-ID-fältet borta — premium-baren punkt 6)
  • F02.11.05 Shipped

    admin_rollrequests_inbox — läs + skapa role_requests (GET/POST /role-requests), status ikon + text; godkänn/avslå disabled (F1-6 äger statusmaskinen)

    ✅ f07177c7.m-6.t-2
  • F02.11.06 Shipped

    admin_ui_gating — all nav/åtgärd gatad mot /me/capabilities (fail-closed) via delad useCapabilities/<RequireCapability>; enhetlig RFC 9457-felpresentation (messageForProblem), 404-ej-403-semantik bevarad

    ✅ f07177c7.m-6.t-2
  • F02.11.07 Shipped

    admin_login_mfa_paritet — /login bygger på den delade login-UI:n (@petanque-life/shared/auth-ui: LoginCard + OAuthProviderButton, samma komponenter som sys) med bevarade testID:n och env-gatad dev-login; /auth/complete läser ?mfa_required=1 och renderar det delade MfaStep (TOTP · passkey · återställningskod, flow_kind:"web" mot preauth-cookien) innan den vanliga OnboardingLanding-vägen tar vid, och ?error=<code> ger ett ärligt felbesked med väg tillbaka till /login. Tidigare ignorerades ?mfa_required=1 — en admin vars konto krävde andra faktor fastnade efter Google-hoppet. Bevisat mot körande stack av L4-drivrutinen tools/gate/dauthux-m1-e2e.mjs. Kontrakt: docs/engineering/auth/login-flows.md §"Web-fullbordan av inloggningen"

    ✅ da906fd6.m-1.t-2
  • F02.12.01 Shipped

    sys_session_devlogin — app-lokal session/token-provider (sys.auth.token) + AuthzClient-fabrik + inline "Dev login"-affordans på /login (dead-stripped i prod via NEXT_PUBLIC_ENVIRONMENT); auth-redirect till /login när session saknas

    ✅ f07177c7.m-6.t-3
  • F02.12.02 Shipped

    sys_console_gating — konsol-nav (/roller, /audit) gatad fail-closed via oraklet capability-check (sys.console.access); tenant-token får ingen konsol; orakel-konsistent utan 403-benägna /me/capabilities

    ✅ f07177c7.m-6.t-3
  • F02.12.03 Shipped

    sys_roll_matris — rollkatalog (GET /sys/roles) + uppslag av identitets sys-roller (GET /sys/users/{email}/roles); ikon + text, ingen tenant-branding

    ✅ f07177c7.m-6.t-3
  • F02.12.04 Shipped

    sys_roll_grant_revoke — tilldela (POST /sys/users/{email}/roles) + återkalla (DELETE …/{role}) under fyra ögon; self-grant → 400 self_grant_blocked presenterat blockerande utan radändring; roller omläses från API efter mutation

    ✅ f07177c7.m-6.t-3
  • F02.12.05 Shipped

    sys_fresh_auth_seam — 401 reauth_required → uttrycklig sys-reauth-affordans (navigerar till /login), mutationen återupptas aldrig tyst; dokumenterat seam (m-5 saknar re-auth-endpoint, F1-3/F1-10)

    ✅ f07177c7.m-6.t-3
  • F02.12.06 Shipped

    sys_audit_seam — audit-ytan (/audit) som skal + seam: tomtillstånd "data levereras av F1-5", inga audit-row-* hävdas; live audit-läsning ägs av F1-5

    ✅ f07177c7.m-6.t-3
  • F02.13.01 Shipped

    web_session_devlogin — app-lokal session/token-provider (web.auth.token) + AuthzClient-fabrik + inline "Dev login"-affordans på /login (dead-stripped i prod via NEXT_PUBLIC_ENVIRONMENT); anonym besökare felar closed utan att krascha den publika ytan

    ✅ f07177c7.m-6.t-4
  • F02.13.02 Shipped

    web_action_gating — gatad genväg "Förbundsadministration" (web-gated-action) i SiteHeader gatad mot admin.roles.manage via <RequireCapability> (fail-closed); dold för anonym/utan-capability, klick ger aldrig 403; full white-label (endast design-tokens)

    ✅ f07177c7.m-6.t-4
  • F02.13.03 Shipped

    app_session_devlogin — app-lokal session/token-provider (app.auth.token) + AuthzClient-fabrik + dev-login-affordans på /login (dead-stripped i prod via EXPO_PUBLIC_ENVIRONMENT); Metro-monorepo-resolution av @petanque-life/shared

    ✅ f07177c7.m-6.t-4
  • F02.13.04 Shipped

    app_action_gating_per_tenant — gatad åtgärd "Anmäl lag" (app-action-register-team) mot competition:register; cross-tenant <TenantSwitcher> (app-tenant-switcher/app-tenant-option-<key>) som laddar om /me/capabilities per aktiv tenant (X-Tenant-Id) — ingen capability-läcka mellan tenants (AC-14); fail-closed under omladdning

    ✅ f07177c7.m-6.t-4
  • F02.14.01 Shipped

    app_login_otp — /login enligt skärm-kontraktet design/src/pages/app/login.astro: e-post-engångskod som primär ingång (POST /auth/otp/start → kodfält → /auth/otp/verify), account-first-ruta + /signup-länk, inget lösenordsfält, dev-login-affordansen kvar men env-gatad (literal EXPO_PUBLIC_ENVIRONMENT ⇒ Metro dead-strippar den ur prod-bundlar). Fel kod ⇒ ärligt fel + fortsatt 401

    ✅ da906fd6.m-2.t-1
  • F02.14.02 Shipped

    app_login_oidc — sociala inloggningar renderade dynamiskt ur GET /auth/oidc/providers?application=app (tom lista döljer sektionen utan fel); web navigerar till /auth/oidc/{provider}/authorize/app (PKCE S256), native öppnar systemwebbläsaren med ?flow=native och löser in deep-linkens engångskod via POST /auth/exchange

    ✅ da906fd6.m-2.t-1
  • F02.14.03 Shipped

    app_auth_complete — ny route /auth/complete: cookie-probe (GET /auth/me) → startsidan, ?mfa_required=1 → MFA-steget, ?error=<kod> → ärliga svenska besked för hela kodlistan med väg tillbaka till /login (tidigare 404:ade API:ts redirect — en social inloggning i appen kunde inte fullbordas alls)

    ✅ da906fd6.m-2.t-1
  • F02.14.04 Shipped

    app_mfa_steg — MfaStepNative (RN-tvillingen till delade MfaStep): TOTP · passkey · återställningskod mot auth-ui/mfa-client med injicerad request; credentialen väljs på leverans, inte plattform (body-levererad preauth ⇒ Bearer på web *och* native, redirect-levererad ⇒ pl_preauth-cookien). Fel kod ⇒ live-region-annonserat fel och fortsatt 401

    ✅ da906fd6.m-2.t-1
  • F02.14.05 Shipped

    app_native_preauth_exchange (API, FR-5) — native redirect-flöden som stannar på MFA-gaten levererar preauth via en engångskod i deep-linken (petanquelife://auth/native-complete?mfa_required=1&preauth_code=…), inlöst på befintliga POST /auth/exchange → {mfa_required, preauth_token, methods}; aldrig råtoken i URL, aldrig session-cookies, replay = 401. Utan den fastnade ett MFA-konto i native (appen kan inte läsa pl_preauth-cookien)

    ✅ da906fd6.m-2.t-1
  • F02.14.06 Shipped

    app_session_gating — delad SessionGate monterad i app/_layout.tsx: default-deny med sluten publik lista (/, /login, /signup, /auth/complete, /resultat, /spectator, /live/*, /tavling/* samt /anmalan + /min-match per F02.14.11); sessionProbing väntar (app-gate-probing) så en cookie-inloggad spelare aldrig studsar till /login vid omladdning, anonym → /login utan att personligt innehåll når DOM:en

    ✅ da906fd6.m-2.t-1
  • F02.14.07 Shipped

    app_session_model — EN sessionsmodell för båda plattformarna: web bootstrappar cookie-sessionen via /auth/me-probe (+ kort retry, identitets-backfill), native persisterar tokenparet i expo-secure-store; tyst förnyelse (POST /auth/session/refresh) och onUnauthorized inkopplade i den delade transporten

    ✅ da906fd6.m-2.t-1
  • F02.14.08 Shipped

    app_logout — synlig utloggning (app-logout) i Inställningar: POST /auth/session/logout river refresh-familjen SERVER-side först, därefter store + säker persistens + tenant-token-cache; idempotent för användaren, landar på /login (publik ⇒ ingen loop)

    ✅ da906fd6.m-2.t-1
  • F02.14.09 Shipped

    app_login_gating_l4 — L4-röken tools/gate/dauthux-m2-e2e.mjs (descriptor 460-dauthux-m2.mjs): anonym → gatad sida → login-redirect → autentiserad → innehåll mot den KÖRANDE stacken; gating-svepet över samtliga gatade rutter + hela /me-svepet, publika ytor mot /v1/public/competitions, OTP-login med fel/rätt kod, probing utan studs, authorize-leget (S256/state/redirect_uri) mot en driver-egen sidecar, web-/native-fullbordan, MFA i BÅDA leveransvägarna, logout, 401-hantering, axe utan critical/serious och prod-exportens dead-strip (app-dev-login-*)

    ✅ da906fd6.m-2.t-2
  • F02.14.10 Shipped

    app_gate_redirect_recovery — guardens redirect överlever att rot-navigatorn rivs i SAMMA render som gaten slår till (fail-closed-mekaniken renderar platshållaren i stället för <Slot/>): router.replace körs i try/catch — obehandlat rev kastet *hela* React-trädet, dvs. den blanka sidan — med plattformens egen utväg som fallback (web window.location.replace('/login') + en 1,2 s landningskontroll; native osynlig ommontering av navigatorn). Stänger de TVÅ defekter F02.14.09 fann mot körande stack: in-app-navigering till en gatad rutt (URL:en stod still, ingen login-vy) och terminalt 401 under användning (blank sida i stället för login-vyn, spec AC-10). Underlag: …t-2.finding.md

    ✅ da906fd6.m-2.t-2
  • F02.14.11 Shipped

    app_inskarms_fail_closed — /anmalan och /min-match redirectar inte: de bär skärmens EGEN fail-closed-uppmaning (app-anmalan-login · "Logga in" i app-min-match) och bevarar därmed djuplänkens kontext (tävling + klass), vilket en /login-redirect tappar (return-to är Non-goal 6). Det bekräftar ägarbeslutet i D-COMP-PLAYER m-2 AC-9 / m-4 AC-8; spec FR-7 rättad + AC-1b tillagt. Eftersom guarden inte längre håller dessa två skärmar äger de nu SITT EGET probing-tillstånd: sessionProbing renderar skelett/Kontrollerar inloggning…, aldrig "du är utloggad" åt en inloggad spelare och aldrig personlig data — och useNextMatch stannar i loading (i stället för ready utan data) tills session + tenant finns, så ingen tyst blank vy och ingen läsning utan tenant-anspråk. Underlag: …t-2.finding.md §Fynd 5

    ✅ da906fd6.m-2.t-2
  • F02.15.01 Shipped

    sys_mfa_gate — sys-konsolåtkomst utan aktiv passkey nekas med den DISTINKTA signalen mfa_enrollment_required (oraklet: {allowed:false, reason:…}; /v1/sys/*: 403 problem+json) ur ETT delat predikat som orakel-vägen, requireSysConsole-familjen och de direkta frågeställarna alla konsulterar. Beslutet tas ur AKTUELLT DB-tillstånd vid varje avgörande ⇒ kontinuerlig enforcement, ingen MFA-claim i tokenen. Ingen grace-period, inget trusted-device-skip för sys, ingen dev-login-bakdörr.

    ✅ c5488ceb.m-1.t-1
  • F02.15.02 Shipped

    sys_mfa_last_factor — DELETE /auth/mfa/passkey/credentials/{id} av den SISTA kvalificerande passkeyn för en sys-bärande identitet avvisas med 409 last_mfa_factor_required (inget raderas); ägarskapsguarden körs FÖRST, så en främmande credential-id svarar exakt som förr (ingen 409-läcka). GET /auth/mfa/enforcement bär sys_console_gated + reason:"sys_console".

    ✅ c5488ceb.m-1.t-1
  • F02.15.03 Shipped

    sys_mfa_enrollment_ux — ett TREDJE gate-tillstånd i sys/lib/authz.tsx (mfaEnrollmentRequired, ur oraklets reason — aldrig ett allow): SessionGate renderar den spärrande vyn sys-mfa-enroll på ALLA rutter utan ShellHost/nav/data. Flöde: passkey (primär) → återställningskoder EN gång med spärrande "jag har sparat"-bekräftelse → TOTP som frivilligt komplement → re-proba oraklet, konsolen öppnas UTAN ny inloggning. Utan WebAuthn-stöd: ärligt role="alert"-besked och ingen TOTP-genväg (fail-closed). Ett 500/nätverksfel på capability-check ger den befintliga fail-closed-vägen — ett fel tolkas ALDRIG som "saknar faktor". Utloggning alltid nåbar (sys-mfa-enroll-logout).

    ✅ c5488ceb.m-1.t-2
  • F02.15.04 Shipped

    sys_mfa_faktorhantering — ny sida /sakerhet (sys-security-factors) nåbar ur SysAccountMenu i BÅDA varianterna (sys-account-security, sys-account-security-mobile): passkey-lista med label/registrerad/senast använd ur GET /auth/mfa/passkey/credentials (aldrig publik nyckel), sys-passkey-add (riktig ceremoni), sys-passkey-remove (fresh-auth; sista ⇒ API:ts eget 409-besked), sys-recovery-count + sys-recovery-regenerate (nya koder EN gång) och sys-totp-toggle (otpauth-URI + kodfält). Riktiga loading/empty/error-tillstånd; alla anrop via den delade mfa-client (inga nya råa fetch().

    ✅ c5488ceb.m-1.t-2
  • F02.15.05 Shipped

    mfa_enrollment_delad — MfaEnrollment + registreringsceremonin (registerPasskey, listPasskeys, removePasskey, recovery/TOTP-legen) ligger i packages/shared/src/auth-ui/, så admin-nivåns per-förbunds-policy monterar SAMMA flöde i stället för en kopia. Transportfel mappas till ett ärligt svenskt besked i stället för webbläsarens råa Failed to fetch.

    ✅ c5488ceb.m-1.t-2
  • F02.15.06 Shipped

    sys_mfa_l4 — L4-drivrutinen tools/gate/dauthpolicy-m1-e2e.mjs (descriptor 480-dauthpolicy-m1.mjs) kör WebAuthn-ceremonin på RIKTIGT via CDP:s virtuella authenticator mot körande stack: spärr på flera rutter med backendsignalen verifierad, felad ceremoni + runtime utan WebAuthn, utloggning i spärrat läge, fullföljd registrering ⇒ konsol med API:ts EGNA rader, faktorhantering i desktop OCH mobil chrome (inkl. 409-fallet), fail-closed vid 500/nedsläckt API, axe utan critical/serious. Övriga sys-drivrutiner (410, 430, 440, 450, 470) registrerar numera en riktig passkey i sin SETUP (tools/gate/lib/sys-passkey.mjs) — deras påståenden är oförändrade.

    ✅ c5488ceb.m-1.t-2
  • F02.16.01 Shipped

    admin_mfa_gate_ux — ett EGET gate-tillstånd i admin/lib/mfa-gate.tsx (probat ur GET /auth/mfa/enforcement, aldrig ett allow): ShellHost renderar den spärrande vyn admin-mfa-enroll på ALLA rutter utan skal/nav/data. Fyra lägen hålls isär — probing, open, gated (tenant_policy_gated), error — plus grace som ett femte, icke-blockerande. Probe-FEL ger den ärliga felvyn admin-mfa-probe-error, aldrig spärrvyn och aldrig konsolen (NFR-1). Gaten är PER TENANT: ett svar som gäller en annan (eller ännu ovald) tenant räknas som "probe pågår", kontrollerat vid render. Utloggning alltid nåbar (admin-mfa-enroll-logout) och river sessionen server-side.

    ✅ c5488ceb.m-2.t-2
  • F02.16.02 Shipped

    mfa_enrollment_policy_medveten — det DELADE MfaEnrollment (m-1 F02.15.05) utökas, aldrig forkas, med qualifyingMethods: innehåller policyns qualifying_methods totp blir autentiseringsappen en FULLT kvalificerande väg vid sidan av passkey (eget steg → samma återställningskoder EN gång → samma komplementsteg). En passkey-only-policy ger exakt sys-beteendet (utan WebAuthn: ärligt role="alert", ingen TOTP-genväg). Sys skickar ingen prop och är oförändrad.

    ✅ c5488ceb.m-2.t-2
  • F02.16.03 Shipped

    admin_sjalvbetjaning_faktorer — säkerhetssektionen på /installningar (admin-security-factors) för VARJE inloggad admin, utan capability-krav (sidans förbundsfält behåller sin gating): passkey-lista med label/registrerad/senast använd, admin-passkey-add/-remove, admin-totp-toggle (URI + kodfält), admin-recovery-count/-regenerate (nya koder EN gång). Sista KVALIFICERANDE faktorn (enligt policyns metodmängd) föregås av den ärliga varningen admin-last-factor-warning FÖRE borttagningen — API:t tillåter den för icke-sys-konton och gaten stänger då omedelbart (FR-B13); sys-bärare får API:ts eget 409 last_mfa_factor_required. Riktiga loading/empty/error-tillstånd, allt genom den delade mfa-client (inga nya råa fetch(). Mockupens "Change password"/"Session length" byggs medvetet INTE (inga lokala lösenord, plattformsstyrd sessionslängd — spec A4).

    ✅ c5488ceb.m-2.t-2
  • F02.16.04 Shipped

    admin_mfa_policyvy — ny vy /identitet-sakerhet (fliken "MFA policy") + NAV-post i ShellHost (Organisation & styrning, synlig-men-låst), gated tenant-config.manage: rollkryssrutor ur tenantens RIKTIGA rollista (GET /roles — aldrig hårdkodad), metodval (passkey/TOTP — de verkliga metoderna, spec A4), gracedagar, fresh window, trusted-device-valet, Spara ⇒ PUT /auth/mfa/tenant-policy med 422 vid rätt fält och ärlig 403, härkomst (egen rad vs ärvd default) och efterlevnadsvarningen ur compliance (ENDAST räknetal — aldrig namn). Utan capability: den ärliga behörighetsförklaringen, aldrig formulärdata. Mockupens övriga flikar ägs av andra initiativ och stubbas inte.

    ✅ c5488ceb.m-2.t-2
  • F02.16.05 Shipped

    admin_mfa_grace_banner — admin-mfa-grace-banner i skalet för en matchad, o-enrollad admin INOM grace: uppläsbar, med API:ts grace_expires_at som tidsfrist (aldrig klientberäknad), avvisbar för sessionen (admin-mfa-grace-dismiss, återkommer vid nästa inloggning) och länkad till /installningar. Visas ALDRIG för en omatchad aktör.

    ✅ c5488ceb.m-2.t-2
  • F02.16.06 Shipped

    admin_mfa_l4 — L4-drivrutinen tools/gate/dauthpolicy-m2-e2e.mjs (descriptor 490-dauthpolicy-m2.mjs) mot körande stack: spärr på flera rutter med backendsignalen (mfa_enrollment_required + reason:"tenant_policy") och orakel-konsistensen på /me/capabilities verifierade, avbruten ceremoni, BÅDA enrollment-vägarna (TOTP deterministiskt enligt RFC 6238, passkey via CDP:s virtuella authenticator), självbetjäningen i desktop OCH mobil chrome inkl. sista-faktor-varningen, policyvyn (roller nyckel-för-nyckel mot GET /roles, sparande verifierat via nytt API-läst tillstånd, efterlevnadsaggregatet, 403-vägen), grace-bannern, opåverkad omatchad aktör, fail-closed (500 på proben ⇒ varken konsol eller spärrvy; API:t nere ⇒ /login) och axe utan critical/serious på de tre nya ytorna. Policy och fixturer sätts uteslutande via API:t och nollställs före OCH efter körningen.

    ✅ c5488ceb.m-2.t-2
  • F02.16.07 Shipped

    mfa_rp_origin_lista — PL_MFA_WEBAUTHN_RP_ORIGIN accepterar kommaseparerade origins (api/internal/auth/mfa/webauthn.go). Nödvändigt eftersom admin nu enrollar passkeys från en ANNAN origin än sys, och WebAuthn verifierar origin strikt. Vidgar konfigurationen, aldrig verifieringen: varje origin matchas exakt, en olistad origin nekas, och ett enda värde beter sig precis som förut.

    ✅ c5488ceb.m-2.t-2
  • F02.17.01 Shipped

    auth_transport_malbild — beslutsdokumentet docs/migration/auth-transport.md: målbildstabell per yta (admin/sys/web + web-SSR-avvikelsen + app native/web), mekanismerna med namn och filpekare (GET /auth/me, POST /auth/session/refresh, POST /auth/session/logout, POST /auth/tenant/select, X-Tenant-Id), avvikelseregel + aktuell avvikelselista, landnings- och förbudsregler för m-2…m-4, dev-login/E2E-vägen och m-5-noteringen om de tre authHeaders-kopiorna i packages/shared. Länkat från CLAUDE.md och api-client-README:n.

    ✅ d78fbcde.m-1.t-1
  • F02.17.02 Shipped

    session_utan_localstorage_bearer — createSessionStore({ persistToken: false }) på alla webbytor (admin, sys, web, app-web-exporten): den råa bearern skrivs aldrig till Web Storage, en kvarliggande legacy-nyckel <app>.auth.token tas bort vid boot (engångsstädning, ingen läsning tillbaka in i sessionen) och hydrate() återuppväcker aldrig en session ur storage. Storen är fortsatt den synkrona in-memory-sanningen. Native (app på enhet) är oförändrat: där är backenden expo-secure-store (Keychain / EncryptedSharedPreferences), vilket ÄR den härdade modellen. XSS-exfiltrationsytan för sessionstoken är därmed stängd på webben.

    ✅ d78fbcde.m-1.t-1
  • F02.17.03 Shipped

    delad_cookie_probe — probeCookieSession(store, apiBase) i packages/shared/src/authz-ui/cookie-session.ts är den enda cookie-proben: GET /auth/me → session, 401 → EN tyst POST /auth/session/refresh → EN omprövad probe, terminalt fel ⇒ false utan att etablera session (fail-closed, kastar aldrig på ett rent 401). admin och web bootar nu genom den (defekten där en cookie-inloggad admin såg ut som anonym är stängd), sys och app har bytt sina egna kopior mot den — och fick därmed refresh-legen de saknade. sessionProbing gör att gatade ytor VÄNTAR i stället för att studsa till /login.

    ✅ d78fbcde.m-1.t-1
  • F02.17.04 Shipped

    provider_som_enda_klientplats — ApiClientProvider wiras i alla fyra rötter enligt målbilden: roten deklarerar modellen som ren data (basadress, silent-refresh-endpoint, landning vid terminalt 401) och den delade createSessionApiClientOptions bygger klientens options (token ur storen, cookie-refresh, onUnauthorized ⇒ storen töms + ytans landning). Ingen "naken" options={{ baseUrl }} kvar någonstans; antalet klientkonstruktioner ökar inte (baseline 77). useApiClient() är den dokumenterade konsumtionsvägen.

    ✅ d78fbcde.m-1.t-1
  • F02.17.05 Shipped

    tenant_injektion_i_transporten — ApiClientOptions.tenantId?: () => string \

    undefined sätter X-Tenant-Id vid anropstid på hela transportytan (request/call, requestBlob, requestMultipart, requestEventStream), inklusive den omprövade requesten efter en tyst refresh. Utan provider sätts headern inte alls (JWT-claimen är auktoritativ) — additivt och bakåtkompatibelt. Migrerade anrop i m-2…m-4 behöver aldrig handrulla headern. | ✅ d78fbcde.m-1.t-1
  • F02.17.06 Shipped

    web_tenantbindning — den inloggade besökaren på en publik förbundssajt får nu en tenant-bunden session: web/lib/authz.tsx bekräftar medlemskapet med den genererade operationen getMyTenants genom provider-klienten (useApiClient()) och binder sessionen till deploymentens förbund (NEXT_PUBLIC_TENANT_CODE) via modellens egen mekanism POST /auth/tenant/select (delade hjälparen selectCookieTenant). Utan bindningen svarar /me/capabilities 400 tenant_id_required, och en förbundsadministratör såg exakt samma publika sajt som en anonym besökare. Är identiteten inte medlem i förbundet sker ingen bindning — ytan förblir anonym (fail-closed).

    ✅ d78fbcde.m-1.t-2
  • F02.17.07 Shipped

    vakt_i_shared — tools/gate/fetch-ratchet.mjs (gate-steg L3d) täcker även packages/shared/src/ (baseline-nyckel packages/shared): ett otaggat rått fetch( i delad kod gör gaten röd, så vakten inte kan kringgås genom att lägga anropet i shared och re-exportera det.

    ✅ d78fbcde.m-1.t-2
  • F02.18.01 Shipped

    identity_email — tabellen (migration 00141) med DB-tvingade invarianter: partiellt unikt index på email där verified_at is not null (en validerad adress kan bara ägas av EN identitet), unikt (auth_identity_id, email), exakt EN primär per identitet och den villkorade primärregeln: en ovaliderad rad kan inte bli primär när identiteten har någon validerad rad (den obetingade formen is_primary ⇒ verified_at is not null vore självmotsägande mot backfillen av ovaliderade legacy-konton och mot GDPR-anonymiseringens primära, ovaliderade erased-…-rad; säkerhetsegenskapen är exakt densamma och avvisas av databasen, inte av app-lagret). Backfill: en rad per befintlig auth_identity. Ovaliderade rader är medvetet inte globalt unika (anti-squatting, spec A-2).

    ✅ 037909c7.m-1.t-1 · doc-rättelse 037909c7.m-4.t-3
  • F02.18.02 Shipped

    primary_email_spegling — auth_identity.primary_email/email_verified gjordes härledda och speglades i databasen (trigger), så repotets ~90 befintliga läsare (sys-vyer, GDPR-export, signup, OIDC, medlemsimport, seedvägarnas råa insert … on conflict) var gröna under migreringen. Speglingen är AVVECKLAD i 037909c7.m-4 (migration 00144): kolumnerna och triggern är borta ur schemat och identity_email är enda sanningen (se F02.21.01). Divergensfrågan, som mätte spegeln, är därmed ersatt av föräldralös-frågan (F02.21.02).

    ✅ 037909c7.m-1.t-1 → ⛔️ avvecklad i 037909c7.m-4.t-2
  • F02.18.03 Shipped

    identitet_ur_valfri_validerad_adress — EN delad resolver (GetIdentityByVerifiedEmail) slår upp identiteten via identity_email där verified_at is not null. OTP- och magic-link-inloggning landar därför i samma konto oavsett vilken validerad adress som används, och OIDC-inloggning med en validerad sekundäradress binds till den befintliga identiteten i stället för att skapa ett andra konto. En ovaliderad adress svarar exakt som en okänd (ingen enumeration).

    ✅ 037909c7.m-1.t-1
  • F02.18.04 Shipped

    sjalvbetjaningsrutterna — GET/POST /v1/me/email-addresses, POST …/{id}/verify, DELETE …/{id}, POST …/{id}/make-primary. Identiteten tas ur claims (IDOR-skydd, 404 före 403), inkorgen är beviset för att lägga till/validera, fresh auth krävs för att ta bort/byta primär (401 reauth_required), taken är 10 adresser / 3 väntande, anti-abuse-fönstren delas med inloggningen (email_verify räknas i samma kvot) och varje mutation går atomärt med sin auditrad (identity_email_added/verified/removed/primary_changed/conflict). Kollisionen avslöjas först efter bevisad inkorgskontroll (409 email_owned_by_other_identity) och bär ingenting om den andra identiteten. Kontrakt: specs/api/endpoints/me-email-addresses.md.

    ✅ 037909c7.m-1.t-2
  • F02.18.05 Shipped

    app_konto_epostadresser — player-appens skärm Konto → E-postadresser (/e-postadresser, ingång i Inställningar): riktiga rader ur API:t med statusmärkena Primär / Validerad / Väntar på verifiering (ikon + text, aldrig enbart färg), lägg till + kodruta som namnger den adress koden gick till, skicka om (vänlig text när kvoten slår), ta bort med bekräftelsesteg och gör till primär. Borttagning renderas inte på den primära eller den sista validerade raden — skärmen förklarar varför i stället för att neka efteråt. 401 reauth_required ⇒ vänlig omauth-prompt, aldrig en utloggning; 409 email_owned_by_other_identity ⇒ ärlig text + supportväg utan att antyda vems kontot är och utan att lova en merge som inte finns än. Transport: useApiClient() + de genererade operationerna (noll råa fetchar). Vy-spec: specs/app/views/e-postadresser.md.

    ✅ 037909c7.m-1.t-3
  • F02.18.06 Shipped

    domain_401_i_transporten — den delade transporten fick den additiva RequestInput.domainUnauthorized: ett 401 som endpointen använder för att svara om REQUESTEN (fel engångskod; reauth_required på en giltig men för gammal session) mappas rakt till det typade ApiError — ingen tyst refresh, inget omförsök (som annars bränner ett andra försök mot serverns attempts-tak) och ingen sessionsrivning. Utan flaggan är beteendet oförändrat. Utan den hade appens adresskärm loggat ut personen för ett feltryck i kodrutan.

    ✅ 037909c7.m-1.t-3
  • F02.18.07 Shipped

    l4_bevis_pa_riktig_stack — tools/gate/dprofiles-m1-app-e2e.mjs (descriptor e2e.d/700-dprofiles-m1.mjs) kör hela resan i en riktig webbläsare mot körande API + riktig Postgres: riktig OTP-inloggning → lägg till → fel kod → skicka om → validera → gör till primär → ta bort med bekräftelse → logga ut → logga in med den nya adressen → samma identitet; plus kvotens vänliga 429-ruta, kollisionen mot en ANNAN riktig identitet, fresh-auth-prompten utan utloggning, axe utan critical/serious och DB-facit (adressrader, auditrader, divergensfrågan = 0). Hela resan körs på gratisnivån — utan medlemskap, licens eller betalning. Cutover i 037909c7.m-4.t-3: DB-facit läser numera föräldralös-frågan (divergensfrågan dog med spegelkolumnerna), och kollisionssteget accepterar båda de lagliga ytformerna — app-sammanslagning-erbjudande när servern sätter merge_available (formen sedan m-3) och annars m-1:s app-epostadresser-konflikt — med m-1:s krav (supportväg, ingen enumeration, ingen rå felkod, raden förblir väntande) oförändrade.

    ✅ 037909c7.m-1.t-3 · cutover 037909c7.m-4.t-3
  • F02.19.01 Shipped

    identity_profile — tabellen (migration 00142) 1:1 mot en validerad identity_email: unikt index på identity_email_id, DB-trigger som gör en profil på en rad med verified_at is null omöjlig (även med rå SQL), och on delete cascade så ingen föräldralös profil kan existera. Tenant-lös som identity_email/auth_identity, registrerad i tenant-konventionsvakten; +goose Down verifierad (tabellerna + mekanismerna bort, identity_email/auth_identity orörda).

    ✅ 037909c7.m-2.t-1
  • F02.19.02 Shipped

    default_profil_for_varje_skrivvag — profilen skapas på DB-nivå när en adressrad blir validerad, så mekanismen täcker även spegel-triggerns rader från de råa insert into auth_identity-seedvägarna (en Go-synk hade inte gjort det). Namnet tas ur auth_identity.display_name och blir aldrig e-postadressen eller dess local-part. Backfill i samma migration: exakt en profil per redan validerad rad; identiteter utan validerad adress får noll profiler (legacy-kanten — korrekt, inte ett fel). Divergens är ett fel: FR-2:s divergensfråga ska ge noll rader efter varje seed-, migrations- och API-väg.

    ✅ 037909c7.m-2.t-1
  • F02.19.03 Shipped

    kontaktprofilen_och_resolvern — identity_contact_profile bär en designation per identitet ("vilken adress når klubb och förbund mig på"), med primäradressens profil som dynamisk default och deterministiskt, auditerat fallback när den designerade profilen försvinner. GetContactEmailForIdentity(identityID) är den enda sanktionerade vägen för framtida kommunikationsytor (D-COMM, medlemsutskick) att slå upp en persons kontaktadress. En väntande adress kan aldrig vara kontaktväg — den har ingen profil.

    ✅ 037909c7.m-2.t-1
  • F02.19.04 Shipped

    schemavakten_for_en_manniska — ett låst konventionstest enumererar de sanktionerade referenserna till identity_profile (identity_contact_profile, social_play_member, identity_profile_photo) och gör varje annan FK — dvs. varje försök att hänga domändata (ELO, licens, medlemskap) på en presentationsprofil — till ett rött tills den uttryckligen sanktioneras med skriven motivering. Vaktens negativa gren bevisar att den biter.

    ✅ 037909c7.m-2.t-1
  • F02.19.05 Shipped

    gdpr_per_identitet — raderingsmodulen auth täcker identity_profile (namn, smeknamn, avatar = personuppgifter): icke-primära adressers profiler kaskadraderas, den anonymiserade primärraden (verified_at = null) lämnar ingen profil kvar — annars hade FR-2:s divergensfråga brutit. DSAR-exporten innehåller profilerna (namn, smeknamn, synlighetsval, kontaktdesignation, avatar-referens). Radering är fortsatt per identitet — det finns inget profil- eller adress-subjekt. Erasure-coverage-vakten grön med den nya tabellen täckt.

    ✅ 037909c7.m-2.t-1
  • F02.19.06 Shipped

    sjalvbetjaningsrutterna_for_profiler — GET /v1/me/identity-profiles, PATCH …/{id} (If-Match, 412 stale_version), POST/DELETE …/{id}/avatar och POST …/{id}/make-contact. Ruttfamiljen kolliderar inte med D-USERS /v1/me/profiles (rollprofiler per förbund) — begreppsdisambigueringen är ett krav, inte en stilfråga. Identiteten tas ur claims (IDOR-skydd, 404 före 403), namn får inte vara en e-postadress (422 validation_failed), och kontraktet har varken skapa- eller raderaväg: profiler föds av validering och dör med adressen. Kontrakt: specs/api/endpoints/me-identity-profiles.md.

    ✅ 037909c7.m-2.t-2
  • F02.19.07 Shipped

    avatar_per_profil_pa_befintligt_kuvert — per-profil-avatar (identity_profile_photo) återanvänder D-USERS fotomaskineri oförändrat: magic-byte-validering (aldrig bara Content-Type), PNG/JPEG/WebP, > 5 MB ⇒ 413 photo_too_large, förfalskad typ ⇒ 415, SHA-256-ETag och content-addresserad blob-URL utan PII. Ingen ny blob-mekanism, ingen ny lagringsyta; /v1/me/photo är orörd.

    ✅ 037909c7.m-2.t-2
  • F02.19.08 Shipped

    socialspelet_presenterar_profilen — social_play_member.identity_profile_id (nullbar FK, on delete set null) fångar den agerande profilen vid join, och presentationen resolverar profil-först: coalesce(guest_name, profilens utåtvända namn enligt synlighetsval, auth_identity.display_name, <neutralt fallback>) — aldrig e-postadressen. En vänsterjoin, ingen N+1. Historik skrivs inte om av en senare växling; raderas profilen faller presentationen tillbaka till identitetens namn utan bruten vy. Befintliga rader utan bindning presenteras exakt som före m-2, och en främmande profil kan inte ageras med (404).

    ✅ 037909c7.m-2.t-2
  • F02.19.09 Shipped

    en_manniska_under_ytan_last — låsta bevis (inte påståenden) att profilväxling bara är presentation: samma sub/tenants/capabilities/medlemskapsbild under varje profil, en enda ELO/rankingbild nycklad på auth_identity_id, samma anti-dubbelanmälan (registration_active_entrant_unique) oavsett profil, spärr som följer identiteten, och regelverksytor (licenskontroll, domare/admin/sys) som fortsatt läser identitetsnivån. Reglerna är skrivna som bindande för framtida ytor i docs/engineering/architecture/02a-authentication.md.

    ✅ 037909c7.m-2.t-2
  • F02.19.10 Shipped

    app_konto_profiler — player-appens skärm Konto → Profiler (/profiler, ingång i Inställningar direkt efter E-postadresser): riktiga rader ur API:t med märkena Primär adress / Kontaktprofil / Aktiv (ikon + text, aldrig enbart färg), redigering av visningsnamn/smeknamn/synlighetsval, avatar upp/av med vänliga feltexter per felkod, kontaktvalet med förklarande text där det gäller, och verkliga ladd-/läsfel-/tomtillstånd (noll-profil-läget länkar till adressvalideringen i stället för att krascha). Terminologin disambiguerar mot nav-postens Profil (D-USERS rollprofil per förbund). Transport: useApiClient() + de genererade operationerna (noll råa fetchar). Vy-spec: specs/app/views/profiler.md.

    ✅ 037909c7.m-2.t-3
  • F02.19.11 Shipped

    snabbvaxlaren_i_kontomenyn — profilväxlaren bor i skalets kontomeny-slot som syskon till TenantSwitcher (tenantväxlaren byter förbund = rättigheter, profilväxlaren byter ansikte = presentation). Den aktiva profilen är markerad i text och form; med 0–1 profiler renderas ingen växlare alls (inget dött UI, och noll-profil-läget kan aldrig låsa skalet). Valet är en preferens per enhet — profilens id i enhetslagringen (app.identity-profile.active), aldrig en token, ingen serverlagring, inga nya claims — och överlever omladdning/appomstart. Fallbacket är deterministiskt: sparat id → primäradressens profil → första raden → identitetens egen presentation. Den aktiva profilen följer med som explicit parameter i socialspelets join; aktören kommer alltid ur claims.

    ✅ 037909c7.m-2.t-3
  • F02.19.12 Shipped

    l4_bevis_pa_riktig_stack_m2 — tools/gate/dprofiles-m2-app-e2e.mjs (descriptor e2e.d/710-dprofiles-m2.mjs) kör hela resan i en riktig webbläsare mot körande API + riktig Postgres: riktig OTP-inloggning → ingen växlare vid en profil → lägg till + validera adress ⇒ profilen dyker upp automatiskt → redigera → e-postadress som namn avvisas vänligt → sätt kontaktprofil → växla profil → omladdning ⇒ valet består → socialspelets deltagarrad visar profilnamnet (två riktiga sessioner) → väntande adress syns inte bland profilerna → logga in med den andra adressen ⇒ samma identitet → ta bort adressen ⇒ profilen kaskadraderad + deterministiskt fallback utan fel. Plus axe utan critical/serious (även med växlarmenyn öppen) och DB-facit: profil-, bindnings- och auditrader samt båda divergensfrågorna 0. Hela resan körs på gratisnivån — utan medlemskap, licens eller betalning.

    ✅ 037909c7.m-2.t-3
  • F02.20.01 Shipped

    merge_motorn_och_journalen — store.ExecuteIdentityMerge(survivor, absorbed) flyttar rollprofiler, medlemskap, licenser, ranking/ELO, anmälningar, samtycken, adresser (med sina profiler) och inloggningsvägar i EN transaktion med radlås på båda identiteterna i id-ordning, löser varje kollision deterministiskt (klassreglerna), tar spärr-unionen, revokerar B:s säkerhetsartefakter och terminalstänger B som en anonymiserad tombstone (status='merged', merged-<id>@merged.invalid). Journalen identity_merge_journal (migration 00143, tenant-lös, append-only, PII-maskerad) är den enda sanktionerade supportvägen: vem, när, vilka konton, vad som flyttades och varje kollisionsupplösning. Ett unikt index på absorbed_auth_identity_id gör att en identitet kan absorberas en gång — terminaliteten ligger i databasen.

    ✅ 037909c7.m-3.t-1
  • F02.20.02 Shipped

    merge_manifestet_och_vakten — api/db/migrations/identity-merge-manifest.json räknar upp varje identitetsreferens mot auth_identity (live-schemats FK:er + de dokumenterat FK-lösa) med exakt ETT beslut ur en sluten vokabulär (move, move-dedupe, resolve, revoke, actor-repoint, actor-keep, blocker, sanctioned-residual; de två sista kräver motivering ≥ 40 tecken). Motorns plan speglar manifestet och vägrar köra vid divergens; ett stående vakttest gör varje osanktionerad ny FK röd. Restinvarianten: efter en merge ger varje post vars beslut inte är sanctioned-residual/actor-keep/blocker noll rader med B:s id. En ny tabell kan alltså inte glömmas bort av mergen.

    ✅ 037909c7.m-3.t-1
  • F02.20.03 Shipped

    blockerare_audit_och_gdpr — öppen Art. 17-begäran, målsmanslänk och terminal status blockerar (merge_blocked_*, re-validerade under låset, noll ändringar, auditrad på båda identiteterna); suspended blockerar medvetet inte — spärren följer i stället med. DSAR-exporten bär sammanslagningarna för båda parter, och vid radering anonymiseras journalraden (raderas aldrig — den är bevisningen), sista-tenant-gatad som personfotot. Erasure-coverage-vakten grön med båda subjektkolumnerna klassade.

    ✅ 037909c7.m-3.t-1
  • F02.20.04 Shipped

    merge_sessionen_och_rutterna — /v1/me/identity-merge (start, fresh auth), …/verify-code (tvåsidigt bevis, fri ordning), …/summary (öppnar först när båda koderna är inlösta), …/confirm (fresh auth) och …/cancel. Starten kräver ett serverkänt 409-bevis (≤ 15 min) — utan det svarar rutten 409 merge_not_available med en byte-identisk kropp oavsett om adressen är fri eller upptagen (ingen enumeration). Koderna är auth_challenge-rader under metodnamnet identity_merge, räknade i samma anti-abuse-fönster som inloggningen, med kvotkontroll för båda inkorgarna före minten. Högst EN levande session per inblandat konto. Kontrakt: specs/api/endpoints/me-identity-merge.md.

    ✅ 037909c7.m-3.t-2
  • F02.20.05 Shipped

    bindande_sammanfattning — GET …/summary kommer ur en torrkörning av den riktiga motorn (en transaktion som alltid rullas tillbaka), så bekräftelsen utför exakt det som redovisas: flyttade adresser, berörda förbund/klubbar, varje kollisionsupplösning med sin klass och sin regel i klartext, spärr-unionen och konsekvenserna. Redovisningen binds vid ett fingeravtryck av världen — har något ändrats vid bekräftelsen svarar den 409 merge_stale och ingenting är halvgjort. Före det tvåsidiga beviset bär inget svar ett tecken om det andra kontot (NFR-1).

    ✅ 037909c7.m-3.t-2
  • F02.20.06 Shipped

    kollisionens_additiva_signal — m-1:s 409 email_owned_by_other_identity bär sedan m-3 det additiva merge_available: true: maskinläsbart "för den här raden kan en sammanslagning startas", utan ett ord om den andra identiteten. Status, felkod och struktur är oförändrade, så varje befintlig läsare och m-1:s låsta assertions är gröna (NFR-5). Appytan grindar sitt erbjudande på exakt det värdet — utan signalen står m-1:s supporttext kvar orörd.

    ✅ 037909c7.m-3.t-2
  • F02.20.07 Shipped

    app_guidat_merge_flode — player-appens skärm Slå ihop två konton (/konto-sammanslagning), nådd ur kollisionens ärliga erbjudande i /e-postadresser: fyra steg med textburen stegindikator (aldrig färg ensam), två tydligt märkta kodrutor ("koden som skickades till <adressen>" / "…till din primära adress"), "skicka nya koder" (cancel + start, sagt rakt ut) med vänlig väntetext ur Retry-After, sammanfattningen renderad ur API-svaret (ingen klientgissning om vad som flyttas), uttrycklig bekräftelse, klart-läge som pekar mot adress- och profillistan, och ett nej-läge med läsbar förklaring + supportväg — aldrig en rå felsträng, aldrig ett B-läckage. Fresh-auth ⇒ vänlig prompt, aldrig en utloggning. Transport: useApiClient() + de genererade operationerna (noll råa fetchar, ratchet-baseline app: 0 orörd). Vy-spec: specs/app/views/konto-sammanslagning.md.

    ✅ 037909c7.m-3.t-3
  • F02.20.08 Shipped

    l4_bevis_pa_riktig_stack_m3 — tools/gate/dprofiles-m3-app-e2e.mjs (descriptor e2e.d/720-dprofiles-m3.mjs) kör hela resan i en riktig webbläsare mot körande API + riktig Postgres: två riktiga identiteter via OTP (B med två validerade adresser/profiler, båda i samma socialspelsrunda ⇒ en riktig dubblett) → kollision ⇒ erbjudandet → guidat flöde → fel kod ⇒ vänlig felruta → avbrott ⇒ ingenting ändrat → båda koderna ur dev-återläsningen (method=identity_merge) → sammanfattningen jämförd rad för rad mot API:t (adresser, notiser, kollisionsregeln i klartext) → bekräftelse → klart-läget → adress- och profillistan (tre + tre) → logga ut → logga in med B:s gamla adress ⇒ A:s konto. DB-facit: journalraden finns, den manifestdrivna restinvarianten ger noll osanktionerade rader med B:s id och båda divergensfrågorna ger noll rader. Plus axe utan critical/serious på flödets skärmar och gratisnivån (noll medlemskap/licens/betalning) före och efter.

    ✅ 037909c7.m-3.t-3
  • F02.21.01 Shipped

    identity_email_enda_sanningen — migration 00144 kör cutovern i säkerhetskritisk ordning: pre-flight (divergerar spegeln > 0 rader avbryts HELA migrationen — cutover på divergent data är ett stopp, inte en chansning), spegel-trigger + funktion droppas, kolumnerna primary_email/email_verified droppas, ersättningsinvarianterna installeras och 00141:s primärvakt skrivs om kolumnfritt. Down är förlustfri: kolumnerna återskapas och backfillas radvis ur primärraden (primary_email = ie.email, email_verified = (ie.verified_at is not null)), så upp → ned → upp är radvis identiskt.

    ✅ 037909c7.m-4.t-2
  • F02.21.02 Shipped

    ersattningsinvarianterna — det kolumnen garanterade garanteras igen av DATABASEN: partiellt unikt index identity_email_primary_email_unique på (email) where is_primary (global primär-unikhet), constraint-triggern auth_identity_require_primary_email_trg (DEFERRABLE INITIALLY DEFERRED — identitet + primär adressrad skrivs i EN transaktion) och identity_email_require_primary_survivor_trg (en radering får aldrig lämna en levande identitet utan primär rad). Föräldralös-frågan — identiteter utan primär identity_email-rad — är initiativets nya stående 0-rads-invariant och ersätter m-1:s divergensfråga, som dog med spegeln.

    ✅ 037909c7.m-4.t-2
  • F02.21.03 Shipped

    kolumn_referensvakten — api/db/migrations/identity_email_truth_guard_test.go gör bygget rött om produktions-SQL eller produktions-Go refererar de droppade kolumnerna igen (on conflict (primary_email), insert/update mot dem, kvalificerad kolumnreferens mot vilket alias som helst, where/select-former), med en negativ gren som bevisar att vakten biter. Allowlist: migrationer ≤ 00144, docs, testfiler och det sanktionerade kontraktsaliaset … as primary_email (så de yttre JSON-fälten kan behålla sina namn). Att bredda allowlisten för att bli grön är en defekt värre än ett ärligt rött.

    ✅ 037909c7.m-4.t-2
  • F02.21.04 Shipped

    legacy_bryggan_avvecklar_sig — resolverns steg 2 grundas i identity_email (identiteten vars primära rad bär adressen utan verified_at) och promoterar raden vid inloggning: verified_at sätts i samma transaktion som sessionen mintas, auditerat i den befintliga familjen (identity_email_verified, reason: legacy_bridge_login), varpå m-2:s trigger föder default-profilen. En väntande sekundär rad matchar aldrig (squatting-garantin orörd), och statusgrinden håller: ett konto med status='erased'/'merged' kan aldrig logga in genom bryggan.

    ✅ 037909c7.m-4.t-1
  • F02.21.05 Shipped

    gdpr_och_seeds_mot_sanningen — Art. 17-raderingen opererar direkt på identity_email (icke-primära rader raderas, primärraden anonymiseras till erased-<id>@erased.invalid med verified_at = null, profilerna följer med), DSAR-exporten läser adresserna ur samma tabell, och samtliga seedvägars insert into auth_identity … on conflict (primary_email) är omskrivna med bevarad idempotens (hela seedmängden körd två gånger ⇒ identiskt sluttillstånd, bortsett från den append-only körhistoriken seed_run).

    ✅ 037909c7.m-4.t-2
  • F02.21.06 Shipped

    felkodsnycklarna_rattade — appens merge-skärm band sina två blockeringstexter till nycklar backend aldrig skickar (merge_blocked_deletion/merge_blocked_guardian), så de specifika texterna var död kod och personen fick alltid den generiska fallbacken. Nycklarna är nu backends ordagranna kodsträngar merge_blocked_deletion_request/merge_blocked_guardian_link (m-3 hardening-fynd §5.2.1); fallbacken för varje ANNAN merge_blocked*-kod står kvar som sista utväg. Bindningen är exekverbart bevisad tautologifritt: acceptansgrinden läser koderna ur api/internal/store/auth_identity_merge.go, aldrig ur en kopia.

    ✅ 037909c7.m-4.t-3
  • F02.21.07 Shipped

    initiativets_helhetsbevis — tools/gate/dprofiles-init-e2e.mjs (descriptor e2e.d/730-dprofiles-init.mjs) kör visionens hela DoD-resa i EN sekvens mot byggd app + körande API + färsk post-cutover-databas: konto A via riktig OTP → adress 2 tillagd (väntande rad utan profil — en ovaliderad adress gäller inte) → validerad med koden ⇒ profilen föds → snabbväxlaren byter aktiv profil och valet överlever omladdning → logga in med adress 2 ⇒ samma identitet → konto B med två validerade adresser → kollision ⇒ merge-erbjudandet ⇒ guidat flöde ⇒ tvåsidig kodverifiering (en kod räcker inte — negativt bevisat) ⇒ sammanfattningen ur API:t ⇒ bekräftelse → slutbilden: EN identitet med fyra adresser som fyra profiler, växling till en inflyttad profil, och inloggning med B:s gamla adress ⇒ A:s konto. DB-facit: föräldralös-frågan 0, merge-restinvarianten 0, journalraden finns. Plus axe utan critical/serious och gratisnivån (noll medlemskap/licenser/betalningar) före OCH efter. 700/710/720 består — 730 är ett tillägg, inte en ersättning; alla fyra kördes gröna på samma post-cutover-databas (per-milstolpe-resornas DB-frågor och m-1:s kollisionsyta är mekaniskt cutover-ade i t-3, med oförändrade förväntningsvärden).

    ✅ 037909c7.m-4.t-3
  • F02.21.08 Shipped

    initiativ_dod_matrisen — milestones/037909c7.m-4/initiative-dod.md mappar varje punkt i visionens DoD (DOD-1…DOD-8) till en namngiven exekverbar kontroll (testfunktion, drivrutin eller vakt) och till var den senast kördes grön. En punkt vars kontroll inte finns eller inte är grön är ett rött, inte en fotnot — acceptansgrinden app/__acceptance__/dprofiles-m4-helhet.acceptance.mjs verifierar mekaniskt att varje rad finns och att varje refererad kontroll existerar i trädet.

    ✅ 037909c7.m-4.t-3