Front : donner un accès de connexion à un bénévole (comptes manquants)

Root cause du « je n'ajoute un bénévole avec son mail, je ne reçois aucun
mail » : renseigner un email sur la fiche d'un bénévole ne crée jamais de
compte (`Account`). `POST .../auth/magic-link` cherche un compte par email
et ne fait rien s'il n'en trouve pas (202 quoi qu'il arrive, par conception,
pour ne jamais révéler si un compte existe) — silencieusement, sans erreur.
L'API `POST/GET/DELETE /editions/{id}/accounts` existait déjà côté backend
(testée) mais n'avait jamais été branchée au frontend.

- `api/schema.ts` / `types.ts` / `http-client.ts` / `hooks.ts` : nouvelle
  surface `accounts` (list/create/remove) + `useAccounts`, `useCreateAccount`,
  `useDeleteAccount`.
- `mock-client.ts` : même surface (règles miroir du backend : email unique,
  mot de passe interdit hors COORDINATOR) ; la suppression d'un bénévole
  supprime aussi son compte (cascade RGPD, alignée sur le backend).
- `ConfigVolunteers.tsx` : section « Accès de connexion » sur la fiche du
  bénévole — bouton pour donner l'accès (crée un compte VOLUNTEER, lien
  magique) si un email est renseigné, ou le révoquer si un accès existe déjà.
- Corrigé au passage : `config.vol.addHint` affirmait qu'un email donnait
  l'accès automatiquement — c'était faux, texte mis à jour.
- CLAUDE.md à jour (section authentification).

Vérifié : typecheck, lint, build, smoke (7 routes) OK.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
This commit is contained in:
AdamThomas
2026-09-15 20:22:56 +02:00
co-authored by Claude Sonnet 5
parent fb6e26eddb
commit e405590bbc
8 changed files with 174 additions and 2 deletions
+40
View File
@@ -730,6 +730,46 @@ export function createMockApi(): PlanningApi {
state.volunteers = state.volunteers.filter((x) => x.id !== id);
state.assignments = state.assignments.filter((a) => a.volunteer_id !== id);
state.availability.delete(id);
// RGPD : effacement complet, y compris le compte de connexion éventuel.
state.accounts = state.accounts.filter((a) => a.volunteer_id !== id);
return delay(undefined);
},
},
accounts: {
list: () =>
delay([...state.accounts].sort((a, b) => a.email.localeCompare(b.email, 'fr'))),
create: (_edition, body) => {
const email = body.email.trim();
if (state.accounts.some((a) => a.email.toLowerCase() === email.toLowerCase())) {
throw new ApiError(409, 'account.duplicate');
}
if (body.volunteer_id && !state.volunteers.some((v) => v.id === body.volunteer_id)) {
throw new ApiError(422, 'account.volunteer_wrong_edition');
}
if (body.role === 'COORDINATOR') {
if (!body.password || body.password.length < 10) {
throw new ApiError(422, 'account.password_required');
}
} else if (body.password) {
throw new ApiError(422, 'account.password_not_allowed');
}
const now = touch();
const account: AccountRead = {
id: `acc${state.seq++}`,
edition_id: state.edition.id,
email,
role: body.role,
volunteer_id: body.volunteer_id ?? null,
auth_method: body.role === 'COORDINATOR' ? 'password' : 'magic_link',
created_at: now,
updated_at: now,
};
state.accounts.push(account);
return delay(account);
},
remove: (_edition, id) => {
state.accounts = state.accounts.filter((a) => a.id !== id);
return delay(undefined);
},
},