TypeScript 50.6%
JavaScript 46.1%
SQL 2.1%
Shell 0.6%
CSS 0.5%
1"use client";2import { useState } from "react";3import { useRouter } from "next/navigation";4import { Notice } from "@/components/ui";5import { fmtDate } from "@/lib/format";6type S = { id: string; issuedAt: string; lastSeenAt: string; expiresAt: string; deviceLabel: string | null; current: boolean };7function deviceName(ua: string | null) { if (!ua) return "Appareil inconnu"; const os = /iPhone/.test(ua) ? "iPhone" : /iPad/.test(ua) ? "iPad" : /Android/.test(ua) ? "Android" : /Mac OS/.test(ua) ? "Mac" : /Windows/.test(ua) ? "Windows" : /Linux/.test(ua) ? "Linux" : "Appareil"; const br = /Edg\//.test(ua) ? "Edge" : /Chrome\//.test(ua) ? "Chrome" : /Safari\//.test(ua) ? "Safari" : /Firefox\//.test(ua) ? "Firefox" : "navigateur"; return `${os} · ${br}`; }8export function SecurityView({ email, sessions: init, isAdmin }: { email: string; sessions: S[]; isAdmin: boolean }) {9 const [sessions, setSessions] = useState(init); const router = useRouter(); const [msg, setMsg] = useState<string | null>(null);10 const [newEmail, setNewEmail] = useState(""); const [chal, setChal] = useState<string | null>(null); const [code, setCode] = useState("");11 const revoke = async (id: string) => { await fetch(`/api/me/sessions/${id}`, { method: "DELETE" }); setSessions((s) => s.filter((x) => x.id !== id)); };12 const logoutAll = async () => { if (!confirm("Se déconnecter de tous les appareils, y compris celui-ci ?")) return; await fetch("/api/auth/logout-all", { method: "POST" }); router.push("/"); router.refresh(); };13 const askChange = async () => { setMsg(null); const r = await fetch("/api/me/email-change", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ newEmail }) }); const j = await r.json(); if (!r.ok) { setMsg(j.error?.message ?? "Erreur"); return; } setChal(j.challengeId); setMsg("Un code a été envoyé à la nouvelle adresse."); };14 const confirmChange = async () => { const r = await fetch("/api/me/email-change", { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ challengeId: chal, code }) }); const j = await r.json(); if (!r.ok) { setMsg(j.error?.message ?? "Erreur"); return; } setMsg("Adresse modifiée. Les autres sessions ont été déconnectées."); setChal(null); router.refresh(); };15 const exportData = async () => { const r = await fetch("/api/me/export"); const b = await r.blob(); const a = document.createElement("a"); a.href = URL.createObjectURL(b); a.download = "toveli-mes-donnees.json"; a.click(); };16 const del = async () => { if (!confirm("Supprimer définitivement votre compte, vos annonces et vos messages ? Cette action est irréversible.")) return; const r = await fetch("/api/me", { method: "DELETE" }); if (r.ok) { router.push("/"); router.refresh(); } else setMsg("Suppression impossible pour l'instant."); };17 return (18 <div className="space-y-6 max-w-2xl">19 <div><h1 className="text-2xl md:text-3xl font-extrabold tracking-tight">Sécurité et sessions</h1><p className="text-sm text-ink-2 mt-1">Connexion sans mot de passe par code courriel. Chaque appareil a sa propre session, révocable ici.</p></div>20 <section className="tv-card p-4"><div className="flex items-center justify-between"><h2 className="font-semibold">Mes sessions ({sessions.length})</h2><button type="button" onClick={logoutAll} className="text-xs underline text-brand-dark">Se déconnecter partout</button></div>21 <ul className="divide-y divide-line mt-2 text-sm">{sessions.map((s) => <li key={s.id} className="py-2 flex items-center justify-between gap-3"><span><span className="font-medium">{deviceName(s.deviceLabel)}</span>{s.current && <span className="tv-badge bg-good-soft text-good ml-2">cet appareil</span>}<span className="block text-xs text-ink-3">Dernière activité {fmtDate(s.lastSeenAt)} · ouverte le {fmtDate(s.issuedAt)} · expire le {fmtDate(s.expiresAt)}</span></span>{!s.current && <button type="button" onClick={() => revoke(s.id)} className="text-xs underline text-ink-3">Révoquer</button>}</li>)}</ul>22 <p className="text-xs text-ink-3 mt-2">Sessions valides 30 jours, fermées après 14 jours d'inactivité. Cookie HttpOnly, Secure, SameSite=Lax.</p></section>23 <section className="tv-card p-4 space-y-3"><h2 className="font-semibold">Changer d'adresse courriel</h2><p className="text-xs text-ink-2">Adresse actuelle : {email}. La nouvelle adresse doit être vérifiée par un code ; les autres sessions seront déconnectées.</p>24 {!chal ? <div className="flex gap-2"><input type="email" value={newEmail} onChange={(e) => setNewEmail(e.target.value)} placeholder="nouvelle@adresse.ca" className="tv-chip flex-1 !pl-3" /><button type="button" onClick={askChange} className="tv-btn tv-btn-ghost">Envoyer un code</button></div>25 : <div className="flex gap-2"><input inputMode="numeric" maxLength={6} value={code} onChange={(e) => setCode(e.target.value.replace(/\D/g, ""))} placeholder="Code à 6 chiffres" className="tv-chip flex-1 !pl-3 tabular-nums" /><button type="button" onClick={confirmChange} className="tv-btn tv-btn-primary">Confirmer</button></div>}26 {msg && <Notice>{msg}</Notice>}</section>27 {isAdmin && <section className="tv-card p-4"><h2 className="font-semibold">Authentification forte (administration)</h2><p className="text-xs text-ink-2">Les actions d'administration exigent un second facteur TOTP (application d'authentification). <a href="/admin/mfa" className="underline">Configurer / vérifier</a>.</p></section>}28 <section className="tv-card p-4 space-y-2"><h2 className="font-semibold">Mes données (Loi 25)</h2><p className="text-xs text-ink-2">Exportez l'ensemble de vos données (profil, annonces, messages, favoris) ou supprimez votre compte. Les annonces sont retirées, les messages anonymisés ; les journaux de sécurité minimisés sont conservés 12 mois.</p><div className="flex gap-2"><button type="button" onClick={exportData} className="tv-btn tv-btn-ghost">Exporter (JSON)</button><button type="button" onClick={del} className="tv-btn tv-btn-ghost !text-brand-dark">Supprimer mon compte</button></div></section>29 </div>30 );31}32