fix: enforce gallery access and consolidate gallery workflows (#1357)

Harden gallery authentication and authorization, consolidate gallery workflows, and prevent token-bearing URLs from leaking through nginx request error logs.
This commit is contained in:
Paul Nothaft
2026-09-08 15:34:09 +02:00
committed by GitHub
parent 895e5ab3cc
commit f0e6d2dfb1
120 changed files with 7147 additions and 8525 deletions
@@ -0,0 +1,15 @@
import { act, renderHook } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { usePhotoSelection } from '../usePhotoSelection';
describe('photo selection through changing query results', () => {
it('handles empty → populated → reordered → removed → empty without changing hook order', () => {
const { result, rerender } = renderHook(({ photos }) => usePhotoSelection(photos, 1), { initialProps: { photos: [] as { id: number }[] } });
expect(result.current.currentPhoto).toBeUndefined();
rerender({ photos: [{ id: 1 }, { id: 2 }, { id: 3 }] }); expect(result.current.currentPhoto?.id).toBe(2);
rerender({ photos: [{ id: 3 }, { id: 2 }, { id: 1 }] }); expect(result.current.currentPhoto?.id).toBe(2);
act(() => result.current.setCurrentIndex(2)); expect(result.current.currentPhoto?.id).toBe(1);
rerender({ photos: [{ id: 3 }, { id: 2 }] }); expect(result.current.currentPhoto?.id).toBe(2);
rerender({ photos: [] }); expect(result.current.currentPhoto).toBeUndefined();
rerender({ photos: [{ id: 2 }, { id: 4 }] }); expect(result.current.currentPhoto?.id).toBe(2);
});
});
+1 -1
View File
@@ -26,7 +26,7 @@ export const useGalleryPhotos = (
return useQuery({
queryKey: ['gallery-photos', slug, filter, guestId],
// Pass guestId so backend can filter per-guest views when needed
queryFn: () => galleryService.getGalleryPhotos(slug, filter, guestId),
queryFn: ({ signal }) => galleryService.getGalleryPhotos(slug, filter, guestId, signal),
enabled,
retry: 1,
staleTime: 5 * 60 * 1000, // 5 minutes
+26
View File
@@ -0,0 +1,26 @@
import { useCallback, useEffect, useState, type SetStateAction } from 'react';
/** Keep the selected photo through reordering/refetches; clamp after removal. */
export function usePhotoSelection<T extends { id: number }>(photos: T[], initialIndex = 0) {
const [selection, setSelection] = useState(() => ({ id: photos[initialIndex]?.id, index: initialIndex }));
const found = photos.findIndex(photo => photo.id === selection.id);
const currentIndex = found >= 0 ? found : Math.max(0, Math.min(selection.index, photos.length - 1));
const currentPhoto = photos[currentIndex];
useEffect(() => {
if (currentPhoto && (currentPhoto.id !== selection.id || currentIndex !== selection.index)) {
setSelection({ id: currentPhoto.id, index: currentIndex });
}
}, [currentPhoto, currentIndex, selection.id, selection.index]);
const setCurrentIndex = useCallback((next: SetStateAction<number>) => {
setSelection(previous => {
const previousIndex = photos.findIndex(photo => photo.id === previous.id);
const index = previousIndex >= 0 ? previousIndex : Math.max(0, Math.min(previous.index, photos.length - 1));
const requested = typeof next === 'function' ? next(index) : next;
const clamped = Math.max(0, Math.min(requested, photos.length - 1));
return { id: photos[clamped]?.id, index: clamped };
});
}, [photos]);
return { currentPhoto, currentIndex, setCurrentIndex };
}