fixed relationship issues

This commit is contained in:
2026-02-05 00:10:10 +11:00
parent 6a058ab636
commit 59ee880b63
22 changed files with 236 additions and 254 deletions

View File

@@ -1,20 +1,18 @@
package main
import (
"context"
"database/sql"
"fmt"
"time"
"git.haelnorr.com/h/oslstats/internal/config"
"git.haelnorr.com/h/oslstats/internal/db"
"github.com/pkg/errors"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/driver/pgdriver"
)
func setupBun(ctx context.Context, cfg *config.Config) (conn *bun.DB, close func() error, err error) {
func setupBun(cfg *config.Config) (conn *bun.DB, close func() error) {
dsn := fmt.Sprintf("postgres://%s:%s@%s:%v/%s?sslmode=%s",
cfg.DB.User, cfg.DB.Password, cfg.DB.Host, cfg.DB.Port, cfg.DB.DB, cfg.DB.SSL)
sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
@@ -26,30 +24,19 @@ func setupBun(ctx context.Context, cfg *config.Config) (conn *bun.DB, close func
conn = bun.NewDB(sqldb, pgdialect.New())
close = sqldb.Close
err = loadModels(ctx, conn)
if err != nil {
return nil, nil, errors.Wrap(err, "loadModels")
}
return conn, close, nil
return conn, close
}
func loadModels(ctx context.Context, conn *bun.DB) error {
func registerDBModels(conn *bun.DB) {
models := []any{
(*db.RolePermission)(nil),
(*db.UserRole)(nil),
(*db.User)(nil),
(*db.DiscordToken)(nil),
(*db.Season)(nil),
(*db.Role)(nil),
(*db.Permission)(nil),
(*db.AuditLog)(nil),
}
for _, model := range models {
_, err := conn.NewCreateTable().
Model(model).
IfNotExists().
Exec(ctx)
if err != nil {
return errors.Wrap(err, "db.NewCreateTable")
}
}
return nil
conn.RegisterModel(models...)
}

View File

@@ -61,7 +61,7 @@ func setupHTTPServer(
}
// Initialize permissions checker
perms, err := rbac.NewChecker(bun, server)
perms, err := rbac.NewChecker(bun, httpServer)
if err != nil {
return nil, errors.Wrap(err, "rbac.NewChecker")
}

View File

@@ -10,6 +10,8 @@ import (
"text/tabwriter"
"time"
stderrors "errors"
"git.haelnorr.com/h/oslstats/cmd/oslstats/migrations"
"git.haelnorr.com/h/oslstats/internal/backup"
"git.haelnorr.com/h/oslstats/internal/config"
@@ -21,11 +23,8 @@ import (
// runMigrations executes database migrations
func runMigrations(ctx context.Context, cfg *config.Config, command string) error {
conn, close, err := setupBun(ctx, cfg)
if err != nil {
return errors.Wrap(err, "setupBun")
}
defer close()
conn, close := setupBun(cfg)
defer func() { _ = close() }()
migrator := migrate.NewMigrator(conn, migrations.Migrations)
@@ -36,7 +35,14 @@ func runMigrations(ctx context.Context, cfg *config.Config, command string) erro
switch command {
case "up":
return migrateUp(ctx, migrator, conn, cfg)
err := migrateUp(ctx, migrator, conn, cfg)
if err != nil {
err2 := migrateRollback(ctx, migrator, conn, cfg)
if err2 != nil {
return stderrors.Join(errors.Wrap(err2, "error while rolling back after migration error"), err)
}
}
return err
case "rollback":
return migrateRollback(ctx, migrator, conn, cfg)
case "status":
@@ -171,8 +177,8 @@ func migrateStatus(ctx context.Context, migrator *migrate.Migrator) error {
fmt.Println("╚══════════════════════════════════════════════════════════╝")
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
fmt.Fprintln(w, "STATUS\tMIGRATION\tGROUP\tMIGRATED AT")
fmt.Fprintln(w, "------\t---------\t-----\t-----------")
_, _ = fmt.Fprintln(w, "STATUS\tMIGRATION\tGROUP\tMIGRATED AT")
_, _ = fmt.Fprintln(w, "------\t---------\t-----\t-----------")
appliedCount := 0
for _, m := range ms {
@@ -189,10 +195,10 @@ func migrateStatus(ctx context.Context, migrator *migrate.Migrator) error {
}
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", status, m.Name, group, migratedAt)
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", status, m.Name, group, migratedAt)
}
w.Flush()
_ = w.Flush()
fmt.Printf("\n📊 Summary: %d applied, %d pending\n\n",
appliedCount, len(ms)-appliedCount)
@@ -299,12 +305,12 @@ func init() {
Migrations.MustRegister(
// UP migration
func(ctx context.Context, dbConn *bun.DB) error {
// TODO: Add your migration code here
// Add your migration code here
return nil
},
// DOWN migration
func(ctx context.Context, dbConn *bun.DB) error {
// TODO: Add your rollback code here
// Add your rollback code here
return nil
},
)
@@ -326,7 +332,7 @@ func init() {
// resetDatabase drops and recreates all tables (destructive)
func resetDatabase(ctx context.Context, cfg *config.Config) error {
fmt.Println("⚠️ WARNING: This will DELETE ALL DATA in the database!")
fmt.Println("⚠️ WARNING - This will DELETE ALL DATA in the database!")
fmt.Print("Type 'yes' to continue: ")
reader := bufio.NewReader(os.Stdin)
@@ -340,11 +346,8 @@ func resetDatabase(ctx context.Context, cfg *config.Config) error {
fmt.Println("❌ Reset cancelled")
return nil
}
conn, close, err := setupBun(ctx, cfg)
if err != nil {
return errors.Wrap(err, "setupBun")
}
defer close()
conn, close := setupBun(cfg)
defer func() { _ = close() }()
models := []any{
(*db.User)(nil),

View File

@@ -12,20 +12,11 @@ func init() {
Migrations.MustRegister(
// UP migration
func(ctx context.Context, dbConn *bun.DB) error {
// Create roles table using raw SQL to avoid m2m relationship issues
// Bun tries to resolve relationships when creating tables from models
// TODO: use proper m2m table instead of raw sql
_, err := dbConn.ExecContext(ctx, `
CREATE TABLE roles (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
display_name VARCHAR(100) NOT NULL,
description TEXT,
is_system BOOLEAN DEFAULT FALSE,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL
)
`)
dbConn.RegisterModel((*db.RolePermission)(nil), (*db.UserRole)(nil))
// Create permissions table
_, err := dbConn.NewCreateTable().
Model((*db.Role)(nil)).
Exec(ctx)
if err != nil {
return err
}
@@ -39,7 +30,6 @@ func init() {
}
// Create indexes for permissions
// TODO: why do we need this?
_, err = dbConn.NewCreateIndex().
Model((*db.Permission)(nil)).
Index("idx_permissions_resource").
@@ -49,7 +39,6 @@ func init() {
return err
}
// TODO: why do we need this?
_, err = dbConn.NewCreateIndex().
Model((*db.Permission)(nil)).
Index("idx_permissions_action").
@@ -59,22 +48,13 @@ func init() {
return err
}
// Create role_permissions join table (Bun doesn't auto-create m2m tables)
// TODO: use proper m2m table instead of raw sql
_, err = dbConn.ExecContext(ctx, `
CREATE TABLE role_permissions (
id SERIAL PRIMARY KEY,
role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
permission_id INTEGER NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
created_at BIGINT NOT NULL,
UNIQUE(role_id, permission_id)
)
`)
_, err = dbConn.NewCreateTable().
Model((*db.RolePermission)(nil)).
Exec(ctx)
if err != nil {
return err
}
// TODO: why do we need this?
_, err = dbConn.ExecContext(ctx, `
CREATE INDEX idx_role_permissions_role ON role_permissions(role_id)
`)
@@ -82,7 +62,6 @@ func init() {
return err
}
// TODO: why do we need this?
_, err = dbConn.ExecContext(ctx, `
CREATE INDEX idx_role_permissions_permission ON role_permissions(permission_id)
`)
@@ -99,7 +78,6 @@ func init() {
}
// Create indexes for user_roles
// TODO: why do we need this?
_, err = dbConn.NewCreateIndex().
Model((*db.UserRole)(nil)).
Index("idx_user_roles_user").
@@ -109,7 +87,6 @@ func init() {
return err
}
// TODO: why do we need this?
_, err = dbConn.NewCreateIndex().
Model((*db.UserRole)(nil)).
Index("idx_user_roles_role").
@@ -128,7 +105,6 @@ func init() {
}
// Create indexes for audit_log
// TODO: why do we need this?
_, err = dbConn.NewCreateIndex().
Model((*db.AuditLog)(nil)).
Index("idx_audit_log_user").
@@ -138,7 +114,6 @@ func init() {
return err
}
// TODO: why do we need this?
_, err = dbConn.NewCreateIndex().
Model((*db.AuditLog)(nil)).
Index("idx_audit_log_action").
@@ -148,7 +123,6 @@ func init() {
return err
}
// TODO: why do we need this?
_, err = dbConn.NewCreateIndex().
Model((*db.AuditLog)(nil)).
Index("idx_audit_log_resource").
@@ -158,7 +132,6 @@ func init() {
return err
}
// TODO: why do we need this?
_, err = dbConn.NewCreateIndex().
Model((*db.AuditLog)(nil)).
Index("idx_audit_log_created").
@@ -176,12 +149,12 @@ func init() {
DisplayName: "Administrator",
Description: "Full system access with all permissions",
IsSystem: true,
CreatedAt: now, // TODO: this should be defaulted in table
UpdatedAt: now, // TODO: this should be defaulted in table
CreatedAt: now,
}
_, err = dbConn.NewInsert().
Model(adminRole).
Returning("id").
Exec(ctx)
if err != nil {
return err
@@ -192,9 +165,7 @@ func init() {
DisplayName: "User",
Description: "Standard user with basic permissions",
IsSystem: true,
CreatedAt: now, // TODO: this should be defaulted in table
UpdatedAt: now, // TODO: this should be defaulted in table
CreatedAt: now,
}
_, err = dbConn.NewInsert().
@@ -205,7 +176,6 @@ func init() {
}
// Seed system permissions
// TODO: timestamps for created should be defaulted in table
permissionsData := []*db.Permission{
{Name: "*", DisplayName: "Wildcard (All Permissions)", Description: "Grants access to all permissions, past, present, and future", Resource: "*", Action: "*", IsSystem: true, CreatedAt: now},
{Name: "seasons.create", DisplayName: "Create Seasons", Description: "Create new seasons", Resource: "seasons", Action: "create", IsSystem: true, CreatedAt: now},
@@ -235,11 +205,14 @@ func init() {
}
// Insert role_permission mapping
// TODO: use proper m2m table, and default now in table settings
_, err = dbConn.ExecContext(ctx, `
INSERT INTO role_permissions (role_id, permission_id, created_at)
VALUES ($1, $2, $3)
`, adminRole.ID, wildcardPerm.ID, now)
adminRolePerms := &db.RolePermission{
RoleID: adminRole.ID,
PermissionID: wildcardPerm.ID,
}
_, err = dbConn.NewInsert().
Model(adminRolePerms).
On("CONFLICT (role_id, permission_id) DO NOTHING").
Exec(ctx)
if err != nil {
return err
}
@@ -250,7 +223,6 @@ func init() {
func(ctx context.Context, dbConn *bun.DB) error {
// Drop tables in reverse order
// Use raw SQL to avoid relationship resolution issues
// TODO: surely we can use proper bun methods?
tables := []string{
"audit_log",
"user_roles",

View File

@@ -30,6 +30,11 @@ func addRoutes(
) error {
// Create the routes
pageroutes := []hws.Route{
{
Path: "/permtest",
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST},
Handler: handlers.PermTester(s, conn),
},
{
Path: "/static/",
Method: hws.MethodGET,
@@ -63,8 +68,7 @@ func addRoutes(
{
Path: "/notification-tester",
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST},
Handler: handlers.NotifyTester(s),
// TODO: add login protection
Handler: perms.RequireAdmin(s)(handlers.NotifyTester(s)),
},
{
Path: "/seasons",

View File

@@ -25,10 +25,8 @@ func run(ctx context.Context, logger *hlog.Logger, cfg *config.Config) error {
// Setup the database connection
logger.Debug().Msg("Config loaded and logger started")
logger.Debug().Msg("Connecting to database")
bun, closedb, err := setupBun(ctx, cfg)
if err != nil {
return errors.Wrap(err, "setupDBConn")
}
bun, closedb := setupBun(cfg)
registerDBModels(bun)
// Setup embedded files
logger.Debug().Msg("Getting embedded files")