Fix code-review findings on multi-tenant branch

- images.rs: scope /api/img and /api/photos/{id}/original by album owner —
  close the cross-tenant original/thumbnail leak (tenancy test now covers
  these routes)
- migration 0003: refuse to run when albums exist and users != 1 instead of
  silently reassigning every album to the oldest user
- Gallery: callback-ref ResizeObserver so a gallery mounted empty still
  lays out once photos arrive (was permanently blank)
- upload dedup: re-uploading identical content whose photo is in 'error'
  resets and re-enqueues it instead of returning the broken row
- client hashing: skip (and fall back to plain upload) above 512MB to avoid
  whole-file arrayBuffer OOM / the ~2GiB cap
- zip: always spool each entry (no unread prefetched S3 body held across a
  slow client stream) and backfill BOTH sha256 and crc32 for legacy photos
- tests/auth: share one session_payload builder instead of re-implementing
  the cookie format in the test and mint_session
This commit is contained in:
2026-07-17 15:16:25 +02:00
parent 40f7f2fb5e
commit 84323ab637
9 changed files with 149 additions and 78 deletions
+20 -1
View File
@@ -44,10 +44,12 @@ fn test_config(database_url: String) -> Config {
fn session_for(user_id: Uuid, email: &str) -> String {
let key = Key::from(&Sha512::digest(SECRET.as_bytes()));
let exp = chrono::Utc::now().timestamp() + 3600;
// Use the production payload builder so the test can't drift from what
// user_from_jar parses.
let mut jar = CookieJar::new();
jar.signed_mut(&key).add(Cookie::new(
"photos_session",
format!("{user_id}|{exp}|{email}"),
photos::auth::session_payload(user_id, email, exp),
));
format!("photos_session={}", jar.get("photos_session").unwrap().value())
}
@@ -184,6 +186,23 @@ async fn tenant_isolation_matrix() {
assert_eq!(status, StatusCode::OK);
assert_eq!(list.as_array().unwrap().len(), 0, "bob must see no albums");
// ---- Dual-auth image routes must ALSO reject a non-owning photographer.
// Bob owns no share and doesn't own the album, so authorize_photo falls
// through to the share-cookie path and 401s BEFORE any S3 access — the
// cross-tenant original leak these routes previously allowed. ----
for path in [
format!("/api/img/{photo_id}/thumb"),
format!("/api/img/{photo_id}/preview"),
format!("/api/photos/{photo_id}/original"),
] {
let (status, _) = request(&router, "GET", &path, Some(&cookie_b), None).await;
assert_eq!(
status,
StatusCode::UNAUTHORIZED,
"cross-tenant image GET {path} must not authorize"
);
}
// ---- Alice keeps full access to her own resources. ----
let (status, list) = request(&router, "GET", "/api/albums", Some(&cookie_a), None).await;
assert_eq!(status, StatusCode::OK);