- DeactivatingEvent and DeactivatingEventName in classes - FireDeactivatingEvent modelled on FireDeletingEvent - the deactivate bulk action runs DeactivateUsers unchanged, then fires the event per newly deactivated user in the action's transaction
155 lines
5.6 KiB
Go
155 lines
5.6 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
|
|
}
|
|
if err := classes.AssignHandle(ctx, db, user); 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})
|
|
}
|
|
|
|
// FireCreatedEvent fires classes.CreatedEvent (user.Created) for user and
|
|
// returns the listeners' error. /register ignores that error, as it does
|
|
// for FireRegisterEvent.
|
|
func FireCreatedEvent(ctx context.Context, app *backpack.App, user *models.User) error {
|
|
if app == nil || app.Events == nil {
|
|
return nil
|
|
}
|
|
return app.Events.Fire(ctx, &classes.CreatedEvent{User: user})
|
|
}
|
|
|
|
// FireDeletingEvent fires classes.DeletingEvent (user.Deleting) for user
|
|
// inside the delete transaction. FormAfterDelete returns the listeners'
|
|
// error so a failed cascade rolls the delete back.
|
|
func FireDeletingEvent(ctx context.Context, app *backpack.App, user *models.User, tx *gorm.DB) error {
|
|
if app == nil || app.Events == nil {
|
|
return nil
|
|
}
|
|
return app.Events.Fire(ctx, &classes.DeletingEvent{User: user, Tx: tx})
|
|
}
|
|
|
|
// FireDeactivatingEvent fires classes.DeactivatingEvent (user.Deactivating)
|
|
// for user inside the deactivation transaction and returns the listeners'
|
|
// error, so a failed listener rolls the deactivation back.
|
|
func FireDeactivatingEvent(ctx context.Context, app *backpack.App, user *models.User, tx *gorm.DB) error {
|
|
if app == nil || app.Events == nil {
|
|
return nil
|
|
}
|
|
return app.Events.Fire(ctx, &classes.DeactivatingEvent{User: user, Tx: tx})
|
|
}
|
|
|
|
// 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)
|
|
}
|