- 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
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_modeisauto,user(a mailed code) oradmin, with codes fromIssueActivationCodeandVerifyActivationCode. - JWT sessions.
POST /login,GET /fetch,POST /refresh,POST /logout. Boot registers thejwtguard with the framework'sbouncer.Registryand exposes it as thejwt.authmiddleware for other plugins. Logged-out tokens go to the Postgresjwt_blacklist, which a background sweep prunes. - Password reset and change.
POST /forgot-passwordanswers the same body for known and unknown addresses;POST /reset-passwordconsumes a code fromIssueResetCode.POST /change-passwordrequires the current password. - Must-change-password lock. The
user:require-password-changeconsole command flags an account. The flag rides on the authenticated principal (bouncer.Principal.MustChangePassword) for guards and other plugins to act on, and a successfulPOST /change-passwordclears 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-apibucket: 120 requests a minute per authenticated user, or per client IP for anonymous calls. - Profile.
POST /update,POST /marketing-consent,POST /avatarandPOST /avatar/remove(the avatar is stored through the framework's attachment storage). - Organisations. The
Organisationmodel and theorganisation_idandorganisation_roleuser columns, as a foreign-key target for other plugins. - User groups. The
user_groupstable (seeded with theguestandregisteredgroups, as in PHP) and theusers_groupsmembership pivot, theUserGroupmodel and theUser.Groupsrelation, which is never serialized.UserGroupCodesandHasGroupCodelet other plugins check membership server-side, for example a site-admin check on the group codeadmin. 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 IDgolem15.user.users), the port of the WinterCMSUserscontroller: 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(defaultadmin) is protected against takeover: changing that user's email or password, and deleting them for good, needs the backend permissiongolem15.users.manage_privileged_groupson top ofgolem15.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 onlygolem15.users.access_users.PrivilegedGroupCodes,IsPrivilegedCode,PrivilegedGroupIDsandIsPrivilegedMemberexpose the rule to other code. - Frontend permissions.
golem15_user_frontend_permissionsis 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 inusers.permissionsand a group's inuser_groups.permissions, as a JSON object of code to1(allow) or-1(deny).MergedPermissionscombines them as PHPgetMergedPermissionsdoes: among a user's groups the most positive value wins, then the user's own value replaces the groups'.HasPermissionandUserHasPermissionanswer whether a code is held: only at exactly1, with Winter's*wildcards. Neither value appears in the user payload, whosepermissionskey staysnull. - Last seen.
users.last_seenis stamped byPOST /loginandPOST /refreshthroughTouchLastSeen, 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_tokensstores one hashed personal/OAuth credential per user for the whole application. Theuser.api_tokenguard authenticates it for any plugin;user.scope:<scope>limits delegated access anduser.permission:<code>checks the user's current group permissions.POST/GET/DELETE /tokensmanage personal tokens, anduser:token:create,user:token:listanduser:token:revokeprovide 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.*, layoutgolem15.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.
RegisterEventfires after a user row is created; itsPayloadis a copy of the register input (extra keys a client sent included,passwordandpassword_confirmationnever).GetApiArrayEventlets other plugins merge keys into the user payload. - Registration for other plugins.
RegisterUser,FireRegisterEvent,IssueTokenandAPIArrayare the stepsPOST /registerruns, 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-providerslists 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) andPermissionAccessUsers, plus the registration stepsPOST /registerruns, for other plugins:RegisterUser(ctx, app, db, fields, RegisterOptions)validatesfieldsagainst the user model's rules, hashes the password withgolem15.user.password.bcrypt_costand creates the user ondb(which may be a transaction). It returns the validation errors instead of writing a response and never modifiesfields.RegisterOptions.Activatecreates the account activated;RegisterOptions.IPrecords the client IP.FireRegisterEvent(ctx, app, user, fields)firesRegisterEventwithPayloadset to a copy offieldswithoutpasswordandpassword_confirmation, and returns the listeners' error (/registerignores it). Fire it after the user row is committed.IssueToken(app, user, r)mints the session JWT as/registerand/logindo.APIArray(ctx, app, user)builds the user payload, extended byGetApiArrayEventlisteners.
classes:JWTSecret,GormUsers(thebouncer.UserProvider),LookupByEmail,UserPermissionGrants,UserGroupCodes,HasGroupCode,APITokenPrefix,MintAPIToken,HashAPITokenandRevokeAPIToken, plus the activation/reset, throttle, mail locale and event helpers.RegisterEventcarriesUserandPayload 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)andUserHasPermission(ctx, db, userID, code), the frontend permission resolver, andFrontendPermissionOptions(ctx, db), the catalogue by tab and code.PermissionManagePrivilegedGroups,PrivilegedGroupCodes(cfg),IsPrivilegedCode(codes, code),PrivilegedGroupIDs(ctx, db, codes)andIsPrivilegedMember(ctx, db, codes, userID), the privileged-group rule.ActivateUsers,DeactivateUsers,RestoreUsers,BanUsers,UnbanUsers,UnsuspendUser,ThrottleStatesandForceDeleteCleanup, the user actions the admin runs. Each takes the handle of the caller's transaction.TouchLastSeen(ctx, db, userID).
middleware:APITokenGuard,Scope,PermissionandBearerToken.models:User,Throttle,Organisation,UserGroup,APIToken,FrontendPermissionandPermissionSet.PermissionSetreads a stored permission column tolerantly (NULL, an empty string,[], numeric strings) and writes a JSON object of integers.User.FilterScopeanswers the admin list scopeFilterByGroup, andUser.AttachRelationsdeclares the avatar (AvatarField).updates: the gormigrate migrations, returned byupdates.All().console:RequirePasswordChangeandAPITokenCommands.
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(modulesbackpack,bonfire,bouncer,pact,party,surfand 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.