diff --git a/backend/app/api/volunteers.py b/backend/app/api/volunteers.py
index d237ea9..0497acd 100644
--- a/backend/app/api/volunteers.py
+++ b/backend/app/api/volunteers.py
@@ -226,12 +226,14 @@ def get_volunteer_planning(
def get_volunteer_photo(
volunteer_id: uuid.UUID,
edition: Edition = Depends(get_edition),
- _guard: Account = Depends(require_staff),
+ account: Account = Depends(current_account_in_edition),
session: Session = Depends(get_session),
) -> Response:
"""Photo transmise par le bénévole via le formulaire public
- (`app/api/forms.py`) — réservée à COORDINATION/RÉFÉRENT, pas exposée à
- l'écran bénévole."""
+ (`app/api/forms.py`) — COORDINATION/RÉFÉRENT (ex. écran référent, en
+ face du nom de chaque membre de l'équipe) ou le bénévole lui-même (écran
+ bénévole)."""
+ _ensure_can_read(account, volunteer_id)
volunteer = owned_or_404(session.get(Volunteer, volunteer_id), edition.id)
if volunteer.photo is None:
raise HTTPException(status_code=404, detail={"code": "error.not_found"})
diff --git a/backend/tests/test_authz.py b/backend/tests/test_authz.py
index 3120805..c838908 100644
--- a/backend/tests/test_authz.py
+++ b/backend/tests/test_authz.py
@@ -67,6 +67,22 @@ def test_volunteer_scope(session, edition, make_account, as_account):
)
+def test_volunteer_sees_only_own_photo(session, edition, make_account, as_account):
+ me = make_volunteer(session, edition, email="me@e.fr")
+ other = make_volunteer(session, edition, email="other@e.fr")
+ me.photo = b"fake-bytes"
+ me.photo_content_type = "image/png"
+ other.photo = b"fake-bytes"
+ other.photo_content_type = "image/png"
+ session.add_all([me, other])
+ session.commit()
+ account = make_account(Role.VOLUNTEER, volunteer_id=me.id, email="me@e.fr")
+ client = as_account(account)
+
+ assert client.get(_e(edition, f"/volunteers/{me.id}/photo")).status_code == 200
+ assert client.get(_e(edition, f"/volunteers/{other.id}/photo")).status_code == 403
+
+
def test_volunteer_sees_only_own_assignments(
session, edition, make_account, as_account
):
diff --git a/frontend/src/components/VolunteerAvatar.tsx b/frontend/src/components/VolunteerAvatar.tsx
new file mode 100644
index 0000000..b1816d8
--- /dev/null
+++ b/frontend/src/components/VolunteerAvatar.tsx
@@ -0,0 +1,43 @@
+import { useVolunteerPhotoUrl } from '@/api/hooks';
+import { cx } from '@/lib/cx';
+
+/**
+ * Photo du bénévole (formulaire public, voir `app/api/forms.py`) si
+ * disponible, sinon repli sur un carré d'initiales — même gabarit dans les
+ * deux cas pour ne jamais faire sauter la mise en page. `className` fixe la
+ * taille (ex. `h-[38px] w-[38px] text-[13px]`).
+ */
+export function VolunteerAvatar({
+ volunteerId,
+ hasPhoto,
+ initials,
+ className,
+}: {
+ volunteerId: string;
+ hasPhoto: boolean;
+ initials: string;
+ className?: string;
+}) {
+ const photoUrl = useVolunteerPhotoUrl(volunteerId, hasPhoto);
+
+ if (photoUrl) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {initials}
+
+ );
+}
diff --git a/frontend/src/pages/MobileLeadPage.tsx b/frontend/src/pages/MobileLeadPage.tsx
index 89c9935..600b941 100644
--- a/frontend/src/pages/MobileLeadPage.tsx
+++ b/frontend/src/pages/MobileLeadPage.tsx
@@ -5,6 +5,7 @@ import { useAssignments, usePositions, useShifts, useVolunteers } from '@/api/ho
import { useAuth } from '@/auth/auth-context';
import { ErrorNote, Loading } from '@/components/state';
import { NowLine } from '@/components/NowLine';
+import { VolunteerAvatar } from '@/components/VolunteerAvatar';
import { hourRangeLabel, longDayLabel, naiveMinutes } from '@/lib/time';
import { useNowMinutes } from '@/lib/useNowMinutes';
import { t } from '@/i18n';
@@ -122,12 +123,14 @@ export function MobileLeadPage() {
const volunteer = volunteersById.get(a.volunteer_id);
return {
id: a.id,
+ volunteerId: a.volunteer_id,
name: volunteer
? `${volunteer.first_name} ${volunteer.last_name}`
: a.volunteer_id,
initials: volunteer
? `${volunteer.first_name[0]}${volunteer.last_name[0]}`
: '?',
+ hasPhoto: volunteer?.has_photo ?? false,
phone: volunteer?.phone ?? '',
present: presence[a.id] === 'present',
absent: presence[a.id] === 'absent',
@@ -155,9 +158,12 @@ export function MobileLeadPage() {
className="border-[3px] border-ink bg-cream shadow-brutal-sm"
>