feat(account): self-service account deletion, MODULE_API 1.20.0
ACCOUNT_DELETION.md phase 2 (core): a member deletes their own account on
the website, and the admin route runs the same procedure.
- DELETE /api/v1/auth/me/account: typed username, password always (AD7),
and a 2FA or recovery code when 2FA is on. A wrong password or code is
400, not 401 (AD12), so the app's refresh-on-401 never replays it.
409 password_required and last_admin; the demo's shared logins are
locked.
- auth/accountDeletion.js: module hooks first, then one transaction that
reserves a staff username (reserved_usernames, AD9) and deletes the row,
then a Teams reconcile, the audit entry (no username) and a notice to
the old address (new protected template auth.account-deleted).
- MODULE_API 1.20.0: api.onUserDeleting({ userId, deletedBy }), before the
delete, 10 s limit, cannot veto (AD10, AD11).
- utils/displayAuthor.js: a deleted account's username snapshots read
"[deleted player]" to members and the stored name, marked, to staff
(AD5, AD6). Used by the forum, the Reports queue, the guest list and
the Team digest. A deleted account drops off Forum guests (AD13).
- Reserved names refuse at every username write, answered as "taken".
- Web: Delete my account on the player and staff Account pages; staff
views mark "(deleted account)".
- OpenAPI regenerated; route manifest and engagement manifest regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -82,6 +82,9 @@ async function resolveForum(req) {
|
||||
async function viewerFor(ctx, user) {
|
||||
return {
|
||||
userId: user.id,
|
||||
// The account role, for whose name a deleted author shows as (displayAuthor):
|
||||
// an editor is not forum staff and still sees it (AD6).
|
||||
role: user.role,
|
||||
isStaff: ctx.staff,
|
||||
windowMinutes: await forumSettings.editWindowMinutes(),
|
||||
}
|
||||
@@ -122,7 +125,7 @@ async function listThreads(req, res) {
|
||||
const ctx = await resolveForum(req)
|
||||
if (!ctx) return res.status(404).json({ message: 'Not found' })
|
||||
return res.json({
|
||||
threads: await forum.listThreads(ctx.team.id, { canModerate: ctx.canModerate }),
|
||||
threads: await forum.listThreads(ctx.team.id, { canModerate: ctx.canModerate, viewer: { role: req.user.role } }),
|
||||
// Two capabilities, not one. Phase 4 had a single `canPost` because there
|
||||
// was a single kind of thread to post; phase 5 opened discussion to every
|
||||
// participant while announcements stayed with the leaders, so a client that
|
||||
@@ -334,7 +337,7 @@ async function listGrants(req, res) {
|
||||
const authority = await grants.authorityFor(team.id, req.user)
|
||||
if (!authority.may) return res.status(403).json({ message: 'Not a leader of this Team' })
|
||||
return res.json({
|
||||
guests: await grants.forumGuests(team.id),
|
||||
guests: await grants.forumGuests(team.id, req.user),
|
||||
cap: await grants.grantCap(),
|
||||
as: authority.as,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user