feat(cabana): add backend user avatars to My account and the top bar

- cabana.BackendUser is an attach.Owner (MorphName Backend\Models\User,
  cabana.BackendUserMorphType) with one public attachOne avatar
  (cabana.BackendUserAvatarField), as WinterCMS stores it
- POST /account/avatar stores one file_data image (jpg, jpeg, png, gif or
  webp, image guard, at most cabana.AvatarMaxBytes = 2 MiB, under a
  server-generated name) and replaces the previous avatar: old rows are
  deleted in the transaction, their blobs after the commit
- DELETE /account/avatar removes the avatar and is idempotent
- GET /auth/me and the account routes return avatar {url, thumb_url}
  (250x250 crop); cabana.BackendUserAvatar lets plugins read it
- swag annotations, regenerated admin.json and schema.d.ts; route
  inventories, CSRF walk (29) and OpenAPI conformance learn both routes
- SPA: avatar upload and remove on My account, avatar image in the user
  menu trigger and header; backend::lang.myaccount avatar strings
- rebuilt boardwalk dist; cabana README, users-and-permissions and
  admin-spa docs
This commit is contained in:
2026-10-09 17:45:43 +02:00
parent 143eca5a1f
commit 64d21c8caf
25 changed files with 1012 additions and 49 deletions

View File

@@ -64,6 +64,20 @@
],
"type": "object"
},
"cabana.AdminAvatar": {
"properties": {
"thumb_url": {
"type": "string"
},
"url": {
"type": "string"
}
},
"required": [
"url"
],
"type": "object"
},
"cabana.AdminFileCaptionRequest": {
"properties": {
"description": {
@@ -158,6 +172,14 @@
},
"cabana.AdminProfile": {
"properties": {
"avatar": {
"allOf": [
{
"$ref": "#/components/schemas/cabana.AdminAvatar"
}
],
"description": "Avatar is present when the admin has a public avatar."
},
"email": {
"type": "string"
},
@@ -2132,6 +2154,136 @@
]
}
},
"/account/avatar": {
"delete": {
"description": "Removes the signed-in admin's avatar and its files. Removing an avatar that is not there also answers 200.",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.Envelope-cabana_AdminProfile"
}
}
},
"description": "OK"
},
"401": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Unauthorized"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Forbidden"
}
},
"security": [
{
"BackendBearer": []
}
],
"summary": "Remove my avatar",
"tags": [
"admin"
]
},
"post": {
"description": "Stores one multipart file_data part as the signed-in admin's avatar, a public attachOne (system_files attachment_type Backend\\Models\\User, field avatar) under a server-generated name, and replaces the previous avatar. Only jpg, jpeg, png, gif and webp images that pass the image check, of at most 2 MiB, are accepted; anything else is a 422 on avatar. A body over the upload cap answers 413 payload_too_large.",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"properties": {
"file_data": {
"description": "Image (jpg, jpeg, png, gif or webp, at most 2 MiB)",
"format": "binary",
"type": "string"
}
},
"required": [
"file_data"
],
"type": "object"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.Envelope-cabana_AdminProfile"
}
}
},
"description": "OK"
},
"401": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Unauthorized"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Forbidden"
},
"413": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Request Entity Too Large"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
}
}
},
"description": "Unprocessable Entity"
}
},
"security": [
{
"BackendBearer": []
}
],
"summary": "Upload my avatar",
"tags": [
"admin"
]
}
},
"/auth/login": {
"post": {
"parameters": [
@@ -2240,7 +2392,7 @@
},
"/auth/me": {
"get": {
"description": "The signed-in admin's profile: login, email, names, the subtitle (backend_users.metadata.subtitle, \"\" when not set), the superuser flag and the role.",
"description": "The signed-in admin's profile: login, email, names, the subtitle (backend_users.metadata.subtitle, \"\" when not set), the superuser flag, the role and, when the admin has one, the avatar with its public url and the 250x250 crop thumb_url.",
"responses": {
"200": {
"content": {