Jakub Zych 58595fef27 feat: Organisations admin screen with the avatar and the members manager
- controller golem15.user.organisations behind golem15.users.access_users
- list and form (name, slug preset from the name, description, avatar)
- members relation manager: link sets and unlink clears users.organisation_id
- deleting an organisation releases its members first
- Organisation gains Fillable, Rules, MorphName, AttachRelations, BeforeValidate
  and the Members relation; models.Slugify
- navigation side item organisations
- TestAdminOrganisations, TestAdminOrganisationMembers, TestSlugify
2026-10-05 13:58:54 +02:00
2026-10-04 22:30:20 +02:00
2026-10-04 22:30:20 +02:00

golem15.user

The shared front-end user plugin for SummerCMS applications: registration, login with JWT sessions, activation and password reset, profile and avatar endpoints.

import _ "git.golem15.com/golem15/sm-user-plugin"

Module git.golem15.com/golem15/sm-user-plugin, package user, plugin ID golem15.user.

Overview

The plugin is a compiled SummerCMS plugin. Its init registers it with the framework, so an application loads it by listing it in summer.yaml; summer build then emits the blank import above into the application's plugins.gen.go.

It owns the users table extensions, user_throttle, jwt_blacklist, user_api_tokens, golem15_user_organisations, user_groups, users_groups and golem15_user_frontend_permissions, the JSON API under /_user/api/v1, and the Users screen of the admin. It is a core plugin shared by several applications, so its routes, payloads, table names, migration IDs, permission codes and config keys are a contract: change them only in a backwards-compatible way.

Features

  • Registration and activation. POST /register, POST /activate, POST /activate-by-code. Registration can be switched off and throttled; activate_mode is auto, user (a mailed code) or admin, with codes from IssueActivationCode and VerifyActivationCode.
  • JWT sessions. POST /login, GET /fetch, POST /refresh, POST /logout. Boot registers the jwt guard with the framework's bouncer.Registry and exposes it as the jwt.auth middleware for other plugins. Logged-out tokens go to the Postgres jwt_blacklist, which a background sweep prunes.
  • Password reset and change. POST /forgot-password answers the same body for known and unknown addresses; POST /reset-password consumes a code from IssueResetCode. POST /change-password requires the current password.
  • Must-change-password lock. The user:require-password-change console command flags an account. The flag rides on the authenticated principal (bouncer.Principal.MustChangePassword) for guards and other plugins to act on, and a successful POST /change-password clears it.
  • Login throttling. Failed logins are counted per user and IP in user_throttle (CheckAndRecordLogin, RejectIfThrottled) and suspend the account for a configured time.
  • Rate limiting. Every route runs behind the user-api bucket: 120 requests a minute per authenticated user, or per client IP for anonymous calls.
  • Profile. POST /update, POST /marketing-consent, POST /avatar and POST /avatar/remove (the avatar is stored through the framework's attachment storage).
  • Organisations. The Organisation model and the organisation_id and organisation_role user columns, as a foreign-key target for other plugins.
  • User groups. The user_groups table (seeded with the guest and registered groups, as in PHP) and the users_groups membership pivot, the UserGroup model and the User.Groups relation, which is never serialized. UserGroupCodes and HasGroupCode let other plugins check membership server-side, for example a site-admin check on the group code admin. No user API route reads or writes groups, and the user payload still answers "groups":[]; the admin Users list can filter by group.
  • Admin screens. The plugin registers the Users screen with the framework's admin (pact.HasAdminControllers, controller ID golem15.user.users), the port of the WinterCMS Users controller: a list with search, the group, registration date and activated filters and row states (deactivated, banned, not activated); a read-only preview with a status hint; and a form with name, email, a password with confirmation, an optional invitation mail, the organisation, the avatar and the user's frontend permissions. Bulk actions activate, deactivate, restore, ban and unban users; the preview offers activate, unban and unsuspend while they apply. Deactivating is the soft delete; the delete button and the bulk delete remove a user for good, together with the throttle rows, group memberships and attachments. A user created in the admin starts not activated. The User Groups and Organisations screens follow in the same release. There is no impersonation and no guest account.
  • Privileged group members. A user who belongs to a group whose code is listed in golem15.user.privileged_groups (default admin) is protected against takeover: changing that user's email or password, and deleting them for good, needs the backend permission golem15.users.manage_privileged_groups on top of golem15.users.access_users. Without it the request answers 403 and nothing is saved; a bulk delete that contains such a user deletes none of the selected users. Everything else on such a user (name, organisation, avatar, frontend permissions, activate, deactivate, restore, ban, unban, unsuspend) needs only golem15.users.access_users. PrivilegedGroupCodes, IsPrivilegedCode, PrivilegedGroupIDs and IsPrivilegedMember expose the rule to other code.
  • Frontend permissions. golem15_user_frontend_permissions is the catalogue of permissions a site defines for its users (rows come from the application's migrations or seeds). A user's own choices are stored in users.permissions and a group's in user_groups.permissions, as a JSON object of code to 1 (allow) or -1 (deny). MergedPermissions combines them as PHP getMergedPermissions does: among a user's groups the most positive value wins, then the user's own value replaces the groups'. HasPermission and UserHasPermission answer whether a code is held: only at exactly 1, with Winter's * wildcards. Neither value appears in the user payload, whose permissions key stays null.
  • Last seen. users.last_seen is stamped by POST /login and POST /refresh through TouchLastSeen, at most once per five minutes per user. A failed write is logged and never fails authentication. It is shown in the admin list and is not part of any user payload.
  • Shared API tokens. user_api_tokens stores one hashed personal/OAuth credential per user for the whole application. The user.api_token guard authenticates it for any plugin; user.scope:<scope> limits delegated access and user.permission:<code> checks the user's current group permissions. POST/GET/DELETE /tokens manage personal tokens, and user:token:create, user:token:list and user:token:revoke provide the CLI equivalent. A raw secret is shown only when created.
  • Mail templates. Activation, reset, reactivation and invitation mails in Polish and English (golem15.user::mail.*, layout golem15.user::mail.layouts.user); the locale follows the user (ResolveMailLocale). The invitation (golem15.user::mail.invite) is sent when an administrator creates a user with "Send invitation by email" set; it carries the user's login and an activation link, never the password.
  • Extension events. RegisterEvent fires after a user row is created; its Payload is a copy of the register input (extra keys a client sent included, password and password_confirmation never). GetApiArrayEvent lets other plugins merge keys into the user payload.
  • Registration for other plugins. RegisterUser, FireRegisterEvent, IssueToken and APIArray are the steps POST /register runs, exported so another plugin (a first-run setup endpoint, for example) creates a user with the same validation, hashing, event, token and payload.
  • OAuth providers. GET /oauth-providers lists configured social login providers (none yet: it returns an empty list).

Usage

Mount the plugin in an application as a git submodule at plugins/golem15/user:

git submodule add git@git.golem15.com:golem15/sm-user-plugin.git plugins/golem15/user

Add it to the application's go.work:

use (
	.
	./plugins/golem15/user
)

Require it in the application's go.mod with a local replace:

require git.golem15.com/golem15/sm-user-plugin v0.0.0

replace git.golem15.com/golem15/sm-user-plugin => ./plugins/golem15/user

List it in summer.yaml and run summer build:

module: git.golem15.com/golem15/acme
binary: acme
plugins:
  - id: golem15.user
    module: git.golem15.com/golem15/sm-user-plugin

The plugin's own go.mod replaces the framework with ../../../../summercms.go, so it expects the framework checkout next to the application checkout (<dir>/summercms.go and <dir>/acme/plugins/golem15/user).

Because the plugin registers an admin screen, an application that loads it mounts the framework's admin API and must set admin.jwt.secret (SUMMER_ADMIN__JWT__SECRET); start-up fails while it is empty. The admin screens need framework release v0.1.3 or later.

Another plugin that needs the signed-in user puts its routes behind jwt.auth. A machine API uses user.api_token, optionally followed by user.scope:read, user.scope:write, or user.permission:vendor.plugin.code. Both guards resolve the same user and current group permissions. Import git.golem15.com/golem15/sm-user-plugin/models, .../classes, or .../middleware for the shared types and helpers.

API reference

All routes are mounted under /_user/api/v1 with the throttle:user-api middleware.

Method and path Handler
POST /login controllers.Login
POST /logout controllers.Logout
GET /fetch controllers.Fetch
POST /refresh controllers.Refresh
POST /register controllers.Register
GET /oauth-providers controllers.OAuthProviders
POST /forgot-password controllers.ForgotPassword
POST /reset-password controllers.ResetPassword
POST /activate controllers.Activate
POST /activate-by-code controllers.ActivateByCode
POST /update controllers.Update
POST /change-password controllers.ChangePassword
POST /marketing-consent controllers.MarketingConsent
POST /avatar controllers.UploadAvatar
POST /avatar/remove controllers.RemoveAvatar
POST /tokens controllers.APITokenStore (jwt.auth)
GET /tokens controllers.APITokenIndex (jwt.auth)
DELETE /tokens/{id} controllers.APITokenDestroy (jwt.auth)

Admin

The Users screen is served by the framework's generic admin API under <admin prefix>/api/v1/golem15/user/users; the plugin adds no admin route of its own.

Item Value
Admin controller golem15.user.users, from controllers.AdminControllers
Navigation main item user (order 555) with the side item users
Bulk actions activate, deactivate, restore, ban, unban, and the built-in delete
Record actions activate, unban, unsuspend

Backend permissions, all on the tab golem15.user::lang.plugin.tab:

Code Default role Grants
golem15.users.access_users developer The Users screen and every action on it.
golem15.users.access_groups developer The User Groups screen, which follows in the same release.
golem15.users.access_settings developer Registered for roles that already name it; no screen uses it yet.
golem15.users.impersonate_user developer Registered for roles that already name it; impersonation is not available.
golem15.users.manage_privileged_groups none Changing the email or password of a privileged-group member, and deleting one for good. Only a superuser or a role that was granted it holds it.

Migrations added for the admin, all additive:

ID Adds
202610040001_add_users_permissions users.permissions (text, nullable)
202610040002_add_users_last_seen users.last_seen (timestamp, nullable)
202610040003_create_frontend_permissions the table golem15_user_frontend_permissions (code unique, label, tab, comment)

Packages:

  • controllers: the route handlers above, AdminControllers (the admin controller list) and PermissionAccessUsers, plus the registration steps POST /register runs, for other plugins:
    • RegisterUser(ctx, app, db, fields, RegisterOptions) validates fields against the user model's rules, hashes the password with golem15.user.password.bcrypt_cost and creates the user on db (which may be a transaction). It returns the validation errors instead of writing a response and never modifies fields. RegisterOptions.Activate creates the account activated; RegisterOptions.IP records the client IP.
    • FireRegisterEvent(ctx, app, user, fields) fires RegisterEvent with Payload set to a copy of fields without password and password_confirmation, and returns the listeners' error (/register ignores it). Fire it after the user row is committed.
    • IssueToken(app, user, r) mints the session JWT as /register and /login do.
    • APIArray(ctx, app, user) builds the user payload, extended by GetApiArrayEvent listeners.
  • classes: JWTSecret, GormUsers (the bouncer.UserProvider), LookupByEmail, UserPermissionGrants, UserGroupCodes, HasGroupCode, APITokenPrefix, MintAPIToken, HashAPIToken and RevokeAPIToken, plus the activation/reset, throttle, mail locale and event helpers. RegisterEvent carries User and Payload map[string]any, a read-only copy of the register input without the password keys. For the admin and for other plugins it also has:
    • MergedPermissions(ctx, db, userID), HasPermission(merged, code) and UserHasPermission(ctx, db, userID, code), the frontend permission resolver, and FrontendPermissionOptions(ctx, db), the catalogue by tab and code.
    • PermissionManagePrivilegedGroups, PrivilegedGroupCodes(cfg), IsPrivilegedCode(codes, code), PrivilegedGroupIDs(ctx, db, codes) and IsPrivilegedMember(ctx, db, codes, userID), the privileged-group rule.
    • ActivateUsers, DeactivateUsers, RestoreUsers, BanUsers, UnbanUsers, UnsuspendUser, ThrottleStates and ForceDeleteCleanup, the user actions the admin runs. Each takes the handle of the caller's transaction.
    • TouchLastSeen(ctx, db, userID).
  • middleware: APITokenGuard, Scope, Permission and BearerToken.
  • models: User, Throttle, Organisation, UserGroup, APIToken, FrontendPermission and PermissionSet. PermissionSet reads a stored permission column tolerantly (NULL, an empty string, [], numeric strings) and writes a JSON object of integers. User.FilterScope answers the admin list scope FilterByGroup, and User.AttachRelations declares the avatar (AvatarField).
  • updates: the gormigrate migrations, returned by updates.All().
  • console: RequirePasswordChange and APITokenCommands.

Configuration

Defaults live in config/config.yaml and are merged under golem15.user.*. Any key can be overridden by an environment variable in the framework's SUMMER_ form.

Key Default Meaning
golem15.user.jwt.secret "" HS256 signing secret. Required: boot fails while it is empty. Set it with SUMMER_GOLEM15__USER__JWT__SECRET; there is no production default.
golem15.user.jwt.ttl 60 Access token lifetime in minutes.
golem15.user.jwt.refresh_ttl 20160 Refresh window in minutes.
golem15.user.jwt.blacklist_grace 0 Grace period in seconds for a blacklisted token.
golem15.user.jwt.leeway 0 Clock leeway in seconds (declared for parity, not read yet).
golem15.user.jwt.blacklist_sweep_interval 10m How often expired blacklist rows are deleted (a Go duration).
golem15.user.activation.require_activation true Whether a new account must be activated before login.
golem15.user.activation.activate_mode auto auto, user (mailed code) or admin.
golem15.user.activation.reset_ttl_minutes 60 Password reset code lifetime.
golem15.user.activation.activation_ttl_hours 72 Activation code lifetime.
golem15.user.registration.allow_registration true Whether POST /register accepts new accounts.
golem15.user.registration.use_register_throttle true Whether registration is limited to three accounts per IP an hour.
golem15.user.throttle.attempt_limit 5 Failed logins before suspension.
golem15.user.throttle.suspension_minutes 15 Suspension length.
golem15.user.throttle.use_throttle true Whether login throttling is on.
golem15.user.password.bcrypt_cost 10 bcrypt cost for new password hashes.
golem15.user.password.min_length 8 Minimum password length.
golem15.user.api_tokens.prefix usr_ Prefix for newly minted and accepted shared API-token secrets. Override an existing application's legacy prefix without creating a plugin-specific token table.
golem15.user.privileged_groups [admin] Codes of the user groups whose members are protected: their email, password and permanent delete need golem15.users.manage_privileged_groups. Read on every request. An empty list protects no group.

CLI commands

Command Purpose
user:require-password-change <email> Flag an account so its next session must set a new password.
user:token:create <email> [--name api] [--scopes read,write] [--expires RFC3339] Create an application-wide API token and print its secret once.
user:token:list List token metadata without secrets or hashes.
user:token:revoke <id> Revoke a shared API token immediately.

Dependencies

  • The SummerCMS framework, git.golem15.com/golem15/summercms (modules backpack, bonfire, bouncer, pact, party, surf and others).
  • gormigrate, GORM, golang-jwt and gocloud.dev, as pinned in go.mod.
  • testcontainers-go for the Postgres-backed tests.

Testing

go test -short ./...    # fast run, no Docker
go test ./...           # full run, starts Postgres with testcontainers (needs Docker)

TestRegisterCORSPath reads the host application's config/http.yaml, so run the tests inside an application checkout where the plugin is mounted at plugins/golem15/user.

The admin tests (TestAdmin*) boot the plugin with the admin API mounted on a database of their own and sign in backend administrators with a chosen permission set; the helpers are in admin_harness_test.go.

Description
No description provided
Readme 461 KiB
Languages
Go 99.5%
HTML 0.5%