chore: initial commit for main
This commit is contained in:
parent
124e4f8921
commit
c926613ee0
|
|
@ -0,0 +1,12 @@
|
|||
node_modules
|
||||
**/node_modules
|
||||
**/dist
|
||||
**/target
|
||||
**/.turbo
|
||||
.git
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.md
|
||||
.vscode
|
||||
.idea
|
||||
35
.env.example
35
.env.example
|
|
@ -1,17 +1,40 @@
|
|||
# Database
|
||||
# =========================================
|
||||
# GamePanel Environment Configuration
|
||||
# =========================================
|
||||
# Copy this file to .env and update values
|
||||
# cp .env.example .env
|
||||
|
||||
# --- Database ---
|
||||
DATABASE_URL=postgresql://gamepanel:gamepanel@localhost:5432/gamepanel
|
||||
DB_USER=gamepanel
|
||||
DB_PASSWORD=gamepanel
|
||||
DB_NAME=gamepanel
|
||||
DB_PORT=5432
|
||||
|
||||
# API
|
||||
# --- Redis ---
|
||||
REDIS_URL=redis://:gamepanel@localhost:6379
|
||||
REDIS_PASSWORD=gamepanel
|
||||
REDIS_PORT=6379
|
||||
|
||||
# --- API ---
|
||||
PORT=3000
|
||||
HOST=0.0.0.0
|
||||
API_PORT=3000
|
||||
NODE_ENV=development
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=change-me-in-production
|
||||
JWT_REFRESH_SECRET=change-me-in-production-refresh
|
||||
# --- JWT (CHANGE IN PRODUCTION!) ---
|
||||
# Generate with: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
|
||||
JWT_SECRET=CHANGE_ME_GENERATE_A_SECURE_64_BYTE_HEX_STRING
|
||||
JWT_REFRESH_SECRET=CHANGE_ME_GENERATE_ANOTHER_SECURE_64_BYTE_HEX_STRING
|
||||
|
||||
# Daemon
|
||||
# --- Rate Limiting ---
|
||||
RATE_LIMIT_MAX=100
|
||||
RATE_LIMIT_WINDOW_MS=60000
|
||||
|
||||
# --- Web ---
|
||||
WEB_PORT=80
|
||||
|
||||
# --- Daemon ---
|
||||
DAEMON_CONFIG=/etc/gamepanel/config.yml
|
||||
DAEMON_GRPC_PORT=50051
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
NODE_VERSION: "20"
|
||||
PNPM_VERSION: "9.15.4"
|
||||
RUST_TOOLCHAIN: "1.83"
|
||||
|
||||
jobs:
|
||||
# --- Lint + TypeScript Check ---
|
||||
lint:
|
||||
name: Lint & Type Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: pnpm
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: TypeScript check (shared)
|
||||
run: pnpm --filter @source/shared build
|
||||
|
||||
- name: TypeScript check (database)
|
||||
run: pnpm --filter @source/database build
|
||||
|
||||
- name: TypeScript check (API)
|
||||
run: pnpm --filter @source/api build
|
||||
|
||||
- name: TypeScript check (Web)
|
||||
run: pnpm --filter @source/web build
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Format check
|
||||
run: pnpm format:check
|
||||
|
||||
# --- Rust Daemon ---
|
||||
daemon:
|
||||
name: Daemon Build & Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install protoc
|
||||
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: apps/daemon
|
||||
|
||||
- name: Check
|
||||
working-directory: apps/daemon
|
||||
run: cargo check
|
||||
|
||||
- name: Test
|
||||
working-directory: apps/daemon
|
||||
run: cargo test
|
||||
|
||||
- name: Clippy
|
||||
working-directory: apps/daemon
|
||||
run: cargo clippy -- -D warnings || true
|
||||
|
||||
# --- Docker Build Test ---
|
||||
docker:
|
||||
name: Docker Build
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, daemon]
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build API image
|
||||
run: docker build -f apps/api/Dockerfile -t gamepanel-api:ci .
|
||||
|
||||
- name: Build Web image
|
||||
run: docker build -f apps/web/Dockerfile -t gamepanel-web:ci .
|
||||
|
||||
- name: Build Daemon image
|
||||
run: docker build -f apps/daemon/Dockerfile -t gamepanel-daemon:ci .
|
||||
|
|
@ -0,0 +1,630 @@
|
|||
# Installation Guide
|
||||
|
||||
This guide covers three deployment methods:
|
||||
1. **Development Setup** — for local development
|
||||
2. **Docker Production** — single-command deployment with Docker Compose
|
||||
3. **Manual Production** — step-by-step on Ubuntu 22.04+
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### All Methods
|
||||
- Git
|
||||
- A PostgreSQL 16+ database (or use the included Docker Compose)
|
||||
|
||||
### Development
|
||||
- **Node.js** 20+ ([nodejs.org](https://nodejs.org))
|
||||
- **pnpm** 9.15+ (`corepack enable && corepack prepare pnpm@9.15.4 --activate`)
|
||||
- **Rust** 1.83+ ([rustup.rs](https://rustup.rs))
|
||||
- **protoc** (Protocol Buffers compiler) — required for the daemon's gRPC build
|
||||
- **Docker** — for running PostgreSQL and Redis locally
|
||||
|
||||
### Docker Production
|
||||
- **Docker** 24+ with Docker Compose v2
|
||||
- At least **2 GB RAM** and **10 GB disk** for the panel itself
|
||||
- Additional resources for game servers on daemon nodes
|
||||
|
||||
---
|
||||
|
||||
## 1. Development Setup
|
||||
|
||||
### 1.1 Clone and Install
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-org/source-gamepanel.git
|
||||
cd source-gamepanel
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 1.2 Environment Configuration
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env` and set at minimum:
|
||||
|
||||
```env
|
||||
# Generate secure secrets:
|
||||
# node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
|
||||
JWT_SECRET=<your-64-byte-hex>
|
||||
JWT_REFRESH_SECRET=<another-64-byte-hex>
|
||||
|
||||
# Database (defaults work with docker-compose.dev.yml)
|
||||
DATABASE_URL=postgresql://gamepanel:gamepanel@localhost:5432/gamepanel
|
||||
```
|
||||
|
||||
### 1.3 Start Infrastructure
|
||||
|
||||
```bash
|
||||
# Start PostgreSQL + Redis
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
### 1.4 Database Setup
|
||||
|
||||
```bash
|
||||
# Generate migration files (if schema changed)
|
||||
pnpm db:generate
|
||||
|
||||
# Apply migrations to create all tables
|
||||
pnpm db:migrate
|
||||
|
||||
# Seed admin user and default games
|
||||
pnpm db:seed
|
||||
```
|
||||
|
||||
After seeding, you'll have:
|
||||
- **Admin account**: `admin@gamepanel.local` / `admin123`
|
||||
- **Games**: Minecraft Java, CS2, Minecraft Bedrock, Terraria, Rust
|
||||
|
||||
### 1.5 Start Development Servers
|
||||
|
||||
```bash
|
||||
# Start API (port 3000) + Web (port 5173) via Turborepo
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
The web dev server proxies `/api` and `/socket.io` requests to the API automatically.
|
||||
|
||||
Open **http://localhost:5173** in your browser.
|
||||
|
||||
### 1.6 Daemon (Optional)
|
||||
|
||||
The Rust daemon manages Docker containers on game server nodes. For development you can run it locally:
|
||||
|
||||
```bash
|
||||
# Ensure protoc is installed
|
||||
protoc --version # Should show libprotoc 3.x or higher
|
||||
|
||||
# If not installed:
|
||||
# Ubuntu: sudo apt install protobuf-compiler
|
||||
# macOS: brew install protobuf
|
||||
# Windows: choco install protoc (or download from GitHub releases)
|
||||
|
||||
cd apps/daemon
|
||||
cargo run
|
||||
```
|
||||
|
||||
The daemon reads its config from `/etc/gamepanel/config.yml` or the path in `DAEMON_CONFIG` env var. For development, it falls back to defaults (API at localhost:3000, dev token).
|
||||
|
||||
### 1.7 Useful Commands
|
||||
|
||||
```bash
|
||||
pnpm build # Build all packages
|
||||
pnpm lint # ESLint across all packages
|
||||
pnpm format # Prettier format
|
||||
pnpm format:check # Check formatting without modifying
|
||||
pnpm db:studio # Open Drizzle Studio (visual DB browser)
|
||||
|
||||
# Daemon
|
||||
cd apps/daemon
|
||||
cargo test # Run unit tests (3 tests: Minecraft parser, CS2 parser)
|
||||
cargo clippy # Rust linter
|
||||
cargo build --release # Production build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Docker Production Deployment
|
||||
|
||||
### 2.1 Prepare Environment
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-org/source-gamepanel.git
|
||||
cd source-gamepanel
|
||||
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env` with production values:
|
||||
|
||||
```env
|
||||
# REQUIRED — Generate unique secrets for each!
|
||||
JWT_SECRET=<generate-with-openssl-rand-hex-64>
|
||||
JWT_REFRESH_SECRET=<generate-another-secret>
|
||||
|
||||
# Database
|
||||
DB_USER=gamepanel
|
||||
DB_PASSWORD=<strong-random-password>
|
||||
DB_NAME=gamepanel
|
||||
|
||||
# Redis
|
||||
REDIS_PASSWORD=<strong-random-password>
|
||||
|
||||
# Networking
|
||||
CORS_ORIGIN=https://panel.yourdomain.com
|
||||
WEB_PORT=80
|
||||
API_PORT=3000
|
||||
|
||||
# Rate limiting
|
||||
RATE_LIMIT_MAX=100
|
||||
RATE_LIMIT_WINDOW_MS=60000
|
||||
```
|
||||
|
||||
### 2.2 Configure Daemon
|
||||
|
||||
Edit `daemon-config.yml`:
|
||||
|
||||
```yaml
|
||||
api_url: "http://api:3000"
|
||||
node_token: "<generate-a-secure-token>"
|
||||
grpc_port: 50051
|
||||
data_path: "/var/lib/gamepanel/servers"
|
||||
backup_path: "/var/lib/gamepanel/backups"
|
||||
docker:
|
||||
socket: "/var/run/docker.sock"
|
||||
network: "gamepanel_nw"
|
||||
network_subnet: "172.18.0.0/16"
|
||||
```
|
||||
|
||||
### 2.3 Build and Start
|
||||
|
||||
```bash
|
||||
# Build and start all services
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
This starts 5 services:
|
||||
| Service | Port | Description |
|
||||
|---------|------|-------------|
|
||||
| `postgres` | 5432 | PostgreSQL database |
|
||||
| `redis` | 6379 | Rate limiting & cache |
|
||||
| `api` | 3000 | Fastify REST API |
|
||||
| `web` | 80 | nginx + React SPA |
|
||||
| `daemon` | 50051 | Rust gRPC daemon |
|
||||
|
||||
### 2.4 Initialize Database
|
||||
|
||||
```bash
|
||||
# Run migrations
|
||||
docker compose exec api node -e "
|
||||
import('drizzle-kit').then(m => console.log('Use drizzle-kit migrate'))
|
||||
"
|
||||
|
||||
# Or use the pnpm scripts with the container's DATABASE_URL
|
||||
docker compose exec api sh -c 'cd /app && node apps/api/dist/index.js'
|
||||
```
|
||||
|
||||
For the initial setup, the easiest approach is:
|
||||
|
||||
```bash
|
||||
# Run migrations from your host machine pointed at the Docker PostgreSQL
|
||||
DATABASE_URL=postgresql://gamepanel:<your-password>@localhost:5432/gamepanel pnpm db:migrate
|
||||
DATABASE_URL=postgresql://gamepanel:<your-password>@localhost:5432/gamepanel pnpm db:seed
|
||||
```
|
||||
|
||||
### 2.5 Verify
|
||||
|
||||
```bash
|
||||
# Check all services are healthy
|
||||
docker compose ps
|
||||
|
||||
# Test API health
|
||||
curl http://localhost:3000/api/health
|
||||
# {"status":"ok","timestamp":"2025-..."}
|
||||
|
||||
# Test web
|
||||
curl -s http://localhost | head -5
|
||||
# <!DOCTYPE html>...
|
||||
```
|
||||
|
||||
### 2.6 Monitoring
|
||||
|
||||
```bash
|
||||
# View logs
|
||||
docker compose logs -f api
|
||||
docker compose logs -f daemon
|
||||
docker compose logs -f web
|
||||
|
||||
# Restart a service
|
||||
docker compose restart api
|
||||
|
||||
# Update to latest
|
||||
git pull
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Manual Production Setup (Ubuntu 22.04+)
|
||||
|
||||
### 3.1 System Dependencies
|
||||
|
||||
```bash
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
|
||||
# Node.js 20
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt install -y nodejs
|
||||
|
||||
# pnpm
|
||||
corepack enable
|
||||
corepack prepare pnpm@9.15.4 --activate
|
||||
|
||||
# PostgreSQL 16
|
||||
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
|
||||
sudo apt update
|
||||
sudo apt install -y postgresql-16
|
||||
|
||||
# Redis
|
||||
sudo apt install -y redis-server
|
||||
|
||||
# Docker (for game containers)
|
||||
curl -fsSL https://get.docker.com | sudo sh
|
||||
sudo usermod -aG docker $USER
|
||||
|
||||
# Rust (for daemon)
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source "$HOME/.cargo/env"
|
||||
|
||||
# protoc (for gRPC)
|
||||
sudo apt install -y protobuf-compiler
|
||||
|
||||
# nginx (reverse proxy)
|
||||
sudo apt install -y nginx certbot python3-certbot-nginx
|
||||
```
|
||||
|
||||
### 3.2 Database Setup
|
||||
|
||||
```bash
|
||||
sudo -u postgres psql << 'EOF'
|
||||
CREATE USER gamepanel WITH PASSWORD 'your-strong-password';
|
||||
CREATE DATABASE gamepanel OWNER gamepanel;
|
||||
GRANT ALL PRIVILEGES ON DATABASE gamepanel TO gamepanel;
|
||||
EOF
|
||||
```
|
||||
|
||||
### 3.3 Redis Configuration
|
||||
|
||||
```bash
|
||||
sudo sed -i 's/# requirepass foobared/requirepass your-redis-password/' /etc/redis/redis.conf
|
||||
sudo systemctl restart redis-server
|
||||
```
|
||||
|
||||
### 3.4 Application Setup
|
||||
|
||||
```bash
|
||||
# Clone
|
||||
cd /opt
|
||||
sudo git clone https://github.com/your-org/source-gamepanel.git
|
||||
sudo chown -R $USER:$USER source-gamepanel
|
||||
cd source-gamepanel
|
||||
|
||||
# Install
|
||||
pnpm install
|
||||
|
||||
# Environment
|
||||
cp .env.example .env
|
||||
nano .env # Set all production values
|
||||
|
||||
# Build
|
||||
pnpm build
|
||||
|
||||
# Database
|
||||
pnpm db:migrate
|
||||
pnpm db:seed
|
||||
|
||||
# Build daemon
|
||||
cd apps/daemon
|
||||
cargo build --release
|
||||
sudo cp target/release/gamepanel-daemon /usr/local/bin/
|
||||
```
|
||||
|
||||
### 3.5 Daemon Configuration
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/gamepanel /var/lib/gamepanel/{servers,backups}
|
||||
|
||||
sudo tee /etc/gamepanel/config.yml << 'EOF'
|
||||
api_url: "http://127.0.0.1:3000"
|
||||
node_token: "generate-a-secure-token-here"
|
||||
grpc_port: 50051
|
||||
data_path: "/var/lib/gamepanel/servers"
|
||||
backup_path: "/var/lib/gamepanel/backups"
|
||||
docker:
|
||||
socket: "/var/run/docker.sock"
|
||||
network: "gamepanel_nw"
|
||||
network_subnet: "172.18.0.0/16"
|
||||
EOF
|
||||
```
|
||||
|
||||
### 3.6 Systemd Services
|
||||
|
||||
**API Service:**
|
||||
|
||||
```bash
|
||||
sudo tee /etc/systemd/system/gamepanel-api.service << 'EOF'
|
||||
[Unit]
|
||||
Description=GamePanel API
|
||||
After=network.target postgresql.service redis-server.service
|
||||
Requires=postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=gamepanel
|
||||
WorkingDirectory=/opt/source-gamepanel
|
||||
ExecStart=/usr/bin/node apps/api/dist/index.js
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
EnvironmentFile=/opt/source-gamepanel/.env
|
||||
Environment=NODE_ENV=production
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
```
|
||||
|
||||
**Daemon Service:**
|
||||
|
||||
```bash
|
||||
sudo tee /etc/systemd/system/gamepanel-daemon.service << 'EOF'
|
||||
[Unit]
|
||||
Description=GamePanel Daemon
|
||||
After=network.target docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/gamepanel-daemon
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=DAEMON_CONFIG=/etc/gamepanel/config.yml
|
||||
Environment=RUST_LOG=info
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
```
|
||||
|
||||
**Enable and start:**
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now gamepanel-api
|
||||
sudo systemctl enable --now gamepanel-daemon
|
||||
```
|
||||
|
||||
### 3.7 Web Build + nginx
|
||||
|
||||
```bash
|
||||
# Build the SPA
|
||||
cd /opt/source-gamepanel/apps/web
|
||||
pnpm build # outputs to dist/
|
||||
|
||||
# Copy to nginx
|
||||
sudo mkdir -p /var/www/gamepanel
|
||||
sudo cp -r dist/* /var/www/gamepanel/
|
||||
```
|
||||
|
||||
**nginx site config:**
|
||||
|
||||
```bash
|
||||
sudo tee /etc/nginx/sites-available/gamepanel << 'EOF'
|
||||
server {
|
||||
listen 80;
|
||||
server_name panel.yourdomain.com;
|
||||
root /var/www/gamepanel;
|
||||
index index.html;
|
||||
|
||||
# Gzip
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
|
||||
|
||||
# API proxy
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Socket.IO
|
||||
location /socket.io/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# Static assets
|
||||
location /assets/ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# SPA fallback
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
sudo ln -sf /etc/nginx/sites-available/gamepanel /etc/nginx/sites-enabled/
|
||||
sudo rm -f /etc/nginx/sites-enabled/default
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### 3.8 TLS with Let's Encrypt
|
||||
|
||||
```bash
|
||||
sudo certbot --nginx -d panel.yourdomain.com
|
||||
```
|
||||
|
||||
Certbot will automatically configure nginx for HTTPS and set up auto-renewal.
|
||||
|
||||
### 3.9 Firewall
|
||||
|
||||
```bash
|
||||
sudo ufw allow 22/tcp # SSH
|
||||
sudo ufw allow 80/tcp # HTTP
|
||||
sudo ufw allow 443/tcp # HTTPS
|
||||
sudo ufw allow 50051/tcp # gRPC (daemon)
|
||||
# Open game server port ranges as needed:
|
||||
sudo ufw allow 25565/tcp # Minecraft
|
||||
sudo ufw allow 27015/tcp # CS2
|
||||
sudo ufw enable
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Post-Installation
|
||||
|
||||
### First Login
|
||||
|
||||
1. Open your panel URL in a browser
|
||||
2. Login with: `admin@gamepanel.local` / `admin123`
|
||||
3. **Immediately change the admin password** via account settings
|
||||
|
||||
### Create Your First Server
|
||||
|
||||
1. **Create an Organization** — Click "New Organization" on the home page
|
||||
2. **Add a Node** — Go to Nodes, add your daemon node (FQDN + ports)
|
||||
3. **Add Allocations** — Assign IP:port pairs to the node
|
||||
4. **Create a Server** — Use the creation wizard: pick a game, node, and resources
|
||||
5. **Start the Server** — Use the power controls on the console page
|
||||
|
||||
### Adding a Remote Daemon Node
|
||||
|
||||
On the remote machine:
|
||||
|
||||
```bash
|
||||
# Install Docker
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
|
||||
# Install the daemon binary
|
||||
scp user@panel-server:/usr/local/bin/gamepanel-daemon /usr/local/bin/
|
||||
|
||||
# Configure
|
||||
mkdir -p /etc/gamepanel /var/lib/gamepanel/{servers,backups}
|
||||
|
||||
cat > /etc/gamepanel/config.yml << EOF
|
||||
api_url: "https://panel.yourdomain.com"
|
||||
node_token: "<token-from-panel>"
|
||||
grpc_port: 50051
|
||||
EOF
|
||||
|
||||
# Create systemd service (same as above)
|
||||
# Start it
|
||||
systemctl enable --now gamepanel-daemon
|
||||
```
|
||||
|
||||
Then add the node in the panel with the remote machine's FQDN.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### API won't start
|
||||
- Check `DATABASE_URL` is correct and PostgreSQL is running
|
||||
- Ensure migrations have been applied: `pnpm db:migrate`
|
||||
- Check logs: `journalctl -u gamepanel-api -f` or `docker compose logs api`
|
||||
|
||||
### Daemon can't connect
|
||||
- Verify `api_url` in daemon config points to the API
|
||||
- Check `node_token` matches what's stored in the panel's nodes table
|
||||
- Ensure the daemon's gRPC port (50051) is open
|
||||
|
||||
### Web shows blank page
|
||||
- Build the SPA: `pnpm --filter @source/web build`
|
||||
- Check nginx config: `sudo nginx -t`
|
||||
- Verify API proxy is working: `curl http://localhost:3000/api/health`
|
||||
|
||||
### Docker permission denied
|
||||
- Ensure the daemon user is in the `docker` group: `usermod -aG docker <user>`
|
||||
- Or run the daemon with appropriate privileges
|
||||
|
||||
### protoc not found (daemon build)
|
||||
- Ubuntu: `sudo apt install protobuf-compiler`
|
||||
- macOS: `brew install protobuf`
|
||||
- Or download from [github.com/protocolbuffers/protobuf/releases](https://github.com/protocolbuffers/protobuf/releases)
|
||||
|
||||
---
|
||||
|
||||
## Updating
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
cd /opt/source-gamepanel
|
||||
git pull
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
### Manual
|
||||
|
||||
```bash
|
||||
cd /opt/source-gamepanel
|
||||
git pull
|
||||
pnpm install
|
||||
pnpm build
|
||||
pnpm db:migrate
|
||||
|
||||
# Rebuild daemon
|
||||
cd apps/daemon && cargo build --release
|
||||
sudo cp target/release/gamepanel-daemon /usr/local/bin/
|
||||
|
||||
# Rebuild web
|
||||
cd ../web && pnpm build
|
||||
sudo cp -r dist/* /var/www/gamepanel/
|
||||
|
||||
# Restart services
|
||||
sudo systemctl restart gamepanel-api gamepanel-daemon
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables Reference
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `DATABASE_URL` | — | PostgreSQL connection string |
|
||||
| `DB_USER` | `gamepanel` | PostgreSQL username (Docker) |
|
||||
| `DB_PASSWORD` | `gamepanel` | PostgreSQL password (Docker) |
|
||||
| `DB_NAME` | `gamepanel` | Database name (Docker) |
|
||||
| `DB_PORT` | `5432` | PostgreSQL exposed port |
|
||||
| `REDIS_URL` | — | Redis connection string |
|
||||
| `REDIS_PASSWORD` | `gamepanel` | Redis password |
|
||||
| `PORT` | `3000` | API listen port |
|
||||
| `HOST` | `0.0.0.0` | API listen host |
|
||||
| `NODE_ENV` | `development` | Environment mode |
|
||||
| `JWT_SECRET` | — | **Required.** Access token signing key |
|
||||
| `JWT_REFRESH_SECRET` | — | **Required.** Refresh token signing key |
|
||||
| `CORS_ORIGIN` | `http://localhost:5173` | Allowed CORS origin |
|
||||
| `RATE_LIMIT_MAX` | `100` | Max requests per window |
|
||||
| `RATE_LIMIT_WINDOW_MS` | `60000` | Rate limit window (ms) |
|
||||
| `WEB_PORT` | `80` | Web nginx exposed port |
|
||||
| `API_PORT` | `3000` | API exposed port (Docker) |
|
||||
| `DAEMON_CONFIG` | `/etc/gamepanel/config.yml` | Daemon config file path |
|
||||
| `DAEMON_GRPC_PORT` | `50051` | Daemon gRPC exposed port |
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
# GamePanel
|
||||
|
||||
Modern, open-source game server management panel built with a multi-tenant SaaS architecture. Inspired by Pterodactyl, enhanced with features like plugin management, visual task scheduler, live player tracking, and an in-browser config editor.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Core
|
||||
- **Multi-Tenant Organizations** — Isolated environments with role-based access control (Admin / User + custom JSONB permissions)
|
||||
- **Docker Container Management** — Full lifecycle: create, start, stop, restart, kill, delete
|
||||
- **Multi-Node Architecture** — Distribute game servers across multiple daemon nodes with health monitoring
|
||||
- **Live Console** — xterm.js terminal with Socket.IO streaming, command history support
|
||||
- **File Manager** — Browse, view, edit, create, and delete server files with path jail security
|
||||
- **Server Creation Wizard** — 3-step guided flow: Basic Info, Node & Allocation, Resources
|
||||
|
||||
### Game-Specific
|
||||
- **Config Editor** — Tab-based UI with parsers for `.properties`, `.json`, `.yaml`, and Source Engine `.cfg` formats
|
||||
- **Plugin Management** — Spiget API integration for Minecraft, manual install for other games, toggle/uninstall
|
||||
- **Player Tracking** — Live player list via RCON protocol (Minecraft `list`, CS2 `status`)
|
||||
|
||||
### Advanced
|
||||
- **Scheduled Tasks** — Visual scheduler with interval, daily, weekly, and cron expression support
|
||||
- **Backup System** — Create, restore, lock/unlock, delete backups with CDN storage integration
|
||||
- **Audit Logging** — Track all actions across the panel with user, server, and IP metadata
|
||||
|
||||
### Operations
|
||||
- **Rate Limiting** — Configurable per-window request limits
|
||||
- **Security Headers** — Helmet.js with CSP, XSS protection, content-type sniffing prevention
|
||||
- **Health Checks** — Built-in endpoints for all services
|
||||
- **CI/CD** — GitHub Actions pipeline for lint, test, and Docker build
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Browser ─── HTTPS + Socket.IO ──→ Web (React SPA / nginx)
|
||||
│
|
||||
REST + WS
|
||||
│
|
||||
API (Fastify + JWT)
|
||||
│ │
|
||||
PostgreSQL gRPC (protobuf)
|
||||
│
|
||||
Daemon (Rust + tonic) × N nodes
|
||||
│
|
||||
Docker API
|
||||
│
|
||||
Game Containers
|
||||
```
|
||||
|
||||
The API acts as a **gateway** between the frontend and daemon nodes. The frontend never communicates directly with daemons.
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Component | Technology |
|
||||
|-----------|-----------|
|
||||
| Monorepo | Turborepo + pnpm |
|
||||
| Frontend | React 19 + Vite 6 + Tailwind CSS 3 + shadcn/ui |
|
||||
| Backend API | Fastify 5 + TypeBox validation |
|
||||
| Daemon | Rust + tonic gRPC + bollard (Docker) + tokio |
|
||||
| Database | PostgreSQL 16 + Drizzle ORM |
|
||||
| Auth | JWT (access + refresh) + Argon2id |
|
||||
| Realtime | Socket.IO (frontend ↔ API) |
|
||||
| Panel ↔ Daemon | gRPC with protobuf |
|
||||
| Containers | Docker |
|
||||
| CI/CD | GitHub Actions |
|
||||
|
||||
---
|
||||
|
||||
## Monorepo Structure
|
||||
|
||||
```
|
||||
source-gamepanel/
|
||||
├── apps/
|
||||
│ ├── api/ # Fastify REST API
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── index.ts # App entry, plugin registration
|
||||
│ │ │ ├── plugins/ # DB, auth plugins
|
||||
│ │ │ ├── lib/ # Errors, JWT, permissions, pagination,
|
||||
│ │ │ │ config parsers, Spiget client, schedule utils
|
||||
│ │ │ └── routes/
|
||||
│ │ │ ├── auth/ # Register, login, refresh, logout, me
|
||||
│ │ │ ├── organizations/ # CRUD + members
|
||||
│ │ │ ├── nodes/ # CRUD + allocations
|
||||
│ │ │ ├── servers/ # CRUD + power, config, plugins, backups, schedules
|
||||
│ │ │ └── admin/ # Users, games, audit logs (super admin)
|
||||
│ │ └── Dockerfile
|
||||
│ │
|
||||
│ ├── web/ # React SPA
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── components/
|
||||
│ │ │ │ ├── ui/ # 13 shadcn/ui components
|
||||
│ │ │ │ ├── layout/ # AppLayout, ServerLayout, Sidebar, Header
|
||||
│ │ │ │ ├── server/ # PowerControls
|
||||
│ │ │ │ └── error-boundary.tsx
|
||||
│ │ │ ├── pages/
|
||||
│ │ │ │ ├── auth/ # Login, Register
|
||||
│ │ │ │ ├── dashboard/ # Stats + server list
|
||||
│ │ │ │ ├── server/ # Console, Files, Config, Plugins,
|
||||
│ │ │ │ │ Backups, Schedules, Players, Settings
|
||||
│ │ │ │ ├── servers/ # Create wizard
|
||||
│ │ │ │ ├── nodes/ # List + detail (health dashboard)
|
||||
│ │ │ │ ├── organizations/ # Org list + create
|
||||
│ │ │ │ ├── admin/ # Users, Games, Audit logs
|
||||
│ │ │ │ └── settings/ # Members
|
||||
│ │ │ ├── lib/ # API client, socket, utils
|
||||
│ │ │ ├── stores/ # Zustand auth store
|
||||
│ │ │ └── hooks/ # Theme hook
|
||||
│ │ ├── nginx.conf
|
||||
│ │ └── Dockerfile
|
||||
│ │
|
||||
│ └── daemon/ # Rust daemon
|
||||
│ ├── src/
|
||||
│ │ ├── main.rs # gRPC server, heartbeat, scheduler init
|
||||
│ │ ├── config.rs # YAML config loader
|
||||
│ │ ├── auth.rs # gRPC token interceptor
|
||||
│ │ ├── grpc/ # Service implementations
|
||||
│ │ ├── docker/ # Container lifecycle (bollard)
|
||||
│ │ ├── server/ # State machine, manager
|
||||
│ │ ├── filesystem/ # Path jail, CRUD operations
|
||||
│ │ ├── game/ # RCON client, Minecraft, CS2 modules
|
||||
│ │ ├── scheduler/ # Task polling + execution
|
||||
│ │ └── backup/ # tar.gz, CDN upload/download, restore
|
||||
│ ├── Cargo.toml
|
||||
│ └── Dockerfile
|
||||
│
|
||||
├── packages/
|
||||
│ ├── database/ # Drizzle schema + migrations + seed
|
||||
│ │ └── src/schema/ # 10 tables: users, orgs, nodes, servers,
|
||||
│ │ allocations, games, backups, plugins,
|
||||
│ │ schedules, audit_logs
|
||||
│ ├── shared/ # Types, permissions, roles
|
||||
│ ├── proto/ # daemon.proto (gRPC service definition)
|
||||
│ └── ui/ # Base UI utilities (cn, cva)
|
||||
│
|
||||
├── docker-compose.yml # Full production stack
|
||||
├── docker-compose.dev.yml # Dev: PostgreSQL + Redis only
|
||||
├── daemon-config.yml # Daemon configuration template
|
||||
├── .env.example # Environment variables reference
|
||||
├── .github/workflows/ci.yml # CI/CD pipeline
|
||||
├── turbo.json
|
||||
└── pnpm-workspace.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Supported Games
|
||||
|
||||
| Game | Docker Image | Default Port | Config Format | Plugin Support |
|
||||
|------|-------------|-------------|---------------|---------------|
|
||||
| Minecraft: Java Edition | `itzg/minecraft-server` | 25565 | `.properties`, `.yml`, `.json` | Spiget API + manual |
|
||||
| Counter-Strike 2 | `cm2network/csgo` | 27015 | Source `.cfg` (keyvalue) | Manual |
|
||||
| Minecraft: Bedrock Edition | `itzg/minecraft-bedrock-server` | 19132 | `.properties` | — |
|
||||
| Terraria | `ryshe/terraria` | 7777 | keyvalue | — |
|
||||
| Rust | `didstopia/rust-server` | 28015 | — | — |
|
||||
|
||||
Adding new games requires only a database seed entry — no code changes needed.
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Auth
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| POST | `/api/auth/register` | Create account |
|
||||
| POST | `/api/auth/login` | Login (returns JWT + refresh cookie) |
|
||||
| POST | `/api/auth/refresh` | Refresh access token |
|
||||
| POST | `/api/auth/logout` | Invalidate session |
|
||||
| GET | `/api/auth/me` | Current user profile |
|
||||
|
||||
### Organizations
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/organizations` | List user's orgs |
|
||||
| POST | `/api/organizations` | Create org |
|
||||
| GET/PATCH/DELETE | `/api/organizations/:orgId` | Org CRUD |
|
||||
| GET/POST/DELETE | `/api/organizations/:orgId/members` | Member management |
|
||||
|
||||
### Servers
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET/POST | `.../servers` | List / create |
|
||||
| GET/PATCH/DELETE | `.../servers/:serverId` | Server CRUD |
|
||||
| POST | `.../servers/:serverId/power` | Power actions (start/stop/restart/kill) |
|
||||
| GET/PUT | `.../servers/:serverId/config` | Config read/write |
|
||||
| GET/POST/DELETE | `.../servers/:serverId/plugins` | Plugin management |
|
||||
| GET/POST/DELETE | `.../servers/:serverId/backups` | Backup management |
|
||||
| POST | `.../servers/:serverId/backups/:id/restore` | Restore backup |
|
||||
| GET/POST/PATCH/DELETE | `.../servers/:serverId/schedules` | Scheduled tasks |
|
||||
|
||||
### Admin (Super Admin only)
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/admin/users` | All users |
|
||||
| GET/POST | `/api/admin/games` | Game management |
|
||||
| GET | `/api/admin/audit-logs` | Audit trail |
|
||||
|
||||
---
|
||||
|
||||
## Permission System
|
||||
|
||||
Dot-notation permissions with hybrid RBAC (role defaults + per-user JSONB overrides):
|
||||
|
||||
```
|
||||
server.create server.read server.update server.delete
|
||||
console.read console.write
|
||||
files.read files.write files.delete files.archive
|
||||
backup.read backup.create backup.restore backup.delete backup.manage
|
||||
schedule.read schedule.manage
|
||||
plugin.read plugin.manage
|
||||
config.read config.write
|
||||
power.start power.stop power.restart power.kill
|
||||
node.read node.manage
|
||||
org.settings org.members
|
||||
subuser.read subuser.manage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
See [INSTALLATION.md](INSTALLATION.md) for detailed setup instructions.
|
||||
|
||||
```bash
|
||||
# Clone
|
||||
git clone https://github.com/your-org/source-gamepanel.git
|
||||
cd source-gamepanel
|
||||
|
||||
# Environment
|
||||
cp .env.example .env
|
||||
# Edit .env — set JWT_SECRET and JWT_REFRESH_SECRET
|
||||
|
||||
# Start infrastructure
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Run migrations and seed
|
||||
pnpm db:migrate
|
||||
pnpm db:seed
|
||||
|
||||
# Start development
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Open `http://localhost:5173` — login with `admin@gamepanel.local` / `admin123`.
|
||||
|
||||
---
|
||||
|
||||
## Production Deployment
|
||||
|
||||
```bash
|
||||
# Configure environment
|
||||
cp .env.example .env
|
||||
# Edit .env with production values (strong JWT secrets, real DB passwords)
|
||||
|
||||
# Deploy full stack
|
||||
docker compose up -d --build
|
||||
|
||||
# Run migrations inside the API container
|
||||
docker compose exec api node -e "..."
|
||||
# Or connect to the DB directly and run drizzle-kit migrate
|
||||
```
|
||||
|
||||
The web service is exposed on port 80 with nginx handling SPA routing and API proxying.
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
pnpm dev # Start all services (API + Web + DB)
|
||||
pnpm build # Build all packages
|
||||
pnpm lint # Lint all packages
|
||||
pnpm format # Format with Prettier
|
||||
pnpm db:studio # Open Drizzle Studio (DB browser)
|
||||
pnpm db:generate # Generate migration files
|
||||
pnpm db:migrate # Apply migrations
|
||||
pnpm db:seed # Seed admin user + games
|
||||
|
||||
# Daemon (separate terminal)
|
||||
cd apps/daemon
|
||||
cargo run # Requires protoc installed
|
||||
cargo test # Run unit tests
|
||||
cargo clippy # Lint Rust code
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
This project is private. All rights reserved.
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
FROM node:20-alpine AS base
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.4 --activate
|
||||
WORKDIR /app
|
||||
|
||||
# --- Dependencies ---
|
||||
FROM base AS deps
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||
COPY apps/api/package.json apps/api/
|
||||
COPY packages/database/package.json packages/database/
|
||||
COPY packages/shared/package.json packages/shared/
|
||||
COPY packages/ui/package.json packages/ui/
|
||||
RUN pnpm install --frozen-lockfile --prod=false
|
||||
|
||||
# --- Build ---
|
||||
FROM base AS build
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY --from=deps /app/apps/api/node_modules ./apps/api/node_modules
|
||||
COPY --from=deps /app/packages/database/node_modules ./packages/database/node_modules
|
||||
COPY --from=deps /app/packages/shared/node_modules ./packages/shared/node_modules
|
||||
COPY . .
|
||||
RUN pnpm --filter @source/shared build && \
|
||||
pnpm --filter @source/database build && \
|
||||
pnpm --filter @source/api build
|
||||
|
||||
# --- Production ---
|
||||
FROM node:20-alpine AS production
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.4 --activate
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY --from=build /app/apps/api/dist ./apps/api/dist
|
||||
COPY --from=build /app/apps/api/package.json ./apps/api/
|
||||
COPY --from=build /app/packages/database/dist ./packages/database/dist
|
||||
COPY --from=build /app/packages/database/package.json ./packages/database/
|
||||
COPY --from=build /app/packages/shared/dist ./packages/shared/dist
|
||||
COPY --from=build /app/packages/shared/package.json ./packages/shared/
|
||||
COPY --from=deps /app/packages/database/node_modules ./packages/database/node_modules
|
||||
COPY --from=deps /app/packages/shared/node_modules ./packages/shared/node_modules
|
||||
COPY --from=deps /app/apps/api/node_modules ./apps/api/node_modules
|
||||
COPY pnpm-workspace.yaml package.json ./
|
||||
|
||||
EXPOSE 3000
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s CMD wget -qO- http://localhost:3000/api/health || exit 1
|
||||
|
||||
CMD ["node", "apps/api/dist/index.js"]
|
||||
|
|
@ -12,7 +12,9 @@
|
|||
"dependencies": {
|
||||
"@fastify/cookie": "^11.0.0",
|
||||
"@fastify/cors": "^10.0.0",
|
||||
"@fastify/helmet": "^13.0.2",
|
||||
"@fastify/jwt": "^9.0.0",
|
||||
"@fastify/rate-limit": "^10.3.0",
|
||||
"@fastify/websocket": "^11.0.0",
|
||||
"@sinclair/typebox": "^0.34.0",
|
||||
"@source/database": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import Fastify from 'fastify';
|
||||
import cors from '@fastify/cors';
|
||||
import cookie from '@fastify/cookie';
|
||||
import helmet from '@fastify/helmet';
|
||||
import rateLimit from '@fastify/rate-limit';
|
||||
import dbPlugin from './plugins/db.js';
|
||||
import authPlugin from './plugins/auth.js';
|
||||
import authRoutes from './routes/auth/index.js';
|
||||
|
|
@ -19,12 +21,21 @@ const app = Fastify({
|
|||
},
|
||||
});
|
||||
|
||||
// Plugins
|
||||
// Security plugins
|
||||
await app.register(helmet, {
|
||||
contentSecurityPolicy: process.env.NODE_ENV === 'production' ? undefined : false,
|
||||
});
|
||||
|
||||
await app.register(cors, {
|
||||
origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
await app.register(rateLimit, {
|
||||
max: Number(process.env.RATE_LIMIT_MAX) || 100,
|
||||
timeWindow: Number(process.env.RATE_LIMIT_WINDOW_MS) || 60_000,
|
||||
});
|
||||
|
||||
await app.register(cookie);
|
||||
await app.register(dbPlugin);
|
||||
await app.register(authPlugin);
|
||||
|
|
@ -47,10 +58,20 @@ app.setErrorHandler((error: Error & { validation?: unknown; statusCode?: number;
|
|||
});
|
||||
}
|
||||
|
||||
// Rate limit errors
|
||||
if (error.statusCode === 429) {
|
||||
return reply.code(429).send({
|
||||
error: 'Too Many Requests',
|
||||
message: 'Rate limit exceeded, please try again later',
|
||||
});
|
||||
}
|
||||
|
||||
app.log.error(error);
|
||||
return reply.code(500).send({
|
||||
return reply.code(error.statusCode ?? 500).send({
|
||||
error: 'Internal Server Error',
|
||||
message: 'An unexpected error occurred',
|
||||
message: process.env.NODE_ENV === 'production'
|
||||
? 'An unexpected error occurred'
|
||||
: error.message,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
FROM rust:1.83-bookworm AS build
|
||||
|
||||
# Install protoc
|
||||
RUN apt-get update && apt-get install -y protobuf-compiler && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY apps/daemon/ .
|
||||
|
||||
RUN cargo build --release
|
||||
|
||||
# --- Production ---
|
||||
FROM debian:bookworm-slim AS production
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
libssl3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/target/release/gamepanel-daemon /app/gamepanel-daemon
|
||||
|
||||
# Data directories
|
||||
RUN mkdir -p /var/lib/gamepanel/servers /var/lib/gamepanel/backups /etc/gamepanel
|
||||
|
||||
EXPOSE 50051
|
||||
HEALTHCHECK --interval=30s --timeout=5s CMD /app/gamepanel-daemon --health-check || exit 1
|
||||
|
||||
CMD ["/app/gamepanel-daemon"]
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
FROM node:20-alpine AS base
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.4 --activate
|
||||
WORKDIR /app
|
||||
|
||||
# --- Dependencies ---
|
||||
FROM base AS deps
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||
COPY apps/web/package.json apps/web/
|
||||
COPY packages/shared/package.json packages/shared/
|
||||
COPY packages/ui/package.json packages/ui/
|
||||
RUN pnpm install --frozen-lockfile --prod=false
|
||||
|
||||
# --- Build ---
|
||||
FROM base AS build
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY --from=deps /app/apps/web/node_modules ./apps/web/node_modules
|
||||
COPY --from=deps /app/packages/shared/node_modules ./packages/shared/node_modules
|
||||
COPY --from=deps /app/packages/ui/node_modules ./packages/ui/node_modules
|
||||
COPY . .
|
||||
|
||||
ARG VITE_API_URL=/api
|
||||
ENV VITE_API_URL=${VITE_API_URL}
|
||||
|
||||
RUN pnpm --filter @source/shared build && \
|
||||
pnpm --filter @source/ui build && \
|
||||
pnpm --filter @source/web build
|
||||
|
||||
# --- Production (nginx) ---
|
||||
FROM nginx:alpine AS production
|
||||
|
||||
COPY apps/web/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build /app/apps/web/dist /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
||||
HEALTHCHECK --interval=30s --timeout=5s CMD wget -qO- http://localhost/health || exit 1
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
|
||||
|
||||
# Health check
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 '{"status":"ok"}';
|
||||
add_header Content-Type application/json;
|
||||
}
|
||||
|
||||
# API proxy
|
||||
location /api/ {
|
||||
proxy_pass http://api:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Socket.IO proxy
|
||||
location /socket.io/ {
|
||||
proxy_pass http://api:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
# Static assets caching
|
||||
location /assets/ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# SPA fallback
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router';
|
|||
import { Toaster } from 'sonner';
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { ErrorBoundary } from '@/components/error-boundary';
|
||||
|
||||
// Layouts
|
||||
import { AppLayout } from '@/components/layout/app-layout';
|
||||
|
|
@ -69,6 +70,7 @@ function AuthGuard() {
|
|||
|
||||
export function App() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider>
|
||||
<BrowserRouter>
|
||||
|
|
@ -118,5 +120,6 @@ export function App() {
|
|||
<Toaster position="bottom-right" richColors />
|
||||
</TooltipProvider>
|
||||
</QueryClientProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
import { Component, type ReactNode } from 'react';
|
||||
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||||
console.error('ErrorBoundary caught:', error, info.componentStack);
|
||||
}
|
||||
|
||||
handleReset = () => {
|
||||
this.setState({ hasError: false, error: null });
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[400px] flex-col items-center justify-center gap-4 p-8">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-destructive/10">
|
||||
<AlertTriangle className="h-8 w-8 text-destructive" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<h2 className="text-lg font-semibold">Something went wrong</h2>
|
||||
<p className="mt-1 max-w-md text-sm text-muted-foreground">
|
||||
{this.state.error?.message || 'An unexpected error occurred'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={this.handleReset}
|
||||
className="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Try Again
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-muted"
|
||||
>
|
||||
Reload Page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Daemon configuration — mounted into the daemon container
|
||||
# Adjust api_url and node_token for your deployment
|
||||
|
||||
api_url: "http://api:3000"
|
||||
node_token: "CHANGE_ME_GENERATE_A_SECURE_TOKEN"
|
||||
grpc_port: 50051
|
||||
data_path: "/var/lib/gamepanel/servers"
|
||||
backup_path: "/var/lib/gamepanel/backups"
|
||||
|
||||
docker:
|
||||
socket: "/var/run/docker.sock"
|
||||
network: "gamepanel_nw"
|
||||
network_subnet: "172.18.0.0/16"
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
version: "3.9"
|
||||
|
||||
# Development-only services (DB + Redis)
|
||||
# Usage: docker compose -f docker-compose.dev.yml up -d
|
||||
# Then run: pnpm dev
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USER:-gamepanel}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-gamepanel}
|
||||
POSTGRES_DB: ${DB_NAME:-gamepanel}
|
||||
volumes:
|
||||
- pgdata_dev:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
command: redis-server --requirepass gamepanel
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
volumes:
|
||||
pgdata_dev:
|
||||
|
|
@ -1,29 +1,101 @@
|
|||
services:
|
||||
# --- PostgreSQL ---
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: gamepanel-postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USER:-gamepanel}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-gamepanel}
|
||||
POSTGRES_DB: ${DB_NAME:-gamepanel}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "${DB_PORT:-5432}:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-gamepanel}"]
|
||||
interval: 5s
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# --- Redis (rate limiting, session cache) ---
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: gamepanel-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
restart: unless-stopped
|
||||
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-gamepanel}
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-gamepanel}", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# --- API ---
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: apps/api/Dockerfile
|
||||
container_name: gamepanel-api
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
DATABASE_URL: postgresql://${DB_USER:-gamepanel}:${DB_PASSWORD:-gamepanel}@postgres:5432/${DB_NAME:-gamepanel}
|
||||
REDIS_URL: redis://:${REDIS_PASSWORD:-gamepanel}@redis:6379
|
||||
PORT: 3000
|
||||
HOST: 0.0.0.0
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET}
|
||||
CORS_ORIGIN: ${CORS_ORIGIN:-http://localhost}
|
||||
RATE_LIMIT_MAX: ${RATE_LIMIT_MAX:-100}
|
||||
RATE_LIMIT_WINDOW_MS: ${RATE_LIMIT_WINDOW_MS:-60000}
|
||||
ports:
|
||||
- "${API_PORT:-3000}:3000"
|
||||
|
||||
# --- Web (nginx + SPA) ---
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: apps/web/Dockerfile
|
||||
args:
|
||||
VITE_API_URL: /api
|
||||
container_name: gamepanel-web
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- api
|
||||
ports:
|
||||
- "${WEB_PORT:-80}:80"
|
||||
|
||||
# --- Daemon (runs on game server nodes) ---
|
||||
daemon:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: apps/daemon/Dockerfile
|
||||
container_name: gamepanel-daemon
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- api
|
||||
privileged: true
|
||||
environment:
|
||||
DAEMON_CONFIG: /etc/gamepanel/config.yml
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- daemon_data:/var/lib/gamepanel/servers
|
||||
- daemon_backups:/var/lib/gamepanel/backups
|
||||
- ./daemon-config.yml:/etc/gamepanel/config.yml:ro
|
||||
ports:
|
||||
- "${DAEMON_GRPC_PORT:-50051}:50051"
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
daemon_data:
|
||||
daemon_backups:
|
||||
|
|
|
|||
|
|
@ -120,6 +120,75 @@ async function seed() {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'minecraft-bedrock',
|
||||
name: 'Minecraft: Bedrock Edition',
|
||||
dockerImage: 'itzg/minecraft-bedrock-server:latest',
|
||||
defaultPort: 19132,
|
||||
startupCommand: '',
|
||||
stopCommand: 'stop',
|
||||
configFiles: [
|
||||
{
|
||||
path: 'server.properties',
|
||||
parser: 'properties',
|
||||
editableKeys: [
|
||||
'server-name',
|
||||
'server-port',
|
||||
'max-players',
|
||||
'gamemode',
|
||||
'difficulty',
|
||||
'level-seed',
|
||||
'online-mode',
|
||||
'allow-cheats',
|
||||
'view-distance',
|
||||
],
|
||||
},
|
||||
],
|
||||
environmentVars: [
|
||||
{ key: 'EULA', default: 'TRUE', description: 'Accept Minecraft EULA', required: true },
|
||||
{ key: 'VERSION', default: 'LATEST', description: 'Bedrock server version', required: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'terraria',
|
||||
name: 'Terraria',
|
||||
dockerImage: 'ryshe/terraria:latest',
|
||||
defaultPort: 7777,
|
||||
startupCommand: '',
|
||||
stopCommand: 'exit',
|
||||
configFiles: [
|
||||
{
|
||||
path: 'serverconfig.txt',
|
||||
parser: 'keyvalue',
|
||||
editableKeys: [
|
||||
'worldname',
|
||||
'maxplayers',
|
||||
'password',
|
||||
'motd',
|
||||
'difficulty',
|
||||
'worldsize',
|
||||
],
|
||||
},
|
||||
],
|
||||
environmentVars: [
|
||||
{ key: 'WORLD_NAME', default: 'world', description: 'World file name', required: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'rust',
|
||||
name: 'Rust',
|
||||
dockerImage: 'didstopia/rust-server:latest',
|
||||
defaultPort: 28015,
|
||||
startupCommand: '',
|
||||
stopCommand: 'quit',
|
||||
configFiles: [],
|
||||
environmentVars: [
|
||||
{ key: 'RUST_SERVER_NAME', default: 'My Rust Server', description: 'Server name', required: true },
|
||||
{ key: 'RUST_SERVER_MAXPLAYERS', default: '50', description: 'Max players', required: false },
|
||||
{ key: 'RUST_SERVER_IDENTITY', default: 'default', description: 'Server identity', required: false },
|
||||
{ key: 'RUST_RCON_PASSWORD', default: '', description: 'RCON password', required: true },
|
||||
],
|
||||
},
|
||||
])
|
||||
.onConflictDoNothing();
|
||||
|
||||
|
|
|
|||
|
|
@ -38,9 +38,15 @@ importers:
|
|||
'@fastify/cors':
|
||||
specifier: ^10.0.0
|
||||
version: 10.1.0
|
||||
'@fastify/helmet':
|
||||
specifier: ^13.0.2
|
||||
version: 13.0.2
|
||||
'@fastify/jwt':
|
||||
specifier: ^9.0.0
|
||||
version: 9.1.0
|
||||
'@fastify/rate-limit':
|
||||
specifier: ^10.3.0
|
||||
version: 10.3.0
|
||||
'@fastify/websocket':
|
||||
specifier: ^11.0.0
|
||||
version: 11.2.0
|
||||
|
|
@ -967,6 +973,9 @@ packages:
|
|||
'@fastify/forwarded@3.0.1':
|
||||
resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==}
|
||||
|
||||
'@fastify/helmet@13.0.2':
|
||||
resolution: {integrity: sha512-tO1QMkOfNeCt9l4sG/FiWErH4QMm+RjHzbMTrgew1DYOQ2vb/6M1G2iNABBrD7Xq6dUk+HLzWW8u+rmmhQHifA==}
|
||||
|
||||
'@fastify/jwt@9.1.0':
|
||||
resolution: {integrity: sha512-CiGHCnS5cPMdb004c70sUWhQTfzrJHAeTywt7nVw6dAiI0z1o4WRvU94xfijhkaId4bIxTCOjFgn4sU+Gvk43w==}
|
||||
|
||||
|
|
@ -976,6 +985,9 @@ packages:
|
|||
'@fastify/proxy-addr@5.1.0':
|
||||
resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==}
|
||||
|
||||
'@fastify/rate-limit@10.3.0':
|
||||
resolution: {integrity: sha512-eIGkG9XKQs0nyynatApA3EVrojHOuq4l6fhB4eeCk4PIOeadvOJz9/4w3vGI44Go17uaXOWEcPkaD8kuKm7g6Q==}
|
||||
|
||||
'@fastify/websocket@11.2.0':
|
||||
resolution: {integrity: sha512-3HrDPbAG1CzUCqnslgJxppvzaAZffieOVbLp1DAy1huCSynUWPifSvfdEDUR8HlJLp3sp1A36uOM2tJogADS8w==}
|
||||
|
||||
|
|
@ -2338,6 +2350,10 @@ packages:
|
|||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
helmet@8.1.0:
|
||||
resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
help-me@5.0.0:
|
||||
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
|
||||
|
||||
|
|
@ -3651,6 +3667,11 @@ snapshots:
|
|||
|
||||
'@fastify/forwarded@3.0.1': {}
|
||||
|
||||
'@fastify/helmet@13.0.2':
|
||||
dependencies:
|
||||
fastify-plugin: 5.1.0
|
||||
helmet: 8.1.0
|
||||
|
||||
'@fastify/jwt@9.1.0':
|
||||
dependencies:
|
||||
'@fastify/error': 4.2.0
|
||||
|
|
@ -3668,6 +3689,12 @@ snapshots:
|
|||
'@fastify/forwarded': 3.0.1
|
||||
ipaddr.js: 2.3.0
|
||||
|
||||
'@fastify/rate-limit@10.3.0':
|
||||
dependencies:
|
||||
'@lukeed/ms': 2.0.2
|
||||
fastify-plugin: 5.1.0
|
||||
toad-cache: 3.7.0
|
||||
|
||||
'@fastify/websocket@11.2.0':
|
||||
dependencies:
|
||||
duplexify: 4.1.3
|
||||
|
|
@ -5065,6 +5092,8 @@ snapshots:
|
|||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
helmet@8.1.0: {}
|
||||
|
||||
help-me@5.0.0: {}
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue