package user import ( "context" "database/sql" "embed" "io/fs" "log/slog" "net/http" "strings" "time" "git.golem15.com/golem15/fonoteka/plugins/golem15/user/classes" "git.golem15.com/golem15/fonoteka/plugins/golem15/user/console" "git.golem15.com/golem15/fonoteka/plugins/golem15/user/models" "git.golem15.com/golem15/fonoteka/plugins/golem15/user/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{}) }