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