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 9b7bc5334e
commit c3db6bfa2b
23 changed files with 1051 additions and 816 deletions

View File

@@ -4,7 +4,7 @@ export const FormationSchema = z.object({
id: z.string(),
title: z.string(),
school: z.string(),
schoolUrl: z.string().url().optional(),
schoolUrl: z.url().optional(),
period: z.string(),
description: z.string().optional(),
});

View File

@@ -23,11 +23,11 @@ export const ProfileSchema = z.object({
.default([]),
}),
socials: z.object({
email: z.string().email().optional(),
github: z.string().url().optional(),
gitea: z.string().url().optional(),
linkedin: z.string().url().optional(),
website: z.string().url().optional(),
email: z.email().optional(),
github: z.url().optional(),
gitea: z.url().optional(),
linkedin: z.url().optional(),
website: z.url().optional(),
}),
cv: z.string().optional(),
});

View File

@@ -8,8 +8,8 @@ export const ProjectSchema = z.object({
image: z.string().optional(),
category: z.enum(['web', 'mobile', 'bot', 'infra', 'other']).default('web'),
status: z.enum(['live', 'archived', 'private', 'wip']).default('live'),
url: z.string().url().optional(),
repoUrl: z.string().url().optional(),
url: z.url().optional(),
repoUrl: z.url().optional(),
stack: z.array(z.string()).default([]),
featured: z.boolean().default(false),
});

View File

@@ -3,7 +3,7 @@ import { z } from 'zod';
export const SiteSchema = z.object({
title: z.string(),
description: z.string(),
url: z.string().url(),
url: z.url(),
keywords: z.array(z.string()).default([]),
ogImage: z.string(),
twitter: z.string().optional(),