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

@@ -108,6 +108,15 @@ type AdminProfile struct {
Subtitle string `json:"subtitle"`
IsSuperuser bool `json:"is_superuser"`
Role *AdminRoleSummary `json:"role,omitempty"`
// Avatar is present when the admin has a public avatar.
Avatar *AdminAvatar `json:"avatar,omitempty"`
}
// AdminAvatar is an admin's avatar: the public URL of the image and of its
// 250x250 crop thumbnail (left out when the thumbnail cannot be made).
type AdminAvatar struct {
URL string `json:"url"`
ThumbURL string `json:"thumb_url,omitempty"`
}
// AdminAccountRequest is the body of PUT /account. It documents the only keys
@@ -181,7 +190,7 @@ func AdminLogout() {}
// AdminMe documents GET /auth/me.
//
// @Summary Current admin
// @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.
// @Tags admin
// @Produce json
// @Security BackendBearer
@@ -209,6 +218,36 @@ func AdminMe() {}
// @Router /account [put]
func AdminAccountUpdate() {}
// AdminAccountAvatarUpload documents POST /account/avatar.
//
// @Summary Upload my avatar
// @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.
// @Tags admin
// @Accept multipart/form-data
// @Produce json
// @Security BackendBearer
// @Param file_data formData file true "Image (jpg, jpeg, png, gif or webp, at most 2 MiB)"
// @Success 200 {object} Envelope[AdminProfile]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 413 {object} ErrorEnvelope
// @Failure 422 {object} ErrorEnvelope
// @Router /account/avatar [post]
func AdminAccountAvatarUpload() {}
// AdminAccountAvatarRemove documents DELETE /account/avatar.
//
// @Summary Remove my avatar
// @Description Removes the signed-in admin's avatar and its files. Removing an avatar that is not there also answers 200.
// @Tags admin
// @Produce json
// @Security BackendBearer
// @Success 200 {object} Envelope[AdminProfile]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Router /account/avatar [delete]
func AdminAccountAvatarRemove() {}
// AdminNavigation documents GET /navigation.
//
// @Summary Admin navigation