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

@@ -124,7 +124,7 @@ func (p *Plugin) Migrations() []*gormigrate.Migration {
| `lagoon.RollbackLast` | Rolls back the last migration of one plugin. |
| `lagoon.Status` | Lists applied migration IDs per plugin as `lagoon.StatusRow` values. |
| `lagoon.HistoryTableName` | Returns the gormigrate history table for a plugin ID. |
| `lagoon.BackendAdminMigrations` | Creates the backend user, role and admin token blacklist tables, seeds the system roles and adds a case-insensitive unique index on `backend_users.email`. The index migration refuses to run while emails differ only in case and names the clashing logins. |
| `lagoon.BackendAdminMigrations` | Creates the backend user, role and admin token blacklist tables, seeds the system roles, adds a case-insensitive unique index on `backend_users.email` and adds WinterCMS's nullable jsonable `backend_users.metadata` column (TEXT; the admin subtitle lives under its `subtitle` key). The index migration refuses to run while emails differ only in case and names the clashing logins. Rolling back the metadata migration drops only that column. |
| `lagoon.QueueMigrations` | Migrates River's schema to `lagoon.RiverSchemaVersion` and creates the `summer_jobs` job record table. |
| `lagoon.QueueHistoryID` | History id of the job-queue set, `summercms.conga`. |
| `lagoon.JobsTable` | Name of the job record table, `summer_jobs`. |

View File

@@ -13,8 +13,9 @@ import (
const BackendJWTBlacklistTable = "backend_jwt_blacklist"
// BackendAdminMigrations creates Winter-shaped backend identity tables, seeds
// the developer and publisher system roles and makes backend user emails
// unique case-insensitively. History is isolated under the summercms.cabana
// the developer and publisher system roles, makes backend user emails
// unique case-insensitively and adds WinterCMS's nullable jsonable metadata
// column to backend_users (the admin subtitle lives there). History is isolated under the summercms.cabana
// plugin id. DDL is re-runnable so the system-role seed stays idempotent if
// the history row is removed.
var BackendAdminMigrations = []*gormigrate.Migration{
@@ -117,4 +118,16 @@ FROM backend_users GROUP BY lower(email) HAVING count(*) > 1 ORDER BY 1`).Scan(&
return tx.Exec(`DROP INDEX IF EXISTS backend_users_email_lower_unique`).Error
},
},
{
// WinterCMS 2023_02_16_000012_Db_Backend_Add_User_Metadata: a
// nullable jsonable column. It is TEXT, not JSONB, so a copied value
// such as [] or a blank string loads unchanged.
ID: "202610090001_backend_users_metadata",
Migrate: func(tx *gorm.DB) error {
return tx.Exec(`ALTER TABLE backend_users ADD COLUMN IF NOT EXISTS metadata TEXT`).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.Exec(`ALTER TABLE backend_users DROP COLUMN IF EXISTS metadata`).Error
},
},
}

View File

@@ -100,7 +100,7 @@ func TestBackendAdminMigration(t *testing.T) {
"password": false, "activation_code": true, "persist_code": true, "reset_password_code": true,
"permissions": true, "is_activated": false, "is_superuser": false, "role_id": true,
"activated_at": true, "last_login": true, "created_at": false, "updated_at": false,
"deleted_at": true, "tokens_valid_after": true,
"deleted_at": true, "tokens_valid_after": true, "metadata": true,
}
assertColumns(t, "backend_users", users, wantUsers)
roles := columnNullability(t, gdb, "backend_user_roles")
@@ -481,7 +481,9 @@ func TestBackendAdminEmailIndexRefusesCaseDuplicates(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := m.RollbackLast(); err != nil {
// Roll back to the identity tables, which drops the email index and the
// later metadata column.
if err := m.RollbackTo("202609240001_backend_admin_identity"); err != nil {
t.Fatal(err)
}
// A Winter copy can hold emails that differ only in case.