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

@@ -84,6 +84,132 @@ export interface paths {
patch?: never;
trace?: never;
};
"/account/avatar": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* 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.
*/
post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"multipart/form-data": {
/**
* Format: binary
* @description Image (jpg, jpeg, png, gif or webp, at most 2 MiB)
*/
file_data: string;
};
};
};
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.Envelope-cabana_AdminProfile"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Request Entity Too Large */
413: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Unprocessable Entity */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
};
};
/**
* Remove my avatar
* @description Removes the signed-in admin's avatar and its files. Removing an avatar that is not there also answers 200.
*/
delete: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.Envelope-cabana_AdminProfile"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
};
};
};
};
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/auth/login": {
parameters: {
query?: never;
@@ -209,7 +335,7 @@ export interface paths {
};
/**
* 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.
*/
get: {
parameters: {
@@ -4401,6 +4527,10 @@ export interface components {
};
message: string;
};
"cabana.AdminAvatar": {
thumb_url?: string;
url: string;
};
"cabana.AdminFileCaptionRequest": {
description?: string;
title?: string;
@@ -4428,6 +4558,8 @@ export interface components {
html: string;
};
"cabana.AdminProfile": {
/** @description Avatar is present when the admin has a public avatar. */
avatar?: components["schemas"]["cabana.AdminAvatar"];
email: string;
first_name: string;
id: number;

View File

@@ -11,6 +11,8 @@ export type AdminLoginData = Schemas['cabana.AdminLoginData']
export type AdminLoginRequest = Schemas['cabana.AdminLoginRequest']
export type AdminLogoutData = Schemas['cabana.AdminLogoutData']
export type AdminProfile = Schemas['cabana.AdminProfile']
/** An admin's avatar: the public image URL and its 250x250 thumbnail URL. */
export type AdminAvatar = Schemas['cabana.AdminAvatar']
/** Body of PUT /account: the only keys My account may change. */
export type AdminAccountRequest = Schemas['cabana.AdminAccountRequest']
export type NavigationEntry = Schemas['cabana.NavigationEntry']