Profiel (Backend)
Technisch Ontwerp
De backend van de profielmodule beheert per gebruiker een ProfileSettings-rij in het tenant-schema. De module is bewust dun: er is geen aparte servicelaag, alle logica zit in de views (features/profile/views.py).
- Privacy-master: zet een gebruiker
visible_to_teamopfalse, dan worden ookshare_phone,share_email,share_birthday_with_teamenshare_work_days_with_teamautomatisch opfalsegezet binnen dezelfde PATCH. Andere modules (Team, Verjaardagen, Agenda) lezen deze velden bij het serializen van collega's. - Avatar-upload: synchroon. De frontend resized de afbeelding al naar 512 bij 512. De backend valideert magic bytes plus pixel- en bytecap, decodeert met Pillow, encodeert opnieuw als WebP (
quality=80,method=4) en plaatst het object in S3 onder de sleutel<tenant_schema>/user/<user_id>/<uuid>.webp. De vorige sleutel wordt na succesvolle upload best-effort verwijderd. De response bevat direct een signedavatar_url. Er is geen Celery-task en geen 202/poll-flow. - Pushvoorkeuren: opgeslagen als JSON-blob (
push_preferences) plus master-flag (push_disabled_all). De keys komen uitnotifications.constants.PUSH_NOTIFICATION_KEYS(15 categorieen). Defaults staan optrue, behalveatlas_response_ready,review_invitation_accepted,review_invitation_declinedenreview_letter_failed(die staan default uit). - E-mailvoorkeuren: idem (
email_preferences,email_disabled_all). De keys komen uitEMAIL_NOTIFICATION_KEYS(13 categorieen). Defaults staan opfalse, behalvebirthday_selfenbirthday_colleague. De masteremail_disabled_allstaat standaard optrue(opt-in). - Atlas push prompt: het veld
atlas_push_prompt_seenmarkeert dat de gebruiker eenmalig de Atlas-banner heeft gezien, zodat die niet opnieuw verschijnt. - Thema: theme-voorkeur wordt expliciet niet opgeslagen, omdat licht of donker een apparaat-instelling is.
- Gegevensexport: het profielscherm biedt een zelf-service export (
GET /api/profile/data-export/) voor inzage en dataportabiliteit (AVG art 15 en 20). Deze leest gegevens samen uit meerdere bronnen maar schrijft niets inProfileSettings. - Wachtwoord, 2FA en sessies: deze acties leven in de auth-module (
/api/auth/security/...,/api/auth/devices/...) en worden vanuit het profielscherm aangeroepen, maar horen niet bij dit datamodel. - Gekoppelde accounts: de profielroute toont en beheert de eigen OIDC-koppelingen (ZORG-ID, Google, Apple) via de auth-OIDC-endpoints. De koppelingen leven in het auth-datamodel, niet in
ProfileSettings. Zie de authenticatie-documentatie voor details.
flowchart TD
A[POST /api/profile/avatar/] --> B[AvatarUploadIn validate]
B --> C[_validate_uploaded_image: size + magic bytes]
C --> D[Pillow decode + pixel cap check]
D --> E[Encode WebP quality=80]
E --> F[put_object S3 key tenant/user/uuid.webp]
F --> G[ProfileSettings.avatar_s3_key = key]
G --> H[delete_object oude key best-effort]
H --> I[audit_logger.log AVATAR_UPLOADED]
I --> J[Response avatar_url signed]
Datamodel (ERD)
erDiagram
User ||--|| ProfileSettings : "bezit"
ProfileSettings {
bigint id PK
bigint user_id FK
boolean share_phone
boolean share_email
boolean visible_to_team
boolean share_birthday_with_team
boolean share_work_days_with_team
boolean receive_team_birthday_emails
boolean haptics_enabled
boolean push_disabled_all
json push_preferences
boolean atlas_push_prompt_seen
boolean email_disabled_all
json email_preferences
string avatar_s3_key
datetime updated_at
}
ProfileSettings heeft een OneToOneField naar settings.AUTH_USER_MODEL. De rij wordt lazily aangemaakt via get_or_create bij de eerste GET of PATCH. Het veld avatar_s3_key bevat alleen de S3-sleutel; de signed URL wordt per response opnieuw gegenereerd door ProfileSettingsOut.get_avatar_url.
API & Communicatie
Alle endpoints staan onder /api/profile/ en vereisen IsAuthenticated. JSON in en JSON uit, behalve avatar-upload (multipart).
GET /api/profile/ping/: healthcheck, geeft{ok: true, feature: "profile"}.GET /api/profile/settings/: huidige privacy- en voorkeurinstellingen plus signedavatar_url.PATCH /api/profile/settings/: optionele veldenshare_phone,share_email,visible_to_team,share_birthday_with_team,share_work_days_with_team,receive_team_birthday_emails,haptics_enabled. Bijvisible_to_team=falseworden afhankelijke deel-toggles geforceerd opfalse.POST /api/profile/avatar/: multipartimage. Synchroon: valideert, converteert naar WebP, plaatst in S3, slaat sleutel op en geeft{avatar_url}terug. Geen polling.DELETE /api/profile/avatar/: verwijdert het object in S3 en leegtavatar_s3_key. Response{avatar_url: ""}.GET /api/profile/push-preferences/: master-flag, JSON-map per categorie enatlas_push_prompt_seen.PATCH /api/profile/push-preferences/: optionele veldenpush_disabled_all,push_preferences(object met booleans),atlas_push_prompt_seen. De backend doet een merge in plaats van replace, zodat onbekende keys niet verloren gaan.GET /api/profile/email-preferences/: master-flag plus JSON-map.PATCH /api/profile/email-preferences/: optionele veldenemail_disabled_all,email_preferences(merge-update).GET /api/profile/data-export/: zelf-service gegevensexport (AVG art 15 en 20). Antwoord bevatschema_version,user,profile_settings,reviews_created_by_userenaudit_entries_about_user. De export is afgeschermd met een aparte rate-throttle (ScopedRateThrottle, scopedata_export). Patientgegevens vallen er bewust buiten.
Wachtwoord, 2FA, sessies en gekoppelde accounts zijn auth-endpoints en horen niet bij dit module-datamodel.
Foutafhandeling & Statuscodes
200 OK: succesvolle GET, PATCH, avatar-upload of avatar-delete.400 Bad Request: validatiefout op een veld, te grote afbeelding, niet-ondersteund bestandstype, decompression-bomb, of ongeldige boolean-map (Voorkeur '<key>' moet boolean zijn,Te veel voorkeuren (max 250),Voorkeur key is te lang (max 80)).401 Unauthorized: ontbrekend of verlopen JWT.502 Bad Gateway: S3 weigerde de upload (Upload naar S3 mislukt.).
Avatarvalidatie controleert magic bytes voor PNG, JPEG, GIF, WebP en HEIC, met een harde cap van 5 MB en 10 megapixels.
Autorisatie & Beveiliging
- Tenant-isolatie:
ProfileSettingsleeft per tenant-schema. JWT bevattenant_schemaen de queryset gaat altijd viarequest.user. - Eigenaarschap: een gebruiker kan alleen zijn eigen
ProfileSettingslezen of muteren; views gebruikenrequest.userdirect als sleutel. - Geen RBAC: profiel is een basisscherm voor elke ingelogde gebruiker en heeft geen
required_permission. - Gekoppelde accounts koppelen of ontkoppelen verloopt via de auth-OIDC-endpoints. De profielmodule toont alleen de actie en bewaart de koppeling niet zelf.
- S3-sleutels bevatten een UUID-suffix en zijn niet raadbaar; toegang loopt altijd via een signed URL.
- Avatarverwerking is hard begrensd: 5 MB bytes-cap, 10 MP pixel-cap, en Pillow's
MAX_IMAGE_PIXELSis op dezelfde waarde gezet om decompression-bombs te blokkeren. - Audit log: muterende acties worden vastgelegd via
audit_logger. De gebeurtenissen zijnPROFILE_PRIVACY_CHANGED(privacy-instellingen),PROFILE_NOTIFICATIONS_CHANGED(push- of e-mailvoorkeuren, met kanaal en gewijzigde velden),AVATAR_UPLOADED(grootte en of een eerdere foto is vervangen) enDATA_EXPORTED(aantallen reviews en audit-regels).
Bestandsstructuur & Verantwoordelijkheden
backend/features/profile/models.py: definitie vanProfileSettingsmet privacy-, push- en e-mailvoorkeuren plus avatar-sleutel.backend/features/profile/serializers.py: in- en uit-serializers voor settings, push, e-mail en avatar; bevat de generieke_validate_bool_dictvoor JSON-maps.backend/features/profile/views.py: de endpoints ping, settings, avatar, push en e-mail, avatar-validatie en WebP-conversie, S3-upload en cleanup, plus de privacy-master-cascade en de notificatie-defaults.backend/features/profile/data_export.py: de zelf-service gegevensexport (UserDataExportView) met de export-serializers.backend/features/profile/urls.py: route-tabel onder/api/profile/.backend/features/profile/apps.py: Django app-config.backend/features/profile/migrations/: schema-migraties voorProfileSettings.backend/features/profile/tests/test_profile_endpoints.py: integratietests voor settings, avatar, push en e-mail.
Belangrijke bestanden
backend/features/profile/models.pybackend/features/profile/views.pybackend/features/profile/serializers.pybackend/features/profile/urls.pybackend/notifications/constants.py