Files
oslstats/internal/db/role.go
2026-02-03 21:37:06 +11:00

205 lines
4.8 KiB
Go

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
}