Prefix replacement of the old application-nested module path in go.mod and every import. No identifier, string, table, migration ID, config key or comment changes.
214 lines
5.1 KiB
Go
214 lines
5.1 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"embed"
|
|
"io/fs"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/sm-user-plugin/classes"
|
|
"git.golem15.com/golem15/sm-user-plugin/console"
|
|
"git.golem15.com/golem15/sm-user-plugin/models"
|
|
"git.golem15.com/golem15/sm-user-plugin/updates"
|
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
|
"git.golem15.com/golem15/summercms/modules/bonfire"
|
|
"git.golem15.com/golem15/summercms/modules/bouncer"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"git.golem15.com/golem15/summercms/modules/party"
|
|
"git.golem15.com/golem15/summercms/modules/surf"
|
|
"github.com/go-gormigrate/gormigrate/v2"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var (
|
|
_ party.Plugin = (*Plugin)(nil)
|
|
_ pact.HasConfig = (*Plugin)(nil)
|
|
_ pact.HasMigrations = (*Plugin)(nil)
|
|
_ pact.HasMiddleware = (*Plugin)(nil)
|
|
_ pact.HasModels = (*Plugin)(nil)
|
|
_ pact.HasRoutes = (*Plugin)(nil)
|
|
_ pact.HasMailTemplates = (*Plugin)(nil)
|
|
_ pact.HasLang = (*Plugin)(nil)
|
|
_ pact.HasCommands = (*Plugin)(nil)
|
|
_ surf.BucketProvider = (*Plugin)(nil)
|
|
_ bouncer.UserProvider = classes.GormUsers{}
|
|
)
|
|
|
|
//go:embed config
|
|
var configFS embed.FS
|
|
|
|
//go:embed views/mail
|
|
var mailFS embed.FS
|
|
|
|
//go:embed lang
|
|
var langFS embed.FS
|
|
|
|
// Plugin is the golem15.user plugin.
|
|
type Plugin struct {
|
|
app *backpack.App
|
|
sweepStop chan struct{}
|
|
}
|
|
|
|
func (p *Plugin) ID() string { return "golem15.user" }
|
|
func (p *Plugin) Requires() []string { return nil }
|
|
|
|
func (p *Plugin) Register(app *backpack.App) error {
|
|
p.app = app
|
|
return nil
|
|
}
|
|
|
|
func (p *Plugin) Boot(app *backpack.App) error {
|
|
p.app = app
|
|
secret, err := classes.JWTSecret(app)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if gdb, ok := app.Lookup[*gorm.DB](); ok {
|
|
if err := classes.RegisterHooks(gdb); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
reg, ok := app.Lookup[*bouncer.Registry]()
|
|
if !ok || reg == nil {
|
|
reg = bouncer.NewRegistry()
|
|
if err := app.Publish(reg); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
var bl bouncer.BlacklistStore
|
|
if sqlDB, ok := app.Lookup[*sql.DB](); ok && sqlDB != nil {
|
|
bl = bouncer.NewPostgresBlacklist(sqlDB, "jwt_blacklist")
|
|
} else if gdb, ok := app.Lookup[*gorm.DB](); ok && gdb != nil {
|
|
if sqlDB, err := gdb.DB(); err == nil && sqlDB != nil {
|
|
bl = bouncer.NewPostgresBlacklist(sqlDB, "jwt_blacklist")
|
|
}
|
|
}
|
|
if bl != nil {
|
|
if err := app.Publish[bouncer.BlacklistStore](bl); err != nil {
|
|
return err
|
|
}
|
|
p.startBlacklistSweep(bl)
|
|
}
|
|
return reg.Register(p.ID(), "jwt", bouncer.NewJWTGuard(secret, classes.GormUsers{App: app}, bl, "token", "auth_token"))
|
|
}
|
|
|
|
func (p *Plugin) startBlacklistSweep(bl bouncer.BlacklistStore) {
|
|
interval := 10 * time.Minute
|
|
if p.app != nil && p.app.Config != nil {
|
|
raw := p.app.Config.String("golem15.user.jwt.blacklist_sweep_interval")
|
|
if raw != "" {
|
|
parsed, err := time.ParseDuration(raw)
|
|
if err != nil || parsed <= 0 {
|
|
return
|
|
}
|
|
interval = parsed
|
|
}
|
|
}
|
|
if p.sweepStop != nil {
|
|
return
|
|
}
|
|
p.sweepStop = make(chan struct{})
|
|
stop := p.sweepStop
|
|
go func() {
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
if err := bl.Sweep(context.Background(), time.Now()); err != nil {
|
|
slog.Default().Error("golem15.user: jwt blacklist sweep", "err", err)
|
|
}
|
|
case <-stop:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (p *Plugin) Buckets() map[string]surf.Bucket {
|
|
trusted := surf.TrustedProxies(nil)
|
|
if p.app != nil {
|
|
trusted = surf.TrustedProxies(p.app.Config)
|
|
}
|
|
secret := ""
|
|
if p.app != nil {
|
|
secret, _ = classes.JWTSecret(p.app)
|
|
}
|
|
return map[string]surf.Bucket{
|
|
"user-api": {
|
|
Max: 120,
|
|
Decay: time.Minute,
|
|
Key: func(r *http.Request) string {
|
|
if sub, err := bouncer.Verify(bearerFrom(r), secret); err == nil {
|
|
return "u:" + sub
|
|
}
|
|
return surf.ClientIP(r, trusted)
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func bearerFrom(r *http.Request) string {
|
|
h := strings.TrimSpace(r.Header.Get("Authorization"))
|
|
token, ok := strings.CutPrefix(h, "Bearer ")
|
|
token = strings.TrimSpace(token)
|
|
if !ok || token == "" {
|
|
return ""
|
|
}
|
|
return token
|
|
}
|
|
|
|
func (p *Plugin) ConfigFS() fs.FS { return configFS }
|
|
|
|
func (p *Plugin) LangFS() fs.FS { return langFS }
|
|
|
|
func (p *Plugin) MailTemplatesFS() fs.FS { return mailFS }
|
|
|
|
func (p *Plugin) MailTemplates() []string {
|
|
return []string{
|
|
"golem15.user::mail.activate",
|
|
"golem15.user::mail.activate-en",
|
|
"golem15.user::mail.restore",
|
|
"golem15.user::mail.restore-en",
|
|
"golem15.user::mail.reactivate",
|
|
"golem15.user::mail.reactivate-en",
|
|
}
|
|
}
|
|
|
|
func (p *Plugin) MailLayouts() map[string]string {
|
|
return map[string]string{
|
|
"user": "golem15.user::mail.layouts.user",
|
|
}
|
|
}
|
|
|
|
func (p *Plugin) Commands() []bonfire.Command {
|
|
return []bonfire.Command{console.RequirePasswordChange(p.app)}
|
|
}
|
|
|
|
func (p *Plugin) Migrations() []*gormigrate.Migration { return updates.All() }
|
|
|
|
func (p *Plugin) Models() []any { return models.All() }
|
|
|
|
func (p *Plugin) Middlewares() map[string]pact.Middleware {
|
|
if p.app == nil {
|
|
return nil
|
|
}
|
|
reg, ok := p.app.Lookup[*bouncer.Registry]()
|
|
if !ok || reg == nil {
|
|
return nil
|
|
}
|
|
mw, err := reg.Middleware("jwt")
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return map[string]pact.Middleware{"jwt.auth": mw}
|
|
}
|
|
|
|
func init() {
|
|
party.Register(&Plugin{})
|
|
}
|