161 lines
3.9 KiB
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
|
|
}
|