Skip to content

Add delete buttons for User and Community#3574

Open
isTravis wants to merge 23 commits intomainfrom
tr/delete-buttons
Open

Add delete buttons for User and Community#3574
isTravis wants to merge 23 commits intomainfrom
tr/delete-buttons

Conversation

@isTravis
Copy link
Copy Markdown
Member

@isTravis isTravis commented Apr 13, 2026

This one's a doozy. We implement two user-facing features: Delete Community and Delete Account. Both are sensitive operations that must preserve the scholarly record - specifically, DOI resolution and author attribution on published works.


The Problem

No delete functionality existed

There were no endpoints or UI for deleting a community or a user account. Account deletion was handled manually via email to privacy@pubpub.org.

The CASCADE problem

The Sequelize models used onDelete: 'CASCADE' extensively on userId foreign keys. This meant that if a User row were simply destroyed, the database would silently cascade-delete all of the following:

Model What would be lost
PubAttribution Authorship credit on published works
CollectionAttribution Authorship credit on collections
Discussion Entire discussion threads (including OTHER users' comments, because the Discussion row owns the Thread)
ThreadComment Individual comments, creating holes in conversations
ThreadEvent Thread status history (close/reopen audit trail)
ReviewNew Peer review records
ReviewEvent Review audit trail
CommunityBan (via actorId) Bans issued by a moderator -- deleting the moderator would effectively un-ban users

Similarly, deleting a Community would cascade through Pub and all its children, destroying DOI'd publications whose DOIs would then resolve to dead links.

Additional FK issues

Several models had no onDelete behavior defined at all, which would cause FK constraint violations blocking deletion:

Model Field Risk
Release userId (NOT NULL, no FK association) Would block or orphan
ZoteroIntegration userId (no onDelete) Would block deletion
VisibilityUser userId (no onDelete) Would block deletion
UserScopeVisit userId (no FK association) Orphaned records
UserDismissable userId (no FK association) Orphaned records
ActivityItem actorId (no FK association) Orphaned references

The DOI constraint

A DOI is a permanent identifier. Per Crossref/DataCite policy, once registered, a DOI must always resolve somewhere. The doi field lives directly on the Pub model as a TEXT column. Destroying the pub row means the DOI becomes a dead link in citation databases worldwide. There was no server-side logic to deactivate or re-point DOIs.

The attribution constraint

PubAttribution already had standalone name, avatar, and orcid fields (originally designed for attributing non-users). But onDelete: 'CASCADE' on userId meant these records would be destroyed rather than having userId set to NULL, losing the authorship information.


Solution Design

Delete Community

When a community is deleted:

  1. Pubs WITH a DOI are moved to a global archive community at archive.pubpub.org instead of being destroyed. Everything travels with them:

    • Releases (published document snapshots)
    • Draft + DraftCheckpoints
    • Discussions, Threads, ThreadComments (all conversation content)
    • PubAttributions (author names)
    • Reviews
    • PubEdges (connections to other works)

    This works because all these child models reference the pub via pubId — none have a communityId column, so they automatically follow the pub when Pub.communityId is updated.

  2. Pubs WITHOUT a DOI are hard-deleted via the existing destroyPub() cascade.

  3. All other community data is deleted: Pages, Collections, Members, DepositTargets, etc.

  4. The community row is destroyed last.

The operation requires typing the exact community title to confirm, and runs inside a database transaction (all-or-nothing).

Delete Account

When a user deletes their account:

  1. Attributions are decoupled: the user's fullName, avatar, and orcid are copied into the standalone fields on PubAttribution and CollectionAttribution, then userId is set to NULL. This applies to all attributions regardless of DOI or release status.

  2. Everything else is reassigned to a sentinel system user (_deleted-user, id 00000000-...-000000000000). This keeps all userId columns NOT NULL and avoids any need for NULL-handling in the frontend. The sentinel's fullName is "Deleted User", so joins render that naturally. Affected models:

    • Discussion.userId
    • ThreadComment.userId
    • ReviewNew.userId
    • ThreadEvent.userId
    • ReviewEvent.userId
    • Release.userId
    • CommunityBan.actorId
  3. User-owned data with no scholarly value is explicitly deleted: ZoteroIntegration, VisibilityUser, UserScopeVisit, UserDismissable.

  4. CASCADE handles the rest: Member, AuthToken, EmailChangeToken, UserNotification, UserSubscription, UserNotificationPreferences, FeatureFlagUser.

  5. ActivityItem.actorId has no FK constraint, so it's left as an orphaned UUID. The UI renders missing actor lookups as "Deleted User".

  6. The User row is destroyed last.

The operation requires password confirmation and runs inside a transaction.


Schema Migration

FK constraint changes

Model Column Old behavior New behavior
PubAttribution userId CASCADE SET NULL
CollectionAttribution userId CASCADE SET NULL
Discussion userId CASCADE NO ACTION
ThreadComment userId CASCADE NO ACTION
ThreadEvent userId CASCADE NO ACTION
ReviewEvent userId CASCADE NO ACTION
ReviewNew userId CASCADE NO ACTION
CommunityBan actorId CASCADE NO ACTION

Column changes

None. CommunityBan.actorId stays NOT NULL; it's reassigned to the sentinel user rather than nulled.

Seeded entities

Entity ID Purpose
Deleted User 00000000-0000-0000-0000-000000000000 Sentinel user for NOT NULL userId FKs after account deletion
PubPub Archive community 00000000-0000-0000-0000-000000000001 Home for DOI'd pubs from deleted communities, at archive.pubpub.org

Migration file: tools/migrations/2026_04_13_prepareAccountAndCommunityDeletion.js

Both the migration AND the Sequelize model decorator files were updated to stay in sync.


Files Changed

New files

File Purpose
server/utils/systemEntities.ts Well-known UUIDs for sentinel user and archive community
server/community/destroyCommunity.ts getCommunityDeletionAudit() + destroyCommunity()
server/user/destroyUser.ts getUserDeletionAudit() + destroyUser()
client/containers/DashboardSettings/CommunitySettings/DeleteCommunity.tsx Delete community UI component
client/containers/Legal/DeleteAccount.tsx Delete account UI component
tools/migrations/2026_04_13_prepareAccountAndCommunityDeletion.js DB migration
server/community/__tests__/destroyCommunity.test.ts Integration tests for community deletion
server/user/__tests__/destroyUser.test.ts Integration tests for account deletion
server/doi/updateUrls.ts URL-only DOI updates for Crossref (doDOICitUpload) and DataCite (PUT url)
client/containers/Pub/PubDocument/PubArchiveNotice.tsx Archive community banner for pub pages
client/containers/Pub/PubDocument/pubArchiveNotice.scss Styles for archive banner

Modified model files (onDelete changes)

  • server/pubAttribution/model.ts
  • server/collectionAttribution/model.ts
  • server/discussion/model.ts
  • server/threadComment/model.ts
  • server/threadEvent/model.ts
  • server/reviewEvent/model.ts
  • server/review/model.ts
  • server/communityBan/model.ts
  • server/user/model.ts

Modified API files

  • utils/api/contracts/community.ts -- added deletionAudit and remove routes
  • utils/api/contracts/account.ts -- added deletionAudit and deleteAccount routes
  • server/community/api.ts -- wired deletion audit + remove handlers
  • server/user/account.ts -- wired deletion audit + delete account handlers

Modified UI files

  • client/containers/DashboardSettings/CommunitySettings/CommunitySettings.tsx -- added "Danger zone" tab
  • client/containers/Legal/PrivacySettings.tsx -- replaced mailto link with interactive DeleteAccount component
  • client/containers/Pub/PubDocument/PubDocument.tsx -- renders PubArchiveNotice above the document
  • types/request.ts -- InitialCommunityData extended with isArchiveCommunity flag
  • server/utils/initData.ts -- sets isArchiveCommunity based on community ID

API Endpoints

Community deletion

Method Path Auth Description
GET /api/communities/:id/deletionAudit Community admin Returns counts: total pubs, DOI pubs, non-DOI pubs
DELETE /api/communities/:id Community admin Body: { confirmationTitle } -- must match community title

Account deletion

Method Path Auth Description
GET /api/account/deletionAudit Logged in Returns counts: attributions, discussions, comments, releases, memberships
DELETE /api/account Logged in Body: { password } -- SHA3-hashed password for confirmation

UI Behavior

Delete Community (Dashboard → Settings → Danger zone)

  • Only visible to community admins
  • Shows a pre-flight audit on load (pub counts, DOI breakdown)
  • Explains that DOI pubs will be moved to archive.pubpub.org and non-DOI pubs will be permanently deleted
  • Requires typing the exact community title to enable the delete button
  • Redirects to pubpub.org after deletion

Delete Account (Legal → Privacy Settings)

  • Replaces the old "email us to delete" card
  • Shows a pre-flight audit on load (attribution counts, discussion counts, etc.)
  • Explains that attributions will be preserved with the user's name, and other contributions will show as "Deleted User"
  • Requires entering password to confirm
  • Redirects to pubpub.org after deletion

DOI URL Re-pointing

After community deletion, DOI resolution URLs are updated to point to https://archive.pubpub.org/pub/<slug>. This is a URL-only update that intentionally preserves all other metadata at the registrar (title, authors, collection/issue context, references, etc.).

Why URL-only?

A full redeposit would overwrite the metadata at Crossref/DataCite with whatever PubPub currently has in its database. But the pub may have been registered as part of an Issue collection, with Crossref grouping metadata linking it to other articles in that issue. A full redeposit would lose that context. The URL-only approach changes just the resolution target.

Crossref

Uses the doDOICitUpload operation (bulk URL update format). A tab-separated text file is POSTed to the same DOI_SUBMISSION_URL used for deposits, but with a different operation code:

H: email=crossref@pubpub.org;fromPrefix=10.xxxx
10.xxxx/suffix\thttps://archive.pubpub.org/pub/my-pub

DataCite

Uses PUT /dois/{id} with only the url attribute in the JSON:API payload. All other attributes are omitted, so DataCite leaves them unchanged.

Credential timing

The community's DepositTarget row (which holds encrypted Crossref/DataCite credentials) will be cascade-deleted when the community is destroyed. So destroyCommunity reads the credentials before the transaction, collects the list of DOI URL updates inside the transaction, and fires the external API calls after the commit.

Error handling

DOI URL updates are best-effort. They run sequentially (one DOI at a time) to avoid overwhelming Crossref's submission queue. Failures are logged with a warning listing the affected DOIs, but they don't roll back the community deletion since the transaction has already committed.


Archive Pub Banner

Pubs at archive.pubpub.org display a Blueprint <Callout> banner above the document:

This publication's community has been removed. This page is maintained to preserve the scholarly record.

The PubArchiveNotice component checks communityData.isArchiveCommunity (a boolean flag set by the server in getInitialData()) and renders nothing for pubs in normal communities.

The flag is set server-side in server/utils/initData.ts by comparing the community ID against ARCHIVE_COMMUNITY_ID. This keeps the sentinel UUID on the server and avoids leaking system entity constants to client bundles.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds end-user “Delete Community” and “Delete Account” flows designed to preserve the scholarly record (DOI resolution + authorship attribution) by avoiding destructive cascades and introducing archive/sentinel system entities.

Changes:

  • Added API endpoints and server-side destroy logic for community deletion (archive DOI pubs; delete non-DOI content) and account deletion (preserve attributions; reassign authored content to a sentinel user).
  • Added schema migration to adjust FK onDelete behaviors and seed system entities (deleted-user + archive community).
  • Added UI surfaces for deleting a community (dashboard danger zone) and deleting an account (privacy settings), plus an archive banner on archived pub pages.

Reviewed changes

Copilot reviewed 29 out of 29 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
utils/api/contracts/community.ts Adds community deletion audit + delete routes to the API contract.
utils/api/contracts/account.ts Adds account deletion audit + delete routes to the API contract.
types/request.ts Extends initial community payload with isArchiveCommunity.
tools/migrations/2026_04_13_prepareAccountAndCommunityDeletion.js Alters FK delete behaviors and seeds sentinel/archive entities.
server/utils/systemEntities.ts Centralizes UUIDs for sentinel user and archive community.
server/utils/initData.ts Sets isArchiveCommunity for client rendering logic.
server/user/model.ts Updates user associations’ onDelete behavior to avoid cascades.
server/user/destroyUser.ts Implements account deletion preserving attributions and reassigning authored records.
server/user/account.ts Wires account deletion audit + delete handlers.
server/user/tests/destroyUser.test.ts Integration coverage for account deletion semantics.
server/threadEvent/model.ts Changes user FK behavior to prevent cascade deletion.
server/threadComment/model.ts Changes user FK behavior to prevent cascade deletion.
server/reviewEvent/model.ts Changes user FK behavior to prevent cascade deletion.
server/review/model.ts Changes review author FK behavior to prevent cascade deletion.
server/pubAttribution/model.ts Switches attribution user FK to SET NULL to preserve credit.
server/collectionAttribution/model.ts Switches attribution user FK to SET NULL to preserve credit.
server/discussion/model.ts Changes discussion author FK behavior to prevent cascade deletion.
server/communityBan/model.ts Changes ban actor FK behavior to prevent cascade deletion.
server/doi/updateUrls.ts Adds registrar-specific URL-only DOI update helpers.
server/community/destroyCommunity.ts Implements community deletion (archive DOI pubs + best-effort DOI URL updates).
server/community/api.ts Wires community deletion audit + delete handlers.
server/community/tests/destroyCommunity.test.ts Integration coverage for community deletion semantics.
client/containers/DashboardSettings/CommunitySettings/CommunitySettings.tsx Adds “Danger zone” tab and mounts delete UI.
client/containers/DashboardSettings/CommunitySettings/DeleteCommunity.tsx Implements delete community UI (audit + typed confirmation).
client/containers/Legal/PrivacySettings.tsx Replaces mailto flow with interactive delete account component.
client/containers/Legal/DeleteAccount.tsx Implements delete account UI (audit + password confirmation).
client/containers/Pub/PubDocument/PubDocument.tsx Renders archive notice on pub pages.
client/containers/Pub/PubDocument/PubArchiveNotice.tsx Implements archive banner rendering based on isArchiveCommunity.
client/containers/Pub/PubDocument/pubArchiveNotice.scss Styles for the archive banner.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

isTravis and others added 7 commits April 14, 2026 10:29
@gabestein
Copy link
Copy Markdown
Member

gabestein commented Apr 15, 2026

This is amazing work!

Just to make sure I understand this correctly:

Attributions are decoupled: the user's fullName, avatar, and orcid are copied into the standalone fields on PubAttribution and CollectionAttribution, then userId is set to NULL. This applies to all attributions regardless of DOI or release status.

Essentially, when an author deletes their account, it copies any attributions they had into a shadow user on those pubs, preserving the name etc but allowing the account to be removed? If so, I think it's a very good solution, as it effectively prevents them from being able to remove their name from a record in the case of a disagreement over e.g. a correction or retraction. This is a clear exception to right to be forgotten and other privacy laws in the case of academic content.

The only thing I think we should consider is whether this applies to all pubs or just ones with DOIs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants