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

@@ -0,0 +1,94 @@
import { describe, it, expect, vi } from 'vitest';
// Mock config to provide a predictable API key and subpage config
vi.mock('@/lib/config', () => ({
getConfig: () => ({
immich: { apiKey: 'test-api-key-for-auth-tests' },
subpages: [
{
name: 'Private',
slug: 'private',
albumIds: ['00000000-0000-0000-0000-000000000001'],
password: 'secret123',
},
{
name: 'Public',
slug: 'public',
albumIds: ['00000000-0000-0000-0000-000000000002'],
// no password
},
],
}),
}));
import { isProtected, authenticate, isAuthenticated, findSubpageBySlug } from '@/lib/auth';
describe('findSubpageBySlug', () => {
it('returns the subpage config for a known slug', () => {
const sp = findSubpageBySlug('private');
expect(sp).toBeDefined();
expect(sp?.name).toBe('Private');
});
it('returns undefined for an unknown slug', () => {
expect(findSubpageBySlug('nonexistent')).toBeUndefined();
});
});
describe('isProtected', () => {
it('returns true for a password-protected subpage', () => {
expect(isProtected('private')).toBe(true);
});
it('returns false for a non-protected subpage', () => {
expect(isProtected('public')).toBe(false);
});
it('returns false for an unknown slug', () => {
expect(isProtected('unknown')).toBe(false);
});
});
describe('authenticate', () => {
it('returns a Set-Cookie string for the correct password', () => {
const cookie = authenticate('private', 'secret123');
expect(cookie).toBeTruthy();
expect(cookie).toContain('lb_auth_private=');
expect(cookie).toContain('HttpOnly');
expect(cookie).toContain('SameSite=Strict');
});
it('returns null for the wrong password', () => {
expect(authenticate('private', 'wrongpass')).toBeNull();
});
it('returns null for a non-protected slug', () => {
expect(authenticate('public', 'anything')).toBeNull();
});
});
describe('isAuthenticated', () => {
it('returns true when a valid cookie is present', () => {
// First authenticate to get the expected token
const cookie = authenticate('private', 'secret123')!;
const token = cookie.split('=')[1].split(';')[0];
const getCookie = (name: string) => (name === 'lb_auth_private' ? token : undefined);
expect(isAuthenticated('private', getCookie)).toBe(true);
});
it('returns false when no cookie is present', () => {
const getCookie = () => undefined;
expect(isAuthenticated('private', getCookie)).toBe(false);
});
it('returns false for an invalid token', () => {
const getCookie = (name: string) => (name === 'lb_auth_private' ? 'badtoken' : undefined);
expect(isAuthenticated('private', getCookie)).toBe(false);
});
it('returns true for non-protected pages (no auth needed)', () => {
const getCookie = () => undefined;
expect(isAuthenticated('public', getCookie)).toBe(true);
});
});