fix(pad): don't issue a deletion token (or show its modal) when allowPadDeletionByAllUsers is on (#7929)

When `allowPadDeletionByAllUsers` is true, anyone can delete a pad with no
token at all (handlePadDelete's flagOk branch), so the one-time deletion
token is pointless and the "Save your pad deletion token" modal only
overwhelms users who will never need it.

Gate token issuance on `!settings.allowPadDeletionByAllUsers` so no token
reaches clientVars; the client's showDeletionTokenModalIfPresent() then
returns early and the modal never appears. No new setting — it derives
automatically from the existing one.

Closes #7926.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
John McLear 2026-06-10 09:55:13 +01:00 committed by GitHub
parent 1e7c4b8c21
commit f47626049c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 53 additions and 4 deletions

View file

@ -1287,9 +1287,15 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
// once. Readonly sessions never see it.
const isCreator =
!sessionInfo.readonly && sessionInfo.author === await pad.getRevisionAuthor(0);
// Skip token issuance when requireAuthentication is on: every creator has a
// stable identity so the cookie/identity path is sufficient.
const padDeletionToken = isCreator && !settings.requireAuthentication
// Skip token issuance — and so the client never shows the "Save your pad
// deletion token" modal (issue #7926) — when the token cannot help:
// - requireAuthentication: every creator already has a stable identity, so
// the cookie/identity path is sufficient.
// - allowPadDeletionByAllUsers: anyone can delete the pad with no token at
// all (see handlePadDelete's flagOk branch), so a recovery token is noise
// and the modal only overwhelms users who will never need it.
const padDeletionToken =
isCreator && !settings.requireAuthentication && !settings.allowPadDeletionByAllUsers
? await padDeletionManager.createDeletionTokenIfAbsent(sessionInfo.padId)
: null;

View file

@ -34,7 +34,7 @@ describe(__filename, function () {
plugins.hooks[hookName] = [];
}
backups.settings = {};
for (const setting of ['editOnly', 'requireAuthentication', 'requireAuthorization', 'users', 'enablePadWideSettings']) {
for (const setting of ['editOnly', 'requireAuthentication', 'requireAuthorization', 'users', 'enablePadWideSettings', 'allowPadDeletionByAllUsers']) {
// @ts-ignore
backups.settings[setting] = settings[setting];
}
@ -492,6 +492,49 @@ describe(__filename, function () {
});
});
describe('Pad deletion token issuance (#7926)', function () {
const removeIfExists = async (padId: string) => {
if (await padManager.doesPadExist(padId)) {
const p = await padManager.getPad(padId);
await p.remove();
}
};
beforeEach(async function () {
// @ts-ignore - public setting toggled per test
settings.allowPadDeletionByAllUsers = false;
await removeIfExists('pad');
});
afterEach(async function () {
if (socket) socket.close();
socket = null;
await removeIfExists('pad');
});
it('anonymous creator receives a deletion token by default', async function () {
const res = await agent.get('/p/pad').expect(200);
socket = await common.connect(res);
const cv: any = await common.handshake(socket, 'pad');
assert.equal(cv.type, 'CLIENT_VARS');
assert.equal(typeof cv.data.padDeletionToken, 'string',
'creator should get a token so the client can show the save-token modal');
assert.ok(cv.data.padDeletionToken.length >= 32);
});
it('no token (and so no modal) when allowPadDeletionByAllUsers is true', async function () {
// @ts-ignore - public setting
settings.allowPadDeletionByAllUsers = true;
const res = await agent.get('/p/pad').expect(200);
socket = await common.connect(res);
const cv: any = await common.handshake(socket, 'pad');
assert.equal(cv.type, 'CLIENT_VARS');
// A null token means showDeletionTokenModalIfPresent() returns early on the
// client, so the "Save your pad deletion token" modal never appears. Anyone
// can already delete the pad without a token in this configuration.
assert.equal(cv.data.padDeletionToken, null);
});
});
describe('SocketIORouter.js', function () {
const Module = class {
setSocketIO(io:any) {}