feat(auth): add user invitation flow with account setup
- add `createAccountSetup`, `verifyAccountSetupToken`, `deleteAccountSetupToken` to verifications core - add `completeAccountSetup` function to auth core for password creation on invite - add `InvitationEmail` template for sending invite links - add `SetupAccountPage` client page for invited users to set their password - add `UserCreateModal` admin component to invite new users - wire invitation action and API endpoint in auth feature - update admin `UsersPage` to include user creation modal - update auth and admin README docs
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Card, Input, Button, PasswordStrengthIndicator } from '@zen/core/shared/components';
|
||||
import AuthPageHeader from '../components/AuthPageHeader.js';
|
||||
|
||||
export default function SetupAccountPage({ onSubmit, onNavigate, email, token }) {
|
||||
const [error, setError] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [success, setSuccess] = useState('');
|
||||
const [formData, setFormData] = useState({ newPassword: '', confirmPassword: '' });
|
||||
|
||||
const validatePassword = (password) => {
|
||||
const errors = [];
|
||||
if (password.length < 8) errors.push('Le mot de passe doit contenir au moins 8 caractères');
|
||||
if (password.length > 128) errors.push('Le mot de passe doit contenir 128 caractères ou moins');
|
||||
if (!/[A-Z]/.test(password)) errors.push('Le mot de passe doit contenir au moins une majuscule');
|
||||
if (!/[a-z]/.test(password)) errors.push('Le mot de passe doit contenir au moins une minuscule');
|
||||
if (!/\d/.test(password)) errors.push('Le mot de passe doit contenir au moins un chiffre');
|
||||
return errors;
|
||||
};
|
||||
|
||||
const isFormValid = () => {
|
||||
return validatePassword(formData.newPassword).length === 0 &&
|
||||
formData.newPassword === formData.confirmPassword &&
|
||||
formData.newPassword.length > 0;
|
||||
};
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setSuccess('');
|
||||
setIsLoading(true);
|
||||
|
||||
const passwordErrors = validatePassword(formData.newPassword);
|
||||
if (passwordErrors.length > 0) { setError(passwordErrors[0]); setIsLoading(false); return; }
|
||||
if (formData.newPassword !== formData.confirmPassword) {
|
||||
setError('Les mots de passe ne correspondent pas');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const submitData = new FormData();
|
||||
submitData.append('newPassword', formData.newPassword);
|
||||
submitData.append('confirmPassword', formData.confirmPassword);
|
||||
submitData.append('email', email);
|
||||
submitData.append('token', token);
|
||||
|
||||
try {
|
||||
const result = await onSubmit(submitData);
|
||||
|
||||
if (result.success) {
|
||||
setSuccess(result.message);
|
||||
setIsLoading(false);
|
||||
setTimeout(() => onNavigate('login'), 2000);
|
||||
} else {
|
||||
setError(result.error || 'Impossible de créer le mot de passe');
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Setup account error:', err);
|
||||
setError('Une erreur inattendue s\'est produite');
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card variant="default" padding="md" spacing="none" className="w-full max-w-md mx-auto">
|
||||
<AuthPageHeader
|
||||
title="Créez votre mot de passe"
|
||||
description="Un administrateur a créé votre compte. Choisissez un mot de passe pour y accéder."
|
||||
/>
|
||||
|
||||
{error && !success && (
|
||||
<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"></div>
|
||||
<span className="text-xs text-red-700 dark:text-red-400">{error}</span>
|
||||
</div>
|
||||
</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"></div>
|
||||
<span className="text-xs text-green-700 dark:text-green-400">{success}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
<div>
|
||||
<Input
|
||||
id="newPassword"
|
||||
name="newPassword"
|
||||
type="password"
|
||||
label="Mot de passe"
|
||||
value={formData.newPassword}
|
||||
onChange={(value) => setFormData(prev => ({ ...prev, newPassword: value }))}
|
||||
placeholder="••••••••"
|
||||
disabled={!!success}
|
||||
minLength="8"
|
||||
maxLength="128"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
/>
|
||||
<PasswordStrengthIndicator password={formData.newPassword} showRequirements={true} />
|
||||
</div>
|
||||
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
label="Confirmer le mot de passe"
|
||||
value={formData.confirmPassword}
|
||||
onChange={(value) => setFormData(prev => ({ ...prev, confirmPassword: value }))}
|
||||
placeholder="••••••••"
|
||||
disabled={!!success}
|
||||
minLength="8"
|
||||
maxLength="128"
|
||||
autoComplete="new-password"
|
||||
required
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
loading={isLoading}
|
||||
disabled={!!success || !isFormValid()}
|
||||
className="w-full mt-2"
|
||||
>
|
||||
Créer mon mot de passe
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 flex justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="fullghost"
|
||||
onClick={() => onNavigate('login')}
|
||||
>
|
||||
← Retour à la connexion
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user