3.6 KiB
User Avatar — Design Spec
Goal
Allow users to upload a profile avatar image (with client-side circular crop) that replaces initials everywhere in the app.
Backend
Entity Changes
User — add nullable field:
avatarFileName: ?string(length 255) — UUID-based filename stored on disk
Storage
- Directory:
var/uploads/avatars/ - Parameter in
services.yaml:avatar_upload_dir
Endpoints
All under /api/users/{id}/avatar:
| Method | Description | Auth |
|---|---|---|
POST |
Upload avatar (multipart file) | Owner or ROLE_ADMIN |
GET |
Serve avatar image (inline) | ROLE_USER or ROLE_CLIENT |
DELETE |
Remove avatar | Owner or ROLE_ADMIN |
POST accepts a single file field. Validates: image MIME (jpeg, png, webp, gif), max 5 MB. Stores with UUID filename, updates avatarFileName. Deletes previous file if exists.
GET returns the image with proper Content-Type. Returns 404 if no avatar.
DELETE removes file from disk, sets avatarFileName to null.
These are custom Symfony controllers (not API Platform resources) under /api/ with priority: 1.
Serialization
Add a virtual avatarUrl field to User serialization (group user:read):
- If
avatarFileNameis set:/api/users/{id}/avatar - If null:
null
This way the frontend knows if an avatar exists from any user payload.
Migration
- Add
avatar_file_namecolumn (VARCHAR 255, nullable) tousertable.
Frontend
New Components
UserAvatar.vue (frontend/components/user/UserAvatar.vue):
- Props:
user: { id: number, username: string, avatarUrl?: string | null },size: 'xs' | 'sm' | 'md' | 'lg' - Sizes: xs=20px, sm=24px, md=32px, lg=48px
- If
avatarUrl:<img>rounded-full, object-cover - Else: initials badge (current bg-primary-500 style), 2 first chars of username uppercased
- Handles
@erroron img to fallback to initials (broken image)
AvatarCropper.vue (frontend/components/user/AvatarCropper.vue):
- Uses
vue-advanced-cropperwithCircleStencil - Props:
imageFile: File - Emits:
crop(blob: Blob),cancel - Fixed output size: 256x256px
- Modal overlay with crop area + confirm/cancel buttons
New Page
/profile (frontend/pages/profile.vue):
- Shows current avatar (large) with "Change" button
- File input triggers AvatarCropper modal
- On confirm: POST blob to
/api/users/{id}/avatar - On success: refresh auth store user data
- "Remove avatar" button if avatar exists
- Accessible from "Mon profil" button in AppTopNav dropdown
New Service
frontend/services/avatar.ts:
upload(userId: number, file: Blob): Promise<void>— POST multipartremove(userId: number): Promise<void>— DELETEgetUrl(userId: number): string— returns URL path
DTO Update
UserData — add: avatarUrl?: string | null
Replacement Points
Replace initials/icon with <UserAvatar> in:
| File | Current display | Size |
|---|---|---|
TaskCard.vue:48-53 |
Initials badge (h-5 w-5) | xs |
archives.vue:50-55 |
Initials badge (h-5 w-5) | xs |
AppTopNav.vue:13 |
mdi:account-circle-outline icon |
md |
AdminClientTicketTab.vue |
Username text for submitter | sm |
ClientTicketDetailModal.vue |
submittedBy display | sm |
Auth Store
After avatar upload/delete, re-fetch current user data so avatarUrl updates everywhere reactively.
Dependencies
vue-advanced-cropper— npm install in frontend/
Out of Scope
- Server-side image processing/resize
- Multiple image formats conversion
- Avatar for clients (entities), only users