chore: production readiness audit

- Move plans/ to dev-docs/, delete unused scaffold SVGs
- Create .dockerignore, rename container to immich-folio
- Extract BackLink component and toPhotoItems() helper in page.tsx
- Add keyboard accessibility (role=button, tabIndex, onKeyDown) to PhotoGrid
- Add aria-label to Lightbox EXIF toggle
- Fix React lint: replace setState-in-effect with useState initializer
- Add rate limiting to /api/auth (brute-force protection, 10 RPM)
- Add HSTS and X-DNS-Prefetch-Control security headers
- Add Secure flag to auth cookie in production
- New unit tests: rate-limit.test.ts (5), auth.test.ts (12)
- All 47 tests pass, 0 lint errors, build clean
This commit is contained in:
ralksta
2026-02-15 11:23:51 +01:00
parent 3d71e9ffb7
commit a0ee0ff433
26 changed files with 314 additions and 786 deletions

View File

@@ -8,7 +8,7 @@
'use client';
import { useState, useCallback, useEffect, useRef } from 'react';
import { useState, useCallback, useEffect } from 'react';
import Image from 'next/image';
import { Lightbox } from '@/components/Lightbox';
import { FadeIn } from '@/components/FadeIn';
@@ -50,19 +50,11 @@ function buildPhotoHash(index: number): string {
}
export function PhotoGrid({ assets, layout = 'masonry', gridStyle }: PhotoGridProps) {
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
const isInitialized = useRef(false);
// ── On mount: check URL hash for a deep-link ──────────────────
useEffect(() => {
if (isInitialized.current) return;
isInitialized.current = true;
const [lightboxIndex, setLightboxIndex] = useState<number | null>(() => {
if (typeof window === 'undefined') return null;
const idx = parsePhotoHash(window.location.hash);
if (idx !== null && idx < assets.length) {
setLightboxIndex(idx);
}
}, [assets.length]);
return idx !== null && idx < assets.length ? idx : null;
});
// ── Sync URL hash when lightbox state changes ─────────────────
useEffect(() => {
@@ -90,14 +82,11 @@ export function PhotoGrid({ assets, layout = 'masonry', gridStyle }: PhotoGridPr
return () => window.removeEventListener('hashchange', handleHashChange);
}, [assets.length]);
const openLightbox = useCallback(
(index: number) => {
setLightboxIndex(index);
// Push state (not replace) so the user can press Back to close
window.history.pushState(null, '', buildPhotoHash(index));
},
[],
);
const openLightbox = useCallback((index: number) => {
setLightboxIndex(index);
// Push state (not replace) so the user can press Back to close
window.history.pushState(null, '', buildPhotoHash(index));
}, []);
const closeLightbox = useCallback(() => {
setLightboxIndex(null);
@@ -149,6 +138,15 @@ export function PhotoGrid({ assets, layout = 'masonry', gridStyle }: PhotoGridPr
<div
className="photo-grid__item"
onClick={() => openLightbox(index)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
openLightbox(index);
}
}}
role="button"
tabIndex={0}
aria-label={`View photo ${index + 1}`}
style={{
...(asset.dominantColor ? { backgroundColor: asset.dominantColor } : {}),
...(layout === 'masonry' && asset.aspectRatio