feat(account): unlink game accounts on the shard before an account is deleted (MODULE_API 1.20.0)
All checks were successful
PR Checks / server-tests (pull_request) Successful in 1m0s
PR Checks / client-build (pull_request) Successful in 1m0s
PR Checks / frozen-manifest (pull_request) Successful in 1m20s

ACCOUNT_DELETION.md §4.4 (AD10, AD11). api.onUserDeleting queues every linked
account in the new shard_pending_unlinks table, then calls the sidecar's
DELETE /link/{account} (actor web:account-deletion) and drops the mirror on 200/404.
A shard that is away leaves the row pending, retried on WS reconnect and on a live
server.hello; a protected account (403) is refused and listed on Admin -> Shard as
still linked on the shard. A live mob.login without the deleted user's webId clears
the row. coreApi ^1.20.0; core-ref pinned to website#232's merge (7bd93b8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-29 20:38:33 -05:00
parent 6246383962
commit 582a55dde5
16 changed files with 630 additions and 9 deletions

View File

@@ -11,11 +11,15 @@ const uoLinkClient = require('../../utils/uoLinkClient')
const uoLinkSocket = require('../../utils/uoLinkSocket')
const shardBroadcast = require('../../utils/shardBroadcast')
const gameSignup = require('../../utils/gameSignup')
const accountDeletion = require('../../utils/accountDeletion')
const { activity } = require('../../core')
const log = require('../../core').logger('admin-uolink')
// Assemble the masked config + live health + ingestion stats for the panel.
// Assemble the masked config + live health + ingestion stats for the panel, and
// the game accounts a website account's deletion left linked on the shard
// (ACCOUNT_DELETION.md §4.4): `pending` waits for the shard, `refused` is a
// protected staff account the shard's staff must clear by hand.
async function buildStatus() {
const config = await uoLinkConfig.getSafe()
const health = await uoLinkClient.health()
@@ -24,6 +28,7 @@ async function buildStatus() {
health: health.ok ? health.data : { ok: false, error: health.error || `status ${health.status}` },
ingest: uoLinkSocket.getState(),
sse: shardBroadcast.stats(),
pendingUnlinks: await accountDeletion.listForAdmin(),
}
}

View File

@@ -35,7 +35,8 @@ uoLinkRouter.get(
// #swagger.tags = ['Admin · Shard']
// #swagger.summary = 'Get uo-link config + live status + ingestion stats (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.responses[200] = { description: 'Masked config, health and ingestion stats', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
// #swagger.description = 'pendingUnlinks lists the game accounts a website account deletion left linked on the shard: state pending (the shard was away; retried when the bridge reconnects) or refused (a protected staff account; shard staff clear it by hand). A row goes once the shard reports a login on the account without the deleted user id.'
/* #swagger.responses[200] = { description: 'Masked config, health, ingestion stats and pendingUnlinks', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
adminOnly,
uoLink.getConfig,