"use client"; import { useState } from "react"; import { useRouter } from "next/navigation"; import { Notice } from "@/components/ui"; import { fmtDate } from "@/lib/format"; type S = { id: string; issuedAt: string; lastSeenAt: string; expiresAt: string; deviceLabel: string | null; current: boolean }; function 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}`; } export function SecurityView({ email, sessions: init, isAdmin }: { email: string; sessions: S[]; isAdmin: boolean }) { const [sessions, setSessions] = useState(init); const router = useRouter(); const [msg, setMsg] = useState(null); const [newEmail, setNewEmail] = useState(""); const [chal, setChal] = useState(null); const [code, setCode] = useState(""); const revoke = async (id: string) => { await fetch(`/api/me/sessions/${id}`, { method: "DELETE" }); setSessions((s) => s.filter((x) => x.id !== id)); }; 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(); }; 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."); }; 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(); }; 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(); }; 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."); }; return (

Sécurité et sessions

Connexion sans mot de passe par code courriel. Chaque appareil a sa propre session, révocable ici.

Mes sessions ({sessions.length})

    {sessions.map((s) =>
  • {deviceName(s.deviceLabel)}{s.current && cet appareil}Dernière activité {fmtDate(s.lastSeenAt)} · ouverte le {fmtDate(s.issuedAt)} · expire le {fmtDate(s.expiresAt)}{!s.current && }
  • )}

Sessions valides 30 jours, fermées après 14 jours d'inactivité. Cookie HttpOnly, Secure, SameSite=Lax.

Changer d'adresse courriel

Adresse actuelle : {email}. La nouvelle adresse doit être vérifiée par un code ; les autres sessions seront déconnectées.

{!chal ?
setNewEmail(e.target.value)} placeholder="nouvelle@adresse.ca" className="tv-chip flex-1 !pl-3" />
:
setCode(e.target.value.replace(/\D/g, ""))} placeholder="Code à 6 chiffres" className="tv-chip flex-1 !pl-3 tabular-nums" />
} {msg && {msg}}
{isAdmin &&

Authentification forte (administration)

Les actions d'administration exigent un second facteur TOTP (application d'authentification). Configurer / vérifier.

}

Mes données (Loi 25)

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.

); }