fixed relationship issues
This commit is contained in:
@@ -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...)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user