- RegisterEvent.Payload: a copy of the register input without password and password_confirmation (Winter's golem15.user.register data array) - RegisterUser, RegisterOptions, FireRegisterEvent, IssueToken and APIArray: the steps POST /register runs, now called by Register itself; its status codes and bodies are unchanged - README documents the exports and Payload
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, golem15_user_organisations, user_groups and users_groups, and the JSON API under /_user/api/v1. It is a core plugin shared by several applications, so its routes, payloads, table names, migration IDs 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 route of this plugin reads or writes groups, and the user payload still answers"groups":[]. - Mail templates. Activation, reset and reactivation mails in Polish and English (
golem15.user::mail.*, layoutgolem15.user::mail.layouts.user); the locale follows the user (ResolveMailLocale). - 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).
Another plugin that needs the signed-in user puts its routes behind the jwt.auth middleware and imports git.golem15.com/golem15/sm-user-plugin/models or .../classes for the user model 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 |
Packages:
controllers: the route handlers above, 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,UserGroupCodesandHasGroupCode, the activation and reset code helpers, the throttle helpers, mail locale helpers and the two events.RegisterEventcarriesUserandPayload map[string]any, a read-only copy of the register input without the password keys.models:User,Throttle,Organisation,UserGroup.updates: the gormigrate migrations, returned byupdates.All().console:RequirePasswordChange, theuser:require-password-changecommand.
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. |
CLI commands
| Command | Purpose |
|---|---|
user:require-password-change <email> |
Flag an account so its next session must set a new password. |
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.