Files
summercms/.planning/phases/12.1-user-plugin-admin-screens/12.1-CONTEXT.md
2026-10-04 14:52:44 +02:00

17 KiB

Phase 12.1: User plugin admin screens - Context

Gathered: 2026-10-04 Status: Ready for planning

## Phase Boundary

Backend admins manage frontend users, user groups and organisations in the admin SPA, so the PHP backend is not needed for user administration after cutover. The PHP user plugin's Users, UserGroups and Organisations screens are ported to golem15.user (sm-user-plugin, mounted in fonoteka.go at plugins/golem15/user), driven by its fields.yaml / columns.yaml.

The discussion widened the framework half of the phase. summercms.go (modules/cabana, modules/pact, the admin SPA, READMEs and docs/) gains five generic admin features the screens need: declared bulk actions, record actions, a preview context, row state, and a permissioneditor field type. That work lands first and is tagged v0.1.3; the plugin screens build on the tag.

Out of scope: impersonating a user, the guest concept and convert-guest, MailBlocker / block-mail, a username column, the user plugin's Settings screen, and any change to the user API payloads (the Nuxt app's contract stays byte-identical).

## Implementation Decisions

Impersonate and guests

  • D-01: Impersonate is not ported: no action, no button. PHP impersonation is session-based and never reaches the JWT-driven frontend, so nothing is lost at cutover.
  • D-02: The permission golem15.users.impersonate_user is still registered by the Go plugin, so backend roles imported at cutover that reference it stay valid and a later phase can use it. The plugin registers all four PHP codes: golem15.users.access_users, access_groups, access_settings, impersonate_user.
  • D-03: The guest concept is dropped entirely: no convert-guest action, no is_guest column or list column, no guest hint. The seeded Guest group row stays as data.

Group membership and privileged groups (T-12-18)

  • D-04: golem15.users.access_users is enough to change a user's membership of ordinary groups, as in PHP. Adding or removing a privileged group additionally requires a new backend permission. The check is on the server and covers every path that writes users_groups (the user form's groups field, any relation manager, bulk or record actions). — Reversibility: costly — the permission code is stored in backend roles; renaming it later means rewriting role rows in every host application.
  • D-05: Privileged groups are a config list of group codes under the golem15.user config namespace, default [admin]. The plugin is shared across projects, so an application adds a code without a plugin change.
  • D-06: The User Groups screen is gated by golem15.users.access_groups. Creating a group with a privileged code, changing a code to or from a privileged one, and deleting a privileged group all require the same extra permission as D-04. Ordinary groups are edited freely.
  • D-07: For an admin without the extra permission, privileged groups are shown in the user form's groups field but locked (disabled), so the admin can see that a user is a site admin. A save that tries to change a privileged membership anyway is refused with a forbidden error and changes nothing (no partial save).
  • D-08: User.Groups stays out of every user API payload (P12 D-25). Only the admin API reads or writes it.

Actions, delete semantics and preview (framework + plugin)

  • D-09: cabana gains declared bulk actions, generic for every plugin: a controller registers named bulk actions, the list declares which ones it offers, the SPA sends the selected ids, and the framework resolves them through the controller's list scope inside one transaction before calling the plugin. Ids outside the scope never reach the plugin. Bulk delete keeps working as today. — Reversibility: costly — list YAML and the pact action contract grow; every plugin's list config may come to depend on the shape.
  • D-10: cabana gains record actions: named actions shown as buttons for one record, run against the record loaded through the controller's form scope, each with its own permissions on top of the controller's, and each able to say whether it applies to the record's current state. — Reversibility: costly — same contract growth as D-09.
  • D-11: cabana gains Winter's preview context: a read-only record screen with its own toolbar, fields with context: preview, and a recordUrl that may point at it. The Users list opens preview first, then Update, as in PHP. Status hints (not activated, banned, suspended, deleted) and the record actions live on the preview screen. — Reversibility: costly — a new screen and route in the SPA and a new context value in the form schema.
  • D-12: cabana gains row state: a controller returns a state per list row from a fixed framework-defined set (Winter's listInjectRowClass, limited to known states such as deleted, negative, disabled, not free CSS classes). The SPA styles states with design tokens. Users: deleted when trashed, negative when banned, disabled when not activated.
  • D-13: Delete semantics follow PHP Users.php exactly. The Users list and form include trashed users (withTrashed). deactivate is the soft delete, restore brings a user back, and delete (form button and bulk) is a permanent force delete. No extra typed confirmation beyond the standard confirm.
  • D-14: Bulk actions on Users, as in PHP index_onBulkAction: delete, activate, deactivate, restore, ban, unban. Record actions: activate, unban, unsuspend. Ban and suspend state lives in the existing user_throttle table.

Fields and columns

  • D-15: Frontend permissions are ported in full: the golem15_user_frontend_permissions table (code, label, tab, comment), the users.permissions column, the existing user_groups.permissions column, the Permissions tab on the user form (radio mode: allow / deny / inherit) and the group form (checkbox mode), and a Go resolver equivalent to PHP User::getMergedPermissions() that a plugin can call to ask whether a user holds a permission. Nothing in the application calls the resolver yet; unit tests prove it. No plugin-declared registry: rows come from migrations or seed, as in PHP. — Reversibility: costly — additive migrations in the shared user plugin; the stored JSON shape must match PHP for the cutover import.
  • D-16: The editor is a built-in framework field type, permissioneditor, in cabana and the SPA: tabbed, with a radio or checkbox mode, and an option list supplied by the controller. It replaces the PHP YAML's Golem15\User\FormWidgets\FrontendPermissionEditor class name. It is built to be reusable for backend role permissions later. The 10.1 widget contract (scalar fill keys only) is not widened. — Reversibility: costly — a new field type in the typed schema and the generated TS types.
  • D-17: last_seen is added to users as an additive column, updated by the Go auth path on login or token refresh, and shown in the Users list as in PHP. It must not appear in any user API payload unless PHP already returns it.
  • D-18: username is dropped from the ported YAML and no column is added (login is by email).
  • D-19: The user form carries the PHP create/update behaviour: password with confirmation on create, password reset on update, send_invite (default on, create only) that sends the invitation mail, and created_ip_address / last_ip_address on preview.
  • D-20: Avatars use the Phase 12.2 fileupload field (mode: image) on both the user form (260x260) and the organisation form (120x120). They must bind to the same attachment the user API already serves, so an avatar set in the admin shows in the app and the other way round.
  • D-21: block_mail and MailBlocker are left out. A todo records them for the mailing work (mail settings, templates).

Screens, as ported

  • D-22: Organisations: list plus form (name, slug with preset from name, description, avatar) gated by golem15.users.access_users, and a members relation manager that assigns existing users (PHP add|remove on the hasMany, which sets or clears the user's organisation_id). The user form's organisation field is a belongsTo relation picker.
  • D-23: Users filters follow PHP config_filter.yaml: groups (scope filterByGroup), created date (daterange) and activated (switch). The PHP conditions: strings are re-expressed as model scopes, since a conditions key is a boot error (P9).
  • D-24: User Groups list keeps the users_count column.

Release and ordering

  • D-25: Framework first. The cabana and SPA work (D-09 to D-12, D-16) lands in the early plans with module READMEs, docs/ pages, the admin OpenAPI document, generated TS types and the rebuilt dist/, and is tagged v0.1.3 (v0.1.2 already exists). The plugin screens build on that tag. Framework fixtures use neutral names and never name the application.

Claude's Discretion

  • The code of the extra permission in D-04 (for example golem15.users.manage_privileged_groups), its label and tab, and the config key name for the privileged list.
  • YAML keys and Go interface names for bulk actions, record actions, preview and row state, provided they follow the existing fail-loud rules (unknown keys and unregistered actions are boot errors), sit under the {prefix}/api/v1/{vendor}/{plugin}/{controller}/... scheme, use requireAjax on writes and carry swag annotations.
  • The exact set of row states and their token styling; whether a state is also announced in text for accessibility.
  • What the preview screen shows beyond status hints, preview-context fields and actions (PHP's scoreboard is optional).
  • Whether last_seen is written on login only or also on refresh, and any throttling of that write.
  • How force delete cleans up what hangs off a user (attachments, throttle, groups pivot), following PHP User delete behaviour.
  • Plan count and split, subject to the plan-count checkpoint and "unit tests are the last plan". Run the security-review agent: the phase touches authorization (T-12-18), the plugin API (pact) and destructive bulk operations.

<canonical_refs>

Canonical References

Downstream agents MUST read these before planning or implementing.

Roadmap and prior decisions

  • .planning/ROADMAP.md § "Phase 12.1" — goal, five success criteria, open questions (now answered by D-01 to D-07)
  • .planning/phases/12-p-ytarium-api-collections-and-albums/12-01-PLAN.md — user groups (D-25) and threat T-12-18, which this phase revisits
  • .planning/phases/12-p-ytarium-api-collections-and-albums/12-CONTEXT.md — D-25 site-admin predicate (group code admin)
  • .planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-CONTEXT.md — typed schema, fail-loud boot, permissions (D-03), hooks (D-13), filters and scopes
  • .planning/phases/10-admin-vue-spa/10-CONTEXT.md — SPA stack, FieldRenderer registry, toolbar buttons (D-14), cookie auth and CSRF, OpenAPI to TS types (D-15), committed dist/
  • .planning/phases/10.1-runtime-admin-extension-point/10.1-CONTEXT.md — AdminAction contract, why toolbar actions carry no record ids (D-12), widget fill limits (D-05 to D-07)
  • .planning/phases/12.2-admin-form-fields-date-file-upload-relation-editing-with-def/12.2-CONTEXT.md — fileupload, attach.Relation, relation child CRUD and parent scoping (D-15)

PHP reference (read-only): /media/nvme/dev/golem15/fonoteka/plugins/golem15/user

  • controllers/Users.php — actions, bulk actions, withTrashed, listInjectRowClass, force delete
  • controllers/users/ — config_list.yaml, config_form.yaml, config_filter.yaml, config_relation.yaml, _list_toolbar.htm, _preview_toolbar.htm, _hint_*.htm
  • controllers/UserGroups.php, controllers/usergroups/, controllers/Organisations.php, controllers/organisations/ (incl. config_relation.yaml)
  • models/user/fields.yaml, models/user/columns.yaml, models/usergroup/*.yaml, models/organisation/*.yaml
  • models/User.php — getMergedPermissions, attemptActivation, ban / unban, unsuspend, sendInvitation, delete behaviour
  • models/FrontendPermission.php, formwidgets/FrontendPermissionEditor.php and its partials, updates/v2.8.0/ — frontend permissions (D-15, D-16)
  • Plugin.php — registerPermissions, registerNavigation

Go code this phase touches

  • ../fonoteka.go/plugins/golem15/user/ (repo sm-user-plugin) — plugin.go, models/, classes/, updates/, lang/, README.md
  • modules/cabana/ — actions.go, list_schema.go, form_schema.go, filter_schema.go, crud.go, http.go, relation*.go, README.md
  • modules/pact/capabilities.go — AdminAction, HasAdminActions, Permission, NavigationItem
  • admin/src/ — form field registry, list view and toolbar, relation manager
  • ../fonoteka.go/plugins/golem15/fonoteka/controllers/*_admin_controller.go — working examples of admin controllers in an application plugin
  • docs/backend/ — admin-controllers.md, forms.md, lists-and-filters.md, relation-manager.md, users-and-permissions.md; go test ./cmd/summer -run TestDocsTree and summer docs:build --check must pass

</canonical_refs>

<code_context>

Existing Code Insights

Reusable Assets

  • sm-user-plugin already has models.User (soft delete via DeletedAt, Groups many2many never serialized), models.UserGroup, models.Organisation, models.Throttle (is_banned, is_suspended), and classes.UserGroupCodes / HasGroupCode. It has no admin controllers, permissions or navigation yet.
  • cabana: list, form, filters (switch, daterange, model scopes), relation and relation-manager fields with child CRUD and parent scoping, fileupload, datepicker, widget, partial, toolbar actions, bulk delete.
  • pact.AdminAction / HasAdminActions: the existing action contract that bulk and record actions extend.
  • The Go user API already stores user avatars; D-20 reuses that attachment.

Established Patterns

  • Optional capabilities are interfaces the framework type-asserts (Has*, admin hooks).
  • Unknown YAML keys, unknown types, unregistered actions and a conditions: filter fail boot.
  • Every write route uses requireAjax, sits under the backend guard and enforces RequiredPermissions; an action's own permissions are checked on top.
  • Toolbar actions deliberately take no record ids, so an id list can never become an unscoped lookup. Bulk actions (D-09) must keep that property by resolving ids through the list scope.
  • Framework stays app-agnostic: contract tests use a nameless fixture plugin; the real screens live in the plugin repo.
  • Framework changes update the module README, docs/, the admin OpenAPI document, generated TS types and the committed dist/ in the same change.

Integration Points

  • sm-user-plugin plugin.go: add pact.HasAdminControllers, HasPermissions, navigation, and embed the admin YAML.
  • New additive migrations in sm-user-plugin/updates: users.permissions, users.last_seen, golem15_user_frontend_permissions. The fonoteka.go parity schema allow-list needs matching entries where the frozen PHP snapshot differs.
  • The Go auth path (login / refresh) writes last_seen.
  • fonoteka.go bumps the submodule pointer and the framework version (v0.1.3).

</code_context>

## Specific Ideas
  • "Framework first, but I believe it's v0.1.3, v0.1.2 already exists" — the tag for this phase's framework work is v0.1.3.
  • The user chose the fuller option over the lean one in three places: a real preview screen in the framework (not status on the update form), the frontend permission editor ported now (not deferred), and last_seen added. Treat PHP parity of the admin experience as the bar, not minimum viable.
  • Block-mail is wanted, but together with the mailing work (mail settings, templates), not here.
## Deferred Ideas
  • Impersonate a frontend user from the admin by minting a short-lived user JWT, with an audit record and a Nuxt entry point. The permission is already registered (D-02).
  • MailBlocker and the block-mail checkbox: recorded as todo mailblocker-with-mailing.md, to be done with the mailing development.
  • Guest users and convert-guest, for a project that uses guests.
  • A username column and login by username.
  • A plugin-declared frontend permission registry synced at boot.
  • Using permissioneditor for backend role permissions.
  • The user plugin's Settings screen (access_settings).

Reviewed Todos (not folded)

  • nest-framework-packages-under-modules.md, per-module-readmes-after-nest.md, readme-go-fences-src.md, rewrite-summercms-readme.md, refresh-fonoteka-readme.md: repo and README housekeeping; keyword matches only.
  • 2026-10-01-benchmark-the-application-on-wintercms-vs-summercms.md: benchmarking, unrelated.
  • backend-admin-api-tokens.md: backend admin auth, not frontend user administration.
  • orphan-pending-routes.md: covered by Phase 14.1.
  • scaffold-admin-controller-incomplete.md, scaffold-same-second-migration-order.md, scaffold-generated-header-and-command-deps.md, bonfire-duplicate-command-names.md: CLI scaffolding bugs; worth knowing if the planner uses make:admin-controller, but not this phase's scope.
  • lagoon-readme-after-commit-callback-order.md, sitemap-plugin-port.md: unrelated.

Phase: 12.1-user-plugin-admin-screens Context gathered: 2026-10-04