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

@@ -2,6 +2,7 @@
import { onDestroy } from 'svelte';
import type { FeedUpload } from '$lib/types';
import { api } from '$lib/api';
import { onSseEvent } from '$lib/sse';
import { getUserId } from '$lib/auth';
import { dataMode, pickMediaUrl } from '$lib/data-mode-store';
import { doubletap } from '$lib/actions/doubletap';
@@ -48,8 +49,18 @@
burstTimer = setTimeout(() => (heartBurst = false), 700);
}
// Drop a comment live when it's deleted elsewhere (host moderation or the
// author on another device), so the open panel doesn't show a ghost comment.
const unsubCommentDeleted = onSseEvent('comment-deleted', (data) => {
try {
const { comment_id } = JSON.parse(data) as { comment_id: string };
comments = comments.filter((c) => c.id !== comment_id);
} catch { /* ignore */ }
});
onDestroy(() => {
if (burstTimer) clearTimeout(burstTimer);
unsubCommentDeleted();
});
// Only refetch when a *different* upload is shown. The feed reassigns the
@@ -74,7 +85,7 @@
if (!newComment.trim()) return;
loading = true;
try {
const comment = await api.post<CommentDto>(`/upload/${upload.id}/comment`, {
const comment = await api.post<CommentDto>(`/upload/${upload.id}/comments`, {
body: newComment.trim()
});
comments = [...comments, comment];