feat: change email, export my data, delete account (with data anonymization)

This commit is contained in:
Florian Sylvain committed 2026-10-30 20:19:07 +01:00
1 parent bb191c389a
commit 10a66cd979
123 files changed
+10789 -76

No files matched your search

+8 -8
View File
@@ -15,14 +15,15 @@ Readers of your public site are not tracked by RenewCMS: the public routes set n
| Data | Where | Kept |
|---|---|---|
| Username, email, display name, role, bcrypt password hash | `users` table | Until the account is deleted (see [Gaps](#gaps)). |
| Username, email, display name, role, bcrypt password hash | `users` table | Until the user deletes the account (see Erasure under [Answering requests from the people concerned](#answering-requests-from-the-people-concerned)). |
| Verification and password-reset codes | `users` table, bcrypt-hashed | Verification: until the account is verified. Reset: one hour, then useless; cleared when used. |
| Pending email change: the new address (`pending_email`), its bcrypt-hashed code (`email_change_code`) and the expiry (`email_change_expiration`) | `users` table | Two hours, then useless; cleared when confirmed, cancelled or replaced. |
| Unverified sign-ups | `users` table | Deleted automatically 7 days after their verification code expired. |
| Invites: invitee email, hashed code, inviter | `invites` table | Deleted automatically 30 days after they expired or were accepted. |
| Sessions | The `jwt` cookie in the user's browser, valid 24 hours | Logging out revokes the session on the server; the revocation (a random id and an expiry, nothing else) is deleted once the session would have expired anyway. |
| API keys: name, prefix, SHA-256 hash, creator, last use | `api_keys` table | Until revoked. A revoked key stays listed. |
| Activity log: who (user id and name, API key name, or the username typed at a failed login), what, when, IP address, browser (user agent, truncated to 255 characters) | `audit_events` table | Settings → Activity: 30, 90 (default), 365 days or forever, and an optional size cap. Cleaned up once a night, at 03:00 server time. |
| Authors: name, slug, bio, avatar, website, social links; the linked user's email is shown to admins | `authors` table | Until deleted. |
| Authors: name, slug, bio, avatar, website, social links; the linked user's email is shown to admins | `authors` table | Until deleted. When the linked user deletes their account, the author is deleted if no article credits it, otherwise it is kept as "Deleted user" with no bio, avatar, website or links. |
| Articles and custom entries | `articles`, `entries` tables | Deleted articles are soft-deleted: the row and its content stay in the database (see [Gaps](#gaps)). |
| Uploaded images | `UPLOAD_DIR` or the S3 bucket, plus the `images` table (file name, size, uploader) | Until deleted from the media library. Deleted images are soft-deleted in the table; the file itself is removed. |
@@ -45,7 +46,7 @@ RenewCMS sets no analytics, advertising or third-party cookie, so no consent ban
| Destination | When | What |
|---|---|---|
| Your SMTP provider (`SMTP_HOST`) | Sign-up, invite, password reset | Recipient address, the message (with a one-time link). STARTTLS or implicit TLS is required for any non-loopback host unless you set `SMTP_ALLOW_INSECURE=true`. |
| Your SMTP provider (`SMTP_HOST`) | Sign-up, invite, password reset, email change, account deleted | Recipient address, the message (with a one-time link, except the account-deleted notice, which holds the site address only). STARTTLS or implicit TLS is required for any non-loopback host unless you set `SMTP_ALLOW_INSECURE=true`. |
| Your S3 provider, if `STORAGE_DRIVER=s3` | Every upload and image read | The image files. |
| `UPDATE_FEED_URL` (default `https://git.floriansylvain.fr`) | Only when the owner clicks "Check for updates" or downloads one | The server's IP address and a standard HTTP request. No account or content data. |
@@ -62,16 +63,15 @@ The back office loads nothing from third parties: fonts, icons and scripts are s
## Answering requests from the people concerned
- **Access and portability (art. 15 and 20)**: there is no self-service export yet. The data of a user is their row in `users`, their author profile, their API keys and the activity events where they are the actor; an owner can export the activity log as CSV from the Activity page and filter it by person.
- **Rectification (art. 16)**: users can change their display name and author profile themselves. Changing an email address currently needs a database edit.
- **Erasure (art. 17)**: an unverified account can cancel its own registration. Deleting a verified user has no button yet; see [Gaps](#gaps).
- **Access and portability (art. 15 and 20)**: every verified user downloads a copy of their data from the Your data card on Profile (`GET /v1/user/me/export`). It is a ZIP of four pretty-printed JSON files: `profile.json` (account, role, author profile, API-key metadata without the key or its hash), `authored-content.json` (every article they created, drafts included, the published articles crediting their author profile, and entries they created; credited authors appear as id, name and slug only; other people's drafts are never included), `uploaded-media.json` (references to the files they uploaded, no bytes) and `account-activity.json` (the events where they are the actor). It never holds the password hash, any code, the token version or other people's data, and it leaves out anonymous failed-login events (their actor is not an account), events whose target is another user, an invite or another person's author profile, and soft-deleted articles and images. The archive is built in memory, and at most 2 exports are built at the same time across all users; a request whose wait for a free slot is cancelled or times out answers 503 and does not use the user's slot. One export is allowed per user every 15 minutes, and each one is recorded as `user.export_data`. An owner can also export the activity log as CSV from the Activity page and filter it by person.
- **Rectification (art. 16)**: users can change their display name, author profile and email address themselves. An email change needs the current password and a link sent to the new address; the old address is told about the request and keeps working until the link is opened.
- **Erasure (art. 17)**: every verified user deletes their own account from the Danger zone card on Profile (`DELETE /v1/user/me`), with their password and the typed word `DELETE`; an unverified account can still cancel its own registration. The last Owner, and the last administrator (built-in Admin role with no Owner), are refused until another verified user holds the role, so the workspace is never left without one. In one atomic operation, **removed**: the user row (username, display name, email, password hash, codes), every API key, every session, and the linked author profile when no article credits it. An author profile credited on articles is **scrubbed**, not deleted: it becomes "Deleted user" with the slug `deleted-user-<id>` (that prefix is reserved, so nobody can pick it), no bio, website, social links or user link. Its **avatar image** is deleted (the media row in the same operation, the stored file and its generated variants right after the commit; a storage failure is logged and does not undo the deletion) unless the image is also used by an article (body, cover or social image), another author, a site's default social image or a custom entry, in which case it stays. In the **activity log** the events where the user is the actor or the target lose their name (it becomes "Deleted user"), their IP address and browser, and their `params` and `changes` (which can hold display names, author names, bios and invitee emails); author events the user made on their own profile also lose the profile name. The events themselves (action, outcome, time) stay, and so do author events another person made on that profile, and events written by other people about the user, which keep their text but not the user's name. A mail "Your RenewCMS account has been deleted" (the site address only) is sent to the address the account had, and the deletion is recorded as a `user.delete_account` event under "Deleted user", with the IP address and browser of the request: it is the one trace of the erasure.
What remains, honestly: articles, entries and media the user created keep the old user id in `created_by` / `updated_by` / `uploaded_by_id` (shown as "Deleted user"; the id no longer points to anyone); `invites.invited_by` can point at the deleted id; anonymous failed-login events keep the username that was typed (see [Gaps](#gaps)); soft-deleted articles and images stay as rows (see [Gaps](#gaps)); and the browser of the person may still hold unsaved drafts and interface choices in `localStorage` / `sessionStorage` on their device, which RenewCMS cannot reach.
## Gaps
These are known and tracked; until they are fixed, handle them by hand.
- No way to delete a verified user or remove an admin from the interface.
- No self-service export or email change.
- Soft-deleted articles and images keep their rows indefinitely.
- The username typed at a failed login is stored as typed, so a password typed in the wrong field can end up in the activity log; clean such events by hand if it happens.
- Images uploaded before metadata stripping keep their metadata.