Commit Graph

77 Commits

Author SHA1 Message Date
89611eaaf9 test(cabana): cover My account profile, password and avatar
- account_internal_test: metadata subtitle reader and writer (NULL, blank,
  null, [], invalid JSON, non-objects, key preservation, NULL on the last
  key), the attachOne contract, avatar cap, unique-violation mapping,
  English fallbacks, and 401 for non-backend principals on all three routes
- account_test (Postgres): profile payload incl. copied WinterCMS metadata
  and avatar rows, trimmed profile saves and metadata keys, the forbidden-key
  allow-list with unchanged rows, email rules, password rules, the failed
  attempt log, Bearer and cookie session effects, CSRF, avatar upload,
  refusals (SVG, HTML as PNG, size, part shape, 413, no bucket) with no row
  or blob left, replace and remove with blob cleanup
- lagoon: TestBackendAdminMetadataColumn covers type, Winter row round trip,
  rollback of only the column and re-migrate
- SPA: AccountView, the user menu account item and avatar image,
  setCurrentUser and profileInitials; lang fixture gains myaccount keys
2026-10-09 17:59:27 +02:00
4f1cda33d7 fix(cabana): answer an empty or malformed avatar upload body with 422
- POST /account/avatar with no multipart part (NextPart io.EOF) or a broken
  boundary fell through to the generic 500; it is now the invalid-body 422
- a body over the cap still answers 413 payload_too_large
2026-10-09 17:53:21 +02:00
64d21c8caf 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
2026-10-09 17:45:43 +02:00
143eca5a1f 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
2026-10-09 17:38:04 +02:00
Jakub Zych
da21670870 feat(surf): print the serve URL and admin status on startup
- Add surf.Router.AdminPrefix (additive): the admin mount path and true
  when BuildRouter mounted the cabana admin, false otherwise
- serve binds with net.Listen before printing, so a busy port fails
  without a misleading URL, then prints the listen address with a
  browser URL and an admin status line: the admin URL, a warning naming
  summer admin:create when backend_users is empty or missing, or
  admin: off with the steps to add an admin controller
- The administrator count is bounded by a 2 s timeout and never fails boot
- route:list ends with the same admin status line
- surf and cabana READMEs and the console and routing docs describe the output
2026-10-08 16:36:21 +02:00
Jakub Zych
fa25b4e000 fix(cabana): show to-many relation list columns joined with commas 2026-10-07 18:43:17 +02:00
Jakub Zych
4a9b0896b1 feat(cabana): settings entries that link to an admin controller
pact.SettingsItem gains an additive Controller field, the equivalent of a
WinterCMS registerSettings 'url' => Backend::url(...) entry. A link entry
declares no Model, Form or NewModel and needs no AdminFS; start-up fails
when it combines Controller with a singleton form or a model, or names an
unregistered controller. Settings codes stay one namespace.

GET /settings lists a link entry with its controller only when the
principal passes the item's permissions and may open the controller.
Registry.Setting never returns a link entry, so the singleton settings
endpoints answer 404 for its code. SettingsEntry carries controller,
empty for singletons; the OpenAPI document, generated SPA types and
settings fixture follow, and the pact and cabana READMEs and the
settings docs describe the link.
2026-10-07 18:22:01 +02:00
Jakub Zych
aa3a7c2e90 docs(cabana): document mltextarea field type
- forms.md: field-types row, ML section and example, lift paragraph,
  preset refusal
- admin-controllers.md and the cabana README name mltextarea
2026-10-06 21:37:08 +02:00
Jakub Zych
3596df4ff5 feat(cabana): add mltextarea field type
- mltextarea compiles (with size), binds as a writable scalar and goes
  through the mltext lift, translation write and hydrate paths
- preset on or from an mltextarea field stays refused
- tests: compile, Postgres round trip over a nullable host column with
  multi-line text, invalid locale maps, preset refusals
2026-10-06 21:30:56 +02:00
Jakub Zych
5932172148 docs(cabana): document preset on mltext fields
- schema_types.go and the form-schema swag description name text or mltext
- regenerated admin/openapi/admin.json and admin/src/api/schema.d.ts
- README and docs/backend/forms.md describe the per-locale rules
2026-10-06 21:20:25 +02:00
Jakub Zych
7afad732c7 feat(cabana): accept preset on mltext fields
- compilePresetKey admits a text or mltext target
- checkPresets admits a text or mltext source
- TestPresetML pins every text/mltext pairing and the refusals
2026-10-06 21:09:48 +02:00
Jakub Zych
b492e79f2b feat(cabana): add markdown preview admin route
- POST {prefix}/api/v1/markdown/preview renders {markdown} through
  cabana.RenderMarkdown in the backend-guarded group behind requireAjax
- refused output is a 422 validation_failed on markdown with a fixed message
- swag annotation, regenerated admin.json and schema.d.ts
- route inventories, CSRF walk (26) and OpenAPI conformance learn the route
- README and docs/backend/forms.md document the route
2026-10-06 20:53:07 +02:00
Jakub Zych
4f7e69fd2b test(cabana): cover pointer scalars in ML host hydration
- table-driven hostScalarString cases for nil and live pointers to string, ints, uint, float, bool, []byte, named string and double pointers
- DB-free hydrateMLRecord cases for nil, empty and filled *string hosts
2026-10-06 20:36:59 +02:00
Jakub Zych
90b87d6363 fix(cabana): dereference nullable pointer scalars in ML host hydration
- hostScalarString walks pointers via reflect; nil at any depth is ""
- string kinds return raw text, byte slices decode, other kinds format the dereferenced value
- Postgres round-trip regression for an mltext field on a *string column
2026-10-06 20:35:10 +02:00
Jakub Zych
4e37d877f1 feat(14.2.1-06): hydrate relation-child update and show ML maps
UpdateChild and ShowChild now lift, apply, and hydrate nested locales the same way as host CRUD, so a child form keeps English on the column and Polish through the writer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 15:53:24 +02:00
Jakub Zych
88a3a88876 feat(14.2.1-06): persist nested ML maps on relation-child create
RelationService looks up TranslationWriter like CRUDService so CreateChild can lift locale maps before fillChild, write Polish after the child PK, and return a hydrated title map.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 15:51:38 +02:00
Jakub Zych
424fe9ccc0 feat(14.2.1-05): cover mlmarkdown, child adopt, and regen admin artifacts
MLMarkdownField and relation-child forms share the enabled-locale
inject and adopt merge; OpenAPI, types, docs, and boardwalk dist match.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 15:41:46 +02:00
Jakub Zych
0f8d13738d feat(14.2.1-05): hydrate ML create/GET/save through TranslatedExact
Form schema meta lists enabled locales, Show/save expand mltext maps
without D-11 fallback, and the SPA seeds and adopt-merges every locale.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 15:37:00 +02:00
Jakub Zych
1bc6200dbd test(14.2.1-04): add ML, markdown, resolver, and SPA test matrices
Prove nested ML writes, unsafe markdown rejection, request-locale isolation, and generated FormView types.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 14:02:49 +02:00
Jakub Zych
e3b3475b1c feat(14.2.1-03): document ML fields and regenerate admin artifacts
- Document markdown, mltext, and mlmarkdown plus TranslationWriter save semantics
- Extend the SPA registry tests and rebuild committed boardwalk dist

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 13:29:00 +02:00
Jakub Zych
a04116d41f feat(14.2.1-03): add markdown and multilingual cabana fields
Lift locale maps before ProjectWritableFields so a Journal-shaped save can persist the default host scalar and non-default locales through TranslationWriter without dropping nested JSON.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 12:32:54 +02:00
Jakub Zych
e723c391fb docs(cabana): widget element contract and payload fixture (quick-261006-eyj)
- acme-demo-lookup fixture is a reorder widget: a click reverses its items,
  sends the new id order as detail.payload and renders the returned items from
  the data attribute and the summer-result event with textContent, no HTTP
- partials-and-widgets gains a Widget element contract subsection listing the
  attributes the SPA sets and the summer-action / summer-result events
- cabana README names both events in the form widgets bullet
2026-10-06 11:18:03 +02:00
Jakub Zych
6af88f9df6 feat(cabana): widget action payload and data channel (quick-261006-eyj)
- pact.AdminActionInput.Payload (json.RawMessage) carries the widget's own
  JSON value untouched; pact.AdminActionResult.Data is passed through as data
- cabana decodes payload with a 64 KiB cap (422 on body), refuses it on the
  toolbar and record routes, and embeds Data once encoded with a 256 KiB cap
  (opaque 500 when larger or unencodable); fill stays filtered
- root .swaggo overrides json.RawMessage so swag keeps record_id and values;
  admin.json and schema.d.ts regenerated (payload?: unknown, data?: unknown)
- TestWidgetPayloadAndData covers pass-through, cap, refusal and data 500
- cabana and pact READMEs, partials-and-widgets and admin-spa docs updated
2026-10-06 11:13:16 +02:00
Jakub Zych
2e94cbf9f8 test(12.1-05): unit tests for bulk and record actions, row state, forbidden, preview and the form seams
- bulk action: empty, duplicate, unordered, absent, partial, out-of-scope, rollback, concurrent runs, permissions, CSRF, body cap
- record action: scope, Applies, strict body, offered order, rollback, Applies error
- ForbiddenError from every Form hook, the bulk delete and the relation link and child hooks
- permission editor modes, locked codes and provider errors; relation locks on create, update and belongsTo
- TestPhase121BootErrors: every boot error of plans 01 and 02 with plugin, controller and file
- pact: the action, row state and filter contracts on a sample controller
2026-10-05 14:48:34 +02:00
Jakub Zych
c076b4c059 test(12.1-05): threat test for the Phase 12.1 framework contracts and the first gate stages
- TestPhase121Threats: one subtest per mitigated threat T-12.1-01 to T-12.1-15
- roster fixture: sentinel names and knobs for failing hooks and providers
- scripts/check-phase12.1.sh: fail-closed go test detector, --self-test and --security
2026-10-05 14:30:31 +02:00
Jakub Zych
df5cace852 feat(12.1-02): writable foreign keys, locked relation options, invisible columns
- FieldRelationContract.WritableForeignKey makes a belongsTo field over a
  protected foreign key writable; the protected key list is unchanged
- cabana.RelationLockProvider names related ids an administrator may not add
  or remove: options and labels carry locked, and a create or update that
  changes the locked subset is 403 before any row is written
- columns.yaml invisible keeps a column searchable and out of the rows
- a controller implementing pact.FilterOptions serves a scope filter's
  choices before the model
- SPA: locked chips and options in RelationField, DataTable skips invisible
  columns
- README, docs, OpenAPI document, TS types and dist updated
2026-10-05 10:58:38 +02:00
Jakub Zych
f50d9b8f10 feat(12.1-02): permissioneditor field in radio or checkbox mode
- type: permissioneditor with mode radio (1, -1) or checkbox (1); the
  controller serves the options per request through
  cabana.PermissionEditorProvider and reads and stores the values
- a save answers 422 for a non-object, an unknown code or a value outside the
  mode's set and 403 for a changed locked code; stored codes that are not
  offered are kept
- record responses carry the stored permissions as an object
- SPA: PermissionEditorField with sections by tab, locked rows and a read-only
  mode for the preview
- README, docs, OpenAPI document, TS types and dist updated
2026-10-05 10:44:50 +02:00
Jakub Zych
a1c6bb1ce6 feat(12.1-02): password and form-only fields, rules per operation and preset
- pact.FormVirtualFields lists form fields that are not model columns: never
  bound, filled or projected; their values reach the Form hooks through
  cabana.VirtualFieldsFromContext when the field's context allows the operation
- type: password is a masked field that must be listed as virtual
- pact.FormRules supplies the rule set per operation and replaces the model's
  Rules() for admin saves; a rule on a virtual field sees the submitted value
- preset on a text field follows another text field on the create form
- SPA: PasswordField, preset handling in FormView, empty password left out of
  an update
- README, docs, OpenAPI document, TS types and dist updated
2026-10-05 10:35:08 +02:00
Jakub Zych
a65c670574 feat(12.1-02): read-only preview screen with a status hint and record actions
- config_form.yaml preview block (optional headerPartial), reported in the form schema as preview
- fields with context: preview show only on the preview screen and are never written
- form messages preview and edit; recordActions without a preview block stops boot
- SPA route {id}/preview, PreviewView and PreviewField, record actions in the footer
- mapWinterUrl maps preview/:id; the update form returns to the preview
- summer-callout partial style classes for status hints
- README, docs, OpenAPI document, TS types and the embedded build updated
2026-10-05 00:10:48 +02:00
Jakub Zych
71073bc8a2 feat(12.1-01): cabana.ForbiddenError answers a refused write with 403
- hooks and bulk, record, toolbar and widget actions may return it
- 403 forbidden with the localized message and field details; the write's
  transaction is rolled back; other errors stay the opaque 500
- form shows a refused save as a persistent banner and keeps the values;
  a refused delete is a toast
- smoke tests, OpenAPI notes, dist, README, docs
2026-10-04 23:53:34 +02:00
Jakub Zych
61d5fc72ad feat(12.1-01): list row states from one controller call per page
- pact.ListRowStates with the fixed RowState set deleted, negative, disabled
- list response meta.row_states keyed by row id; unknown values dropped
- list messages rowStateDeleted, rowStateNegative, rowStateDisabled
- update writes through the scope the load used, so a soft-deleted record
  a controller includes stays soft-deleted
- DataTable row state badges and text styles
- roster fixture, smoke tests, OpenAPI, TS types, dist, READMEs, docs
2026-10-04 23:45:44 +02:00
Jakub Zych
e0ccced76a feat(12.1-01): declared record actions with an applicability rule
- pact.HasAdminRecordActions with AdminRecordAction (Applies, Run)
- config_form.yaml recordActions, compiled fail-loud
- show response meta.actions lists the permitted actions that apply
- POST .../{controller}/{id}/actions/{action}: record loaded and locked
  through the form scope; 404 out of scope, 409 when it does not apply
- RecordActions.vue with confirm and request flow (mounted by plan 02)
- roster fixture, smoke tests, OpenAPI, TS types, READMEs, docs
2026-10-04 23:37:30 +02:00
Jakub Zych
a879d6388c feat(12.1-01): declared bulk actions on admin lists
- pact.HasAdminBulkActions with AdminBulkAction, its input and result
- config_list.yaml bulkActions, compiled fail-loud, needs showCheckboxes
- POST .../{controller}/bulk/{action}: ids resolved and locked through the
  list scope in one transaction; partial selection is 409
- list schema offers declared actions per principal, with confirm text
- admin SPA bulk actions menu with confirm, busy state and failure toasts
- acme.roster fixture, tracer test, OpenAPI, TS types, dist, READMEs, docs
2026-10-04 23:28:30 +02:00
Jakub Zych
55314e41f8 fix(admin): datetime popover clock, unsaved confirm, inferred list dates
BM UAT on v0.1.1 showed a date-only calendar for datetime fields, a stuck discard dialog, and raw ISO timestamps when columns.yaml omitted type. The picker now edits time in the popover, confirm sits above the calendar, and omitted time.Time / Date / TimeOfDay columns compile as datetime / date / time.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 05:58:24 +02:00
Jakub Zych
516f9c9025 fix(12.2): close code-review blockers on uploads, JSON caps, and pivot fill
Keep form save behind in-flight uploads, make retries idempotent via X-Upload-Id, cap remaining JSON bodies, and surface pending pivot type errors instead of zeroing them.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-02 23:10:48 +02:00
Jakub Zych
c8d41a68c6 test(12.2-05): cover every Go behaviour of phase 12.2
- lagoon: Date and TimeOfDay through JSON, text and real DATE/TIME
  columns; Fill text fallback without changing earlier conversions;
  required on zero dates; deferred_bindings shape, store isolation and
  envelope; PurgeDeferred cut-off, after-commit blobs, SKIP LOCKED,
  skipped types and the deferred:purge command
- attach: Store limits, extensions, MIME patterns, default lists, key
  shape and blob cleanup; IsAllowedImage formats, polyglots, ceiling
- conga and pact: framework purge schedule entry and forged jobs; the
  six relation child hook interfaces
- cabana: fileupload and datepicker compile, upload, remove, caption,
  reorder and bounds; deferred commit order, rollback, applied-only and
  concurrent saves; relation contracts, forms, CRUD, deferral, schema
2026-10-02 20:34:23 +02:00
Jakub Zych
9d2b1c1848 test(12.2-05): prove relation child and protected file scoping through the router
- acme.deferred fixture plugin over testdata/deferred (test-only), two
  controllers, recording Form and Relation hooks, two admins
- TestRelationChildScope*: every child route answers 404 for another
  parent, a hidden parent and another admin's pending child, changes
  nothing; undeclared toolbar buttons 403 before SQL; pivot whitelist
- TestProtectedFile*: foreign, pending and public files 404; only jpeg,
  png, gif and webp inline; nosniff, no-store and sandbox CSP everywhere
2026-10-02 20:10:10 +02:00
Jakub Zych
fe9e8baaf1 feat(12.2-03): defer relation work on unsaved records and add child file routes
- record id 0 with X-Session-Key manages deferrable relations: create, link, unlink, delete and pivot edits are held in deferred_bindings
- the record's create save applies relation bindings with the file bindings; an ineligible link is a 422 on the relation-manager field
- child forms upload files through .../records/{child}/files/{field} keyed by X-Child-Session-Key; the child save commits them
- boot refuses a deferrable relation with create whose related model no plugin lists in Models()
2026-10-02 19:08:16 +02:00
Jakub Zych
afb05b6ee4 feat(12.2-03): add parent-scoped child show, update, delete and pivot routes
- loadChild finds a child with one query carrying the parent predicate; a foreign child is 404
- GET/PUT .../records/{child} and POST .../delete (all or nothing) per relation kind
- hasMany link adopts NULL-key rows and unlink clears the key; pending created children are never candidates
- link accepts pivot values for one id through the pivot.form whitelist; GET/PUT .../pivot/{child}
- Link and Unlink share linkRelated/unlinkRelated for the deferred commit
2026-10-02 18:44:34 +02:00
Jakub Zych
48a5b8045a feat(12.2-03): add hasMany relation contracts, relation forms and child create
- RelationContract gains Kind (empty is belongsToMany) and ForeignKey, with kind-aware boot checks
- manage.form, view.form and pivot.form compile against the related or pivot model; $/ paths resolve inside the plugin
- view toolbarButtons accept create|update|delete|link|unlink, each the capability of its routes
- POST .../relations/{name}/records creates a child through the manage form; the server sets the hasMany key
- relation schema carries kind, deferrable and the localized forms; 17 new relation message keys in en and pl
2026-10-02 18:37:13 +02:00
Jakub Zych
67d4c7ff13 feat(12.2-02): add the datepicker field with server-side bounds and date list columns
- type: datepicker compiles the D-20 keys; format maps to displayFormat with WinterCMS's momentFormat table
- boot fails when the mode does not match the column's Go type (time.Time, lagoon.Date, lagoon.TimeOfDay)
- datepicker is a writable scalar field; minDate and maxDate are rechecked on save
- columns.yaml accepts type: date and type: time; Scanner/Valuer structs are columns, not relations
- conformance fixture carries date and datetime fields; README, forms and lists docs
2026-10-02 18:19:04 +02:00
Jakub Zych
e54fd257ee feat(12.2-02): add file removal, caption, reorder and protected downloads
- DELETE, PUT and POST reorder under .../{id}/files/{field}, each scoped by one parent query (404 for a foreign file)
- protected download and thumb routes: is_public=false only, nosniff, private no-store, sandbox CSP, inline only for jpeg/png/gif/webp
- the save applies deferred removals, replaces attachOne files and rechecks maxFiles and required
- blobs of deleted files are removed after commit
- swagger2openapi emits binary content for file responses
- admin OpenAPI, TS types, conformance, README and attachments docs
2026-10-02 18:11:56 +02:00
Jakub Zych
044e0450ef feat(12.2-02): add the fileupload field with deferred uploads committed on save
- type: fileupload compiles the D-08 keys and binds to the model's attach.Relation at boot
- X-Session-Key (cabana.SessionKeyHeader) carries the form session key; RecordInput.SessionKey
- GET and POST .../{id}/files/{field}: list with pending uploads, multipart upload into attach.Store
- the create and update save attaches the session's pending files in its transaction
- swagger2openapi folds formData parameters into a multipart requestBody
- admin OpenAPI, TS types, conformance cases, README and forms docs
2026-10-02 18:04:34 +02:00
Jakub Zych
4ae272e3cc fix(09): WR-19 expose the write transaction to hooks and scopes through TxFromContext 2026-10-01 21:37:13 +02:00
Jakub Zych
8479defe53 fix(09): WR-17 merge an admin's own permissions over the role's, honouring denies 2026-10-01 21:33:07 +02:00
Jakub Zych
629fac4d29 fix(09): WR-16 resolve model columns through embedded structs and explicit column tags 2026-10-01 21:31:46 +02:00
Jakub Zych
2d96adf775 fix(09): WR-10 keep cabana's own backend guard when it is activated twice on one application 2026-10-01 21:31:46 +02:00
Jakub Zych
299d220b51 fix(09): WR-14 let logout revoke an expired token that is still refreshable and always clear the cookie 2026-10-01 21:23:42 +02:00
Jakub Zych
c9bb14944a fix(09): WR-13 read admin passwords from a prompt or stdin and deprecate the --password flag 2026-10-01 21:20:20 +02:00
Jakub Zych
331351a73c fix(09): WR-11 reject ambiguous admin logins and cross-field login or email collisions 2026-10-01 21:17:50 +02:00