diff --git a/README.md b/README.md index 4672e3b..6d776d6 100644 --- a/README.md +++ b/README.md @@ -1,557 +1,557 @@ -# ZeroHost Dashboard - -Game server management dashboard for the ZeroHost platform. Provides user-facing server lifecycle management (creation, renewal, suspension, deletion) backed by a panel API. - -[![ZeroHost](https://img.zero-host.org/assets/github.png)](https://github.com/ZeroHost-Code/legal/blob/main/zhsl.md) - -image - ---- - -## Table of Contents - -- [Panel Compatibility](#panel-compatibility) -- [Architecture Overview](#architecture-overview) -- [Tech Stack](#tech-stack) -- [Project Structure](#project-structure) -- [Request Lifecycle](#request-lifecycle) -- [Routing](#routing) -- [Database Schema](#database-schema) -- [Authentication Flow](#authentication-flow) -- [Server Lifecycle](#server-lifecycle) -- [Scheduler](#scheduler) -- [Configuration](#configuration) -- [Deployment](#deployment) - ---- - -## Panel Compatibility - -| Panel | Status | -|---|---| -| Pterodactyl | ⚠️ Not fully tested | -| Pyrodactyl | ✅ Tested — working | -| Hydrodactyl | ✅ Tested — working | -| Pelican | ❓ Not tested | - -## Architecture Overview - -```mermaid -flowchart TB - subgraph Client["Client Layer"] - A["Browser (Vanilla JS SPA)"] - B["Cap CAPTCHA Widget"] - end - - subgraph CDN["CDN Layer"] - C["jsDelivr\n(cap-widget)"] - D["Google Fonts"] - end - - subgraph App["Application Layer (Express)"] - E["server.js\n(Entry Point)"] - F["middleware/auth.js\n(JWT Auth)"] - G["config/migrate.js\n(Schema Migration)"] - H["services/scheduler.js\n(Expiry Cron)"] - end - - subgraph Routes["Route Handlers"] - I["routes/auth.js"] - J["routes/servers.js"] - end - - subgraph Services["Service Layer"] - K["services/pyrodactyl.js\n(Panel API Wrapper)"] - L["services/activity.js\n(Activity Logger)"] - end - - subgraph Config["Configuration"] - M["config/db.js\n(MariaDB Pool)"] - N["config/pyrodactyl.js\n(Panel Config & Limits)"] - O["config/cap.js\n(CAPTCHA Client)"] - end - - subgraph External["External Systems"] - P["Pyrodactyl Panel\n(panel.zero-host.org)"] - Q["MariaDB"] - R["ip-api.com\n(VPN/Proxy Detection)"] - end - - A -- "HTTP(S)" --> E - B -- "CAPTCHA Token" --> E - C -- "Web Component" --> A - D -- "Font Assets" --> A - E --> F - E --> G - E --> H - E --> I - E --> J - I --> K - I --> L - I --> O - J --> K - J --> L - J --> O - K --> P - K --> Q - L --> Q - I --> R - M --> Q -``` - ---- - -## Tech Stack - -| Component | Technology | -|---|---| -| Runtime | Node.js >= 18 (ES Modules) | -| HTTP Framework | Express 4.21 | -| Database | MariaDB via `mariadb` driver 3.4 | -| Authentication | JWT (HS256, `jsonwebtoken` 9) | -| Password Hashing | Argon2id (`argon2` 0.41) | -| Frontend | Vanilla JavaScript (SPA, no framework) | -| Styling | Custom CSS | -| CAPTCHA | Self-hosted Cap CAPTCHA | -| Panel API | Pterodactyl / Pyrodactyl Application API | -| Process Manager | PM2 | -| CI/CD | GitHub Actions (SSH deploy) | - ---- - -## Project Structure - -```mermaid -flowchart LR - subgraph Root["/"] - direction TB - sv["server.js\n(Express entry)"] - env[".env"] - pkg["package.json"] - pt["port.txt"] - end - - subgraph Routes["routes/"] - auth["auth.js\nAuth endpoints"] - srv["servers.js\nServer CRUD"] - end - - subgraph Middleware["middleware/"] - mw["auth.js\nJWT verification"] - end - - subgraph Services["services/"] - p["pyrodactyl.js\nPanel API wrapper"] - a["activity.js\nActivity log"] - sc["scheduler.js\nExpiry cron"] - end - - subgraph Config["config/"] - db["db.js\nMariaDB pool"] - mg["migrate.js\nSchema migrator"] - cp["cap.js\nCAPTCHA client"] - py["pyrodactyl.js\nPanel config"] - end - - subgraph Public["public/"] - html["index.html\nShell"] - css["css/style.css\nStyles"] - js["js/app.js\nSPA frontend"] - end - - subgraph GH["github/workflows/"] - beta["deploy-beta.yml"] - main["deploy-main.yml"] - end - - Root --> Routes - Root --> Services - Root --> Config - Root --> Public - Root --> GH -``` - ---- - -## Request Lifecycle - -```mermaid -sequenceDiagram - participant Browser - participant Express - participant Middleware - participant Handler - participant Service - participant Database - participant Pyrodactyl - - Browser->>Express: HTTP Request - Express->>Express: helmet (security headers) - Express->>Express: cors - Express->>Express: rate-limit check - Express->>Middleware: authenticateToken (if /api/* except auth routes) - - alt Valid Token - Middleware->>Middleware: jwt.verify(token, JWT_SECRET) - Middleware->>Express: req.user = { userId, email, username, pteroId } - Express->>Handler: Route handler - Handler->>Service: Business logic - Service->>Database: Query / Mutation - Service->>Pyrodactyl: Panel API call (if applicable) - Pyrodactyl-->>Service: Response - Database-->>Service: Result - Service-->>Handler: Processed result - Handler-->>Express: JSON Response - Express-->>Browser: 200 JSON - else Invalid / Expired Token - Middleware-->>Express: 401 / 403 - Express-->>Browser: Error JSON - end -``` - ---- - -## Routing - -### Frontend SPA Routes - -```mermaid -flowchart TD - A["/ or /overview\nDashboard Home"] --> B["Stat Cards\n(Total/Active/Slots/To-Renew)"] - A --> C["Resource Usage\n(RAM/CPU/Disk bars)"] - A --> D["Recent Servers\n(List of last 5)"] - A --> E["Recent Activity\n(Timeline)"] - - F["/servers\nServer List"] --> G["Table with search & filter"] - G --> H["Settings Button"] - G --> I["Open Panel Button"] - G --> J["Renew Button"] - - K["/server/:id\nServer Detail"] --> L["Tab: Info\n(egg, allocation, lifetime)"] - K --> M["Tab: Resources\n(live CPU/Memory/Disk gauges)"] - K --> N["Tab: Actions\n(Open, Reinstall, Delete)"] - - O["/create\nCreate Server"] --> P["Name Input"] - O --> Q["Egg Selector\n(grouped by nest)"] - O --> R["Cap CAPTCHA"] - O --> S["Default Limits\n(512MB/50%/3GB)"] - - T["/pyrodactyl\nPanel Redirect"] --> U["5-second auto-redirect\nto panel.zero-host.org"] - - V["/account\nAccount Hub"] --> W["/account/edit\n(Email, Password, API Key)"] - V --> Y["/account/dangerous\n(Delete Account, RGPD Export)"] - - Z["/log\nActivity Log"] --> AA["Paginated list\n(50 per page)"] -``` - -### Backend API Endpoints - -```mermaid -flowchart LR - subgraph Auth["/api/auth"] - direction TB - r["POST /register"] - l["POST /login"] - o["POST /logout"] - cp["POST /change-password"] - ce["POST /change-email"] - da["POST /delete-account"] - ed["GET /export-data"] - end - - subgraph Servers["/api/servers"] - direction TB - ls["GET /list"] - ov["GET /overview"] - eg["GET /eggs"] - cr["POST /create"] - dt["GET /details/:id"] - rn["PATCH /:id\n(rename)"] - rw["POST /renew/:id"] - ri["POST /:id/reinstall"] - dl["DELETE /:id"] - rs["GET /resources/:identifier"] - ak["PUT /client-api-key"] - end - - subgraph Other["/api"] - ac["GET /activity"] - hl["GET /health"] - end -``` - ---- - -## Database Schema - -```mermaid -erDiagram - users { - int id PK "AUTO_INCREMENT" - varchar email "NOT NULL" - varchar username "NOT NULL" - varchar password_hash "Argon2id hash" - int ptero_user_id "Pyrodactyl user ID" - varchar ptero_uuid "Pyrodactyl UUID" - varchar first_name - varchar last_name - tinyint password_set "DEFAULT 0" - varchar ptero_client_api_key "NULLABLE" - timestamp created_at "DEFAULT CURRENT_TIMESTAMP" - } - - server_meta { - int id PK "AUTO_INCREMENT" - int ptero_server_id "NOT NULL" - int user_id "FK -> users" - timestamp created_at "DEFAULT CURRENT_TIMESTAMP" - timestamp expires_at "90d from creation/renewal" - enum status "'active' | 'suspended' | 'expired'" - } - - user_ips { - int id PK "AUTO_INCREMENT" - int user_id "FK -> users ON DELETE CASCADE" - varchar ip_address "IPv4 or IPv6" - timestamp created_at "DEFAULT CURRENT_TIMESTAMP" - } - - activity_log { - int id PK "AUTO_INCREMENT" - int user_id "NOT NULL" - varchar action "e.g. server_created" - varchar details "DEFAULT ''" - int server_id "NULLABLE" - timestamp created_at "DEFAULT CURRENT_TIMESTAMP" - } - - users ||--o{ server_meta : "has" - users ||--o{ user_ips : "has" - users ||--o{ activity_log : "performs" -``` - -### Indexes & Constraints - -| Table | Index / Constraint | Type | -|---|---|---| -| `server_meta` | `idx_expires` (expires_at) | Index | -| `server_meta` | `idx_user` (user_id) | Index | -| `server_meta` | `idx_status` (status) | Index | -| `user_ips` | `idx_ip` (ip_address) | Index | -| `user_ips` | `idx_user` (user_id) | Index | -| `user_ips` | `fk_user_ips_user` (user_id -> users.id) | Foreign Key (CASCADE) | -| `activity_log` | `idx_activity_user` (user_id) | Index | -| `activity_log` | `idx_activity_created` (created_at) | Index | - -### External Table (panel database, not migrated by this project) - -| Table | Columns | -|---|---| -| `panel.egg_variables` | `egg_id`, `name`, `env_variable`, `default_value`, `rules`, `description`, `user_viewable`, `user_editable` | - ---- - -## Authentication Flow - -### Registration - -```mermaid -sequenceDiagram - participant Browser - participant Express - participant Cap as "Cap CAPTCHA" - participant ipapi as "ip-api.com" - participant DB as "MariaDB" - participant Panel as "Pyrodactyl Panel" - - Browser->>Express: POST /api/auth/register - Note over Browser,Express: Body: { email, username, password, capToken } - Express->>Cap: POST /siteverify (capToken) - Cap-->>Express: { success: boolean } - - alt CAPTCHA Failed - Express-->>Browser: 400 { error: "CAPTCHA verification failed" } - end - - Express->>ipapi: GET /json/{clientIP} - ipapi-->>Express: { proxy: boolean, ... } - - alt Proxy / VPN Detected - Express-->>Browser: 403 { error: "VPN/Proxy detected" } - end - - Express->>DB: SELECT IP count for client IP - DB-->>Express: count - - alt 2+ accounts from same IP - Express-->>Browser: 403 { error: "Maximum 2 accounts per IP" } - end - - Express->>Express: argon2.hash(password) - Express->>Panel: POST /api/application/users - Panel-->>Express: { attributes: { id, uuid } } - - Express->>DB: INSERT INTO users (...) - DB-->>Express: OK - - Express->>DB: INSERT INTO user_ips (...) - DB-->>Express: OK - - Express->>Express: jwt.sign({ userId, email, username, pteroId }) - Express->>DB: INSERT INTO activity_log (action='account_registered') - Express-->>Browser: 201 { token, user } + Set-Cookie: token -``` - -### Login - -```mermaid -sequenceDiagram - participant Browser - participant Express - participant DB as "MariaDB" - participant ipapi as "ip-api.com" - - Browser->>Express: POST /api/auth/login - Express->>Express: CAPTCHA verification - Express->>ipapi: VPN / Proxy check - ipapi-->>Express: result - - alt Blocked - Express-->>Browser: 403 - end - - Express->>DB: SELECT * FROM users WHERE email = ? - DB-->>Express: user row - Express->>Express: argon2.verify(hash, password) - - alt Invalid Password - Express-->>Browser: 401 { error: "Invalid credentials" } - end - - Express->>Express: jwt.sign({ userId, email, username, pteroId }) - Express-->>Browser: 200 { token, user } + Set-Cookie: token -``` - ---- - -## Server Lifecycle - -```mermaid -stateDiagram-v2 - [*] --> Creating: POST /api/servers/create - Creating --> Active: Pyrodactyl API success - Creating --> Failed: API error - - Active --> Suspended: Scheduler (expired) - Active --> Suspended: Manual suspend (Pyrodactyl) - Active --> Renewed: POST /api/servers/renew/:id - Active --> Deleted: DELETE /api/servers/:id - - Suspended --> Active: Renew (within 7d window) - Suspended --> Expired: 7 days past expiry - Suspended --> Deleted: DELETE /api/servers/:id - - Renewed --> Active: expires_at += 90 days - - Expired --> [*] - Failed --> [*] - Deleted --> [*] -``` - -### Server Default Limits - -| Resource | Value | -|---|---| -| RAM | 512 MB | -| CPU | 50% | -| Disk | 3 GB | -| Swap | 0 | -| Backups | 1 | -| Allocations | 1 | - -### Renewal Policy - -- Servers expire 90 days after creation or last renewal. -- Renewal is only permitted within a 7-day window before or after the expiration date. -- Renewing an expired server automatically unsuspends it. - ---- - -## Scheduler - -The scheduler (`services/scheduler.js`) runs daily at midnight via `setInterval`: - -```mermaid -flowchart TD - A["Scheduler Start"] --> B["Hourly check:\nis it midnight?"] - B -->|Yes| C["SELECT * FROM server_meta\nWHERE expires_at < NOW()\nAND status = 'active'"] - C --> D["For each expired server:"] - D --> E["POST /api/application/servers/{id}/suspend"] - E --> F["UPDATE server_meta\nSET status = 'suspended'"] - F --> G["INSERT INTO activity_log\naction = 'server_suspended'"] - B -->|No| B -``` - ---- - -## Configuration - -### Environment Variables (.env) - -| Variable | Description | -|---|---| -| `JWT_SECRET` | Secret key for JWT signing (HS256) | -| `JWT_EXPIRES_IN` | Token expiry duration (default: `2h`) | -| `COOKIE_SECRET` | Secret for signed cookies | -| `DB_HOST` | MariaDB host | -| `DB_USER` | MariaDB user | -| `DB_PASSWORD` | MariaDB password | -| `DB_NAME` | MariaDB database name | -| `DB_PORT` | MariaDB port (default: 3306) | -| `PTERO_URL` | Pyrodactyl panel base URL | -| `PTERO_API_KEY` | Pyrodactyl Application API key | -| `CAP_ENDPOINT` | Cap CAPTCHA endpoint URL | -| `CAP_SECRET` | Cap CAPTCHA secret key | -| `NODE_ENV` | `production` or `development` | - -### Rate Limiting - -| Scope | Window | Max Requests | -|---|---|---| -| Auth endpoints (login, register) | 15 minutes | 10 | -| General API | 60 seconds | 100 | - ---- - -## Deployment - -```mermaid -flowchart LR - subgraph Dev["Development"] - A["git push beta"] - end - - subgraph CI["GitHub Actions"] - B["Checkout"] - C["Install deps\n(npm ci)"] - D["SSH Deploy"] - E["PM2 Restart"] - end - - subgraph Prod["Production Server"] - F["Pull from beta/main"] - G["npm install --production"] - H["PM2 reload"] - I["DB migration\n(auto on startup)"] - end - - A --> B --> C --> D --> E - E --> F --> G --> H --> I -``` - -Two deployment workflows are configured: - -- **deploy-beta.yml**: Triggered on push to `beta` branch. -- **deploy-main.yml**: Triggered on push to `main` branch. - -Both use SSH credentials configured as GitHub repository secrets (`SSH_HOST`, `SSH_USER`, `SSH_PASSWORD`, `SSH_PORT`). +# ZeroHost Dashboard + +Game server management dashboard for the ZeroHost platform. Provides user-facing server lifecycle management (creation, renewal, suspension, deletion) backed by a panel API. + +[![ZeroHost](https://img.zero-host.org/assets/github.png)](https://github.com/ZeroHost-Code/legal/blob/main/zhsl.md) + +image + +--- + +## Table of Contents + +- [Panel Compatibility](#panel-compatibility) +- [Architecture Overview](#architecture-overview) +- [Tech Stack](#tech-stack) +- [Project Structure](#project-structure) +- [Request Lifecycle](#request-lifecycle) +- [Routing](#routing) +- [Database Schema](#database-schema) +- [Authentication Flow](#authentication-flow) +- [Server Lifecycle](#server-lifecycle) +- [Scheduler](#scheduler) +- [Configuration](#configuration) +- [Deployment](#deployment) + +--- + +## Panel Compatibility + +| Panel | Status | +|---|---| +| Pterodactyl | ⚠️ Not fully tested | +| Pyrodactyl | ✅ Tested — working | +| Hydrodactyl | ✅ Tested — working | +| Pelican | ❓ Not tested | + +## Architecture Overview + +```mermaid +flowchart TB + subgraph Client["Client Layer"] + A["Browser (Vanilla JS SPA)"] + B["Cap CAPTCHA Widget"] + end + + subgraph CDN["CDN Layer"] + C["jsDelivr\n(cap-widget)"] + D["Google Fonts"] + end + + subgraph App["Application Layer (Express)"] + E["server.js\n(Entry Point)"] + F["middleware/auth.js\n(JWT Auth)"] + G["config/migrate.js\n(Schema Migration)"] + H["services/scheduler.js\n(Expiry Cron)"] + end + + subgraph Routes["Route Handlers"] + I["routes/auth.js"] + J["routes/servers.js"] + end + + subgraph Services["Service Layer"] + K["services/pyrodactyl.js\n(Panel API Wrapper)"] + L["services/activity.js\n(Activity Logger)"] + end + + subgraph Config["Configuration"] + M["config/db.js\n(MariaDB Pool)"] + N["config/pyrodactyl.js\n(Panel Config & Limits)"] + O["config/cap.js\n(CAPTCHA Client)"] + end + + subgraph External["External Systems"] + P["Pyrodactyl Panel\n(panel.zero-host.org)"] + Q["MariaDB"] + R["ip-api.com\n(VPN/Proxy Detection)"] + end + + A -- "HTTP(S)" --> E + B -- "CAPTCHA Token" --> E + C -- "Web Component" --> A + D -- "Font Assets" --> A + E --> F + E --> G + E --> H + E --> I + E --> J + I --> K + I --> L + I --> O + J --> K + J --> L + J --> O + K --> P + K --> Q + L --> Q + I --> R + M --> Q +``` + +--- + +## Tech Stack + +| Component | Technology | +|---|---| +| Runtime | Node.js >= 18 (ES Modules) | +| HTTP Framework | Express 4.21 | +| Database | MariaDB via `mariadb` driver 3.4 | +| Authentication | JWT (HS256, `jsonwebtoken` 9) | +| Password Hashing | Argon2id (`argon2` 0.41) | +| Frontend | Vanilla JavaScript (SPA, no framework) | +| Styling | Custom CSS | +| CAPTCHA | Self-hosted Cap CAPTCHA | +| Panel API | Pterodactyl / Pyrodactyl Application API | +| Process Manager | PM2 | +| CI/CD | GitHub Actions (SSH deploy) | + +--- + +## Project Structure + +```mermaid +flowchart LR + subgraph Root["/"] + direction TB + sv["server.js\n(Express entry)"] + env[".env"] + pkg["package.json"] + pt["port.txt"] + end + + subgraph Routes["routes/"] + auth["auth.js\nAuth endpoints"] + srv["servers.js\nServer CRUD"] + end + + subgraph Middleware["middleware/"] + mw["auth.js\nJWT verification"] + end + + subgraph Services["services/"] + p["pyrodactyl.js\nPanel API wrapper"] + a["activity.js\nActivity log"] + sc["scheduler.js\nExpiry cron"] + end + + subgraph Config["config/"] + db["db.js\nMariaDB pool"] + mg["migrate.js\nSchema migrator"] + cp["cap.js\nCAPTCHA client"] + py["pyrodactyl.js\nPanel config"] + end + + subgraph Public["public/"] + html["index.html\nShell"] + css["css/style.css\nStyles"] + js["js/app.js\nSPA frontend"] + end + + subgraph GH["github/workflows/"] + beta["deploy-beta.yml"] + main["deploy-main.yml"] + end + + Root --> Routes + Root --> Services + Root --> Config + Root --> Public + Root --> GH +``` + +--- + +## Request Lifecycle + +```mermaid +sequenceDiagram + participant Browser + participant Express + participant Middleware + participant Handler + participant Service + participant Database + participant Pyrodactyl + + Browser->>Express: HTTP Request + Express->>Express: helmet (security headers) + Express->>Express: cors + Express->>Express: rate-limit check + Express->>Middleware: authenticateToken (if /api/* except auth routes) + + alt Valid Token + Middleware->>Middleware: jwt.verify(token, JWT_SECRET) + Middleware->>Express: req.user = { userId, email, username, pteroId } + Express->>Handler: Route handler + Handler->>Service: Business logic + Service->>Database: Query / Mutation + Service->>Pyrodactyl: Panel API call (if applicable) + Pyrodactyl-->>Service: Response + Database-->>Service: Result + Service-->>Handler: Processed result + Handler-->>Express: JSON Response + Express-->>Browser: 200 JSON + else Invalid / Expired Token + Middleware-->>Express: 401 / 403 + Express-->>Browser: Error JSON + end +``` + +--- + +## Routing + +### Frontend SPA Routes + +```mermaid +flowchart TD + A["/ or /overview\nDashboard Home"] --> B["Stat Cards\n(Total/Active/Slots/To-Renew)"] + A --> C["Resource Usage\n(RAM/CPU/Disk bars)"] + A --> D["Recent Servers\n(List of last 5)"] + A --> E["Recent Activity\n(Timeline)"] + + F["/servers\nServer List"] --> G["Table with search & filter"] + G --> H["Settings Button"] + G --> I["Open Panel Button"] + G --> J["Renew Button"] + + K["/server/:id\nServer Detail"] --> L["Tab: Info\n(egg, allocation, lifetime)"] + K --> M["Tab: Resources\n(live CPU/Memory/Disk gauges)"] + K --> N["Tab: Actions\n(Open, Reinstall, Delete)"] + + O["/create\nCreate Server"] --> P["Name Input"] + O --> Q["Egg Selector\n(grouped by nest)"] + O --> R["Cap CAPTCHA"] + O --> S["Default Limits\n(512MB/50%/3GB)"] + + T["/pyrodactyl\nPanel Redirect"] --> U["5-second auto-redirect\nto panel.zero-host.org"] + + V["/account\nAccount Hub"] --> W["/account/edit\n(Email, Password, API Key)"] + V --> Y["/account/dangerous\n(Delete Account, RGPD Export)"] + + Z["/log\nActivity Log"] --> AA["Paginated list\n(50 per page)"] +``` + +### Backend API Endpoints + +```mermaid +flowchart LR + subgraph Auth["/api/auth"] + direction TB + r["POST /register"] + l["POST /login"] + o["POST /logout"] + cp["POST /change-password"] + ce["POST /change-email"] + da["POST /delete-account"] + ed["GET /export-data"] + end + + subgraph Servers["/api/servers"] + direction TB + ls["GET /list"] + ov["GET /overview"] + eg["GET /eggs"] + cr["POST /create"] + dt["GET /details/:id"] + rn["PATCH /:id\n(rename)"] + rw["POST /renew/:id"] + ri["POST /:id/reinstall"] + dl["DELETE /:id"] + rs["GET /resources/:identifier"] + ak["PUT /client-api-key"] + end + + subgraph Other["/api"] + ac["GET /activity"] + hl["GET /health"] + end +``` + +--- + +## Database Schema + +```mermaid +erDiagram + users { + int id PK "AUTO_INCREMENT" + varchar email "NOT NULL" + varchar username "NOT NULL" + varchar password_hash "Argon2id hash" + int ptero_user_id "Pyrodactyl user ID" + varchar ptero_uuid "Pyrodactyl UUID" + varchar first_name + varchar last_name + tinyint password_set "DEFAULT 0" + varchar ptero_client_api_key "NULLABLE" + timestamp created_at "DEFAULT CURRENT_TIMESTAMP" + } + + server_meta { + int id PK "AUTO_INCREMENT" + int ptero_server_id "NOT NULL" + int user_id "FK -> users" + timestamp created_at "DEFAULT CURRENT_TIMESTAMP" + timestamp expires_at "90d from creation/renewal" + enum status "'active' | 'suspended' | 'expired'" + } + + user_ips { + int id PK "AUTO_INCREMENT" + int user_id "FK -> users ON DELETE CASCADE" + varchar ip_address "IPv4 or IPv6" + timestamp created_at "DEFAULT CURRENT_TIMESTAMP" + } + + activity_log { + int id PK "AUTO_INCREMENT" + int user_id "NOT NULL" + varchar action "e.g. server_created" + varchar details "DEFAULT ''" + int server_id "NULLABLE" + timestamp created_at "DEFAULT CURRENT_TIMESTAMP" + } + + users ||--o{ server_meta : "has" + users ||--o{ user_ips : "has" + users ||--o{ activity_log : "performs" +``` + +### Indexes & Constraints + +| Table | Index / Constraint | Type | +|---|---|---| +| `server_meta` | `idx_expires` (expires_at) | Index | +| `server_meta` | `idx_user` (user_id) | Index | +| `server_meta` | `idx_status` (status) | Index | +| `user_ips` | `idx_ip` (ip_address) | Index | +| `user_ips` | `idx_user` (user_id) | Index | +| `user_ips` | `fk_user_ips_user` (user_id -> users.id) | Foreign Key (CASCADE) | +| `activity_log` | `idx_activity_user` (user_id) | Index | +| `activity_log` | `idx_activity_created` (created_at) | Index | + +### External Table (panel database, not migrated by this project) + +| Table | Columns | +|---|---| +| `panel.egg_variables` | `egg_id`, `name`, `env_variable`, `default_value`, `rules`, `description`, `user_viewable`, `user_editable` | + +--- + +## Authentication Flow + +### Registration + +```mermaid +sequenceDiagram + participant Browser + participant Express + participant Cap as "Cap CAPTCHA" + participant ipapi as "ip-api.com" + participant DB as "MariaDB" + participant Panel as "Pyrodactyl Panel" + + Browser->>Express: POST /api/auth/register + Note over Browser,Express: Body: { email, username, password, capToken } + Express->>Cap: POST /siteverify (capToken) + Cap-->>Express: { success: boolean } + + alt CAPTCHA Failed + Express-->>Browser: 400 { error: "CAPTCHA verification failed" } + end + + Express->>ipapi: GET /json/{clientIP} + ipapi-->>Express: { proxy: boolean, ... } + + alt Proxy / VPN Detected + Express-->>Browser: 403 { error: "VPN/Proxy detected" } + end + + Express->>DB: SELECT IP count for client IP + DB-->>Express: count + + alt 2+ accounts from same IP + Express-->>Browser: 403 { error: "Maximum 2 accounts per IP" } + end + + Express->>Express: argon2.hash(password) + Express->>Panel: POST /api/application/users + Panel-->>Express: { attributes: { id, uuid } } + + Express->>DB: INSERT INTO users (...) + DB-->>Express: OK + + Express->>DB: INSERT INTO user_ips (...) + DB-->>Express: OK + + Express->>Express: jwt.sign({ userId, email, username, pteroId }) + Express->>DB: INSERT INTO activity_log (action='account_registered') + Express-->>Browser: 201 { token, user } + Set-Cookie: token +``` + +### Login + +```mermaid +sequenceDiagram + participant Browser + participant Express + participant DB as "MariaDB" + participant ipapi as "ip-api.com" + + Browser->>Express: POST /api/auth/login + Express->>Express: CAPTCHA verification + Express->>ipapi: VPN / Proxy check + ipapi-->>Express: result + + alt Blocked + Express-->>Browser: 403 + end + + Express->>DB: SELECT * FROM users WHERE email = ? + DB-->>Express: user row + Express->>Express: argon2.verify(hash, password) + + alt Invalid Password + Express-->>Browser: 401 { error: "Invalid credentials" } + end + + Express->>Express: jwt.sign({ userId, email, username, pteroId }) + Express-->>Browser: 200 { token, user } + Set-Cookie: token +``` + +--- + +## Server Lifecycle + +```mermaid +stateDiagram-v2 + [*] --> Creating: POST /api/servers/create + Creating --> Active: Pyrodactyl API success + Creating --> Failed: API error + + Active --> Suspended: Scheduler (expired) + Active --> Suspended: Manual suspend (Pyrodactyl) + Active --> Renewed: POST /api/servers/renew/:id + Active --> Deleted: DELETE /api/servers/:id + + Suspended --> Active: Renew (within 7d window) + Suspended --> Expired: 7 days past expiry + Suspended --> Deleted: DELETE /api/servers/:id + + Renewed --> Active: expires_at += 90 days + + Expired --> [*] + Failed --> [*] + Deleted --> [*] +``` + +### Server Default Limits + +| Resource | Value | +|---|---| +| RAM | 512 MB | +| CPU | 50% | +| Disk | 3 GB | +| Swap | 0 | +| Backups | 1 | +| Allocations | 1 | + +### Renewal Policy + +- Servers expire 90 days after creation or last renewal. +- Renewal is only permitted within a 7-day window before or after the expiration date. +- Renewing an expired server automatically unsuspends it. + +--- + +## Scheduler + +The scheduler (`services/scheduler.js`) runs daily at midnight via `setInterval`: + +```mermaid +flowchart TD + A["Scheduler Start"] --> B["Hourly check:\nis it midnight?"] + B -->|Yes| C["SELECT * FROM server_meta\nWHERE expires_at < NOW()\nAND status = 'active'"] + C --> D["For each expired server:"] + D --> E["POST /api/application/servers/{id}/suspend"] + E --> F["UPDATE server_meta\nSET status = 'suspended'"] + F --> G["INSERT INTO activity_log\naction = 'server_suspended'"] + B -->|No| B +``` + +--- + +## Configuration + +### Environment Variables (.env) + +| Variable | Description | +|---|---| +| `JWT_SECRET` | Secret key for JWT signing (HS256) | +| `JWT_EXPIRES_IN` | Token expiry duration (default: `2h`) | +| `COOKIE_SECRET` | Secret for signed cookies | +| `DB_HOST` | MariaDB host | +| `DB_USER` | MariaDB user | +| `DB_PASSWORD` | MariaDB password | +| `DB_NAME` | MariaDB database name | +| `DB_PORT` | MariaDB port (default: 3306) | +| `PTERO_URL` | Pyrodactyl panel base URL | +| `PTERO_API_KEY` | Pyrodactyl Application API key | +| `CAP_ENDPOINT` | Cap CAPTCHA endpoint URL | +| `CAP_SECRET` | Cap CAPTCHA secret key | +| `NODE_ENV` | `production` or `development` | + +### Rate Limiting + +| Scope | Window | Max Requests | +|---|---|---| +| Auth endpoints (login, register) | 15 minutes | 10 | +| General API | 60 seconds | 100 | + +--- + +## Deployment + +```mermaid +flowchart LR + subgraph Dev["Development"] + A["git push beta"] + end + + subgraph CI["GitHub Actions"] + B["Checkout"] + C["Install deps\n(npm ci)"] + D["SSH Deploy"] + E["PM2 Restart"] + end + + subgraph Prod["Production Server"] + F["Pull from beta/main"] + G["npm install --production"] + H["PM2 reload"] + I["DB migration\n(auto on startup)"] + end + + A --> B --> C --> D --> E + E --> F --> G --> H --> I +``` + +Two deployment workflows are configured: + +- **deploy-beta.yml**: Triggered on push to `beta` branch. +- **deploy-main.yml**: Triggered on push to `main` branch. + +Both use SSH credentials configured as GitHub repository secrets (`SSH_HOST`, `SSH_USER`, `SSH_PASSWORD`, `SSH_PORT`).