simpleadmin-web 0.1.0: web panel for CS2-SimpleAdmin

This commit is contained in:
Astra 2026-09-25 20:08:14 +01:00
commit 4f38daf0e6
31 changed files with 6870 additions and 0 deletions

244
internal/web/public.go Normal file
View file

@ -0,0 +1,244 @@
package web
import (
"errors"
"net/http"
"slices"
"time"
"git.zio.sh/cs2/simpleadmin-web/internal/live"
"git.zio.sh/cs2/simpleadmin-web/internal/steam"
"git.zio.sh/cs2/simpleadmin-web/internal/store"
)
// publicPenalty is a penalty with everything about staff and IPs removed.
type publicPenalty struct {
ID int64 `json:"id"`
Kind string `json:"kind"`
Type string `json:"type,omitempty"`
Name string `json:"name"`
SteamID string `json:"steamid"`
Reason string `json:"reason"`
Duration int `json:"duration"`
Passed *int `json:"passed,omitempty"`
Ends *time.Time `json:"ends,omitempty"`
Created time.Time `json:"created"`
Status string `json:"status"`
}
func toPublic(ps []store.Penalty) []publicPenalty {
out := make([]publicPenalty, 0, len(ps))
for _, p := range ps {
out = append(out, publicPenalty{ID: p.ID, Kind: p.Kind, Type: p.Type, Name: p.Name, SteamID: p.SteamID,
Reason: p.Reason, Duration: p.Duration, Passed: p.Passed, Ends: p.Ends, Created: p.Created, Status: p.Status})
}
return out
}
type livePlayer struct {
live.Player
Rank *rankView `json:"rank,omitempty"`
// Staff-only fields, left empty on the public page.
Tags []string `json:"tags,omitempty"`
CanTarget bool `json:"canTarget,omitempty"`
}
type serverView struct {
Online bool `json:"online"`
Hostname string `json:"hostname"`
Map string `json:"map"`
Count int `json:"count"`
MaxPlayers int `json:"maxPlayers"`
ScoreT int `json:"scoreT"`
ScoreCT int `json:"scoreCt"`
Warmup bool `json:"warmup"`
StaffOnline int `json:"staffOnline"`
Address string `json:"address"`
Players []livePlayer `json:"players"`
Updated time.Time `json:"updated"`
}
func (s *Server) serverView(x *staffIndex, st live.Status) serverView {
v := serverView{Online: st.Online, Hostname: st.Hostname, Map: st.Map, MaxPlayers: st.MaxPlayers,
ScoreT: st.ScoreT, ScoreCT: st.ScoreCT, Warmup: st.Warmup, Address: s.cfg.ServerAddress,
Players: []livePlayer{}, Updated: st.Updated}
for _, p := range st.Players {
lp := livePlayer{Player: p}
if !p.Bot {
v.Count++
lp.Rank = s.rankOf(x, p.SteamID)
if s.isStaff(x, p.SteamID) {
v.StaffOnline++
}
}
v.Players = append(v.Players, lp)
}
return v
}
func (s *Server) publicServer(w http.ResponseWriter, r *http.Request) {
x, err := s.staffIdx(r.Context())
if err != nil {
internal(w, r, err)
return
}
writeJSON(w, http.StatusOK, s.serverView(x, s.poll.Current()))
}
// penaltyQuery reads ?status=&q=&page= (and for comms, a type in place of the status).
func penaltyQuery(r *http.Request, comms bool) (store.Query, int) {
lim, off, num := page(r)
q := store.Query{Search: r.URL.Query().Get("q"), Limit: lim, Offset: off}
switch st := r.URL.Query().Get("status"); st {
case store.StatusActive, store.StatusExpired, store.StatusLifted:
q.Status = st
case "GAG", "MUTE", "SILENCE":
if comms {
q.Status, q.Type = store.StatusActive, st
}
case "all":
default:
q.Status = store.StatusActive
}
return q, num
}
type penaltyPage struct {
list[publicPenalty]
Active int `json:"active"`
}
func (s *Server) publicBans(w http.ResponseWriter, r *http.Request) {
q, num := penaltyQuery(r, false)
items, total, err := s.st.Bans(r.Context(), q)
if err != nil {
internal(w, r, err)
return
}
_, active, err := s.st.Bans(r.Context(), store.Query{Status: store.StatusActive, Limit: 1})
if err != nil {
internal(w, r, err)
return
}
writeJSON(w, http.StatusOK, penaltyPage{newList(toPublic(items), total, num), active})
}
func (s *Server) publicComms(w http.ResponseWriter, r *http.Request) {
q, num := penaltyQuery(r, true)
items, total, err := s.st.Comms(r.Context(), q)
if err != nil {
internal(w, r, err)
return
}
_, active, err := s.st.Comms(r.Context(), store.Query{Status: store.StatusActive, Limit: 1})
if err != nil {
internal(w, r, err)
return
}
writeJSON(w, http.StatusOK, penaltyPage{newList(toPublic(items), total, num), active})
}
type memberView struct {
SteamID string `json:"steamid"`
Name string `json:"name"`
Online bool `json:"online"`
Ends *time.Time `json:"ends,omitempty"`
LastSeen *time.Time `json:"lastSeen,omitempty"`
}
type groupView struct {
ID int64 `json:"id"`
Name string `json:"name"`
Label string `json:"label"`
Color string `json:"color"`
About string `json:"about,omitempty"`
Supporter bool `json:"supporter"`
Immunity int `json:"immunity,omitempty"`
Flags []string `json:"flags,omitempty"`
Members []memberView `json:"members"`
}
// groupViews lists groups by immunity with their members. Staff get flags and expiry dates.
func (s *Server) groupViews(x *staffIndex, st live.Status, forStaff bool) []groupView {
out := make([]groupView, 0, len(x.groups))
for i, g := range x.groups {
r := s.site.rank(g.Name, i)
gv := groupView{ID: g.ID, Name: g.Name, Label: r.Label, Color: r.Color, About: r.About,
Supporter: r.Supporter, Immunity: g.Immunity, Members: []memberView{}}
if forStaff {
gv.Flags = g.Flags
} else {
gv.Immunity = 0
}
for _, a := range x.admins {
if x.primaryGroup(&a) != g.Name {
continue
}
_, online := st.Find(a.SteamID)
m := memberView{SteamID: a.SteamID, Name: a.Name, Online: online}
if forStaff {
m.Ends = a.Ends
}
gv.Members = append(gv.Members, m)
}
slices.SortStableFunc(gv.Members, func(a, b memberView) int {
if a.Online != b.Online {
if a.Online {
return -1
}
return 1
}
return 0
})
out = append(out, gv)
}
return out
}
func (s *Server) publicStaff(w http.ResponseWriter, r *http.Request) {
x, err := s.staffIdx(r.Context())
if err != nil {
internal(w, r, err)
return
}
st := s.poll.Current()
groups := s.groupViews(x, st, false)
// Hide empty ranks from the public.
groups = slices.DeleteFunc(groups, func(g groupView) bool { return len(g.Members) == 0 })
writeJSON(w, http.StatusOK, map[string]any{"groups": groups, "staffOnline": s.serverView(x, st).StaffOnline})
}
func (s *Server) publicPlayer(w http.ResponseWriter, r *http.Request) {
steamid := r.PathValue("steamid")
if !steam.ValidID(steamid) {
fail(w, http.StatusBadRequest, "That isn't a SteamID64.")
return
}
d, err := s.st.Player(r.Context(), steamid)
if errors.Is(err, store.ErrNotFound) {
d = store.PlayerDetail{SteamID: steamid}
} else if err != nil {
internal(w, r, err)
return
}
bans, _, err := s.st.Bans(r.Context(), store.Query{SteamID: steamid, Limit: 200})
if err != nil {
internal(w, r, err)
return
}
comms, _, err := s.st.Comms(r.Context(), store.Query{SteamID: steamid, Limit: 200})
if err != nil {
internal(w, r, err)
return
}
x, err := s.staffIdx(r.Context())
if err != nil {
internal(w, r, err)
return
}
_, online := s.poll.Current().Find(steamid)
writeJSON(w, http.StatusOK, map[string]any{
"steamid": steamid, "name": d.Name, "online": online, "rank": s.rankOf(x, steamid),
"bans": toPublic(bans), "comms": toPublic(comms),
})
}