chore: harden security, clean deps, add vitest baseline

- .env.example: vider les secrets (à rotater côté Plesk)
- src/lib/auth.ts: fail-fast lazy si ADMIN_JWT_SECRET < 32 chars
- src/pages/api/upload.ts: retirer .svg (anti-XSS) + try/catch FS
- src/pages/api/content/[section].ts: masquer err.message (sauf ZodError)
- retirer gsap/three/@types/three/@react-three/{fiber,drei}/lucide-react
- bump astro 6.1.10 (GHSA-xr5h-phrj-8vxv)
- zod 4: z.email()/z.url() au lieu de .email()/.url()
- nettoyer vars inutilisées + Header script is:inline

Vitest (38 tests, 615ms):
- tests/lib/auth.test.ts: verifyPassword + verifySession (round-trip, secrets distincts)
- tests/lib/content.test.ts: load/save + comportement seed
- tests/api/files.test.ts: path traversal (../, /, imbriqué) + 200
- tests/api/upload.test.ts: extension/taille/sanitize folder + bucket
- tests/middleware.test.ts: gating admin pages vs APIs vs routes publiques
- astro check: 0 errors / 0 warnings / 0 hints

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Johan LEROY
2026-05-13 16:07:05 +02:00
parent 859683b624
commit 7327959e9e
23 changed files with 1051 additions and 816 deletions

47
tests/lib/auth.test.ts Normal file
View File

@@ -0,0 +1,47 @@
import { describe, it, expect } from 'vitest';
import { SignJWT } from 'jose';
import { createSession, verifySession, verifyPassword } from '../../src/lib/auth';
describe('verifyPassword', () => {
it('accepte le mot de passe correct', async () => {
expect(await verifyPassword('test-password-123')).toBe(true);
});
it('rejette un mauvais mot de passe', async () => {
expect(await verifyPassword('wrong')).toBe(false);
});
it('rejette un mot de passe vide', async () => {
expect(await verifyPassword('')).toBe(false);
});
});
describe('verifySession', () => {
it('rejette un token undefined', async () => {
expect(await verifySession(undefined)).toBe(false);
});
it('rejette un token vide', async () => {
expect(await verifySession('')).toBe(false);
});
it('rejette un token mal formé', async () => {
expect(await verifySession('not.a.jwt')).toBe(false);
});
it('rejette un JWT signé avec un autre secret', async () => {
const otherKey = new TextEncoder().encode('different-secret-of-32-chars-min-aaaa');
const token = await new SignJWT({ role: 'admin' })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(otherKey);
expect(await verifySession(token)).toBe(false);
});
it('round-trip : createSession puis verifySession → true', async () => {
const token = await createSession();
expect(token).toMatch(/^eyJ/);
expect(await verifySession(token)).toBe(true);
});
});