fix(review-2): critical — repair comment posting + close export data leak

CR1: the lightbox comment button POSTed to /upload/{id}/comment (singular);
     no such route exists, so every UI-submitted comment 404'd and was lost.
     Fixed to /comments (plural). The prior e2e passed because its seed helper
     POSTs the API directly — added comment-ui.spec.ts which drives the real
     component so this can't regress silently again.

CR2: export archives (Gallery.zip / Memories.zip / HTML viewer) were written
     under media_path, which is a public ServeDir — so GET /media/exports/
     Gallery.zip served the entire event (every photo, caption, comment,
     uploader name) to any anonymous visitor at a guessable URL, bypassing the
     ticket + release gate. Moved exports to a separate EXPORT_PATH (=/exports)
     on its own volume (Dockerfile chown, compose volume, gated handler reads
     the new path). export-leak.spec.ts asserts /media/exports/Gallery.zip → 404.

Riders (git can't split hunks): LightboxModal also gains H3 live-eviction on
comment-deleted/upload-deleted; config.rs also wires compression_concurrency
from boot config (medium).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
fabi
2026-07-02 22:19:32 +02:00
parent 239459bb91
commit dba4d3f932
8 changed files with 124 additions and 9 deletions

View File

@@ -0,0 +1,22 @@
/**
* Regression for the review's CR2: export archives (Gallery.zip / Memories.zip)
* were written under media_path/exports, and /media is a public ServeDir — so
* anyone could GET /media/exports/Gallery.zip and download the whole gallery,
* bypassing the ticket + export_*_ready gate. Exports now live OUTSIDE media_path
* and are reachable only via the gated /api/v1/export/{zip,html} handlers (covered
* by export.spec.ts). Here we assert the public path is dead.
*/
import { test, expect } from '../../fixtures/test';
const BASE = process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101';
test.describe('Export — no public leak (CR2)', () => {
test('archives are not reachable through the public /media path', async () => {
for (const name of ['Gallery.zip', 'Memories.zip']) {
const res = await fetch(`${BASE}/media/exports/${name}`);
// 404 (not 200): a 200 here would mean the whole-gallery archive is
// downloadable without any auth — the CR2 data-exposure regression.
expect(res.status).toBe(404);
}
});
});