mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-21 06:08:04 +02:00
Durcissement : mot de passe actuel, sessions révocables, dépendances, CSP, tests
Suite de l'audit : les cinq points laissés en suspens sont traités. Mot de passe - `changeOwnPassword` exige désormais le mot de passe actuel. Un poste laissé ouvert ne suffit plus à s'approprier un compte. Les erreurs reviennent sur l'écran avec un message au lieu d'une page d'erreur brute. Sessions révocables - Colonne `users.session_version`, portée dans le jeton et comparée à la base. - `getSession()` (src/lib/session.ts) remplace `auth()` sur les 20 pages et actions : rôle, rattachement adhérent et existence du compte sont relus à chaque requête. Supprimer un compte ou réinitialiser un mot de passe coupe immédiatement les sessions ouvertes, sans attendre l'expiration du jeton. Dépendances — de 8 vulnérabilités (2 critiques) à zéro - next 15.5.19 → 15.5.21, next-auth beta.25 → beta.32, drizzle-orm 0.38 → 0.45. - postcss et sharp forcés par `overrides` sur leurs versions corrigées, Next ne les ayant pas encore reprises ; drizzle-kit et esbuild montés côté outillage. - `npm audit fix --force` a été écarté : il proposait de RÉTROGRADER Next en 9.3.3 et eslint-config-next en 12, ce qui aurait cassé l'application. - `eslint-config-next` traînait dans node_modules sans être déclaré : retiré. CSP complète - Politique à nonce posée par le middleware, nonce régénéré à chaque requête, `script-src` sans 'unsafe-inline'. `style-src` garde 'unsafe-inline' : tout le design repose sur des attributs style, et une injection de style n'a pas la portée d'une injection de script. - Conséquence assumée : rendu dynamique pour toutes les pages, un HTML pré-généré ne pouvant pas porter de nonce. Tests - `npm test` (runner natif node:test via tsx), 18 tests sur le filtre XSS, la limitation des tentatives de connexion et le chiffrement des jetons. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
This commit is contained in:
36 files changed
+2151
-6053
No files matched your search
@@ -6,7 +6,8 @@ import { headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { auth, signOut } from "@/auth";
|
||||
import { signOut } from "@/auth";
|
||||
import { getSession } from "@/lib/session";
|
||||
import { db } from "@/db";
|
||||
import {
|
||||
activityLog,
|
||||
@@ -86,7 +87,7 @@ async function requireRole(): Promise<{
|
||||
name: string;
|
||||
userId: number | null;
|
||||
}> {
|
||||
const session = await auth();
|
||||
const session = await getSession();
|
||||
if (!session?.user) throw new Error("Non authentifié");
|
||||
const userId = Number(session.user.id);
|
||||
return {
|
||||
@@ -509,6 +510,8 @@ export async function resetMemberPassword(formData: FormData) {
|
||||
passwordHash: await bcrypt.hash(tempPassword, 10),
|
||||
tempPassword,
|
||||
mustChangePassword: true,
|
||||
// Coupe les sessions ouvertes avec l'ancien mot de passe.
|
||||
sessionVersion: (u.sessionVersion ?? 0) + 1,
|
||||
})
|
||||
.where(eq(users.id, u.id));
|
||||
|
||||
@@ -727,7 +730,7 @@ export async function saveMeetingRegistration(
|
||||
_previousState: MeetingRegistrationState,
|
||||
formData: FormData,
|
||||
): Promise<MeetingRegistrationState> {
|
||||
const session = await auth();
|
||||
const session = await getSession();
|
||||
if (!session?.user?.memberId) {
|
||||
return { status: "error", message: "Connectez-vous avec un compte adhérent pour vous inscrire." };
|
||||
}
|
||||
@@ -995,7 +998,7 @@ export async function saveSiteSettings(formData: FormData) {
|
||||
|
||||
// ---- RGPD / droit à l'image ----
|
||||
export async function saveImageConsent(formData: FormData) {
|
||||
const session = await auth();
|
||||
const session = await getSession();
|
||||
if (!session?.user?.memberId) throw new Error("Compte adhérent requis");
|
||||
|
||||
const decision = asString(formData, "decision");
|
||||
@@ -1025,14 +1028,29 @@ export async function saveImageConsent(formData: FormData) {
|
||||
|
||||
// ---- Self password change (forced at first login) ----
|
||||
export async function changeOwnPassword(formData: FormData) {
|
||||
const session = await auth();
|
||||
const session = await getSession();
|
||||
if (!session?.user) throw new Error("Non authentifié");
|
||||
const userId = Number(session.user.id);
|
||||
|
||||
const current = String(formData.get("currentPassword") ?? "");
|
||||
const password = String(formData.get("password") ?? "");
|
||||
const confirm = String(formData.get("confirm") ?? "");
|
||||
if (password.length < 8) throw new Error("Le mot de passe doit faire au moins 8 caractères.");
|
||||
if (password !== confirm) throw new Error("Les deux mots de passe ne correspondent pas.");
|
||||
|
||||
const back = (message: string) =>
|
||||
redirect(`/backend/changer-mot-de-passe?error=${encodeURIComponent(message)}`);
|
||||
|
||||
if (password.length < 8) back("Le nouveau mot de passe doit faire au moins 8 caractères.");
|
||||
if (password !== confirm) back("Les deux mots de passe ne correspondent pas.");
|
||||
|
||||
const [user] = await db.select().from(users).where(eq(users.id, userId));
|
||||
if (!user) back("Compte introuvable.");
|
||||
|
||||
// Le mot de passe actuel est exigé : sans lui, un poste laissé ouvert suffit
|
||||
// à un tiers pour s'approprier le compte.
|
||||
if (!current || !(await bcrypt.compare(current, user!.passwordHash))) {
|
||||
back("Le mot de passe actuel est incorrect.");
|
||||
}
|
||||
if (current === password) back("Le nouveau mot de passe doit être différent de l'actuel.");
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
@@ -1040,6 +1058,8 @@ export async function changeOwnPassword(formData: FormData) {
|
||||
passwordHash: await bcrypt.hash(password, 10),
|
||||
tempPassword: null,
|
||||
mustChangePassword: false,
|
||||
// Invalide toutes les autres sessions ouvertes sur ce compte.
|
||||
sessionVersion: (user!.sessionVersion ?? 0) + 1,
|
||||
})
|
||||
.where(eq(users.id, userId));
|
||||
|
||||
@@ -1049,7 +1069,7 @@ export async function changeOwnPassword(formData: FormData) {
|
||||
|
||||
// ---- Member: edit own profile ----
|
||||
export async function updateOwnProfile(formData: FormData) {
|
||||
const session = await auth();
|
||||
const session = await getSession();
|
||||
if (!session?.user) throw new Error("Non authentifié");
|
||||
const memberId = session.user.memberId;
|
||||
if (!memberId) throw new Error("Aucune fiche adhérent liée à votre compte.");
|
||||
|
||||
Reference in new issue
Block a user