Files

156 lines
8.4 KiB
Markdown

# MijnBesluit — architectuur
## Wat het is
Een platform om je beslissingen bij leven vast te leggen in een digitale kluis, en om berichten
te versturen naar familie en vrienden — nu of in de toekomst, en ook pas na je overlijden.
## De vorm in één plaatje
```
Website (Blazor) Android / iOS (MAUI)
| |
| in-proces | TLS-socket, poort 5200
v v
+---------------------------------------------+
| MijnBesluit.Server (dispatcher, sessies) | <- alleen voor de socketkant
+---------------------------------------------+
\ /
v v
+---------------------------------------------+
| MijnBesluit.Application |
| AccountService, FamilyService, VaultService, |
| MessageService, LegacyService + crypto |
+---------------------------------------------+
|
+---------------------------------------------+
| MijnBesluit.Domain (entiteiten + regels) |
+---------------------------------------------+
|
+---------------------------------------------+
| MijnBesluit.Infrastructure (EF Core) |
+---------------------------------------------+
|
MariaDB + versleutelde bestanden op schijf
```
De regels staan één keer, in Application en Domain. De website en de app zijn allebei niet meer
dan een manier om diezelfde regels aan te roepen. Wat op de website niet mag, kan dus ook via de
app niet — dat hoeft nergens tweemaal te worden afgedwongen.
## De projecten
| Project | Wat het doet | Externe packages |
|---|---|---|
| `MijnBesluit.Domain` | Entiteiten en de regels: eigendom van profielen, overlijdensprocedure, vrijgave van berichten | geen |
| `MijnBesluit.Application` | De handelingen: registreren, uitnodigen, kluis, berichten, nalatenschap. Plus alle versleuteling | geen |
| `MijnBesluit.Protocol` | Het socketprotocol: framing, de server-luisteraar en de client voor de app | geen |
| `MijnBesluit.Server` | Vertaalt socketverzoeken naar Application-aanroepen; sessiebeheer | geen |
| `MijnBesluit.Infrastructure` | EF Core op MariaDB, sleutelbestand, bestandsopslag, e-mail | EF Core, Pomelo |
| `MijnBesluit.ServiceBeheer` | De Windows-service installeren, verwijderen, starten en stoppen | geen |
| `MijnBesluit.SocketServer` | De zelfstandige service voor de app, het beheerschermpje en de achtergrondtaken | Hosting, WinForms |
| `MijnBesluit.Web` | De website in Blazor | geen |
| `MijnBesluit.SelfCheck` | Controleert de regels zonder database of webserver | geen |
Dat de eerste vier geen enkel extern package nodig hebben is met opzet: daardoor zijn de regels
en het protocol te bouwen en te controleren zonder dat er ook maar iets geïnstalleerd hoeft te
worden, en is `MijnBesluit.Protocol` zonder gedoe te hergebruiken in de MAUI-app.
## Geen webservice, geen IIS
De website is Blazor Server en roept de Application-laag rechtstreeks aan, in hetzelfde proces.
Er zit dus geen webservice tussen die serialiseert, over HTTP praat en weer deserialiseert.
De app praat met `MijnBesluit.SocketServer`: een zelfstandig draaiende Windows-service (64 bits)
met een eigen TCP-luisteraar. Daar komt geen IIS aan te pas.
Dezelfde exe is ook zijn eigen installatieprogramma: start Windows hem, dan draait de server;
start jij hem met de rechtermuisknop als administrator, dan verschijnt een schermpje met een
poortnummer en een knop **Install** — of **UnInstall** als hij al geïnstalleerd is. Zie
`docs/SocketServer-installeren.md`.
```
dotnet publish src\MijnBesluit.SocketServer -c Release -r win-x64 --self-contained false -o D:\MijnBesluit VS\MijnBesluit\publish
```
Binnen de service:
* één thread accepteert verbindingen;
* elke verbinding krijgt een eigen thread met een blokkerende leeslus;
* elk verzoek gaat naar zijn eigen thread, zodat een trage databaseactie de volgende verzoeken
van diezelfde app niet ophoudt. Elk verzoek krijgt daarmee ook zijn eigen databasecontext.
## Hoe een profiel van eigenaar wisselt
Dit is de kern van hoe MijnBesluit zich verspreidt.
1. Jan maakt een account. Hij krijgt een profiel (dat hij meteen zelf bezit) en een familieproject.
2. Jan voegt zijn dochter Eva toe. Er komt een profiel voor Eva, **beheerd door Jan**. Jan kan
alles aan dat profiel wijzigen.
3. Jan stuurt een uitnodiging naar Eva's e-mailadres. In de database staat alleen de SHA-256 van
het uitnodigingstoken; het token zelf bestaat enkel in die ene e-mail.
4. Eva klikt op de link en kiest een wachtwoord. Op dat moment wordt het profiel **geclaimd**:
`claimed_by_user_id` wordt gevuld en `managed_by_user_id` leeggemaakt.
5. Vanaf nu kan Jan niets meer aan Eva's profiel wijzigen. Hij ziet haar nog steeds in zijn
familie staan, met het label "geclaimd".
6. Eva krijgt haar eigen familieproject en nodigt op haar beurt anderen uit.
De regel staat op één plek: `ProfileOwnership.CanEdit`. Alles wat een profiel wil wijzigen gaat
daarlangs.
## Overlijden en nalatenschap
0. Je wijst beheerders aan, op volgorde. Zij zien dat meteen staan onder *Aan mij toevertrouwd*,
met hun volgorde erbij — daar reken je tenslotte op als het zover is. Meer dan de aanwijzing
zien ze niet: geen datum, geen aantallen, geen kluis.
Weghalen kan ook (`legacy.remove-manager`). Dat hoort erbij: een beheerder kan zelf overlijden,
en aanwijzen zonder kunnen terugnemen is geen keuze. Eén geval kan niet — iemand die je kluis
al heeft gekregen. Dan is de procedure afgerond en heeft hij gezien wat erin stond; die regel
weghalen zou alleen de administratie opschonen en niet wat er gebeurd is.
1. **Iedereen uit je kring** kan melden dat je bent overleden: de mensen die jij hebt toegevoegd
en die hun uitnodiging hebben aangenomen. Eén melding per gebruiker, afgedwongen met een
unieke sleutel in de database.
2. **Minstens één van die meldingen komt van een aangewezen beheerder.** Zolang die er niet is,
begint er niets: geen bericht aan de betrokkene, geen termijn die verloopt.
Waarom niet "alleen beheerders mogen melden", wat voor de hand lag: dan heb je evenveel
beheerders nodig als er meldingen vereist zijn. Wijs je er twee aan terwijl er drie meldingen
nodig zijn — wat de meeste mensen zullen doen — dan kan de procedure nooit worden afgerond en
gebeurt er bij een overlijden niets. Stilzwijgend, want niemand merkt het.
Zo werkt het nu dus niet. Het *aantal* haal je met je kring, want die merkt een overlijden als
eerste en is groot genoeg. De *beheerder* is de stem die er in elk geval bij moet zitten. Twee
verschillende soorten mensen moeten het dus eens zijn voordat er iets in beweging komt.
Blijft die beheerdersmelding uit, dan zegt het nalatenschapsscherm dat met zoveel woorden
(`WachtOpBeheerder`). En wie nog niemand heeft aangewezen, krijgt daar een waarschuwing: zonder
beheerder blijft de kluis ook na een overlijden dicht.
3. Zodra aan allebei is voldaan (standaard drie meldingen, per profiel instelbaar), krijgt de
betrokkene zelf een e-mail: *klopt dit niet, laat het weten*.
4. Reageert die persoon binnen de bezwaartermijn (standaard 14 dagen), dan vervallen alle
meldingen en gebeurt er niets.
5. Reageert die persoon niet, dan wordt het overlijden bevestigd. De aangewezen
nalatenschapsbeheerders krijgen toegang tot precies die kluisitems die zijn gemarkeerd als
"vrijgeven na overlijden" — de rest blijft dicht. Berichten met "pas als ik er niet meer ben"
worden bezorgd.
Een **lopende** procedure ziet er bij de beheerder precies zo uit als iemand die springlevend is.
Dat is met opzet: anders kon hij aan zijn eigen scherm aflezen dat er meldingen binnen zijn over
iemand die dat nog kan tegenspreken. Pas bij stap 5 verandert de kaart.
Later kan een koppeling met de BRP (de opvolger van de GBA) stap 1 tot en met 4 vervangen door
één betrouwbare bron. De rest van de code verandert dan niet: alleen wie `DeathConfirmedAt`
vult, verandert.
## Achtergrondtaken
`AchtergrondTaken` in de SocketServer draait elk kwartier en doet drie dingen: berichten bezorgen
waarvan de datum is bereikt of de afzender bevestigd is overleden, bezwaartermijnen die zijn
afgelopen afronden, en verlopen sessies opruimen.
## Tijd
Alles wordt in UTC opgeslagen en pas op het scherm omgerekend naar lokale tijd. Een bericht dat
op 25 december bezorgd moet worden, verschuift zo nooit door zomertijd.