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:
2026-04-25 09:03:15 -04:00
parent 96c8cf1e97
commit abd9d651dc
16 changed files with 681 additions and 21 deletions
+37 -1
View File
@@ -1,6 +1,6 @@
'use server';
import { register, login, requestPasswordReset, resetPassword, verifyUserEmail } from './auth.js';
import { register, login, requestPasswordReset, resetPassword, verifyUserEmail, completeAccountSetup } from './auth.js';
import { validateSession, deleteSession } from './session.js';
import { verifyEmailToken, verifyResetToken, sendVerificationEmail, sendPasswordResetEmail } from './email.js';
import { fail } from '@zen/core/shared/logger';
@@ -323,6 +323,42 @@ export async function resetPasswordAction(formData) {
}
}
export async function setupAccountAction(formData) {
try {
const ip = await getClientIp();
const rl = enforceRateLimit(ip, 'setup_account');
if (rl && !rl.allowed) {
return { success: false, error: `Trop de tentatives. Réessayez dans ${formatRetryAfter(rl.retryAfterMs)}.` };
}
const email = formData.get('email');
const token = formData.get('token');
const newPassword = formData.get('newPassword');
const confirmPassword = formData.get('confirmPassword');
if (!newPassword || !confirmPassword) {
throw new UserFacingError('Les deux champs de mot de passe sont requis');
}
if (newPassword !== confirmPassword) {
throw new UserFacingError('Les mots de passe ne correspondent pas');
}
await completeAccountSetup({ email, token, password: newPassword });
return {
success: true,
message: 'Mot de passe créé avec succès. Vous pouvez maintenant vous connecter.'
};
} catch (error) {
if (error instanceof UserFacingError) {
return { success: false, error: error.message };
}
fail(`Auth: setupAccountAction error: ${error.message}`);
return { success: false, error: 'Une erreur interne est survenue. Veuillez réessayer.' };
}
}
export async function verifyEmailAction(formData) {
try {
const ip = await getClientIp();