Lien de connexion directe permanent dans le récapitulatif de planning
{lien_connexion} devient un lien permanent (réutilisable, sans expiration,
signé HMAC, rien de stocké) au lieu d'un lien magique de 30 min à usage
unique ou d'un simple lien vers le site. Compte bénévole créé à l'envoi
s'il manque ; révocation par « Renouveler le lien de connexion » sur la
fiche (Account.login_link_version, migration 0031) ; jamais pour la
coordination.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8d6781e6ce
commit
05cdb0276b
@@ -351,6 +351,14 @@ export function useUpdateAccount() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useResetLoginLink() {
|
||||
const invalidate = useInvalidate();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => api.accounts.resetLoginLink(edition, id),
|
||||
onSuccess: invalidate.accounts,
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteAccount() {
|
||||
const invalidate = useInvalidate();
|
||||
return useMutation({
|
||||
|
||||
@@ -183,6 +183,8 @@ export function createHttpApi(baseUrl: string): PlanningApi {
|
||||
request(`${base(editionId)}/accounts/${id}`, { method: 'PATCH', body: JSON.stringify(body) }),
|
||||
remove: (editionId, id) =>
|
||||
request(`${base(editionId)}/accounts/${id}`, { method: 'DELETE' }),
|
||||
resetLoginLink: (editionId, id) =>
|
||||
request(`${base(editionId)}/accounts/${id}/reset-login-link`, { method: 'POST' }),
|
||||
},
|
||||
imports: {
|
||||
analyze: (editionId, file: File) => {
|
||||
|
||||
@@ -1240,6 +1240,9 @@ export function createMockApi(): PlanningApi {
|
||||
account.updated_at = touch();
|
||||
return delay(account);
|
||||
},
|
||||
// Pas de lien permanent en démo (liens magiques simulés) : rien à
|
||||
// révoquer, le compte est renvoyé tel quel.
|
||||
resetLoginLink: (_edition, id) => delay(state.accounts.find((a) => a.id === id) ?? notFound()),
|
||||
remove: (_edition, id) => {
|
||||
state.accounts = state.accounts.filter((a) => a.id !== id);
|
||||
return delay(undefined);
|
||||
|
||||
@@ -111,6 +111,9 @@ export interface PlanningApi {
|
||||
/** Changement de rôle seulement (LEAD ↔ VOLUNTEER) — voir `AccountUpdate`. */
|
||||
update(editionId: string, id: string, body: AccountUpdate): Promise<AccountRead>;
|
||||
remove(editionId: string, id: string): Promise<void>;
|
||||
/** Révoque le lien de connexion permanent (récapitulatif de planning) :
|
||||
* les liens déjà envoyés cessent de fonctionner. */
|
||||
resetLoginLink(editionId: string, id: string): Promise<AccountRead>;
|
||||
};
|
||||
imports: {
|
||||
/** Phase 1 : lit le fichier, propose un mapping des colonnes. */
|
||||
|
||||
@@ -263,7 +263,7 @@
|
||||
"config.mail.subject": "OBJET",
|
||||
"config.mail.body": "CORPS DU MESSAGE",
|
||||
"config.mail.variables": "VARIABLES DISPONIBLES",
|
||||
"config.mail.variablesHint": "Cliquer une variable l'ajoute au corps. {lien_connexion} insère un lien de connexion personnel.",
|
||||
"config.mail.variablesHint": "Cliquer une variable l'ajoute au corps. {lien_connexion} insère le lien de connexion directe permanent du bénévole (un clic suffit, sans limite de durée ; crée son accès s'il n'en a pas encore) — révocable depuis sa fiche. {creneaux} insère le tableau de ses créneaux.",
|
||||
"config.mail.send": "ENVOYER LE RÉCAPITULATIF",
|
||||
"config.mail.sendWithLink": "ENVOYER AVEC LIEN DE CONNEXION",
|
||||
"config.mail.sending": "ENVOI EN COURS…",
|
||||
@@ -327,11 +327,15 @@
|
||||
"config.vol.photo.remove": "Retirer",
|
||||
"config.vol.photo.removeConfirm": "Retirer la photo de ce bénévole ?",
|
||||
"config.vol.access.title": "Accès de connexion",
|
||||
"config.vol.access.hint": "Un email sur la fiche ne suffit pas à lui seul : sans un accès donné ici, le bénévole ne reçoit jamais de lien de connexion.",
|
||||
"config.vol.access.hint": "Un email sur la fiche ne suffit pas à lui seul : il faut un accès donné ici, ou l'envoi d'un récapitulatif de planning avec {lien_connexion} (qui en crée un).",
|
||||
"config.vol.access.grant": "Donner un accès de connexion",
|
||||
"config.vol.access.active": "Accès actif : {email}",
|
||||
"config.vol.access.revoke": "Révoquer",
|
||||
"config.vol.access.revokeConfirm": "Révoquer l'accès de connexion de ce bénévole ?",
|
||||
"config.vol.access.resetLink": "Renouveler le lien de connexion",
|
||||
"config.vol.access.resetLinkHint": "Le lien de connexion des récapitulatifs de planning est permanent. Le renouveler rend inutilisables tous les liens déjà envoyés (email transféré, téléphone perdu…) ; le prochain récapitulatif en portera un nouveau.",
|
||||
"config.vol.access.resetLinkConfirm": "Rendre inutilisables les liens de connexion déjà envoyés à ce bénévole ?",
|
||||
"config.vol.access.resetLinkDone": "✓ Anciens liens révoqués",
|
||||
"config.vol.access.needEmail": "Renseignez un email ci-dessus pour pouvoir donner un accès.",
|
||||
"config.vol.field.referentPositions": "Référent des postes",
|
||||
"config.vol.referentHint": "Cocher un poste crée ou met à niveau son accès de connexion en référent : à sa prochaine connexion, il verra l'écran référent (son équipe, le pointage) au lieu de l'écran bénévole. Décocher retire seulement le poste, sans jamais rétrograder l'accès automatiquement.",
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useCreateAccount,
|
||||
useCreateVolunteer,
|
||||
useDeleteAccount,
|
||||
useResetLoginLink,
|
||||
useDeleteVolunteer,
|
||||
useExecuteImport,
|
||||
useExportMecenatReceipts,
|
||||
@@ -112,6 +113,7 @@ function VolunteerAccessSection({
|
||||
const accounts = useAccounts();
|
||||
const createAccount = useCreateAccount();
|
||||
const deleteAccount = useDeleteAccount();
|
||||
const resetLoginLink = useResetLoginLink();
|
||||
const account = accounts.data?.find((a) => a.volunteer_id === volunteerId);
|
||||
|
||||
return (
|
||||
@@ -157,8 +159,29 @@ function VolunteerAccessSection({
|
||||
{t('config.vol.access.needEmail')}
|
||||
</div>
|
||||
) : null}
|
||||
{account && account.auth_method === 'magic_link' ? (
|
||||
<div className="mt-[6px] flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={resetLoginLink.isPending}
|
||||
onClick={() => {
|
||||
if (window.confirm(t('config.vol.access.resetLinkConfirm'))) {
|
||||
resetLoginLink.mutate(account.id);
|
||||
}
|
||||
}}
|
||||
className="cursor-pointer border-2 border-ink bg-paper px-[8px] py-[5px] font-display text-[9.5px] font-extrabold uppercase tracking-wide hover:bg-yellow disabled:opacity-50"
|
||||
>
|
||||
{t('config.vol.access.resetLink')}
|
||||
</button>
|
||||
<HelpTip text={t('config.vol.access.resetLinkHint')} />
|
||||
{resetLoginLink.isSuccess ? (
|
||||
<span className="font-mono text-[9.5px] font-bold">{t('config.vol.access.resetLinkDone')}</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{createAccount.isError ? <ErrorNote error={createAccount.error} /> : null}
|
||||
{deleteAccount.isError ? <ErrorNote error={deleteAccount.error} /> : null}
|
||||
{resetLoginLink.isError ? <ErrorNote error={resetLoginLink.error} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user