mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-22 22:58:04 +02:00
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
158 lines
6.8 KiB
TypeScript
158 lines
6.8 KiB
TypeScript
import { redirect } from "next/navigation";
|
|
import type { CSSProperties } from "react";
|
|
import { getSession } from "@/lib/session";
|
|
import { ImageField } from "@/components/ImageField";
|
|
import { can } from "@/lib/rbac";
|
|
import { getSiteSettings } from "@/lib/site-settings";
|
|
import { saveSiteSettings } from "../actions";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export default async function ParametresPage() {
|
|
const session = await getSession();
|
|
if (!can(session?.user.role, "manageSettings")) redirect("/backend");
|
|
|
|
const settings = await getSiteSettings();
|
|
|
|
return (
|
|
<form action={saveSiteSettings} style={{ display: "grid", gap: 22, maxWidth: 1080 }}>
|
|
<section style={panel}>
|
|
<h2 className="font-display" style={title}>Identité du site</h2>
|
|
<div className="grid grid-2" style={{ gap: 16 }}>
|
|
<Field label="Nom association" name="association_name" value={settings.association_name} />
|
|
<Field label="Slogan" name="association_tagline" value={settings.association_tagline} />
|
|
<Field label="E-mail" name="association_email" value={settings.association_email} />
|
|
<Field label="Téléphone" name="association_phone" value={settings.association_phone} />
|
|
<Field label="Site web" name="association_website" value={settings.association_website} />
|
|
<Field label="Contact principal" name="association_contact" value={settings.association_contact} />
|
|
<Field
|
|
label="Page Facebook"
|
|
name="association_facebook"
|
|
value={settings.association_facebook}
|
|
placeholder="https://www.facebook.com/..."
|
|
/>
|
|
<Field
|
|
label="Page LinkedIn"
|
|
name="association_linkedin"
|
|
value={settings.association_linkedin}
|
|
placeholder="https://www.linkedin.com/company/..."
|
|
/>
|
|
<div style={{ gridColumn: "1 / -1" }}>
|
|
<Field label="Adresse" name="association_address" value={settings.association_address} />
|
|
</div>
|
|
<Area label="Texte d'introduction Association" name="association_intro" value={settings.association_intro} />
|
|
<Area
|
|
label="Mission (une ligne vide sépare les paragraphes)"
|
|
name="association_mission"
|
|
value={settings.association_mission}
|
|
/>
|
|
<Area
|
|
label="Ce que l'association apporte (Titre|Description, une ligne par point)"
|
|
name="association_pillars"
|
|
value={settings.association_pillars}
|
|
/>
|
|
</div>
|
|
<div style={{ marginTop: 14, color: "#9a6638", fontSize: 13, lineHeight: 1.6 }}>
|
|
Les pages Facebook et LinkedIn s'affichent sur la page d'accueil et dans le pied
|
|
de page. Laissez un champ vide pour masquer le réseau correspondant. La publication
|
|
automatique des promotions, elle, se configure côté serveur (jetons d'accès dans les
|
|
variables d'environnement).
|
|
</div>
|
|
</section>
|
|
|
|
<section style={panel}>
|
|
<h2 className="font-display" style={title}>Directoire</h2>
|
|
<div className="grid grid-2" style={{ gap: 16 }}>
|
|
<Field label="Président(e)" name="association_president" value={settings.association_president} />
|
|
<Field label="Intitulé du rôle" name="association_president_role" value={settings.association_president_role} />
|
|
<Field label="Vice-président(e)" name="association_vice_president" value={settings.association_vice_president} />
|
|
<Field label="Trésorier(ère)" name="association_treasurer" value={settings.association_treasurer} />
|
|
<Field label="Secrétaire" name="association_secretary" value={settings.association_secretary} />
|
|
<ImageField name="association_president_photo" label="Photo président(e)" defaultValue={settings.association_president_photo} height={130} />
|
|
<Area label="Message président(e)" name="association_president_message" value={settings.association_president_message} />
|
|
<Area
|
|
label="Autres membres du directoire (Nom|Fonction, une ligne par personne)"
|
|
name="association_board_members"
|
|
value={settings.association_board_members}
|
|
/>
|
|
</div>
|
|
</section>
|
|
|
|
<section style={panel}>
|
|
<h2 className="font-display" style={title}>Mentions légales & confidentialité</h2>
|
|
<div className="grid grid-2" style={{ gap: 16 }}>
|
|
<Field label="SIRET" name="association_siret" value={settings.association_siret} />
|
|
<Field label="RNA" name="association_rna" value={settings.association_rna} />
|
|
<Field label="Directeur de publication" name="association_publication_director" value={settings.association_publication_director} />
|
|
<Field label="Contact confidentialité/RGPD" name="association_privacy_contact" value={settings.association_privacy_contact} />
|
|
<Field label="Hébergeur" name="association_host_name" value={settings.association_host_name} />
|
|
<Field label="Téléphone hébergeur" name="association_host_phone" value={settings.association_host_phone} />
|
|
<div style={{ gridColumn: "1 / -1" }}>
|
|
<Field label="Adresse hébergeur" name="association_host_address" value={settings.association_host_address} />
|
|
</div>
|
|
<Field label="Dernière mise à jour légale" name="legal_updated" value={settings.legal_updated} placeholder="17 juillet 2026" />
|
|
</div>
|
|
<div style={{ marginTop: 14, color: "#9a6638", fontSize: 13, lineHeight: 1.6 }}>
|
|
Remplissez ces champs avant mise en production: ils alimentent les pages publiques
|
|
Mentions légales et Confidentialité.
|
|
</div>
|
|
</section>
|
|
|
|
<button type="submit" style={submitButton}>Enregistrer les paramètres</button>
|
|
</form>
|
|
);
|
|
}
|
|
|
|
function Field({
|
|
label,
|
|
name,
|
|
value,
|
|
placeholder,
|
|
}: {
|
|
label: string;
|
|
name: string;
|
|
value: string;
|
|
placeholder?: string;
|
|
}) {
|
|
return (
|
|
<div>
|
|
<label className="field-label">{label}</label>
|
|
<input name={name} defaultValue={value} placeholder={placeholder} className="field" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Area({ label, name, value }: { label: string; name: string; value: string }) {
|
|
return (
|
|
<div style={{ gridColumn: "1 / -1" }}>
|
|
<label className="field-label">{label}</label>
|
|
<textarea name={name} rows={4} defaultValue={value} className="field" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const panel: CSSProperties = {
|
|
background: "#fff",
|
|
border: "1px solid #e6dcc6",
|
|
borderRadius: 16,
|
|
padding: 22,
|
|
};
|
|
|
|
const title: CSSProperties = {
|
|
margin: "0 0 16px",
|
|
fontSize: 20,
|
|
color: "#26201a",
|
|
};
|
|
|
|
const submitButton: CSSProperties = {
|
|
justifySelf: "start",
|
|
border: "none",
|
|
background: "#13324F",
|
|
color: "#fff",
|
|
fontWeight: 800,
|
|
fontSize: 14.5,
|
|
padding: "13px 22px",
|
|
borderRadius: 10,
|
|
cursor: "pointer",
|
|
};
|