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> **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- 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### 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.04Two-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- 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- 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> **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
No features match your filters.