Files
sm-user-plugin/controllers/registration.go
Jakub Zych d80d7990f6 feat: carry the register input on RegisterEvent and export the registration steps
- 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
2026-10-03 07:12:00 +02:00

122 lines
4.2 KiB
Go

package controllers
import (
"context"
"errors"
"maps"
"net/http"
"time"
"git.golem15.com/golem15/sm-user-plugin/classes"
"git.golem15.com/golem15/sm-user-plugin/models"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/bouncer"
"git.golem15.com/golem15/summercms/modules/lagoon"
"gorm.io/gorm"
)
// The registration steps of POST /register, exported so another plugin can
// create a user exactly as /register does (same validation, password
// hashing, register event, token and user payload) without duplicating
// them. Register itself calls these functions.
// RegisterOptions tunes RegisterUser. Activate creates the account already
// activated (activate_mode auto, or a caller that activates explicitly).
// IP, when set, is recorded as created_ip_address and last_ip_address.
type RegisterOptions struct {
Activate bool
IP string
}
// RegisterUser validates fields against the user model's rules, hashes the
// password with golem15.user.password.bcrypt_cost (default 10), fills the
// fillable columns and creates the users row on db, which may be a
// transaction. Validation failures are returned as Laravel's errors object
// (attribute to messages) with a nil user and nothing written; they are not
// written to a response. fields is not modified. The register event is not
// fired: call FireRegisterEvent once the row is committed.
func RegisterUser(ctx context.Context, app *backpack.App, db *gorm.DB, fields map[string]any, opts RegisterOptions) (*models.User, map[string][]string, error) {
if db == nil {
return nil, nil, errors.New("golem15.user: gorm handle is missing")
}
input := maps.Clone(fields)
if input == nil {
input = map[string]any{}
}
errs, err := lagoon.Validate(ctx, db, models.User{}, (models.User{}).Rules(), input, appTranslator(app))
if err != nil {
return nil, nil, err
}
if len(errs) > 0 {
return nil, errs, nil
}
cost := 10
if app != nil && app.Config != nil && app.Config.Int("golem15.user.password.bcrypt_cost") > 0 {
cost = app.Config.Int("golem15.user.password.bcrypt_cost")
}
hashed, err := bouncer.HashPassword(cost, strField(input, "password"))
if err != nil {
return nil, nil, err
}
input["password"] = hashed
if opts.IP != "" {
input["created_ip_address"] = opts.IP
input["last_ip_address"] = opts.IP
}
user := &models.User{}
production := app == nil || app.Config == nil || !app.Config.Bool("app.debug")
if err := lagoon.Fill(user, user.Fillable(), input, production); err != nil {
return nil, nil, err
}
user.HasSelfSetPassword = true
if opts.Activate {
user.IsActivated = true
now := time.Now()
user.ActivatedAt = &now
}
if err := db.WithContext(ctx).Create(user).Error; err != nil {
return nil, nil, err
}
return user, nil, nil
}
// FireRegisterEvent fires classes.RegisterEvent for user with Payload set
// to a copy of fields without password and password_confirmation, and
// returns the listeners' error. /register ignores that error, as Winter
// does not stop a registration for a failing listener.
func FireRegisterEvent(ctx context.Context, app *backpack.App, user *models.User, fields map[string]any) error {
if app == nil || app.Events == nil {
return nil
}
payload := maps.Clone(fields)
if payload == nil {
payload = map[string]any{}
}
delete(payload, "password")
delete(payload, "password_confirmation")
return app.Events.Fire(ctx, &classes.RegisterEvent{User: user, Payload: payload})
}
// IssueToken mints the session JWT for user exactly as /register and
// /login do: the plugin's JWT secret, golem15.user.jwt.ttl and the request
// URL as the issuer.
func IssueToken(app *backpack.App, user *models.User, r *http.Request) (string, error) {
if user == nil || r == nil {
return "", errors.New("golem15.user: a token needs a user and a request")
}
secret, err := classes.JWTSecret(app)
if err != nil {
return "", err
}
return mintFor(app, secret, user, requestURL(r))
}
// APIArray is the user payload the API answers (getApiArray in PHP),
// extended by GetApiArrayEvent listeners.
func APIArray(ctx context.Context, app *backpack.App, user *models.User) (map[string]any, error) {
if user == nil {
return nil, errors.New("golem15.user: the user payload needs a user")
}
return apiArray(ctx, app, user)
}