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
This commit is contained in:
Jakub Zych
2026-09-18 18:43:31 +02:00
parent 5530fa4ebf
commit 2d4245681e
7 changed files with 100 additions and 26 deletions

21
classes/registry.go Normal file
View File

@@ -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
}

View File

@@ -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)")
}

13
models/registry.go Normal file
View File

@@ -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
}

15
models/user.go Normal file
View File

@@ -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{})
}

View File

@@ -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}),
}
}

View File

@@ -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...)
}

15
updates/registry.go Normal file
View File

@@ -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
}