phase01
This commit is contained in:
commit
2215003a4d
|
|
@ -0,0 +1,17 @@
|
|||
# Database
|
||||
DATABASE_URL=postgresql://gamepanel:gamepanel@localhost:5432/gamepanel
|
||||
DB_USER=gamepanel
|
||||
DB_PASSWORD=gamepanel
|
||||
DB_NAME=gamepanel
|
||||
|
||||
# API
|
||||
PORT=3000
|
||||
HOST=0.0.0.0
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=change-me-in-production
|
||||
JWT_REFRESH_SECRET=change-me-in-production-refresh
|
||||
|
||||
# Daemon
|
||||
DAEMON_CONFIG=/etc/gamepanel/config.yml
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
node_modules/
|
||||
dist/
|
||||
.turbo/
|
||||
*.log
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Rust
|
||||
apps/daemon/target/
|
||||
|
||||
# Database
|
||||
packages/database/drizzle/
|
||||
|
||||
# Common JS/TS
|
||||
coverage/
|
||||
.nyc_output/
|
||||
.next/
|
||||
out/
|
||||
build/
|
||||
.tsbuildinfo
|
||||
*.tsbuildinfo
|
||||
.pnpm-store/
|
||||
|
||||
# Claude
|
||||
.claude/
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
@source:registry=https://gits.hibna.com.tr/api/packages/hibna/npm/
|
||||
//gits.hibna.com.tr/api/packages/hibna/npm/:_authToken=c30642e24095b2e0f56936b17b2230c389d52e47
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
dist
|
||||
.turbo
|
||||
pnpm-lock.yaml
|
||||
apps/daemon/target
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "@source/api",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"lint": "eslint src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cookie": "^11.0.0",
|
||||
"@fastify/cors": "^10.0.0",
|
||||
"@fastify/jwt": "^9.0.0",
|
||||
"@fastify/websocket": "^11.0.0",
|
||||
"@sinclair/typebox": "^0.34.0",
|
||||
"@source/database": "workspace:*",
|
||||
"@source/shared": "workspace:*",
|
||||
"argon2": "^0.41.0",
|
||||
"fastify": "^5.2.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"socket.io": "^4.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsx": "^4.19.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import Fastify from 'fastify';
|
||||
import cors from '@fastify/cors';
|
||||
import cookie from '@fastify/cookie';
|
||||
|
||||
const app = Fastify({
|
||||
logger: {
|
||||
transport: {
|
||||
target: 'pino-pretty',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await app.register(cors, {
|
||||
origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
await app.register(cookie);
|
||||
|
||||
app.get('/api/health', async () => {
|
||||
return { status: 'ok', timestamp: new Date().toISOString() };
|
||||
});
|
||||
|
||||
const PORT = Number(process.env.PORT) || 3000;
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
|
||||
try {
|
||||
await app.listen({ port: PORT, host: HOST });
|
||||
app.log.info(`API server running on http://${HOST}:${PORT}`);
|
||||
} catch (err) {
|
||||
app.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
[package]
|
||||
name = "gamepanel-daemon"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "GamePanel daemon - manages game server containers"
|
||||
|
||||
[dependencies]
|
||||
# gRPC
|
||||
tonic = "0.12"
|
||||
prost = "0.13"
|
||||
prost-types = "0.13"
|
||||
|
||||
# Async runtime
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
# Docker
|
||||
bollard = "0.18"
|
||||
|
||||
# Serialization
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
|
||||
# HTTP client (for CDN uploads, API callbacks)
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
|
||||
# Logging
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||
|
||||
# Error handling
|
||||
anyhow = "1"
|
||||
thiserror = "2"
|
||||
|
||||
# UUID
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
[build-dependencies]
|
||||
tonic-build = "0.12"
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tonic_build::compile_protos("../../packages/proto/daemon.proto")?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
use anyhow::Result;
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DaemonConfig {
|
||||
pub api_url: String,
|
||||
pub node_token: String,
|
||||
#[serde(default = "default_grpc_port")]
|
||||
pub grpc_port: u16,
|
||||
#[serde(default)]
|
||||
pub docker: DockerConfig,
|
||||
#[serde(default = "default_data_path")]
|
||||
pub data_path: PathBuf,
|
||||
#[serde(default = "default_backup_path")]
|
||||
pub backup_path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DockerConfig {
|
||||
#[serde(default = "default_docker_socket")]
|
||||
pub socket: String,
|
||||
#[serde(default = "default_docker_network")]
|
||||
pub network: String,
|
||||
#[serde(default = "default_docker_subnet")]
|
||||
pub network_subnet: String,
|
||||
}
|
||||
|
||||
impl Default for DockerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
socket: default_docker_socket(),
|
||||
network: default_docker_network(),
|
||||
network_subnet: default_docker_subnet(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_grpc_port() -> u16 {
|
||||
50051
|
||||
}
|
||||
|
||||
fn default_data_path() -> PathBuf {
|
||||
PathBuf::from("/var/lib/gamepanel/servers")
|
||||
}
|
||||
|
||||
fn default_backup_path() -> PathBuf {
|
||||
PathBuf::from("/var/lib/gamepanel/backups")
|
||||
}
|
||||
|
||||
fn default_docker_socket() -> String {
|
||||
"/var/run/docker.sock".to_string()
|
||||
}
|
||||
|
||||
fn default_docker_network() -> String {
|
||||
"gamepanel_nw".to_string()
|
||||
}
|
||||
|
||||
fn default_docker_subnet() -> String {
|
||||
"172.18.0.0/16".to_string()
|
||||
}
|
||||
|
||||
impl DaemonConfig {
|
||||
pub fn load() -> Result<Self> {
|
||||
let config_path =
|
||||
std::env::var("DAEMON_CONFIG").unwrap_or_else(|_| "/etc/gamepanel/config.yml".into());
|
||||
|
||||
let content = std::fs::read_to_string(&config_path)
|
||||
.unwrap_or_else(|_| {
|
||||
tracing::warn!("Config file not found at {}, using defaults", config_path);
|
||||
// Minimal default config for development
|
||||
r#"
|
||||
api_url: "http://localhost:3000"
|
||||
node_token: "dev-token"
|
||||
grpc_port: 50051
|
||||
"#
|
||||
.to_string()
|
||||
});
|
||||
|
||||
let config: DaemonConfig = serde_yaml::from_str(&content)?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
use anyhow::Result;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
mod config;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Initialize logging
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
|
||||
)
|
||||
.init();
|
||||
|
||||
info!("GamePanel Daemon starting...");
|
||||
|
||||
let config = config::DaemonConfig::load()?;
|
||||
info!(grpc_port = config.grpc_port, "Configuration loaded");
|
||||
|
||||
// TODO: Initialize Docker client
|
||||
// TODO: Start gRPC server
|
||||
// TODO: Begin heartbeat loop
|
||||
|
||||
info!("GamePanel Daemon ready");
|
||||
|
||||
// Keep the process running
|
||||
tokio::signal::ctrl_c().await?;
|
||||
info!("Shutting down...");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>GamePanel</title>
|
||||
</head>
|
||||
<body class="min-h-screen bg-background text-foreground antialiased">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "@source/web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@source/shared": "workspace:*",
|
||||
"@source/ui": "workspace:*",
|
||||
"@tanstack/react-query": "^5.62.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router": "^7.1.0",
|
||||
"socket.io-client": "^4.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 5 * 1000,
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold">GamePanel</h1>
|
||||
<p className="mt-2 text-muted-foreground">Game Server Management Panel</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from './App';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import type { Config } from 'tailwindcss';
|
||||
|
||||
export default {
|
||||
darkMode: 'class',
|
||||
content: ['./index.html', './src/**/*.{ts,tsx}', '../../packages/ui/src/**/*.{ts,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
} satisfies Config;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/socket.io': {
|
||||
target: 'http://localhost:3000',
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: gamepanel-postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USER:-gamepanel}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-gamepanel}
|
||||
POSTGRES_DB: ${DB_NAME:-gamepanel}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-gamepanel}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: gamepanel-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
|
||||
export default tseslint.config(
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
prettier,
|
||||
{
|
||||
ignores: ['**/dist/**', '**/node_modules/**', '**/.turbo/**', 'apps/daemon/**'],
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/consistent-type-imports': 'warn',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "source-gamepanel",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Game server management panel",
|
||||
"scripts": {
|
||||
"dev": "turbo dev",
|
||||
"build": "turbo build",
|
||||
"lint": "turbo lint",
|
||||
"format": "prettier --write \"**/*.{ts,tsx,js,json,md}\"",
|
||||
"format:check": "prettier --check \"**/*.{ts,tsx,js,json,md}\"",
|
||||
"db:generate": "turbo db:generate --filter=@source/database",
|
||||
"db:migrate": "turbo db:migrate --filter=@source/database",
|
||||
"db:seed": "turbo db:seed --filter=@source/database",
|
||||
"db:studio": "turbo db:studio --filter=@source/database"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.4",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"prettier": "^3.4.0",
|
||||
"turbo": "^2.3.0",
|
||||
"typescript": "^5.7.0",
|
||||
"typescript-eslint": "^8.18.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/schema/index.ts',
|
||||
out: './drizzle',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "@source/database",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"lint": "eslint src/",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:seed": "tsx src/seed.ts",
|
||||
"db:studio": "drizzle-kit studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"drizzle-orm": "^0.38.0",
|
||||
"postgres": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"drizzle-kit": "^0.30.0",
|
||||
"tsx": "^4.19.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||
import postgres from 'postgres';
|
||||
import * as schema from './schema';
|
||||
|
||||
export function createDb(connectionString: string) {
|
||||
const client = postgres(connectionString);
|
||||
return drizzle(client, { schema });
|
||||
}
|
||||
|
||||
export type Database = ReturnType<typeof createDb>;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './schema';
|
||||
export * from './client';
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { pgTable, uuid, varchar, jsonb, timestamp } from 'drizzle-orm/pg-core';
|
||||
import { organizations } from './organizations';
|
||||
import { users } from './users';
|
||||
import { servers } from './servers';
|
||||
|
||||
export const auditLogs = pgTable('audit_logs', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
organizationId: uuid('organization_id')
|
||||
.notNull()
|
||||
.references(() => organizations.id, { onDelete: 'cascade' }),
|
||||
userId: uuid('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
serverId: uuid('server_id').references(() => servers.id, { onDelete: 'set null' }),
|
||||
action: varchar('action', { length: 255 }).notNull(),
|
||||
metadata: jsonb('metadata').default({}).notNull(),
|
||||
ipAddress: varchar('ip_address', { length: 45 }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { pgTable, uuid, varchar, bigint, text, boolean, timestamp } from 'drizzle-orm/pg-core';
|
||||
import { servers } from './servers';
|
||||
|
||||
export const backups = pgTable('backups', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
serverId: uuid('server_id')
|
||||
.notNull()
|
||||
.references(() => servers.id, { onDelete: 'cascade' }),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
sizeBytes: bigint('size_bytes', { mode: 'number' }),
|
||||
cdnPath: text('cdn_path'),
|
||||
checksum: text('checksum'),
|
||||
isLocked: boolean('is_locked').default(false).notNull(),
|
||||
completedAt: timestamp('completed_at', { withTimezone: true }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { pgTable, uuid, varchar, integer, text, jsonb, timestamp } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const games = pgTable('games', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
slug: varchar('slug', { length: 100 }).notNull().unique(),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
dockerImage: text('docker_image').notNull(),
|
||||
defaultPort: integer('default_port').notNull(),
|
||||
configFiles: jsonb('config_files').default([]).notNull(),
|
||||
startupCommand: text('startup_command').notNull(),
|
||||
stopCommand: text('stop_command'),
|
||||
environmentVars: jsonb('environment_vars').default([]).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export * from './users';
|
||||
export * from './organizations';
|
||||
export * from './nodes';
|
||||
export * from './games';
|
||||
export * from './servers';
|
||||
export * from './backups';
|
||||
export * from './plugins';
|
||||
export * from './schedules';
|
||||
export * from './audit-logs';
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
varchar,
|
||||
integer,
|
||||
bigint,
|
||||
text,
|
||||
boolean,
|
||||
timestamp,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { organizations } from './organizations';
|
||||
import { servers } from './servers';
|
||||
|
||||
export const nodes = pgTable('nodes', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
organizationId: uuid('organization_id')
|
||||
.notNull()
|
||||
.references(() => organizations.id, { onDelete: 'cascade' }),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
fqdn: varchar('fqdn', { length: 255 }).notNull(),
|
||||
daemonPort: integer('daemon_port').default(8443).notNull(),
|
||||
grpcPort: integer('grpc_port').default(50051).notNull(),
|
||||
location: varchar('location', { length: 255 }),
|
||||
memoryTotal: bigint('memory_total', { mode: 'number' }).notNull(),
|
||||
diskTotal: bigint('disk_total', { mode: 'number' }).notNull(),
|
||||
memoryOveralloc: integer('memory_overalloc').default(0).notNull(),
|
||||
diskOveralloc: integer('disk_overalloc').default(0).notNull(),
|
||||
daemonToken: text('daemon_token').notNull(),
|
||||
tlsEnabled: boolean('tls_enabled').default(true).notNull(),
|
||||
isOnline: boolean('is_online').default(false).notNull(),
|
||||
lastHeartbeat: timestamp('last_heartbeat', { withTimezone: true }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const allocations = pgTable('allocations', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
nodeId: uuid('node_id')
|
||||
.notNull()
|
||||
.references(() => nodes.id, { onDelete: 'cascade' }),
|
||||
serverId: uuid('server_id').references(() => servers.id, { onDelete: 'set null' }),
|
||||
ip: varchar('ip', { length: 45 }).notNull(),
|
||||
port: integer('port').notNull(),
|
||||
isDefault: boolean('is_default').default(false).notNull(),
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { pgTable, uuid, varchar, integer, timestamp, jsonb, pgEnum } from 'drizzle-orm/pg-core';
|
||||
import { users } from './users';
|
||||
|
||||
export const orgRoleEnum = pgEnum('org_role', ['admin', 'user']);
|
||||
|
||||
export const organizations = pgTable('organizations', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
slug: varchar('slug', { length: 255 }).notNull().unique(),
|
||||
ownerId: uuid('owner_id')
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
maxServers: integer('max_servers').default(0).notNull(),
|
||||
maxNodes: integer('max_nodes').default(0).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const organizationMembers = pgTable('organization_members', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
organizationId: uuid('organization_id')
|
||||
.notNull()
|
||||
.references(() => organizations.id, { onDelete: 'cascade' }),
|
||||
userId: uuid('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
role: orgRoleEnum('role').default('user').notNull(),
|
||||
customPermissions: jsonb('custom_permissions').default({}).notNull(),
|
||||
joinedAt: timestamp('joined_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
varchar,
|
||||
text,
|
||||
boolean,
|
||||
timestamp,
|
||||
pgEnum,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { games } from './games';
|
||||
import { servers } from './servers';
|
||||
|
||||
export const pluginSourceEnum = pgEnum('plugin_source', ['spiget', 'manual']);
|
||||
|
||||
export const plugins = pgTable('plugins', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
gameId: uuid('game_id')
|
||||
.notNull()
|
||||
.references(() => games.id, { onDelete: 'cascade' }),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
slug: varchar('slug', { length: 255 }).notNull(),
|
||||
description: text('description'),
|
||||
source: pluginSourceEnum('source').notNull(),
|
||||
externalId: varchar('external_id', { length: 255 }),
|
||||
downloadUrl: text('download_url'),
|
||||
version: varchar('version', { length: 100 }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const serverPlugins = pgTable('server_plugins', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
serverId: uuid('server_id')
|
||||
.notNull()
|
||||
.references(() => servers.id, { onDelete: 'cascade' }),
|
||||
pluginId: uuid('plugin_id')
|
||||
.notNull()
|
||||
.references(() => plugins.id, { onDelete: 'cascade' }),
|
||||
installedVersion: varchar('installed_version', { length: 100 }),
|
||||
isActive: boolean('is_active').default(true).notNull(),
|
||||
installedAt: timestamp('installed_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
varchar,
|
||||
text,
|
||||
boolean,
|
||||
jsonb,
|
||||
timestamp,
|
||||
pgEnum,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { servers } from './servers';
|
||||
|
||||
export const scheduleActionEnum = pgEnum('schedule_action', ['command', 'power', 'backup']);
|
||||
|
||||
export const scheduleTypeEnum = pgEnum('schedule_type', [
|
||||
'interval',
|
||||
'daily',
|
||||
'weekly',
|
||||
'cron',
|
||||
]);
|
||||
|
||||
export const scheduledTasks = pgTable('scheduled_tasks', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
serverId: uuid('server_id')
|
||||
.notNull()
|
||||
.references(() => servers.id, { onDelete: 'cascade' }),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
action: scheduleActionEnum('action').notNull(),
|
||||
payload: text('payload').notNull(),
|
||||
scheduleType: scheduleTypeEnum('schedule_type').notNull(),
|
||||
scheduleData: jsonb('schedule_data').notNull(),
|
||||
isActive: boolean('is_active').default(true).notNull(),
|
||||
lastRunAt: timestamp('last_run_at', { withTimezone: true }),
|
||||
nextRunAt: timestamp('next_run_at', { withTimezone: true }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
varchar,
|
||||
integer,
|
||||
bigint,
|
||||
text,
|
||||
jsonb,
|
||||
timestamp,
|
||||
pgEnum,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { organizations } from './organizations';
|
||||
import { nodes } from './nodes';
|
||||
import { games } from './games';
|
||||
import { users } from './users';
|
||||
|
||||
export const serverStatusEnum = pgEnum('server_status', [
|
||||
'installing',
|
||||
'running',
|
||||
'stopped',
|
||||
'suspended',
|
||||
'error',
|
||||
]);
|
||||
|
||||
export const servers = pgTable('servers', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
uuid: varchar('uuid', { length: 36 }).notNull().unique(),
|
||||
organizationId: uuid('organization_id')
|
||||
.notNull()
|
||||
.references(() => organizations.id, { onDelete: 'cascade' }),
|
||||
nodeId: uuid('node_id')
|
||||
.notNull()
|
||||
.references(() => nodes.id),
|
||||
gameId: uuid('game_id')
|
||||
.notNull()
|
||||
.references(() => games.id),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
description: text('description'),
|
||||
status: serverStatusEnum('status').default('installing').notNull(),
|
||||
memoryLimit: bigint('memory_limit', { mode: 'number' }).notNull(),
|
||||
diskLimit: bigint('disk_limit', { mode: 'number' }).notNull(),
|
||||
cpuLimit: integer('cpu_limit').default(100).notNull(),
|
||||
port: integer('port').notNull(),
|
||||
additionalPorts: jsonb('additional_ports').default([]).notNull(),
|
||||
environment: jsonb('environment').default({}).notNull(),
|
||||
startupOverride: text('startup_override'),
|
||||
installedAt: timestamp('installed_at', { withTimezone: true }),
|
||||
suspendedAt: timestamp('suspended_at', { withTimezone: true }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const serverSubusers = pgTable('server_subusers', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
serverId: uuid('server_id')
|
||||
.notNull()
|
||||
.references(() => servers.id, { onDelete: 'cascade' }),
|
||||
userId: uuid('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
permissions: jsonb('permissions').notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { pgTable, uuid, varchar, text, boolean, timestamp } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: uuid('id').defaultRandom().primaryKey(),
|
||||
email: varchar('email', { length: 255 }).notNull().unique(),
|
||||
username: varchar('username', { length: 100 }).notNull().unique(),
|
||||
passwordHash: text('password_hash').notNull(),
|
||||
isSuperAdmin: boolean('is_super_admin').default(false).notNull(),
|
||||
avatarUrl: text('avatar_url'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||
});
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
import { createDb } from './client';
|
||||
import { games } from './schema/games';
|
||||
|
||||
async function seed() {
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
if (!databaseUrl) {
|
||||
console.error('DATABASE_URL is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const db = createDb(databaseUrl);
|
||||
|
||||
console.log('Seeding games...');
|
||||
|
||||
await db
|
||||
.insert(games)
|
||||
.values([
|
||||
{
|
||||
slug: 'minecraft-java',
|
||||
name: 'Minecraft: Java Edition',
|
||||
dockerImage: 'itzg/minecraft-server:latest',
|
||||
defaultPort: 25565,
|
||||
startupCommand: '/start',
|
||||
stopCommand: 'stop',
|
||||
configFiles: JSON.stringify([
|
||||
{
|
||||
path: 'server.properties',
|
||||
parser: 'properties',
|
||||
editableKeys: [
|
||||
'server-port',
|
||||
'max-players',
|
||||
'motd',
|
||||
'difficulty',
|
||||
'gamemode',
|
||||
'level-seed',
|
||||
'pvp',
|
||||
'spawn-protection',
|
||||
'view-distance',
|
||||
'online-mode',
|
||||
'white-list',
|
||||
],
|
||||
},
|
||||
{ path: 'ops.json', parser: 'json' },
|
||||
{ path: 'whitelist.json', parser: 'json' },
|
||||
{ path: 'bukkit.yml', parser: 'yaml' },
|
||||
{ path: 'spigot.yml', parser: 'yaml' },
|
||||
]),
|
||||
environmentVars: JSON.stringify([
|
||||
{ key: 'EULA', default: 'TRUE', description: 'Accept Minecraft EULA', required: true },
|
||||
{
|
||||
key: 'TYPE',
|
||||
default: 'PAPER',
|
||||
description: 'Server type (VANILLA, PAPER, SPIGOT, FORGE, FABRIC)',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'VERSION',
|
||||
default: 'LATEST',
|
||||
description: 'Minecraft version',
|
||||
required: true,
|
||||
},
|
||||
{ key: 'MEMORY', default: '1G', description: 'JVM memory allocation', required: false },
|
||||
]),
|
||||
},
|
||||
{
|
||||
slug: 'cs2',
|
||||
name: 'Counter-Strike 2',
|
||||
dockerImage: 'cm2network/csgo:latest',
|
||||
defaultPort: 27015,
|
||||
startupCommand:
|
||||
'./srcds_run -game csgo -console -usercon +game_type 0 +game_mode 0 +mapgroup mg_active +map de_dust2',
|
||||
stopCommand: 'quit',
|
||||
configFiles: JSON.stringify([
|
||||
{
|
||||
path: 'csgo/cfg/server.cfg',
|
||||
parser: 'keyvalue',
|
||||
editableKeys: [
|
||||
'hostname',
|
||||
'sv_password',
|
||||
'rcon_password',
|
||||
'sv_cheats',
|
||||
'mp_autoteambalance',
|
||||
'mp_limitteams',
|
||||
],
|
||||
},
|
||||
{ path: 'csgo/cfg/autoexec.cfg', parser: 'keyvalue' },
|
||||
]),
|
||||
environmentVars: JSON.stringify([
|
||||
{
|
||||
key: 'SRCDS_TOKEN',
|
||||
default: '',
|
||||
description: 'Steam Game Server Login Token',
|
||||
required: true,
|
||||
},
|
||||
{ key: 'SRCDS_RCONPW', default: '', description: 'RCON password', required: false },
|
||||
{ key: 'SRCDS_PW', default: '', description: 'Server password', required: false },
|
||||
{
|
||||
key: 'SRCDS_MAXPLAYERS',
|
||||
default: '16',
|
||||
description: 'Max players',
|
||||
required: false,
|
||||
},
|
||||
]),
|
||||
},
|
||||
])
|
||||
.onConflictDoNothing();
|
||||
|
||||
console.log('Seed completed successfully!');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
seed().catch((err) => {
|
||||
console.error('Seed failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package gamepanel.daemon;
|
||||
|
||||
// Empty message for parameterless RPCs
|
||||
message Empty {}
|
||||
|
||||
// === Node ===
|
||||
|
||||
message NodeStatus {
|
||||
string version = 1;
|
||||
bool is_healthy = 2;
|
||||
int64 uptime_seconds = 3;
|
||||
int32 active_servers = 4;
|
||||
}
|
||||
|
||||
message NodeStats {
|
||||
double cpu_percent = 1;
|
||||
int64 memory_used = 2;
|
||||
int64 memory_total = 3;
|
||||
int64 disk_used = 4;
|
||||
int64 disk_total = 5;
|
||||
}
|
||||
|
||||
// === Server Lifecycle ===
|
||||
|
||||
message ServerIdentifier {
|
||||
string uuid = 1;
|
||||
}
|
||||
|
||||
message PortMapping {
|
||||
int32 host_port = 1;
|
||||
int32 container_port = 2;
|
||||
string protocol = 3; // "tcp" or "udp"
|
||||
}
|
||||
|
||||
message CreateServerRequest {
|
||||
string uuid = 1;
|
||||
string docker_image = 2;
|
||||
int64 memory_limit = 3;
|
||||
int64 disk_limit = 4;
|
||||
int32 cpu_limit = 5;
|
||||
string startup_command = 6;
|
||||
map<string, string> environment = 7;
|
||||
repeated PortMapping ports = 8;
|
||||
repeated string install_plugin_urls = 9;
|
||||
}
|
||||
|
||||
message ServerResponse {
|
||||
string uuid = 1;
|
||||
string status = 2;
|
||||
}
|
||||
|
||||
// === Power ===
|
||||
|
||||
enum PowerAction {
|
||||
START = 0;
|
||||
STOP = 1;
|
||||
RESTART = 2;
|
||||
KILL = 3;
|
||||
}
|
||||
|
||||
message PowerRequest {
|
||||
string uuid = 1;
|
||||
PowerAction action = 2;
|
||||
}
|
||||
|
||||
// === Server Status ===
|
||||
|
||||
message ServerStatus {
|
||||
string uuid = 1;
|
||||
string state = 2;
|
||||
double cpu_percent = 3;
|
||||
int64 memory_bytes = 4;
|
||||
int64 disk_bytes = 5;
|
||||
int64 network_rx = 6;
|
||||
int64 network_tx = 7;
|
||||
int64 uptime_seconds = 8;
|
||||
}
|
||||
|
||||
message ServerResourceStats {
|
||||
string uuid = 1;
|
||||
double cpu_percent = 2;
|
||||
int64 memory_bytes = 3;
|
||||
int64 disk_bytes = 4;
|
||||
int64 network_rx = 5;
|
||||
int64 network_tx = 6;
|
||||
string state = 7;
|
||||
}
|
||||
|
||||
// === Console ===
|
||||
|
||||
message ConsoleOutput {
|
||||
string uuid = 1;
|
||||
string line = 2;
|
||||
int64 timestamp = 3;
|
||||
}
|
||||
|
||||
message CommandRequest {
|
||||
string uuid = 1;
|
||||
string command = 2;
|
||||
}
|
||||
|
||||
// === Files ===
|
||||
|
||||
message FileEntry {
|
||||
string name = 1;
|
||||
string path = 2;
|
||||
bool is_directory = 3;
|
||||
int64 size = 4;
|
||||
int64 modified_at = 5;
|
||||
string mime_type = 6;
|
||||
}
|
||||
|
||||
message FileListRequest {
|
||||
string uuid = 1;
|
||||
string path = 2;
|
||||
}
|
||||
|
||||
message FileListResponse {
|
||||
repeated FileEntry files = 1;
|
||||
}
|
||||
|
||||
message FileReadRequest {
|
||||
string uuid = 1;
|
||||
string path = 2;
|
||||
}
|
||||
|
||||
message FileContent {
|
||||
bytes data = 1;
|
||||
string mime_type = 2;
|
||||
}
|
||||
|
||||
message FileWriteRequest {
|
||||
string uuid = 1;
|
||||
string path = 2;
|
||||
bytes data = 3;
|
||||
}
|
||||
|
||||
message FileDeleteRequest {
|
||||
string uuid = 1;
|
||||
repeated string paths = 2;
|
||||
}
|
||||
|
||||
message CompressRequest {
|
||||
string uuid = 1;
|
||||
repeated string paths = 2;
|
||||
string destination = 3;
|
||||
}
|
||||
|
||||
message DecompressRequest {
|
||||
string uuid = 1;
|
||||
string path = 2;
|
||||
string destination = 3;
|
||||
}
|
||||
|
||||
// === Backup ===
|
||||
|
||||
message BackupRequest {
|
||||
string server_uuid = 1;
|
||||
string backup_id = 2;
|
||||
string cdn_upload_url = 3;
|
||||
}
|
||||
|
||||
message BackupResponse {
|
||||
string backup_id = 1;
|
||||
int64 size_bytes = 2;
|
||||
string checksum = 3;
|
||||
bool success = 4;
|
||||
}
|
||||
|
||||
message BackupIdentifier {
|
||||
string server_uuid = 1;
|
||||
string backup_id = 2;
|
||||
}
|
||||
|
||||
message RestoreBackupRequest {
|
||||
string server_uuid = 1;
|
||||
string backup_id = 2;
|
||||
string cdn_download_url = 3;
|
||||
}
|
||||
|
||||
// === Players ===
|
||||
|
||||
message Player {
|
||||
string name = 1;
|
||||
string uuid = 2;
|
||||
int64 connected_at = 3;
|
||||
}
|
||||
|
||||
message PlayerList {
|
||||
repeated Player players = 1;
|
||||
int32 max_players = 2;
|
||||
}
|
||||
|
||||
// === Install Progress ===
|
||||
|
||||
message InstallProgress {
|
||||
string uuid = 1;
|
||||
int32 percent = 2;
|
||||
string message = 3;
|
||||
}
|
||||
|
||||
// === Service Definition ===
|
||||
|
||||
service DaemonService {
|
||||
// Node
|
||||
rpc GetNodeStatus(Empty) returns (NodeStatus);
|
||||
rpc StreamNodeStats(Empty) returns (stream NodeStats);
|
||||
|
||||
// Server lifecycle
|
||||
rpc CreateServer(CreateServerRequest) returns (ServerResponse);
|
||||
rpc DeleteServer(ServerIdentifier) returns (Empty);
|
||||
rpc ReinstallServer(ServerIdentifier) returns (Empty);
|
||||
|
||||
// Power
|
||||
rpc SetPowerState(PowerRequest) returns (Empty);
|
||||
rpc GetServerStatus(ServerIdentifier) returns (ServerStatus);
|
||||
|
||||
// Console
|
||||
rpc StreamConsole(ServerIdentifier) returns (stream ConsoleOutput);
|
||||
rpc SendCommand(CommandRequest) returns (Empty);
|
||||
|
||||
// Files
|
||||
rpc ListFiles(FileListRequest) returns (FileListResponse);
|
||||
rpc ReadFile(FileReadRequest) returns (FileContent);
|
||||
rpc WriteFile(FileWriteRequest) returns (Empty);
|
||||
rpc DeleteFiles(FileDeleteRequest) returns (Empty);
|
||||
rpc CompressFiles(CompressRequest) returns (FileContent);
|
||||
rpc DecompressFile(DecompressRequest) returns (Empty);
|
||||
|
||||
// Backup
|
||||
rpc CreateBackup(BackupRequest) returns (BackupResponse);
|
||||
rpc RestoreBackup(RestoreBackupRequest) returns (Empty);
|
||||
rpc DeleteBackup(BackupIdentifier) returns (Empty);
|
||||
|
||||
// Stats
|
||||
rpc StreamServerStats(ServerIdentifier) returns (stream ServerResourceStats);
|
||||
|
||||
// Install progress
|
||||
rpc StreamInstallProgress(ServerIdentifier) returns (stream InstallProgress);
|
||||
|
||||
// Players
|
||||
rpc GetActivePlayers(ServerIdentifier) returns (PlayerList);
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "@source/proto",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"generate": "buf generate"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// Proto generated types will be exported here after running `pnpm generate`
|
||||
// For now, this is a placeholder
|
||||
|
||||
export const PROTO_PATH = new URL('../daemon.proto', import.meta.url).pathname;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "@source/shared",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint src/",
|
||||
"build": "tsc"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from './permissions';
|
||||
export * from './roles';
|
||||
export * from './types';
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
export const PERMISSIONS = {
|
||||
// Server
|
||||
'server.create': 'Create servers',
|
||||
'server.read': 'View servers',
|
||||
'server.update': 'Update server settings',
|
||||
'server.delete': 'Delete servers',
|
||||
|
||||
// Console
|
||||
'console.read': 'View console output',
|
||||
'console.write': 'Send console commands',
|
||||
|
||||
// Files
|
||||
'files.read': 'View and download files',
|
||||
'files.write': 'Create, edit, and upload files',
|
||||
'files.delete': 'Delete files',
|
||||
'files.archive': 'Compress and decompress files',
|
||||
|
||||
// Backup
|
||||
'backup.create': 'Create backups',
|
||||
'backup.restore': 'Restore backups',
|
||||
'backup.delete': 'Delete backups',
|
||||
'backup.download': 'Download backups',
|
||||
|
||||
// Schedule
|
||||
'schedule.read': 'View scheduled tasks',
|
||||
'schedule.manage': 'Create, edit, and delete scheduled tasks',
|
||||
|
||||
// Subuser
|
||||
'subuser.read': 'View subusers',
|
||||
'subuser.manage': 'Create, edit, and delete subusers',
|
||||
|
||||
// Plugin
|
||||
'plugin.read': 'View installed plugins',
|
||||
'plugin.manage': 'Install and remove plugins',
|
||||
|
||||
// Config
|
||||
'config.read': 'View server config',
|
||||
'config.write': 'Edit server config',
|
||||
|
||||
// Power
|
||||
'power.start': 'Start server',
|
||||
'power.stop': 'Stop server',
|
||||
'power.restart': 'Restart server',
|
||||
'power.kill': 'Kill server',
|
||||
|
||||
// Node (org admin)
|
||||
'node.read': 'View nodes',
|
||||
'node.manage': 'Manage nodes and allocations',
|
||||
|
||||
// Organization
|
||||
'org.settings': 'Manage organization settings',
|
||||
'org.members': 'Manage organization members',
|
||||
} as const;
|
||||
|
||||
export type Permission = keyof typeof PERMISSIONS;
|
||||
|
||||
export const ALL_PERMISSIONS = Object.keys(PERMISSIONS) as Permission[];
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import type { Permission } from './permissions';
|
||||
import { ALL_PERMISSIONS } from './permissions';
|
||||
|
||||
export const ROLES = {
|
||||
admin: {
|
||||
name: 'Admin',
|
||||
description: 'Full access to the organization',
|
||||
permissions: ALL_PERMISSIONS,
|
||||
},
|
||||
user: {
|
||||
name: 'User',
|
||||
description: 'Limited access, must be granted server-specific permissions',
|
||||
permissions: [
|
||||
'server.read',
|
||||
'console.read',
|
||||
'files.read',
|
||||
'backup.create',
|
||||
'backup.download',
|
||||
'schedule.read',
|
||||
'plugin.read',
|
||||
'config.read',
|
||||
'power.start',
|
||||
'power.stop',
|
||||
'power.restart',
|
||||
] as Permission[],
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type Role = keyof typeof ROLES;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
export type ServerStatus = 'installing' | 'running' | 'stopped' | 'suspended' | 'error';
|
||||
|
||||
export type PowerAction = 'start' | 'stop' | 'restart' | 'kill';
|
||||
|
||||
export type ScheduleType = 'interval' | 'daily' | 'weekly' | 'cron';
|
||||
|
||||
export type ScheduleAction = 'command' | 'power' | 'backup';
|
||||
|
||||
export type PluginSource = 'spiget' | 'manual';
|
||||
|
||||
export interface PaginationParams {
|
||||
page: number;
|
||||
perPage: number;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
meta: {
|
||||
page: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "@source/ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint src/",
|
||||
"build": "tsc"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"tailwind-merge": "^2.6.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { cn } from './lib/utils';
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,3 @@
|
|||
packages:
|
||||
- "apps/*"
|
||||
- "packages/*"
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022"],
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"tasks": {
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"db:generate": {
|
||||
"cache": false
|
||||
},
|
||||
"db:migrate": {
|
||||
"cache": false
|
||||
},
|
||||
"db:seed": {
|
||||
"cache": false
|
||||
},
|
||||
"db:studio": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue