Jakub Zych cdea47a178 feat: frontend permissions and the merged-permission resolver
- PermissionSet reads the stored JSON tolerantly and writes an object
  of integers; FrontendPermission is the catalogue row
- MergedPermissions, HasPermission and UserHasPermission port PHP
  getMergedPermissions and Winter's hasPermission: groups merge with
  most positive wins, the user's own values override, held means
  exactly 1, wildcards as in Winter
- User gains Permissions (never serialized) and declares the avatar
  attachment relation the user API already writes
2026-10-05 13:21:51 +02:00
2026-10-04 22:30:20 +02:00
2026-10-04 22:30:20 +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 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_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 route of this plugin reads or writes groups, and the user payload still answers "groups":[].
  • 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 and reactivation mails in Polish and English (golem15.user::mail.*, layout golem15.user::mail.layouts.user); the locale follows the user (ResolveMailLocale).
  • 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).

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)

Packages:

  • controllers: the route handlers above, 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.
  • middleware: APITokenGuard, Scope, Permission and BearerToken.
  • models: User, Throttle, Organisation, UserGroup, APIToken.
  • 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.

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.

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