feat(cabana): add the My account page with subtitle and password change

- lagoon: 202610090001_backend_users_metadata adds WinterCMS's nullable
  jsonable backend_users.metadata column (TEXT); rollback drops only it
- cabana.BackendUser.Metadata and Subtitle read metadata.subtitle leniently
  (NULL, blank, null, [], invalid JSON and non-strings read as empty)
- GET /auth/me returns subtitle; PUT /account edits the signed-in admin's
  own names, email and subtitle, and the password with current_password;
  any other key is a 422 and nothing is written
- email: required, valid, 6-255 chars, unique against other admins' email
  and login ignoring case (soft-deleted included); a unique-index race is 422
- a password change sets tokens_valid_after, revokes the presented token and
  gives a cookie session a fresh cookie
- swag annotation, regenerated admin.json and schema.d.ts; route inventories,
  CSRF walk (27) and OpenAPI conformance learn the route
- SPA: My account item in the user menu opens /account (AccountView), which
  saves through PUT /account and updates the top bar via setCurrentUser
- backend::lang.myaccount strings (en, pl), rebuilt boardwalk dist
- cabana and lagoon READMEs, users-and-permissions and admin-spa docs
This commit is contained in:
2026-10-09 17:38:04 +02:00
parent 5ba1a4ae1d
commit 143eca5a1f
33 changed files with 1191 additions and 47 deletions

View File

@@ -4,6 +4,86 @@
*/
export interface paths {
"/account": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
/**
* Update my account
* @description Saves the signed-in admin's own first name, last name, email and subtitle; the row is the token subject, never a request value. Only the AdminAccountRequest keys are accepted: any other key (login, role_id, is_superuser, permissions, is_activated, id) is a 422 on that key and nothing is written. The email must be valid, 6 to 255 characters, and not equal (ignoring case) the email or login of another admin. A non-empty password needs current_password, at least 4 characters, at most 72 bytes and an equal password_confirmation; it ends every other session, and a cookie session receives a fresh cookie.
*/
put: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** @description Profile and optional password change */
requestBody: {
content: {
"application/json": components["schemas"]["cabana.AdminAccountRequest"];
};
};
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"];
};
};
};
};
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/auth/login": {
parameters: {
query?: never;
@@ -127,7 +207,10 @@ export interface paths {
path?: never;
cookie?: never;
};
/** Current admin */
/**
* 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.
*/
get: {
parameters: {
query?: never;
@@ -4286,6 +4369,15 @@ export interface paths {
export type webhooks = Record<string, never>;
export interface components {
schemas: {
"cabana.AdminAccountRequest": {
current_password?: string;
email: string;
first_name?: string;
last_name?: string;
password?: string;
password_confirmation?: string;
subtitle?: string;
};
"cabana.AdminActionRequest": {
/**
* @description Payload is the widget's own JSON value (the summer-action event's
@@ -4343,6 +4435,8 @@ export interface components {
last_name: string;
login: string;
role?: components["schemas"]["cabana.AdminRoleSummary"];
/** @description Subtitle is backend_users.metadata.subtitle; "" when not set. */
subtitle: string;
};
"cabana.AdminRecord": {
[key: string]: unknown;

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']
/** Body of PUT /account: the only keys My account may change. */
export type AdminAccountRequest = Schemas['cabana.AdminAccountRequest']
export type NavigationEntry = Schemas['cabana.NavigationEntry']
export type SettingsEntry = Schemas['cabana.SettingsEntry']
export type SettingsResult = Schemas['cabana.SettingsResult']