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
@@ -2,9 +2,12 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { ApiError } from '@/api';
import {
useAccounts,
useAnalyzeImport,
useArtists,
useCreateAccount,
useCreateVolunteer,
useDeleteAccount,
useDeleteVolunteer,
useEdition,
useExecuteImport,
@@ -69,6 +72,70 @@ function positionOptions(positions: PositionRead[] | undefined): WishOption[] {
}
/** Sélecteur de vœux : liste déroulante + puces retirables. */
/**
* Accès de connexion (lien magique) du bénévole. Ajouter un email sur sa
* fiche ne crée aucun compte : sans ce geste explicite, `auth/magic-link` ne
* trouve personne à qui écrire et ne fait rien (par conception, pour ne
* jamais révéler si un email existe).
*/
function VolunteerAccessSection({
volunteerId,
email,
}: {
volunteerId: string;
email: string | null;
}) {
const accounts = useAccounts();
const createAccount = useCreateAccount();
const deleteAccount = useDeleteAccount();
const account = accounts.data?.find((a) => a.volunteer_id === volunteerId);
return (
<div>
<FieldLabel>
{t('config.vol.access.title')} <HelpTip text={t('config.vol.access.hint')} />
</FieldLabel>
{account ? (
<div className="flex items-center justify-between gap-2 border-2 border-ink bg-cream p-[9px]">
<span className="min-w-0 truncate font-mono text-[10px]">
{t('config.vol.access.active', { email: account.email })}
</span>
<button
type="button"
disabled={deleteAccount.isPending}
onClick={() => {
if (window.confirm(t('config.vol.access.revokeConfirm'))) {
deleteAccount.mutate(account.id);
}
}}
className="flex-none cursor-pointer border-2 border-ink bg-paper px-[8px] py-[5px] font-display text-[9.5px] font-extrabold uppercase tracking-wide hover:bg-orange disabled:opacity-50"
>
{t('config.vol.access.revoke')}
</button>
</div>
) : (
<button
type="button"
disabled={!email || createAccount.isPending}
onClick={() =>
email && createAccount.mutate({ email, role: 'VOLUNTEER', volunteer_id: volunteerId })
}
className="cursor-pointer border-2 border-ink bg-cream px-[10px] py-[7px] font-display text-[10.5px] font-extrabold uppercase tracking-wide hover:bg-yellow disabled:cursor-not-allowed disabled:opacity-50"
>
{createAccount.isPending ? t('app.loading') : t('config.vol.access.grant')}
</button>
)}
{!account && !email ? (
<div className="mt-1 font-mono text-[9.5px] opacity-60">
{t('config.vol.access.needEmail')}
</div>
) : null}
{createAccount.isError ? <ErrorNote error={createAccount.error} /> : null}
{deleteAccount.isError ? <ErrorNote error={deleteAccount.error} /> : null}
</div>
);
}
function WishPicker({
label,
options,
@@ -342,6 +409,8 @@ export function ConfigVolunteers() {
/>
</label>
<VolunteerAccessSection volunteerId={selected.id} email={selected.email} />
<WishPicker
label={t('config.vol.field.artists')}
options={artistOpts}