The public scoreboard and player pages no longer show or receive whether a player is verified. The staff panel still does.
307 lines
18 KiB
Markdown
307 lines
18 KiB
Markdown
# simpleadmin-web
|
|
|
|
A SourceBans-style web panel for [CS2-SimpleAdmin](https://github.com/daffyyyy/CS2-SimpleAdmin)
|
|
(built against 1.8.2b's MySQL schema).
|
|
|
|
- **Public site** (`/`): the live server (name, map, players, team score and a scoreboard), bans,
|
|
gags and mutes, the staff list and its rank changes (promotions, demotions, who made them).
|
|
Read-only. It never shows who issued a penalty, IP addresses or internal immunity values.
|
|
- **Staff panel** (`/admin`): sign in through Steam. Ban, gag, mute, silence, warn and kick; lift
|
|
bans and blocks; manage ranks, permissions and staff; change server cvars and SimpleAdmin's
|
|
punishment settings.
|
|
- **Notifications** (staff): push notifications for new and lifted bans, gags, mutes and silences,
|
|
and the game server going down, on phones and computers, with quiet hours (see below).
|
|
- **Verification** (optional): who our GroupVerify plugin verifies, with overrides to give and
|
|
remove, and its settings. Only appears when GroupVerify is installed (see below).
|
|
- **Playtime** (optional): who plays the most, when the server is busy, and each player's time,
|
|
stats and maps, from our PlaytimeHistory plugin's database. Staff also see who's on with their
|
|
location, staff activity, disconnect reasons and players' name and session history. Only appears
|
|
when `SAW_PT_DSN` is set (see below).
|
|
|
|
Live data comes from the **WebPanelBridge** plugin (`plugins/WebPanelBridge` in this repo) over
|
|
RCON. Without it the panel still works, but shows the server as offline and can't kick or warn.
|
|
|
|
## How it works
|
|
|
|
- **Reading**: straight from SimpleAdmin's MySQL tables (`sa_bans`, `sa_mutes`, `sa_warns`,
|
|
`sa_admins`, `sa_groups` and their flag, unban and unmute tables, `sa_players_ips`).
|
|
- **Penalties**: sent over RCON as SimpleAdmin's own commands (`css_addban`, `css_addgag`,
|
|
`css_unban`, `css_warn`, …), so they take effect in game at once, with SimpleAdmin's usual
|
|
announcements, Discord messages and IP handling. SimpleAdmin records console commands as
|
|
"Console", so the panel then rewrites that row to credit the staff member who did it.
|
|
- **Game server unreachable**: bans and blocks are written to the database directly, the way
|
|
SimpleAdmin writes them. They apply when the player next joins (bans are also picked up by
|
|
SimpleAdmin within about a minute). Kicks and warnings need the server.
|
|
- **Ranks and staff**: written to the database, then `css_reloadadmins`. A rank is a SimpleAdmin
|
|
group (`#rank/mod`); staff are admins holding exactly one such group. The rank editor offers the
|
|
`@css/...` flags and our `@zio/...` ones (from the server's `admin_overrides.json` and our
|
|
SimpleAdmin fork); a new `@zio/` flag needs adding to `knownFlags` in `internal/web/admin.go`.
|
|
- **Permissions**: commands sent over RCON run as Console, which skips all of SimpleAdmin's own
|
|
checks, so the panel enforces them itself with the same flags:
|
|
|
|
| Action | Needs |
|
|
|---|---|
|
|
| Use the panel | `@css/generic` |
|
|
| Ban | `@css/ban`; permanent or longer than `MaxBanDuration` also `@css/permban` |
|
|
| Unban | `@css/unban` |
|
|
| Gag, mute, silence, lift them | `@css/chat`; permanent or longer than `MaxMuteDuration` also `@css/permmute` |
|
|
| Kick, warn | `@css/kick` |
|
|
| See IP addresses | `@css/showip` |
|
|
| Change map | `@css/changemap` |
|
|
| Server cvars | `@css/cvar` |
|
|
| Raw console commands (Server settings > Console; sent without waiting, output only logged) | `@css/rcon` |
|
|
| See the Staff & ranks page | `@zio/webpanel` |
|
|
| Give and remove verification overrides | `@zio/verify` (not on themselves, unless Root) |
|
|
| Staff playtime figures (Playtime page, player drawers, and on the public site when signed in) | `@css/generic` |
|
|
| PlaytimeHistory's own status on the Playtime page | `@css/root` |
|
|
| Ranks, staff, SimpleAdmin settings, reload admins, GroupVerify's group and on/off | `@css/root` |
|
|
|
|
`@css/root` grants everything. Nobody can act on a player whose immunity is higher than theirs,
|
|
or on themselves, or give a rank with more immunity than their own.
|
|
|
|
## Setup
|
|
|
|
1. Install WebPanelBridge on the game server (see its README).
|
|
2. Make sure RCON works: `rcon_password` set, and the panel can reach the game port over TCP.
|
|
SimpleAdmin stores the server's address and RCON password in `sa_servers`, and the panel uses
|
|
those unless `SAW_RCON_ADDR` / `SAW_RCON_PASSWORD` are set.
|
|
3. Copy `example.env` to `simpleadmin-web.env` and fill it in. `SAW_BASE_URL` must be the exact
|
|
public address; Steam returns signed-in users there.
|
|
4. Put the panel behind HTTPS (a reverse proxy). Session cookies are marked Secure when
|
|
`SAW_BASE_URL` is https.
|
|
|
|
Run it with podman:
|
|
|
|
```
|
|
podman build -t simpleadmin-web .
|
|
podman run -d --name simpleadmin-web --env-file simpleadmin-web.env \
|
|
--user 1000:1000 \
|
|
-v /srv/cs2/game/csgo/addons/counterstrikesharp/configs/plugins/CS2-SimpleAdmin:/data:Z \
|
|
-p 127.0.0.1:8080:8080 simpleadmin-web
|
|
```
|
|
|
|
`--user 1000:1000` matches the game container's user, which owns `CS2-SimpleAdmin.json`, so the
|
|
panel can edit it. Mount it with `:ro` instead to make those settings read-only. Changes to that
|
|
file apply the next time SimpleAdmin loads (a server restart). The panel keeps the previous
|
|
version as `CS2-SimpleAdmin.json.bak`. It writes keys in alphabetical order, so expect a reordered
|
|
file after the first save.
|
|
|
|
Or build and run it directly: `go build && ./simpleadmin-web` (Go 1.25, settings from the
|
|
environment).
|
|
|
|
### Rank names and colours
|
|
|
|
SimpleAdmin groups have no display name or colour, so those come from a site config
|
|
(`SAW_SITE_CONFIG`, see `site.example.json`). The built-in defaults cover `#rank/owner`,
|
|
`senioradmin`, `admin`, `trialadmin`, `mod`, `trialmod`, `helper` and `guardian`. Ranks marked
|
|
`"supporter": true` (VIPs) are listed apart from staff and don't count as staff online. Ranks
|
|
marked `"inactive": true` (Guardian: staff who are stepping back) are listed with the staff; they
|
|
only change how the rank log reads (see below). Nobody can sign in without `@css/generic`.
|
|
|
|
### Rank changes
|
|
|
|
Every change to a player's rank (their highest-immunity group) is recorded in `sa_rank_log` and
|
|
listed under the public Staff page: who, from which rank to which, who changed it and when. A
|
|
change is a promotion or demotion by the ranks' immunity, except for inactive ranks: moving
|
|
between an inactive rank and any other rank is a promotion either way, and losing an inactive rank
|
|
is a demotion. The
|
|
panel creates the table at startup (`sql/rank-log.sql`, embedded; run it by hand if the database
|
|
user can't create tables). It logs changes made in the panel (giving, moving or removing a rank,
|
|
and deleting a rank that people hold). Our SimpleAdmin fork (1.8.2b-zio10+) logs `css_addadmin`,
|
|
`css_deladmin` and the in-game admin menu to the same table, marked "in game". Not logged: ranks
|
|
that lapse when their expiry passes, `css_delgroup`, and edits made straight in the database.
|
|
History starts when the table is created.
|
|
|
|
### Verification (GroupVerify, optional)
|
|
|
|
[GroupVerify](https://git.zio.sh/cs2/GroupVerify) (`plugins/GroupVerify` in the cs2 repo) verifies
|
|
players who are in the server's Steam group or have a manual override, and other plugins give
|
|
verified players perks. It keeps everything in SimpleAdmin's database (`gv_verified`,
|
|
`gv_settings`), so the panel needs no setup for it:
|
|
|
|
- **Staff** get a Verification page as soon as GroupVerify's tables are in the database: whether
|
|
it's on, the plugin's state and version, the Steam group, its saved member list, and the
|
|
overrides. The player drawer shows why a player is or isn't verified. With `@zio/verify` (give
|
|
it to the ranks that should have it in the rank editor), staff give overrides for a day up to a
|
|
year or with no expiry, change them and remove them. Root sets the group and turns verification
|
|
on and off.
|
|
- **The public site** shows nothing about verification. On the staff panel, the Overview
|
|
scoreboard shows a "Verified" tag only while verification is on (enabled, with a group set) and
|
|
GroupVerify isn't missing from the server's plugin list.
|
|
- **Clan tag**: Root can set the scoreboard tag verified players without a rank get (GroupVerify's
|
|
`clan_tag`, at most 12 characters). CS2-Tags (1.0.4c-zio6+) applies it, and gives unverified
|
|
players without a rank no tag at all. Blank turns it off.
|
|
- **Whether it's loaded** comes from `css_plugins list` over RCON, read once a minute alongside
|
|
the live status. While the server is down it's unknown, and that doesn't hide anything.
|
|
- **Changes**: overrides are written to `gv_verified` and GroupVerify picks them up on its next
|
|
reload (`db_reload_seconds`, 60 by default). Settings are written to `gv_settings` and, when
|
|
the plugin is loaded, also sent as its own `css_gv_group`/`css_gv_enable`/`css_gv_disable`, so
|
|
they apply at once.
|
|
- **Times** in its tables are in GroupVerify's own `timezone` setting, which the panel follows.
|
|
Keep it the same as SimpleAdmin's `Timezone`.
|
|
|
|
GroupVerify (0.3.0+) checks each player with the Steam Web API as they join, so "Confirmed group
|
|
members" is everyone Steam has said is in the group when they joined, not the group's full member
|
|
list. The panel never asks Steam itself.
|
|
|
|
### Playtime (PlaytimeHistory, optional)
|
|
|
|
[PlaytimeHistory](https://git.zio.sh/cs2/PlaytimeHistory) (`plugins/PlaytimeHistory` in the cs2
|
|
repo) records sessions, team and alive/dead time, maps, stats, ping and location in **its own
|
|
database**, not SimpleAdmin's. To show it, give the panel a second connection, ideally as a user
|
|
that can only read:
|
|
|
|
```sql
|
|
CREATE USER 'pt_web'@'<panel host>' IDENTIFIED BY '<password>';
|
|
GRANT SELECT ON playtime_history.* TO 'pt_web'@'<panel host>';
|
|
```
|
|
|
|
```
|
|
SAW_PT_DSN=pt_web:<password>@tcp(<db-host>:3306)/playtime_history
|
|
```
|
|
|
|
- **Nothing shows** without `SAW_PT_DSN`, or while that database is down or doesn't have
|
|
PlaytimeHistory's tables. The panel checks once a minute and logs when that changes; it starts
|
|
and runs normally either way.
|
|
- **Public site**: a Playtime tab (once something's recorded) with the top players for the last 7
|
|
days, 30 days or all time (time on T or CT; spectating doesn't count), the last 7 days against
|
|
the 7 before, the busiest hours, players per day, a day-by-hour heatmap and maps. With
|
|
PlaytimeHistory 0.2.0+ (its `pt_events` table), also Records: the most bomb plants, defuses,
|
|
knife kills, Zeus kills and headshot kills for the same periods, counted as they happen, never in
|
|
warmup, and not counting kills of bots. Player pages get time played with the T/CT/spectating
|
|
split, the last 30 days, K/D, headshots, damage per round, MVPs, their record counts and
|
|
favourite maps.
|
|
- **Staff** with `@css/generic` (PlaytimeHistory's own default for looking up others) get, on the
|
|
staff panel's Playtime page and on the public site while signed in: who's on now with how long,
|
|
team, ping and location; each staff member's time in the last 7 or 30 days; disconnect reasons;
|
|
countries; and in player drawers, the names they've used, recent sessions, location and ping.
|
|
Root also sees whether the plugin is loaded and what `css_pt_status` says. Who's on comes from
|
|
the live status, so admins hidden with `css_hide` stay hidden.
|
|
- **Days and hours of the day** are in the viewer's own time zone (the browser sends it). An hour
|
|
goes to the day it starts on, which is exact for zones a whole number of hours from UTC.
|
|
- **Locations** come from DB-IP City Lite, credited (CC BY 4.0) wherever they're shown. No IP
|
|
addresses are stored.
|
|
- `SAW_PT_SERVER_ID` limits it to one `pt_servers.id` when several servers share the database.
|
|
All-time totals (pt_players) always cover every server.
|
|
|
|
### Name history (optional)
|
|
|
|
SimpleAdmin keeps one row per SteamID + IP in `sa_players_ips` and overwrites its name on each
|
|
connect, so a rename on the same IP loses the old name. `sql/players-ips-name-history.sql` adds
|
|
`name` to that table's primary key, so every name a SteamID has joined with is kept and shows up
|
|
on its player page and in name search. SimpleAdmin needs no changes. Run it once against the
|
|
SimpleAdmin database; the file has the revert. Names are still only recorded at connect.
|
|
|
|
### Installing as an app
|
|
|
|
The site is a PWA, so Chrome on Android (and desktop Chrome or Edge) offers to install it: menu >
|
|
**Add to Home screen** / **Install app**. The public site and the staff panel install as separate
|
|
apps (`/manifest.webmanifest` opens on `/`, `/admin.webmanifest` on `/admin`); install the staff one
|
|
from the staff panel or its sign-in page. Installing needs the site on HTTPS. The service worker
|
|
(`assets/sw.js`, served at `/sw.js`) caches only an offline page, never data.
|
|
|
|
The icons are rendered from `assets/icon.svg` and `assets/icon-maskable.svg` (the brand mark kept
|
|
inside the maskable safe zone). After changing either:
|
|
|
|
```
|
|
cd internal/web/assets
|
|
rsvg-convert -w 192 -h 192 icon.svg -o icon-192.png
|
|
rsvg-convert -w 512 -h 512 icon.svg -o icon-512.png
|
|
rsvg-convert -w 512 -h 512 icon-maskable.svg -o icon-maskable-512.png
|
|
rsvg-convert -w 180 -h 180 icon-maskable.svg -o apple-touch-icon.png
|
|
```
|
|
|
|
### Notifications
|
|
|
|
Staff turn notifications on per device from the panel's **Notifications** page, and pick which
|
|
types they want and their quiet hours there; those apply to all their devices. It's standard Web
|
|
Push: it works in Chrome, Edge, Firefox and Safari (on iPhone only from a home-screen install), and
|
|
in the Android app, where they show as the app's own.
|
|
|
|
- **Types**: ban added or lifted, and the same for gags, mutes and silences, each on its own
|
|
switch; and the game server down (no RCON answer for 30 seconds) and back up. "Live score" and
|
|
"Admin called" are listed but switched off until they're built (admin calls need a plugin).
|
|
- **What's watched**: SimpleAdmin's `sa_bans`, `sa_mutes`, `sa_unbans` and `sa_unmutes`, every 5
|
|
seconds, so penalties made in game are sent too, not just ones from the panel. New rows wait 7
|
|
seconds first, so panel actions are credited to the staff member rather than "Console".
|
|
- **Who gets them**: staff who can use the panel (`@css/generic`), checked on every send, so people
|
|
removed from staff stop getting them at once. Nobody is told about their own actions.
|
|
- **Links**: tapping a penalty notification, or its **View player** button, opens the public ban or
|
|
gag/mute list searching for the player (`/#bans?q=<steamid64>`, `/#comms?q=...`).
|
|
- **Setup**: none. The panel creates `saw_push_devices`, `saw_push_prefs` and `saw_settings` at
|
|
startup (`sql/push.sql`), and makes its VAPID key on first start, kept in `saw_settings`. Deleting
|
|
that key stops every device until it's turned on again. Messages are encrypted for each device
|
|
(RFC 8291) and only sent to the browsers' own push services (Google, Mozilla, Apple, Microsoft).
|
|
|
|
### Android app
|
|
|
|
`android/` is the site as an Android app: a Trusted Web Activity, which opens the public site in
|
|
the phone's Chrome, full screen, with no code of its own (Google's `androidbrowserhelper` does it
|
|
all). Build it with podman:
|
|
|
|
```
|
|
android/build.sh
|
|
```
|
|
|
|
That builds an image with JDK 17, the Android SDK and Gradle, and writes a signed
|
|
`android/out/simpleadmin-web.apk`. The signing key is kept outside the repo, in
|
|
`~/.config/simpleadmin-web/android/` (`release.jks` and its `password`), and made on the first run.
|
|
Back it up: an APK signed with another key won't install over the old one. Then:
|
|
|
|
- Set `SAW_ANDROID_PACKAGE` and `SAW_ANDROID_CERT_SHA256` (the fingerprint `build.sh` prints) so
|
|
the panel serves `/.well-known/assetlinks.json`. Without it, Chrome shows its URL bar over the app.
|
|
- Bump `versionCode` in `android/app/build.gradle` for every APK you hand out.
|
|
- The site and start page are at the top of `android/app/build.gradle`; never change `applicationId`.
|
|
|
|
The app needs Chrome (or another browser with TWA support) on the phone. Notifications from the
|
|
panel are delegated to the app (`DelegationService`), so they show under its name and icon, and
|
|
Android 13+ asks for the app's notification permission when they're turned on.
|
|
|
|
## Limitations and caveats
|
|
|
|
- **Tested against a real MySQL 8.4** with SimpleAdmin 1.8.2b's migrations applied, and a fake game
|
|
server that speaks Source RCON and imitates SimpleAdmin's console commands. **Not yet tested
|
|
against a live CS2 server or live Steam sign-in.** Things to check on first run: that RCON
|
|
returns WebPanelBridge's output (it prints through `Server.PrintToConsole`, like `css version`),
|
|
and that the cvar values on the settings page read correctly.
|
|
- In-game announcements for panel actions name "Console", because that's who SimpleAdmin sees.
|
|
The database records the real staff member.
|
|
- Unban and lift act on the player: `css_unban <steamid>` lifts every active ban on that SteamID,
|
|
as it does in game.
|
|
- Bans without a SteamID (IP-only bans from `css_banip`) are listed but can only be lifted in game.
|
|
- One server per panel. Set `SAW_SERVER_ID` if `sa_servers` lists several.
|
|
- Times are read in SimpleAdmin's `Timezone` (UTC by default), matching how it writes them.
|
|
|
|
## Development
|
|
|
|
```
|
|
go vet ./... && go test ./...
|
|
```
|
|
|
|
`internal/web/verify_db_test.go` also runs the Verification endpoints against a real MySQL when
|
|
`SAW_TEST_DSN` is set. It empties SimpleAdmin's admin and group tables and GroupVerify's, so give
|
|
it a scratch database holding SimpleAdmin 1.8.2b's migrations and GroupVerify's
|
|
`Migrations/001_CreateTables.sql`:
|
|
|
|
```
|
|
podman run -d --rm --name sa-test -e MYSQL_ROOT_PASSWORD=test -e MYSQL_DATABASE=sa -p 127.0.0.1:33061:3306 mysql:8.4
|
|
# apply the migrations, then:
|
|
SAW_TEST_DSN='root:test@tcp(127.0.0.1:33061)/sa?parseTime=true&loc=UTC' go test ./internal/web -run VerifyDB
|
|
```
|
|
|
|
`internal/playtime/playtime_db_test.go` runs the playtime queries against a real MySQL or MariaDB
|
|
when `SAW_TEST_PT_DSN` is set. It drops and recreates PlaytimeHistory's tables from
|
|
`internal/playtime/testdata/schema.sql` (a copy of the plugin's migration; keep it in step), so
|
|
give it a scratch database:
|
|
|
|
```
|
|
podman run -d --rm --name pt-test -e MARIADB_ROOT_PASSWORD=test -e MARIADB_DATABASE=playtime_history -p 127.0.0.1:33071:3306 mariadb:11
|
|
SAW_TEST_PT_DSN='root:test@tcp(127.0.0.1:33071)/playtime_history' go test ./internal/playtime -run DB
|
|
```
|
|
|
|
`internal/web/notify_db_test.go` does the same for notifications (`-run NotifyDB`): the endpoints,
|
|
and the watcher sending to a fake push service. It needs only SimpleAdmin's migrations, and empties
|
|
its ban, mute, admin and group tables and the panel's push tables.
|
|
|
|
The pages are plain HTML, CSS and JS in `internal/web/assets`, embedded into the binary. The
|
|
design mockup they came from is in `docs/mockup` at the repo root.
|