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

@@ -99,15 +99,32 @@ type AdminRoleSummary struct {
// AdminProfile is the GET /auth/me payload.
type AdminProfile struct {
ID uint `json:"id"`
Login string `json:"login"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
ID uint `json:"id"`
Login string `json:"login"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
// Subtitle is backend_users.metadata.subtitle; "" when not set.
Subtitle string `json:"subtitle"`
IsSuperuser bool `json:"is_superuser"`
Role *AdminRoleSummary `json:"role,omitempty"`
}
// AdminAccountRequest is the body of PUT /account. It documents the only keys
// the route accepts: any other key is a 422 on that key and nothing is
// written. PUT replaces the profile, so an absent first_name, last_name or
// subtitle is saved empty. The password keys change the password only when
// password is not empty, and then current_password must match.
type AdminAccountRequest struct {
Email string `json:"email"`
FirstName *string `json:"first_name,omitempty"`
LastName *string `json:"last_name,omitempty"`
Subtitle *string `json:"subtitle,omitempty"`
CurrentPassword string `json:"current_password,omitempty"`
Password string `json:"password,omitempty"`
PasswordConfirmation string `json:"password_confirmation,omitempty"`
}
// AdminLogin documents POST /auth/login.
//
// @Summary Admin login
@@ -164,6 +181,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.
// @Tags admin
// @Produce json
// @Security BackendBearer
@@ -174,6 +192,23 @@ func AdminLogout() {}
// @Router /auth/me [get]
func AdminMe() {}
// AdminAccountUpdate documents PUT /account.
//
// @Summary 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.
// @Tags admin
// @Accept json
// @Produce json
// @Security BackendBearer
// @Param body body AdminAccountRequest true "Profile and optional password change"
// @Success 200 {object} Envelope[AdminProfile]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 413 {object} ErrorEnvelope
// @Failure 422 {object} ErrorEnvelope
// @Router /account [put]
func AdminAccountUpdate() {}
// AdminNavigation documents GET /navigation.
//
// @Summary Admin navigation