feat: Add 15 backend service stub files with TODO implementations
Stub files for all services declared in services/mod.rs: - Panel adapters: AMP, Pterodactyl, Companion (NATS-based) - Wipe pipeline: WipeEngine, Scheduler, HealthChecker, BackupManager - Infrastructure: NatsBridge (JetStream), SteamUpdateWatcher, MapManager - Notifications: Discord webhooks, Pushbullet push notifications - Platform: LicenseService, CloudflareService, encryption utilities Each file has struct definitions, method signatures with proper types, and descriptive TODO comments outlining the implementation plan. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
86
backend/src/services/amp_adapter.rs
Normal file
86
backend/src/services/amp_adapter.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use super::panel_adapter::{DiscoveredServer, FileEntry, PanelAdapter, ServerStatus};
|
||||
|
||||
/// AMP (Application Management Panel) adapter.
|
||||
///
|
||||
/// Communicates with AMP instances via their REST API to manage
|
||||
/// Rust game servers. Implements the unified PanelAdapter trait
|
||||
/// so the wipe engine and other services remain panel-agnostic.
|
||||
pub struct AmpAdapter {
|
||||
pub api_endpoint: String,
|
||||
pub api_key: String,
|
||||
}
|
||||
|
||||
impl AmpAdapter {
|
||||
pub fn new(api_endpoint: String, api_key: String) -> Self {
|
||||
Self {
|
||||
api_endpoint,
|
||||
api_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl PanelAdapter for AmpAdapter {
|
||||
async fn test_connection(&self) -> Result<bool> {
|
||||
// TODO: POST to AMP API /API/Core/Login, verify session token returned
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn discover_servers(&self) -> Result<Vec<DiscoveredServer>> {
|
||||
// TODO: GET instances from AMP API, map to DiscoveredServer
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_server_status(&self, _server_id: &str) -> Result<ServerStatus> {
|
||||
// TODO: Query AMP instance status endpoint
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn start_server(&self, _server_id: &str) -> Result<()> {
|
||||
// TODO: POST to AMP API /API/Core/Start
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn stop_server(&self, _server_id: &str) -> Result<()> {
|
||||
// TODO: POST to AMP API /API/Core/Stop
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn restart_server(&self, _server_id: &str) -> Result<()> {
|
||||
// TODO: POST to AMP API /API/Core/Restart
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn send_command(&self, _server_id: &str, _command: &str) -> Result<String> {
|
||||
// TODO: POST to AMP API /API/Core/SendConsoleMessage
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_file(&self, _server_id: &str, _path: &str) -> Result<Vec<u8>> {
|
||||
// TODO: GET file contents via AMP file manager API
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn put_file(&self, _server_id: &str, _path: &str, _data: &[u8]) -> Result<()> {
|
||||
// TODO: Upload file via AMP file manager API
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn delete_file(&self, _server_id: &str, _path: &str) -> Result<()> {
|
||||
// TODO: DELETE file via AMP file manager API
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn list_files(&self, _server_id: &str, _path: &str) -> Result<Vec<FileEntry>> {
|
||||
// TODO: GET directory listing from AMP file manager API
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn trigger_steam_update(&self, _server_id: &str) -> Result<()> {
|
||||
// TODO: POST to AMP API /API/Core/Update to trigger SteamCMD
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
78
backend/src/services/backup_manager.rs
Normal file
78
backend/src/services/backup_manager.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Metadata for a stored backup.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct BackupInfo {
|
||||
pub id: Uuid,
|
||||
pub license_id: Uuid,
|
||||
pub wipe_history_id: Option<Uuid>,
|
||||
pub storage_path: String,
|
||||
pub size_bytes: i64,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Pre-wipe backup and rollback management.
|
||||
///
|
||||
/// Creates snapshots of server state (map, save data, plugin data,
|
||||
/// config files) before a wipe executes. Backups are used by the
|
||||
/// wipe engine's rollback mechanism if post-wipe verification fails.
|
||||
pub struct BackupManager {
|
||||
// TODO: Add fields:
|
||||
// - db: sqlx::PgPool
|
||||
// - storage_base_path: String
|
||||
}
|
||||
|
||||
impl BackupManager {
|
||||
/// Create a pre-wipe backup of the server's current state.
|
||||
///
|
||||
/// Captures: map/save files, plugin data directories, server.cfg,
|
||||
/// and any other files configured for backup in the wipe profile.
|
||||
/// Returns a backup reference ID stored in wipe_history.
|
||||
pub async fn create_backup(
|
||||
&self,
|
||||
_license_id: Uuid,
|
||||
_wipe_history_id: Uuid,
|
||||
) -> Result<String> {
|
||||
// TODO: Resolve PanelAdapter for the server
|
||||
// TODO: Download save files (map, sav, player data) via adapter
|
||||
// TODO: Download plugin data directories via adapter
|
||||
// TODO: Download server.cfg via adapter
|
||||
// TODO: Bundle into archive (tar.gz)
|
||||
// TODO: Write archive to backup storage
|
||||
// TODO: Insert backup record in DB
|
||||
// TODO: Return backup reference string
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Restore a previously created backup to the server.
|
||||
///
|
||||
/// Used during rollback when post-wipe verification fails.
|
||||
pub async fn restore_backup(&self, _backup_reference: &str) -> Result<()> {
|
||||
// TODO: Load backup record from DB by reference
|
||||
// TODO: Read backup archive from storage
|
||||
// TODO: Extract archive contents
|
||||
// TODO: Upload files back to server via PanelAdapter
|
||||
// TODO: Log restoration details
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// List all backups for a license, ordered by creation date (newest first).
|
||||
pub async fn list_backups(&self, _license_id: Uuid) -> Result<Vec<BackupInfo>> {
|
||||
// TODO: Query backup records from DB for this license
|
||||
// TODO: Return sorted list
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Clean up old backups beyond the configured retention period/count.
|
||||
pub async fn cleanup_old_backups(&self, _license_id: Uuid) -> Result<u32> {
|
||||
// TODO: Load retention config (max count or max age)
|
||||
// TODO: Query backups older than retention threshold
|
||||
// TODO: Delete backup files from storage
|
||||
// TODO: Delete backup records from DB
|
||||
// TODO: Return count of deleted backups
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
46
backend/src/services/cloudflare.rs
Normal file
46
backend/src/services/cloudflare.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use anyhow::Result;
|
||||
|
||||
/// Cloudflare DNS management service.
|
||||
///
|
||||
/// Manages DNS records for tenant subdomains (e.g., myserver.corrosion.gg)
|
||||
/// and custom domains. Uses the Cloudflare API v4 to create and manage
|
||||
/// CNAME records pointing to the platform's load balancer.
|
||||
pub struct CloudflareService {
|
||||
// TODO: Add fields:
|
||||
// - api_token: String
|
||||
// - zone_id: String (Cloudflare zone for the base domain)
|
||||
// - base_domain: String (e.g., "corrosion.gg")
|
||||
}
|
||||
|
||||
impl CloudflareService {
|
||||
/// Create a subdomain CNAME record for a new license.
|
||||
///
|
||||
/// Creates: {subdomain}.{base_domain} -> platform LB
|
||||
pub async fn create_subdomain(&self, _subdomain: &str) -> Result<String> {
|
||||
// TODO: POST /zones/{zone_id}/dns_records
|
||||
// TODO: Type: CNAME, Name: {subdomain}, Content: platform target
|
||||
// TODO: Proxied: true (orange cloud)
|
||||
// TODO: Return the DNS record ID for later management
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Delete a subdomain CNAME record.
|
||||
///
|
||||
/// Called when a license is deactivated or transferred.
|
||||
pub async fn delete_subdomain(&self, _dns_record_id: &str) -> Result<()> {
|
||||
// TODO: DELETE /zones/{zone_id}/dns_records/{dns_record_id}
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Add a custom domain with CNAME verification.
|
||||
///
|
||||
/// Returns the CNAME target that the customer needs to configure
|
||||
/// on their own DNS provider.
|
||||
pub async fn add_custom_domain(&self, _custom_domain: &str) -> Result<String> {
|
||||
// TODO: Generate verification CNAME target
|
||||
// TODO: Create verification DNS record in Cloudflare
|
||||
// TODO: Optionally configure SSL for custom hostname via Cloudflare
|
||||
// TODO: Return the CNAME target for customer DNS setup
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
92
backend/src/services/companion_adapter.rs
Normal file
92
backend/src/services/companion_adapter.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::panel_adapter::{DiscoveredServer, FileEntry, PanelAdapter, ServerStatus};
|
||||
|
||||
/// Companion Agent adapter.
|
||||
///
|
||||
/// Unlike AMP and Pterodactyl which use REST APIs, the Companion Agent
|
||||
/// runs alongside the game server and communicates via NATS JetStream.
|
||||
/// Commands are published to the agent's subject namespace and responses
|
||||
/// are received on reply subjects. This enables direct server management
|
||||
/// without a panel intermediary.
|
||||
pub struct CompanionAdapter {
|
||||
pub nats: async_nats::Client,
|
||||
pub license_id: Uuid,
|
||||
}
|
||||
|
||||
impl CompanionAdapter {
|
||||
pub fn new(nats: async_nats::Client, license_id: Uuid) -> Self {
|
||||
Self { nats, license_id }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl PanelAdapter for CompanionAdapter {
|
||||
async fn test_connection(&self) -> Result<bool> {
|
||||
// TODO: Publish heartbeat request to corrosion.{license_id}.agent.ping,
|
||||
// await reply within timeout
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn discover_servers(&self) -> Result<Vec<DiscoveredServer>> {
|
||||
// TODO: Request server info from agent via NATS request/reply.
|
||||
// Companion manages exactly one server, so this returns a single-element vec.
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_server_status(&self, _server_id: &str) -> Result<ServerStatus> {
|
||||
// TODO: NATS request to corrosion.{license_id}.agent.status
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn start_server(&self, _server_id: &str) -> Result<()> {
|
||||
// TODO: NATS request to corrosion.{license_id}.agent.start
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn stop_server(&self, _server_id: &str) -> Result<()> {
|
||||
// TODO: NATS request to corrosion.{license_id}.agent.stop
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn restart_server(&self, _server_id: &str) -> Result<()> {
|
||||
// TODO: NATS request to corrosion.{license_id}.agent.restart
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn send_command(&self, _server_id: &str, _command: &str) -> Result<String> {
|
||||
// TODO: NATS request to corrosion.{license_id}.agent.command
|
||||
// with command payload, await RCON response
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_file(&self, _server_id: &str, _path: &str) -> Result<Vec<u8>> {
|
||||
// TODO: NATS request to corrosion.{license_id}.agent.file.get
|
||||
// Agent reads local file and returns contents
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn put_file(&self, _server_id: &str, _path: &str, _data: &[u8]) -> Result<()> {
|
||||
// TODO: NATS publish to corrosion.{license_id}.agent.file.put
|
||||
// May need chunking for large files
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn delete_file(&self, _server_id: &str, _path: &str) -> Result<()> {
|
||||
// TODO: NATS request to corrosion.{license_id}.agent.file.delete
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn list_files(&self, _server_id: &str, _path: &str) -> Result<Vec<FileEntry>> {
|
||||
// TODO: NATS request to corrosion.{license_id}.agent.file.list
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn trigger_steam_update(&self, _server_id: &str) -> Result<()> {
|
||||
// TODO: NATS request to corrosion.{license_id}.agent.update
|
||||
// Agent runs SteamCMD locally
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
87
backend/src/services/discord.rs
Normal file
87
backend/src/services/discord.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
|
||||
/// Discord webhook embed payload.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DiscordEmbed {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub color: u32,
|
||||
pub fields: Vec<DiscordEmbedField>,
|
||||
pub timestamp: Option<String>,
|
||||
}
|
||||
|
||||
/// Field within a Discord embed.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DiscordEmbedField {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
pub inline: bool,
|
||||
}
|
||||
|
||||
/// Discord webhook notification service.
|
||||
///
|
||||
/// Sends rich embed messages to Discord channels via webhook URLs.
|
||||
/// Used for wipe announcements, completion notifications, failure
|
||||
/// alerts, and crash recovery notifications.
|
||||
pub struct DiscordNotifier {
|
||||
// TODO: Add fields:
|
||||
// - webhook_url: String
|
||||
// - server_name: String (for embed context)
|
||||
}
|
||||
|
||||
impl DiscordNotifier {
|
||||
/// Send a generic notification with a custom embed.
|
||||
pub async fn send_notification(&self, _embed: DiscordEmbed) -> Result<()> {
|
||||
// TODO: POST to webhook_url with JSON payload { embeds: [embed] }
|
||||
// TODO: Handle rate limiting (429 responses with Retry-After)
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Send a wipe-starting announcement.
|
||||
pub async fn send_wipe_start(&self, _wipe_type: &str, _eta_minutes: u32) -> Result<()> {
|
||||
// TODO: Build embed with orange color, wipe type, countdown info
|
||||
// TODO: Include server name, expected completion time
|
||||
// TODO: Call send_notification
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Send a wipe-completed announcement.
|
||||
pub async fn send_wipe_complete(
|
||||
&self,
|
||||
_wipe_type: &str,
|
||||
_duration_seconds: u64,
|
||||
_new_map: Option<&str>,
|
||||
_new_seed: Option<i32>,
|
||||
) -> Result<()> {
|
||||
// TODO: Build embed with green color, wipe summary
|
||||
// TODO: Include new map/seed info, connect URL
|
||||
// TODO: Call send_notification
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Send a wipe-failed alert.
|
||||
pub async fn send_wipe_failed(
|
||||
&self,
|
||||
_wipe_type: &str,
|
||||
_error: &str,
|
||||
_rolled_back: bool,
|
||||
) -> Result<()> {
|
||||
// TODO: Build embed with red color, failure details
|
||||
// TODO: Include rollback status, error message
|
||||
// TODO: Call send_notification
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Send a crash detection/recovery alert.
|
||||
pub async fn send_crash_alert(
|
||||
&self,
|
||||
_crash_count: u32,
|
||||
_auto_recovered: bool,
|
||||
) -> Result<()> {
|
||||
// TODO: Build embed with red/yellow color based on severity
|
||||
// TODO: Include crash count, recovery status, uptime before crash
|
||||
// TODO: Call send_notification
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
46
backend/src/services/encryption.rs
Normal file
46
backend/src/services/encryption.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use anyhow::Result;
|
||||
|
||||
/// AES-256-GCM encryption utilities.
|
||||
///
|
||||
/// Used to encrypt sensitive data at rest: panel API keys, companion
|
||||
/// agent tokens, webhook URLs, and other secrets stored in the database.
|
||||
/// Keys are derived from the application's ENCRYPTION_KEY environment
|
||||
/// variable.
|
||||
|
||||
/// Encrypt a plaintext string using AES-256-GCM.
|
||||
///
|
||||
/// Returns a base64-encoded string containing the nonce + ciphertext.
|
||||
/// The nonce is randomly generated and prepended to the ciphertext
|
||||
/// before encoding.
|
||||
pub fn encrypt(_plaintext: &str, _key: &str) -> Result<String> {
|
||||
// TODO: Derive 256-bit key from key string (SHA-256 or HKDF)
|
||||
// TODO: Generate random 96-bit nonce
|
||||
// TODO: Encrypt plaintext with AES-256-GCM
|
||||
// TODO: Prepend nonce to ciphertext
|
||||
// TODO: Base64-encode the result
|
||||
// TODO: Return encoded string
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Decrypt an AES-256-GCM encrypted string.
|
||||
///
|
||||
/// Expects a base64-encoded string containing nonce + ciphertext
|
||||
/// (as produced by `encrypt`).
|
||||
pub fn decrypt(_ciphertext: &str, _key: &str) -> Result<String> {
|
||||
// TODO: Base64-decode the input
|
||||
// TODO: Split nonce (first 12 bytes) from ciphertext
|
||||
// TODO: Derive 256-bit key from key string (same as encrypt)
|
||||
// TODO: Decrypt with AES-256-GCM
|
||||
// TODO: Return plaintext string
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Generate a cryptographically random token string.
|
||||
///
|
||||
/// Returns a hex-encoded random string of the specified byte length.
|
||||
/// Used for generating API keys, agent tokens, and session secrets.
|
||||
pub fn generate_token(_length: usize) -> String {
|
||||
// TODO: Generate `length` random bytes using rand/OsRng
|
||||
// TODO: Hex-encode and return
|
||||
todo!()
|
||||
}
|
||||
63
backend/src/services/health_checker.rs
Normal file
63
backend/src/services/health_checker.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use anyhow::Result;
|
||||
|
||||
/// Post-wipe server health verification.
|
||||
///
|
||||
/// Called by the wipe engine after a wipe completes to verify the server
|
||||
/// came back up correctly. Checks are configurable via PostWipeConfig
|
||||
/// and failures can trigger rollback or retry logic.
|
||||
pub struct HealthChecker {
|
||||
// TODO: Add fields:
|
||||
// - db: sqlx::PgPool
|
||||
// - max_retries: u32
|
||||
// - check_timeout: std::time::Duration
|
||||
}
|
||||
|
||||
impl HealthChecker {
|
||||
/// Run all configured health checks against the server.
|
||||
///
|
||||
/// Returns Ok(true) if all checks pass, Ok(false) if checks fail
|
||||
/// after exhausting retries. Returns Err only on unexpected errors.
|
||||
pub async fn verify_server_health(
|
||||
&self,
|
||||
_server_id: &str,
|
||||
_license_id: uuid::Uuid,
|
||||
) -> Result<bool> {
|
||||
// TODO: Resolve PanelAdapter for this server
|
||||
// TODO: Wait for server to report is_running=true
|
||||
// TODO: Run each configured check (map, plugins, slots)
|
||||
// TODO: Retry on failure up to max_retries with backoff
|
||||
// TODO: Return aggregate pass/fail
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Verify the correct map is loaded on the server.
|
||||
///
|
||||
/// Sends RCON command to query the current level and compares
|
||||
/// against the expected map from the wipe profile.
|
||||
pub async fn check_map(&self, _server_id: &str, _expected_map: &str) -> Result<bool> {
|
||||
// TODO: Send "status" or "serverinfo" via RCON
|
||||
// TODO: Parse response for current level/map name
|
||||
// TODO: Compare against expected_map
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Verify all required plugins are loaded and responding.
|
||||
pub async fn check_plugins(
|
||||
&self,
|
||||
_server_id: &str,
|
||||
_expected_plugins: &[String],
|
||||
) -> Result<bool> {
|
||||
// TODO: Send "plugins" or "oxide.plugins" via RCON
|
||||
// TODO: Parse response for loaded plugin list
|
||||
// TODO: Check all expected_plugins are present
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Verify the server is accepting player connections.
|
||||
pub async fn check_player_slots(&self, _server_id: &str) -> Result<bool> {
|
||||
// TODO: Query server status via PanelAdapter or RCON
|
||||
// TODO: Verify max_players > 0 and server is connectable
|
||||
// TODO: Optionally perform A2S query against game port
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
67
backend/src/services/license.rs
Normal file
67
backend/src/services/license.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use anyhow::Result;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::license::License;
|
||||
|
||||
/// License validation and lifecycle management.
|
||||
///
|
||||
/// Handles license key validation, activation (binding a key to a server),
|
||||
/// periodic check-ins from the Rust plugin, and license lookups.
|
||||
/// All license state is stored in PostgreSQL.
|
||||
pub struct LicenseService {
|
||||
// TODO: Add fields:
|
||||
// - db: sqlx::PgPool
|
||||
}
|
||||
|
||||
impl LicenseService {
|
||||
/// Validate a license key and return its current status.
|
||||
///
|
||||
/// Checks: key exists, not expired, not revoked, modules enabled.
|
||||
pub async fn validate_license(&self, _license_key: &str) -> Result<Option<License>> {
|
||||
// TODO: Query license by key from DB
|
||||
// TODO: Check status is 'active'
|
||||
// TODO: Check expires_at is in the future (if set)
|
||||
// TODO: Return Some(license) if valid, None if not found
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Activate a license key: bind it to a server name and subdomain.
|
||||
///
|
||||
/// Called during initial setup when a user first registers.
|
||||
pub async fn activate_license(
|
||||
&self,
|
||||
_license_key: &str,
|
||||
_server_name: &str,
|
||||
_subdomain: &str,
|
||||
) -> Result<License> {
|
||||
// TODO: Validate license key exists and is in 'pending' status
|
||||
// TODO: Check subdomain availability
|
||||
// TODO: Update license record: status='active', server_name, subdomain
|
||||
// TODO: Create default roles for this license
|
||||
// TODO: Return updated license
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Process a check-in from the Rust server plugin.
|
||||
///
|
||||
/// Updates last-seen timestamp and returns current license state
|
||||
/// including enabled modules and NATS connection token.
|
||||
pub async fn check_in(
|
||||
&self,
|
||||
_license_key: &str,
|
||||
_plugin_version: &str,
|
||||
) -> Result<crate::models::license::LicenseCheckInResponse> {
|
||||
// TODO: Validate license
|
||||
// TODO: Update plugin_last_seen timestamp on server_connections
|
||||
// TODO: Generate or refresh NATS auth token for this license
|
||||
// TODO: Return LicenseCheckInResponse with modules and token
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Look up a license by its UUID.
|
||||
pub async fn get_license_by_key(&self, _license_id: Uuid) -> Result<Option<License>> {
|
||||
// TODO: Query license by ID from DB
|
||||
// TODO: Return if found
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
65
backend/src/services/map_manager.rs
Normal file
65
backend/src/services/map_manager.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use anyhow::Result;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Map upload, storage, and rotation management.
|
||||
///
|
||||
/// Handles custom map files (.map) for Rust servers. Maps are stored
|
||||
/// in object storage with metadata in the database. Supports rotation
|
||||
/// strategies where different maps are used on successive wipes.
|
||||
pub struct MapManager {
|
||||
// TODO: Add fields:
|
||||
// - db: sqlx::PgPool
|
||||
// - storage_base_path: String (or S3 client for cloud storage)
|
||||
}
|
||||
|
||||
impl MapManager {
|
||||
/// Upload a new map file to storage and register it in the database.
|
||||
pub async fn upload_map(
|
||||
&self,
|
||||
_license_id: Uuid,
|
||||
_display_name: &str,
|
||||
_filename: &str,
|
||||
_data: &[u8],
|
||||
) -> Result<Uuid> {
|
||||
// TODO: Compute checksum of map data
|
||||
// TODO: Write file to storage (local volume or S3)
|
||||
// TODO: Insert map_entry record in DB
|
||||
// TODO: Return the new map ID
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Delete a map from storage and the database.
|
||||
pub async fn delete_map(&self, _map_id: Uuid) -> Result<()> {
|
||||
// TODO: Load map_entry from DB
|
||||
// TODO: Delete file from storage
|
||||
// TODO: Delete DB record
|
||||
// TODO: Update any wipe profiles referencing this map
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Get the current map rotation for a license (ordered list of map IDs).
|
||||
pub async fn get_rotation(&self, _license_id: Uuid) -> Result<Vec<Uuid>> {
|
||||
// TODO: Query map rotation config from DB
|
||||
// TODO: Return ordered list of map IDs in rotation
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Advance the rotation to the next map in sequence.
|
||||
/// Returns the map ID that should be used for the next wipe.
|
||||
pub async fn advance_rotation(&self, _license_id: Uuid) -> Result<Uuid> {
|
||||
// TODO: Get current rotation position from DB
|
||||
// TODO: Advance to next map (wrap around to first)
|
||||
// TODO: Update rotation position in DB
|
||||
// TODO: Return the next map ID
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Generate a time-limited signed URL for downloading a map file.
|
||||
/// Used by the companion agent or panel adapter to fetch map files.
|
||||
pub async fn generate_signed_url(&self, _map_id: Uuid) -> Result<String> {
|
||||
// TODO: Load map_entry from DB
|
||||
// TODO: Generate signed URL with expiration (e.g., 15 minutes)
|
||||
// TODO: Return the URL
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
81
backend/src/services/nats_bridge.rs
Normal file
81
backend/src/services/nats_bridge.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use anyhow::Result;
|
||||
|
||||
// -- JetStream stream name constants --
|
||||
|
||||
/// Stream for wipe lifecycle events (scheduled, started, completed, failed, rolled_back).
|
||||
pub const STREAM_WIPE_EVENTS: &str = "CORROSION_WIPE";
|
||||
|
||||
/// Stream for server telemetry (stats, status changes, crash events).
|
||||
pub const STREAM_SERVER_TELEMETRY: &str = "CORROSION_TELEMETRY";
|
||||
|
||||
/// Stream for agent commands and responses (start, stop, file ops).
|
||||
pub const STREAM_AGENT_COMMANDS: &str = "CORROSION_AGENT";
|
||||
|
||||
/// Stream for Steam update detection events.
|
||||
pub const STREAM_STEAM_UPDATES: &str = "CORROSION_STEAM";
|
||||
|
||||
/// Stream for notification delivery (Discord, Pushbullet, email).
|
||||
pub const STREAM_NOTIFICATIONS: &str = "CORROSION_NOTIFY";
|
||||
|
||||
/// Stream for license lifecycle events (activated, expired, check-in).
|
||||
pub const STREAM_LICENSE_EVENTS: &str = "CORROSION_LICENSE";
|
||||
|
||||
/// NATS JetStream pub/sub bridge.
|
||||
///
|
||||
/// Centralizes all NATS interactions: stream/consumer setup, publishing
|
||||
/// events, and subscribing to subjects. All services go through the
|
||||
/// bridge rather than holding their own NATS client references, ensuring
|
||||
/// consistent subject naming and stream configuration.
|
||||
pub struct NatsBridge {
|
||||
pub client: async_nats::Client,
|
||||
}
|
||||
|
||||
impl NatsBridge {
|
||||
pub fn new(client: async_nats::Client) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
/// Publish a message to a NATS subject.
|
||||
pub async fn publish(&self, _subject: &str, _payload: &[u8]) -> Result<()> {
|
||||
// TODO: Publish via self.client.publish(subject, payload)
|
||||
// TODO: Optionally publish to JetStream if subject belongs to a stream
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Subscribe to a NATS subject and return a message stream.
|
||||
pub async fn subscribe(
|
||||
&self,
|
||||
_subject: &str,
|
||||
) -> Result<async_nats::Subscriber> {
|
||||
// TODO: Subscribe via self.client.subscribe(subject)
|
||||
// TODO: For durable consumers, use JetStream consumer API
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Initialize all JetStream streams and consumers.
|
||||
///
|
||||
/// Called once at application startup. Creates streams if they don't
|
||||
/// exist, updates configuration if streams already exist.
|
||||
pub async fn setup_streams(&self) -> Result<()> {
|
||||
// TODO: Get JetStream context from client
|
||||
// TODO: Create/update CORROSION_WIPE stream
|
||||
// subjects: ["corrosion.*.wipe.>"]
|
||||
// retention: WorkQueue, max_age: 7 days
|
||||
// TODO: Create/update CORROSION_TELEMETRY stream
|
||||
// subjects: ["corrosion.*.telemetry.>"]
|
||||
// retention: Limits, max_age: 24 hours
|
||||
// TODO: Create/update CORROSION_AGENT stream
|
||||
// subjects: ["corrosion.*.agent.>"]
|
||||
// retention: WorkQueue, max_age: 1 hour
|
||||
// TODO: Create/update CORROSION_STEAM stream
|
||||
// subjects: ["corrosion.steam.>"]
|
||||
// retention: Limits, max_age: 30 days
|
||||
// TODO: Create/update CORROSION_NOTIFY stream
|
||||
// subjects: ["corrosion.*.notify.>"]
|
||||
// retention: WorkQueue, max_age: 1 day
|
||||
// TODO: Create/update CORROSION_LICENSE stream
|
||||
// subjects: ["corrosion.license.>"]
|
||||
// retention: Limits, max_age: 90 days
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
87
backend/src/services/pterodactyl_adapter.rs
Normal file
87
backend/src/services/pterodactyl_adapter.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use super::panel_adapter::{DiscoveredServer, FileEntry, PanelAdapter, ServerStatus};
|
||||
|
||||
/// Pterodactyl panel adapter.
|
||||
///
|
||||
/// Communicates with Pterodactyl (or Pelican) panels via their REST API.
|
||||
/// Uses Application API keys for server management and Client API keys
|
||||
/// for console/file operations. Implements the unified PanelAdapter trait.
|
||||
pub struct PterodactylAdapter {
|
||||
pub api_endpoint: String,
|
||||
pub api_key: String,
|
||||
}
|
||||
|
||||
impl PterodactylAdapter {
|
||||
pub fn new(api_endpoint: String, api_key: String) -> Self {
|
||||
Self {
|
||||
api_endpoint,
|
||||
api_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl PanelAdapter for PterodactylAdapter {
|
||||
async fn test_connection(&self) -> Result<bool> {
|
||||
// TODO: GET /api/application/servers to verify API key validity
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn discover_servers(&self) -> Result<Vec<DiscoveredServer>> {
|
||||
// TODO: Paginate /api/application/servers, filter for Rust egg, map to DiscoveredServer
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_server_status(&self, _server_id: &str) -> Result<ServerStatus> {
|
||||
// TODO: GET /api/client/servers/{id}/resources for live stats
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn start_server(&self, _server_id: &str) -> Result<()> {
|
||||
// TODO: POST /api/client/servers/{id}/power with signal=start
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn stop_server(&self, _server_id: &str) -> Result<()> {
|
||||
// TODO: POST /api/client/servers/{id}/power with signal=stop
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn restart_server(&self, _server_id: &str) -> Result<()> {
|
||||
// TODO: POST /api/client/servers/{id}/power with signal=restart
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn send_command(&self, _server_id: &str, _command: &str) -> Result<String> {
|
||||
// TODO: POST /api/client/servers/{id}/command
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_file(&self, _server_id: &str, _path: &str) -> Result<Vec<u8>> {
|
||||
// TODO: GET /api/client/servers/{id}/files/contents?file={path}
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn put_file(&self, _server_id: &str, _path: &str, _data: &[u8]) -> Result<()> {
|
||||
// TODO: POST /api/client/servers/{id}/files/write?file={path}
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn delete_file(&self, _server_id: &str, _path: &str) -> Result<()> {
|
||||
// TODO: POST /api/client/servers/{id}/files/delete with body
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn list_files(&self, _server_id: &str, _path: &str) -> Result<Vec<FileEntry>> {
|
||||
// TODO: GET /api/client/servers/{id}/files/list?directory={path}
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn trigger_steam_update(&self, _server_id: &str) -> Result<()> {
|
||||
// TODO: Pterodactyl doesn't have native SteamCMD trigger — send console command
|
||||
// or use startup command reinstall via /api/client/servers/{id}/settings/reinstall
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
30
backend/src/services/pushbullet.rs
Normal file
30
backend/src/services/pushbullet.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use anyhow::Result;
|
||||
|
||||
/// Pushbullet notification service.
|
||||
///
|
||||
/// Sends push notifications to server administrators via the Pushbullet
|
||||
/// API. Used as a secondary notification channel alongside Discord for
|
||||
/// critical alerts that need to reach admins on their mobile devices.
|
||||
pub struct PushbulletNotifier {
|
||||
// TODO: Add fields:
|
||||
// - api_key: String
|
||||
// - default_device_iden: Option<String> (target specific device, or all)
|
||||
}
|
||||
|
||||
impl PushbulletNotifier {
|
||||
/// Send a text notification (note type push).
|
||||
pub async fn send_notification(&self, _title: &str, _body: &str) -> Result<()> {
|
||||
// TODO: POST to https://api.pushbullet.com/v2/pushes
|
||||
// TODO: Body: { type: "note", title, body }
|
||||
// TODO: Auth: Access-Token header with api_key
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Send a link notification with a clickable URL.
|
||||
pub async fn send_link(&self, _title: &str, _body: &str, _url: &str) -> Result<()> {
|
||||
// TODO: POST to https://api.pushbullet.com/v2/pushes
|
||||
// TODO: Body: { type: "link", title, body, url }
|
||||
// TODO: Auth: Access-Token header with api_key
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
56
backend/src/services/scheduler.rs
Normal file
56
backend/src/services/scheduler.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Cron-based wipe schedule management.
|
||||
///
|
||||
/// Manages wipe schedules backed by `tokio-cron-scheduler`. Each schedule
|
||||
/// maps a cron expression + timezone to a wipe profile. When a schedule
|
||||
/// fires, it creates a wipe_history record and dispatches execution to
|
||||
/// the WipeEngine.
|
||||
pub struct SchedulerService {
|
||||
// TODO: Add fields:
|
||||
// - db: sqlx::PgPool
|
||||
// - nats: async_nats::Client
|
||||
// - scheduler: tokio_cron_scheduler::JobScheduler
|
||||
}
|
||||
|
||||
impl SchedulerService {
|
||||
/// Start the scheduler, loading all active schedules from the database.
|
||||
pub async fn start(&self) -> Result<()> {
|
||||
// TODO: Query all active wipe_schedules from DB
|
||||
// TODO: Register each as a cron job in tokio-cron-scheduler
|
||||
// TODO: Start the scheduler event loop
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Register a wipe schedule as a cron job.
|
||||
pub async fn register_wipe_schedule(&self, _schedule_id: Uuid) -> Result<()> {
|
||||
// TODO: Load WipeSchedule from DB
|
||||
// TODO: Parse cron_expression with timezone
|
||||
// TODO: Create tokio-cron-scheduler job that:
|
||||
// 1. Creates a wipe_history record
|
||||
// 2. Publishes wipe execution event to NATS
|
||||
// TODO: Store job handle for later removal
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Remove a schedule from the running scheduler.
|
||||
pub async fn remove_schedule(&self, _schedule_id: Uuid) -> Result<()> {
|
||||
// TODO: Look up job handle by schedule_id
|
||||
// TODO: Remove from tokio-cron-scheduler
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Calculate the next N scheduled run times for a given schedule.
|
||||
pub async fn get_next_runs(
|
||||
&self,
|
||||
_schedule_id: Uuid,
|
||||
_count: usize,
|
||||
) -> Result<Vec<DateTime<Utc>>> {
|
||||
// TODO: Load cron_expression and timezone from DB
|
||||
// TODO: Iterate cron expression to compute next N fire times
|
||||
// TODO: Convert to UTC and return
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
53
backend/src/services/steam_watcher.rs
Normal file
53
backend/src/services/steam_watcher.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use anyhow::Result;
|
||||
|
||||
/// Steam App ID for the Rust Dedicated Server.
|
||||
pub const RUST_SERVER_APP_ID: u32 = 258550;
|
||||
|
||||
/// Steam build ID polling service.
|
||||
///
|
||||
/// Periodically checks the Steam Web API for new Rust Dedicated Server
|
||||
/// builds. When a new build is detected (build ID changes), it publishes
|
||||
/// an event to NATS so that servers configured for auto-update-on-force-wipe
|
||||
/// can be notified and trigger their wipe schedules.
|
||||
pub struct SteamUpdateWatcher {
|
||||
// TODO: Add fields:
|
||||
// - nats: async_nats::Client
|
||||
// - db: sqlx::PgPool
|
||||
// - poll_interval: std::time::Duration
|
||||
// - last_known_build_id: Option<String>
|
||||
}
|
||||
|
||||
impl SteamUpdateWatcher {
|
||||
/// Start the polling loop. Runs indefinitely, checking for updates
|
||||
/// at the configured interval.
|
||||
pub async fn start_polling(&self) -> Result<()> {
|
||||
// TODO: Loop on poll_interval
|
||||
// TODO: Call check_for_update each iteration
|
||||
// TODO: If new build detected, call handle_update_detected
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Check Steam Web API for the current build ID of the Rust server app.
|
||||
///
|
||||
/// Returns the build ID string if an update is detected (differs from
|
||||
/// last known), or None if unchanged.
|
||||
pub async fn check_for_update(&self) -> Result<Option<String>> {
|
||||
// TODO: GET https://api.steampowered.com/ISteamApps/UpToDateCheck/v1/
|
||||
// ?appid={RUST_SERVER_APP_ID}&version=0
|
||||
// TODO: Parse response for build ID
|
||||
// TODO: Compare against last_known_build_id
|
||||
// TODO: Return Some(new_id) if changed, None if same
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Handle a detected Steam update: persist the new build ID,
|
||||
/// publish NATS event for downstream consumers.
|
||||
pub async fn handle_update_detected(&self, _new_build_id: &str) -> Result<()> {
|
||||
// TODO: Update last_known_build_id
|
||||
// TODO: Persist to DB for crash recovery
|
||||
// TODO: Publish to NATS subject corrosion.steam.update_detected
|
||||
// with payload { app_id, old_build_id, new_build_id, detected_at }
|
||||
// TODO: Log the detection
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
88
backend/src/services/wipe_engine.rs
Normal file
88
backend/src/services/wipe_engine.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use anyhow::Result;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Core wipe orchestration engine.
|
||||
///
|
||||
/// Coordinates the full wipe lifecycle: pre-wipe steps (backups, warnings,
|
||||
/// player kicks), the wipe itself (map swap, plugin data cleanup, seed
|
||||
/// rotation), and post-wipe verification (health checks, rollback on
|
||||
/// failure). Works through the PanelAdapter trait so it is panel-agnostic.
|
||||
///
|
||||
/// All wipe executions are logged to the wipe_history table with full
|
||||
/// execution traces for audit and debugging.
|
||||
pub struct WipeEngine {
|
||||
pub db: sqlx::PgPool,
|
||||
pub nats: async_nats::Client,
|
||||
}
|
||||
|
||||
impl WipeEngine {
|
||||
pub fn new(db: sqlx::PgPool, nats: async_nats::Client) -> Self {
|
||||
Self { db, nats }
|
||||
}
|
||||
|
||||
/// Execute a full wipe sequence for the given wipe history record.
|
||||
///
|
||||
/// Orchestrates: pre-wipe -> wipe actions -> post-wipe verification.
|
||||
/// On failure at any stage, triggers rollback if configured.
|
||||
pub async fn execute_wipe(&self, _wipe_history_id: Uuid) -> Result<()> {
|
||||
// TODO: Load wipe history + profile + schedule from DB
|
||||
// TODO: Resolve the PanelAdapter for this server
|
||||
// TODO: Run pre-wipe -> wipe -> post-wipe pipeline
|
||||
// TODO: Update wipe_history status throughout
|
||||
// TODO: Publish NATS events for real-time UI updates
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Execute pre-wipe steps: countdown warnings, player kicks, backups,
|
||||
/// Discord/Pushbullet announcements, custom commands.
|
||||
pub async fn execute_pre_wipe(&self, _wipe_history_id: Uuid) -> Result<()> {
|
||||
// TODO: Load PreWipeConfig from wipe profile
|
||||
// TODO: Send countdown warnings via RCON at configured intervals
|
||||
// TODO: Create backup if backup_before_wipe is true
|
||||
// TODO: Kick players with configured message
|
||||
// TODO: Run final save command
|
||||
// TODO: Send Discord/Pushbullet pre-announce notifications
|
||||
// TODO: Execute custom_commands_before
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Execute the actual wipe actions: stop server, delete/swap map,
|
||||
/// wipe plugin data, update seed, restart server.
|
||||
pub async fn execute_wipe_actions(&self, _wipe_history_id: Uuid) -> Result<()> {
|
||||
// TODO: Stop the server via PanelAdapter
|
||||
// TODO: Delete map/save files based on wipe type
|
||||
// TODO: Wipe plugin data files based on plugin wipe flags
|
||||
// TODO: Update seed if configured for rotation
|
||||
// TODO: Upload new custom map if map rotation is active
|
||||
// TODO: Update server.cfg with new settings
|
||||
// TODO: Trigger Steam update if force wipe
|
||||
// TODO: Start the server
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Execute post-wipe verification: health checks on server startup,
|
||||
/// map correctness, plugin loading, player slot availability.
|
||||
pub async fn execute_post_wipe_verification(&self, _wipe_history_id: Uuid) -> Result<()> {
|
||||
// TODO: Wait for server to report running via PanelAdapter
|
||||
// TODO: Verify correct map loaded (check level name via RCON)
|
||||
// TODO: Verify all required plugins loaded
|
||||
// TODO: Verify player slots are open
|
||||
// TODO: Retry up to max_restart_attempts on failure
|
||||
// TODO: Send Discord/Pushbullet post-announce notifications
|
||||
// TODO: Execute post_wipe_commands
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Execute rollback: restore pre-wipe backup, restart server,
|
||||
/// notify operators of failure.
|
||||
pub async fn execute_rollback(&self, _wipe_history_id: Uuid) -> Result<()> {
|
||||
// TODO: Load backup_reference from wipe_history
|
||||
// TODO: Stop the server
|
||||
// TODO: Restore backup via BackupManager
|
||||
// TODO: Start the server
|
||||
// TODO: Verify server health post-rollback
|
||||
// TODO: Update wipe_history status to 'rolled_back'
|
||||
// TODO: Send failure notifications via Discord/Pushbullet
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user