Files
EventSnap/frontend/src/lib/data-mode-store.ts
fabi a490642f5f Merge fix/security-review-batch-2: cross-event authz + upload OOM backstop
Brings the batch-2 security hardening into main (forked from the same base as
the UX batch; auto-merged cleanly — verified both sides' edits to social.rs /
main.rs coexist):

- Cross-event authorization: toggle_like / list_comments / add_comment now
  event-scope the upload via Upload::find_by_id_and_event (404 on cross-event
  access); delete_comment uses Comment::soft_delete_in_event. Closes the gap
  where a guest could like/comment/list across events by upload UUID.
- Upload OOM backstop: the /upload route gets DefaultBodyLimit::max(576 MiB)
  instead of disable(), so a multi-GB body can't be buffered before the
  handler's per-class size checks run.
- upload.rs per-class size-limit refactor, XSS allowlist, deploy hardening
  (Caddyfile, Dockerfiles, docker-compose, .env.example), and a data-mode
  doc-comment clarifying the original-media route is capability-(UUID-)gated.

The event-scope checks sit before, and batch-3's best-effort count broadcasts
after, the like/comment mutations — both preserved.

Verified: cargo build clean, svelte-check 0 errors.
2026-06-30 20:02:47 +02:00

59 lines
2.0 KiB
TypeScript

// Per-device "Datenmodus" — Saver loads compressed previews (default), Original loads
// the full file via the `/api/v1/upload/{id}/original` endpoint. That route is
// intentionally unauthenticated (the UUID acts as the capability) so it works from
// plain `<img src>` / `<video src>` without an Authorization header.
//
// Stored per-device in localStorage (not per-user) because data plans are a property
// of the device the guest is currently holding, not their identity.
//
// Used by:
// - Feed cards (FeedListCard / VirtualFeed grid tiles) to pick which URL to render
// - Lightbox
// - Diashow
// See [docs/FEATURES.md §2.5] for the user-facing model.
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
export type DataMode = 'saver' | 'original';
const KEY = 'eventsnap_data_mode';
const DEFAULT: DataMode = 'saver';
function readInitial(): DataMode {
if (!browser) return DEFAULT;
const raw = localStorage.getItem(KEY);
return raw === 'original' || raw === 'saver' ? raw : DEFAULT;
}
export const dataMode = writable<DataMode>(readInitial());
if (browser) {
dataMode.subscribe((value) => {
try {
localStorage.setItem(KEY, value);
} catch {
// localStorage may be unavailable (Safari private mode); ignore.
}
});
}
/**
* Build the URL for a feed upload given the current data mode and the URL variants
* the backend returned. Centralised so every consumer (cards, lightbox, diashow)
* follows the same fallback rule:
* Original mode → original API route. Falls back to preview if no upload id is
* available (defensive — shouldn't happen in practice).
* Saver mode → preview URL (compressed), falling back to thumbnail and then
* original.
*/
export function pickMediaUrl(
mode: DataMode,
upload: { id: string; preview_url: string | null; thumbnail_url: string | null }
): string {
if (mode === 'original') {
return `/api/v1/upload/${upload.id}/original`;
}
return upload.preview_url ?? upload.thumbnail_url ?? `/api/v1/upload/${upload.id}/original`;
}