simpleadmin-web 0.1.0: web panel for CS2-SimpleAdmin
This commit is contained in:
commit
4f38daf0e6
31 changed files with 6870 additions and 0 deletions
242
internal/saconfig/saconfig.go
Normal file
242
internal/saconfig/saconfig.go
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
// Package saconfig reads and edits CS2-SimpleAdmin.json (config version 25, SimpleAdmin 1.8.2b).
|
||||
//
|
||||
// Only the options listed in Settings can be changed. Everything else in the file, including keys
|
||||
// this package doesn't know about, is written back unchanged. Missing options take SimpleAdmin's
|
||||
// defaults, since CounterStrikeSharp fills those in when it loads the file.
|
||||
package saconfig
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Setting describes one editable option under OtherSettings.
|
||||
type Setting struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Help string `json:"help"`
|
||||
Type string `json:"type"` // int, bool, select
|
||||
Options []Option `json:"options,omitempty"`
|
||||
Default any `json:"-"`
|
||||
Min int `json:"min,omitempty"`
|
||||
}
|
||||
|
||||
type Option struct {
|
||||
Value int `json:"value"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
// Settings are the OtherSettings the panel exposes, with SimpleAdmin 1.8.2b's defaults.
|
||||
var Settings = []Setting{
|
||||
{Key: "BanType", Label: "Ban by", Type: "select", Default: 1,
|
||||
Help: "What a ban matches when the banned player tries to join.",
|
||||
Options: []Option{{0, "SteamID only"}, {1, "SteamID and IP address"}}},
|
||||
{Key: "CheckMultiAccountsByIp", Label: "Catch alt accounts by IP", Type: "bool", Default: true,
|
||||
Help: "Also refuse players who share any IP address a banned player has used."},
|
||||
{Key: "ExpireOldIpBans", Label: "Stop matching IPs after", Type: "int", Default: 0,
|
||||
Help: "Days. Dynamic and shared IPs make old IP matches hit the wrong people. 0 keeps them forever."},
|
||||
{Key: "MaxBanDuration", Label: "Longest ban", Type: "int", Default: 60 * 24 * 7, Min: 1,
|
||||
Help: "Minutes. Staff without @css/permban can't ban longer than this, or permanently."},
|
||||
{Key: "MaxMuteDuration", Label: "Longest gag or mute", Type: "int", Default: 60 * 24 * 7, Min: 1,
|
||||
Help: "Minutes. Staff without @css/permmute can't block longer than this, or permanently."},
|
||||
{Key: "TimeMode", Label: "Count block time", Type: "select", Default: 1,
|
||||
Help: "Whether gags and mutes run out in real time, or only while the player is on the server.",
|
||||
Options: []Option{{1, "In real time"}, {0, "Only while online"}}},
|
||||
{Key: "ShowActivityType", Label: "Announce punishments", Type: "select", Default: 2,
|
||||
Help: "Tell players in chat when someone is banned, gagged or muted.",
|
||||
Options: []Option{{0, "Don't announce"}, {1, "Announce, admin name shown to staff only"}, {2, "Announce with admin name"}, {3, "Announce to staff only"}}},
|
||||
{Key: "DisableDangerousCommands", Label: "Block commands on several players", Type: "bool", Default: true,
|
||||
Help: "Stops in-game commands from hitting more than one player at once, such as @all."},
|
||||
{Key: "ReloadAdminsEveryMapChange", Label: "Reload admins on map change", Type: "bool", Default: false,
|
||||
Help: "Re-read staff and ranks from the database at every map change."},
|
||||
{Key: "NotifyPenaltiesToAdminOnConnect", Label: "Tell staff about joining players' records", Type: "bool", Default: true,
|
||||
Help: "When a player with bans or blocks on record joins, list them to staff in chat."},
|
||||
}
|
||||
|
||||
// Database is the DatabaseConfig block.
|
||||
type Database struct {
|
||||
Type string
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
Name string
|
||||
SSLMode string
|
||||
}
|
||||
|
||||
type File struct {
|
||||
path string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func Open(path string) *File { return &File{path: path} }
|
||||
|
||||
func (f *File) Path() string { return f.path }
|
||||
|
||||
func (f *File) load() (map[string]any, error) {
|
||||
data, err := os.ReadFile(f.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.UseNumber()
|
||||
var m map[string]any
|
||||
if err := dec.Decode(&m); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", f.path, err)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func section(m map[string]any, key string) map[string]any {
|
||||
if v, ok := m[key].(map[string]any); ok {
|
||||
return v
|
||||
}
|
||||
return map[string]any{}
|
||||
}
|
||||
|
||||
func str(m map[string]any, key, def string) string {
|
||||
if v, ok := m[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func num(m map[string]any, key string, def int) int {
|
||||
if v, ok := m[key].(json.Number); ok {
|
||||
if n, err := v.Int64(); err == nil {
|
||||
return int(n)
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func boolean(m map[string]any, key string, def bool) bool {
|
||||
if v, ok := m[key].(bool); ok {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// Database returns the database connection settings.
|
||||
func (f *File) Database() (Database, error) {
|
||||
m, err := f.load()
|
||||
if err != nil {
|
||||
return Database{}, err
|
||||
}
|
||||
d := section(m, "DatabaseConfig")
|
||||
return Database{
|
||||
Type: str(d, "DatabaseType", "SQLite"),
|
||||
Host: str(d, "DatabaseHost", ""),
|
||||
Port: num(d, "DatabasePort", 3306),
|
||||
User: str(d, "DatabaseUser", ""),
|
||||
Password: str(d, "DatabasePassword", ""),
|
||||
Name: str(d, "DatabaseName", ""),
|
||||
SSLMode: str(d, "DatabaseSSlMode", "preferred"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// General holds top-level options the panel needs to behave like SimpleAdmin.
|
||||
type General struct {
|
||||
Timezone string
|
||||
MultiServerMode bool
|
||||
}
|
||||
|
||||
func (f *File) General() (General, error) {
|
||||
m, err := f.load()
|
||||
if err != nil {
|
||||
return General{}, err
|
||||
}
|
||||
return General{
|
||||
Timezone: str(m, "Timezone", "UTC"),
|
||||
MultiServerMode: boolean(m, "MultiServerMode", true),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Values returns the current value of every editable setting.
|
||||
func (f *File) Values() (map[string]any, error) {
|
||||
m, err := f.load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o := section(m, "OtherSettings")
|
||||
out := map[string]any{}
|
||||
for _, s := range Settings {
|
||||
switch def := s.Default.(type) {
|
||||
case bool:
|
||||
out[s.Key] = boolean(o, s.Key, def)
|
||||
case int:
|
||||
out[s.Key] = num(o, s.Key, def)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Update validates and writes changed settings. It keeps a copy of the previous file as .bak.
|
||||
func (f *File) Update(changes map[string]any) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
m, err := f.load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o := section(m, "OtherSettings")
|
||||
for key, raw := range changes {
|
||||
var def *Setting
|
||||
for i := range Settings {
|
||||
if Settings[i].Key == key {
|
||||
def = &Settings[i]
|
||||
}
|
||||
}
|
||||
if def == nil {
|
||||
return fmt.Errorf("%s can't be changed here.", key)
|
||||
}
|
||||
switch def.Type {
|
||||
case "bool":
|
||||
v, ok := raw.(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s must be on or off.", def.Label)
|
||||
}
|
||||
o[key] = v
|
||||
default:
|
||||
fv, ok := raw.(float64)
|
||||
if !ok || fv != float64(int(fv)) {
|
||||
return fmt.Errorf("%s must be a whole number.", def.Label)
|
||||
}
|
||||
v := int(fv)
|
||||
if v < def.Min {
|
||||
return fmt.Errorf("%s must be at least %d.", def.Label, def.Min)
|
||||
}
|
||||
if def.Type == "select" {
|
||||
valid := false
|
||||
for _, opt := range def.Options {
|
||||
valid = valid || opt.Value == v
|
||||
}
|
||||
if !valid {
|
||||
return fmt.Errorf("%s has no option %d.", def.Label, v)
|
||||
}
|
||||
}
|
||||
o[key] = v
|
||||
}
|
||||
}
|
||||
m["OtherSettings"] = o
|
||||
|
||||
old, err := os.ReadFile(f.path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
enc := json.NewEncoder(&buf)
|
||||
enc.SetIndent("", " ")
|
||||
enc.SetEscapeHTML(false)
|
||||
if err := enc.Encode(m); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(f.path+".bak", old, 0o600); err != nil {
|
||||
return fmt.Errorf("back up %s: %w", f.path, err)
|
||||
}
|
||||
// Write in place rather than rename, so the file keeps its owner (the game server's user).
|
||||
return os.WriteFile(f.path, buf.Bytes(), 0o644)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue