From 2d4245681e1294aa57eb4090c366bf2ba182850a Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Fri, 18 Sep 2026 18:43:31 +0200 Subject: [PATCH] feat(05-01): restructure fonoteka plugins into Winter directories - Move models, classes, controllers, middleware, and updates into subpackages - Add self-registering models/updates/classes registries so later plans add files - Keep the genres route and shipped migration SQL unchanged --- classes/registry.go | 21 +++++++++++++++++++ user.go => classes/user_lookup.go | 31 ++++++++++++----------------- models/registry.go | 13 ++++++++++++ models/user.go | 15 ++++++++++++++ plugin.go | 25 ++++++++++++++++------- migrations.go => updates/00_base.go | 6 +++++- updates/registry.go | 15 ++++++++++++++ 7 files changed, 100 insertions(+), 26 deletions(-) create mode 100644 classes/registry.go rename user.go => classes/user_lookup.go (61%) create mode 100644 models/registry.go create mode 100644 models/user.go rename migrations.go => updates/00_base.go (92%) create mode 100644 updates/registry.go diff --git a/classes/registry.go b/classes/registry.go new file mode 100644 index 0000000..7bfe14e --- /dev/null +++ b/classes/registry.go @@ -0,0 +1,21 @@ +package classes + +import "gorm.io/gorm" + +var hookRegistrars []func(*gorm.DB) error + +// RegisterHook appends a GORM-callback registrant so later plans add files +// without editing plugin.go Boot. +func RegisterHook(fn func(*gorm.DB) error) { + hookRegistrars = append(hookRegistrars, fn) +} + +// RegisterHooks runs every registered hook against gdb. +func RegisterHooks(gdb *gorm.DB) error { + for _, fn := range hookRegistrars { + if err := fn(gdb); err != nil { + return err + } + } + return nil +} diff --git a/user.go b/classes/user_lookup.go similarity index 61% rename from user.go rename to classes/user_lookup.go index 2b0a949..1f57bf9 100644 --- a/user.go +++ b/classes/user_lookup.go @@ -1,4 +1,4 @@ -package user +package classes import ( "context" @@ -6,34 +6,28 @@ import ( "fmt" "strings" + "git.golem15.com/golem15/fonoteka/plugins/golem15/user/models" "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"` +var _ bouncer.UserProvider = GormUsers{} + +// GormUsers loads JWT principals from the users table. +type GormUsers struct { + App *backpack.App } -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 { +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]() + gdb, ok := g.App.Lookup[*gorm.DB]() if !ok || gdb == nil { return nil, fmt.Errorf("golem15.user: gorm handle is missing") } - var row User + var row models.User err := gdb.WithContext(ctx).Where("id = ?", id).Take(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil @@ -44,7 +38,8 @@ func (g gormUsers) FindByID(ctx context.Context, id uint) (*bouncer.Principal, e return &bouncer.Principal{ID: row.ID, MustChangePassword: row.MustChangePassword}, nil } -func jwtSecret(app *backpack.App) (string, error) { +// JWTSecret reads golem15.user.jwt.secret and fails loudly when it is empty. +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)") } diff --git a/models/registry.go b/models/registry.go new file mode 100644 index 0000000..2bc30cf --- /dev/null +++ b/models/registry.go @@ -0,0 +1,13 @@ +package models + +var all []any + +// Register appends GORM models so later plans add files without editing plugin.go. +func Register(models ...any) { + all = append(all, models...) +} + +// All returns every model registered by this plugin's models package. +func All() []any { + return all +} diff --git a/models/user.go b/models/user.go new file mode 100644 index 0000000..dc6769e --- /dev/null +++ b/models/user.go @@ -0,0 +1,15 @@ +package models + +// 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" } + +func init() { + Register(User{}) +} diff --git a/plugin.go b/plugin.go index 7459d2a..2ff29ad 100644 --- a/plugin.go +++ b/plugin.go @@ -4,11 +4,15 @@ import ( "embed" "io/fs" + "git.golem15.com/golem15/fonoteka/plugins/golem15/user/classes" + "git.golem15.com/golem15/fonoteka/plugins/golem15/user/models" + "git.golem15.com/golem15/fonoteka/plugins/golem15/user/updates" "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" + "gorm.io/gorm" ) var ( @@ -17,7 +21,7 @@ var ( _ pact.HasMigrations = (*Plugin)(nil) _ pact.HasMiddleware = (*Plugin)(nil) _ pact.HasModels = (*Plugin)(nil) - _ bouncer.UserProvider = gormUsers{} + _ bouncer.UserProvider = classes.GormUsers{} ) //go:embed config @@ -38,23 +42,30 @@ func (p *Plugin) Register(app *backpack.App) error { func (p *Plugin) Boot(app *backpack.App) error { p.app = app - _, err := jwtSecret(app) - return err + if _, err := classes.JWTSecret(app); err != nil { + return err + } + if gdb, ok := app.Lookup[*gorm.DB](); ok { + if err := classes.RegisterHooks(gdb); err != nil { + return err + } + } + return nil } func (p *Plugin) ConfigFS() fs.FS { return configFS } -func (p *Plugin) Migrations() []*gormigrate.Migration { return migrations } +func (p *Plugin) Migrations() []*gormigrate.Migration { return updates.All() } -func (p *Plugin) Models() []any { return []any{User{}} } +func (p *Plugin) Models() []any { return models.All() } func (p *Plugin) Middlewares() map[string]pact.Middleware { - secret, err := jwtSecret(p.app) + secret, err := classes.JWTSecret(p.app) if err != nil { return nil } return map[string]pact.Middleware{ - "jwt.auth": bouncer.Middleware(secret, gormUsers{app: p.app}), + "jwt.auth": bouncer.Middleware(secret, classes.GormUsers{App: p.app}), } } diff --git a/migrations.go b/updates/00_base.go similarity index 92% rename from migrations.go rename to updates/00_base.go index 4a09622..795560e 100644 --- a/migrations.go +++ b/updates/00_base.go @@ -1,4 +1,4 @@ -package user +package updates import ( "github.com/go-gormigrate/gormigrate/v2" @@ -27,3 +27,7 @@ CREATE TABLE users ( }, }, } + +func init() { + Register(migrations...) +} diff --git a/updates/registry.go b/updates/registry.go new file mode 100644 index 0000000..4f87cfd --- /dev/null +++ b/updates/registry.go @@ -0,0 +1,15 @@ +package updates + +import "github.com/go-gormigrate/gormigrate/v2" + +var all []*gormigrate.Migration + +// Register appends gormigrate steps so later plans add files without editing plugin.go. +func Register(ms ...*gormigrate.Migration) { + all = append(all, ms...) +} + +// All returns every migration registered by this plugin's updates package. +func All() []*gormigrate.Migration { + return all +}