Files
Cibello-app/docs/FAS4-HOUSEHOLD-COLLAB-AUDIT.md
T
Sven (AAMOS AI) 7216417413 docs(4): uppdatera FAS4-hushållsaudit efter granskningsjusteringar
- GDPR: tydligt skilj lämna hushåll (åtkomst) från kontoradering.
- Token-säkerhet: sha256-hash i DB, single-use, 7 dagars expiry, max 10
  aktiva länkar, roll adult vid join, ingen owner-transfer.
- Analytics: aldrig token eller invite_code i properties.
- Webbfallback noterad som fas-1-spår utanför Fas 4.
2026-08-07 18:10:03 +07:00

17 KiB
Raw Blame History

Fas 4 Hushållssamarbete: REUSE/GAP-matris och stegplan

Levereras före kod. Styrande dokument: utbyggnadsdokumentet §7 + docs/03-funktionskatalog.md + docs/07-datamodell.md + docs/12-sakerhet-gdpr.md + docs/19-beslutslogg.md.

Audit först, ingen kod. Invänta godkännande innan 4a påbörjas.


Hermetiska testregler (samma som Fas 3)

Alla integrationstester ska vara hermetiska:

  • De får inte bero på innehållet i .env (värden som AAMOS_MODE, EMAIL_MODE, S3_MODE skrivs över i setup-env.ts).
  • De får inte bero på förkonfigurerad konfiguration eller externa tjänster.
  • De får inte bero på databasinnehåll som skapats utanför testet. Testkörningen ansvarar själv för att testdatabasen är migrerad och seedad innan testerna startar; seeden är idempotent.
  • Körning: pnpm --filter @app/database run db:test-setup (migrate + seed) ska alltid föregå API-testerna.

1. REUSE/GAP-matris mot befintligt hushållsflöde

§7-krav / område Befintlig implementation Status Rekommenderad åtgärd
Hushållsgrundstruktur Tabellerna households och household_members finns. households.invite_code är unik. Roller: owner, adult, member, child. Finns Återanvänd oförändrat. Utöka med tidsbunden inbjudanslänk, inte ny inbjudningsmodell.
Gå med via kod POST /v1/households/join med { inviteCode }. Nya medlemmar får roll adult. Plan-gräns maxHouseholdMembers kontrolleras. Finns Återanvänd. Utöka med djuplänk + QR så att koden kan fyllas i automatiskt, men backend-logiken är densamma.
Medlemslista GET /v1/households/:id returnerar medlemmar med userId, displayName, role, portionFactor, joinedAt. Finns Återanvänd. Lägg till UI för att visa/ändra.
Ändra roll PATCH /v1/households/:id/members/:userId med role. Endast owner får ändra roller. Finns Återanvänd. Säkerställ att child inte kan upphöjas till owner.
Ta bort medlem DELETE /v1/households/:id/members/:userId. Endast owner kan ta bort andra; själv kan lämna. Finns Återanvänd. GDPR: avgöra om matdata ska behållas eller anonymiseras (se avsnitt 4).
Portionsfaktor household_members.portion_factor per medlem, default 1, validerat 0.13. Finns Återanvänd. Exponera i mobil-UI för ägare/vuxna.
Delade vs privata data Lager, plan, inköpslista, matlådor, budget är hushållsdelat. Mål, allergier, smakprofil, måltidshistorik är individuellt (§7/§56). Finns Fortsätt. Inga ändringar.
Länkinbjudan / djuplänk Saknas. Idag måste användaren manuellt skriva in inviteCode. Saknas Inför tidsbunden inbjudnings-token (household_invite_links) med djuplänk via brand.config.json urlScheme.
QR-inbjudan Saknas. Saknas Generera QR-kod i mobilen som kodar samma djuplänk. Ingen ny backend-modell.
Återkalla inbjudan Saknas. När en kod/länk delats finns den kvar tills hushållet byter kod. Saknas Lägg till revoked_at på inbjudningslänk och/eller invite_code_expires_at på hushållet.
Anti-dubblettkartan: inga parallella invite- eller medlemsmodeller households.invite_code + household_members är navet. OK Behåll. Alla inbjudningsvägar ska peka mot samma households.invite_code.
Händelsespårning HOUSEHOLD_MEMBER_ADDED finns i EVENT_TYPES. Finns Lägg till HOUSEHOLD_MEMBER_REMOVED, HOUSEHOLD_MEMBER_ROLE_CHANGED, HOUSEHOLD_INVITE_LINK_CREATED, HOUSEHOLD_INVITE_LINK_REVOKED.
Analytics Analytics-events är consent-gatade via trackProductAnalytics. Finns Lägg till household_member_added, household_member_removed, household_invite_link_used (inga PII, inga fritext-fält).
Notifieringar till medlemmar notifications-tabell + push_tokens finns. Delvis Återanvänd. Skicka push när någon går med eller lämnar (opt-in via notis-samtycke).
Plan-gränser loadEntitlements(app.db, owner.userId) ger maxHouseholdMembers. Finns Återanvänd vid både kod- och länkinbjudan.

Slutsats av matrisen

Hushållssamarbete är en utbyggnad, inte ett parallellsystem:

  • Kärnan (households, household_members, invite_code, roller, plan-gränser) finns redan.
  • Det som saknas är (a) djuplänk/QR för smidigare inbjudan, (b) tidsstyrning/återkallning av inbjudningar, (c) medlemskaps-händelser och analytics, (d) GDPR-klar hantering när medlem lämnar.

2. Anti-dubblettkartan varför INGA nya parallella modeller

2.1 Varför inte en separat household_invites-tabell med egen kod?

households.invite_code är redan unik och fungerar. Att skapa en separat tabell med egna koder skulle ge:

  • Dubbla källor på sanning: vilken kod gäller?
  • Dubbla join-endpoints och valideringar.
  • Risk att gamla och nya inbjudningar krockar.

Beslut: Använd households.invite_code som kanonisk permanent kod (multi-use fallback). En separat household_invite_links-tabell får endast lagra metadata om länken: tokenHash, createdBy, expiresAt, revokedAt, usedAt. Själva inbjudningskoden hämtas alltid från households.invite_code när länken löses in.

2.2 Varför inte en separat household_member_history-tabell?

Medlemskapet är PK på (household_id, user_id). Aktuellt medlemskap finns i household_members. Om vi behöver historik (vem var medlem när?) kan vi använda:

  • audit_logs (som redan loggar household.member_removed)
  • domain_events (HOUSEHOLD_MEMBER_ADDED, HOUSEHOLD_MEMBER_REMOVED)

Ingen ytterligare tabell behövs i Fas 4.

2.3 Varför inte en separat household_permissions-tabell?

Rollerna owner, adult, member, child är tillräckligt granulära för Fas 4. Rollbaserade behörigheter hårdkodas i routes (som idag). Om vi senare behöver finare rättigheter kan vi migrera då.


3. STEGPLAN

Steg 4a Djuplänk/QR för inbjudan

Mål: En användare kan trycka på en länk/QR och automatiskt hamna i appen med inbjudningskoden ifylld.

Ändringar:

  1. Ny tabell household_invite_links:
    • id uuid PK
    • householdId FK → households
    • tokenHash text unik, sha256 av slumpmässigt token (≥128 bitar, base64url)
    • createdBy FK → users
    • expiresAt timestamp with time zone (default nu()+7 dagar)
    • revokedAt timestamp with time zone nullable
    • usedAt timestamp with time zone nullable
    • createdAt, updatedAt
    • Token lagras ALDRIG i klartext i DB. Uppslag görs via sha256(token).
  2. Djuplänk: <urlScheme>://households/join?token=<token> där urlScheme läses ur brand.config.json (aldrig hårdkodat).
  3. Ny endpoint POST /v1/households/join-link:
    • Tar { token }.
    • Slår upp household_invite_links → hushåll via tokenHash = sha256(token).
    • Kontrollerar expiresAt, revokedAt, usedAt.
    • Återanvänder samma plan-gräns- och join-logik som /v1/households/join.
    • Efter lyckad join: markera länken usedAt.
  4. Ny endpoint POST /v1/households/:id/invite-links (owner/adult):
    • Skapar en ny länk med 7 dagars giltighet.
    • Begränsa antalet aktiva länkar per hushåll (t.ex. max 10).
    • Returnerar endast token EN GÅNG (i svaret). Servern sparar bara hash.
    • Roll vid join via länk är alltid adult — aldrig owner.
    • Owner-transfer ingår INTE i Fas 4.
  5. GET /v1/households/:id/invite-links (owner/adult): lista aktiva länkar med expiresAt (utan token, såklart).
  6. DELETE /v1/households/:id/invite-links/:linkId (owner/adult): återkalla (sätt revokedAt).

Berörda filer:

  • packages/database/src/schema/households.ts (household_invite_links)
  • packages/validation/src/household.ts (nya schemas)
  • apps/api/src/routes/households.ts
  • apps/api/src/lib/helpers.ts (generateInviteLinkToken, hashInviteLinkToken)
  • apps/mobile/src/app/household.tsx (QR + dela-länk)
  • apps/mobile/src/app/household-join.tsx (ny skärm för att ta emot länk)
  • apps/mobile/app.json / deep-linking-konfiguration
  • apps/mobile/src/locales/*/common.json (+12)

Migration: 0019_household_invite_links.sql

Testplan:

  • Skapa länk → token returneras EN gång.
  • Länk med giltig token → användare blir medlem med roll adult.
  • Utgången länk → 410 Gone med i18n-nyckel household.inviteExpired.
  • Återkallad länk → 410 Gone med i18n-nyckel household.inviteRevoked.
  • Redan använd länk → 410 Gone med i18n-nyckel household.inviteUsed. Länkar är engångsbruk (usedAt).
  • Plan-gräns nådd → 402 Payment Required (samma som kod).maxHouseholdMembers.
  • Max aktiva länkar överskridet → 429.
  • Token återfinns inte i DB i klartext.

Steg 4b Medlemshantering i mobilen

Mål: Ägare/vuxna kan se medlemmar, ändra roll, ta bort medlem; alla kan lämna hushållet.

Ändringar:

  1. Uppdatera apps/mobile/src/app/household.tsx:
    • Visa medlemmar med roll och portionsfaktor.
    • Ägare kan ändra roll och ta bort (utom sig själv om enda ägaren).
    • Varje medlem kan lämna hushållet.
  2. Lägg till bekräftelsedialoger för borttag/rolländring.
  3. Använd befintliga endpoints:
    • PATCH /v1/households/:id/members/:userId
    • DELETE /v1/households/:id/members/:userId

Berörda filer:

  • apps/mobile/src/app/household.tsx
  • apps/mobile/src/locales/*/common.json (+12)

Migration: ingen.

Testplan:

  • Ägare ändrar memberadult.
  • Ägare tar bort medlem → medlem försvinner.
  • Enda ägaren kan inte ta bort sig själv (409).
  • child kan inte ändra andras roller (403).

Steg 4c Händelser, analytics och notiser

Mål: Hushållsförändringar syns i domain_events, analytics och (opt-in) push-notiser.

Ändringar:

  1. Lägg till i EVENT_TYPES:
    • HOUSEHOLD_MEMBER_REMOVED
    • HOUSEHOLD_MEMBER_ROLE_CHANGED
    • HOUSEHOLD_INVITE_LINK_CREATED
    • HOUSEHOLD_INVITE_LINK_REVOKED
  2. Lägg till analytics-events (inga PII, inga fritext-fält):
    • household_member_added
    • household_member_removed
    • household_invite_link_used
  3. Emit events vid:
    • join (både kod och länk)
    • borttag
    • rolländring
    • länk skapad/återkallad
  4. Push-notis (opt-in via notis-samtycke) till övriga medlemmar när någon går med eller lämnar.

Berörda filer:

  • packages/shared-types/src/enums.ts
  • packages/events/src/index.ts
  • packages/analytics/src/builders.ts
  • apps/api/src/routes/households.ts
  • apps/api/src/lib/notifications.ts (om sådan finns)

Migration: 0020_expand_event_types_household.sql

Testplan:

  • Efter join: HOUSEHOLD_MEMBER_ADDED + analytics.
  • Efter borttag: HOUSEHOLD_MEMBER_REMOVED + audit-log.
  • Efter rolländring: HOUSEHOLD_MEMBER_ROLE_CHANGED.
  • Consent-gate: analytics skickas bara om användaren samtyckt.

4. RISKER

Risk Påverkan Minskning
GDPR: data när medlem lämnar Hög Att lämna/tas bort ur ett hushåll är inte kontoradering. Vid borttag av household_members-raden tas endast åtkomsten bort. Personens meals, taste_signals, memory_items, user_health_profiles och push_tokens tillhör kontot och rörs inte — användaren kan gå med i ett annat hushåll. Hushållets historiska aggregat (budget, svinn, gemensamma matlådor, inventory-transaktioner) ändras inte retroaktivt. Radering av persondata sker endast via den befintliga kontoraderingsprocessen (DELETE /v1/me, dokumenterad i docs/12). Beslutet skrivs in explicit innan 4b kodas.
Behörigheter: vem får bjuda in/ta bort Hög Bjud in: owner och adult. Ta bort andra: endast owner. Ta bort sig själv: alla utom enda ägaren. Ändra roller: endast owner. child får aldrig bjuda in eller ta bort.
Inbjudningslänkar läcker Medel Token är slumpmässig ≥128 bitar, tidsbegränsad, engångsbruk. Servern lagrar endast sha256(token). Återkallning sätts omedelbart. Inga PII i länken.
Parallella inbjudningsmodeller Hög Enda kanoniska koden är households.invite_code. household_invite_links är endast metadata. /v1/households/join och /v1/households/join-link konvergerar till samma join-kärna.
Plan-gränser kringgås Medel Både kod- och länkinbjudan kontrollerar maxHouseholdMembers mot ägarens entitlements före insert.
i18n-paritet fallerar Låg Alla nya nycklar läggs i samtliga 12 locales och testas med apps/mobile/test/i18n.test.ts.
Djuplänk hårdkodar app-namn Medel urlScheme läses dynamiskt från brand.config.json. Ingen hårdkodning av app-namn.
Testdata läcker mellan tester Låg Hermetiska tester skapar egna hushåll per test; invite_code genereras unikt.

5. Översiktliga berörda tabeller

Tabell Förändring
households Ingen schemaändring. invite_code fortsätter vara kanonisk.
household_members Ingen schemaändring. Befintlig roll- och delete-logik återanvänds.
household_invite_links Ny (metadata om djuplänkar).
users Ingen ändring, men vid kontoradering används befintligt GDPR-flöde (docs/12).
domain_events Nya event-typer: HOUSEHOLD_MEMBER_REMOVED, HOUSEHOLD_MEMBER_ROLE_CHANGED, HOUSEHOLD_INVITE_LINK_CREATED, HOUSEHOLD_INVITE_LINK_REVOKED.
audit_logs Återanvänds för household.member_removed (finns redan).
notifications / push_tokens Återanvänds för opt-in-notiser.

6. Leveransordning

  1. Steg 4a: djuplänk/QR för inbjudan (household_invite_links).
  2. Steg 4b: medlemshantering i mobilen.
  3. Steg 4c: händelser, analytics och notiser.

Per steg: brand-guard, typecheck, test, build, first-deploy staging, patch + bundle.


7. Länk-invites detaljerade regler

7.1 Token och djuplänk

  • Token: slumpmässig ≥128 bitar, base64url → 22 tecken. Skickas endast till klienten vid skapande.
  • Servern lagrar sha256(token) i household_invite_links.tokenHash; uppslag vid inbjudan görs via hash.
  • Djuplänk: <urlScheme>://households/join?token=<token> där urlScheme kommer från brand.config.json.
  • Giltighet: 7 dagar (expiresAt).
  • Engångsbruk: efter lyckad join sätts usedAt. Länkar är single-use by design.
  • Återkallning: owner eller adult sätter revokedAt.
  • Max 10 aktiva länkar per hushåll.
  • Roll vid join: alltid adult — aldrig owner.
  • Owner-transfer ingår inte i Fas 4.

7.2 Ingen PII i länken

Länken innehåller endast token. Inget hushållsnamn, ingen e-post, inga medlemsnamn. Den som har länken kan gå med, därför ska användare dela den via säkra kanaler.

7.3 Token-säkerhet och användningsregler

  • Token genereras slumpmässigt med ≥128 bitar.
  • Servern lagrar endast sha256(token) i tokenHash; klartext-token returneras bara vid skapande.
  • Länkar är single-use by design: usedAt sätts vid lyckad join.
  • Giltighetstid: 7 dagar.
  • Max 10 aktiva länkar per hushåll.
  • Roll vid join via länk: alltid adult — aldrig owner.
  • Owner-transfer ingår inte i Fas 4.

7.4 Konvergens med kod-inbjudan

Både POST /v1/households/join (kod) och POST /v1/households/join-link (token) ska i slutändan anropa samma interna joinHouseholdCore(db, householdId, userId, role) som:

  • kontrollerar plan-gräns,
  • sätter in household_members med angiven roll (adult för länk),
  • emittar HOUSEHOLD_MEMBER_ADDED,
  • loggar analytics (consent-gatat),
  • skickar notis till övriga medlemmar (opt-in).

7.5 i18n

Nya nycklar för 4a:

  • household.inviteLink, household.inviteLinkShare, household.inviteLinkCopied
  • household.inviteExpired, household.inviteRevoked, household.inviteUsed
  • household.qrScanHint

Nya nycklar för 4b:

  • household.leaveConfirmTitle, household.leaveConfirmBody
  • household.removeMemberConfirmTitle, household.removeMemberConfirmBody
  • household.changeRoleConfirmTitle

Alla läggs i samtliga 12 locales och testas med apps/mobile/test/i18n.test.ts.

7.6 Analytics

  • household_member_added: { household_id_hash, role, via: 'code' | 'link' } — hashat hushålls-id, inga personuppgifter.
  • household_member_removed: { household_id_hash }.
  • household_invite_link_used: { household_id_hash, link_id }aldrig token eller invite_code.

Alla går via trackProductAnalytics och är därmed consent-gatade.

7.7 Webbfallback

Om mottagaren inte har appen installerad och trycker på länken i en webbläsare krävs en associerad domän + universal link / app link. Detta är ett fas-1-spår som ligger utanför Fas 4. I Fas 4 förutsätts att mottagaren har Expo Go/appen installerad.


Rapport färdig. Inväntar godkännande innan kod påbörjas.