simpleadmin-web/internal/web/staff.go

161 lines
3.9 KiB
Go

package web
import (
"context"
"slices"
"sync"
"time"
"git.zio.sh/cs2/simpleadmin-web/internal/store"
)
// staffIndex is SimpleAdmin's admins and groups for this server, merged the way CounterStrikeSharp's
// AdminManager sees them: an admin's permissions are their own @flags plus their groups' flags, and
// their immunity is the highest of their own and their groups'.
type staffIndex struct {
groups []store.Group
byName map[string]*store.Group
admins []store.Admin
bySteam map[string]*store.Admin
}
type staffCache struct {
st *store.Store
ttl time.Duration
mu sync.Mutex
idx *staffIndex
fetched time.Time
}
func (c *staffCache) get(ctx context.Context) (*staffIndex, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.idx != nil && time.Since(c.fetched) < c.ttl {
return c.idx, nil
}
groups, err := c.st.Groups(ctx)
if err != nil {
return nil, err
}
admins, err := c.st.Admins(ctx)
if err != nil {
return nil, err
}
idx := &staffIndex{groups: groups, admins: admins, byName: map[string]*store.Group{}, bySteam: map[string]*store.Admin{}}
for i := range idx.groups {
idx.byName[idx.groups[i].Name] = &idx.groups[i]
}
for i := range idx.admins {
idx.bySteam[idx.admins[i].SteamID] = &idx.admins[i]
}
c.idx, c.fetched = idx, time.Now()
return idx, nil
}
func (c *staffCache) invalidate() {
c.mu.Lock()
c.idx = nil
c.mu.Unlock()
}
// flags returns an admin's effective permissions.
func (x *staffIndex) flags(a *store.Admin) []string {
out := slices.Clone(a.Flags)
for _, g := range a.Groups {
if grp, ok := x.byName[g]; ok {
for _, f := range grp.Flags {
if !slices.Contains(out, f) {
out = append(out, f)
}
}
}
}
return out
}
// immunity returns a player's effective immunity, 0 for non-staff.
func (x *staffIndex) immunity(steamid string) int {
a, ok := x.bySteam[steamid]
if !ok {
return 0
}
imm := a.Immunity
for _, g := range a.Groups {
if grp, ok := x.byName[g]; ok {
imm = max(imm, grp.Immunity)
}
}
return imm
}
// primaryGroup is the admin's highest-immunity group that exists, or "".
func (x *staffIndex) primaryGroup(a *store.Admin) string {
best, bestImm := "", -1
for _, g := range a.Groups {
if grp, ok := x.byName[g]; ok && grp.Immunity > bestImm {
best, bestImm = g, grp.Immunity
}
}
return best
}
// groupIndex is a group's position, used to pick a stable fallback colour.
func (x *staffIndex) groupIndex(name string) int {
for i, g := range x.groups {
if g.Name == name {
return i
}
}
return 0
}
// Identity is a signed-in staff member.
type Identity struct {
SteamID string
Name string
Flags []string
Immunity int
RowID int64
}
// Has reports whether the identity holds a permission. @css/root grants everything.
func (id *Identity) Has(flag string) bool {
return slices.Contains(id.Flags, "@css/root") || slices.Contains(id.Flags, flag)
}
// CanTarget follows CounterStrikeSharp: you can act on players whose immunity isn't above yours.
// Nobody acts on themselves through the panel.
func (id *Identity) CanTarget(x *staffIndex, steamid string) bool {
if steamid == id.SteamID {
return false
}
return x.immunity(steamid) <= id.Immunity
}
// rankView is how a rank is shown to the browser.
type rankView struct {
Name string `json:"name"`
Label string `json:"label"`
Color string `json:"color"`
Supporter bool `json:"supporter,omitempty"`
}
func (s *Server) rankOf(x *staffIndex, steamid string) *rankView {
a, ok := x.bySteam[steamid]
if !ok {
return nil
}
g := x.primaryGroup(a)
if g == "" {
return &rankView{Label: "Staff", Color: "#8a91a0"}
}
r := s.site.rank(g, x.groupIndex(g))
return &rankView{Name: g, Label: r.Label, Color: r.Color, Supporter: r.Supporter}
}
// isStaff is true for admins whose rank isn't a supporter rank.
func (s *Server) isStaff(x *staffIndex, steamid string) bool {
r := s.rankOf(x, steamid)
return r != nil && !r.Supporter
}