fix(review-2): high — read-only ban model, failed-compression cleanup, live SSE eviction
H1: corrected a round-1 over-reach. Bans are read-only per USER_JOURNEYS §10:
the base AuthUser extractor no longer rejects banned users (they keep read
access); Require{Host,Admin} and write handlers enforce the ban via
auth.is_banned → 403 (incl. self-unban). Demoted hosts still lose powers
immediately (live DB role) and are bounced off the dashboard. Also fixed the
login/recover redirect regression on unauthenticated 401.
H2: failed compression now self-heals instead of leaving a permanent broken
card — refund the quota, delete the orphaned original, soft-delete the row;
the frontend toasts the uploader and evicts the card on upload-error.
H3: self-delete, ban-with-hide (user-hidden), and comment-deletes now broadcast
SSE; feed, diashow, and lightbox evict the affected content live instead of
waiting for a reload. sse-eviction.spec.ts covers it.
Riders: feed/+page.svelte also carries the medium truncated-delta pill; host.rs
also carries the low atomic release_gallery claim; admin/+page.svelte also drops
the dead compression_concurrency control (medium).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -97,4 +97,27 @@ test.describe('Host — live role/ban revocation (H1)', () => {
|
||||
api.unbanUser(host.jwt, host.userId, { expectedStatus: [204] })
|
||||
).rejects.toThrow(/→ 403/);
|
||||
});
|
||||
|
||||
// H1 / USER_JOURNEYS §10: a banned user keeps *read* access but every write is
|
||||
// rejected with 403. The ban is enforced on write handlers + Require{Host,Admin},
|
||||
// NOT in the base extractor (which would wrongly block reads too).
|
||||
test('a banned user keeps read access but is blocked from writes', async ({ api, host, guest }) => {
|
||||
const base = process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101';
|
||||
const target = await guest('BannedRW');
|
||||
await api.banUser(host.jwt, target.userId, false);
|
||||
|
||||
// Reads still succeed.
|
||||
const read = await fetch(base + '/api/v1/me/context', {
|
||||
headers: { Authorization: `Bearer ${target.jwt}` },
|
||||
});
|
||||
expect(read.status).toBe(200);
|
||||
|
||||
// Writes are rejected.
|
||||
const write = await fetch(base + '/api/v1/upload', {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${target.jwt}` },
|
||||
body: new FormData(),
|
||||
});
|
||||
expect(write.status).toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
51
e2e/specs/04-host/sse-eviction.spec.ts
Normal file
51
e2e/specs/04-host/sse-eviction.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Regression for the review's H3: self-deleting an upload and banning a user with
|
||||
* hide_uploads mutated visibility server-side but broadcast nothing, so the
|
||||
* content lingered on every other viewer's feed (and the projector diashow) until
|
||||
* a manual reload. Both now emit SSE so clients evict live.
|
||||
*/
|
||||
import { test, expect } from '../../fixtures/test';
|
||||
import { seedUpload } from '../../helpers/seed';
|
||||
import { SseListener } from '../../helpers/sse-listener';
|
||||
|
||||
const BASE = process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101';
|
||||
|
||||
test.describe('Host — live SSE eviction (H3)', () => {
|
||||
test('self-deleting an upload broadcasts upload-deleted', async ({ guest }) => {
|
||||
const g = await guest('SelfDeleter');
|
||||
const uploadId = await seedUpload(g.jwt, { caption: 'delete me' });
|
||||
|
||||
const sse = new SseListener();
|
||||
await sse.start(g.jwt);
|
||||
|
||||
const res = await fetch(`${BASE}/api/v1/upload/${uploadId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${g.jwt}` },
|
||||
});
|
||||
expect(res.status).toBe(204);
|
||||
|
||||
await sse.waitForEvent(
|
||||
'upload-deleted',
|
||||
(e) => e.data.upload_id === uploadId
|
||||
);
|
||||
});
|
||||
|
||||
test('banning a user with hide_uploads broadcasts user-hidden', async ({
|
||||
api,
|
||||
host,
|
||||
guest,
|
||||
}) => {
|
||||
const target = await guest('HideTarget');
|
||||
await seedUpload(target.jwt, { caption: 'should vanish' });
|
||||
|
||||
const sse = new SseListener();
|
||||
await sse.start(host.jwt);
|
||||
|
||||
await api.banUser(host.jwt, target.userId, true);
|
||||
|
||||
await sse.waitForEvent(
|
||||
'user-hidden',
|
||||
(e) => e.data.user_id === target.userId
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user