feat: provider source for oauth-providers and sign-in helpers for social login
- classes.OAuthProvider and classes.OAuthProviderSource; GET /oauth-providers looks the source up per request and keeps the empty body without one - exported RegistrationSettings, RegistrationsFromIP, RefreshTTL, RemoteIP - RegisterOptions.PasswordLess (has_self_set_password=false) and FinishLogin (restore + reactivate mail, last_ip_address, last_seen; no event) - TestOAuthProvidersSourceSmoke, TestSocialHooksSmoke; README
This commit is contained in:
@@ -3,6 +3,7 @@ package controllers
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"net/http"
|
||||
"time"
|
||||
@@ -26,6 +27,11 @@ import (
|
||||
type RegisterOptions struct {
|
||||
Activate bool
|
||||
IP string
|
||||
// PasswordLess creates the account with has_self_set_password=false: its
|
||||
// password is random and was never known to the user (social sign-in,
|
||||
// PHP password_bootstrap_source oauth_registration). The zero value
|
||||
// keeps true.
|
||||
PasswordLess bool
|
||||
}
|
||||
|
||||
// RegisterUser validates fields against the user model's rules, hashes the
|
||||
@@ -68,7 +74,7 @@ func RegisterUser(ctx context.Context, app *backpack.App, db *gorm.DB, fields ma
|
||||
if err := lagoon.Fill(user, user.Fillable(), input, production); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
user.HasSelfSetPassword = true
|
||||
user.HasSelfSetPassword = !opts.PasswordLess
|
||||
if opts.Activate {
|
||||
user.IsActivated = true
|
||||
now := time.Now()
|
||||
@@ -83,6 +89,40 @@ func RegisterUser(ctx context.Context, app *backpack.App, db *gorm.DB, fields ma
|
||||
return user, nil, nil
|
||||
}
|
||||
|
||||
// FinishLogin runs the steps password Login takes after it has
|
||||
// authenticated a user, for a caller that authenticated the user another way
|
||||
// (a social sign-in). It restores a soft-deleted account and sends the
|
||||
// reactivate mail, records ip as last_ip_address, and touches last_seen. It
|
||||
// checks no password, throttle, activation or ban and fires no event; the
|
||||
// caller does those checks (Go has no login event). A failed last_seen
|
||||
// write is logged and does not fail it, as in Login.
|
||||
func FinishLogin(ctx context.Context, app *backpack.App, db *gorm.DB, user *models.User, ip string) error {
|
||||
if db == nil {
|
||||
return errors.New("golem15.user: gorm handle is missing")
|
||||
}
|
||||
if user == nil || user.ID == 0 {
|
||||
return errors.New("golem15.user: finishing a login needs a stored user")
|
||||
}
|
||||
if user.DeletedAt.Valid {
|
||||
if err := db.WithContext(ctx).Unscoped().Model(user).Update("deleted_at", nil).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
user.DeletedAt = gorm.DeletedAt{}
|
||||
sendTemplateVars(ctx, app, mailName(ctx, user, "golem15.user::mail.reactivate"), user.Email, mailVars(displayName(user), "", ""))
|
||||
}
|
||||
if ip != "" {
|
||||
if err := db.WithContext(ctx).Unscoped().Model(user).Update("last_ip_address", ip).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
addr := ip
|
||||
user.LastIPAddress = &addr
|
||||
}
|
||||
if err := classes.TouchLastSeen(ctx, db, user.ID); err != nil {
|
||||
slog.Default().Warn("golem15.user: stamp last_seen on login", "user_id", user.ID, "err", err)
|
||||
}
|
||||
return 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
|
||||
|
||||
Reference in New Issue
Block a user