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:
fabi
2026-07-02 22:19:43 +02:00
parent dba4d3f932
commit 80179357a7
13 changed files with 245 additions and 20 deletions

View File

@@ -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);
});
});