web: moderate accounts, accept 12MB uploads, put SAVE under CREATE
- /admin User account rows gain BLOCK/UNBLOCK, REMOVE/RESTORE and DELETE. Blocked = cannot sign in (sessions swept), removed = hidden from the strip and cannot sign in, both reversible; DELETE drops the account with its photos and recipes and unlinks the files. An allowlisted account is never a target, so an admin cannot moderate or delete itself. - Photo uploads move from a 3MB API cap / 4m nginx cap to 12MB / 16m, and the browser shrinks an oversized still before sending it (2048px JPEG, avatars 512px) so the declared type still matches the sniffed bytes. - The studio SAVE leaves the top bar and sits under the CREATE RECIPES tab, labelled SAVE RECIPES.
This commit is contained in:
@@ -187,7 +187,7 @@ try {
|
||||
);
|
||||
check('empty body is refused', (await user.upload(Buffer.alloc(0), 'image/png')).status === 400);
|
||||
|
||||
const tooBig = Buffer.concat([PNG, Buffer.alloc(4 * 1024 * 1024)]);
|
||||
const tooBig = Buffer.concat([PNG, Buffer.alloc(13 * 1024 * 1024)]);
|
||||
check('an over-limit body is refused', (await user.upload(tooBig, 'image/png')).status === 413);
|
||||
|
||||
const created = await user.upload(PNG, 'image/png');
|
||||
@@ -345,6 +345,57 @@ try {
|
||||
const memberRow = listedUsers.find((u) => u.id === memberId);
|
||||
check('the user list carries each account’s picture', memberRow?.avatar === replacedUrl, JSON.stringify(memberRow));
|
||||
|
||||
// ---- moderation: block, remove, delete an account -----------------------
|
||||
const target = actor();
|
||||
await target.signup(`moderated${stamp}@test.local`);
|
||||
const targetId = (await target.req('/auth/me')).body?.user?.id;
|
||||
const targetPhoto = (await target.upload(PNG, 'image/png')).body?.photo;
|
||||
const targetPhotoUrl = `http://127.0.0.1:${PORT}/api/photos/${targetPhoto?.id}/file`;
|
||||
const moderate = (a, id, body) => a.req(`/admin/users/${id}`, { method: 'PATCH', headers: JSON_HDR, body: JSON.stringify(body) });
|
||||
check('a fresh account is moderatable', Number.isInteger(targetId) && Number.isInteger(targetPhoto?.id), `${targetId}/${targetPhoto?.id}`);
|
||||
|
||||
const anonModerate = await moderate(actor(), targetId, { blocked: true });
|
||||
check('moderating needs a session', anonModerate.status === 401, `got ${anonModerate.status}`);
|
||||
const memberModerate = await moderate(member, targetId, { blocked: true });
|
||||
check('a member cannot moderate', memberModerate.status === 403, `got ${memberModerate.status}`);
|
||||
const selfModerate = await moderate(admin, adminRow?.id, { blocked: true });
|
||||
check('an allowlisted account cannot be moderated', selfModerate.status === 403, `got ${selfModerate.status}`);
|
||||
const ghostModerate = await moderate(admin, 999999, { blocked: true });
|
||||
check('an unknown account is a 404', ghostModerate.status === 404, `got ${ghostModerate.status}`);
|
||||
const badModerate = await moderate(admin, targetId, { blocked: 'yes' });
|
||||
check('a moderation body is type-checked', badModerate.status === 400, `got ${badModerate.status}`);
|
||||
|
||||
const blocked = await moderate(admin, targetId, { blocked: true });
|
||||
check('an admin blocks an account', blocked.status === 200 && blocked.body?.user?.blocked === true, JSON.stringify(blocked.body));
|
||||
const blockedLogin = await login(`moderated${stamp}@test.local`, 'supersecret1');
|
||||
check('a blocked account cannot sign in', blockedLogin.status === 403 && blockedLogin.body?.error === 'account blocked', JSON.stringify(blockedLogin.body));
|
||||
check('a blocked session stops being a user', (await target.req('/auth/me')).body?.user === null, JSON.stringify((await target.req('/auth/me')).body));
|
||||
const blockedRow = ((await admin.req('/admin/users')).body?.users ?? []).find((u) => u.id === targetId);
|
||||
check('the list flags a blocked account', blockedRow?.blocked === true, JSON.stringify(blockedRow));
|
||||
|
||||
const unblocked = await moderate(admin, targetId, { blocked: false });
|
||||
check('an admin unblocks an account', unblocked.status === 200 && unblocked.body?.user?.blocked === false, JSON.stringify(unblocked.body));
|
||||
check('an unblocked account signs in again', (await login(`moderated${stamp}@test.local`, 'supersecret1')).status === 200);
|
||||
check('its photo is back on the strip', ((await actor().req('/photos')).body?.photos ?? []).some((p) => p.id === targetPhoto?.id));
|
||||
|
||||
const removed = await moderate(admin, targetId, { removed: true });
|
||||
check('an admin removes an account', removed.status === 200 && removed.body?.user?.removed === true, JSON.stringify(removed.body));
|
||||
const removedLogin = await login(`moderated${stamp}@test.local`, 'supersecret1');
|
||||
check('a removed account cannot sign in', removedLogin.status === 403 && removedLogin.body?.error === 'account removed', JSON.stringify(removedLogin.body));
|
||||
check('a removed account’s photos leave the strip', !((await actor().req('/photos')).body?.photos ?? []).some((p) => p.id === targetPhoto?.id));
|
||||
const removedRow = ((await admin.req('/admin/users')).body?.users ?? []).find((u) => u.id === targetId);
|
||||
check('the list flags a removed account', removedRow?.removed === true, JSON.stringify(removedRow));
|
||||
|
||||
const restored = await moderate(admin, targetId, { removed: false });
|
||||
check('an admin restores an account', restored.status === 200 && restored.body?.user?.removed === false, JSON.stringify(restored.body));
|
||||
check('a restored account signs in again', (await login(`moderated${stamp}@test.local`, 'supersecret1')).status === 200);
|
||||
|
||||
const hardDelete = await admin.req(`/admin/users/${targetId}`, { method: 'DELETE' });
|
||||
check('an admin deletes an account outright', hardDelete.status === 204, `got ${hardDelete.status}`);
|
||||
check('a deleted account leaves the list', !((await admin.req('/admin/users')).body?.users ?? []).some((u) => u.id === targetId));
|
||||
check('a deleted account cannot sign in', (await login(`moderated${stamp}@test.local`, 'supersecret1')).status === 401);
|
||||
check('a deleted account’s photo file is unlinked', (await fetch(targetPhotoUrl)).status === 404);
|
||||
|
||||
// ---- pre-existing guarantees still hold ---------------------------------
|
||||
const foreignRecipe = await user.req('/recipes/1', { method: 'DELETE' });
|
||||
check("another account's recipe is not deletable", foreignRecipe.status === 404, `got ${foreignRecipe.status}`);
|
||||
|
||||
Reference in New Issue
Block a user