feat(admin): add email change flow with confirmation for users
- add `ConfirmEmailChangePage.client.js` for email change token confirmation - add `emailChange.js` core utility to generate and verify email change tokens - add `EmailChangeConfirmEmail.js` and `EmailChangeNotifyEmail.js` email templates - update `UserEditModal` to handle email changes with password verification for self-edits - update `ProfilePage` to support email change initiation - update `UsersPage` to pass `currentUserId` to `UserEditModal` - add email change API endpoints in `auth/api.js` and `auth/email.js` - register `ConfirmEmailChangePage` in `AdminPage.client.js`
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
'use client';
|
||||
|
||||
import { registerPage } from '../registry.js';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Card } from '@zen/core/shared/components';
|
||||
|
||||
const ConfirmEmailChangePage = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const token = searchParams.get('token');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [success, setSuccess] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const hasConfirmedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
setError('Lien de confirmation invalide.');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
if (hasConfirmedRef.current) return;
|
||||
hasConfirmedRef.current = true;
|
||||
|
||||
fetch(`/zen/api/users/email/confirm?token=${encodeURIComponent(token)}`, { credentials: 'include' })
|
||||
.then(res => res.json().then(data => ({ ok: res.ok, data })))
|
||||
.then(({ ok, data }) => {
|
||||
if (ok && data.success) {
|
||||
setSuccess('Votre adresse courriel a été mise à jour avec succès.');
|
||||
setTimeout(() => { window.location.href = '/admin/profile'; }, 3000);
|
||||
} else {
|
||||
setError(data.error || data.message || 'Lien de confirmation invalide ou expiré.');
|
||||
}
|
||||
})
|
||||
.catch(() => setError('Une erreur inattendue est survenue.'))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [token]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-64">
|
||||
<Card variant="default" padding="md" spacing="none" className="w-full max-w-md">
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-2">
|
||||
Confirmation du courriel
|
||||
</h1>
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
Validation de votre nouvelle adresse courriel...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex flex-col items-center py-10">
|
||||
<div className="w-12 h-12 border-4 border-neutral-300 border-t-neutral-900 rounded-full animate-spin dark:border-neutral-700/50 dark:border-t-white" />
|
||||
<p className="mt-5 text-neutral-600 dark:text-neutral-400 text-sm">Confirmation en cours...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<>
|
||||
<div className="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg dark:bg-green-500/10 dark:border-green-500/20">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-1.5 h-1.5 bg-green-500 rounded-full flex-shrink-0" />
|
||||
<span className="text-xs text-green-700 dark:text-green-400">{success}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-center text-neutral-600 dark:text-neutral-400 text-sm">
|
||||
Redirection vers votre profil...
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg dark:bg-red-500/10 dark:border-red-500/20">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-1.5 h-1.5 bg-red-500 rounded-full flex-shrink-0" />
|
||||
<span className="text-xs text-red-700 dark:text-red-400">{error}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmEmailChangePage;
|
||||
|
||||
registerPage({ slug: 'confirm-email-change', title: 'Confirmation du courriel', Component: ConfirmEmailChangePage });
|
||||
Reference in New Issue
Block a user