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

115 lines
3.0 KiB
Go

package db
import (
"context"
"time"
"git.haelnorr.com/h/oslstats/internal/roles"
"github.com/pkg/errors"
"github.com/uptrace/bun"
)
type UserRole struct {
bun.BaseModel `bun:"table:user_roles,alias:ur"`
ID int `bun:"id,pk,autoincrement"`
UserID int `bun:"user_id,notnull"`
RoleID int `bun:"role_id,notnull"`
GrantedBy *int `bun:"granted_by"`
GrantedAt int64 `bun:"granted_at,notnull"` // TODO: default now
ExpiresAt *int64 `bun:"expires_at"`
// Relations
User *User `bun:"rel:belongs-to,join:user_id=id"`
Role *Role `bun:"rel:belongs-to,join:role_id=id"`
}
// GetUserRoles loads all roles for a given user
func GetUserRoles(ctx context.Context, tx bun.Tx, userID int) ([]*Role, error) {
if userID <= 0 {
return nil, errors.New("userID must be positive")
}
var roles []*Role
err := tx.NewSelect().
Model(&roles).
// TODO: why are we joining? can we do relation?
Join("JOIN user_roles AS ur ON ur.role_id = r.id").
Where("ur.user_id = ?", userID).
Where("ur.expires_at IS NULL OR ur.expires_at > ?", time.Now().Unix()).
Scan(ctx)
if err != nil {
return nil, errors.Wrap(err, "tx.NewSelect")
}
return roles, nil
}
// AssignRole grants a role to a user
func AssignRole(ctx context.Context, tx bun.Tx, userID, roleID int, grantedBy *int) error {
if userID <= 0 {
return errors.New("userID must be positive")
}
if roleID <= 0 {
return errors.New("roleID must be positive")
}
now := time.Now().Unix()
// TODO: use proper m2m table instead of raw SQL
_, err := tx.ExecContext(ctx, `
INSERT INTO user_roles (user_id, role_id, granted_by, granted_at)
VALUES ($1, $2, $3, $4)
ON CONFLICT (user_id, role_id) DO NOTHING
`, userID, roleID, grantedBy, now)
if err != nil {
return errors.Wrap(err, "tx.ExecContext")
}
return nil
}
// RevokeRole removes a role from a user
func RevokeRole(ctx context.Context, tx bun.Tx, userID, roleID int) error {
if userID <= 0 {
return errors.New("userID must be positive")
}
if roleID <= 0 {
return errors.New("roleID must be positive")
}
// TODO: use proper m2m table instead of raw sql
_, err := tx.ExecContext(ctx, `
DELETE FROM user_roles
WHERE user_id = $1 AND role_id = $2
`, userID, roleID)
if err != nil {
return errors.Wrap(err, "tx.ExecContext")
}
return nil
}
// HasRole checks if a user has a specific role
func HasRole(ctx context.Context, tx bun.Tx, userID int, roleName roles.Role) (bool, error) {
if userID <= 0 {
return false, errors.New("userID must be positive")
}
if roleName == "" {
return false, errors.New("roleName cannot be empty")
}
// TODO: use proper m2m table instead of TableExpr and Join?
count, err := tx.NewSelect().
TableExpr("user_roles AS ur").
Join("JOIN roles AS r ON r.id = ur.role_id").
Where("ur.user_id = ?", userID).
Where("r.name = ?", roleName).
Where("ur.expires_at IS NULL OR ur.expires_at > ?", time.Now().Unix()).
Count(ctx)
if err != nil {
return false, errors.Wrap(err, "tx.NewSelect")
}
return count > 0, nil
}