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) }