package db import ( "context" "database/sql" "git.haelnorr.com/h/oslstats/internal/roles" "github.com/pkg/errors" "github.com/uptrace/bun" ) type Role struct { bun.BaseModel `bun:"table:roles,alias:r"` ID int `bun:"id,pk,autoincrement"` Name roles.Role `bun:"name,unique,notnull"` DisplayName string `bun:"display_name,notnull"` Description string `bun:"description"` IsSystem bool `bun:"is_system,default:false"` CreatedAt int64 `bun:"created_at,notnull"` UpdatedAt int64 `bun:"updated_at,notnull"` // Relations (loaded on demand) Permissions []*Permission `bun:"m2m:role_permissions,join:Role=Permission"` } // GetRoleByName queries the database for a role matching the given name // Returns nil, nil if no role is found func GetRoleByName(ctx context.Context, tx bun.Tx, name roles.Role) (*Role, error) { if name == "" { return nil, errors.New("name cannot be empty") } role := new(Role) err := tx.NewSelect(). Model(role). Where("name = ?", name). Limit(1). Scan(ctx) if err != nil && err != sql.ErrNoRows { return nil, errors.Wrap(err, "tx.NewSelect") } return role, nil } // GetRoleByID queries the database for a role matching the given ID // Returns nil, nil if no role is found func GetRoleByID(ctx context.Context, tx bun.Tx, id int) (*Role, error) { if id <= 0 { return nil, errors.New("id must be positive") } role := new(Role) err := tx.NewSelect(). Model(role). Where("id = ?", id). Limit(1). Scan(ctx) if err != nil && err != sql.ErrNoRows { return nil, errors.Wrap(err, "tx.NewSelect") } return role, nil } // GetRoleWithPermissions loads a role and all its permissions func GetRoleWithPermissions(ctx context.Context, tx bun.Tx, id int) (*Role, error) { if id <= 0 { return nil, errors.New("id must be positive") } role := new(Role) err := tx.NewSelect(). Model(role). Where("id = ?", id). Relation("Permissions"). Limit(1). Scan(ctx) if err != nil && err != sql.ErrNoRows { return nil, errors.Wrap(err, "tx.NewSelect") } return role, nil } // ListAllRoles returns all roles func ListAllRoles(ctx context.Context, tx bun.Tx) ([]*Role, error) { var roles []*Role err := tx.NewSelect(). Model(&roles). Order("name ASC"). Scan(ctx) if err != nil && err != sql.ErrNoRows { return nil, errors.Wrap(err, "tx.NewSelect") } return roles, nil } // CreateRole creates a new role func CreateRole(ctx context.Context, tx bun.Tx, role *Role) error { if role == nil { return errors.New("role cannot be nil") } _, err := tx.NewInsert(). Model(role). Exec(ctx) if err != nil { return errors.Wrap(err, "tx.NewInsert") } return nil } // UpdateRole updates an existing role func UpdateRole(ctx context.Context, tx bun.Tx, role *Role) error { if role == nil { return errors.New("role cannot be nil") } if role.ID <= 0 { return errors.New("role id must be positive") } _, err := tx.NewUpdate(). Model(role). WherePK(). Exec(ctx) if err != nil { return errors.Wrap(err, "tx.NewUpdate") } return nil } // DeleteRole deletes a role (checks IsSystem protection) func DeleteRole(ctx context.Context, tx bun.Tx, id int) error { if id <= 0 { return errors.New("id must be positive") } // Check if role is system role role, err := GetRoleByID(ctx, tx, id) if err != nil { return errors.Wrap(err, "GetRoleByID") } if role == nil { return errors.New("role not found") } if role.IsSystem { return errors.New("cannot delete system role") } _, err = tx.NewDelete(). Model((*Role)(nil)). Where("id = ?", id). Exec(ctx) if err != nil { return errors.Wrap(err, "tx.NewDelete") } return nil } // AddPermissionToRole grants a permission to a role func AddPermissionToRole(ctx context.Context, tx bun.Tx, roleID, permissionID int, createdAt int64) error { if roleID <= 0 { return errors.New("roleID must be positive") } if permissionID <= 0 { return errors.New("permissionID must be positive") } // TODO: use proper m2m table // also make createdAt automatic in table so not required as input here _, err := tx.ExecContext(ctx, ` INSERT INTO role_permissions (role_id, permission_id, created_at) VALUES ($1, $2, $3) ON CONFLICT (role_id, permission_id) DO NOTHING `, roleID, permissionID, createdAt) if err != nil { return errors.Wrap(err, "tx.ExecContext") } return nil } // RemovePermissionFromRole revokes a permission from a role func RemovePermissionFromRole(ctx context.Context, tx bun.Tx, roleID, permissionID int) error { if roleID <= 0 { return errors.New("roleID must be positive") } if permissionID <= 0 { return errors.New("permissionID must be positive") } // TODO: use proper m2m table _, err := tx.ExecContext(ctx, ` DELETE FROM role_permissions WHERE role_id = $1 AND permission_id = $2 `, roleID, permissionID) if err != nil { return errors.Wrap(err, "tx.ExecContext") } return nil }