Add parental consent for youth events

Youth events need a name and date of birth for every attendee, and a
parent/guardian's details for under-18s. Parents get an email and
WhatsApp link to a consent page that records the signed name, time, IP,
device and exact consent text; medical and emergency details are stored
encrypted. Tickets are issued but check-in is blocked until consent is
given (supervisor override audited). A daily job sends reminders and
purges medical details after the retention period. Adds a supervisor
consent tracker, consent status and medical list reports, and clears
consent data when an account is anonymised.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 13:24:44 +02:00
co-authored by Claude Opus 5.5
parent f1e0b62c73
commit 809b6a11d7
41 changed files with 1988 additions and 88 deletions
+26
View File
@@ -19,6 +19,7 @@ Tokens are issued by `POST /api/users/login`.
- [Event Attachments](#event-attachments)
- [Registrations](#registrations)
- [Coupons](#coupons)
- [Parental consent](#parental-consent)
- [Payments](#payments)
- [Tickets](#tickets)
- [Broadcasts (Email)](#broadcasts-email)
@@ -276,6 +277,31 @@ For unauthenticated (guest) registration on events with `requiresAuth: false`:
---
## Parental consent
Youth events (`requiresParentalConsent`, plus `consentText`, `consentCollectMedical`, `consentCollectEmergency` and `consentCollectPhoto` on the event) need every main place named with a `dateOfBirth`. Attendees under 18 on the start date also need `parentName`, `parentEmail` and `parentPhone`. Changing `consentText` bumps `consentTextVersion`.
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/consent/:token` | public | What the parent sees: attendee, event, consent text, fields to fill in |
| POST | `/api/consent/:token` | public | `{ decision: "give"\|"decline", signedName, isGuardian: true, medical?, emergency?, photoConsent? }` |
| GET | `/api/consent/events/:eventId` | staff+ | Consent status per attendee, with counts |
| POST | `/api/consent/requests/:id/resend` | user+ | Re-send one request with a new link (registrant or staff) |
| POST | `/api/consent/events/:eventId/resend-pending` | supervisor+ | Re-send every pending request |
| POST | `/api/consent/events/:eventId/rerequest` | supervisor+ | New requests for consent given on older text, and minors with none |
| GET | `/api/consent/events/:eventId/medical` | supervisor+ | Decrypted medical & emergency list (audited as `medical_list_viewed`) |
How requests work:
- Requests go to the parent by both email and WhatsApp, whatever anyone's notification preference.
- Links are random tokens; only an HMAC is stored. They expire when the event ends.
- The answer stores the signed name, time, IP, device, and the exact text and version shown.
- Medical and emergency details are AES-256-GCM encrypted. A daily job (09:00, `CONSENT_JOBS_ENABLED=false` to disable) sends reminders (day 2, then every 3 days) and deletes them `consent_medical_retention_days` (default 30) after the event.
Check-in rules:
- `GET /api/tickets/scan-preview/:qrCode` returns `consentStatus` (`not_required`, `missing`, `pending`, `given` or `declined`).
- `POST /api/tickets/scan/:qrCode` returns `403 { code: "CONSENT_REQUIRED" }` unless consent is given.
- Supervisors can pass `overrideConsent: true`, which is audited as `consent_override`.
## Coupons
| Method | Path | Auth | Description |