feat(03-01): serve seeded genres behind JWT middleware
golem15.user registers jwt.auth, golem15.fonoteka mounts GET /_fonoteka/api/v1/genres through that guard, and a smoke test reads the 15 migrated rows with a test-only token. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1
go.mod
1
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
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
38
plugin.go
38
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{})
|
||||
}
|
||||
|
||||
56
user.go
Normal file
56
user.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user