276 lines
16 KiB
Markdown
276 lines
16 KiB
Markdown
# Wie is wie: dubbele profielen, claimen en samenvoegen
|
|
|
|
Dit document legt vast hoe MijnBesluit omgaat met de vraag "is dit dezelfde persoon?". Het is
|
|
geschreven na een ontwerpgesprek op 15 september 2026, omdat de keuzes hier niet vanzelfsprekend
|
|
zijn en de redenen erachter over een paar maanden niet meer uit de code te halen zijn.
|
|
|
|
## Het probleem
|
|
|
|
Jonathan voegt zijn dochter Ysabeau toe als dochter. Zijn ex-partner voegt dezelfde Ysabeau toe,
|
|
ook als dochter. Straks doen haar grootouders dat ook. Uiteindelijk maakt Ysabeau zelf een
|
|
account en wil ze haar eigen profiel beheren — en als ze daar een foto zet, horen haar familie en
|
|
vrienden die te zien.
|
|
|
|
Zonder maatregelen levert dat vier keer dezelfde persoon op.
|
|
|
|
## De kern van de oplossing
|
|
|
|
**Een niet-geclaimd profiel is een privé-aantekening van degene die hem maakte.**
|
|
|
|
Wat Jonathan heeft aangemaakt is niet "Ysabeau", het is *zijn aantekening over zijn dochter*. Dat
|
|
zijn ex haar eigen aantekening heeft is normaal en eerlijk. Dubbele profielen zijn vóór het
|
|
claimen dus geen fout.
|
|
|
|
Het wordt pas een probleem op het moment dat Ysabeau zelf zegt: die ben ik. Daarmee verschuift de
|
|
hele vraag — niet "hoe voorkom ik duplicaten bij het aanmaken", maar "hoe smelten ze samen op het
|
|
moment van claimen".
|
|
|
|
## Wat we bewust NIET doen
|
|
|
|
**Niet zoeken op naam en geboortedatum.** MyHeritage doet dat wel: je typt een naam en
|
|
geboortedatum en krijgt "bedoelt u deze?". Dat werkt daar, maar het lekt. Wie mag zo'n vraag
|
|
stellen? Als iemand "Ysabeau Flietstra, 12 maart 1998" intikt en het systeem zegt "die kennen
|
|
we", dan weet die persoon nu dat ze hier zit. Met genoeg pogingen is dat een zoekmachine op
|
|
mensen, en dat is precies wat MijnBesluit niet moet zijn.
|
|
|
|
**Niet stilzwijgend koppelen op e-mailadres.** (Wél vrágen; zie het hoofdstuk hieronder over de
|
|
vraag bij het aanmelden. Het verschil is wie er beslist.) Dit zat er wél in, tot 15 september 2026:
|
|
`AddMemberAsync` zocht een bestaand profiel op adres en hing dat aan je familie. Gevolg: wie het
|
|
adres van iemand kende, zag meteen wat een ander over die persoon had ingevuld — naam,
|
|
geboortedatum, profielfoto. Dat is dezelfde zoekmachine, via een andere deur.
|
|
|
|
De regel is nu: **je hergebruikt alleen je eigen aantekeningen, nooit die van een ander.** Voeg
|
|
je iemand toe aan een tweede familie van jezelf, dan is dat wél hetzelfde profiel — anders zie je
|
|
je eigen dochter twee keer.
|
|
|
|
## Hoe het dan wel samenkomt
|
|
|
|
**De uitnodiging is de match.** Jonathan nodigt Ysabeau uit op haar adres, zijn ex doet dat ook,
|
|
misschien op een ander adres. Ze neemt beide uitnodigingen aan. Daarmee heeft zíj gezegd dat
|
|
beide profielen haar zijn — en zij is de enige die dat mag zeggen.
|
|
|
|
Concreet:
|
|
|
|
| Situatie | Wat er gebeurt |
|
|
|---|---|
|
|
| Eerste uitnodiging, nog geen account | Er wordt een account gemaakt en het profiel wordt geclaimd. Zoals het altijd al ging. |
|
|
| Tweede uitnodiging, **ingelogd** | Het profiel uit de uitnodiging gaat op in het profiel dat je al hebt. |
|
|
| Tweede uitnodiging, **niet ingelogd**, adres is bekend | Geweigerd, met de uitleg dat je eerst moet inloggen. Een tweede account zou juist het probleem veroorzaken. |
|
|
|
|
Dit werkt ook als beide ouders een ander e-mailadres van haar hadden: het adres speelt geen rol
|
|
bij het koppelen, alleen de link die zij aanklikt.
|
|
|
|
## De vraag bij het aanmelden (17 september 2026)
|
|
|
|
De uitnodiging is de match, maar hij is niet de enige weg. Wat er in de praktijk gebeurde: iemand
|
|
voegt een vriend toe met zijn e-mailadres, die vriend maakt zelf een account aan, en dan staan er
|
|
twee losse dingen naast elkaar die allebei over dezelfde man gaan. De vriend moet dan wachten tot
|
|
er een uitnodiging komt die de ander misschien nooit verstuurt.
|
|
|
|
Automatisch koppelen op adres is daar niet het antwoord op, om twee redenen. Het lekt (zie
|
|
hierboven), en een adres is geen identiteit: twee mensen kunnen er één delen - een echtpaar, een
|
|
ouder die het adres van zijn kind gebruikt, een `info@` - en sinds `db/15` kunnen er ook meerdere
|
|
aantekeningen met hetzelfde adres bestaan. Automatisch koppelen moet dan gokken welke, en een
|
|
verkeerde gok maakt van twee mensen stilletjes één. Dat krijg je er niet meer uit.
|
|
|
|
Daarom vragen we het aan de enige die het antwoord weet. Wie is aangemeld, ziet bovenaan zijn
|
|
overzicht staan: *"X heeft een profiel voor je aangemaakt op jouw e-mailadres, als vriend. Ben jij
|
|
dat?"* Met twee knoppen, en dat is alles.
|
|
|
|
| Antwoord | Wat er gebeurt |
|
|
|---|---|
|
|
| **Ja, dat ben ik** | Dezelfde samenvoeging als bij een uitnodiging die je ingelogd aanneemt. |
|
|
| **Nee, dat ben ik niet** | Er verandert niets, en de vraag komt niet terug (`persons.link_declined_by_user_id`, `db/17`). |
|
|
| **Niets** | Er verandert niets. De vraag blijft staan tot hij hem beantwoordt. |
|
|
|
|
Waarom dat mag zonder dat het lekt: wie deze vraag krijgt, heeft zojuist met een code in dat
|
|
postvak bewezen dat het adres van hem is. De enige die iets te zien krijgt is dus de eigenaar van
|
|
het adres - en over zichzelf. Hij ziet de naam van degene die de aantekening bijhoudt en wat hij
|
|
van hem zou zijn; dat is precies genoeg om de vraag te kunnen beantwoorden, en niet meer.
|
|
|
|
Andersom gebeurt er niets: degene die de aantekening maakte krijgt geen melding dat er iets is
|
|
gevraagd, en geen melding dat het antwoord nee was. Anders zou het weigeren zélf een mededeling
|
|
worden over wie er wel of niet een account heeft.
|
|
|
|
De vraag staat op het overzicht en niet als popup bij het aanmelden. Iemand kan je ook toevoegen
|
|
nádat jij je al had aangemeld, en een popup die je wegklikt is een vraag die je nooit meer krijgt.
|
|
|
|
Er is hierdoor **geen apart goedkeuringsverzoek nodig** voor het toevoegen van iemand die al een
|
|
account heeft. Dat stond eerst in het plan, maar het lost zichzelf op: wie nooit andermans
|
|
profiel adopteert, geeft ook niets weg. Wil je die persoon er echt bij, dan nodig je uit — en die
|
|
uitnodiging *is* de toestemming.
|
|
|
|
## Wat er verhuist bij het samenvoegen
|
|
|
|
Zie `SamenvoegService`. Een niet-geclaimd profiel bezit weinig, want kluisitems horen bij de
|
|
persoon áchter een account. Wat er wel aan hangt:
|
|
|
|
- **lidmaatschappen** — staat het doel al in die familie, dan verdwijnt het dubbele lidmaatschap;
|
|
de relatie die er al stond blijft leidend, want die is ingevuld door de eigenaar van die familie
|
|
- **berichten** die voor het duplicaat klaarstonden of ervandaan kwamen
|
|
- **foto's** waar de persoon op staat; hoogstens één blijft profielfoto
|
|
- **uitnodigingen**, ook de gebruikte
|
|
- **rollen in een nalatenschap** — een dubbele aanwijzing of een tweede overlijdensmelding van
|
|
dezelfde gebruiker verdwijnt, anders vervalst dat het aantal
|
|
- **lege velden** worden aangevuld vanuit het duplicaat, nooit overschreven: wat bij het doel
|
|
staat komt van de persoon zelf en weegt zwaarder
|
|
|
|
Het duplicaat wordt **niet verwijderd**. Hij blijft staan met `merged_into_person_id` naar waar
|
|
hij in is opgegaan — er hangt geschiedenis aan, en een verwijderde rij is niet terug te halen.
|
|
|
|
Twee grenzen, allebei met een controle erop:
|
|
|
|
- Je kunt alleen samenvoegen met **je eigen** profiel.
|
|
- Twee **geclaimde** profielen gaan nooit automatisch samen. Dat zijn twee echte accounts met
|
|
ieder een eigen kluis; daar hoort een mens naar te kijken.
|
|
|
|
## De profielfoto
|
|
|
|
Hier lag een echte afweging. Zet Ysabeau zelf een foto, dan is die versleuteld met háár sleutel.
|
|
Haar vader zou hem dan niet kunnen zien — precies het tegenovergestelde van de bedoeling.
|
|
|
|
**Gekozen: de kleine versie is niet versleuteld, de foto op ware grootte wel.**
|
|
|
|
De redenering: een profielfoto is juist het ding dat je aan andere mensen laat zien. Hem
|
|
behandelen als kluismateriaal is niet eerlijk over wat het is, en zou betekenen dat je voor elke
|
|
kijker een sleutel moet uitdelen en weer intrekken zodra families veranderen.
|
|
|
|
De prijs is echt en bewust genomen: wie toegang heeft tot de database, ziet gezichten van 160
|
|
pixels. Alles waar het in MijnBesluit om gaat — wachtwoorden, polissen, uitvaartwensen, berichten
|
|
— blijft versleuteld, net als de foto op ware grootte.
|
|
|
|
**Wie hem mag zien:** iedereen met wie je in een familie zit. Dat is de kring die mensen zelf
|
|
samenstellen door elkaar toe te voegen en uit te nodigen. Die controle staat in `VaultService` en
|
|
niet in de schermen: een toegangsregel hoort niet af te hangen van de vraag of elk scherm eraan
|
|
denkt.
|
|
|
|
Kleine versies uit de allereerste opzet waren nog wél versleuteld. Die worden bij het eerste
|
|
gebruik automatisch vervangen; daar is geen migratie voor nodig.
|
|
|
|
**Waar hij staat:** overal waar een mens in een lijst staat — Mijn mensen, je beheerders, de
|
|
nalatenschappen die aan jou zijn toevertrouwd — en in app en website hetzelfde. Dat zit in één
|
|
onderdeel, `Penning.razor`. Dat bestand staat twee keer op schijf (de app en de site delen geen
|
|
project) en een zelfcontrole vergelijkt ze letterlijk, zodat ze niet uit elkaar lopen.
|
|
|
|
**Jezelf** sta je niet tussen je eigen mensen — je bent geen familielid van jezelf. Bovenaan Mijn
|
|
mensen staat daarom een eigen kaart die naar Mijn profiel gaat. Daar wijzig je je naam, je
|
|
geboortedatum, je foto en je e-mailadres; dat laatste met een code naar het nieuwe adres. Zonder
|
|
die kaart gingen mensen hun eigen gegevens zoeken in de lijst waar ze juist niet in staan.
|
|
|
|
## Het adres van een persoon is niet uniek
|
|
|
|
Dat lijkt een detail van de database, maar het is de hele regel hierboven in één index.
|
|
|
|
Op `persons.email` stond een `UNIQUE`-index. Het gevolg: voegde je iemand toe met een adres dat
|
|
al ergens in de database stond - bij wie dan ook, ook bij een wildvreemde - dan liep het opslaan
|
|
stuk. Voor de gebruiker was dat een knop die niets deed. Wie een adres het eerst invulde,
|
|
bepaalde daarmee dat niemand anders die persoon nog kon toevoegen. In een applicatie waar twee
|
|
ouders allebei hun dochter toevoegen is dat precies de verkeerde kant om.
|
|
|
|
Sinds `db/15_adres_niet_uniek.sql` is het een gewone index. Wat dat betekent voor de code:
|
|
|
|
- **Zoeken op adres kan meer dan één profiel opleveren.** `ListByEmailAsync` geeft ze allemaal;
|
|
`FindByEmailAsync` geeft er één, geclaimde eerst, zodat je in elk geval altijd dezelfde
|
|
terugkrijgt.
|
|
- **Wie zijn eigen profiel zoekt, moet daarop filteren** en niet op de eerste treffer. Met de
|
|
eerste de beste kreeg je die van een ander te pakken, viel de eigendomscontrole uit, en maakte
|
|
je een tweede aantekening aan voor iemand die je zelf al had staan.
|
|
- **Bij het wijzigen is alleen een botsing met je eigen profiel een bezwaar.** Dat een ander
|
|
dezelfde persoon kent, is geen fout maar het uitgangspunt.
|
|
- **Een account maken koppelt niet meer op adres.** Wie zonder uitnodiging een account maakt,
|
|
krijgt zijn eigen profiel; de aantekening van een ander komt bij hem terecht op het moment dat
|
|
hij díens uitnodiging aanneemt. Zoeken op adres betekende andermans aantekening adopteren
|
|
zonder dat die ander er iets over te zeggen had, en met meerdere aantekeningen was het
|
|
bovendien de vraag welke je dan kreeg.
|
|
|
|
Bij `users.email` blijft uniek juist wél gelden: dat is een account, en er kan er maar één per
|
|
adres zijn. Een zelfcontrole bewaakt allebei die kanten.
|
|
|
|
## Wat hier nog niet is opgelost
|
|
|
|
- **Mensen zonder e-mailadres** kunnen nooit claimen, dus hun duplicaten blijven bestaan. Voor
|
|
een kluis en berichten maakt dat niet uit. Zodra er ooit een gedeelde stamboom komt, wordt het
|
|
wel een vraag.
|
|
- **Samenvoegingen van vóór 15 september 2026** zijn niet terug te draaien. Daar is geen bonnetje
|
|
van (zie hieronder); de gegevens om het mee te doen bestaan eenvoudigweg niet. Dat is geen fout
|
|
die nog te repareren valt.
|
|
|
|
## Samenvoegen terugdraaien
|
|
|
|
Samenvoegen was lange tijd onomkeerbaar, en dat stond hier ook als open punt. Dat is het nu niet
|
|
meer.
|
|
|
|
Waarom het niet zomaar kon: ná een samenvoeging is niet meer te zien wélke lidmaatschappen,
|
|
berichten en bestanden bij het duplicaat hoorden. Ze staan gewoon bij het doel, tussen alles wat
|
|
daar al stond. Een gok daarop zou andermans gegevens meesleuren.
|
|
|
|
**Daarom schrijft elke samenvoeging nu een bonnetje.** In de tabel `samenvoegingen` komt per
|
|
samenvoeging één regel te staan met precies wat er is verhuisd, als lijstje stappen in JSON.
|
|
Terugdraaien is die lijst in omgekeerde volgorde aflopen. Regels die bij het samenvoegen zijn
|
|
weggegooid — een dubbel lidmaatschap, een dubbele beheerder — staan er als volledige kopie in,
|
|
want die moeten opnieuw worden aangemaakt.
|
|
|
|
Drie dingen die daarbij met opzet zo zijn:
|
|
|
|
- **Wat er ná de samenvoeging is bijgekomen, blijft staan.** Wat Ysabeau daarna zelf in haar kluis
|
|
heeft gelegd, is van haar en hoort bij haar. Alleen wat in het bonnetje staat gaat terug.
|
|
- **Een veld dat we hebben aangevuld gaat alleen leeg als het nog steeds is wat wij invulden.**
|
|
Heeft de persoon het daarna zelf aangepast, dan is het van hem en blijft het staan.
|
|
- **Terugdraaien mag alleen een beheerder.** Samenvoegen doet de persoon zelf — alleen hij mag
|
|
zeggen dat twee profielen hem zijn — maar het raakt ook de gegevens van degene die het
|
|
duplicaat ooit aanmaakte. Gaat het mis, dan hoort er een mens naar te kijken.
|
|
|
|
Is het doelprofiel zelf later ergens in opgegaan, dan weigert het terugdraaien: het bonnetje
|
|
wijst dan niet meer naar waar de gegevens staan. Die latere samenvoeging gaat eerst terug.
|
|
|
|
Het bonnetje blijft na afloop staan, met wanneer en waarom het is teruggedraaid. Dát er ooit is
|
|
samengevoegd, is zelf ook geschiedenis.
|
|
|
|
## De beheerder
|
|
|
|
Er is één beheerpagina, `/beheer`, en die is met opzet bijna leeg. Hij bestaat voor de twee
|
|
situaties waar niemand anders uit komt: een overlijden dat ten onrechte is bevestigd terwijl de
|
|
bezwaarlink verlopen is — het account staat dan op "overleden", inloggen kan niet meer, en er is
|
|
geen weg terug — en een samenvoeging die niet had gemoeten.
|
|
|
|
Wat een beheerder wél kan:
|
|
|
|
- een profiel opzoeken op het **volledige** e-mailadres
|
|
- een bevestigd overlijden terugdraaien, met een verplichte reden die in het logboek komt
|
|
- een samenvoeging terugdraaien, ook met een verplichte reden
|
|
|
|
Beide handelingen hebben iets gemeen dat ze veilig maakt: ze geven nooit toegang. De ene neemt
|
|
toegang weg, de andere zet terug wat er stond.
|
|
|
|
Wat hij niet kan, en ook nooit moet kunnen:
|
|
|
|
- **een kluis openen.** Zijn enige handeling neemt toegang wég en geeft hem nooit. Wie dat
|
|
verandert, haalt de bodem onder het hele product vandaan.
|
|
- **op naam zoeken.** Dat zou de zoekmachine op mensen zijn die hierboven wordt afgewezen. Je
|
|
moet al weten wie je zoekt, omdat diegene contact met je heeft opgenomen.
|
|
- **iemand beheerder maken.** Dat gaat met de hand in de database:
|
|
`UPDATE users SET is_beheerder = 1 WHERE email = '...'`. Wat niet via de applicatie kan, kan
|
|
ook niet via een lek in de applicatie.
|
|
|
|
Wie geen beheerder is, krijgt op die pagina "Niet gevonden" te zien — niet "geen toegang". Een
|
|
pagina waarvan je niet weet dat hij bestaat, ga je ook niet proberen.
|
|
|
|
### Het menu-item Beheer
|
|
|
|
In het menu staat Beheer alleen voor een beheerder. Of dat zo is, komt uit het inlogcookie
|
|
(de claim `mijnbesluit:beheer`) en niet uit de database.
|
|
|
|
Dat is niet uit zuinigheid. Het menu zit in de layout, en die staat op élke pagina; hij wordt
|
|
tegelijk met de pagina zelf opgebouwd. Vroeg de layout het op bij de database, dan liep die
|
|
vraag gelijk op met de vraag van de pagina — over dezelfde DbContext, en die kan er maar één
|
|
tegelijk aan. Het gevolg was `A second operation was started on this context instance`: een
|
|
foutpagina die er soms wel en soms niet was, afhankelijk van welke vraag net eerder klaar was.
|
|
Een layout hoort niets op te halen; de zelfcontrole "Een layout haalt zelf niets op" houdt dat
|
|
vast.
|
|
|
|
De prijs is dat iemand die net beheerder is gemaakt het menu-item pas ziet na opnieuw inloggen.
|
|
Dat is te overzien, want de claim is geen sleutel maar een lampje: `/beheer` en
|
|
`/beheer/adverteren` kijken zelf nog in de database of je het écht bent, en doen dat bij elke
|
|
handeling opnieuw. Wordt iemands beheerderschap ingetrokken, dan is hij dus meteen geweerd —
|
|
alleen blijft het menu-item nog staan tot hij uitlogt.
|