Profil utilisateur
Attribut sex (clé i18n)
Section titled “Attribut sex (clé i18n)”Le champ sex exposé sur la ressource users n’est plus un entier brut mais un slug i18n stable :
| Slug API | Code interne (DB user.sex) | Clé i18n complète |
|---|---|---|
null | NULL (non renseigné) | (omis) |
male | 0 | users.sex.male |
female | 1 | users.sex.female |
other | 2 | users.sex.other |
Le client lit attributes.sex === "male" puis résout le label localisé via son bundle i18n (users.sex.male → « Homme » / « Male » / …). La colonne DB reste un INT (0/1/2) — c’est de la sérialisation pure. Changer un slug est un breaking change API.
POST /api/users/me/username
Section titled “POST /api/users/me/username”Permet à l’utilisateur courant de remplacer son username placeholder par un vrai username, et marque son profil comme complété (profile_completed_at = NOW()). Endpoint destiné en priorité aux comptes créés via Google OAuth (qui démarrent avec un username g_<8-hex> et profile_completed_at = NULL).
- Auth : requise (Bearer)
- Action : SetUsernameAction
- Request body (forme plate ou JSON:API
data.attributes) :
{ "username": "havoc" }-
Validation : mêmes règles que
POST /api/auth/register(voir UsernamePolicy). -
Réponse
200:
{ "jsonapi": { "version": "1.1" }, "data": { "type": "users", "id": "<user-uuid>", "attributes": { "username": "havoc", "email": "user@gmail.com", "isConfirmed": true, "profileCompletedAt": "2026-06-07T10:00:00+00:00" } }}-
Réponse
400— corps non-JSON -
Réponse
401— token absent / invalide -
Réponse
409—Profile already completed,meta.code = "profile.alreadyCompleted". Pour modifier un username déjà confirmé, utiliserPOST /api/users/me/username/change. -
Réponse
422—usernamemanquant ou validation échouée. Codes possibles :username.tooShort,username.tooLong,username.invalidCharacters,username.invalidBoundary,username.consecutiveSeparators,username.reserved,username.alreadyTaken
-
Notes :
- L’update est atomique (
username+profile_completed_at+updated_atdans le mêmeUPDATE). - Le username est normalisé (minuscules, trim) avant validation.
- L’assignation est aussi enregistrée dans
username_history(utilisée par/username/changepour le cooldown et la quarantaine).
- L’update est atomique (
POST /api/users/me/username/change
Section titled “POST /api/users/me/username/change”Change le username d’un compte dont le profil est déjà complété (profile_completed_at IS NOT NULL). Pour les comptes OAuth dont le profil n’est pas encore complété (username placeholder g_<hex>), utiliser POST /api/users/me/username.
- Auth : requise (Bearer)
- Action : ChangeUsernameAction
- Request body (forme plate ou JSON:API
data.attributes) :
{ "username": "havoc2" }Vérifications dans l’ordre, refus dès le premier échec :
- Profil complété — sinon
409 username.profileIncomplete. Utiliser l’endpoint de complétion du profil à la place. - Format — règles de UsernamePolicy (3–32 caractères,
[a-z0-9._-], bornes alphanumériques, pas de séparateurs consécutifs). - Différent du username actuel — sinon
409 username.sameAsCurrent. - Cooldown 30 jours — au moins 30 jours doivent s’être écoulés depuis la dernière assignation de username pour cet utilisateur (toute la chaîne, pas seulement l’actuel) — sinon
409 username.onCooldownavecmeta.cooldownUntil(ISO-8601). - Quarantaine 90 jours — si ce username a été libéré par un autre utilisateur il y a moins de 90 jours, il est en quarantaine — sinon
409 username.quarantined. Exception : l’utilisateur courant peut reprendre un username qu’il a lui-même tenu auparavant, sans attendre la quarantaine. - Disponibilité immédiate — le username ne doit pas être détenu actuellement par un autre utilisateur — sinon
409 username.alreadyTaken.
Effet de bord
Section titled “Effet de bord”L’update est atomique (transaction) : user.username + user.updated_at mis à jour, et username_history reçoit deux opérations :
- la ligne ouverte (où
released_at IS NULL) est fermée avecreleased_at = NOW(); - une nouvelle ligne ouverte est insérée avec
assigned_at = NOW().
profile_completed_at n’est pas touché.
- Réponse
200:
{ "jsonapi": { "version": "1.1" }, "data": { "type": "usernameChanges", "id": "<user-uuid>", "attributes": { "previousUsername": "havoc", "username": "havoc2", "cooldownDays": 30 } }}-
Réponse
400— corps non-JSON -
Réponse
401— token absent / invalide -
Réponse
409— code dansmeta.code:username.profileIncompleteusername.sameAsCurrentusername.onCooldown(avecmeta.cooldownUntiletmeta.cooldownDays)username.quarantined(avecmeta.quarantineDays)username.alreadyTaken
-
Réponse
422—usernamemanquant ou format invalide. Codes :username.tooShort,username.tooLong,username.invalidCharacters,username.invalidBoundary,username.consecutiveSeparators,username.reserved -
Notes :
- Les durées (
COOLDOWN_DAYS = 30,QUARANTINE_DAYS = 90) sont définies en constantes surUsernameChangeService. - Le cooldown se calcule sur la dernière assignation toutes valeurs confondues (MAX
assigned_at) — un user qui change pourXpuis veut changer pourYdoit attendre 30 jours.
- Les durées (
PUT /api/users/me/nickname
Section titled “PUT /api/users/me/nickname”Définit ou efface le nickname de l’utilisateur courant : le nom d’affichage libre, modifiable à volonté et non unique (contrairement au username, handle unique encadré). Auth requise.
Règle d’affichage : displayName = nickname quand il est renseigné, sinon username (voir User::displayName()). Un nickname vide fait donc retomber l’affichage sur le username.
Corps (JSON plat ou data.attributes), la clé nickname doit être présente :
{ "nickname": "Jane Doe" } // définit{ "nickname": "" } // efface (retombe sur le username){ "nickname": null } // effaceValidation (NicknamePolicy) — le nickname est volontairement permissif (lettres de tout script, chiffres, espaces, ponctuation/symboles, emojis) :
- normalisation : trim + espaces internes multiples réduits à un seul ; chaîne vide ⇒
null(efface) ; - longueur 1..32 (colonne
user.nickname=VARCHAR(32)) ; - pas de caractères de contrôle / invisibles (retours ligne, zero-width…).
Réponse 200 — la ressource users complète (mêmes attributs que GET /api/auth/me), donc le client voit immédiatement le nouveau displayName.
-
Réponse
400— corps non-JSON. -
Réponse
401— token absent / invalide. -
Réponse
422— clénicknameabsente, type invalide (ni string ni null), ou format invalide. Codes :nickname.tooShort,nickname.tooLong,nickname.invalidCharacters. -
Notes :
- Pas de contrainte d’unicité : deux utilisateurs peuvent partager un même nickname ; seul le
usernameest unique. - La blocklist de noms réservés (
username_blocklist.php) ne s’applique pas au nickname (choix produit : liberté d’affichage). À activer ici si la lutte contre l’usurpation par nom d’affichage devient nécessaire.
- Pas de contrainte d’unicité : deux utilisateurs peuvent partager un même nickname ; seul le
POST /api/users/me/password
Section titled “POST /api/users/me/password”Définit ou change le mot de passe de l’utilisateur courant.
- Auth : requise (Bearer)
- Action : SetPasswordAction
- Request body (forme plate ou JSON:API
data.attributes) :
{ "currentPassword": "MyOldPassw0rd!", "newPassword": "MyNewPassw0rd!"}currentPasswordest requis si l’utilisateur a déjà un mot de passe (password_set_at IS NOT NULL), omis sinon (compte OAuth qui n’en a jamais défini).newPasswordest toujours requis et doit respecter la PasswordPolicy (mêmes règles que/auth/register).
Effet de bord
Section titled “Effet de bord”Lors d’un changement réussi, toutes les autres sessions sont révoquées (déconnexion de tous les appareils sauf celui en cours). Le token courant reste valide.
- Réponse
200:
{ "jsonapi": { "version": "1.1" }, "data": { "type": "passwordChanges", "id": "<user-uuid>", "attributes": { "changedAt": "2026-06-07T10:30:00+00:00", "otherSessionsRevoked": 3, "hadPasswordBefore": true } }}-
Réponse
401:meta.code = "password.currentRequired"—currentPasswordmanquant alors qu’il était attendumeta.code = "password.currentInvalid"—currentPasswordincorrect
-
Réponse
409—meta.code = "password.sameAsCurrent"(le nouveau mot de passe est identique à l’ancien) -
Réponse
422—newPasswordabsent ou échec de la PasswordPolicy. Codes :password.tooShort,password.tooLong,password.missingLowercase,password.missingUppercase,password.missingDigit,password.missingSpecial,password.invalidCharacters,password.invalidBoundary,password.containsUsername,password.containsEmail -
Notes :
- Le hash est bcrypt cost=12 (même paramètres que
/auth/register). password_set_atest mis à NOW() — utile pour audit ou logique métier ultérieure (“forcer renouvellement après N mois”).- La colonne
passwordreste NOT NULL même pour les comptes OAuth (qui stockent un hash bcrypt d’octets aléatoires inutilisable). Pour distinguer “a un vrai mot de passe” :password_set_at IS NOT NULL/User::hasPassword().
- Le hash est bcrypt cost=12 (même paramètres que
POST /api/users/me/oauth/google
Section titled “POST /api/users/me/oauth/google”Lie une identité Google au compte courant déjà authentifié. Utile pour résoudre le cas 409 oauth.linkRefused retourné par POST /api/auth/oauth/google (auto-link refusé par les règles C3) : l’utilisateur se connecte d’abord par mot de passe, puis confirme volontairement le rapprochement.
- Auth : requise (Bearer)
- Action : LinkGoogleAction
- Request body (forme plate ou JSON:API
data.attributes) :
{ "idToken": "<google id_token>" }Règles de linkage (OAuthLinkService)
Section titled “Règles de linkage (OAuthLinkService)”Vérifications dans l’ordre, refus dès le premier échec :
- Email vérifié côté Google (
email_verified = true) — sinon403 oauth.providerEmailNotVerified. - Email Google = email du compte courant — sinon
409 oauth.emailMismatch. Empêche un attaquant de coller son Google sur le compte d’une victime, et empêche une victime trompée de lier le Google d’un attaquant. - Pas déjà une identité Google sur ce compte — sinon
409 oauth.alreadyLinkedToThisUser. Un seul compte Google par user. - Le
subGoogle n’est pas déjà lié à un autre user — sinon409 oauth.alreadyLinkedToAnotherUser. La contrainte UNIQUE(provider, provider_user_id)garantit aussi cette règle au niveau base.
- Réponse
201:
{ "jsonapi": { "version": "1.1" }, "data": { "type": "oauthIdentities", "id": "<identity-uuid>", "attributes": { "provider": "google", "emailAtLink": "user@gmail.com", "createdAt": "2026-06-07T10:45:00+00:00" } }}-
Réponse
400— corps non-JSON -
Réponse
401— id_token non vérifiable (signature, iss, aud, exp…) -
Réponse
403—oauth.providerEmailNotVerified -
Réponse
409— un des trois codes :oauth.emailMismatch,oauth.alreadyLinkedToThisUser,oauth.alreadyLinkedToAnotherUser -
Réponse
422—idTokenmanquant -
Notes :
- Pas d’émission de session — l’utilisateur est déjà connecté avec son token Hydrogen, on ne fait que créer la liaison.
- Une future requête
POST /api/auth/oauth/googleavec le même id_token signera le user via le cheminexistingIdentity(200).
POST /api/users/me/oauth/apple
Section titled “POST /api/users/me/oauth/apple”Lie une identité Apple au compte courant déjà authentifié. C’est le chemin de récupération après un 409 oauth.linkRefused retourné par POST /api/auth/oauth/apple (rappel : Apple n’est jamais auto-lié, politique E.a stricte).
- Auth : requise (Bearer)
- Action : LinkAppleAction
- Request body (forme plate ou JSON:API
data.attributes) :
{ "idToken": "<apple id_token>" }Règles de linkage
Section titled “Règles de linkage”Vérifications dans l’ordre, refus dès le premier échec :
email_verifiedApple doit être vrai — sinon403 oauth.providerEmailNotVerified.- Email Apple = email du compte courant — sinon
409 oauth.emailMismatch. Une alias Private Relay (@privaterelay.appleid.com) est traité comme un email normal pour la comparaison stricte. - Pas déjà une identité Apple sur ce compte — sinon
409 oauth.alreadyLinkedToThisUser. - Le
subApple n’est pas déjà lié à un autre user — sinon409 oauth.alreadyLinkedToAnotherUser.
- Réponse
201:
{ "jsonapi": { "version": "1.1" }, "data": { "type": "oauthIdentities", "id": "<identity-uuid>", "attributes": { "provider": "apple", "emailAtLink": "abc123@privaterelay.appleid.com", "emailIsRelay": true, "createdAt": "2026-06-07T10:45:00+00:00" } }}- Réponses d’erreur :
400,401,403,409(trois codes),422— mêmes formes que pour Google avecprovider: "apple"dans les messages.
POST /api/users/me/oauth/facebook
Section titled “POST /api/users/me/oauth/facebook”Lie une identité Facebook au compte courant déjà authentifié. Identique en intention à oauth/apple mais accepte les deux flows Facebook.
- Auth : requise (Bearer)
- Action : LinkFacebookAction
- Request body :
// Flux classique{ "flow": "classic", "accessToken": "<facebook access token>" }
// Flux limited{ "flow": "limited", "idToken": "<facebook OIDC id_token>" }Règles de linkage
Section titled “Règles de linkage”- Facebook a fourni un email — sinon
422 oauth.facebook.emailMissing. Pas de signalemail_verifiedcôté Facebook (cf. matrice), donc l’étape Google « providerEmailNotVerified » est sans objet ici. - Email Facebook = email du compte courant — sinon
409 oauth.emailMismatch. - Pas déjà une identité Facebook sur ce compte — sinon
409 oauth.alreadyLinkedToThisUser. - Le
subFacebook n’est pas déjà lié à un autre user — sinon409 oauth.alreadyLinkedToAnotherUser.
- Réponse
201— même forme avecprovider: "facebook",emailIsRelay: false. - Réponse
401— token non vérifiable. - Réponse
409— codesoauth.emailMismatch/oauth.alreadyLinkedToThisUser/oauth.alreadyLinkedToAnotherUser. - Réponse
422—flowinvalide, token manquant pour le flux choisi, ouoauth.facebook.emailMissing.
GET /api/users/me/oauth-identities
Section titled “GET /api/users/me/oauth-identities”Liste toutes les identités OAuth liées au compte courant. Renvoie une collection JSON:API, possiblement vide pour un compte créé par mot de passe sans lien OAuth.
-
Auth : requise (Bearer)
-
Action : ListOAuthIdentitiesAction
-
Pas de body (GET)
-
Réponse
200:
{ "jsonapi": { "version": "1.1" }, "data": [ { "type": "oauthIdentities", "id": "<identity-uuid>", "attributes": { "provider": "google", "emailAtLink": "user@gmail.com", "emailIsRelay": false, "linkedAt": "2026-06-07T10:45:00+00:00" } } ], "meta": { "count": 1 }}-
Réponse
401— token absent / invalide -
Notes :
- Tri par
created_at ASC(plus ancienne liaison en premier). provider_user_id(lesubdu provider) n’est pas exposé — il n’a aucun usage côté client.emailAtLinkest l’email retourné par le provider au moment de la liaison (peut différer de l’email actuel du user).emailIsRelayvauttrueuniquement pour les identités Apple créées avec un alias Private Relay (@privaterelay.appleid.com). Toujoursfalsepour Google et Facebook.
- Tri par
GET /api/users/me/settings
Section titled “GET /api/users/me/settings”Retourne le snapshot complet des préférences de l’utilisateur courant : les valeurs par défaut fusionnées avec les éventuels overrides stockés en base. Le client n’a jamais à gérer le cas « préférence pas encore définie » — chaque clé connue est toujours présente.
-
Auth : requise (Bearer)
-
Action : GetSettingsAction
-
Pas de body (GET)
-
Réponse
200:
{ "jsonapi": { "version": "1.1" }, "data": { "type": "userSettings", "id": "<user-uuid>", "attributes": { "locale": "fr-FR", "timezone": "Europe/Paris", "theme": "system", "currency": "EUR", "profileVisibility": "public", "notificationsEmail": true, "notificationsPush": true, "showSensitiveContent": false, "emailVisibleOnProfile": false } }}- Réponse
401— token absent / invalide
Catalogue des clés
Section titled “Catalogue des clés”Définies dans UserSettingRegistry :
| Clé | Type | Défaut | Valeurs autorisées |
|---|---|---|---|
locale | locale | fr-FR | locale effectivement supportée par l’app (voir GET /api/i18n/locales) |
timezone | timezone | Europe/Paris | identifiant IANA (DateTimeZone::listIdentifiers()) |
theme | enum | system | light, dark, system |
currency | currency | EUR | code ISO 4217 présent dans la table currency |
profileVisibility | enum | public | public, private |
notificationsEmail | bool | true | — |
notificationsPush | bool | true | — |
showSensitiveContent | bool | false | — |
emailVisibleOnProfile | bool | false | — |
PATCH /api/users/me/settings
Section titled “PATCH /api/users/me/settings”Met à jour partiellement les préférences de l’utilisateur courant.
- Auth : requise (Bearer)
- Action : UpdateSettingsAction
- Request body (forme plate ou JSON:API
data.attributes) — toutes les clés sont optionnelles, seules les clés présentes sont touchées :
{ "theme": "dark", "notificationsEmail": false}Sémantique « tout ou rien »
Section titled “Sémantique « tout ou rien »”Si une seule clé/valeur échoue à la validation, aucune écriture n’a lieu et la réponse 422 énumère toutes les erreurs. Le client ne se retrouve jamais avec un état mi-appliqué.
Stockage « overrides only » (D.c)
Section titled “Stockage « overrides only » (D.c)”La table user_preference ne stocke que les overrides — pas les défauts.
- Si la valeur écrite égale le défaut actuel, la ligne correspondante est supprimée.
- Sinon, elle est upsertée (
INSERT … ON DUPLICATE KEY UPDATE).
Conséquence : changer un défaut dans le code propage automatiquement à tous les comptes qui n’ont pas d’override explicite.
- Réponse
200— snapshot complet post-update +meta.appliedKeyslistant les clés réellement écrites/supprimées :
{ "jsonapi": { "version": "1.1" }, "data": { "type": "userSettings", "id": "<user-uuid>", "attributes": { "locale": "fr-FR", "timezone": "Europe/Paris", "theme": "dark", "currency": "EUR", "profileVisibility": "public", "notificationsEmail": false, "notificationsPush": true, "showSensitiveContent": false, "emailVisibleOnProfile": false } }, "meta": { "appliedKeys": ["theme", "notificationsEmail"] }}-
Réponse
400— corps non-JSON ou non-objet -
Réponse
422— un ou plusieurserrors[]avecsource.pointer = /data/attributes/<key>etmeta.codeparmi :setting.unknown— clé inconnue du registrysetting.expectedBool— la valeur d’une clé booléenne n’est pas untrue/falsesetting.invalidEnumValue— valeur absente de la liste autoriséesetting.invalidLocale— locale non supportée par l’app (cf. GET /api/i18n/locales)setting.invalidTimezone— identifiant IANA inconnusetting.invalidCurrency— code ISO 4217 inconnu de la tablecurrency
-
Notes :
- Un body vide (
{}) est accepté et retourne200avecappliedKeys: []. - Les booléens sont strictement typés : les chaînes
"true"/"false"sont rejetées (setting.expectedBool). - Le ramassage des erreurs est exhaustif : tous les champs invalides sont signalés en un seul appel, pas seulement le premier.
- Un body vide (
POST /api/users/me/avatar
Section titled “POST /api/users/me/avatar”Téléverse (ou remplace) l’avatar de l’utilisateur authentifié.
-
Auth : requise (Bearer)
-
Action : UploadAvatarAction
-
Body :
multipart/form-dataavec un unique champ fichieravatar.- Formats acceptés : JPEG, PNG, WebP, GIF, HEIC, HEIF.
- Taille maximale :
AVATAR_MAX_UPLOAD_BYTES(par défaut 256 000 octets / 256 ko).
-
Pipeline (AvatarUploadService) :
- Validation (taille, code d’erreur PSR-7, non vide).
- Décodage Imagick — un fichier non décodable ⇒
422 invalidImage. - Whitelist du format source. Animations (GIF/HEIF) : seule la première frame est conservée.
- Auto-orientation EXIF puis strip complet des métadonnées (GPS, appareil, etc. — privacy).
- Redimension
bestfitàAVATAR_MAX_DIMENSION(256px par défaut ; pas d’upscale). - Encodage WebP qualité
AVATAR_WEBP_QUALITY(80 par défaut,webp:method=6). - Écriture atomique (
tmp + rename) sous<AVATAR_STORAGE_PATH>/user/AA/BB/CC/<uuid-hex>/avatar.webp(3 niveaux de dossiers ventilés à partir des 6 premiers caractères hex de l’UUID). - Mise à jour de
user.avatar_updated_at(cache-buster). - Re-hydratation de l’utilisateur.
-
Réponse
200 OK: ressourceuserscomplète avecavatarUrlactualisée (et son cache-buster?v=<timestamp>).
{ "data": { "type": "users", "id": "<uuid>", "attributes": { "avatarUrl": "http://hexatrip-static.dev.com/user/c2/1e/78/c21e7856eb524c8cb9a7786a2f80ce7e/avatar.webp?v=1812345678", "hasAvatar": true, "…": "…" } }}-
Réponses d’erreur (mapping
AvatarErrorCode→ HTTP) :400avatar.uploadFailed— échec côté transport multipart413avatar.tooLarge— fichier >AVATAR_MAX_UPLOAD_BYTES415avatar.unsupportedFormat— format hors whitelist422avatar.empty— pas de fichier ou octets vides (source.pointer = /data/attributes/avatar)422avatar.invalidImage— octets non décodables comme image500avatar.encodingFailed— Imagick a refusé de produire le WebP500avatar.storageWriteFailed— écriture disque échouée
-
Notes :
- L’avatar est toujours écrit à la même URL (
avatar.webp). C’est?v=<unix-ts>(basé suravatar_updated_at) qui force l’invalidation cache navigateur/CDN entre deux uploads. - Quand l’utilisateur n’a pas d’avatar,
avatarUrlpointe vers<AVATAR_PUBLIC_URL>/user/default-avatar.webp(pas de cache-buster — c’est un fichier ops).
- L’avatar est toujours écrit à la même URL (
DELETE /api/users/me/avatar
Section titled “DELETE /api/users/me/avatar”Supprime l’avatar de l’utilisateur authentifié (fichier sur disque + timestamp en base). L’utilisateur retombe sur l’avatar par défaut partagé.
- Auth : requise (Bearer)
- Action : DeleteAvatarAction
- Body : aucun
- Idempotent : appelé sur un compte qui n’a déjà plus d’avatar, retourne quand même
200. - Réponse
200 OK: ressourceuserscomplète avecavatarUrlré-orientée vers le default avatar ethasAvatar: false.
POST /api/users/me/cover
Section titled “POST /api/users/me/cover”Téléverse (ou remplace) la cover de profil de l’utilisateur authentifié.
-
Auth : requise (Bearer)
-
Action : UploadCoverAction
-
Body :
multipart/form-dataavec un unique champ fichiercover.- Formats acceptés : JPEG, PNG, WebP, GIF, HEIC, HEIF.
- Taille maximale :
COVER_MAX_UPLOAD_BYTES(par défaut 600 000 octets / 600 ko).
-
Pipeline (CoverUploadService) : identique à l’upload d’avatar, à deux différences près :
- Redimension
bestfità l’intérieur deCOVER_MAX_WIDTH × COVER_MAX_HEIGHT(par défaut 1500 × 500 px), aspect ratio conservé — une image 3000 × 800 sera réduite à 1500 × 400 (pas d’upscale, pas de crop). - Le fichier est écrit dans le même dossier ventilé que l’avatar, mais sous le nom
cover.webp:<AVATAR_STORAGE_PATH>/user/AA/BB/CC/<uuid-hex>/cover.webp.
- Redimension
-
Effet de bord :
user.cover_updated_atest mis à jour (cache-buster URL). -
Réponse
200 OK: ressourceuserscomplète aveccoverUrlactualisée (et son cache-buster?v=<timestamp>).
{ "data": { "type": "users", "id": "<uuid>", "attributes": { "coverUrl": "http://hexatrip-static.dev.com/user/c2/1e/78/c21e7856eb524c8cb9a7786a2f80ce7e/cover.webp?v=1812345678", "hasCover": true, "…": "…" } }}-
Réponses d’erreur (mapping
CoverErrorCode→ HTTP) :400cover.uploadFailed— échec côté transport multipart413cover.tooLarge— fichier >COVER_MAX_UPLOAD_BYTES415cover.unsupportedFormat— format hors whitelist422cover.empty— pas de fichier ou octets vides (source.pointer = /data/attributes/cover)422cover.invalidImage— octets non décodables comme image500cover.encodingFailed— Imagick a refusé de produire le WebP500cover.storageWriteFailed— écriture disque échouée
-
Notes :
- Comme pour l’avatar, l’URL est toujours
cover.webp; c’est?v=<unix-ts>qui force l’invalidation cache. - Sans cover,
coverUrlpointe vers<AVATAR_PUBLIC_URL>/user/default-cover.webp(sans cache-buster).
- Comme pour l’avatar, l’URL est toujours
DELETE /api/users/me/cover
Section titled “DELETE /api/users/me/cover”Supprime la cover de l’utilisateur authentifié (fichier sur disque + timestamp en base). L’utilisateur retombe sur la cover par défaut partagée.
- Auth : requise (Bearer)
- Action : DeleteCoverAction
- Body : aucun
- Idempotent : appelé sur un compte qui n’a déjà plus de cover, retourne quand même
200. - Réponse
200 OK: ressourceuserscomplète aveccoverUrlré-orientée vers la default cover ethasCover: false.