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 }