diff --git a/go.mod b/go.mod index 2bd7a90..6195f6e 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( require ( github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect diff --git a/go.sum b/go.sum index 630b95b..a787ae5 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/go-gormigrate/gormigrate/v2 v2.1.7 h1:PdT4jVPbRb4R+0Ey2R0yJOdctVf4Whi github.com/go-gormigrate/gormigrate/v2 v2.1.7/go.mod h1:3ouXglTuPrKF5+7cQyVGfvAXTU4vLMaYh9+EPl03uog= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= diff --git a/plugin.go b/plugin.go index 58133f7..7459d2a 100644 --- a/plugin.go +++ b/plugin.go @@ -5,33 +5,59 @@ import ( "io/fs" "git.golem15.com/golem15/summercms/backpack" + "git.golem15.com/golem15/summercms/bouncer" "git.golem15.com/golem15/summercms/pact" "git.golem15.com/golem15/summercms/party" "github.com/go-gormigrate/gormigrate/v2" ) var ( - _ party.Plugin = (*Plugin)(nil) - _ pact.HasConfig = (*Plugin)(nil) - _ pact.HasMigrations = (*Plugin)(nil) + _ party.Plugin = (*Plugin)(nil) + _ pact.HasConfig = (*Plugin)(nil) + _ pact.HasMigrations = (*Plugin)(nil) + _ pact.HasMiddleware = (*Plugin)(nil) + _ pact.HasModels = (*Plugin)(nil) + _ bouncer.UserProvider = gormUsers{} ) //go:embed config var configFS embed.FS // Plugin is the stub golem15.user plugin. -type Plugin struct{} +type Plugin struct { + app *backpack.App +} func (p *Plugin) ID() string { return "golem15.user" } func (p *Plugin) Requires() []string { return nil } -func (p *Plugin) Register(*backpack.App) error { return nil } -func (p *Plugin) Boot(*backpack.App) error { 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 + _, err := jwtSecret(app) + return err +} func (p *Plugin) ConfigFS() fs.FS { return configFS } func (p *Plugin) Migrations() []*gormigrate.Migration { return migrations } +func (p *Plugin) Models() []any { return []any{User{}} } + +func (p *Plugin) Middlewares() map[string]pact.Middleware { + secret, err := jwtSecret(p.app) + if err != nil { + return nil + } + return map[string]pact.Middleware{ + "jwt.auth": bouncer.Middleware(secret, gormUsers{app: p.app}), + } +} + func init() { party.Register(&Plugin{}) } diff --git a/user.go b/user.go new file mode 100644 index 0000000..2b0a949 --- /dev/null +++ b/user.go @@ -0,0 +1,56 @@ +package user + +import ( + "context" + "errors" + "fmt" + "strings" + + "git.golem15.com/golem15/summercms/backpack" + "git.golem15.com/golem15/summercms/bouncer" + "gorm.io/gorm" +) + +// User is the minimal users row the JWT guard loads. +type User struct { + ID uint `gorm:"column:id;primaryKey"` + Email string `gorm:"column:email"` + Password string `gorm:"column:password"` + MustChangePassword bool `gorm:"column:must_change_password"` +} + +func (User) TableName() string { return "users" } + +type gormUsers struct { + app *backpack.App +} + +func (g gormUsers) FindByID(ctx context.Context, id uint) (*bouncer.Principal, error) { + if g.app == nil { + return nil, fmt.Errorf("golem15.user: app is nil") + } + gdb, ok := g.app.Lookup[*gorm.DB]() + if !ok || gdb == nil { + return nil, fmt.Errorf("golem15.user: gorm handle is missing") + } + var row User + err := gdb.WithContext(ctx).Where("id = ?", id).Take(&row).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + if err != nil { + return nil, err + } + return &bouncer.Principal{ID: row.ID, MustChangePassword: row.MustChangePassword}, nil +} + +func jwtSecret(app *backpack.App) (string, error) { + if app == nil || app.Config == nil { + return "", fmt.Errorf("golem15.user: jwt.secret is empty (set SUMMER_GOLEM15__USER__JWT__SECRET)") + } + secret := strings.TrimSpace(app.Config.String("golem15.user.jwt.secret")) + if secret == "" { + return "", fmt.Errorf("golem15.user: jwt.secret is empty (set SUMMER_GOLEM15__USER__JWT__SECRET)") + } + return secret, nil +}