Compare commits
15 Commits
feature/le
...
feature/ac
| Author | SHA1 | Date | |
|---|---|---|---|
| 14aa8a84e5 | |||
| e96b8bca12 | |||
| 85c5874bc7 | |||
| 4441524447 | |||
| c03234d339 | |||
| d4b16d80f3 | |||
| 9172f0256f | |||
| beb9b479df | |||
| 4aa67db3fd | |||
| 747adc77a5 | |||
| c9a32e8889 | |||
| 7d470f3638 | |||
| cf7473b373 | |||
| 35c53cc54c | |||
| 016824e846 |
29
.env.example
Normal file → Executable file
29
.env.example
Normal file → Executable file
@@ -50,6 +50,35 @@ PASSWORD=change-this-default-admin-password
|
|||||||
|
|
||||||
# 4. NEVER commit actual secrets to version control!
|
# 4. NEVER commit actual secrets to version control!
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# PLATFORM SELECTION
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Platform mode: "windows" (default) or "docker"
|
||||||
|
# PLATFORM=windows
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# DOCKER MODE SETTINGS (required when PLATFORM=docker)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Docker image name for ACC server containers (built from docker/acc-server/Dockerfile)
|
||||||
|
# ACC_IMAGE=acc-wine:latest
|
||||||
|
|
||||||
|
# Base path where per-server game installations are stored (inside the manager container).
|
||||||
|
# Each server gets its own subdirectory: ACC_SERVERS_PATH/<service-name>/
|
||||||
|
# ACC_SERVERS_PATH=/data/servers
|
||||||
|
|
||||||
|
# Host-side path for the same directory — used when creating sibling ACC server containers.
|
||||||
|
# Must match the host path bound to ACC_SERVERS_PATH in docker-compose.yml.
|
||||||
|
# ACC_SERVERS_HOST_PATH=/opt/acc-servers
|
||||||
|
|
||||||
|
# Port pool for Docker-mode ACC server containers (one port per server).
|
||||||
|
# PORT_POOL_MIN=9600
|
||||||
|
# PORT_POOL_MAX=9700
|
||||||
|
|
||||||
|
# Linux SteamCMD binary path (pre-installed in the manager Docker image).
|
||||||
|
# STEAMCMD_LINUX_PATH=/usr/games/steamcmd
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# OPTIONAL SETTINGS (These are handled by system config in database)
|
# OPTIONAL SETTINGS (These are handled by system config in database)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
0
.gitea/workflows/release.yml
Normal file → Executable file
0
.gitea/workflows/release.yml
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
63
Dockerfile
Normal file
63
Dockerfile
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Multi-stage build for the acc-server-manager Go backend.
|
||||||
|
# CGO_ENABLED=1 is required for mattn/go-sqlite3 (uses cgo).
|
||||||
|
# The binary is compiled for Linux (Docker mode default).
|
||||||
|
|
||||||
|
# ─── Builder ──────────────────────────────────────────────────────────────────
|
||||||
|
FROM golang:1.25-bookworm AS builder
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Install build dependencies for CGO + sqlite3.
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
gcc \
|
||||||
|
libc6-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Download Go modules first (layer cache).
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source and build.
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \
|
||||||
|
go build -ldflags="-s -w" -o /acc-server-manager ./cmd/api
|
||||||
|
|
||||||
|
# ─── Runtime ──────────────────────────────────────────────────────────────────
|
||||||
|
FROM debian:bookworm-slim AS runtime
|
||||||
|
|
||||||
|
# Install runtime dependencies and SteamCMD.
|
||||||
|
# steamcmd is not in the standard Debian repo — download directly from Valve.
|
||||||
|
RUN dpkg --add-architecture i386 && \
|
||||||
|
apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
lib32gcc-s1 \
|
||||||
|
&& \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN mkdir -p /usr/games && \
|
||||||
|
curl -sSL --retry 3 --retry-delay 5 \
|
||||||
|
-o /tmp/steamcmd.tar.gz \
|
||||||
|
"https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz" && \
|
||||||
|
tar -xzf /tmp/steamcmd.tar.gz -C /usr/games && \
|
||||||
|
rm /tmp/steamcmd.tar.gz && \
|
||||||
|
chmod +x /usr/games/steamcmd.sh && \
|
||||||
|
ln -sf /usr/games/steamcmd.sh /usr/games/steamcmd
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /acc-server-manager /app/acc-server-manager
|
||||||
|
|
||||||
|
# Persistent data directories.
|
||||||
|
RUN mkdir -p /data/servers /data/db
|
||||||
|
|
||||||
|
ENV PLATFORM=docker
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV DB_NAME=/data/db/acc.db
|
||||||
|
ENV STEAMCMD_LINUX_PATH=/usr/games/steamcmd
|
||||||
|
ENV ACC_SERVERS_PATH=/data/servers
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/acc-server-manager"]
|
||||||
0
cmd/api/main.go
Normal file → Executable file
0
cmd/api/main.go
Normal file → Executable file
0
cmd/api/swagger.go
Normal file → Executable file
0
cmd/api/swagger.go
Normal file → Executable file
0
cmd/migrate/main.go
Normal file → Executable file
0
cmd/migrate/main.go
Normal file → Executable file
0
cmd/steam-crypt/main.go
Normal file → Executable file
0
cmd/steam-crypt/main.go
Normal file → Executable file
34
docker-compose.yml
Normal file
34
docker-compose.yml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
services:
|
||||||
|
manager:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: acc-server-manager:latest
|
||||||
|
container_name: acc-server-manager
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
PLATFORM: docker
|
||||||
|
DB_NAME: /data/db/acc.db
|
||||||
|
STEAMCMD_LINUX_PATH: /usr/games/steamcmd
|
||||||
|
PORT_POOL_MIN: "9600"
|
||||||
|
PORT_POOL_MAX: "9699"
|
||||||
|
ACC_IMAGE: acc-wine:latest
|
||||||
|
ACC_SERVERS_PATH: /data/servers
|
||||||
|
ACC_SERVERS_HOST_PATH: /opt/acc-servers
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- /opt/acc-servers:/data/servers
|
||||||
|
- acc-db:/data/db
|
||||||
|
networks:
|
||||||
|
- acc-net
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
acc-db:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
acc-net:
|
||||||
|
driver: bridge
|
||||||
35
docker/acc-server/Dockerfile
Normal file
35
docker/acc-server/Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# ACC Server container: Wine + accServer.exe
|
||||||
|
# Game files are bind-mounted from the host at /acc/game.
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
# Install Wine, Xvfb (headless display for Wine), and minimal runtime deps.
|
||||||
|
RUN dpkg --add-architecture i386 && \
|
||||||
|
apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
wine \
|
||||||
|
wine32 \
|
||||||
|
wine64 \
|
||||||
|
xvfb \
|
||||||
|
ca-certificates \
|
||||||
|
tcpdump \
|
||||||
|
&& \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN mkdir -p /tmp/.X11-unix && chmod 1777 /tmp/.X11-unix
|
||||||
|
|
||||||
|
WORKDIR /root
|
||||||
|
|
||||||
|
ENV WINEARCH=win64
|
||||||
|
ENV WINEPREFIX=/root/.wine
|
||||||
|
ENV WINEDEBUG=-all
|
||||||
|
|
||||||
|
# Copy entrypoint script.
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
# Bind-mounted by DockerRuntime.Create: per-server game install (rw)
|
||||||
|
VOLUME ["/acc/game"]
|
||||||
|
|
||||||
|
ENV SERVER_PORT=9600
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
78
docker/acc-server/entrypoint.sh
Normal file
78
docker/acc-server/entrypoint.sh
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DISPLAY_NUM=$((SERVER_PORT % 1000 + 100))
|
||||||
|
export DISPLAY=:${DISPLAY_NUM}
|
||||||
|
|
||||||
|
rm -f /tmp/.X${DISPLAY_NUM}-lock /tmp/.X11-unix/X${DISPLAY_NUM} 2>/dev/null || true
|
||||||
|
|
||||||
|
Xvfb ${DISPLAY} -screen 0 1024x768x16 -nolisten tcp &
|
||||||
|
XVFB_PID=$!
|
||||||
|
|
||||||
|
TCPDUMP_PID=""
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
echo "Stopping ACC server..."
|
||||||
|
[ -n "${TCPDUMP_PID}" ] && kill "${TCPDUMP_PID}" 2>/dev/null || true
|
||||||
|
kill "${XVFB_PID}" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup SIGTERM SIGINT EXIT
|
||||||
|
|
||||||
|
# Initialize Wine prefix (no-op if already done)
|
||||||
|
wineboot -u 2>/dev/null
|
||||||
|
|
||||||
|
mkdir -p /acc/game/log
|
||||||
|
|
||||||
|
# ─── Debug mode ───────────────────────────────────────────────────────────────
|
||||||
|
# Set DEBUG_MODE=1 on the container to enable extra diagnostics:
|
||||||
|
#
|
||||||
|
# docker run -e DEBUG_MODE=1 ...
|
||||||
|
# or in docker-compose:
|
||||||
|
# environment:
|
||||||
|
# DEBUG_MODE: "1"
|
||||||
|
#
|
||||||
|
# Produces:
|
||||||
|
# /acc/game/log/tcpdump_<port>.pcap — raw packet capture (open in Wireshark)
|
||||||
|
# /acc/game/log/wine_debug.log — Wine debug output
|
||||||
|
# Stderr: Wine version, WINEDEBUG channel, prefix state
|
||||||
|
#
|
||||||
|
# You can also override which Wine debug channels are enabled:
|
||||||
|
# WINEDEBUG_CHANNELS="+loaddll,+seh,+relay" (relay is very verbose)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
if [ "${DEBUG_MODE:-0}" = "1" ]; then
|
||||||
|
echo "[debug] Debug mode enabled for port ${SERVER_PORT}"
|
||||||
|
echo "[debug] Wine: $(wine --version 2>/dev/null || echo unknown)"
|
||||||
|
echo "[debug] WINEPREFIX: ${WINEPREFIX}"
|
||||||
|
|
||||||
|
# Configure Wine debug channels (default: loader + exception + module info)
|
||||||
|
export WINEDEBUG="${WINEDEBUG_CHANNELS:-+loaddll,+module,+seh,+ntdll}"
|
||||||
|
echo "[debug] WINEDEBUG=${WINEDEBUG}"
|
||||||
|
|
||||||
|
# Start packet capture on the server port — captures both TCP and UDP.
|
||||||
|
# The pcap file records every byte Kunos sends/receives so you can inspect
|
||||||
|
# the 2-byte "pakSizeMax" packet and anything that follows.
|
||||||
|
#
|
||||||
|
# NOTE: requires CAP_NET_RAW capability on the container:
|
||||||
|
# docker-compose: cap_add: [NET_RAW]
|
||||||
|
# docker run: --cap-add=NET_RAW
|
||||||
|
# Without it tcpdump will be skipped (no crash, just no capture).
|
||||||
|
CAPTURE_FILE="/acc/game/log/tcpdump_${SERVER_PORT}.pcap"
|
||||||
|
echo "[debug] Starting packet capture → ${CAPTURE_FILE}"
|
||||||
|
echo "[debug] (requires cap_add: [NET_RAW] in docker-compose — will exit silently if missing)"
|
||||||
|
tcpdump -i any -nn -s 0 -w "${CAPTURE_FILE}" \
|
||||||
|
"port ${SERVER_PORT}" 2>/acc/game/log/tcpdump.err &
|
||||||
|
TCPDUMP_PID=$!
|
||||||
|
|
||||||
|
# Redirect Wine debug output to a log file (Wine writes to stderr).
|
||||||
|
WINE_LOG="/acc/game/log/wine_debug.log"
|
||||||
|
echo "[debug] Wine debug log → ${WINE_LOG}"
|
||||||
|
echo "Starting ACC server on port ${SERVER_PORT} (DEBUG)..."
|
||||||
|
cd /acc/game
|
||||||
|
exec wine /acc/game/accServer.exe 2>"${WINE_LOG}"
|
||||||
|
else
|
||||||
|
# Production: suppress all Wine debug output.
|
||||||
|
export WINEDEBUG=-all
|
||||||
|
echo "Starting ACC server on port ${SERVER_PORT}..."
|
||||||
|
cd /acc/game
|
||||||
|
exec wine /acc/game/accServer.exe
|
||||||
|
fi
|
||||||
0
docs/API.md
Normal file → Executable file
0
docs/API.md
Normal file → Executable file
0
docs/CONFIG.md
Normal file → Executable file
0
docs/CONFIG.md
Normal file → Executable file
0
docs/DEPLOYMENT.md
Normal file → Executable file
0
docs/DEPLOYMENT.md
Normal file → Executable file
0
docs/MIGRATION.md
Normal file → Executable file
0
docs/MIGRATION.md
Normal file → Executable file
0
docs/SETUP.md
Normal file → Executable file
0
docs/SETUP.md
Normal file → Executable file
0
docs/STEAM_2FA_IMPLEMENTATION.md
Normal file → Executable file
0
docs/STEAM_2FA_IMPLEMENTATION.md
Normal file → Executable file
0
docs/SWAGGER.md
Normal file → Executable file
0
docs/SWAGGER.md
Normal file → Executable file
0
docs/TROUBLESHOOTING.md
Normal file → Executable file
0
docs/TROUBLESHOOTING.md
Normal file → Executable file
24
go.mod
Normal file → Executable file
24
go.mod
Normal file → Executable file
@@ -1,15 +1,19 @@
|
|||||||
module acc-server-manager
|
module acc-server-manager
|
||||||
|
|
||||||
go 1.23.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/docker/docker v24.0.9+incompatible
|
||||||
|
github.com/docker/go-connections v0.6.0
|
||||||
github.com/gofiber/fiber/v2 v2.52.8
|
github.com/gofiber/fiber/v2 v2.52.8
|
||||||
github.com/gofiber/swagger v1.1.0
|
github.com/gofiber/swagger v1.1.0
|
||||||
|
github.com/gofiber/websocket/v2 v2.2.1
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c
|
github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c
|
||||||
github.com/swaggo/swag v1.16.3
|
github.com/swaggo/swag v1.16.3
|
||||||
|
github.com/valyala/fasthttp v1.51.0
|
||||||
go.uber.org/dig v1.17.1
|
go.uber.org/dig v1.17.1
|
||||||
golang.org/x/crypto v0.39.0
|
golang.org/x/crypto v0.39.0
|
||||||
golang.org/x/sync v0.15.0
|
golang.org/x/sync v0.15.0
|
||||||
@@ -20,13 +24,17 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
|
github.com/Microsoft/go-winio v0.4.21 // indirect
|
||||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
|
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||||
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/fasthttp/websocket v1.5.3 // indirect
|
github.com/fasthttp/websocket v1.5.3 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||||
github.com/go-openapi/spec v0.21.0 // indirect
|
github.com/go-openapi/spec v0.21.0 // indirect
|
||||||
github.com/go-openapi/swag v0.23.0 // indirect
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
github.com/gofiber/websocket/v2 v2.2.1 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
@@ -36,13 +44,21 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
|
github.com/moby/term v0.5.2 // indirect
|
||||||
|
github.com/morikuni/aec v1.1.0 // indirect
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||||
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
github.com/swaggo/files/v2 v2.0.0 // indirect
|
github.com/swaggo/files/v2 v2.0.0 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
|
golang.org/x/time v0.9.0 // indirect
|
||||||
golang.org/x/tools v0.33.0 // indirect
|
golang.org/x/tools v0.33.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
gotest.tools/v3 v3.5.2 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
76
go.sum
Normal file → Executable file
76
go.sum
Normal file → Executable file
@@ -1,9 +1,21 @@
|
|||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
|
github.com/Microsoft/go-winio v0.4.21 h1:+6mVbXh4wPzUrl1COX9A+ZCvEpYsOBZ6/+kwDnvLyro=
|
||||||
|
github.com/Microsoft/go-winio v0.4.21/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
||||||
|
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||||
|
github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0=
|
||||||
|
github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
|
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||||
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek=
|
github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek=
|
||||||
github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs=
|
github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs=
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||||
@@ -20,8 +32,12 @@ github.com/gofiber/swagger v1.1.0 h1:ff3rg1fB+Rp5JN/N8jfxTiZtMKe/9tB9QDc79fPiJKQ
|
|||||||
github.com/gofiber/swagger v1.1.0/go.mod h1:pRZL0Np35sd+lTODTE5The0G+TMHfNY+oC4hM2/i5m8=
|
github.com/gofiber/swagger v1.1.0/go.mod h1:pRZL0Np35sd+lTODTE5The0G+TMHfNY+oC4hM2/i5m8=
|
||||||
github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w=
|
github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w=
|
||||||
github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU=
|
github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
@@ -32,6 +48,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
|||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@@ -49,18 +67,31 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
|
|||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||||
|
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||||
|
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
|
||||||
|
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c h1:kmzxiX+OB0knCo1V0dkEkdPelzCdAzCURCfmFArn2/A=
|
github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c h1:kmzxiX+OB0knCo1V0dkEkdPelzCdAzCURCfmFArn2/A=
|
||||||
github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c/go.mod h1:wNJrtinHyC3YSf6giEh4FJN8+yZV7nXBjvmfjhBIcw4=
|
github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c/go.mod h1:wNJrtinHyC3YSf6giEh4FJN8+yZV7nXBjvmfjhBIcw4=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
||||||
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
|
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
|
||||||
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
|
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
|
||||||
@@ -71,22 +102,53 @@ github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1S
|
|||||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc=
|
go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc=
|
||||||
go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
|
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||||
|
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
@@ -96,3 +158,5 @@ gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
|
|||||||
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||||
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
|
||||||
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
|
|||||||
3
local/api/api.go
Normal file → Executable file
3
local/api/api.go
Normal file → Executable file
@@ -33,6 +33,9 @@ func Init(di *dig.Container, app *fiber.App) {
|
|||||||
System: groups.Group("/system"),
|
System: groups.Group("/system"),
|
||||||
WebSocket: groups.Group("/ws"),
|
WebSocket: groups.Group("/ws"),
|
||||||
Leaderboard: serverIdGroup.Group("/leaderboard"),
|
Leaderboard: serverIdGroup.Group("/leaderboard"),
|
||||||
|
Steam: groups.Group("/steam"),
|
||||||
|
ActivityLog: serverIdGroup.Group("/activity-log"),
|
||||||
|
Groups: groups,
|
||||||
}
|
}
|
||||||
|
|
||||||
accessKeyMiddleware := middleware.NewAccessKeyMiddleware()
|
accessKeyMiddleware := middleware.NewAccessKeyMiddleware()
|
||||||
|
|||||||
72
local/controller/activity_log.go
Normal file
72
local/controller/activity_log.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/middleware"
|
||||||
|
"acc-server-manager/local/model"
|
||||||
|
"acc-server-manager/local/service"
|
||||||
|
"acc-server-manager/local/utl/common"
|
||||||
|
"acc-server-manager/local/utl/error_handler"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityLogController struct {
|
||||||
|
service *service.ActivityLogService
|
||||||
|
errorHandler *error_handler.ControllerErrorHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewActivityLogController(as *service.ActivityLogService, routeGroups *common.RouteGroups, auth *middleware.AuthMiddleware) *ActivityLogController {
|
||||||
|
ac := &ActivityLogController{
|
||||||
|
service: as,
|
||||||
|
errorHandler: error_handler.NewControllerErrorHandler(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-server activity log: GET /server/:id/activity-log
|
||||||
|
routeGroups.ActivityLog.Use(auth.Authenticate)
|
||||||
|
routeGroups.ActivityLog.Get("/", ac.GetServerLogs)
|
||||||
|
|
||||||
|
// Global activity log: GET /activity-log (admin use)
|
||||||
|
routeGroups.Groups.Group("/activity-log").Use(auth.Authenticate).Get("/", ac.GetAllLogs)
|
||||||
|
|
||||||
|
return ac
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetServerLogs returns activity logs for a specific server.
|
||||||
|
//
|
||||||
|
// @Summary Return activity logs for a server
|
||||||
|
// @Tags ActivityLog
|
||||||
|
// @Success 200 {array} model.ActivityLog
|
||||||
|
// @Router /server/{id}/activity-log [get]
|
||||||
|
func (ac *ActivityLogController) GetServerLogs(c *fiber.Ctx) error {
|
||||||
|
var filter model.ActivityLogFilter
|
||||||
|
if err := common.ParseQueryFilter(c, &filter); err != nil {
|
||||||
|
return ac.errorHandler.HandleValidationError(c, err, "query_filter")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ac.service.GetAll(c.UserContext(), &filter)
|
||||||
|
if err != nil {
|
||||||
|
return ac.errorHandler.HandleServiceError(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllLogs returns activity logs across all servers (admin).
|
||||||
|
//
|
||||||
|
// @Summary Return all activity logs
|
||||||
|
// @Tags ActivityLog
|
||||||
|
// @Success 200 {array} model.ActivityLog
|
||||||
|
// @Router /activity-log [get]
|
||||||
|
func (ac *ActivityLogController) GetAllLogs(c *fiber.Ctx) error {
|
||||||
|
var filter model.ActivityLogFilter
|
||||||
|
if err := common.ParseQueryFilter(c, &filter); err != nil {
|
||||||
|
return ac.errorHandler.HandleValidationError(c, err, "query_filter")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ac.service.GetAll(c.UserContext(), &filter)
|
||||||
|
if err != nil {
|
||||||
|
return ac.errorHandler.HandleServiceError(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(result)
|
||||||
|
}
|
||||||
50
local/controller/config.go
Normal file → Executable file
50
local/controller/config.go
Normal file → Executable file
@@ -2,6 +2,7 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"acc-server-manager/local/middleware"
|
"acc-server-manager/local/middleware"
|
||||||
|
"acc-server-manager/local/model"
|
||||||
"acc-server-manager/local/service"
|
"acc-server-manager/local/service"
|
||||||
"acc-server-manager/local/utl/common"
|
"acc-server-manager/local/utl/common"
|
||||||
"acc-server-manager/local/utl/error_handler"
|
"acc-server-manager/local/utl/error_handler"
|
||||||
@@ -34,6 +35,7 @@ func NewConfigController(as *service.ConfigService, routeGroups *common.RouteGro
|
|||||||
|
|
||||||
configGroup := routeGroups.Config
|
configGroup := routeGroups.Config
|
||||||
configGroup.Use(auth.Authenticate)
|
configGroup.Use(auth.Authenticate)
|
||||||
|
configGroup.Patch("/", ac.UpdateAllConfigs)
|
||||||
configGroup.Put("/:file", ac.UpdateConfig)
|
configGroup.Put("/:file", ac.UpdateConfig)
|
||||||
configGroup.Get("/:file", ac.GetConfig)
|
configGroup.Get("/:file", ac.GetConfig)
|
||||||
configGroup.Get("/", ac.GetConfigs)
|
configGroup.Get("/", ac.GetConfigs)
|
||||||
@@ -41,6 +43,54 @@ func NewConfigController(as *service.ConfigService, routeGroups *common.RouteGro
|
|||||||
return ac
|
return ac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateAllConfigs updates all configuration files
|
||||||
|
//
|
||||||
|
// @Summary Bulk update all server configuration files
|
||||||
|
// @Description Update multiple ACC server configuration files in one request. Null or omitted sections are skipped.
|
||||||
|
// @Tags Server Configuration
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Server ID (UUID format)"
|
||||||
|
// @Param restart query bool false "Restart the server after update"
|
||||||
|
// @Param override query bool false "Override instead of merging with existing config"
|
||||||
|
// @Param content body model.ConfigurationsUpdateRequest true "Configuration sections to update (null sections are skipped)"
|
||||||
|
// @Success 200 {array} model.Config "List of updated config audit records"
|
||||||
|
// @Failure 400 {object} error_handler.ErrorResponse "Invalid request or JSON format"
|
||||||
|
// @Failure 401 {object} error_handler.ErrorResponse "Unauthorized"
|
||||||
|
// @Failure 403 {object} error_handler.ErrorResponse "Insufficient permissions"
|
||||||
|
// @Failure 404 {object} error_handler.ErrorResponse "Server not found"
|
||||||
|
// @Failure 500 {object} error_handler.ErrorResponse "Internal server error"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /server/{id}/config [patch]
|
||||||
|
func (ac *ConfigController) UpdateAllConfigs(c *fiber.Ctx) error {
|
||||||
|
restart := c.QueryBool("restart")
|
||||||
|
serverID := c.Params("id")
|
||||||
|
|
||||||
|
if _, err := uuid.Parse(serverID); err != nil {
|
||||||
|
return ac.errorHandler.HandleUUIDError(c, "server ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Locals("serverId", serverID)
|
||||||
|
|
||||||
|
var req model.ConfigurationsUpdateRequest
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return ac.errorHandler.HandleParsingError(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configs, err := ac.service.UpdateAllConfigs(c, &req)
|
||||||
|
if err != nil {
|
||||||
|
return ac.errorHandler.HandleServiceError(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if restart {
|
||||||
|
if _, err := ac.apiService.ServiceControlRestartServer(c); err != nil {
|
||||||
|
logging.ErrorWithContext("CONFIG_RESTART", "Failed to restart server after bulk config update: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(configs)
|
||||||
|
}
|
||||||
|
|
||||||
// updateConfig returns Config
|
// updateConfig returns Config
|
||||||
//
|
//
|
||||||
// @Summary Update server configuration file
|
// @Summary Update server configuration file
|
||||||
|
|||||||
10
local/controller/controller.go
Normal file → Executable file
10
local/controller/controller.go
Normal file → Executable file
@@ -64,4 +64,14 @@ func InitializeControllers(c *dig.Container) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Panic("unable to initialize leaderboard controller")
|
logging.Panic("unable to initialize leaderboard controller")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = c.Invoke(NewSteamController)
|
||||||
|
if err != nil {
|
||||||
|
logging.Panic("unable to initialize steam controller")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.Invoke(NewActivityLogController)
|
||||||
|
if err != nil {
|
||||||
|
logging.Panic("unable to initialize activity log controller")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
local/controller/leaderboard.go
Normal file → Executable file
15
local/controller/leaderboard.go
Normal file → Executable file
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
type LeaderboardController struct {
|
type LeaderboardController struct {
|
||||||
service *service.LeaderboardService
|
service *service.LeaderboardService
|
||||||
errorHandler *error_handler.ControllerErrorHandler
|
errorHandler *error_handler.ControllerErrorHandler
|
||||||
@@ -22,6 +23,9 @@ func NewLeaderboardController(ls *service.LeaderboardService, routeGroups *commo
|
|||||||
errorHandler: error_handler.NewControllerErrorHandler(),
|
errorHandler: error_handler.NewControllerErrorHandler(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apiServerRoutes := routeGroups.Api.Group("/server/:id")
|
||||||
|
apiServerRoutes.Get("/leaderboard", lc.Get)
|
||||||
|
|
||||||
routeGroups.Leaderboard.Get("/", lc.Get)
|
routeGroups.Leaderboard.Get("/", lc.Get)
|
||||||
routeGroups.Leaderboard.Put("/", auth.Authenticate, lc.Update)
|
routeGroups.Leaderboard.Put("/", auth.Authenticate, lc.Update)
|
||||||
|
|
||||||
@@ -52,12 +56,19 @@ func (lc *LeaderboardController) Update(c *fiber.Ctx) error {
|
|||||||
return lc.errorHandler.HandleUUIDError(c, "server ID")
|
return lc.errorHandler.HandleUUIDError(c, "server ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
var input model.LeaderboardInput
|
var input model.Leaderboard
|
||||||
if err := c.BodyParser(&input); err != nil {
|
if err := c.BodyParser(&input); err != nil {
|
||||||
return lc.errorHandler.HandleParsingError(c, err)
|
return lc.errorHandler.HandleParsingError(c, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := lc.service.Update(c.UserContext(), serverID, &input)
|
userInfo, _ := c.Locals("userInfo").(*middleware.CachedUserInfo)
|
||||||
|
actorID, actorUsername := "", "system"
|
||||||
|
if userInfo != nil {
|
||||||
|
actorID = userInfo.UserID
|
||||||
|
actorUsername = userInfo.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := lc.service.Update(c.UserContext(), serverID, &input, actorID, actorUsername)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return lc.errorHandler.HandleServiceError(c, err)
|
return lc.errorHandler.HandleServiceError(c, err)
|
||||||
}
|
}
|
||||||
|
|||||||
0
local/controller/lookup.go
Normal file → Executable file
0
local/controller/lookup.go
Normal file → Executable file
0
local/controller/membership.go
Normal file → Executable file
0
local/controller/membership.go
Normal file → Executable file
0
local/controller/server.go
Normal file → Executable file
0
local/controller/server.go
Normal file → Executable file
0
local/controller/service_control.go
Normal file → Executable file
0
local/controller/service_control.go
Normal file → Executable file
0
local/controller/state_history.go
Normal file → Executable file
0
local/controller/state_history.go
Normal file → Executable file
57
local/controller/steam.go
Normal file
57
local/controller/steam.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/middleware"
|
||||||
|
"acc-server-manager/local/model"
|
||||||
|
"acc-server-manager/local/service"
|
||||||
|
"acc-server-manager/local/utl/common"
|
||||||
|
"acc-server-manager/local/utl/error_handler"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SteamController struct {
|
||||||
|
steamService *service.SteamService
|
||||||
|
auth *middleware.AuthMiddleware
|
||||||
|
errorHandler *error_handler.ControllerErrorHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSteamController(steamService *service.SteamService, auth *middleware.AuthMiddleware, routeGroups *common.RouteGroups) *SteamController {
|
||||||
|
sc := &SteamController{
|
||||||
|
steamService: steamService,
|
||||||
|
auth: auth,
|
||||||
|
errorHandler: error_handler.NewControllerErrorHandler(),
|
||||||
|
}
|
||||||
|
|
||||||
|
routeGroups.Steam.Use(auth.Authenticate, auth.RequireSuperAdmin())
|
||||||
|
routeGroups.Steam.Post("/credentials", sc.SetCredentials)
|
||||||
|
|
||||||
|
return sc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *SteamController) SetCredentials(c *fiber.Ctx) error {
|
||||||
|
type request struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var req request
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return sc.errorHandler.HandleParsingError(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
creds := &model.SteamCredentials{
|
||||||
|
Username: req.Username,
|
||||||
|
Password: req.Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := creds.Validate(); err != nil {
|
||||||
|
return sc.errorHandler.HandleServiceError(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sc.steamService.SaveCredentials(c.UserContext(), creds); err != nil {
|
||||||
|
return sc.errorHandler.HandleServiceError(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"id": creds.ID})
|
||||||
|
}
|
||||||
0
local/controller/system.go
Normal file → Executable file
0
local/controller/system.go
Normal file → Executable file
0
local/controller/websocket.go
Normal file → Executable file
0
local/controller/websocket.go
Normal file → Executable file
0
local/middleware/access_key.go
Normal file → Executable file
0
local/middleware/access_key.go
Normal file → Executable file
19
local/middleware/auth.go
Normal file → Executable file
19
local/middleware/auth.go
Normal file → Executable file
@@ -115,6 +115,7 @@ func (m *AuthMiddleware) AuthenticateWithHandler(jwtHandler *jwt.JWTHandler, isO
|
|||||||
if os.Getenv("TESTING_ENV") == "true" {
|
if os.Getenv("TESTING_ENV") == "true" {
|
||||||
userInfo := CachedUserInfo{UserID: uuid.New().String(), Username: "test@example.com", RoleName: "Admin", Permissions: make(map[string]bool), CachedAt: time.Now()}
|
userInfo := CachedUserInfo{UserID: uuid.New().String(), Username: "test@example.com", RoleName: "Admin", Permissions: make(map[string]bool), CachedAt: time.Now()}
|
||||||
ctx.Locals("userID", userInfo.UserID)
|
ctx.Locals("userID", userInfo.UserID)
|
||||||
|
ctx.Locals("actorUsername", userInfo.Username)
|
||||||
ctx.Locals("userInfo", userInfo)
|
ctx.Locals("userInfo", userInfo)
|
||||||
ctx.Locals("authTime", time.Now())
|
ctx.Locals("authTime", time.Now())
|
||||||
} else {
|
} else {
|
||||||
@@ -127,6 +128,7 @@ func (m *AuthMiddleware) AuthenticateWithHandler(jwtHandler *jwt.JWTHandler, isO
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.Locals("userID", claims.UserID)
|
ctx.Locals("userID", claims.UserID)
|
||||||
|
ctx.Locals("actorUsername", userInfo.Username)
|
||||||
ctx.Locals("userInfo", userInfo)
|
ctx.Locals("userInfo", userInfo)
|
||||||
ctx.Locals("authTime", time.Now())
|
ctx.Locals("authTime", time.Now())
|
||||||
}
|
}
|
||||||
@@ -178,6 +180,23 @@ func (m *AuthMiddleware) HasPermission(requiredPermission string) fiber.Handler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *AuthMiddleware) RequireSuperAdmin() fiber.Handler {
|
||||||
|
return func(ctx *fiber.Ctx) error {
|
||||||
|
if os.Getenv("TESTING_ENV") == "true" {
|
||||||
|
return ctx.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfo, ok := ctx.Locals("userInfo").(*CachedUserInfo)
|
||||||
|
if !ok || userInfo.RoleName != "Super Admin" {
|
||||||
|
return ctx.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||||
|
"error": "Forbidden",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m *AuthMiddleware) AuthRateLimit() fiber.Handler {
|
func (m *AuthMiddleware) AuthRateLimit() fiber.Handler {
|
||||||
return m.securityMW.AuthRateLimit()
|
return m.securityMW.AuthRateLimit()
|
||||||
}
|
}
|
||||||
|
|||||||
0
local/middleware/logging/request_logging.go
Normal file → Executable file
0
local/middleware/logging/request_logging.go
Normal file → Executable file
0
local/middleware/security/security.go
Normal file → Executable file
0
local/middleware/security/security.go
Normal file → Executable file
0
local/migrations/001_upgrade_password_security.go
Normal file → Executable file
0
local/migrations/001_upgrade_password_security.go
Normal file → Executable file
0
local/migrations/002_migrate_to_uuid.go
Normal file → Executable file
0
local/migrations/002_migrate_to_uuid.go
Normal file → Executable file
0
local/migrations/003_update_state_history_sessions.go
Normal file → Executable file
0
local/migrations/003_update_state_history_sessions.go
Normal file → Executable file
0
local/migrations/004_fix_ddl_comments.go
Normal file → Executable file
0
local/migrations/004_fix_ddl_comments.go
Normal file → Executable file
33
local/migrations/005_add_platform_columns.go
Normal file
33
local/migrations/005_add_platform_columns.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/utl/logging"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RunAddPlatformColumnsMigration(db *gorm.DB) error {
|
||||||
|
if err := db.AutoMigrate(&MigrationRecord{}); err != nil {
|
||||||
|
return fmt.Errorf("failed to ensure migration_records table: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var record MigrationRecord
|
||||||
|
if err := db.Where("migration_name = ?", "005_add_platform_columns").First(&record).Error; err == nil {
|
||||||
|
logging.Info("Platform columns migration already applied, skipping")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
record = MigrationRecord{
|
||||||
|
MigrationName: "005_add_platform_columns",
|
||||||
|
AppliedAt: "datetime('now')",
|
||||||
|
Success: true,
|
||||||
|
Notes: "Added Platform and ContainerID columns to servers table via GORM AutoMigrate",
|
||||||
|
}
|
||||||
|
if err := db.Create(&record).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to record migration 005: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("Platform columns migration recorded")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
33
local/migrations/006_add_activity_log.go
Normal file
33
local/migrations/006_add_activity_log.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/utl/logging"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RunAddActivityLogMigration(db *gorm.DB) error {
|
||||||
|
if err := db.AutoMigrate(&MigrationRecord{}); err != nil {
|
||||||
|
return fmt.Errorf("failed to ensure migration_records table: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var record MigrationRecord
|
||||||
|
if err := db.Where("migration_name = ?", "006_add_activity_log").First(&record).Error; err == nil {
|
||||||
|
logging.Info("Activity log migration already applied, skipping")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
record = MigrationRecord{
|
||||||
|
MigrationName: "006_add_activity_log",
|
||||||
|
AppliedAt: "datetime('now')",
|
||||||
|
Success: true,
|
||||||
|
Notes: "Added ActivityLog table via GORM AutoMigrate",
|
||||||
|
}
|
||||||
|
if err := db.Create(&record).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to record migration 006: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("Activity log migration recorded")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
81
local/model/activity_log.go
Normal file
81
local/model/activity_log.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActionType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ActionConfigUpdate ActionType = "config_update"
|
||||||
|
ActionServerStart ActionType = "server_start"
|
||||||
|
ActionServerStop ActionType = "server_stop"
|
||||||
|
ActionServerRestart ActionType = "server_restart"
|
||||||
|
ActionLeaderboardUpdate ActionType = "leaderboard_update"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityLog struct {
|
||||||
|
ID uuid.UUID `json:"id" gorm:"type:uuid;primary_key;"`
|
||||||
|
ServerID uuid.UUID `json:"serverId" gorm:"not null;type:uuid;index"`
|
||||||
|
UserID string `json:"userId" gorm:"not null"`
|
||||||
|
Username string `json:"username" gorm:"not null"`
|
||||||
|
Action ActionType `json:"action" gorm:"not null"`
|
||||||
|
Details string `json:"details" gorm:"type:text"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ActivityLog) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if a.ID == uuid.Nil {
|
||||||
|
a.ID = uuid.New()
|
||||||
|
}
|
||||||
|
if a.CreatedAt.IsZero() {
|
||||||
|
a.CreatedAt = time.Now().UTC()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityLogFilter struct {
|
||||||
|
BaseFilter
|
||||||
|
ServerBasedFilter
|
||||||
|
DateRangeFilter
|
||||||
|
UserID string `query:"user_id"`
|
||||||
|
Action ActionType `query:"action"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *ActivityLogFilter) ApplyFilter(query *gorm.DB) *gorm.DB {
|
||||||
|
if f.ServerID != "" {
|
||||||
|
if serverUUID, err := uuid.Parse(f.ServerID); err == nil {
|
||||||
|
query = query.Where("server_id = ?", serverUUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if f.UserID != "" {
|
||||||
|
query = query.Where("user_id = ?", f.UserID)
|
||||||
|
}
|
||||||
|
if f.Action != "" {
|
||||||
|
query = query.Where("action = ?", f.Action)
|
||||||
|
}
|
||||||
|
timeZero := time.Time{}
|
||||||
|
if f.StartDate != timeZero {
|
||||||
|
query = query.Where("created_at >= ?", f.StartDate)
|
||||||
|
}
|
||||||
|
if f.EndDate != timeZero {
|
||||||
|
query = query.Where("created_at <= ?", f.EndDate)
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *ActivityLogFilter) GetSorting() (field string, desc bool) {
|
||||||
|
if f.SortBy == "" {
|
||||||
|
return "created_at", true
|
||||||
|
}
|
||||||
|
return f.SortBy, f.SortDesc
|
||||||
|
}
|
||||||
|
|
||||||
|
type LastActivityInfo struct {
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Action ActionType `json:"action"`
|
||||||
|
}
|
||||||
0
local/model/cache.go
Normal file → Executable file
0
local/model/cache.go
Normal file → Executable file
8
local/model/config.go
Normal file → Executable file
8
local/model/config.go
Normal file → Executable file
@@ -40,6 +40,14 @@ type Configurations struct {
|
|||||||
Settings ServerSettings `json:"settings"`
|
Settings ServerSettings `json:"settings"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConfigurationsUpdateRequest struct {
|
||||||
|
Configuration *map[string]interface{} `json:"configuration"`
|
||||||
|
AssistRules *map[string]interface{} `json:"assistRules"`
|
||||||
|
Event *map[string]interface{} `json:"event"`
|
||||||
|
EventRules *map[string]interface{} `json:"eventRules"`
|
||||||
|
Settings *map[string]interface{} `json:"settings"`
|
||||||
|
}
|
||||||
|
|
||||||
type ServerSettings struct {
|
type ServerSettings struct {
|
||||||
ServerName string `json:"serverName"`
|
ServerName string `json:"serverName"`
|
||||||
AdminPassword string `json:"adminPassword"`
|
AdminPassword string `json:"adminPassword"`
|
||||||
|
|||||||
0
local/model/filter.go
Normal file → Executable file
0
local/model/filter.go
Normal file → Executable file
84
local/model/leaderboard.go
Normal file → Executable file
84
local/model/leaderboard.go
Normal file → Executable file
@@ -1,10 +1,36 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Score is a custom type that accepts both numbers and strings in JSON.
|
||||||
|
type Score string
|
||||||
|
|
||||||
|
func (s *Score) UnmarshalJSON(data []byte) error {
|
||||||
|
var n json.Number
|
||||||
|
if err := json.Unmarshal(data, &n); err == nil {
|
||||||
|
*s = Score(n.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var str string
|
||||||
|
if err := json.Unmarshal(data, &str); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*s = Score(str)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Score) MarshalJSON() ([]byte, error) {
|
||||||
|
if n, err := strconv.Atoi(string(s)); err == nil {
|
||||||
|
return json.Marshal(n)
|
||||||
|
}
|
||||||
|
return json.Marshal(string(s))
|
||||||
|
}
|
||||||
|
|
||||||
type Leaderboard struct {
|
type Leaderboard struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
||||||
ServerID uuid.UUID `gorm:"uniqueIndex;not null;type:uuid" json:"serverId"`
|
ServerID uuid.UUID `gorm:"uniqueIndex;not null;type:uuid" json:"serverId"`
|
||||||
@@ -12,38 +38,38 @@ type Leaderboard struct {
|
|||||||
FLColor string `gorm:"default:'#8b5cf6'" json:"flColor"`
|
FLColor string `gorm:"default:'#8b5cf6'" json:"flColor"`
|
||||||
FLTextColor string `gorm:"default:'#000000'" json:"flTextColor"`
|
FLTextColor string `gorm:"default:'#000000'" json:"flTextColor"`
|
||||||
Drivers []LeaderboardDriver `gorm:"foreignKey:LeaderboardID;constraint:OnDelete:CASCADE" json:"drivers"`
|
Drivers []LeaderboardDriver `gorm:"foreignKey:LeaderboardID;constraint:OnDelete:CASCADE" json:"drivers"`
|
||||||
Races []LeaderboardRace `gorm:"foreignKey:LeaderboardID;constraint:OnDelete:CASCADE" json:"races"`
|
Races []LeaderboardRace `gorm:"foreignKey:LeaderboardID;constraint:OnDelete:CASCADE" json:"tracks"`
|
||||||
PointRows []LeaderboardPointRow `gorm:"foreignKey:LeaderboardID;constraint:OnDelete:CASCADE" json:"pointRows"`
|
PointRows []LeaderboardPointRow `gorm:"foreignKey:LeaderboardID;constraint:OnDelete:CASCADE" json:"pointsTable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LeaderboardDriver struct {
|
type LeaderboardDriver struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
||||||
LeaderboardID uuid.UUID `gorm:"not null;type:uuid" json:"leaderboardId"`
|
LeaderboardID uuid.UUID `gorm:"not null;type:uuid" json:"-"`
|
||||||
Name string `gorm:"not null" json:"name"`
|
Name string `gorm:"not null" json:"name"`
|
||||||
Initials string `json:"initials"`
|
Initials string `json:"initials"`
|
||||||
Color string `json:"color"`
|
Color string `json:"color"`
|
||||||
Position int `json:"position"`
|
Position int `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LeaderboardRace struct {
|
type LeaderboardRace struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
||||||
LeaderboardID uuid.UUID `gorm:"not null;type:uuid" json:"leaderboardId"`
|
LeaderboardID uuid.UUID `gorm:"not null;type:uuid" json:"-"`
|
||||||
Name string `gorm:"not null" json:"name"`
|
Name string `gorm:"not null" json:"name"`
|
||||||
Position int `json:"position"`
|
Position int `json:"-"`
|
||||||
FastestLapInitials string `json:"fastestLapInitials"`
|
FastestLapDriverID *uuid.UUID `gorm:"type:uuid" json:"fastestLapDriverId"`
|
||||||
Results []LeaderboardResult `gorm:"foreignKey:RaceID;constraint:OnDelete:CASCADE" json:"results"`
|
Results []LeaderboardResult `gorm:"foreignKey:RaceID;constraint:OnDelete:CASCADE" json:"results"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LeaderboardResult struct {
|
type LeaderboardResult struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"-"`
|
||||||
RaceID uuid.UUID `gorm:"not null;type:uuid" json:"raceId"`
|
RaceID uuid.UUID `gorm:"not null;type:uuid" json:"-"`
|
||||||
DriverID uuid.UUID `gorm:"not null;type:uuid" json:"driverId"`
|
DriverID uuid.UUID `gorm:"not null;type:uuid" json:"driverId"`
|
||||||
Score string `json:"score"`
|
Score Score `json:"score"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LeaderboardPointRow struct {
|
type LeaderboardPointRow struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
||||||
LeaderboardID uuid.UUID `gorm:"not null;type:uuid" json:"leaderboardId"`
|
LeaderboardID uuid.UUID `gorm:"not null;type:uuid" json:"-"`
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
Points int `json:"points"`
|
Points int `json:"points"`
|
||||||
Color string `json:"color"`
|
Color string `json:"color"`
|
||||||
@@ -51,42 +77,6 @@ type LeaderboardPointRow struct {
|
|||||||
Priority int `json:"priority"`
|
Priority int `json:"priority"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LeaderboardInput is the wire format matching the HTML's appData shape.
|
|
||||||
type LeaderboardInput struct {
|
|
||||||
Drivers []LeaderboardDriverInput `json:"drivers"`
|
|
||||||
PointsTable []LeaderboardPointRowInput `json:"pointsTable"`
|
|
||||||
FLPoints LeaderboardFLInput `json:"flPoints"`
|
|
||||||
Tracks []LeaderboardTrackInput `json:"tracks"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LeaderboardDriverInput struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Color string `json:"color"`
|
|
||||||
Initials string `json:"initials"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LeaderboardPointRowInput struct {
|
|
||||||
Points int `json:"points"`
|
|
||||||
Label string `json:"label"`
|
|
||||||
Color string `json:"color"`
|
|
||||||
TextColor string `json:"textColor"`
|
|
||||||
Priority int `json:"priority"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LeaderboardFLInput struct {
|
|
||||||
Points int `json:"points"`
|
|
||||||
Label string `json:"label"`
|
|
||||||
Color string `json:"color"`
|
|
||||||
TextColor string `json:"textColor"`
|
|
||||||
Priority int `json:"priority"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LeaderboardTrackInput struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Results []interface{} `json:"results"`
|
|
||||||
FastestLapInitials string `json:"fastestLapInitials"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Leaderboard) BeforeCreate(tx *gorm.DB) error {
|
func (l *Leaderboard) BeforeCreate(tx *gorm.DB) error {
|
||||||
if l.ID == uuid.Nil {
|
if l.ID == uuid.Nil {
|
||||||
l.ID = uuid.New()
|
l.ID = uuid.New()
|
||||||
|
|||||||
0
local/model/lookup.go
Normal file → Executable file
0
local/model/lookup.go
Normal file → Executable file
0
local/model/model.go
Normal file → Executable file
0
local/model/model.go
Normal file → Executable file
0
local/model/permission.go
Normal file → Executable file
0
local/model/permission.go
Normal file → Executable file
0
local/model/permissions.go
Normal file → Executable file
0
local/model/permissions.go
Normal file → Executable file
0
local/model/role.go
Normal file → Executable file
0
local/model/role.go
Normal file → Executable file
3
local/model/server.go
Normal file → Executable file
3
local/model/server.go
Normal file → Executable file
@@ -42,7 +42,10 @@ type Server struct {
|
|||||||
Port int `gorm:"not null" json:"-"`
|
Port int `gorm:"not null" json:"-"`
|
||||||
Path string `gorm:"not null" json:"path"`
|
Path string `gorm:"not null" json:"path"`
|
||||||
ServiceName string `gorm:"not null" json:"serviceName"`
|
ServiceName string `gorm:"not null" json:"serviceName"`
|
||||||
|
Platform string `gorm:"not null;default:'windows'" json:"platform"`
|
||||||
|
ContainerID string `gorm:"default:''" json:"-"`
|
||||||
State *ServerState `gorm:"-" json:"state"`
|
State *ServerState `gorm:"-" json:"state"`
|
||||||
|
LastActivity *LastActivityInfo `gorm:"-" json:"lastActivity,omitempty"`
|
||||||
DateCreated time.Time `json:"dateCreated"`
|
DateCreated time.Time `json:"dateCreated"`
|
||||||
FromSteamCMD bool `gorm:"not null; default:true" json:"-"`
|
FromSteamCMD bool `gorm:"not null; default:true" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
0
local/model/service_control.go
Normal file → Executable file
0
local/model/service_control.go
Normal file → Executable file
0
local/model/state_history.go
Normal file → Executable file
0
local/model/state_history.go
Normal file → Executable file
0
local/model/state_history_stats.go
Normal file → Executable file
0
local/model/state_history_stats.go
Normal file → Executable file
0
local/model/steam_2fa.go
Normal file → Executable file
0
local/model/steam_2fa.go
Normal file → Executable file
0
local/model/steam_credentials.go
Normal file → Executable file
0
local/model/steam_credentials.go
Normal file → Executable file
0
local/model/user.go
Normal file → Executable file
0
local/model/user.go
Normal file → Executable file
0
local/model/websocket.go
Normal file → Executable file
0
local/model/websocket.go
Normal file → Executable file
28
local/platform/interfaces.go
Normal file
28
local/platform/interfaces.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package platform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/model"
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerRuntime interface {
|
||||||
|
Create(ctx context.Context, server *model.Server) error
|
||||||
|
Delete(ctx context.Context, serverID uuid.UUID) error
|
||||||
|
Start(ctx context.Context, serverID uuid.UUID) (string, error)
|
||||||
|
Stop(ctx context.Context, serverID uuid.UUID) (string, error)
|
||||||
|
Restart(ctx context.Context, serverID uuid.UUID) (string, error)
|
||||||
|
Status(ctx context.Context, serverID uuid.UUID) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallManager interface {
|
||||||
|
CreateServerRules(serverName string, tcpPorts, udpPorts []int) error
|
||||||
|
DeleteServerRules(serverName string, tcpPorts, udpPorts []int) error
|
||||||
|
UpdateServerRules(serverName string, tcpPorts, udpPorts []int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogStreamer interface {
|
||||||
|
Start(ctx context.Context, server *model.Server, handleLine func(string)) error
|
||||||
|
Stop(serverID uuid.UUID)
|
||||||
|
}
|
||||||
75
local/repository/activity_log.go
Normal file
75
local/repository/activity_log.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/model"
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityLogRepository struct {
|
||||||
|
*BaseRepository[model.ActivityLog, model.ActivityLogFilter]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewActivityLogRepository(db *gorm.DB) *ActivityLogRepository {
|
||||||
|
return &ActivityLogRepository{
|
||||||
|
BaseRepository: NewBaseRepository[model.ActivityLog, model.ActivityLogFilter](db, model.ActivityLog{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ActivityLogRepository) Insert(ctx context.Context, entry *model.ActivityLog) error {
|
||||||
|
return r.BaseRepository.Insert(ctx, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ActivityLogRepository) GetAll(ctx context.Context, filter *model.ActivityLogFilter) (*[]model.ActivityLog, error) {
|
||||||
|
return r.BaseRepository.GetAll(ctx, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
type lastActivityRow struct {
|
||||||
|
ServerID string `gorm:"column:server_id"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at"`
|
||||||
|
Username string `gorm:"column:username"`
|
||||||
|
Action model.ActionType `gorm:"column:action"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastActivityByServerIDs returns the most recent activity entry per server.
|
||||||
|
// Uses a single GROUP BY query with MAX(created_at); SQLite selects the other
|
||||||
|
// columns from the row that holds the max value — documented SQLite behaviour.
|
||||||
|
func (r *ActivityLogRepository) GetLastActivityByServerIDs(ctx context.Context, serverIDs []uuid.UUID) (map[uuid.UUID]*model.LastActivityInfo, error) {
|
||||||
|
out := make(map[uuid.UUID]*model.LastActivityInfo)
|
||||||
|
if len(serverIDs) == 0 {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to string slice so the IN clause works with SQLite UUID strings.
|
||||||
|
ids := make([]string, len(serverIDs))
|
||||||
|
for i, id := range serverIDs {
|
||||||
|
ids[i] = id.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []lastActivityRow
|
||||||
|
err := r.BaseRepository.db.WithContext(ctx).
|
||||||
|
Model(&model.ActivityLog{}).
|
||||||
|
Select("server_id, MAX(created_at) as created_at, username, action").
|
||||||
|
Where("server_id IN ?", ids).
|
||||||
|
Group("server_id").
|
||||||
|
Scan(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
serverUUID, err := uuid.Parse(row.ServerID)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[serverUUID] = &model.LastActivityInfo{
|
||||||
|
UpdatedAt: row.CreatedAt,
|
||||||
|
Username: row.Username,
|
||||||
|
Action: row.Action,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
0
local/repository/base.go
Normal file → Executable file
0
local/repository/base.go
Normal file → Executable file
0
local/repository/config.go
Normal file → Executable file
0
local/repository/config.go
Normal file → Executable file
2
local/repository/leaderboard.go
Normal file → Executable file
2
local/repository/leaderboard.go
Normal file → Executable file
@@ -92,7 +92,6 @@ func (r *LeaderboardRepository) FullReplace(ctx context.Context, serverID uuid.U
|
|||||||
// Re-insert children with correct leaderboard ID
|
// Re-insert children with correct leaderboard ID
|
||||||
for i := range lb.Drivers {
|
for i := range lb.Drivers {
|
||||||
lb.Drivers[i].LeaderboardID = existing.ID
|
lb.Drivers[i].LeaderboardID = existing.ID
|
||||||
lb.Drivers[i].ID = uuid.Nil // force new ID
|
|
||||||
}
|
}
|
||||||
if len(lb.Drivers) > 0 {
|
if len(lb.Drivers) > 0 {
|
||||||
if err := tx.Create(&lb.Drivers).Error; err != nil {
|
if err := tx.Create(&lb.Drivers).Error; err != nil {
|
||||||
@@ -112,7 +111,6 @@ func (r *LeaderboardRepository) FullReplace(ctx context.Context, serverID uuid.U
|
|||||||
|
|
||||||
for i := range lb.Races {
|
for i := range lb.Races {
|
||||||
lb.Races[i].LeaderboardID = existing.ID
|
lb.Races[i].LeaderboardID = existing.ID
|
||||||
lb.Races[i].ID = uuid.Nil
|
|
||||||
}
|
}
|
||||||
if len(lb.Races) > 0 {
|
if len(lb.Races) > 0 {
|
||||||
// Create races first (without results) to get IDs
|
// Create races first (without results) to get IDs
|
||||||
|
|||||||
0
local/repository/lookup.go
Normal file → Executable file
0
local/repository/lookup.go
Normal file → Executable file
0
local/repository/membership.go
Normal file → Executable file
0
local/repository/membership.go
Normal file → Executable file
1
local/repository/repository.go
Normal file → Executable file
1
local/repository/repository.go
Normal file → Executable file
@@ -20,6 +20,7 @@ func InitializeRepositories(c *dig.Container) {
|
|||||||
c.Provide(NewSteamCredentialsRepository)
|
c.Provide(NewSteamCredentialsRepository)
|
||||||
c.Provide(NewMembershipRepository)
|
c.Provide(NewMembershipRepository)
|
||||||
c.Provide(NewLeaderboardRepository)
|
c.Provide(NewLeaderboardRepository)
|
||||||
|
c.Provide(NewActivityLogRepository)
|
||||||
|
|
||||||
if err := c.Provide(func() *model.Steam2FAManager {
|
if err := c.Provide(func() *model.Steam2FAManager {
|
||||||
manager := model.NewSteam2FAManager()
|
manager := model.NewSteam2FAManager()
|
||||||
|
|||||||
0
local/repository/server.go
Normal file → Executable file
0
local/repository/server.go
Normal file → Executable file
0
local/repository/service_control.go
Normal file → Executable file
0
local/repository/service_control.go
Normal file → Executable file
0
local/repository/state_history.go
Normal file → Executable file
0
local/repository/state_history.go
Normal file → Executable file
0
local/repository/steam_credentials.go
Normal file → Executable file
0
local/repository/steam_credentials.go
Normal file → Executable file
62
local/service/activity_log.go
Normal file
62
local/service/activity_log.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/model"
|
||||||
|
"acc-server-manager/local/repository"
|
||||||
|
"acc-server-manager/local/utl/logging"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityLogService struct {
|
||||||
|
repo *repository.ActivityLogRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewActivityLogService(repo *repository.ActivityLogRepository) *ActivityLogService {
|
||||||
|
return &ActivityLogService{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log persists an activity log entry. Errors are logged but not returned so
|
||||||
|
// callers can treat this as fire-and-forget.
|
||||||
|
func (s *ActivityLogService) Log(ctx context.Context, entry *model.ActivityLog) error {
|
||||||
|
if err := s.repo.Insert(ctx, entry); err != nil {
|
||||||
|
logging.Error("Failed to insert activity log: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityLogService) GetAll(ctx context.Context, filter *model.ActivityLogFilter) (*[]model.ActivityLog, error) {
|
||||||
|
return s.repo.GetAll(ctx, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildConfigDetails produces a JSON string summarising which top-level keys
|
||||||
|
// changed between oldJSON and newJSON.
|
||||||
|
func BuildConfigDetails(file string, oldJSON, newJSON []byte) string {
|
||||||
|
var oldMap, newMap map[string]interface{}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(oldJSON, &oldMap); err != nil {
|
||||||
|
oldMap = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(newJSON, &newMap); err != nil {
|
||||||
|
newMap = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var changedKeys []string
|
||||||
|
for key, newVal := range newMap {
|
||||||
|
oldVal, exists := oldMap[key]
|
||||||
|
if !exists {
|
||||||
|
changedKeys = append(changedKeys, key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
oldBytes, _ := json.Marshal(oldVal)
|
||||||
|
newBytes, _ := json.Marshal(newVal)
|
||||||
|
if string(oldBytes) != string(newBytes) {
|
||||||
|
changedKeys = append(changedKeys, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keysJSON, _ := json.Marshal(changedKeys)
|
||||||
|
return fmt.Sprintf(`{"file":%q,"changed_keys":%s}`, file, string(keysJSON))
|
||||||
|
}
|
||||||
112
local/service/config.go
Normal file → Executable file
112
local/service/config.go
Normal file → Executable file
@@ -68,14 +68,16 @@ type ConfigService struct {
|
|||||||
repository *repository.ConfigRepository
|
repository *repository.ConfigRepository
|
||||||
serverRepository *repository.ServerRepository
|
serverRepository *repository.ServerRepository
|
||||||
serverService *ServerService
|
serverService *ServerService
|
||||||
|
activityLog *ActivityLogService
|
||||||
configCache *model.ServerConfigCache
|
configCache *model.ServerConfigCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfigService(repository *repository.ConfigRepository, serverRepository *repository.ServerRepository) *ConfigService {
|
func NewConfigService(repository *repository.ConfigRepository, serverRepository *repository.ServerRepository, activityLog *ActivityLogService) *ConfigService {
|
||||||
logging.Debug("Initializing ConfigService with 5m expiration and 1s throttle")
|
logging.Debug("Initializing ConfigService with 5m expiration and 1s throttle")
|
||||||
return &ConfigService{
|
return &ConfigService{
|
||||||
repository: repository,
|
repository: repository,
|
||||||
serverRepository: serverRepository,
|
serverRepository: serverRepository,
|
||||||
|
activityLog: activityLog,
|
||||||
configCache: model.NewServerConfigCache(model.CacheConfig{
|
configCache: model.NewServerConfigCache(model.CacheConfig{
|
||||||
ExpirationTime: 5 * time.Minute,
|
ExpirationTime: 5 * time.Minute,
|
||||||
ThrottleTime: 1 * time.Second,
|
ThrottleTime: 1 * time.Second,
|
||||||
@@ -91,12 +93,93 @@ func (as *ConfigService) SetServerService(serverService *ServerService) {
|
|||||||
// context.Context: Application context
|
// context.Context: Application context
|
||||||
// Returns:
|
// Returns:
|
||||||
// string: Application version
|
// string: Application version
|
||||||
|
func (as *ConfigService) UpdateAllConfigs(ctx *fiber.Ctx, req *model.ConfigurationsUpdateRequest) ([]*model.Config, error) {
|
||||||
|
serverID := ctx.Locals("serverId").(string)
|
||||||
|
override := ctx.QueryBool("override", false)
|
||||||
|
|
||||||
|
serverUUID, err := uuid.Parse(serverID)
|
||||||
|
if err != nil {
|
||||||
|
logging.Error("Invalid server ID format: %v", err)
|
||||||
|
return nil, fmt.Errorf("invalid server ID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := as.serverRepository.GetByID(ctx.UserContext(), serverUUID)
|
||||||
|
if err != nil {
|
||||||
|
logging.Error("Server not found")
|
||||||
|
return nil, fmt.Errorf("server not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
type configEntry struct {
|
||||||
|
fileName string
|
||||||
|
body *map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := []configEntry{
|
||||||
|
{ConfigurationJson, req.Configuration},
|
||||||
|
{AssistRulesJson, req.AssistRules},
|
||||||
|
{EventJson, req.Event},
|
||||||
|
{EventRulesJson, req.EventRules},
|
||||||
|
{SettingsJson, req.Settings},
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID, _ := ctx.Locals("userID").(string)
|
||||||
|
actorUsername, _ := ctx.Locals("actorUsername").(string)
|
||||||
|
if actorUsername == "" {
|
||||||
|
actorUsername = "system"
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []*model.Config
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.body == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
oldDataUTF8, newData, err := as.updateConfigFiles(ctx.UserContext(), server, entry.fileName, entry.body, override)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := as.repository.UpdateConfig(ctx.UserContext(), &model.Config{
|
||||||
|
ServerID: serverUUID,
|
||||||
|
ConfigFile: entry.fileName,
|
||||||
|
OldConfig: string(oldDataUTF8),
|
||||||
|
NewConfig: string(newData),
|
||||||
|
ChangedAt: time.Now(),
|
||||||
|
})
|
||||||
|
results = append(results, result)
|
||||||
|
|
||||||
|
if as.activityLog != nil {
|
||||||
|
details := BuildConfigDetails(entry.fileName, oldDataUTF8, newData)
|
||||||
|
go func(details string) {
|
||||||
|
_ = as.activityLog.Log(context.Background(), &model.ActivityLog{
|
||||||
|
ServerID: serverUUID,
|
||||||
|
UserID: actorID,
|
||||||
|
Username: actorUsername,
|
||||||
|
Action: model.ActionConfigUpdate,
|
||||||
|
Details: details,
|
||||||
|
})
|
||||||
|
}(details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) > 0 {
|
||||||
|
as.configCache.InvalidateServerCache(serverID)
|
||||||
|
as.serverService.StartAccServerRuntime(server)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (as *ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface{}) (*model.Config, error) {
|
func (as *ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface{}) (*model.Config, error) {
|
||||||
serverID := ctx.Locals("serverId").(string)
|
serverID := ctx.Locals("serverId").(string)
|
||||||
configFile := ctx.Params("file")
|
configFile := ctx.Params("file")
|
||||||
override := ctx.QueryBool("override", false)
|
override := ctx.QueryBool("override", false)
|
||||||
|
|
||||||
return as.updateConfigInternal(ctx.UserContext(), serverID, configFile, body, override)
|
actorID, _ := ctx.Locals("userID").(string)
|
||||||
|
actorUsername, _ := ctx.Locals("actorUsername").(string)
|
||||||
|
if actorUsername == "" {
|
||||||
|
actorUsername = "system"
|
||||||
|
}
|
||||||
|
|
||||||
|
return as.updateConfigInternal(ctx.UserContext(), serverID, configFile, body, override, actorID, actorUsername)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (as *ConfigService) updateConfigFiles(ctx context.Context, server *model.Server, configFile string, body *map[string]interface{}, override bool) ([]byte, []byte, error) {
|
func (as *ConfigService) updateConfigFiles(ctx context.Context, server *model.Server, configFile string, body *map[string]interface{}, override bool) ([]byte, []byte, error) {
|
||||||
@@ -155,7 +238,7 @@ func (as *ConfigService) updateConfigFiles(ctx context.Context, server *model.Se
|
|||||||
return oldDataUTF8, newData, nil
|
return oldDataUTF8, newData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (as *ConfigService) updateConfigInternal(ctx context.Context, serverID string, configFile string, body *map[string]interface{}, override bool) (*model.Config, error) {
|
func (as *ConfigService) updateConfigInternal(ctx context.Context, serverID string, configFile string, body *map[string]interface{}, override bool, actorID, actorUsername string) (*model.Config, error) {
|
||||||
serverUUID, err := uuid.Parse(serverID)
|
serverUUID, err := uuid.Parse(serverID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Error("Invalid server ID format: %v", err)
|
logging.Error("Invalid server ID format: %v", err)
|
||||||
@@ -174,8 +257,21 @@ func (as *ConfigService) updateConfigInternal(ctx context.Context, serverID stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
as.configCache.InvalidateServerCache(serverID)
|
as.configCache.InvalidateServerCache(serverID)
|
||||||
|
|
||||||
as.serverService.StartAccServerRuntime(server)
|
as.serverService.StartAccServerRuntime(server)
|
||||||
|
|
||||||
|
if as.activityLog != nil {
|
||||||
|
details := BuildConfigDetails(configFile, oldDataUTF8, newData)
|
||||||
|
go func() {
|
||||||
|
_ = as.activityLog.Log(context.Background(), &model.ActivityLog{
|
||||||
|
ServerID: serverUUID,
|
||||||
|
UserID: actorID,
|
||||||
|
Username: actorUsername,
|
||||||
|
Action: model.ActionConfigUpdate,
|
||||||
|
Details: details,
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
return as.repository.UpdateConfig(ctx, &model.Config{
|
return as.repository.UpdateConfig(ctx, &model.Config{
|
||||||
ServerID: serverUUID,
|
ServerID: serverUUID,
|
||||||
ConfigFile: configFile,
|
ConfigFile: configFile,
|
||||||
@@ -420,6 +516,14 @@ func (as *ConfigService) GetConfiguration(server *model.Server) (*model.Configur
|
|||||||
return &config, nil
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (as *ConfigService) SetIgnorePrematureDisconnects(server *model.Server, value int) error {
|
||||||
|
configMap := map[string]interface{}{
|
||||||
|
"ignorePrematureDisconnects": value,
|
||||||
|
}
|
||||||
|
_, _, err := as.updateConfigFiles(context.Background(), server, SettingsJson, &configMap, false)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (as *ConfigService) SaveConfiguration(server *model.Server, config *model.Configuration) error {
|
func (as *ConfigService) SaveConfiguration(server *model.Server, config *model.Configuration) error {
|
||||||
configMap := make(map[string]interface{})
|
configMap := make(map[string]interface{})
|
||||||
configBytes, err := json.Marshal(config)
|
configBytes, err := json.Marshal(config)
|
||||||
|
|||||||
74
local/service/docker_log_streamer.go
Normal file
74
local/service/docker_log_streamer.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/model"
|
||||||
|
"acc-server-manager/local/utl/logging"
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
dockerclient "github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/pkg/stdcopy"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DockerLogStreamer struct {
|
||||||
|
client *dockerclient.Client
|
||||||
|
cancels sync.Map // uuid.UUID → context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDockerLogStreamer(client *dockerclient.Client) *DockerLogStreamer {
|
||||||
|
return &DockerLogStreamer{client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DockerLogStreamer) Start(ctx context.Context, server *model.Server, handleLine func(string)) error {
|
||||||
|
if _, exists := s.cancels.Load(server.ID); exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if server.ContainerID == "" {
|
||||||
|
logging.Warn("DockerLogStreamer.Start: no container ID for server %s, skipping", server.ID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
streamCtx, cancel := context.WithCancel(ctx)
|
||||||
|
s.cancels.Store(server.ID, cancel)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer s.cancels.Delete(server.ID)
|
||||||
|
|
||||||
|
rc, err := s.client.ContainerLogs(streamCtx, server.ContainerID, types.ContainerLogsOptions{
|
||||||
|
ShowStdout: true,
|
||||||
|
ShowStderr: true,
|
||||||
|
Follow: true,
|
||||||
|
Timestamps: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logging.Error("DockerLogStreamer: failed to attach to container %s: %v", server.ContainerID[:12], err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rc.Close()
|
||||||
|
|
||||||
|
pr, pw := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
defer pw.Close()
|
||||||
|
if _, err := stdcopy.StdCopy(pw, pw, rc); err != nil && err != io.EOF {
|
||||||
|
logging.Warn("DockerLogStreamer: stdcopy error for %s: %v", server.ServiceName, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(pr)
|
||||||
|
for scanner.Scan() {
|
||||||
|
handleLine(scanner.Text())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DockerLogStreamer) Stop(serverID uuid.UUID) {
|
||||||
|
if v, ok := s.cancels.LoadAndDelete(serverID); ok {
|
||||||
|
v.(context.CancelFunc)()
|
||||||
|
}
|
||||||
|
}
|
||||||
238
local/service/docker_runtime.go
Normal file
238
local/service/docker_runtime.go
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/model"
|
||||||
|
"acc-server-manager/local/repository"
|
||||||
|
"acc-server-manager/local/utl/env"
|
||||||
|
"acc-server-manager/local/utl/logging"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
dockerclient "github.com/docker/docker/client"
|
||||||
|
"github.com/docker/go-connections/nat"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DockerRuntime struct {
|
||||||
|
client *dockerclient.Client
|
||||||
|
repo *repository.ServerRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDockerRuntime(client *dockerclient.Client, repo *repository.ServerRepository) *DockerRuntime {
|
||||||
|
return &DockerRuntime{
|
||||||
|
client: client,
|
||||||
|
repo: repo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DockerRuntime) Create(ctx context.Context, server *model.Server) error {
|
||||||
|
image := env.GetACCImage()
|
||||||
|
// ACC uses the same port number for both TCP (client connections) and UDP (car positions/ping).
|
||||||
|
portStr := strconv.Itoa(server.Port)
|
||||||
|
|
||||||
|
natTCP, err := nat.NewPort("tcp", portStr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid port %d: %v", server.Port, err)
|
||||||
|
}
|
||||||
|
natUDP, err := nat.NewPort("udp", portStr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid port %d: %v", server.Port, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
portBindings := nat.PortMap{
|
||||||
|
natTCP: []nat.PortBinding{{HostPort: portStr}},
|
||||||
|
natUDP: []nat.PortBinding{{HostPort: portStr}},
|
||||||
|
}
|
||||||
|
exposedPorts := nat.PortSet{
|
||||||
|
natTCP: struct{}{},
|
||||||
|
natUDP: struct{}{},
|
||||||
|
}
|
||||||
|
|
||||||
|
containerBase := env.GetACCServersPath()
|
||||||
|
hostBase := env.GetACCServersHostPath()
|
||||||
|
serverSubPath := strings.TrimPrefix(server.GetServerPath(), containerBase)
|
||||||
|
hostServerPath := filepath.Join(hostBase, serverSubPath)
|
||||||
|
|
||||||
|
binds := []string{
|
||||||
|
fmt.Sprintf("%s:/acc/game:rw", hostServerPath),
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := r.client.ContainerCreate(ctx, &container.Config{
|
||||||
|
Image: image,
|
||||||
|
ExposedPorts: exposedPorts,
|
||||||
|
Env: []string{
|
||||||
|
fmt.Sprintf("SERVER_PORT=%d", server.Port),
|
||||||
|
},
|
||||||
|
}, &container.HostConfig{
|
||||||
|
Binds: binds,
|
||||||
|
PortBindings: portBindings,
|
||||||
|
RestartPolicy: container.RestartPolicy{Name: "unless-stopped"},
|
||||||
|
// NET_RAW is required for tcpdump packet capture in debug mode (DEBUG_MODE=1).
|
||||||
|
CapAdd: []string{"NET_RAW"},
|
||||||
|
}, nil, nil, server.ServiceName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create container %s: %v", server.ServiceName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server.ContainerID = resp.ID
|
||||||
|
logging.Info("Created Docker container %s (ID: %s) for server %s", server.ServiceName, resp.ID[:12], server.ID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DockerRuntime) Delete(ctx context.Context, serverID uuid.UUID) error {
|
||||||
|
containerID, err := r.resolveContainerID(ctx, serverID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if containerID == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.client.ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{
|
||||||
|
Force: true,
|
||||||
|
RemoveVolumes: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to remove container %s: %v", containerID[:12], err)
|
||||||
|
}
|
||||||
|
logging.Info("Removed Docker container %s for server %s", containerID[:12], serverID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DockerRuntime) Start(ctx context.Context, serverID uuid.UUID) (string, error) {
|
||||||
|
server, err := r.repo.GetByID(ctx, serverID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to look up server %s: %v", serverID, err)
|
||||||
|
}
|
||||||
|
if server == nil {
|
||||||
|
return "", fmt.Errorf("server %s not found", serverID)
|
||||||
|
}
|
||||||
|
|
||||||
|
containerID, err := r.ensureContainer(ctx, server)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.client.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to start container: %v", err)
|
||||||
|
}
|
||||||
|
return model.StatusRunning.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DockerRuntime) Stop(ctx context.Context, serverID uuid.UUID) (string, error) {
|
||||||
|
containerID, err := r.resolveContainerID(ctx, serverID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := r.client.ContainerStop(ctx, containerID, container.StopOptions{}); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to stop container: %v", err)
|
||||||
|
}
|
||||||
|
return model.StatusStopped.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DockerRuntime) Restart(ctx context.Context, serverID uuid.UUID) (string, error) {
|
||||||
|
server, err := r.repo.GetByID(ctx, serverID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to look up server %s: %v", serverID, err)
|
||||||
|
}
|
||||||
|
if server == nil {
|
||||||
|
return "", fmt.Errorf("server %s not found", serverID)
|
||||||
|
}
|
||||||
|
|
||||||
|
containerID, err := r.ensureContainer(ctx, server)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.client.ContainerRestart(ctx, containerID, container.StopOptions{}); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to restart container: %v", err)
|
||||||
|
}
|
||||||
|
return model.StatusRunning.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureContainer returns the container ID for a server, recreating the container
|
||||||
|
// from the server's current DB record if it no longer exists in Docker.
|
||||||
|
func (r *DockerRuntime) ensureContainer(ctx context.Context, server *model.Server) (string, error) {
|
||||||
|
if server.ContainerID != "" {
|
||||||
|
if _, err := r.client.ContainerInspect(ctx, server.ContainerID); err == nil {
|
||||||
|
return server.ContainerID, nil
|
||||||
|
}
|
||||||
|
logging.Info("Container %s for server %s no longer exists, recreating", server.ContainerID[:min(12, len(server.ContainerID))], server.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.Create(ctx, server); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to recreate container: %v", err)
|
||||||
|
}
|
||||||
|
if err := r.repo.Update(ctx, server); err != nil {
|
||||||
|
return "", fmt.Errorf("container recreated but failed to persist container ID: %v", err)
|
||||||
|
}
|
||||||
|
return server.ContainerID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DockerRuntime) Status(ctx context.Context, serverID uuid.UUID) (string, error) {
|
||||||
|
server, err := r.repo.GetByID(ctx, serverID)
|
||||||
|
if err != nil || server == nil {
|
||||||
|
return model.StatusUnknown.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
containerID := server.ContainerID
|
||||||
|
if containerID == "" {
|
||||||
|
containerID, err = r.findContainerByName(ctx, server.ServiceName)
|
||||||
|
if err != nil || containerID == "" {
|
||||||
|
return model.StatusStopped.String(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := r.client.ContainerInspect(ctx, containerID)
|
||||||
|
if err != nil {
|
||||||
|
return model.StatusUnknown.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return dockerStateToStatus(info.State.Status), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DockerRuntime) resolveContainerID(ctx context.Context, serverID uuid.UUID) (string, error) {
|
||||||
|
server, err := r.repo.GetByID(ctx, serverID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to look up server %s: %v", serverID, err)
|
||||||
|
}
|
||||||
|
if server == nil {
|
||||||
|
return "", fmt.Errorf("server %s not found", serverID)
|
||||||
|
}
|
||||||
|
return server.ContainerID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DockerRuntime) findContainerByName(ctx context.Context, name string) (string, error) {
|
||||||
|
containers, err := r.client.ContainerList(ctx, types.ContainerListOptions{
|
||||||
|
All: true,
|
||||||
|
Filters: filters.NewArgs(filters.Arg("name", "/"+name)),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(containers) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return containers[0].ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dockerStateToStatus(state string) string {
|
||||||
|
switch state {
|
||||||
|
case "running":
|
||||||
|
return model.StatusRunning.String()
|
||||||
|
case "exited", "dead":
|
||||||
|
return model.StatusStopped.String()
|
||||||
|
case "restarting":
|
||||||
|
return model.StatusRestarting.String()
|
||||||
|
case "paused":
|
||||||
|
return model.StatusStopped.String()
|
||||||
|
default:
|
||||||
|
return model.StatusUnknown.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
38
local/service/file_log_streamer.go
Normal file
38
local/service/file_log_streamer.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/model"
|
||||||
|
"acc-server-manager/local/utl/tracking"
|
||||||
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileLogStreamer struct {
|
||||||
|
tailers sync.Map // uuid.UUID → *tracking.LogTailer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFileLogStreamer() *FileLogStreamer {
|
||||||
|
return &FileLogStreamer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FileLogStreamer) Start(_ context.Context, server *model.Server, handleLine func(string)) error {
|
||||||
|
if _, exists := s.tailers.Load(server.ID); exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logPath := filepath.Join(server.GetLogPath(), "server.log")
|
||||||
|
tailer := tracking.NewLogTailer(logPath, handleLine)
|
||||||
|
s.tailers.Store(server.ID, tailer)
|
||||||
|
|
||||||
|
go tailer.Start()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FileLogStreamer) Stop(serverID uuid.UUID) {
|
||||||
|
if v, exists := s.tailers.LoadAndDelete(serverID); exists {
|
||||||
|
v.(*tracking.LogTailer).Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
0
local/service/firewall_service.go
Normal file → Executable file
0
local/service/firewall_service.go
Normal file → Executable file
204
local/service/leaderboard.go
Normal file → Executable file
204
local/service/leaderboard.go
Normal file → Executable file
@@ -3,6 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"acc-server-manager/local/model"
|
"acc-server-manager/local/model"
|
||||||
"acc-server-manager/local/repository"
|
"acc-server-manager/local/repository"
|
||||||
|
"acc-server-manager/local/utl/logging"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
@@ -11,195 +12,48 @@ import (
|
|||||||
|
|
||||||
type LeaderboardService struct {
|
type LeaderboardService struct {
|
||||||
repo *repository.LeaderboardRepository
|
repo *repository.LeaderboardRepository
|
||||||
|
activityLog *ActivityLogService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLeaderboardService(repo *repository.LeaderboardRepository) *LeaderboardService {
|
func NewLeaderboardService(repo *repository.LeaderboardRepository, activityLog *ActivityLogService) *LeaderboardService {
|
||||||
return &LeaderboardService{repo: repo}
|
return &LeaderboardService{repo: repo, activityLog: activityLog}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the leaderboard for a server in the wire format (LeaderboardInput).
|
// Get returns the leaderboard for a server.
|
||||||
func (s *LeaderboardService) Get(ctx context.Context, serverID uuid.UUID) (*model.LeaderboardInput, error) {
|
func (s *LeaderboardService) Get(ctx context.Context, serverID uuid.UUID) (*model.Leaderboard, error) {
|
||||||
lb, err := s.repo.GetOrCreateByServerID(ctx, serverID)
|
return s.repo.GetOrCreateByServerID(ctx, serverID)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return toInput(lb), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update replaces the leaderboard for a server from wire format input.
|
// Update replaces the leaderboard for a server.
|
||||||
func (s *LeaderboardService) Update(ctx context.Context, serverID uuid.UUID, input *model.LeaderboardInput) (*model.LeaderboardInput, error) {
|
func (s *LeaderboardService) Update(ctx context.Context, serverID uuid.UUID, lb *model.Leaderboard, actorID, actorUsername string) (*model.Leaderboard, error) {
|
||||||
lb, err := fromInput(serverID, input)
|
if lb == nil {
|
||||||
if err != nil {
|
return nil, fmt.Errorf("input is required")
|
||||||
return nil, err
|
}
|
||||||
|
for i := range lb.Drivers {
|
||||||
|
lb.Drivers[i].Position = i
|
||||||
|
}
|
||||||
|
for i := range lb.Races {
|
||||||
|
lb.Races[i].Position = i
|
||||||
}
|
}
|
||||||
result, err := s.repo.FullReplace(ctx, serverID, lb)
|
result, err := s.repo.FullReplace(ctx, serverID, lb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return toInput(result), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// toInput converts the normalized DB model to the flat wire format.
|
|
||||||
func toInput(lb *model.Leaderboard) *model.LeaderboardInput {
|
|
||||||
// Build driver map (id -> index in slice) for result ordering
|
|
||||||
driverIndexByID := make(map[uuid.UUID]int, len(lb.Drivers))
|
|
||||||
|
|
||||||
// Sort drivers by position
|
|
||||||
drivers := make([]model.LeaderboardDriverInput, len(lb.Drivers))
|
|
||||||
for i, d := range lb.Drivers {
|
|
||||||
drivers[i] = model.LeaderboardDriverInput{
|
|
||||||
Name: d.Name,
|
|
||||||
Color: d.Color,
|
|
||||||
Initials: d.Initials,
|
|
||||||
}
|
|
||||||
driverIndexByID[d.ID] = d.Position
|
|
||||||
}
|
|
||||||
// Re-index by position order
|
|
||||||
driverByPos := make(map[int]model.LeaderboardDriver, len(lb.Drivers))
|
|
||||||
for _, d := range lb.Drivers {
|
|
||||||
driverByPos[d.Position] = d
|
|
||||||
}
|
|
||||||
driverCount := len(lb.Drivers)
|
driverCount := len(lb.Drivers)
|
||||||
orderedDrivers := make([]model.LeaderboardDriver, driverCount)
|
raceCount := len(lb.Races)
|
||||||
for i := 0; i < driverCount; i++ {
|
details := fmt.Sprintf(`{"driver_count":%d,"race_count":%d}`, driverCount, raceCount)
|
||||||
if d, ok := driverByPos[i]; ok {
|
go func() {
|
||||||
orderedDrivers[i] = d
|
if logErr := s.activityLog.Log(context.Background(), &model.ActivityLog{
|
||||||
drivers[i] = model.LeaderboardDriverInput{
|
|
||||||
Name: d.Name,
|
|
||||||
Color: d.Color,
|
|
||||||
Initials: d.Initials,
|
|
||||||
}
|
|
||||||
driverIndexByID[d.ID] = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build tracks
|
|
||||||
raceByPos := make(map[int]model.LeaderboardRace, len(lb.Races))
|
|
||||||
for _, r := range lb.Races {
|
|
||||||
raceByPos[r.Position] = r
|
|
||||||
}
|
|
||||||
tracks := make([]model.LeaderboardTrackInput, len(lb.Races))
|
|
||||||
for i := 0; i < len(lb.Races); i++ {
|
|
||||||
race, ok := raceByPos[i]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
results := make([]interface{}, driverCount)
|
|
||||||
for j := range results {
|
|
||||||
results[j] = "0"
|
|
||||||
}
|
|
||||||
for _, res := range race.Results {
|
|
||||||
idx, found := driverIndexByID[res.DriverID]
|
|
||||||
if found && idx < driverCount {
|
|
||||||
results[idx] = res.Score
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tracks[i] = model.LeaderboardTrackInput{
|
|
||||||
Name: race.Name,
|
|
||||||
Results: results,
|
|
||||||
FastestLapInitials: race.FastestLapInitials,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build points table
|
|
||||||
pointsTable := make([]model.LeaderboardPointRowInput, len(lb.PointRows))
|
|
||||||
for i, p := range lb.PointRows {
|
|
||||||
pointsTable[i] = model.LeaderboardPointRowInput{
|
|
||||||
Points: p.Points,
|
|
||||||
Label: p.Label,
|
|
||||||
Color: p.Color,
|
|
||||||
TextColor: p.TextColor,
|
|
||||||
Priority: p.Priority,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flColor := lb.FLColor
|
|
||||||
if flColor == "" {
|
|
||||||
flColor = "#8b5cf6"
|
|
||||||
}
|
|
||||||
flTextColor := lb.FLTextColor
|
|
||||||
if flTextColor == "" {
|
|
||||||
flTextColor = "#000000"
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.LeaderboardInput{
|
|
||||||
Drivers: drivers,
|
|
||||||
PointsTable: pointsTable,
|
|
||||||
FLPoints: model.LeaderboardFLInput{
|
|
||||||
Points: lb.FLPoints,
|
|
||||||
Label: "FL +1",
|
|
||||||
Color: flColor,
|
|
||||||
TextColor: flTextColor,
|
|
||||||
Priority: len(pointsTable) + 1,
|
|
||||||
},
|
|
||||||
Tracks: tracks,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fromInput converts the wire format to the normalized DB model.
|
|
||||||
func fromInput(serverID uuid.UUID, input *model.LeaderboardInput) (*model.Leaderboard, error) {
|
|
||||||
if input == nil {
|
|
||||||
return nil, fmt.Errorf("input is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
lb := &model.Leaderboard{
|
|
||||||
ServerID: serverID,
|
ServerID: serverID,
|
||||||
FLPoints: input.FLPoints.Points,
|
UserID: actorID,
|
||||||
FLColor: input.FLPoints.Color,
|
Username: actorUsername,
|
||||||
FLTextColor: input.FLPoints.TextColor,
|
Action: model.ActionLeaderboardUpdate,
|
||||||
|
Details: details,
|
||||||
|
}); logErr != nil {
|
||||||
|
logging.Error("Failed to log leaderboard update: %v", logErr)
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Build drivers
|
return result, nil
|
||||||
drivers := make([]model.LeaderboardDriver, len(input.Drivers))
|
|
||||||
driverIDs := make([]uuid.UUID, len(input.Drivers))
|
|
||||||
for i, d := range input.Drivers {
|
|
||||||
id := uuid.New()
|
|
||||||
driverIDs[i] = id
|
|
||||||
drivers[i] = model.LeaderboardDriver{
|
|
||||||
ID: id,
|
|
||||||
Name: d.Name,
|
|
||||||
Initials: d.Initials,
|
|
||||||
Color: d.Color,
|
|
||||||
Position: i,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lb.Drivers = drivers
|
|
||||||
|
|
||||||
// Build point rows
|
|
||||||
pointRows := make([]model.LeaderboardPointRow, len(input.PointsTable))
|
|
||||||
for i, p := range input.PointsTable {
|
|
||||||
pointRows[i] = model.LeaderboardPointRow{
|
|
||||||
Label: p.Label,
|
|
||||||
Points: p.Points,
|
|
||||||
Color: p.Color,
|
|
||||||
TextColor: p.TextColor,
|
|
||||||
Priority: p.Priority,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lb.PointRows = pointRows
|
|
||||||
|
|
||||||
// Build races
|
|
||||||
races := make([]model.LeaderboardRace, len(input.Tracks))
|
|
||||||
for i, t := range input.Tracks {
|
|
||||||
results := make([]model.LeaderboardResult, 0, len(t.Results))
|
|
||||||
for j, score := range t.Results {
|
|
||||||
if j >= len(driverIDs) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
scoreStr := fmt.Sprintf("%v", score)
|
|
||||||
results = append(results, model.LeaderboardResult{
|
|
||||||
DriverID: driverIDs[j],
|
|
||||||
Score: scoreStr,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
races[i] = model.LeaderboardRace{
|
|
||||||
Name: t.Name,
|
|
||||||
Position: i,
|
|
||||||
FastestLapInitials: t.FastestLapInitials,
|
|
||||||
Results: results,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lb.Races = races
|
|
||||||
|
|
||||||
return lb, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
0
local/service/lookup.go
Normal file → Executable file
0
local/service/lookup.go
Normal file → Executable file
0
local/service/membership.go
Normal file → Executable file
0
local/service/membership.go
Normal file → Executable file
7
local/service/noop_firewall.go
Normal file
7
local/service/noop_firewall.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
type NoOpFirewall struct{}
|
||||||
|
|
||||||
|
func (f *NoOpFirewall) CreateServerRules(_ string, _, _ []int) error { return nil }
|
||||||
|
func (f *NoOpFirewall) DeleteServerRules(_ string, _, _ []int) error { return nil }
|
||||||
|
func (f *NoOpFirewall) UpdateServerRules(_ string, _, _ []int) error { return nil }
|
||||||
143
local/service/server.go
Normal file → Executable file
143
local/service/server.go
Normal file → Executable file
@@ -2,9 +2,11 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"acc-server-manager/local/model"
|
"acc-server-manager/local/model"
|
||||||
|
"acc-server-manager/local/platform"
|
||||||
"acc-server-manager/local/repository"
|
"acc-server-manager/local/repository"
|
||||||
"acc-server-manager/local/utl/env"
|
"acc-server-manager/local/utl/env"
|
||||||
"acc-server-manager/local/utl/logging"
|
"acc-server-manager/local/utl/logging"
|
||||||
|
"acc-server-manager/local/utl/network"
|
||||||
"acc-server-manager/local/utl/tracking"
|
"acc-server-manager/local/utl/tracking"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -12,8 +14,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"acc-server-manager/local/utl/network"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
@@ -26,17 +26,19 @@ const (
|
|||||||
type ServerService struct {
|
type ServerService struct {
|
||||||
repository *repository.ServerRepository
|
repository *repository.ServerRepository
|
||||||
stateHistoryRepo *repository.StateHistoryRepository
|
stateHistoryRepo *repository.StateHistoryRepository
|
||||||
|
activityLogRepo *repository.ActivityLogRepository
|
||||||
apiService *ServiceControlService
|
apiService *ServiceControlService
|
||||||
configService *ConfigService
|
configService *ConfigService
|
||||||
steamService *SteamService
|
steamService *SteamService
|
||||||
windowsService *WindowsService
|
runtime platform.ServerRuntime
|
||||||
firewallService *FirewallService
|
firewall platform.FirewallManager
|
||||||
|
logStreamer platform.LogStreamer
|
||||||
|
portPool *network.PortPoolManager
|
||||||
webSocketService *WebSocketService
|
webSocketService *WebSocketService
|
||||||
instances sync.Map // Track instances per server
|
instances sync.Map // uuid.UUID → *tracking.AccServerInstance
|
||||||
lastInsertTimes sync.Map // Track last insert time per server
|
lastInsertTimes sync.Map // uuid.UUID → time.Time
|
||||||
debouncers sync.Map // Track debounce timers per server
|
debouncers sync.Map // uuid.UUID → *pendingState
|
||||||
logTailers sync.Map // Track log tailers per server
|
sessionIDs sync.Map // uuid.UUID → uuid.UUID
|
||||||
sessionIDs sync.Map // Track current session ID per server
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type pendingState struct {
|
type pendingState struct {
|
||||||
@@ -45,37 +47,37 @@ type pendingState struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerService) ensureLogTailing(server *model.Server, instance *tracking.AccServerInstance) {
|
func (s *ServerService) ensureLogTailing(server *model.Server, instance *tracking.AccServerInstance) {
|
||||||
if _, exists := s.logTailers.Load(server.ID); exists {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
logPath := filepath.Join(server.GetLogPath(), "server.log")
|
if err := s.logStreamer.Start(context.Background(), server, instance.HandleLogLine); err != nil {
|
||||||
tailer := tracking.NewLogTailer(logPath, instance.HandleLogLine)
|
logging.Error("Failed to start log streaming for server %s: %v", server.ID, err)
|
||||||
s.logTailers.Store(server.ID, tailer)
|
}
|
||||||
|
|
||||||
tailer.Start()
|
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServerService(
|
func NewServerService(
|
||||||
repository *repository.ServerRepository,
|
repository *repository.ServerRepository,
|
||||||
stateHistoryRepo *repository.StateHistoryRepository,
|
stateHistoryRepo *repository.StateHistoryRepository,
|
||||||
|
activityLogRepo *repository.ActivityLogRepository,
|
||||||
apiService *ServiceControlService,
|
apiService *ServiceControlService,
|
||||||
configService *ConfigService,
|
configService *ConfigService,
|
||||||
steamService *SteamService,
|
steamService *SteamService,
|
||||||
windowsService *WindowsService,
|
runtime platform.ServerRuntime,
|
||||||
firewallService *FirewallService,
|
firewall platform.FirewallManager,
|
||||||
|
logStreamer platform.LogStreamer,
|
||||||
|
portPool *network.PortPoolManager,
|
||||||
webSocketService *WebSocketService,
|
webSocketService *WebSocketService,
|
||||||
) *ServerService {
|
) *ServerService {
|
||||||
service := &ServerService{
|
service := &ServerService{
|
||||||
repository: repository,
|
repository: repository,
|
||||||
stateHistoryRepo: stateHistoryRepo,
|
stateHistoryRepo: stateHistoryRepo,
|
||||||
|
activityLogRepo: activityLogRepo,
|
||||||
apiService: apiService,
|
apiService: apiService,
|
||||||
configService: configService,
|
configService: configService,
|
||||||
steamService: steamService,
|
steamService: steamService,
|
||||||
windowsService: windowsService,
|
runtime: runtime,
|
||||||
firewallService: firewallService,
|
firewall: firewall,
|
||||||
|
logStreamer: logStreamer,
|
||||||
|
portPool: portPool,
|
||||||
webSocketService: webSocketService,
|
webSocketService: webSocketService,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,10 +197,20 @@ func (s *ServerService) updateSessionDuration(server *model.Server, sessionType
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerService) GenerateServerPath(server *model.Server) {
|
func (s *ServerService) GenerateServerPath(server *model.Server) {
|
||||||
|
server.GenerateUUID()
|
||||||
|
if server.ServiceName == "" {
|
||||||
|
server.ServiceName = server.GenerateServiceName()
|
||||||
|
}
|
||||||
|
|
||||||
|
if env.IsDockerPlatform() {
|
||||||
|
server.Path = filepath.Join(env.GetACCServersPath(), server.ServiceName)
|
||||||
|
server.FromSteamCMD = true
|
||||||
|
} else {
|
||||||
steamCMDPath := env.GetSteamCMDDirPath()
|
steamCMDPath := env.GetSteamCMDDirPath()
|
||||||
server.Path = server.GenerateServerPath(steamCMDPath)
|
server.Path = server.GenerateServerPath(steamCMDPath)
|
||||||
server.FromSteamCMD = true
|
server.FromSteamCMD = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ServerService) handleStateChange(server *model.Server, state *model.ServerState) {
|
func (s *ServerService) handleStateChange(server *model.Server, state *model.ServerState) {
|
||||||
s.updateSessionDuration(server, state.Session)
|
s.updateSessionDuration(server, state.Session)
|
||||||
@@ -250,9 +262,6 @@ func (s *ServerService) StartAccServerRuntime(server *model.Server) {
|
|||||||
s.ensureLogTailing(server, instance)
|
s.ensureLogTailing(server, instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
// context.Context: Application context
|
|
||||||
// Returns:
|
|
||||||
// string: Application version
|
|
||||||
func (s *ServerService) GetAll(ctx *fiber.Ctx, filter *model.ServerFilter) (*[]model.Server, error) {
|
func (s *ServerService) GetAll(ctx *fiber.Ctx, filter *model.ServerFilter) (*[]model.Server, error) {
|
||||||
servers, err := s.repository.GetAll(ctx.UserContext(), filter)
|
servers, err := s.repository.GetAll(ctx.UserContext(), filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -260,6 +269,16 @@ func (s *ServerService) GetAll(ctx *fiber.Ctx, filter *model.ServerFilter) (*[]m
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collect server IDs for a single last-activity lookup.
|
||||||
|
serverIDs := make([]uuid.UUID, len(*servers))
|
||||||
|
for i, srv := range *servers {
|
||||||
|
serverIDs[i] = srv.ID
|
||||||
|
}
|
||||||
|
lastActivities, laErr := s.activityLogRepo.GetLastActivityByServerIDs(ctx.UserContext(), serverIDs)
|
||||||
|
if laErr != nil {
|
||||||
|
logging.Error("Failed to get last activities for server list: %v", laErr)
|
||||||
|
}
|
||||||
|
|
||||||
for i := range *servers {
|
for i := range *servers {
|
||||||
server := &(*servers)[i]
|
server := &(*servers)[i]
|
||||||
status, err := s.apiService.GetCachedStatus(server.ServiceName)
|
status, err := s.apiService.GetCachedStatus(server.ServiceName)
|
||||||
@@ -276,14 +295,16 @@ func (s *ServerService) GetAll(ctx *fiber.Ctx, filter *model.ServerFilter) (*[]m
|
|||||||
server.State = serverInstance.State
|
server.State = serverInstance.State
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if lastActivities != nil {
|
||||||
|
if info, ok := lastActivities[server.ID]; ok {
|
||||||
|
server.LastActivity = info
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return servers, nil
|
return servers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// context.Context: Application context
|
|
||||||
// Returns:
|
|
||||||
// string: Application version
|
|
||||||
func (as *ServerService) GetById(ctx *fiber.Ctx, serverID uuid.UUID) (*model.Server, error) {
|
func (as *ServerService) GetById(ctx *fiber.Ctx, serverID uuid.UUID) (*model.Server, error) {
|
||||||
server, err := as.repository.GetByID(ctx.UserContext(), serverID)
|
server, err := as.repository.GetByID(ctx.UserContext(), serverID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -291,7 +312,7 @@ func (as *ServerService) GetById(ctx *fiber.Ctx, serverID uuid.UUID) (*model.Ser
|
|||||||
}
|
}
|
||||||
status, err := as.apiService.GetCachedStatus(server.ServiceName)
|
status, err := as.apiService.GetCachedStatus(server.ServiceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Error(err.Error())
|
logging.Error("Failed to get cached status: %v", err)
|
||||||
}
|
}
|
||||||
server.Status = model.ParseServiceStatus(status)
|
server.Status = model.ParseServiceStatus(status)
|
||||||
instance, ok := as.instances.Load(server.ID)
|
instance, ok := as.instances.Load(server.ID)
|
||||||
@@ -377,12 +398,24 @@ func (s *ServerService) createServerBackground(ctx context.Context, server *mode
|
|||||||
important: true,
|
important: true,
|
||||||
description: "",
|
description: "",
|
||||||
callback: func() (string, error) {
|
callback: func() (string, error) {
|
||||||
|
var port int
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if env.IsDockerPlatform() {
|
||||||
|
port, err = s.portPool.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to acquire port from pool: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
ports, err := network.FindAvailablePortRange(DefaultStartPort, RequiredPortCount)
|
ports, err := network.FindAvailablePortRange(DefaultStartPort, RequiredPortCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to find available ports: %v", err)
|
return "", fmt.Errorf("failed to find available ports: %v", err)
|
||||||
}
|
}
|
||||||
|
port = ports[0]
|
||||||
|
}
|
||||||
|
|
||||||
serverPort = ports[0]
|
serverPort = port
|
||||||
|
server.Port = port
|
||||||
|
|
||||||
if err := s.updateServerPort(server, serverPort); err != nil {
|
if err := s.updateServerPort(server, serverPort); err != nil {
|
||||||
return "", fmt.Errorf("failed to update server configuration: %v", err)
|
return "", fmt.Errorf("failed to update server configuration: %v", err)
|
||||||
@@ -396,12 +429,11 @@ func (s *ServerService) createServerBackground(ctx context.Context, server *mode
|
|||||||
important: true,
|
important: true,
|
||||||
description: "",
|
description: "",
|
||||||
callback: func() (string, error) {
|
callback: func() (string, error) {
|
||||||
execPath := filepath.Join(server.GetServerPath(), "accServer.exe")
|
server.Platform = env.GetPlatform()
|
||||||
serverWorkingDir := filepath.Join(server.GetServerPath(), "server")
|
if err := s.runtime.Create(ctx, server); err != nil {
|
||||||
if err := s.windowsService.CreateService(ctx, server.ServiceName, execPath, serverWorkingDir, nil); err != nil {
|
return "", fmt.Errorf("failed to create server instance: %v", err)
|
||||||
return "", fmt.Errorf("failed to create Windows service: %v", err)
|
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("Windows service '%s' created successfully", server.ServiceName), nil
|
return fmt.Sprintf("Server instance '%s' created successfully", server.ServiceName), nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -409,13 +441,12 @@ func (s *ServerService) createServerBackground(ctx context.Context, server *mode
|
|||||||
important: false,
|
important: false,
|
||||||
description: "",
|
description: "",
|
||||||
callback: func() (string, error) {
|
callback: func() (string, error) {
|
||||||
s.configureFirewall(server)
|
|
||||||
tcpPorts = []int{serverPort}
|
tcpPorts = []int{serverPort}
|
||||||
udpPorts = []int{serverPort}
|
udpPorts = []int{serverPort}
|
||||||
if err := s.firewallService.CreateServerRules(server.ServiceName, tcpPorts, udpPorts); err != nil {
|
if err := s.firewall.CreateServerRules(server.ServiceName, tcpPorts, udpPorts); err != nil {
|
||||||
return "", fmt.Errorf("failed to create firewall rules: %v", err)
|
return "", fmt.Errorf("failed to create firewall rules: %v", err)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("Firewall rules created for port %d", serverPort), nil
|
return fmt.Sprintf("Firewall rules configured for port %d", serverPort), nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -469,12 +500,18 @@ func (s *ServerService) rollbackSteps(ctx context.Context, server *model.Server,
|
|||||||
s.repository.Delete(ctx, server.ID)
|
s.repository.Delete(ctx, server.ID)
|
||||||
case model.StepFirewallRules:
|
case model.StepFirewallRules:
|
||||||
if len(tcpPorts) > 0 && len(udpPorts) > 0 {
|
if len(tcpPorts) > 0 && len(udpPorts) > 0 {
|
||||||
s.firewallService.DeleteServerRules(server.ServiceName, tcpPorts, udpPorts)
|
s.firewall.DeleteServerRules(server.ServiceName, tcpPorts, udpPorts)
|
||||||
}
|
}
|
||||||
case model.StepServiceCreation:
|
case model.StepServiceCreation:
|
||||||
s.windowsService.DeleteService(ctx, server.ServiceName)
|
s.runtime.Delete(ctx, server.ID)
|
||||||
case model.StepSteamDownload:
|
case model.StepSteamDownload:
|
||||||
s.steamService.UninstallServer(server.Path)
|
s.steamService.UninstallServer(server.Path)
|
||||||
|
case model.StepDirectoryCreation:
|
||||||
|
s.steamService.UninstallServer(server.Path)
|
||||||
|
case model.StepConfigGeneration:
|
||||||
|
if env.IsDockerPlatform() && server.Port > 0 {
|
||||||
|
s.portPool.Release(ctx, server.Port)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -485,32 +522,35 @@ func (s *ServerService) DeleteServer(ctx *fiber.Ctx, serverID uuid.UUID) error {
|
|||||||
return fmt.Errorf("failed to get server details: %v", err)
|
return fmt.Errorf("failed to get server details: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.windowsService.DeleteService(ctx.UserContext(), server.ServiceName); err != nil {
|
if err := s.runtime.Delete(ctx.UserContext(), server.ID); err != nil {
|
||||||
logging.Error("Failed to delete Windows service: %v", err)
|
logging.Error("Failed to delete server instance: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
configuration, err := s.configService.GetConfiguration(server)
|
configuration, err := s.configService.GetConfiguration(server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Error("Failed to get configuration for server %d: %v", server.ID, err)
|
logging.Error("Failed to get configuration for server %d: %v", server.ID, err)
|
||||||
}
|
}
|
||||||
|
if configuration != nil {
|
||||||
tcpPorts := []int{configuration.TcpPort.ToInt()}
|
tcpPorts := []int{configuration.TcpPort.ToInt()}
|
||||||
udpPorts := []int{configuration.UdpPort.ToInt()}
|
udpPorts := []int{configuration.UdpPort.ToInt()}
|
||||||
if err := s.firewallService.DeleteServerRules(server.ServiceName, tcpPorts, udpPorts); err != nil {
|
if err := s.firewall.DeleteServerRules(server.ServiceName, tcpPorts, udpPorts); err != nil {
|
||||||
logging.Error("Failed to delete firewall rules: %v", err)
|
logging.Error("Failed to delete firewall rules: %v", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.steamService.UninstallServer(server.Path); err != nil {
|
if err := s.steamService.UninstallServer(server.Path); err != nil {
|
||||||
logging.Error("Failed to uninstall server: %v", err)
|
logging.Error("Failed to uninstall server: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if env.IsDockerPlatform() && server.Port > 0 {
|
||||||
|
s.portPool.Release(ctx.UserContext(), server.Port)
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.repository.Delete(ctx.UserContext(), serverID); err != nil {
|
if err := s.repository.Delete(ctx.UserContext(), serverID); err != nil {
|
||||||
return fmt.Errorf("failed to delete server from database: %v", err)
|
return fmt.Errorf("failed to delete server from database: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tailer, exists := s.logTailers.Load(server.ID); exists {
|
s.logStreamer.Stop(server.ID)
|
||||||
tailer.(*tracking.LogTailer).Stop()
|
|
||||||
s.logTailers.Delete(server.ID)
|
|
||||||
}
|
|
||||||
s.instances.Delete(server.ID)
|
s.instances.Delete(server.ID)
|
||||||
s.lastInsertTimes.Delete(server.ID)
|
s.lastInsertTimes.Delete(server.ID)
|
||||||
s.debouncers.Delete(server.ID)
|
s.debouncers.Delete(server.ID)
|
||||||
@@ -533,7 +573,7 @@ func (s *ServerService) configureFirewall(server *model.Server) error {
|
|||||||
|
|
||||||
logging.Info("Configuring firewall for server %d with port %d", server.ID, serverPort)
|
logging.Info("Configuring firewall for server %d with port %d", server.ID, serverPort)
|
||||||
|
|
||||||
if err := s.firewallService.UpdateServerRules(server.Name, tcpPorts, udpPorts); err != nil {
|
if err := s.firewall.UpdateServerRules(server.Name, tcpPorts, udpPorts); err != nil {
|
||||||
return fmt.Errorf("failed to configure firewall: %v", err)
|
return fmt.Errorf("failed to configure firewall: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,10 +592,17 @@ func (s *ServerService) updateServerPort(server *model.Server, port int) error {
|
|||||||
|
|
||||||
config.TcpPort = model.IntString(port)
|
config.TcpPort = model.IntString(port)
|
||||||
config.UdpPort = model.IntString(port)
|
config.UdpPort = model.IntString(port)
|
||||||
|
config.RegisterToLobby = model.IntString(1)
|
||||||
|
|
||||||
if err := s.configService.SaveConfiguration(server, config); err != nil {
|
if err := s.configService.SaveConfiguration(server, config); err != nil {
|
||||||
return fmt.Errorf("failed to save server configuration: %v", err)
|
return fmt.Errorf("failed to save server configuration: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if env.IsDockerPlatform() {
|
||||||
|
if err := s.configService.SetIgnorePrematureDisconnects(server, 0); err != nil {
|
||||||
|
logging.Error("Failed to set ignorePrematureDisconnects for Docker server: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
71
local/service/service.go
Normal file → Executable file
71
local/service/service.go
Normal file → Executable file
@@ -1,39 +1,102 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"acc-server-manager/local/platform"
|
||||||
"acc-server-manager/local/repository"
|
"acc-server-manager/local/repository"
|
||||||
|
"acc-server-manager/local/utl/docker"
|
||||||
|
"acc-server-manager/local/utl/env"
|
||||||
"acc-server-manager/local/utl/logging"
|
"acc-server-manager/local/utl/logging"
|
||||||
|
"acc-server-manager/local/utl/network"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
dockerclient "github.com/docker/docker/client"
|
||||||
"go.uber.org/dig"
|
"go.uber.org/dig"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// *dig.Container: Dig Container
|
// InitializeServices registers all service providers into the DI container.
|
||||||
|
// Platform-specific components (ServerRuntime, FirewallManager, LogStreamer)
|
||||||
|
// are conditionally provided based on the PLATFORM environment variable.
|
||||||
func InitializeServices(c *dig.Container) {
|
func InitializeServices(c *dig.Container) {
|
||||||
logging.Debug("Initializing repositories")
|
logging.Debug("Initializing repositories")
|
||||||
repository.InitializeRepositories(c)
|
repository.InitializeRepositories(c)
|
||||||
|
|
||||||
logging.Debug("Registering services")
|
logging.Debug("Registering services")
|
||||||
c.Provide(NewSteamService)
|
c.Provide(NewSteamService)
|
||||||
|
c.Provide(NewActivityLogService)
|
||||||
c.Provide(NewServerService)
|
c.Provide(NewServerService)
|
||||||
c.Provide(NewStateHistoryService)
|
c.Provide(NewStateHistoryService)
|
||||||
c.Provide(NewServiceControlService)
|
c.Provide(NewServiceControlService)
|
||||||
c.Provide(NewConfigService)
|
c.Provide(NewConfigService)
|
||||||
c.Provide(NewLookupService)
|
c.Provide(NewLookupService)
|
||||||
c.Provide(NewWindowsService)
|
|
||||||
c.Provide(NewFirewallService)
|
|
||||||
c.Provide(NewMembershipService)
|
c.Provide(NewMembershipService)
|
||||||
c.Provide(NewWebSocketService)
|
c.Provide(NewWebSocketService)
|
||||||
c.Provide(NewLeaderboardService)
|
c.Provide(NewLeaderboardService)
|
||||||
|
|
||||||
|
// Always provide the PortPoolManager; in Windows mode it seeds from an
|
||||||
|
// empty result set and sits idle.
|
||||||
|
c.Provide(func(db *gorm.DB) (*network.PortPoolManager, error) {
|
||||||
|
return network.NewPortPoolManager(db)
|
||||||
|
})
|
||||||
|
|
||||||
|
if env.IsDockerPlatform() {
|
||||||
|
registerDockerProviders(c)
|
||||||
|
} else {
|
||||||
|
registerWindowsProviders(c)
|
||||||
|
}
|
||||||
|
|
||||||
logging.Debug("Initializing service dependencies")
|
logging.Debug("Initializing service dependencies")
|
||||||
err := c.Invoke(func(server *ServerService, api *ServiceControlService, config *ConfigService) {
|
err := c.Invoke(func(server *ServerService, api *ServiceControlService, config *ConfigService) {
|
||||||
logging.Debug("Setting up service cross-references")
|
logging.Debug("Setting up service cross-references")
|
||||||
api.SetServerService(server)
|
api.SetServerService(server)
|
||||||
config.SetServerService(server)
|
config.SetServerService(server)
|
||||||
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Panic("unable to initialize services: " + err.Error())
|
logging.Panic("unable to initialize services: " + err.Error())
|
||||||
}
|
}
|
||||||
logging.Debug("Completed service initialization")
|
logging.Debug("Completed service initialization")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func registerWindowsProviders(c *dig.Container) {
|
||||||
|
logging.Debug("Registering Windows platform providers")
|
||||||
|
|
||||||
|
c.Provide(NewWindowsService)
|
||||||
|
|
||||||
|
c.Provide(func(ws *WindowsService, repo *repository.ServerRepository) platform.ServerRuntime {
|
||||||
|
return NewWindowsRuntime(ws, repo)
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Provide(func() platform.FirewallManager {
|
||||||
|
return NewFirewallService()
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Provide(func() platform.LogStreamer {
|
||||||
|
return NewFileLogStreamer()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerDockerProviders(c *dig.Container) {
|
||||||
|
logging.Debug("Registering Docker platform providers")
|
||||||
|
|
||||||
|
if err := c.Provide(func() (*dockerclient.Client, error) {
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to Docker daemon: %v", err)
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
|
}); err != nil {
|
||||||
|
logging.Panic("failed to provide Docker client: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Provide(func(client *dockerclient.Client, repo *repository.ServerRepository) platform.ServerRuntime {
|
||||||
|
return NewDockerRuntime(client, repo)
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Provide(func() platform.FirewallManager {
|
||||||
|
return &NoOpFirewall{}
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Provide(func(client *dockerclient.Client) platform.LogStreamer {
|
||||||
|
return NewDockerLogStreamer(client)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
128
local/service/service_control.go
Normal file → Executable file
128
local/service/service_control.go
Normal file → Executable file
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"acc-server-manager/local/model"
|
"acc-server-manager/local/model"
|
||||||
|
"acc-server-manager/local/platform"
|
||||||
"acc-server-manager/local/repository"
|
"acc-server-manager/local/repository"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -14,24 +15,52 @@ type ServiceControlService struct {
|
|||||||
repository *repository.ServiceControlRepository
|
repository *repository.ServiceControlRepository
|
||||||
serverRepository *repository.ServerRepository
|
serverRepository *repository.ServerRepository
|
||||||
serverService *ServerService
|
serverService *ServerService
|
||||||
|
activityLog *ActivityLogService
|
||||||
statusCache *model.ServerStatusCache
|
statusCache *model.ServerStatusCache
|
||||||
windowsService *WindowsService
|
runtime platform.ServerRuntime
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServiceControlService(repository *repository.ServiceControlRepository,
|
func NewServiceControlService(
|
||||||
serverRepository *repository.ServerRepository) *ServiceControlService {
|
repository *repository.ServiceControlRepository,
|
||||||
|
serverRepository *repository.ServerRepository,
|
||||||
|
runtime platform.ServerRuntime,
|
||||||
|
activityLog *ActivityLogService,
|
||||||
|
) *ServiceControlService {
|
||||||
return &ServiceControlService{
|
return &ServiceControlService{
|
||||||
repository: repository,
|
repository: repository,
|
||||||
serverRepository: serverRepository,
|
serverRepository: serverRepository,
|
||||||
|
runtime: runtime,
|
||||||
|
activityLog: activityLog,
|
||||||
statusCache: model.NewServerStatusCache(model.CacheConfig{
|
statusCache: model.NewServerStatusCache(model.CacheConfig{
|
||||||
ExpirationTime: 30 * time.Second,
|
ExpirationTime: 30 * time.Second,
|
||||||
ThrottleTime: 5 * time.Second,
|
ThrottleTime: 5 * time.Second,
|
||||||
DefaultStatus: model.StatusRunning,
|
DefaultStatus: model.StatusRunning,
|
||||||
}),
|
}),
|
||||||
windowsService: NewWindowsService(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getServerFromCtx resolves the server from ctx locals (serverId or service name).
|
||||||
|
func (as *ServiceControlService) getServerFromCtx(ctx *fiber.Ctx) (*model.Server, error) {
|
||||||
|
serviceName, ok := ctx.Locals("service").(string)
|
||||||
|
if !ok || serviceName == "" {
|
||||||
|
serverId, ok2 := ctx.Locals("serverId").(string)
|
||||||
|
if !ok2 || serverId == "" {
|
||||||
|
return nil, errors.New("service name missing")
|
||||||
|
}
|
||||||
|
return as.serverRepository.GetByID(ctx.UserContext(), serverId)
|
||||||
|
}
|
||||||
|
return as.serverRepository.GetFirstByServiceName(ctx.UserContext(), serviceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractActor(ctx *fiber.Ctx) (actorID, actorUsername string) {
|
||||||
|
actorID, _ = ctx.Locals("userID").(string)
|
||||||
|
actorUsername, _ = ctx.Locals("actorUsername").(string)
|
||||||
|
if actorUsername == "" {
|
||||||
|
actorUsername = "system"
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (as *ServiceControlService) SetServerService(serverService *ServerService) {
|
func (as *ServiceControlService) SetServerService(serverService *ServerService) {
|
||||||
as.serverService = serverService
|
as.serverService = serverService
|
||||||
}
|
}
|
||||||
@@ -57,10 +86,11 @@ func (as *ServiceControlService) GetStatus(ctx *fiber.Ctx) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (as *ServiceControlService) ServiceControlStartServer(ctx *fiber.Ctx) (string, error) {
|
func (as *ServiceControlService) ServiceControlStartServer(ctx *fiber.Ctx) (string, error) {
|
||||||
serviceName, err := as.GetServiceName(ctx)
|
server, err := as.getServerFromCtx(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
serviceName := server.ServiceName
|
||||||
|
|
||||||
as.statusCache.UpdateStatus(serviceName, model.StatusStarting)
|
as.statusCache.UpdateStatus(serviceName, model.StatusStarting)
|
||||||
|
|
||||||
@@ -75,14 +105,29 @@ func (as *ServiceControlService) ServiceControlStartServer(ctx *fiber.Ctx) (stri
|
|||||||
|
|
||||||
status := model.ParseServiceStatus(statusStr)
|
status := model.ParseServiceStatus(statusStr)
|
||||||
as.statusCache.UpdateStatus(serviceName, status)
|
as.statusCache.UpdateStatus(serviceName, status)
|
||||||
|
|
||||||
|
if as.activityLog != nil {
|
||||||
|
actorID, actorUsername := extractActor(ctx)
|
||||||
|
go func() {
|
||||||
|
_ = as.activityLog.Log(context.Background(), &model.ActivityLog{
|
||||||
|
ServerID: server.ID,
|
||||||
|
UserID: actorID,
|
||||||
|
Username: actorUsername,
|
||||||
|
Action: model.ActionServerStart,
|
||||||
|
Details: `{"service":"` + serviceName + `"}`,
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
return status.String(), nil
|
return status.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (as *ServiceControlService) ServiceControlStopServer(ctx *fiber.Ctx) (string, error) {
|
func (as *ServiceControlService) ServiceControlStopServer(ctx *fiber.Ctx) (string, error) {
|
||||||
serviceName, err := as.GetServiceName(ctx)
|
server, err := as.getServerFromCtx(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
serviceName := server.ServiceName
|
||||||
|
|
||||||
as.statusCache.UpdateStatus(serviceName, model.StatusStopping)
|
as.statusCache.UpdateStatus(serviceName, model.StatusStopping)
|
||||||
|
|
||||||
@@ -97,14 +142,29 @@ func (as *ServiceControlService) ServiceControlStopServer(ctx *fiber.Ctx) (strin
|
|||||||
|
|
||||||
status := model.ParseServiceStatus(statusStr)
|
status := model.ParseServiceStatus(statusStr)
|
||||||
as.statusCache.UpdateStatus(serviceName, status)
|
as.statusCache.UpdateStatus(serviceName, status)
|
||||||
|
|
||||||
|
if as.activityLog != nil {
|
||||||
|
actorID, actorUsername := extractActor(ctx)
|
||||||
|
go func() {
|
||||||
|
_ = as.activityLog.Log(context.Background(), &model.ActivityLog{
|
||||||
|
ServerID: server.ID,
|
||||||
|
UserID: actorID,
|
||||||
|
Username: actorUsername,
|
||||||
|
Action: model.ActionServerStop,
|
||||||
|
Details: `{"service":"` + serviceName + `"}`,
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
return status.String(), nil
|
return status.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (as *ServiceControlService) ServiceControlRestartServer(ctx *fiber.Ctx) (string, error) {
|
func (as *ServiceControlService) ServiceControlRestartServer(ctx *fiber.Ctx) (string, error) {
|
||||||
serviceName, err := as.GetServiceName(ctx)
|
server, err := as.getServerFromCtx(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
serviceName := server.ServiceName
|
||||||
|
|
||||||
as.statusCache.UpdateStatus(serviceName, model.StatusRestarting)
|
as.statusCache.UpdateStatus(serviceName, model.StatusRestarting)
|
||||||
|
|
||||||
@@ -119,11 +179,29 @@ func (as *ServiceControlService) ServiceControlRestartServer(ctx *fiber.Ctx) (st
|
|||||||
|
|
||||||
status := model.ParseServiceStatus(statusStr)
|
status := model.ParseServiceStatus(statusStr)
|
||||||
as.statusCache.UpdateStatus(serviceName, status)
|
as.statusCache.UpdateStatus(serviceName, status)
|
||||||
|
|
||||||
|
if as.activityLog != nil {
|
||||||
|
actorID, actorUsername := extractActor(ctx)
|
||||||
|
go func() {
|
||||||
|
_ = as.activityLog.Log(context.Background(), &model.ActivityLog{
|
||||||
|
ServerID: server.ID,
|
||||||
|
UserID: actorID,
|
||||||
|
Username: actorUsername,
|
||||||
|
Action: model.ActionServerRestart,
|
||||||
|
Details: `{"service":"` + serviceName + `"}`,
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
return status.String(), nil
|
return status.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (as *ServiceControlService) StatusServer(serviceName string) (string, error) {
|
func (as *ServiceControlService) StatusServer(serviceName string) (string, error) {
|
||||||
return as.windowsService.Status(context.Background(), serviceName)
|
server, err := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return as.runtime.Status(context.Background(), server.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (as *ServiceControlService) GetCachedStatus(serviceName string) (string, error) {
|
func (as *ServiceControlService) GetCachedStatus(serviceName string) (string, error) {
|
||||||
@@ -142,44 +220,46 @@ func (as *ServiceControlService) GetCachedStatus(serviceName string) (string, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (as *ServiceControlService) StartServer(serviceName string) (string, error) {
|
func (as *ServiceControlService) StartServer(serviceName string) (string, error) {
|
||||||
status, err := as.windowsService.Start(context.Background(), serviceName)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
server, err := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName)
|
server, err := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
status, err := as.runtime.Start(context.Background(), server.ID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
as.serverService.StartAccServerRuntime(server)
|
as.serverService.StartAccServerRuntime(server)
|
||||||
return status, err
|
return status, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (as *ServiceControlService) StopServer(serviceName string) (string, error) {
|
func (as *ServiceControlService) StopServer(serviceName string) (string, error) {
|
||||||
status, err := as.windowsService.Stop(context.Background(), serviceName)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
server, err := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName)
|
server, err := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
as.serverService.instances.Delete(server.ID)
|
|
||||||
|
|
||||||
|
status, err := as.runtime.Stop(context.Background(), server.ID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
as.serverService.instances.Delete(server.ID)
|
||||||
return status, err
|
return status, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (as *ServiceControlService) RestartServer(serviceName string) (string, error) {
|
func (as *ServiceControlService) RestartServer(serviceName string) (string, error) {
|
||||||
status, err := as.windowsService.Restart(context.Background(), serviceName)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
server, err := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName)
|
server, err := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
status, err := as.runtime.Restart(context.Background(), server.ID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
as.serverService.StartAccServerRuntime(server)
|
as.serverService.StartAccServerRuntime(server)
|
||||||
return status, err
|
return status, err
|
||||||
}
|
}
|
||||||
|
|||||||
0
local/service/service_manager.go
Normal file → Executable file
0
local/service/service_manager.go
Normal file → Executable file
0
local/service/state_history.go
Normal file → Executable file
0
local/service/state_history.go
Normal file → Executable file
29
local/service/steam_service.go
Normal file → Executable file
29
local/service/steam_service.go
Normal file → Executable file
@@ -44,6 +44,14 @@ func NewSteamService(repository *repository.SteamCredentialsRepository, tfaManag
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getSteamCMDPath returns the platform-appropriate SteamCMD binary path.
|
||||||
|
func getSteamCMDPath() string {
|
||||||
|
if env.IsDockerPlatform() {
|
||||||
|
return env.GetLinuxSteamCMDPath()
|
||||||
|
}
|
||||||
|
return env.GetSteamCMDPath()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SteamService) GetCredentials(ctx context.Context) (*model.SteamCredentials, error) {
|
func (s *SteamService) GetCredentials(ctx context.Context) (*model.SteamCredentials, error) {
|
||||||
return s.repository.GetCurrent(ctx)
|
return s.repository.GetCurrent(ctx)
|
||||||
}
|
}
|
||||||
@@ -56,6 +64,11 @@ func (s *SteamService) SaveCredentials(ctx context.Context, creds *model.SteamCr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SteamService) ensureSteamCMD(_ context.Context) error {
|
func (s *SteamService) ensureSteamCMD(_ context.Context) error {
|
||||||
|
// In Docker mode, SteamCMD is pre-installed in the manager container image.
|
||||||
|
if env.IsDockerPlatform() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
steamCMDPath := env.GetSteamCMDPath()
|
steamCMDPath := env.GetSteamCMDPath()
|
||||||
steamCMDDir := filepath.Dir(steamCMDPath)
|
steamCMDDir := filepath.Dir(steamCMDPath)
|
||||||
|
|
||||||
@@ -92,10 +105,12 @@ func (s *SteamService) InstallServerWithWebSocket(ctx context.Context, installPa
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !env.IsDockerPlatform() {
|
||||||
if err := s.pathValidator.ValidateInstallPath(installPath); err != nil {
|
if err := s.pathValidator.ValidateInstallPath(installPath); err != nil {
|
||||||
wsService.BroadcastSteamOutput(*serverID, fmt.Sprintf("Invalid installation path: %v", err), true)
|
wsService.BroadcastSteamOutput(*serverID, fmt.Sprintf("Invalid installation path: %v", err), true)
|
||||||
return fmt.Errorf("invalid installation path: %v", err)
|
return fmt.Errorf("invalid installation path: %v", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
absPath, err := filepath.Abs(installPath)
|
absPath, err := filepath.Abs(installPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -117,7 +132,7 @@ func (s *SteamService) InstallServerWithWebSocket(ctx context.Context, installPa
|
|||||||
return fmt.Errorf("failed to get Steam credentials: %v", err)
|
return fmt.Errorf("failed to get Steam credentials: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
steamCMDPath := env.GetSteamCMDPath()
|
steamCMDPath := getSteamCMDPath()
|
||||||
|
|
||||||
steamCMDArgs := []string{
|
steamCMDArgs := []string{
|
||||||
"+force_install_dir", absPath,
|
"+force_install_dir", absPath,
|
||||||
@@ -135,6 +150,10 @@ func (s *SteamService) InstallServerWithWebSocket(ctx context.Context, installPa
|
|||||||
steamCMDArgs = append(steamCMDArgs, "anonymous")
|
steamCMDArgs = append(steamCMDArgs, "anonymous")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if env.IsDockerPlatform() {
|
||||||
|
steamCMDArgs = append(steamCMDArgs, "+@sSteamCmdForcePlatformType", "windows")
|
||||||
|
}
|
||||||
|
|
||||||
steamCMDArgs = append(steamCMDArgs,
|
steamCMDArgs = append(steamCMDArgs,
|
||||||
"+app_update", ACCServerAppID,
|
"+app_update", ACCServerAppID,
|
||||||
"validate",
|
"validate",
|
||||||
@@ -216,10 +235,12 @@ func (s *SteamService) InstallServerWithCallbacks(ctx context.Context, installPa
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !env.IsDockerPlatform() {
|
||||||
if err := s.pathValidator.ValidateInstallPath(installPath); err != nil {
|
if err := s.pathValidator.ValidateInstallPath(installPath); err != nil {
|
||||||
outputCallback(*serverID, fmt.Sprintf("Invalid installation path: %v", err), true)
|
outputCallback(*serverID, fmt.Sprintf("Invalid installation path: %v", err), true)
|
||||||
return fmt.Errorf("invalid installation path: %v", err)
|
return fmt.Errorf("invalid installation path: %v", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
absPath, err := filepath.Abs(installPath)
|
absPath, err := filepath.Abs(installPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -241,7 +262,7 @@ func (s *SteamService) InstallServerWithCallbacks(ctx context.Context, installPa
|
|||||||
return fmt.Errorf("failed to get Steam credentials: %v", err)
|
return fmt.Errorf("failed to get Steam credentials: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
steamCMDPath := env.GetSteamCMDPath()
|
steamCMDPath := getSteamCMDPath()
|
||||||
|
|
||||||
steamCMDArgs := []string{
|
steamCMDArgs := []string{
|
||||||
"+force_install_dir", absPath,
|
"+force_install_dir", absPath,
|
||||||
@@ -259,6 +280,10 @@ func (s *SteamService) InstallServerWithCallbacks(ctx context.Context, installPa
|
|||||||
steamCMDArgs = append(steamCMDArgs, "anonymous")
|
steamCMDArgs = append(steamCMDArgs, "anonymous")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if env.IsDockerPlatform() {
|
||||||
|
steamCMDArgs = append(steamCMDArgs, "+@sSteamCmdForcePlatformType", "windows")
|
||||||
|
}
|
||||||
|
|
||||||
steamCMDArgs = append(steamCMDArgs,
|
steamCMDArgs = append(steamCMDArgs,
|
||||||
"+app_update", ACCServerAppID,
|
"+app_update", ACCServerAppID,
|
||||||
"validate",
|
"validate",
|
||||||
|
|||||||
13
local/service/websocket.go
Normal file → Executable file
13
local/service/websocket.go
Normal file → Executable file
@@ -15,6 +15,7 @@ type WebSocketConnection struct {
|
|||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
serverID *uuid.UUID
|
serverID *uuid.UUID
|
||||||
userID *uuid.UUID
|
userID *uuid.UUID
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebSocketService struct {
|
type WebSocketService struct {
|
||||||
@@ -131,7 +132,7 @@ func (ws *WebSocketService) broadcastToServer(serverID uuid.UUID, message model.
|
|||||||
ws.connections.Range(func(key, value interface{}) bool {
|
ws.connections.Range(func(key, value interface{}) bool {
|
||||||
if wsConn, ok := value.(*WebSocketConnection); ok {
|
if wsConn, ok := value.(*WebSocketConnection); ok {
|
||||||
if wsConn.serverID != nil && *wsConn.serverID == serverID {
|
if wsConn.serverID != nil && *wsConn.serverID == serverID {
|
||||||
if err := wsConn.conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
if err := wsConn.writeMessage(websocket.TextMessage, data); err != nil {
|
||||||
logging.Error("Failed to send WebSocket message to connection %s: %v", key, err)
|
logging.Error("Failed to send WebSocket message to connection %s: %v", key, err)
|
||||||
ws.RemoveConnection(key.(string))
|
ws.RemoveConnection(key.(string))
|
||||||
} else {
|
} else {
|
||||||
@@ -145,7 +146,7 @@ func (ws *WebSocketService) broadcastToServer(serverID uuid.UUID, message model.
|
|||||||
if !sentToAssociatedConnections && (message.Type == model.MessageTypeStep || message.Type == model.MessageTypeError || message.Type == model.MessageTypeComplete) {
|
if !sentToAssociatedConnections && (message.Type == model.MessageTypeStep || message.Type == model.MessageTypeError || message.Type == model.MessageTypeComplete) {
|
||||||
ws.connections.Range(func(key, value interface{}) bool {
|
ws.connections.Range(func(key, value interface{}) bool {
|
||||||
if wsConn, ok := value.(*WebSocketConnection); ok {
|
if wsConn, ok := value.(*WebSocketConnection); ok {
|
||||||
if err := wsConn.conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
if err := wsConn.writeMessage(websocket.TextMessage, data); err != nil {
|
||||||
logging.Error("Failed to send WebSocket message to connection %s: %v", key, err)
|
logging.Error("Failed to send WebSocket message to connection %s: %v", key, err)
|
||||||
ws.RemoveConnection(key.(string))
|
ws.RemoveConnection(key.(string))
|
||||||
}
|
}
|
||||||
@@ -165,7 +166,7 @@ func (ws *WebSocketService) BroadcastToUser(userID uuid.UUID, message model.WebS
|
|||||||
ws.connections.Range(func(key, value interface{}) bool {
|
ws.connections.Range(func(key, value interface{}) bool {
|
||||||
if wsConn, ok := value.(*WebSocketConnection); ok {
|
if wsConn, ok := value.(*WebSocketConnection); ok {
|
||||||
if wsConn.userID != nil && *wsConn.userID == userID {
|
if wsConn.userID != nil && *wsConn.userID == userID {
|
||||||
if err := wsConn.conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
if err := wsConn.writeMessage(websocket.TextMessage, data); err != nil {
|
||||||
logging.Error("Failed to send WebSocket message to connection %s: %v", key, err)
|
logging.Error("Failed to send WebSocket message to connection %s: %v", key, err)
|
||||||
ws.RemoveConnection(key.(string))
|
ws.RemoveConnection(key.(string))
|
||||||
}
|
}
|
||||||
@@ -175,6 +176,12 @@ func (ws *WebSocketService) BroadcastToUser(userID uuid.UUID, message model.WebS
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (wc *WebSocketConnection) writeMessage(messageType int, data []byte) error {
|
||||||
|
wc.mu.Lock()
|
||||||
|
defer wc.mu.Unlock()
|
||||||
|
return wc.conn.WriteMessage(messageType, data)
|
||||||
|
}
|
||||||
|
|
||||||
func (ws *WebSocketService) GetActiveConnections() int {
|
func (ws *WebSocketService) GetActiveConnections() int {
|
||||||
count := 0
|
count := 0
|
||||||
ws.connections.Range(func(key, value interface{}) bool {
|
ws.connections.Range(func(key, value interface{}) bool {
|
||||||
|
|||||||
68
local/service/windows_runtime.go
Normal file
68
local/service/windows_runtime.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/model"
|
||||||
|
"acc-server-manager/local/repository"
|
||||||
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WindowsRuntime struct {
|
||||||
|
windowsService *WindowsService
|
||||||
|
repo *repository.ServerRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWindowsRuntime(windowsService *WindowsService, repo *repository.ServerRepository) *WindowsRuntime {
|
||||||
|
return &WindowsRuntime{
|
||||||
|
windowsService: windowsService,
|
||||||
|
repo: repo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WindowsRuntime) Create(ctx context.Context, server *model.Server) error {
|
||||||
|
execPath := filepath.Join(server.GetServerPath(), "accServer.exe")
|
||||||
|
serverWorkingDir := server.GetServerPath()
|
||||||
|
return r.windowsService.CreateService(ctx, server.ServiceName, execPath, serverWorkingDir, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WindowsRuntime) Delete(ctx context.Context, serverID uuid.UUID) error {
|
||||||
|
server, err := r.repo.GetByID(ctx, serverID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return r.windowsService.DeleteService(ctx, server.ServiceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WindowsRuntime) Start(ctx context.Context, serverID uuid.UUID) (string, error) {
|
||||||
|
server, err := r.repo.GetByID(ctx, serverID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return r.windowsService.Start(ctx, server.ServiceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WindowsRuntime) Stop(ctx context.Context, serverID uuid.UUID) (string, error) {
|
||||||
|
server, err := r.repo.GetByID(ctx, serverID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return r.windowsService.Stop(ctx, server.ServiceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WindowsRuntime) Restart(ctx context.Context, serverID uuid.UUID) (string, error) {
|
||||||
|
server, err := r.repo.GetByID(ctx, serverID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return r.windowsService.Restart(ctx, server.ServiceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WindowsRuntime) Status(ctx context.Context, serverID uuid.UUID) (string, error) {
|
||||||
|
server, err := r.repo.GetByID(ctx, serverID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return r.windowsService.Status(ctx, server.ServiceName)
|
||||||
|
}
|
||||||
0
local/service/windows_service.go
Normal file → Executable file
0
local/service/windows_service.go
Normal file → Executable file
0
local/utl/audit/audit.go
Normal file → Executable file
0
local/utl/audit/audit.go
Normal file → Executable file
0
local/utl/cache/cache.go
vendored
Normal file → Executable file
0
local/utl/cache/cache.go
vendored
Normal file → Executable file
29
local/utl/command/callback_executor.go
Normal file → Executable file
29
local/utl/command/callback_executor.go
Normal file → Executable file
@@ -123,11 +123,12 @@ func (e *CallbackInteractiveCommandExecutor) monitorOutputWithCallbacks(ctx cont
|
|||||||
stdoutScanner := bufio.NewScanner(stdout)
|
stdoutScanner := bufio.NewScanner(stdout)
|
||||||
stderrScanner := bufio.NewScanner(stderr)
|
stderrScanner := bufio.NewScanner(stderr)
|
||||||
|
|
||||||
outputChan := make(chan outputLine, 100)
|
outputChan := make(chan outputLine, 10000)
|
||||||
readersDone := make(chan struct{}, 2)
|
readersDone := make(chan struct{}, 2)
|
||||||
|
|
||||||
steamConsoleStarted := false
|
steamConsoleStarted := false
|
||||||
tfaRequestCreated := false
|
tfaRequestCreated := false
|
||||||
|
tfaAutoCompleted := false
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer func() { readersDone <- struct{}{} }()
|
defer func() { readersDone <- struct{}{} }()
|
||||||
@@ -182,13 +183,14 @@ func (e *CallbackInteractiveCommandExecutor) monitorOutputWithCallbacks(ctx cont
|
|||||||
if readersFinished == 2 {
|
if readersFinished == 2 {
|
||||||
close(outputChan)
|
close(outputChan)
|
||||||
for lineData := range outputChan {
|
for lineData := range outputChan {
|
||||||
if e.is2FAPrompt(lineData.text) {
|
if e.is2FAPrompt(lineData.text) && !tfaRequestCreated {
|
||||||
if err := e.handle2FAPrompt(ctx, lineData.text, serverID); err != nil {
|
tfaRequestCreated = true
|
||||||
|
go func(prompt string) {
|
||||||
|
if err := e.handle2FAPrompt(ctx, prompt, serverID); err != nil {
|
||||||
logging.Error("Failed to handle 2FA prompt: %v", err)
|
logging.Error("Failed to handle 2FA prompt: %v", err)
|
||||||
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true)
|
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true)
|
||||||
done <- err
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}(lineData.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -207,33 +209,34 @@ func (e *CallbackInteractiveCommandExecutor) monitorOutputWithCallbacks(ctx cont
|
|||||||
|
|
||||||
if e.is2FAPrompt(lineData.text) {
|
if e.is2FAPrompt(lineData.text) {
|
||||||
if !tfaRequestCreated {
|
if !tfaRequestCreated {
|
||||||
|
tfaRequestCreated = true
|
||||||
e.callbacks.OnOutput(e.serverID, "2FA prompt detected - waiting for user confirmation", false)
|
e.callbacks.OnOutput(e.serverID, "2FA prompt detected - waiting for user confirmation", false)
|
||||||
if err := e.handle2FAPrompt(ctx, lineData.text, serverID); err != nil {
|
go func(prompt string) {
|
||||||
|
if err := e.handle2FAPrompt(ctx, prompt, serverID); err != nil {
|
||||||
logging.Error("Failed to handle 2FA prompt: %v", err)
|
logging.Error("Failed to handle 2FA prompt: %v", err)
|
||||||
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true)
|
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true)
|
||||||
done <- err
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
tfaRequestCreated = true
|
}(lineData.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if tfaRequestCreated && e.isSteamContinuing(lineData.text) {
|
if tfaRequestCreated && !tfaAutoCompleted && e.isSteamContinuing(lineData.text) {
|
||||||
logging.Info("Steam CMD appears to have continued after 2FA confirmation")
|
logging.Info("Steam CMD appears to have continued after 2FA confirmation")
|
||||||
|
tfaAutoCompleted = true
|
||||||
e.callbacks.OnOutput(e.serverID, "Steam CMD continued after 2FA confirmation", false)
|
e.callbacks.OnOutput(e.serverID, "Steam CMD continued after 2FA confirmation", false)
|
||||||
e.autoCompletePendingRequests(serverID)
|
e.autoCompletePendingRequests(serverID)
|
||||||
}
|
}
|
||||||
case <-time.After(15 * time.Second):
|
case <-time.After(15 * time.Second):
|
||||||
if steamConsoleStarted && !tfaRequestCreated {
|
if steamConsoleStarted && !tfaRequestCreated {
|
||||||
|
tfaRequestCreated = true
|
||||||
logging.Info("Steam Console started but no output for 15 seconds - likely waiting for Steam Guard 2FA")
|
logging.Info("Steam Console started but no output for 15 seconds - likely waiting for Steam Guard 2FA")
|
||||||
e.callbacks.OnOutput(e.serverID, "Waiting for Steam Guard 2FA confirmation...", false)
|
e.callbacks.OnOutput(e.serverID, "Waiting for Steam Guard 2FA confirmation...", false)
|
||||||
|
go func() {
|
||||||
if err := e.handle2FAPrompt(ctx, "Steam CMD appears to be waiting for Steam Guard confirmation after startup", serverID); err != nil {
|
if err := e.handle2FAPrompt(ctx, "Steam CMD appears to be waiting for Steam Guard confirmation after startup", serverID); err != nil {
|
||||||
logging.Error("Failed to handle Steam Guard 2FA prompt: %v", err)
|
logging.Error("Failed to handle Steam Guard 2FA prompt: %v", err)
|
||||||
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle Steam Guard 2FA prompt: %v", err), true)
|
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle Steam Guard 2FA prompt: %v", err), true)
|
||||||
done <- err
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
tfaRequestCreated = true
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
local/utl/command/callbacks.go
Normal file → Executable file
0
local/utl/command/callbacks.go
Normal file → Executable file
0
local/utl/command/executor.go
Normal file → Executable file
0
local/utl/command/executor.go
Normal file → Executable file
59
local/utl/command/interactive_executor.go
Normal file → Executable file
59
local/utl/command/interactive_executor.go
Normal file → Executable file
@@ -106,11 +106,12 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
|
|||||||
stdoutScanner := bufio.NewScanner(stdout)
|
stdoutScanner := bufio.NewScanner(stdout)
|
||||||
stderrScanner := bufio.NewScanner(stderr)
|
stderrScanner := bufio.NewScanner(stderr)
|
||||||
|
|
||||||
outputChan := make(chan string, 100)
|
outputChan := make(chan string, 10000)
|
||||||
readersDone := make(chan struct{}, 2)
|
readersDone := make(chan struct{}, 2)
|
||||||
|
|
||||||
steamConsoleStarted := false
|
steamConsoleStarted := false
|
||||||
tfaRequestCreated := false
|
tfaRequestCreated := false
|
||||||
|
tfaAutoCompleted := false
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer func() { readersDone <- struct{}{} }()
|
defer func() { readersDone <- struct{}{} }()
|
||||||
@@ -165,12 +166,13 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
|
|||||||
if readersFinished == 2 {
|
if readersFinished == 2 {
|
||||||
close(outputChan)
|
close(outputChan)
|
||||||
for line := range outputChan {
|
for line := range outputChan {
|
||||||
if e.is2FAPrompt(line) {
|
if e.is2FAPrompt(line) && !tfaRequestCreated {
|
||||||
if err := e.handle2FAPrompt(ctx, line, serverID); err != nil {
|
tfaRequestCreated = true
|
||||||
|
go func(prompt string) {
|
||||||
|
if err := e.handle2FAPrompt(ctx, prompt, serverID); err != nil {
|
||||||
logging.Error("Failed to handle 2FA prompt: %v", err)
|
logging.Error("Failed to handle 2FA prompt: %v", err)
|
||||||
done <- err
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}(line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -188,28 +190,29 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
|
|||||||
|
|
||||||
if e.is2FAPrompt(line) {
|
if e.is2FAPrompt(line) {
|
||||||
if !tfaRequestCreated {
|
if !tfaRequestCreated {
|
||||||
if err := e.handle2FAPrompt(ctx, line, serverID); err != nil {
|
|
||||||
logging.Error("Failed to handle 2FA prompt: %v", err)
|
|
||||||
done <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tfaRequestCreated = true
|
tfaRequestCreated = true
|
||||||
|
go func(prompt string) {
|
||||||
|
if err := e.handle2FAPrompt(ctx, prompt, serverID); err != nil {
|
||||||
|
logging.Error("Failed to handle 2FA prompt: %v", err)
|
||||||
|
}
|
||||||
|
}(line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if tfaRequestCreated && e.isSteamContinuing(line) {
|
if tfaRequestCreated && !tfaAutoCompleted && e.isSteamContinuing(line) {
|
||||||
logging.Info("Steam CMD appears to have continued after 2FA confirmation - auto-completing 2FA request")
|
logging.Info("Steam CMD appears to have continued after 2FA confirmation - auto-completing 2FA request")
|
||||||
|
tfaAutoCompleted = true
|
||||||
e.autoCompletePendingRequests(serverID)
|
e.autoCompletePendingRequests(serverID)
|
||||||
}
|
}
|
||||||
case <-time.After(15 * time.Second):
|
case <-time.After(15 * time.Second):
|
||||||
if steamConsoleStarted && !tfaRequestCreated {
|
if steamConsoleStarted && !tfaRequestCreated {
|
||||||
|
tfaRequestCreated = true
|
||||||
logging.Info("Steam Console started but no output for 15 seconds - likely waiting for Steam Guard 2FA")
|
logging.Info("Steam Console started but no output for 15 seconds - likely waiting for Steam Guard 2FA")
|
||||||
|
go func() {
|
||||||
if err := e.handle2FAPrompt(ctx, "Steam CMD appears to be waiting for Steam Guard confirmation after startup", serverID); err != nil {
|
if err := e.handle2FAPrompt(ctx, "Steam CMD appears to be waiting for Steam Guard confirmation after startup", serverID); err != nil {
|
||||||
logging.Error("Failed to handle Steam Guard 2FA prompt: %v", err)
|
logging.Error("Failed to handle Steam Guard 2FA prompt: %v", err)
|
||||||
done <- err
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
tfaRequestCreated = true
|
}()
|
||||||
} else if !steamConsoleStarted {
|
} else if !steamConsoleStarted {
|
||||||
logging.Info("No output for 15 seconds (Steam Console not yet started)")
|
logging.Info("No output for 15 seconds (Steam Console not yet started)")
|
||||||
}
|
}
|
||||||
@@ -220,22 +223,15 @@ func (e *InteractiveCommandExecutor) monitorOutput(ctx context.Context, stdout,
|
|||||||
func (e *InteractiveCommandExecutor) is2FAPrompt(line string) bool {
|
func (e *InteractiveCommandExecutor) is2FAPrompt(line string) bool {
|
||||||
twoFAKeywords := []string{
|
twoFAKeywords := []string{
|
||||||
"please enter your steam guard code",
|
"please enter your steam guard code",
|
||||||
"steam guard",
|
"steam guard code",
|
||||||
"two-factor",
|
"two-factor authentication",
|
||||||
"authentication code",
|
|
||||||
"please check your steam mobile app",
|
"please check your steam mobile app",
|
||||||
"confirm in application",
|
|
||||||
"enter the current code from your steam mobile app",
|
"enter the current code from your steam mobile app",
|
||||||
"steam guard mobile authenticator",
|
"steam guard mobile authenticator",
|
||||||
"waiting for user info",
|
|
||||||
"login failure",
|
|
||||||
"two factor code required",
|
"two factor code required",
|
||||||
"enter steam guard code",
|
"enter steam guard code",
|
||||||
"mobile authenticator code",
|
"mobile authenticator code",
|
||||||
"authenticator app",
|
"2fa prompt detected",
|
||||||
"guard code",
|
|
||||||
"mobile app",
|
|
||||||
"confirmation required",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lowerLine := strings.ToLower(line)
|
lowerLine := strings.ToLower(line)
|
||||||
@@ -246,21 +242,6 @@ func (e *InteractiveCommandExecutor) is2FAPrompt(line string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
waitingPatterns := []string{
|
|
||||||
"waiting for",
|
|
||||||
"please enter",
|
|
||||||
"enter code",
|
|
||||||
"code:",
|
|
||||||
"authenticator:",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pattern := range waitingPatterns {
|
|
||||||
if strings.Contains(lowerLine, pattern) {
|
|
||||||
logging.Info("Potential 2FA waiting pattern found: '%s' in line: '%s'", pattern, line)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user