Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| efc6b72838 | |||
| 0e04da031b | |||
| 908adf4468 | |||
| 6c1e9b236e | |||
| f563d52396 | |||
| a490a73392 | |||
| 0ef47ef241 | |||
| ea2e871c9b | |||
| c2c6543068 | |||
| 14aa8a84e5 | |||
| e96b8bca12 | |||
| a6f67dd652 | |||
| e637b5e649 | |||
| 85c5874bc7 | |||
| 4441524447 | |||
| c03234d339 | |||
| d4b16d80f3 | |||
| 9172f0256f | |||
| beb9b479df | |||
| 4aa67db3fd | |||
| 1100072245 | |||
| 49dfa38475 | |||
| 747adc77a5 | |||
| c9a32e8889 | |||
| 7d470f3638 | |||
| cf7473b373 | |||
| 35c53cc54c | |||
| 016824e846 | |||
| d62c8cef34 | |||
| 190e1a3a16 |
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"]
|
||||||
26
docker/acc-server/entrypoint.sh
Normal file
26
docker/acc-server/entrypoint.sh
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Use a display number derived from the server port to avoid conflicts
|
||||||
|
# between containers (each server gets its own unique display).
|
||||||
|
DISPLAY_NUM=$((SERVER_PORT % 1000 + 100))
|
||||||
|
export DISPLAY=:${DISPLAY_NUM}
|
||||||
|
|
||||||
|
# Clean up any stale X lock from a previous container run.
|
||||||
|
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=$!
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
echo "Stopping ACC server..."
|
||||||
|
kill $XVFB_PID 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup SIGTERM SIGINT
|
||||||
|
|
||||||
|
mkdir -p /acc/game/log
|
||||||
|
|
||||||
|
echo "Starting ACC server on port ${SERVER_PORT}..."
|
||||||
|
|
||||||
|
cd /acc/game
|
||||||
|
exec wine /acc/game/accServer.exe
|
||||||
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=
|
||||||
|
|||||||
5
local/api/api.go
Normal file → Executable file
5
local/api/api.go
Normal file → Executable file
@@ -32,6 +32,11 @@ func Init(di *dig.Container, app *fiber.App) {
|
|||||||
Membership: groups.Group("/membership"),
|
Membership: groups.Group("/membership"),
|
||||||
System: groups.Group("/system"),
|
System: groups.Group("/system"),
|
||||||
WebSocket: groups.Group("/ws"),
|
WebSocket: groups.Group("/ws"),
|
||||||
|
Leaderboard: serverIdGroup.Group("/leaderboard"),
|
||||||
|
Steam: groups.Group("/steam"),
|
||||||
|
ActivityLog: serverIdGroup.Group("/activity-log"),
|
||||||
|
Groups: groups,
|
||||||
|
Log: serverIdGroup.Group("/log"),
|
||||||
}
|
}
|
||||||
|
|
||||||
accessKeyMiddleware := middleware.NewAccessKeyMiddleware()
|
accessKeyMiddleware := middleware.NewAccessKeyMiddleware()
|
||||||
|
|||||||
48
local/controller/activity_log.go
Normal file
48
local/controller/activity_log.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global activity log: GET /activity-log (admin use)
|
||||||
|
routeGroups.Groups.Group("/activity-log").Use(auth.Authenticate).Get("/", ac.GetAllLogs)
|
||||||
|
|
||||||
|
return ac
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|||||||
20
local/controller/controller.go
Normal file → Executable file
20
local/controller/controller.go
Normal file → Executable file
@@ -59,4 +59,24 @@ func InitializeControllers(c *dig.Container) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Panic("unable to initialize websocket controller")
|
logging.Panic("unable to initialize websocket controller")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = c.Invoke(NewLeaderboardController)
|
||||||
|
if err != nil {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.Invoke(NewLogController)
|
||||||
|
if err != nil {
|
||||||
|
logging.Panic("unable to initialize log controller")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
77
local/controller/leaderboard.go
Executable file
77
local/controller/leaderboard.go
Executable file
@@ -0,0 +1,77 @@
|
|||||||
|
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"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
type LeaderboardController struct {
|
||||||
|
service *service.LeaderboardService
|
||||||
|
errorHandler *error_handler.ControllerErrorHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLeaderboardController(ls *service.LeaderboardService, routeGroups *common.RouteGroups, auth *middleware.AuthMiddleware) *LeaderboardController {
|
||||||
|
lc := &LeaderboardController{
|
||||||
|
service: ls,
|
||||||
|
errorHandler: error_handler.NewControllerErrorHandler(),
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerRoutes := routeGroups.Api.Group("/server/:id")
|
||||||
|
apiServerRoutes.Get("/leaderboard", lc.Get)
|
||||||
|
|
||||||
|
routeGroups.Leaderboard.Get("/", lc.Get)
|
||||||
|
routeGroups.Leaderboard.Put("/", auth.Authenticate, lc.Update)
|
||||||
|
|
||||||
|
return lc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the leaderboard for a server (public, no auth required).
|
||||||
|
func (lc *LeaderboardController) Get(c *fiber.Ctx) error {
|
||||||
|
serverIDStr := c.Params("id")
|
||||||
|
serverID, err := uuid.Parse(serverIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return lc.errorHandler.HandleUUIDError(c, "server ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := lc.service.Get(c.UserContext(), serverID)
|
||||||
|
if err != nil {
|
||||||
|
return lc.errorHandler.HandleServiceError(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update replaces the leaderboard for a server (requires JWT auth).
|
||||||
|
func (lc *LeaderboardController) Update(c *fiber.Ctx) error {
|
||||||
|
serverIDStr := c.Params("id")
|
||||||
|
serverID, err := uuid.Parse(serverIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return lc.errorHandler.HandleUUIDError(c, "server ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
var input model.Leaderboard
|
||||||
|
if err := c.BodyParser(&input); err != nil {
|
||||||
|
return lc.errorHandler.HandleParsingError(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return lc.errorHandler.HandleServiceError(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(data)
|
||||||
|
}
|
||||||
62
local/controller/log.go
Normal file
62
local/controller/log.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LogController struct {
|
||||||
|
service *service.ServerService
|
||||||
|
errorHandler *error_handler.ControllerErrorHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogController(ss *service.ServerService, routeGroups *common.RouteGroups, auth *middleware.AuthMiddleware) *LogController {
|
||||||
|
lc := &LogController{
|
||||||
|
service: ss,
|
||||||
|
errorHandler: error_handler.NewControllerErrorHandler(),
|
||||||
|
}
|
||||||
|
|
||||||
|
logRoutes := routeGroups.Log
|
||||||
|
logRoutes.Use(auth.Authenticate)
|
||||||
|
logRoutes.Get("/", auth.HasPermission(model.ServerView), lc.GetLogLines)
|
||||||
|
|
||||||
|
return lc
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLogLines godoc
|
||||||
|
// @Summary Get last N lines of server log
|
||||||
|
// @Tags Server
|
||||||
|
// @Param id path string true "Server ID"
|
||||||
|
// @Param lines query int false "Number of lines to return (default 100)"
|
||||||
|
// @Success 200 {object} object{lines=[]string}
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /server/{id}/log [get]
|
||||||
|
func (lc *LogController) GetLogLines(c *fiber.Ctx) error {
|
||||||
|
serverIDStr := c.Params("id")
|
||||||
|
serverID, err := uuid.Parse(serverIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return lc.errorHandler.HandleUUIDError(c, "server ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
n, _ := strconv.Atoi(c.Query("lines", "100"))
|
||||||
|
if n <= 0 {
|
||||||
|
n = 100
|
||||||
|
}
|
||||||
|
if n > 10000 {
|
||||||
|
n = 10000
|
||||||
|
}
|
||||||
|
|
||||||
|
lines, err := lc.service.GetLastLogLines(c, serverID, n)
|
||||||
|
if err != nil {
|
||||||
|
return lc.errorHandler.HandleServiceError(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{"lines": lines})
|
||||||
|
}
|
||||||
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
1
local/controller/service_control.go
Normal file → Executable file
1
local/controller/service_control.go
Normal file → Executable file
@@ -29,6 +29,7 @@ func NewServiceControlController(as *service.ServiceControlService, routeGroups
|
|||||||
}
|
}
|
||||||
|
|
||||||
serviceRoutes := routeGroups.Server.Group("/:id/service")
|
serviceRoutes := routeGroups.Server.Group("/:id/service")
|
||||||
|
serviceRoutes.Use(auth.Authenticate)
|
||||||
serviceRoutes.Get("/:service", ac.getStatus)
|
serviceRoutes.Get("/:service", ac.getStatus)
|
||||||
serviceRoutes.Post("/start", ac.startServer)
|
serviceRoutes.Post("/start", ac.startServer)
|
||||||
serviceRoutes.Post("/stop", ac.stopServer)
|
serviceRoutes.Post("/stop", ac.stopServer)
|
||||||
|
|||||||
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
106
local/migrations/004_fix_ddl_comments.go
Executable file
106
local/migrations/004_fix_ddl_comments.go
Executable file
@@ -0,0 +1,106 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/utl/logging"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var sqlLineCommentRegexp = regexp.MustCompile(`--[^\n]*`)
|
||||||
|
|
||||||
|
type Migration004FixDDLComments struct {
|
||||||
|
DB *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMigration004FixDDLComments(db *gorm.DB) *Migration004FixDDLComments {
|
||||||
|
return &Migration004FixDDLComments{DB: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Migration004FixDDLComments) Up() error {
|
||||||
|
if err := m.DB.AutoMigrate(&MigrationRecord{}); err != nil {
|
||||||
|
return fmt.Errorf("failed to create migration tracking table: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var record MigrationRecord
|
||||||
|
if err := m.DB.Where("migration_name = ?", "004_fix_ddl_comments").First(&record).Error; err == nil {
|
||||||
|
logging.Info("DDL comment fix migration already applied, skipping")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var tables []string
|
||||||
|
if err := m.DB.Raw(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type = 'table' AND sql LIKE '%--%' ORDER BY name",
|
||||||
|
).Scan(&tables).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to query tables with DDL comments: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tables) == 0 {
|
||||||
|
logging.Info("No tables with DDL comments found, skipping")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
if err := m.rebuildTable(table); err != nil {
|
||||||
|
return fmt.Errorf("failed to rebuild table %q: %v", table, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record = MigrationRecord{
|
||||||
|
MigrationName: "004_fix_ddl_comments",
|
||||||
|
AppliedAt: "datetime('now')",
|
||||||
|
Success: true,
|
||||||
|
Notes: fmt.Sprintf("Rebuilt %d table(s): %s", len(tables), strings.Join(tables, ", ")),
|
||||||
|
}
|
||||||
|
if err := m.DB.Create(&record).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to record migration: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Migration004FixDDLComments) rebuildTable(table string) error {
|
||||||
|
var originalDDL string
|
||||||
|
if err := m.DB.Raw(
|
||||||
|
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?", table,
|
||||||
|
).Row().Scan(&originalDDL); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanDDL := sqlLineCommentRegexp.ReplaceAllString(originalDDL, "")
|
||||||
|
|
||||||
|
tmpTable := table + "__fixed"
|
||||||
|
createTmp := strings.Replace(cleanDDL, `"`+table+`"`, "`"+tmpTable+"`", 1)
|
||||||
|
if createTmp == cleanDDL {
|
||||||
|
createTmp = strings.Replace(cleanDDL, table, "`"+tmpTable+"`", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.Info("Rebuilding table %q to remove DDL comments...", table)
|
||||||
|
|
||||||
|
return m.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
for _, sql := range []string{
|
||||||
|
"PRAGMA foreign_keys = OFF",
|
||||||
|
fmt.Sprintf("DROP TABLE IF EXISTS `%s`", tmpTable),
|
||||||
|
createTmp,
|
||||||
|
fmt.Sprintf("INSERT INTO `%s` SELECT * FROM `%s`", tmpTable, table),
|
||||||
|
fmt.Sprintf("DROP TABLE `%s`", table),
|
||||||
|
fmt.Sprintf("ALTER TABLE `%s` RENAME TO `%s`", tmpTable, table),
|
||||||
|
"PRAGMA foreign_keys = ON",
|
||||||
|
} {
|
||||||
|
if err := tx.Exec(sql).Error; err != nil {
|
||||||
|
return fmt.Errorf("exec %q: %w", sql, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logging.Info("Table %q rebuilt successfully", table)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Migration004FixDDLComments) Down() error {
|
||||||
|
return fmt.Errorf("DDL comment fix migration rollback is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunFixDDLCommentsMigration(db *gorm.DB) error {
|
||||||
|
return NewMigration004FixDDLComments(db).Up()
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
82
local/model/activity_log.go
Normal file
82
local/model/activity_log.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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"`
|
||||||
|
Server *Server `json:"server,omitempty" gorm:"foreignKey:ServerID"`
|
||||||
|
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
|
||||||
|
DateRangeFilter
|
||||||
|
UserID string `query:"user_id"`
|
||||||
|
Action ActionType `query:"action"`
|
||||||
|
ServerID string `query:"server_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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
113
local/model/leaderboard.go
Executable file
113
local/model/leaderboard.go
Executable file
@@ -0,0 +1,113 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"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 {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
||||||
|
ServerID uuid.UUID `gorm:"uniqueIndex;not null;type:uuid" json:"serverId"`
|
||||||
|
FLPoints int `gorm:"default:1" json:"flPoints"`
|
||||||
|
FLColor string `gorm:"default:'#8b5cf6'" json:"flColor"`
|
||||||
|
FLTextColor string `gorm:"default:'#000000'" json:"flTextColor"`
|
||||||
|
Drivers []LeaderboardDriver `gorm:"foreignKey:LeaderboardID;constraint:OnDelete:CASCADE" json:"drivers"`
|
||||||
|
Races []LeaderboardRace `gorm:"foreignKey:LeaderboardID;constraint:OnDelete:CASCADE" json:"tracks"`
|
||||||
|
PointRows []LeaderboardPointRow `gorm:"foreignKey:LeaderboardID;constraint:OnDelete:CASCADE" json:"pointsTable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LeaderboardDriver struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
||||||
|
LeaderboardID uuid.UUID `gorm:"not null;type:uuid" json:"-"`
|
||||||
|
Name string `gorm:"not null" json:"name"`
|
||||||
|
Initials string `json:"initials"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
Position int `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LeaderboardRace struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
||||||
|
LeaderboardID uuid.UUID `gorm:"not null;type:uuid" json:"-"`
|
||||||
|
Name string `gorm:"not null" json:"name"`
|
||||||
|
Position int `json:"-"`
|
||||||
|
FastestLapDriverID *uuid.UUID `gorm:"type:uuid" json:"fastestLapDriverId"`
|
||||||
|
Results []LeaderboardResult `gorm:"foreignKey:RaceID;constraint:OnDelete:CASCADE" json:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LeaderboardResult struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"-"`
|
||||||
|
RaceID uuid.UUID `gorm:"not null;type:uuid" json:"-"`
|
||||||
|
DriverID uuid.UUID `gorm:"not null;type:uuid" json:"driverId"`
|
||||||
|
Score Score `json:"score"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LeaderboardPointRow struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
||||||
|
LeaderboardID uuid.UUID `gorm:"not null;type:uuid" json:"-"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Points int `json:"points"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
TextColor string `json:"textColor"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Leaderboard) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if l.ID == uuid.Nil {
|
||||||
|
l.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *LeaderboardDriver) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if d.ID == uuid.Nil {
|
||||||
|
d.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LeaderboardRace) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if r.ID == uuid.Nil {
|
||||||
|
r.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LeaderboardResult) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if r.ID == uuid.Nil {
|
||||||
|
r.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LeaderboardPointRow) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if p.ID == uuid.Nil {
|
||||||
|
p.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
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
23
local/model/server.go
Normal file → Executable file
23
local/model/server.go
Normal file → Executable file
@@ -35,16 +35,19 @@ func (s *Server) ToServerAPI() *ServerAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;" json:"id"`
|
||||||
Name string `gorm:"not null" json:"name"`
|
Name string `gorm:"not null" json:"name"`
|
||||||
Status ServiceStatus `json:"status" gorm:"-"`
|
Status ServiceStatus `json:"status" gorm:"-"`
|
||||||
IP string `gorm:"not null" json:"-"`
|
IP string `gorm:"not null" json:"-"`
|
||||||
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"`
|
||||||
State *ServerState `gorm:"-" json:"state"`
|
Platform string `gorm:"not null;default:'windows'" json:"platform"`
|
||||||
DateCreated time.Time `json:"dateCreated"`
|
ContainerID string `gorm:"default:''" json:"-"`
|
||||||
FromSteamCMD bool `gorm:"not null; default:true" json:"-"`
|
State *ServerState `gorm:"-" json:"state"`
|
||||||
|
LastActivity *LastActivityInfo `gorm:"-" json:"lastActivity,omitempty"`
|
||||||
|
DateCreated time.Time `json:"dateCreated"`
|
||||||
|
FromSteamCMD bool `gorm:"not null; default:true" json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlayerState struct {
|
type PlayerState struct {
|
||||||
|
|||||||
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
5
local/model/websocket.go
Normal file → Executable file
5
local/model/websocket.go
Normal file → Executable file
@@ -33,6 +33,7 @@ const (
|
|||||||
MessageTypeSteamOutput WebSocketMessageType = "steam_output"
|
MessageTypeSteamOutput WebSocketMessageType = "steam_output"
|
||||||
MessageTypeError WebSocketMessageType = "error"
|
MessageTypeError WebSocketMessageType = "error"
|
||||||
MessageTypeComplete WebSocketMessageType = "complete"
|
MessageTypeComplete WebSocketMessageType = "complete"
|
||||||
|
MessageTypeLogLine WebSocketMessageType = "log_line"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WebSocketMessage struct {
|
type WebSocketMessage struct {
|
||||||
@@ -65,6 +66,10 @@ type CompleteMessage struct {
|
|||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LogLineMessage struct {
|
||||||
|
Line string `json:"line"`
|
||||||
|
}
|
||||||
|
|
||||||
func GetStepDescription(step ServerCreationStep) string {
|
func GetStepDescription(step ServerCreationStep) string {
|
||||||
descriptions := map[ServerCreationStep]string{
|
descriptions := map[ServerCreationStep]string{
|
||||||
StepValidation: "Validating server configuration",
|
StepValidation: "Validating server configuration",
|
||||||
|
|||||||
29
local/platform/interfaces.go
Normal file
29
local/platform/interfaces.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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)
|
||||||
|
GetLastLines(ctx context.Context, server *model.Server, n int) ([]string, error)
|
||||||
|
}
|
||||||
96
local/repository/activity_log.go
Normal file
96
local/repository/activity_log.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/model"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"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) {
|
||||||
|
result := &[]model.ActivityLog{}
|
||||||
|
query := r.BaseRepository.db.WithContext(ctx).
|
||||||
|
Model(&model.ActivityLog{}).
|
||||||
|
Preload("Server")
|
||||||
|
|
||||||
|
query = filter.ApplyFilter(query)
|
||||||
|
|
||||||
|
offset, limit := filter.Pagination()
|
||||||
|
query = query.Offset(offset).Limit(limit)
|
||||||
|
|
||||||
|
field, desc := filter.GetSorting()
|
||||||
|
if desc {
|
||||||
|
query = query.Order(field + " DESC")
|
||||||
|
} else {
|
||||||
|
query = query.Order(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Find(result).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting activity logs: %w", err)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
144
local/repository/leaderboard.go
Executable file
144
local/repository/leaderboard.go
Executable file
@@ -0,0 +1,144 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/model"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LeaderboardRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLeaderboardRepository(db *gorm.DB) *LeaderboardRepository {
|
||||||
|
return &LeaderboardRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrCreateByServerID fetches the leaderboard for a server, creating an empty one if it doesn't exist.
|
||||||
|
func (r *LeaderboardRepository) GetOrCreateByServerID(ctx context.Context, serverID uuid.UUID) (*model.Leaderboard, error) {
|
||||||
|
lb := new(model.Leaderboard)
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Preload("Drivers").
|
||||||
|
Preload("Races.Results").
|
||||||
|
Preload("PointRows").
|
||||||
|
Where("server_id = ?", serverID).
|
||||||
|
First(lb).Error
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return lb, nil
|
||||||
|
}
|
||||||
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fmt.Errorf("error fetching leaderboard: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lb = &model.Leaderboard{
|
||||||
|
ServerID: serverID,
|
||||||
|
FLPoints: 1,
|
||||||
|
FLColor: "#8b5cf6",
|
||||||
|
FLTextColor: "#000000",
|
||||||
|
}
|
||||||
|
if err := r.db.WithContext(ctx).Create(lb).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating leaderboard: %w", err)
|
||||||
|
}
|
||||||
|
return lb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FullReplace replaces all leaderboard data for a server in a single transaction.
|
||||||
|
func (r *LeaderboardRepository) FullReplace(ctx context.Context, serverID uuid.UUID, lb *model.Leaderboard) (*model.Leaderboard, error) {
|
||||||
|
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
existing := new(model.Leaderboard)
|
||||||
|
err := tx.Where("server_id = ?", serverID).First(existing).Error
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fmt.Errorf("error fetching existing leaderboard: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
lb.ServerID = serverID
|
||||||
|
return tx.Create(lb).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete children
|
||||||
|
if err := tx.Where("leaderboard_id = ?", existing.ID).Delete(&model.LeaderboardPointRow{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Where("leaderboard_id = ?", existing.ID).Delete(&model.LeaderboardDriver{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get race IDs to delete results
|
||||||
|
var raceIDs []uuid.UUID
|
||||||
|
tx.Model(&model.LeaderboardRace{}).Where("leaderboard_id = ?", existing.ID).Pluck("id", &raceIDs)
|
||||||
|
if len(raceIDs) > 0 {
|
||||||
|
if err := tx.Where("race_id IN ?", raceIDs).Delete(&model.LeaderboardResult{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := tx.Where("leaderboard_id = ?", existing.ID).Delete(&model.LeaderboardRace{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update parent fields
|
||||||
|
existing.FLPoints = lb.FLPoints
|
||||||
|
existing.FLColor = lb.FLColor
|
||||||
|
existing.FLTextColor = lb.FLTextColor
|
||||||
|
if err := tx.Save(existing).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-insert children with correct leaderboard ID
|
||||||
|
for i := range lb.Drivers {
|
||||||
|
lb.Drivers[i].LeaderboardID = existing.ID
|
||||||
|
}
|
||||||
|
if len(lb.Drivers) > 0 {
|
||||||
|
if err := tx.Create(&lb.Drivers).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range lb.PointRows {
|
||||||
|
lb.PointRows[i].LeaderboardID = existing.ID
|
||||||
|
lb.PointRows[i].ID = uuid.Nil
|
||||||
|
}
|
||||||
|
if len(lb.PointRows) > 0 {
|
||||||
|
if err := tx.Create(&lb.PointRows).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range lb.Races {
|
||||||
|
lb.Races[i].LeaderboardID = existing.ID
|
||||||
|
}
|
||||||
|
if len(lb.Races) > 0 {
|
||||||
|
// Create races first (without results) to get IDs
|
||||||
|
for i := range lb.Races {
|
||||||
|
results := lb.Races[i].Results
|
||||||
|
lb.Races[i].Results = nil
|
||||||
|
if err := tx.Create(&lb.Races[i]).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Re-attach results with correct race ID
|
||||||
|
for j := range results {
|
||||||
|
results[j].RaceID = lb.Races[i].ID
|
||||||
|
results[j].ID = uuid.Nil
|
||||||
|
}
|
||||||
|
if len(results) > 0 {
|
||||||
|
if err := tx.Create(&results).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lb.ID = existing.ID
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.GetOrCreateByServerID(ctx, serverID)
|
||||||
|
}
|
||||||
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
2
local/repository/repository.go
Normal file → Executable file
2
local/repository/repository.go
Normal file → Executable file
@@ -19,6 +19,8 @@ func InitializeRepositories(c *dig.Container) {
|
|||||||
c.Provide(NewLookupRepository)
|
c.Provide(NewLookupRepository)
|
||||||
c.Provide(NewSteamCredentialsRepository)
|
c.Provide(NewSteamCredentialsRepository)
|
||||||
c.Provide(NewMembershipRepository)
|
c.Provide(NewMembershipRepository)
|
||||||
|
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
65
local/service/activity_log.go
Normal file
65
local/service/activity_log.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(changedKeys) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
keysJSON, _ := json.Marshal(changedKeys)
|
||||||
|
return fmt.Sprintf(`{"file":%q,"changed_keys":%s}`, file, string(keysJSON))
|
||||||
|
}
|
||||||
143
local/service/config.go
Normal file → Executable file
143
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,104 @@ 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
|
||||||
|
}
|
||||||
|
if entry.fileName == SettingsJson {
|
||||||
|
if nameVal, ok := (*entry.body)["serverName"]; ok {
|
||||||
|
if nameStr, ok := nameVal.(string); ok && nameStr != "" && nameStr != server.Name {
|
||||||
|
server.Name = nameStr
|
||||||
|
if err := as.serverRepository.Update(ctx.UserContext(), server); err != nil {
|
||||||
|
logging.Error("Failed to update server name in DB: %v", 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 {
|
||||||
|
if details := BuildConfigDetails(entry.fileName, oldDataUTF8, newData); details != "" {
|
||||||
|
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 +249,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)
|
||||||
@@ -173,9 +267,34 @@ func (as *ConfigService) updateConfigInternal(ctx context.Context, serverID stri
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
as.configCache.InvalidateServerCache(serverID)
|
if configFile == SettingsJson {
|
||||||
|
if nameVal, ok := (*body)["serverName"]; ok {
|
||||||
|
if nameStr, ok := nameVal.(string); ok && nameStr != "" && nameStr != server.Name {
|
||||||
|
server.Name = nameStr
|
||||||
|
if err := as.serverRepository.Update(ctx, server); err != nil {
|
||||||
|
logging.Error("Failed to update server name in DB: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
as.configCache.InvalidateServerCache(serverID)
|
||||||
as.serverService.StartAccServerRuntime(server)
|
as.serverService.StartAccServerRuntime(server)
|
||||||
|
|
||||||
|
if as.activityLog != nil {
|
||||||
|
if details := BuildConfigDetails(configFile, oldDataUTF8, newData); details != "" {
|
||||||
|
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 +539,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)
|
||||||
@@ -433,3 +560,11 @@ func (as *ConfigService) SaveConfiguration(server *model.Server, config *model.C
|
|||||||
_, _, err = as.updateConfigFiles(context.Background(), server, ConfigurationJson, &configMap, true)
|
_, _, err = as.updateConfigFiles(context.Background(), server, ConfigurationJson, &configMap, true)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (as *ConfigService) SetServerName(server *model.Server, name string) error {
|
||||||
|
configMap := map[string]interface{}{
|
||||||
|
"serverName": name,
|
||||||
|
}
|
||||||
|
_, _, err := as.updateConfigFiles(context.Background(), server, SettingsJson, &configMap, false)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
108
local/service/docker_log_streamer.go
Normal file
108
local/service/docker_log_streamer.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/model"
|
||||||
|
"acc-server-manager/local/utl/logging"
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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 {
|
||||||
|
s.Stop(server.ID)
|
||||||
|
|
||||||
|
if server.ContainerID == "" {
|
||||||
|
logging.Warn("DockerLogStreamer.Start: no container ID for server %s, skipping", server.ID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
since := time.Now().UTC().Format(time.RFC3339)
|
||||||
|
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,
|
||||||
|
Since: since,
|
||||||
|
})
|
||||||
|
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)()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DockerLogStreamer) GetLastLines(ctx context.Context, server *model.Server, n int) ([]string, error) {
|
||||||
|
if n <= 0 || server.ContainerID == "" {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rc, err := s.client.ContainerLogs(ctx, server.ContainerID, types.ContainerLogsOptions{
|
||||||
|
ShowStdout: true,
|
||||||
|
ShowStderr: true,
|
||||||
|
Tail: strconv.Itoa(n),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
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.GetLastLines: stdcopy error for %s: %v", server.ServiceName, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
scanner := bufio.NewScanner(pr)
|
||||||
|
for scanner.Scan() {
|
||||||
|
lines = append(lines, scanner.Text())
|
||||||
|
}
|
||||||
|
return lines, scanner.Err()
|
||||||
|
}
|
||||||
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
77
local/service/file_log_streamer.go
Normal file
77
local/service/file_log_streamer.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/model"
|
||||||
|
"acc-server-manager/local/utl/tracking"
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FileLogStreamer) GetLastLines(_ context.Context, server *model.Server, n int) ([]string, error) {
|
||||||
|
if n <= 0 {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
logPath := filepath.Join(server.GetLogPath(), "server.log")
|
||||||
|
f, err := os.Open(logPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
ring := make([]string, n)
|
||||||
|
head := 0
|
||||||
|
count := 0
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
ring[head] = scanner.Text()
|
||||||
|
head = (head + 1) % n
|
||||||
|
if count < n {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if count < n {
|
||||||
|
return ring[:count], nil
|
||||||
|
}
|
||||||
|
out := make([]string, n)
|
||||||
|
copy(out, ring[head:])
|
||||||
|
copy(out[n-head:], ring[:head])
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
0
local/service/firewall_service.go
Normal file → Executable file
0
local/service/firewall_service.go
Normal file → Executable file
59
local/service/leaderboard.go
Executable file
59
local/service/leaderboard.go
Executable file
@@ -0,0 +1,59 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"acc-server-manager/local/model"
|
||||||
|
"acc-server-manager/local/repository"
|
||||||
|
"acc-server-manager/local/utl/logging"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LeaderboardService struct {
|
||||||
|
repo *repository.LeaderboardRepository
|
||||||
|
activityLog *ActivityLogService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLeaderboardService(repo *repository.LeaderboardRepository, activityLog *ActivityLogService) *LeaderboardService {
|
||||||
|
return &LeaderboardService{repo: repo, activityLog: activityLog}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the leaderboard for a server.
|
||||||
|
func (s *LeaderboardService) Get(ctx context.Context, serverID uuid.UUID) (*model.Leaderboard, error) {
|
||||||
|
return s.repo.GetOrCreateByServerID(ctx, serverID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update replaces the leaderboard for a server.
|
||||||
|
func (s *LeaderboardService) Update(ctx context.Context, serverID uuid.UUID, lb *model.Leaderboard, actorID, actorUsername string) (*model.Leaderboard, error) {
|
||||||
|
if lb == nil {
|
||||||
|
return nil, fmt.Errorf("input is required")
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
driverCount := len(lb.Drivers)
|
||||||
|
raceCount := len(lb.Races)
|
||||||
|
details := fmt.Sprintf(`{"driver_count":%d,"race_count":%d}`, driverCount, raceCount)
|
||||||
|
go func() {
|
||||||
|
if logErr := s.activityLog.Log(context.Background(), &model.ActivityLog{
|
||||||
|
ServerID: serverID,
|
||||||
|
UserID: actorID,
|
||||||
|
Username: actorUsername,
|
||||||
|
Action: model.ActionLeaderboardUpdate,
|
||||||
|
Details: details,
|
||||||
|
}); logErr != nil {
|
||||||
|
logging.Error("Failed to log leaderboard update: %v", logErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return result, 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 }
|
||||||
180
local/service/server.go
Normal file → Executable file
180
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,52 @@ 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")
|
handleFn := func(line string) {
|
||||||
tailer := tracking.NewLogTailer(logPath, instance.HandleLogLine)
|
instance.HandleLogLine(line)
|
||||||
s.logTailers.Store(server.ID, tailer)
|
s.webSocketService.BroadcastLogLine(server.ID, line)
|
||||||
|
}
|
||||||
tailer.Start()
|
if err := s.logStreamer.Start(context.Background(), server, handleFn); err != nil {
|
||||||
|
logging.Error("Failed to start log streaming for server %s: %v", server.ID, err)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetLastLogLines(ctx *fiber.Ctx, serverID uuid.UUID, n int) ([]string, error) {
|
||||||
|
server, err := s.repository.GetByID(ctx.UserContext(), serverID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if n <= 0 {
|
||||||
|
n = 100
|
||||||
|
}
|
||||||
|
return s.logStreamer.GetLastLines(ctx.UserContext(), server, n)
|
||||||
|
}
|
||||||
|
|
||||||
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,9 +212,19 @@ func (s *ServerService) updateSessionDuration(server *model.Server, sessionType
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerService) GenerateServerPath(server *model.Server) {
|
func (s *ServerService) GenerateServerPath(server *model.Server) {
|
||||||
steamCMDPath := env.GetSteamCMDDirPath()
|
server.GenerateUUID()
|
||||||
server.Path = server.GenerateServerPath(steamCMDPath)
|
if server.ServiceName == "" {
|
||||||
server.FromSteamCMD = true
|
server.ServiceName = server.GenerateServiceName()
|
||||||
|
}
|
||||||
|
|
||||||
|
if env.IsDockerPlatform() {
|
||||||
|
server.Path = filepath.Join(env.GetACCServersPath(), server.ServiceName)
|
||||||
|
server.FromSteamCMD = true
|
||||||
|
} else {
|
||||||
|
steamCMDPath := env.GetSteamCMDDirPath()
|
||||||
|
server.Path = server.GenerateServerPath(steamCMDPath)
|
||||||
|
server.FromSteamCMD = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerService) handleStateChange(server *model.Server, state *model.ServerState) {
|
func (s *ServerService) handleStateChange(server *model.Server, state *model.ServerState) {
|
||||||
@@ -250,9 +277,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 +284,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 +310,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 +327,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,17 +413,33 @@ func (s *ServerService) createServerBackground(ctx context.Context, server *mode
|
|||||||
important: true,
|
important: true,
|
||||||
description: "",
|
description: "",
|
||||||
callback: func() (string, error) {
|
callback: func() (string, error) {
|
||||||
ports, err := network.FindAvailablePortRange(DefaultStartPort, RequiredPortCount)
|
var port int
|
||||||
if err != nil {
|
var err error
|
||||||
return "", fmt.Errorf("failed to find available ports: %v", err)
|
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.configService.SetServerName(server, server.Name); err != nil {
|
||||||
|
logging.Error("Failed to set server name in config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("Server configuration generated (Port: %d)", serverPort), nil
|
return fmt.Sprintf("Server configuration generated (Port: %d)", serverPort), nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -396,12 +448,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 +460,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 +519,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 +541,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)
|
||||||
}
|
}
|
||||||
tcpPorts := []int{configuration.TcpPort.ToInt()}
|
if configuration != nil {
|
||||||
udpPorts := []int{configuration.UdpPort.ToInt()}
|
tcpPorts := []int{configuration.TcpPort.ToInt()}
|
||||||
if err := s.firewallService.DeleteServerRules(server.ServiceName, tcpPorts, udpPorts); err != nil {
|
udpPorts := []int{configuration.UdpPort.ToInt()}
|
||||||
logging.Error("Failed to delete firewall rules: %v", err)
|
if err := s.firewall.DeleteServerRules(server.ServiceName, tcpPorts, udpPorts); err != nil {
|
||||||
|
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 +592,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 +611,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
|
||||||
}
|
}
|
||||||
|
|||||||
72
local/service/service.go
Normal file → Executable file
72
local/service/service.go
Normal file → Executable file
@@ -1,38 +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)
|
||||||
|
|
||||||
|
// 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
41
local/service/steam_service.go
Normal file → Executable file
41
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,9 +105,11 @@ func (s *SteamService) InstallServerWithWebSocket(ctx context.Context, installPa
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.pathValidator.ValidateInstallPath(installPath); err != nil {
|
if !env.IsDockerPlatform() {
|
||||||
wsService.BroadcastSteamOutput(*serverID, fmt.Sprintf("Invalid installation path: %v", err), true)
|
if err := s.pathValidator.ValidateInstallPath(installPath); err != nil {
|
||||||
return fmt.Errorf("invalid installation path: %v", err)
|
wsService.BroadcastSteamOutput(*serverID, fmt.Sprintf("Invalid installation path: %v", err), true)
|
||||||
|
return fmt.Errorf("invalid installation path: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
absPath, err := filepath.Abs(installPath)
|
absPath, err := filepath.Abs(installPath)
|
||||||
@@ -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,9 +235,11 @@ func (s *SteamService) InstallServerWithCallbacks(ctx context.Context, installPa
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.pathValidator.ValidateInstallPath(installPath); err != nil {
|
if !env.IsDockerPlatform() {
|
||||||
outputCallback(*serverID, fmt.Sprintf("Invalid installation path: %v", err), true)
|
if err := s.pathValidator.ValidateInstallPath(installPath); err != nil {
|
||||||
return fmt.Errorf("invalid installation path: %v", err)
|
outputCallback(*serverID, fmt.Sprintf("Invalid installation path: %v", err), true)
|
||||||
|
return fmt.Errorf("invalid installation path: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
absPath, err := filepath.Abs(installPath)
|
absPath, err := filepath.Abs(installPath)
|
||||||
@@ -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",
|
||||||
|
|||||||
23
local/service/websocket.go
Normal file → Executable file
23
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 {
|
||||||
@@ -102,6 +103,16 @@ func (ws *WebSocketService) BroadcastError(serverID uuid.UUID, error string, det
|
|||||||
ws.broadcastToServer(serverID, wsMsg)
|
ws.broadcastToServer(serverID, wsMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ws *WebSocketService) BroadcastLogLine(serverID uuid.UUID, line string) {
|
||||||
|
msg := model.WebSocketMessage{
|
||||||
|
Type: model.MessageTypeLogLine,
|
||||||
|
ServerID: &serverID,
|
||||||
|
Timestamp: time.Now().Unix(),
|
||||||
|
Data: model.LogLineMessage{Line: line},
|
||||||
|
}
|
||||||
|
ws.broadcastToServer(serverID, msg)
|
||||||
|
}
|
||||||
|
|
||||||
func (ws *WebSocketService) BroadcastComplete(serverID uuid.UUID, success bool, message string) {
|
func (ws *WebSocketService) BroadcastComplete(serverID uuid.UUID, success bool, message string) {
|
||||||
completeMsg := model.CompleteMessage{
|
completeMsg := model.CompleteMessage{
|
||||||
ServerID: serverID,
|
ServerID: serverID,
|
||||||
@@ -131,7 +142,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 +156,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 +176,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 +186,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
49
local/utl/command/callback_executor.go
Normal file → Executable file
49
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
|
||||||
logging.Error("Failed to handle 2FA prompt: %v", err)
|
go func(prompt string) {
|
||||||
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true)
|
if err := e.handle2FAPrompt(ctx, prompt, serverID); err != nil {
|
||||||
done <- err
|
logging.Error("Failed to handle 2FA prompt: %v", err)
|
||||||
return
|
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true)
|
||||||
}
|
}
|
||||||
|
}(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 {
|
||||||
e.callbacks.OnOutput(e.serverID, "2FA prompt detected - waiting for user confirmation", false)
|
|
||||||
if err := e.handle2FAPrompt(ctx, lineData.text, serverID); err != nil {
|
|
||||||
logging.Error("Failed to handle 2FA prompt: %v", err)
|
|
||||||
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle 2FA prompt: %v", err), true)
|
|
||||||
done <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tfaRequestCreated = true
|
tfaRequestCreated = true
|
||||||
|
e.callbacks.OnOutput(e.serverID, "2FA prompt detected - waiting for user confirmation", false)
|
||||||
|
go func(prompt string) {
|
||||||
|
if err := e.handle2FAPrompt(ctx, prompt, serverID); err != nil {
|
||||||
|
logging.Error("Failed to handle 2FA prompt: %v", err)
|
||||||
|
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle 2FA prompt: %v", err), 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)
|
||||||
if err := e.handle2FAPrompt(ctx, "Steam CMD appears to be waiting for Steam Guard confirmation after startup", serverID); err != nil {
|
go func() {
|
||||||
logging.Error("Failed to handle Steam Guard 2FA prompt: %v", err)
|
if err := e.handle2FAPrompt(ctx, "Steam CMD appears to be waiting for Steam Guard confirmation after startup", serverID); err != nil {
|
||||||
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle Steam Guard 2FA prompt: %v", err), true)
|
logging.Error("Failed to handle Steam Guard 2FA prompt: %v", err)
|
||||||
done <- err
|
e.callbacks.OnOutput(e.serverID, fmt.Sprintf("Failed to handle Steam Guard 2FA prompt: %v", err), true)
|
||||||
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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user