package db import ( "context" "database/sql" "fmt" "time" "git.haelnorr.com/h/golib/hwsauth" "git.haelnorr.com/h/oslstats/internal/permissions" "git.haelnorr.com/h/oslstats/internal/roles" "github.com/bwmarrin/discordgo" "github.com/pkg/errors" "github.com/uptrace/bun" ) var CurrentUser hwsauth.ContextLoader[*User] type User struct { bun.BaseModel `bun:"table:users,alias:u"` ID int `bun:"id,pk,autoincrement"` // Integer ID (index primary key) Username string `bun:"username,unique"` // Username (unique) CreatedAt int64 `bun:"created_at"` // Epoch timestamp when the user was added to the database DiscordID string `bun:"discord_id,unique"` } type Users struct { Users []*User Total int PageOpts PageOpts } func (u *User) GetID() int { return u.ID } // CreateUser creates a new user with the given username and password func CreateUser(ctx context.Context, tx bun.Tx, username string, discorduser *discordgo.User) (*User, error) { if discorduser == nil { return nil, errors.New("user cannot be nil") } user := &User{ Username: username, CreatedAt: time.Now().Unix(), DiscordID: discorduser.ID, } _, err := tx.NewInsert(). Model(user). Exec(ctx) if err != nil { return nil, errors.Wrap(err, "tx.NewInsert") } return user, nil } // GetUserByID queries the database for a user matching the given ID // Returns nil, nil if no user is found func GetUserByID(ctx context.Context, tx bun.Tx, id int) (*User, error) { fmt.Printf("user id requested: %v", id) user := new(User) err := tx.NewSelect(). Model(user). Where("id = ?", id). Limit(1). Scan(ctx) if err != nil { if err.Error() == "sql: no rows in result set" { return nil, nil } return nil, errors.Wrap(err, "tx.NewSelect") } return user, nil } // GetUserByUsername queries the database for a user matching the given username // Returns nil, nil if no user is found func GetUserByUsername(ctx context.Context, tx bun.Tx, username string) (*User, error) { user := new(User) err := tx.NewSelect(). Model(user). Where("username = ?", username). Limit(1). Scan(ctx) if err != nil { if err.Error() == "sql: no rows in result set" { return nil, nil } return nil, errors.Wrap(err, "tx.Select") } return user, nil } // GetUserByDiscordID queries the database for a user matching the given discord id // Returns nil, nil if no user is found func GetUserByDiscordID(ctx context.Context, tx bun.Tx, discordID string) (*User, error) { user := new(User) err := tx.NewSelect(). Model(user). Where("discord_id = ?", discordID). Limit(1). Scan(ctx) if err != nil { if err.Error() == "sql: no rows in result set" { return nil, nil } return nil, errors.Wrap(err, "tx.NewSelect") } return user, nil } // IsUsernameUnique checks if the given username is unique (not already taken) // Returns true if the username is available, false if it's taken func IsUsernameUnique(ctx context.Context, tx bun.Tx, username string) (bool, error) { count, err := tx.NewSelect(). Model((*User)(nil)). Where("username = ?", username). Count(ctx) if err != nil { return false, errors.Wrap(err, "tx.NewSelect") } return count == 0, nil } // GetRoles loads and returns all roles for this user func (u *User) GetRoles(ctx context.Context, tx bun.Tx) ([]*Role, error) { if u == nil { return nil, errors.New("user cannot be nil") } return GetUserRoles(ctx, tx, u.ID) } // GetPermissions loads and returns all permissions for this user (via roles) func (u *User) GetPermissions(ctx context.Context, tx bun.Tx) ([]*Permission, error) { if u == nil { return nil, errors.New("user cannot be nil") } // TODO: use proper m2m tables and relations instead of join var permissions []*Permission err := tx.NewSelect(). Model(&permissions). Join("JOIN role_permissions AS rp ON rp.permission_id = p.id"). Join("JOIN user_roles AS ur ON ur.role_id = rp.role_id"). Where("ur.user_id = ?", u.ID). Where("ur.expires_at IS NULL OR ur.expires_at > ?", time.Now().Unix()). Scan(ctx) if err != nil && err != sql.ErrNoRows { return nil, errors.Wrap(err, "tx.NewSelect") } return permissions, nil } // HasPermission checks if user has a specific permission (including wildcard check) func (u *User) HasPermission(ctx context.Context, tx bun.Tx, permissionName permissions.Permission) (bool, error) { if u == nil { return false, errors.New("user cannot be nil") } if permissionName == "" { return false, errors.New("permissionName cannot be empty") } perms, err := u.GetPermissions(ctx, tx) if err != nil { return false, err } for _, p := range perms { if p.Name == permissionName || p.Name == permissions.Wildcard { return true, nil } } return false, nil } // HasRole checks if user has a specific role func (u *User) HasRole(ctx context.Context, tx bun.Tx, roleName roles.Role) (bool, error) { if u == nil { return false, errors.New("user cannot be nil") } return HasRole(ctx, tx, u.ID, roleName) } // IsAdmin is a convenience method to check if user has admin role func (u *User) IsAdmin(ctx context.Context, tx bun.Tx) (bool, error) { if u == nil { return false, errors.New("user cannot be nil") } return u.HasRole(ctx, tx, "admin") } func GetUsers(ctx context.Context, tx bun.Tx, pageOpts *PageOpts) (*Users, error) { if pageOpts == nil { pageOpts = &PageOpts{} } if pageOpts.Page == 0 { pageOpts.Page = 1 } if pageOpts.PerPage == 0 { pageOpts.PerPage = 50 } if pageOpts.Order == "" { pageOpts.Order = bun.OrderAsc } if pageOpts.OrderBy == "" { pageOpts.OrderBy = "id" } users := []*User{} err := tx.NewSelect(). Model(users). OrderBy(pageOpts.OrderBy, pageOpts.Order). Limit(pageOpts.PerPage). Offset(pageOpts.PerPage * (pageOpts.Page - 1)). Scan(ctx) if err != nil && err != sql.ErrNoRows { return nil, errors.Wrap(err, "tx.NewSelect") } total, err := tx.NewSelect(). Model(users). Count(ctx) if err != nil { return nil, errors.Wrap(err, "tx.NewSelect") } list := &Users{ Users: users, Total: total, PageOpts: *pageOpts, } return list, nil }