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:
@@ -87,15 +87,17 @@ pub fn spawn_export_jobs(
|
||||
event_name: String,
|
||||
pool: PgPool,
|
||||
media_path: PathBuf,
|
||||
export_path: PathBuf,
|
||||
sse_tx: broadcast::Sender<SseEvent>,
|
||||
) {
|
||||
let pool2 = pool.clone();
|
||||
let media_path2 = media_path.clone();
|
||||
let export_path2 = export_path.clone();
|
||||
let sse_tx2 = sse_tx.clone();
|
||||
let event_name2 = event_name.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = run_zip_export(event_id, &pool, &media_path, &sse_tx).await {
|
||||
if let Err(e) = run_zip_export(event_id, &pool, &media_path, &export_path, &sse_tx).await {
|
||||
tracing::error!("ZIP export failed for event {event_id}: {e:#}");
|
||||
mark_failed(&pool, event_id, "zip", &e.to_string()).await;
|
||||
}
|
||||
@@ -104,7 +106,7 @@ pub fn spawn_export_jobs(
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) =
|
||||
run_html_export(event_id, &event_name2, &pool2, &media_path2, &sse_tx2).await
|
||||
run_html_export(event_id, &event_name2, &pool2, &media_path2, &export_path2, &sse_tx2).await
|
||||
{
|
||||
tracing::error!("HTML export failed for event {event_id}: {e:#}");
|
||||
mark_failed(&pool2, event_id, "html", &e.to_string()).await;
|
||||
@@ -119,6 +121,7 @@ async fn run_zip_export(
|
||||
event_id: Uuid,
|
||||
pool: &PgPool,
|
||||
media_path: &Path,
|
||||
export_path: &Path,
|
||||
sse_tx: &broadcast::Sender<SseEvent>,
|
||||
) -> Result<()> {
|
||||
mark_running(pool, event_id, "zip").await;
|
||||
@@ -126,7 +129,8 @@ async fn run_zip_export(
|
||||
let uploads = query_uploads(pool, event_id).await?;
|
||||
let total = uploads.len().max(1) as f32;
|
||||
|
||||
let exports_dir = media_path.join("exports");
|
||||
// Written OUTSIDE media_path: the public /media ServeDir must never reach these.
|
||||
let exports_dir = export_path.to_path_buf();
|
||||
tokio::fs::create_dir_all(&exports_dir).await?;
|
||||
|
||||
let tmp_path = exports_dir.join("Gallery.zip.tmp");
|
||||
@@ -193,6 +197,7 @@ async fn run_html_export(
|
||||
event_name: &str,
|
||||
pool: &PgPool,
|
||||
media_path: &Path,
|
||||
export_path: &Path,
|
||||
sse_tx: &broadcast::Sender<SseEvent>,
|
||||
) -> Result<()> {
|
||||
mark_running(pool, event_id, "html").await;
|
||||
@@ -205,7 +210,8 @@ async fn run_html_export(
|
||||
|
||||
update_progress(pool, event_id, "html", 5).await;
|
||||
|
||||
let exports_dir = media_path.join("exports");
|
||||
// Written OUTSIDE media_path: the public /media ServeDir must never reach these.
|
||||
let exports_dir = export_path.to_path_buf();
|
||||
tokio::fs::create_dir_all(&exports_dir).await?;
|
||||
|
||||
// 2. Create temp directory for media processing
|
||||
|
||||
Reference in New Issue
Block a user