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
This commit is contained in:
@@ -601,7 +601,7 @@ func MarketingConsent(app *backpack.App) http.HandlerFunc {
|
||||
// Register creates an account and follows activate_mode: auto, user, or admin.
|
||||
func Register(app *backpack.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
gdb, secret, ok := sessionDeps(w, app)
|
||||
gdb, _, ok := sessionDeps(w, app)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -623,8 +623,7 @@ func Register(app *backpack.App) http.HandlerFunc {
|
||||
if _, exists := fields["password_confirmation"]; !exists {
|
||||
fields["password_confirmation"] = fields["password"]
|
||||
}
|
||||
rules := (models.User{}).Rules()
|
||||
errs, err := lagoon.Validate(r.Context(), gdb, models.User{}, rules, fields, appTranslator(app))
|
||||
user, errs, err := RegisterUser(r.Context(), app, gdb, fields, RegisterOptions{Activate: mode == "auto", IP: ip})
|
||||
if err != nil {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
@@ -636,40 +635,7 @@ func Register(app *backpack.App) http.HandlerFunc {
|
||||
})
|
||||
return
|
||||
}
|
||||
password := strField(fields, "password")
|
||||
cost := 10
|
||||
if 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, password)
|
||||
if err != nil {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
fields["password"] = hashed
|
||||
if ip != "" {
|
||||
fields["created_ip_address"] = ip
|
||||
fields["last_ip_address"] = ip
|
||||
}
|
||||
user := &models.User{}
|
||||
production := app.Config == nil || !app.Config.Bool("app.debug")
|
||||
if err := lagoon.Fill(user, user.Fillable(), fields, production); err != nil {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
user.HasSelfSetPassword = true
|
||||
if mode == "auto" {
|
||||
user.IsActivated = true
|
||||
now := time.Now()
|
||||
user.ActivatedAt = &now
|
||||
}
|
||||
if err := gdb.WithContext(r.Context()).Create(user).Error; err != nil {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
if app.Events != nil {
|
||||
_ = app.Events.Fire(r.Context(), &classes.RegisterEvent{User: user})
|
||||
}
|
||||
_ = FireRegisterEvent(r.Context(), app, user, fields)
|
||||
ctx := r.Context()
|
||||
if mode == "user" {
|
||||
vars := mailVars(displayName(user), "", "")
|
||||
@@ -685,12 +651,12 @@ func Register(app *backpack.App) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
if mode == "auto" || !require {
|
||||
token, err := mintFor(app, secret, user, requestURL(r))
|
||||
token, err := IssueToken(app, user, r)
|
||||
if err != nil {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
payload, err := apiArray(ctx, app, user)
|
||||
payload, err := APIArray(ctx, app, user)
|
||||
if err != nil {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
|
||||
121
controllers/registration.go
Normal file
121
controllers/registration.go
Normal file
@@ -0,0 +1,121 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user