'use client'; import { registerPage } from '../registry.js'; import { useState, useEffect, useRef } from 'react'; import { Card, Input, Button, TabNav, UserAvatar, Modal, PasswordStrengthIndicator } from '@zen/core/shared/components'; import { SmartPhone01Icon, ComputerIcon } from '@zen/core/shared/icons'; import { useToast } from '@zen/core/toast'; import AdminHeader from '../components/AdminHeader.js'; const TABS = [ { id: 'informations', label: 'Informations' }, { id: 'photo', label: 'Photo de profil' }, { id: 'securite', label: 'Sécurité' }, { id: 'sessions', label: 'Sessions' }, ]; const ProfilePage = ({ user: initialUser }) => { const toast = useToast(); const fileInputRef = useRef(null); const [activeTab, setActiveTab] = useState('informations'); const [user, setUser] = useState(initialUser); const [loading, setLoading] = useState(false); const [uploadingImage, setUploadingImage] = useState(false); const [formData, setFormData] = useState({ name: initialUser?.name || '' }); const [emailModalOpen, setEmailModalOpen] = useState(false); const [emailFormData, setEmailFormData] = useState({ newEmail: '', password: '' }); const [emailLoading, setEmailLoading] = useState(false); const [pendingEmailMessage, setPendingEmailMessage] = useState(''); const [passwordForm, setPasswordForm] = useState({ currentPassword: '', newPassword: '', confirmPassword: '' }); const [passwordLoading, setPasswordLoading] = useState(false); const [sessions, setSessions] = useState([]); const [sessionsLoading, setSessionsLoading] = useState(false); const [currentSessionId, setCurrentSessionId] = useState(null); useEffect(() => { if (initialUser) setFormData({ name: initialUser.name || '' }); }, [initialUser]); useEffect(() => { if (activeTab !== 'sessions') return; setSessionsLoading(true); fetch('/zen/api/users/profile/sessions', { credentials: 'include' }) .then(r => r.json()) .then(data => { if (data.sessions) { setSessions(data.sessions); setCurrentSessionId(data.currentSessionId); } }) .catch(() => toast.error('Impossible de charger les sessions')) .finally(() => setSessionsLoading(false)); }, [activeTab]); const handleRevokeSession = async (sessionId) => { try { const response = await fetch(`/zen/api/users/profile/sessions/${sessionId}`, { method: 'DELETE', credentials: 'include', }); const data = await response.json(); if (!response.ok || !data.success) throw new Error(data.error || 'Impossible de révoquer la session'); if (data.isCurrent) { window.location.href = '/admin/login'; } else { setSessions(prev => prev.filter(s => s.id !== sessionId)); } } catch (error) { toast.error(error.message || 'Impossible de révoquer la session'); } }; const handleRevokeAllSessions = async () => { if (!confirm('Révoquer toutes les sessions ? Vous serez déconnecté de tous les appareils.')) return; try { const response = await fetch('/zen/api/users/profile/sessions', { method: 'DELETE', credentials: 'include', }); const data = await response.json(); if (!response.ok || !data.success) throw new Error(data.error || 'Impossible de révoquer les sessions'); window.location.href = '/admin/login'; } catch (error) { toast.error(error.message || 'Impossible de révoquer les sessions'); } }; const hasChanges = formData.name !== user?.name; const validatePassword = (password) => { if (!password || password.length < 8) return 'Le mot de passe doit contenir au moins 8 caractères'; if (password.length > 128) return 'Le mot de passe doit contenir 128 caractères ou moins'; if (!/[A-Z]/.test(password)) return 'Le mot de passe doit contenir au moins une majuscule'; if (!/[a-z]/.test(password)) return 'Le mot de passe doit contenir au moins une minuscule'; if (!/\d/.test(password)) return 'Le mot de passe doit contenir au moins un chiffre'; return null; }; const handlePasswordSubmit = async (e) => { e.preventDefault(); if (!passwordForm.currentPassword) { toast.error('Le mot de passe actuel est requis'); return; } const pwdError = validatePassword(passwordForm.newPassword); if (pwdError) { toast.error(pwdError); return; } if (passwordForm.newPassword !== passwordForm.confirmPassword) { toast.error('Les mots de passe ne correspondent pas'); return; } setPasswordLoading(true); try { const response = await fetch('/zen/api/users/profile/password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ currentPassword: passwordForm.currentPassword, newPassword: passwordForm.newPassword }), }); const data = await response.json(); if (!response.ok || !data.success) throw new Error(data.error || data.message || 'Échec de la mise à jour du mot de passe'); toast.success(data.message); setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' }); } catch (error) { toast.error(error.message || 'Échec de la mise à jour du mot de passe'); } finally { setPasswordLoading(false); } }; const handleEmailSubmit = async () => { if (!emailFormData.newEmail.trim()) { toast.error('Le nouveau courriel est requis'); return; } if (!emailFormData.password) { toast.error('Le mot de passe est requis'); return; } setEmailLoading(true); try { const response = await fetch('/zen/api/users/profile/email', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ newEmail: emailFormData.newEmail.trim(), password: emailFormData.password }), }); const data = await response.json(); if (!response.ok || !data.success) throw new Error(data.error || data.message || 'Échec de la demande de changement de courriel'); toast.success(data.message); setPendingEmailMessage(data.message); setEmailModalOpen(false); setEmailFormData({ newEmail: '', password: '' }); } catch (error) { toast.error(error.message || 'Échec de la demande de changement de courriel'); } finally { setEmailLoading(false); } }; const handleSubmit = async (e) => { e.preventDefault(); if (!formData.name.trim()) { toast.error('Le nom est requis'); return; } setLoading(true); try { const response = await fetch('/zen/api/users/profile', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ name: formData.name.trim() }), }); const data = await response.json(); if (!response.ok || !data.success) throw new Error(data.error || 'Échec de la mise à jour du profil'); setUser(data.user); toast.success('Profil mis à jour avec succès'); setTimeout(() => window.location.reload(), 1000); } catch (error) { toast.error(error.message || 'Échec de la mise à jour du profil'); } finally { setLoading(false); } }; const handleImageSelect = async (e) => { const file = e.target.files?.[0]; if (!file) return; if (!file.type.startsWith('image/')) { toast.error('Veuillez sélectionner un fichier image'); return; } if (file.size > 5 * 1024 * 1024) { toast.error("L'image doit faire moins de 5MB"); return; } setUploadingImage(true); try { const body = new FormData(); body.append('file', file); const response = await fetch('/zen/api/users/profile/picture', { method: 'POST', credentials: 'include', body, }); const data = await response.json(); if (!response.ok || !data.success) throw new Error(data.message || "Échec du téléchargement de l'image"); setUser(data.user); toast.success('Photo de profil mise à jour avec succès'); } catch (error) { toast.error(error.message || "Échec du téléchargement de l'image"); } finally { setUploadingImage(false); } }; const handleRemoveImage = async () => { if (!user?.image) return; setUploadingImage(true); try { const response = await fetch('/zen/api/users/profile/picture', { method: 'DELETE', credentials: 'include', }); const data = await response.json(); if (!response.ok || !data.success) throw new Error(data.message || "Échec de la suppression de l'image"); setUser(data.user); toast.success('Photo de profil supprimée avec succès'); } catch (error) { toast.error(error.message || "Échec de la suppression de l'image"); } finally { setUploadingImage(false); } }; return ( <>
{activeTab === 'informations' && (
} >
setFormData(prev => ({ ...prev, name: value }))} placeholder="Entrez votre nom complet" required disabled={loading} />
{!pendingEmailMessage && ( )}
)} {activeTab === 'securite' && (
} >
setPasswordForm(prev => ({ ...prev, currentPassword: value }))} placeholder="••••••••" autoComplete="current-password" required disabled={passwordLoading} />
setPasswordForm(prev => ({ ...prev, newPassword: value }))} placeholder="••••••••" autoComplete="new-password" minLength="8" maxLength="128" required disabled={passwordLoading} />
setPasswordForm(prev => ({ ...prev, confirmPassword: value }))} placeholder="••••••••" autoComplete="new-password" minLength="8" maxLength="128" required disabled={passwordLoading} />
)} {activeTab === 'sessions' && ( } > {sessionsLoading ? (
) : sessions.length === 0 ? (

Aucune session active

) : (
{sessions.map(session => (
{session.device === 'mobile' ? ( ) : ( )}
{session.browser} · {session.os} {session.id === currentSessionId && ( Session actuelle )}
{session.ip_address || 'IP inconnue'} · {new Date(session.created_at).toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' })}
))}
)} )} {activeTab === 'photo' && (

Téléchargez une nouvelle photo de profil. Taille max 5MB.

{uploadingImage && (
)}
{user?.image && ( )}
)}
{ setEmailModalOpen(false); setEmailFormData({ newEmail: '', password: '' }); }} title="Modifier le courriel" onSubmit={handleEmailSubmit} submitLabel="Envoyer la confirmation" loading={emailLoading} size="sm" >
setEmailFormData(prev => ({ ...prev, newEmail: value }))} placeholder="nouvelle@adresse.com" required disabled={emailLoading} /> setEmailFormData(prev => ({ ...prev, password: value }))} placeholder="Votre mot de passe" required disabled={emailLoading} />
); }; export default ProfilePage; registerPage({ slug: 'profile', title: 'Mon profil', Component: ProfilePage });