Frontend : connexion (mot de passe + lien magique) et onglets par rôle

Authentification alignée sur le back-end (Étapes 7/8) :
- src/api : schema auth (AccountRead, TokenResponse…), session.ts (jeton
  Bearer partagé + callback 401), méthodes auth du client HTTP
  (POST .../auth/login, .../auth/magic-link, .../auth/verify, GET /auth/me)
  et du mock (comptes coordination + référents + bénévoles, tokens magiques,
  sessions).
- src/auth : AuthProvider/useAuth (jeton en localStorage, réhydratation via
  /auth/me, déconnexion sur 401), RequireAuth (garde + contrôle de rôle),
  HomeRedirect, table ROUTE_ROLES.
- Pages : /connexion (lien magique, avec lien de démo affiché en mode mock),
  /connexion/coordination (email + mot de passe).
- Router : toutes les routes métier passées derrière <RequireAuth> ;
  redirection vers l'accueil du rôle.
- AppShell : onglets filtrés par rôle (01/02/03/06 coordination, 04 bénévole,
  05 référent), email + rôle + bouton Déconnexion.
- Mobile bénévole : cible le bénévole du compte connecté ; dégradation
  propre quand l'API refuse le détail des créneaux à un bénévole.
- Mobile référent : cible le poste dont le compte est référent.
- i18n : clés auth / rôles.
- Vérifs : build tsc + vite, eslint (0 problème), npm run smoke (flux
  login / lien magique + rendu des 6 écrans par rôle et des pages publiques).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwswqSChc3Rwtu61pRXiY2
This commit is contained in:
AdamThomas
2026-09-10 07:43:00 +02:00
co-authored by Claude Sonnet 5
parent 464635656a
commit 6e16db4b7e
22 changed files with 1102 additions and 156 deletions
+60 -1
View File
@@ -1,6 +1,7 @@
import { hoursBetween } from '@/lib/time';
import type {
AccountRead,
AssignmentRead,
AssignmentStatus,
PositionCategory,
@@ -14,11 +15,18 @@ import type {
export const MOCK_EDITION_ID = '00000000-0000-4000-8000-000000000001';
/** Identifiants de démonstration (mode mock uniquement). */
export const DEMO_COORDINATOR_EMAIL = 'coordination@lessonsdautomne.fr';
export const DEMO_COORDINATOR_PASSWORD = 'demo';
export interface MockState {
volunteers: VolunteerRead[];
positions: PositionRead[];
shifts: ShiftRead[];
assignments: AssignmentRead[];
accounts: AccountRead[];
/** accountId -> mot de passe en clair (mock uniquement). */
passwords: Map<string, string>;
/** volunteerId -> clés « YYYY-MM-DD|HH:MM » déclarées disponibles (privé au mock). */
availability: Map<string, Set<string>>;
seq: number;
@@ -181,6 +189,7 @@ export function buildMockState(): MockState {
}
const me = volunteers[0];
me.email = demoEmail(me.first_name, me.last_name); // toujours un compte pour la démo
availability.set(
me.id,
new Set(
@@ -284,7 +293,57 @@ export function buildMockState(): MockState {
seq += 1;
});
return { volunteers, positions, shifts, assignments, availability, seq };
// ---- Comptes ----
const accounts: AccountRead[] = [];
const passwords = new Map<string, string>();
const coordinator: AccountRead = {
id: `acc${seq++}`,
edition_id,
email: DEMO_COORDINATOR_EMAIL,
role: 'COORDINATOR',
volunteer_id: null,
auth_method: 'password',
created_at: NOW,
updated_at: NOW,
};
accounts.push(coordinator);
passwords.set(coordinator.id, DEMO_COORDINATOR_PASSWORD);
// Trois référents, rattachés à trois postes (lien magique, sans mot de passe).
['MONS', 'MONI', 'REG'].forEach((positionId, i) => {
const account: AccountRead = {
id: `acc${seq++}`,
edition_id,
email: `referent${i + 1}@lessonsdautomne.fr`,
role: 'LEAD',
volunteer_id: null,
auth_method: 'magic_link',
created_at: NOW,
updated_at: NOW,
};
accounts.push(account);
const position = positions.find((p) => p.id === positionId);
if (position) position.lead_account_id = account.id;
});
// Bénévoles connectés : tous ceux qui ont une adresse email.
volunteers
.filter((v) => v.email)
.forEach((v) => {
accounts.push({
id: `acc${seq++}`,
edition_id,
email: v.email as string,
role: 'VOLUNTEER',
volunteer_id: v.id,
auth_method: 'magic_link',
created_at: NOW,
updated_at: NOW,
});
});
return { volunteers, positions, shifts, assignments, accounts, passwords, availability, seq };
}
function shiftsOverlap(a: ShiftRead | undefined, b: ShiftRead): boolean {