From e5ed25a86aed7c9f9b6ed809ca93b18261415dbc Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Sat, 14 Feb 2026 21:42:05 -0500 Subject: [PATCH] scaffold: Backend API routes, DB queries, and middleware stubs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 88 handler stubs across 13 route files, 66 DB query stubs across 11 modules, auth/license extractors, and rate limit middleware. All bodies are todo!() — ready for Phase 1b implementation. Co-Authored-By: Claude Opus 4.6 --- backend/src/api/auth.rs | 48 ++++++++++++++++++ backend/src/api/license.rs | 38 +++++++++++++++ backend/src/api/logs.rs | 23 +++++++++ backend/src/api/maps.rs | 48 ++++++++++++++++++ backend/src/api/mod.rs | 13 +++++ backend/src/api/notifications.rs | 33 +++++++++++++ backend/src/api/panels.rs | 43 ++++++++++++++++ backend/src/api/plugins.rs | 43 ++++++++++++++++ backend/src/api/public.rs | 58 ++++++++++++++++++++++ backend/src/api/schedules.rs | 53 ++++++++++++++++++++ backend/src/api/servers.rs | 53 ++++++++++++++++++++ backend/src/api/store.rs | 73 ++++++++++++++++++++++++++++ backend/src/api/team.rs | 53 ++++++++++++++++++++ backend/src/api/wipes.rs | 43 ++++++++++++++++ backend/src/db/chat.rs | 31 ++++++++++++ backend/src/db/licenses.rs | 35 +++++++++++++ backend/src/db/maps.rs | 36 ++++++++++++++ backend/src/db/mod.rs | 14 ++++++ backend/src/db/notifications.rs | 15 ++++++ backend/src/db/plugins.rs | 30 ++++++++++++ backend/src/db/servers.rs | 52 ++++++++++++++++++++ backend/src/db/stats.rs | 26 ++++++++++ backend/src/db/store.rs | 53 ++++++++++++++++++++ backend/src/db/teams.rs | 46 ++++++++++++++++++ backend/src/db/users.rs | 30 ++++++++++++ backend/src/db/wipes.rs | 42 ++++++++++++++++ backend/src/middleware/auth.rs | 32 ++++++++++++ backend/src/middleware/license.rs | 32 ++++++++++++ backend/src/middleware/mod.rs | 3 ++ backend/src/middleware/rate_limit.rs | 17 +++++++ 30 files changed, 1116 insertions(+) create mode 100644 backend/src/api/auth.rs create mode 100644 backend/src/api/license.rs create mode 100644 backend/src/api/logs.rs create mode 100644 backend/src/api/maps.rs create mode 100644 backend/src/api/mod.rs create mode 100644 backend/src/api/notifications.rs create mode 100644 backend/src/api/panels.rs create mode 100644 backend/src/api/plugins.rs create mode 100644 backend/src/api/public.rs create mode 100644 backend/src/api/schedules.rs create mode 100644 backend/src/api/servers.rs create mode 100644 backend/src/api/store.rs create mode 100644 backend/src/api/team.rs create mode 100644 backend/src/api/wipes.rs create mode 100644 backend/src/db/chat.rs create mode 100644 backend/src/db/licenses.rs create mode 100644 backend/src/db/maps.rs create mode 100644 backend/src/db/mod.rs create mode 100644 backend/src/db/notifications.rs create mode 100644 backend/src/db/plugins.rs create mode 100644 backend/src/db/servers.rs create mode 100644 backend/src/db/stats.rs create mode 100644 backend/src/db/store.rs create mode 100644 backend/src/db/teams.rs create mode 100644 backend/src/db/users.rs create mode 100644 backend/src/db/wipes.rs create mode 100644 backend/src/middleware/auth.rs create mode 100644 backend/src/middleware/license.rs create mode 100644 backend/src/middleware/mod.rs create mode 100644 backend/src/middleware/rate_limit.rs diff --git a/backend/src/api/auth.rs b/backend/src/api/auth.rs new file mode 100644 index 0000000..24665cd --- /dev/null +++ b/backend/src/api/auth.rs @@ -0,0 +1,48 @@ +use std::sync::Arc; + +use axum::{ + routing::post, + Router, +}; + +use crate::models::error::ApiResult; +use crate::AppState; + +pub fn router() -> Router> { + Router::new() + .route("/login", post(login)) + .route("/register", post(register)) + .route("/verify-totp", post(verify_totp)) + .route("/refresh", post(refresh)) + .route("/setup-totp", post(setup_totp)) + .route("/backup-codes", post(backup_codes)) + .route("/logout", post(logout)) +} + +async fn login() -> ApiResult { + todo!() +} + +async fn register() -> ApiResult { + todo!() +} + +async fn verify_totp() -> ApiResult { + todo!() +} + +async fn refresh() -> ApiResult { + todo!() +} + +async fn setup_totp() -> ApiResult { + todo!() +} + +async fn backup_codes() -> ApiResult { + todo!() +} + +async fn logout() -> ApiResult { + todo!() +} diff --git a/backend/src/api/license.rs b/backend/src/api/license.rs new file mode 100644 index 0000000..34cab19 --- /dev/null +++ b/backend/src/api/license.rs @@ -0,0 +1,38 @@ +use std::sync::Arc; + +use axum::{ + routing::{get, post, put}, + Router, +}; + +use crate::models::error::ApiResult; +use crate::AppState; + +pub fn router() -> Router> { + Router::new() + .route("/", get(get_license_info)) + .route("/activate", post(activate_license)) + .route("/check-in", post(check_in)) + .route("/subdomain", put(update_subdomain)) + .route("/custom-domain", put(update_custom_domain)) +} + +async fn get_license_info() -> ApiResult { + todo!() +} + +async fn activate_license() -> ApiResult { + todo!() +} + +async fn check_in() -> ApiResult { + todo!() +} + +async fn update_subdomain() -> ApiResult { + todo!() +} + +async fn update_custom_domain() -> ApiResult { + todo!() +} diff --git a/backend/src/api/logs.rs b/backend/src/api/logs.rs new file mode 100644 index 0000000..05f65f5 --- /dev/null +++ b/backend/src/api/logs.rs @@ -0,0 +1,23 @@ +use std::sync::Arc; + +use axum::{ + routing::get, + Router, +}; + +use crate::models::error::ApiResult; +use crate::AppState; + +pub fn router() -> Router> { + Router::new() + .route("/", get(query_logs)) + .route("/export", get(export_logs)) +} + +async fn query_logs() -> ApiResult { + todo!() +} + +async fn export_logs() -> ApiResult { + todo!() +} diff --git a/backend/src/api/maps.rs b/backend/src/api/maps.rs new file mode 100644 index 0000000..448769b --- /dev/null +++ b/backend/src/api/maps.rs @@ -0,0 +1,48 @@ +use std::sync::Arc; + +use axum::{ + routing::{delete, get, post, put}, + Router, +}; + +use crate::models::error::ApiResult; +use crate::AppState; + +pub fn router() -> Router> { + Router::new() + .route("/", get(list_maps)) + .route("/upload", post(upload_map)) + .route("/{id}", get(get_map)) + .route("/{id}", delete(delete_map)) + .route("/{id}/download", get(download_map)) + .route("/rotation/{server_id}", get(get_map_rotation)) + .route("/rotation/{server_id}", put(update_map_rotation)) +} + +async fn list_maps() -> ApiResult { + todo!() +} + +async fn upload_map() -> ApiResult { + todo!() +} + +async fn get_map() -> ApiResult { + todo!() +} + +async fn delete_map() -> ApiResult { + todo!() +} + +async fn download_map() -> ApiResult { + todo!() +} + +async fn get_map_rotation() -> ApiResult { + todo!() +} + +async fn update_map_rotation() -> ApiResult { + todo!() +} diff --git a/backend/src/api/mod.rs b/backend/src/api/mod.rs new file mode 100644 index 0000000..9646619 --- /dev/null +++ b/backend/src/api/mod.rs @@ -0,0 +1,13 @@ +pub mod auth; +pub mod servers; +pub mod wipes; +pub mod maps; +pub mod plugins; +pub mod panels; +pub mod schedules; +pub mod logs; +pub mod public; +pub mod team; +pub mod notifications; +pub mod license; +pub mod store; diff --git a/backend/src/api/notifications.rs b/backend/src/api/notifications.rs new file mode 100644 index 0000000..6d89a24 --- /dev/null +++ b/backend/src/api/notifications.rs @@ -0,0 +1,33 @@ +use std::sync::Arc; + +use axum::{ + routing::{get, post, put}, + Router, +}; + +use crate::models::error::ApiResult; +use crate::AppState; + +pub fn router() -> Router> { + Router::new() + .route("/", get(get_notification_config)) + .route("/", put(update_notification_config)) + .route("/test/discord", post(test_discord)) + .route("/test/pushbullet", post(test_pushbullet)) +} + +async fn get_notification_config() -> ApiResult { + todo!() +} + +async fn update_notification_config() -> ApiResult { + todo!() +} + +async fn test_discord() -> ApiResult { + todo!() +} + +async fn test_pushbullet() -> ApiResult { + todo!() +} diff --git a/backend/src/api/panels.rs b/backend/src/api/panels.rs new file mode 100644 index 0000000..d10c63c --- /dev/null +++ b/backend/src/api/panels.rs @@ -0,0 +1,43 @@ +use std::sync::Arc; + +use axum::{ + routing::{delete, get, post, put}, + Router, +}; + +use crate::models::error::ApiResult; +use crate::AppState; + +pub fn router() -> Router> { + Router::new() + .route("/", get(list_panels)) + .route("/", post(create_panel)) + .route("/{id}", put(update_panel)) + .route("/{id}", delete(delete_panel)) + .route("/{id}/test", post(test_panel)) + .route("/{id}/discover", get(discover_panel)) +} + +async fn list_panels() -> ApiResult { + todo!() +} + +async fn create_panel() -> ApiResult { + todo!() +} + +async fn update_panel() -> ApiResult { + todo!() +} + +async fn delete_panel() -> ApiResult { + todo!() +} + +async fn test_panel() -> ApiResult { + todo!() +} + +async fn discover_panel() -> ApiResult { + todo!() +} diff --git a/backend/src/api/plugins.rs b/backend/src/api/plugins.rs new file mode 100644 index 0000000..b26275a --- /dev/null +++ b/backend/src/api/plugins.rs @@ -0,0 +1,43 @@ +use std::sync::Arc; + +use axum::{ + routing::{delete, get, post, put}, + Router, +}; + +use crate::models::error::ApiResult; +use crate::AppState; + +pub fn router() -> Router> { + Router::new() + .route("/", get(list_plugins)) + .route("/install", post(install_plugin)) + .route("/{id}", put(update_plugin)) + .route("/{id}", delete(delete_plugin)) + .route("/{id}/reload", post(reload_plugin)) + .route("/search", get(search_umod)) +} + +async fn list_plugins() -> ApiResult { + todo!() +} + +async fn install_plugin() -> ApiResult { + todo!() +} + +async fn update_plugin() -> ApiResult { + todo!() +} + +async fn delete_plugin() -> ApiResult { + todo!() +} + +async fn reload_plugin() -> ApiResult { + todo!() +} + +async fn search_umod() -> ApiResult { + todo!() +} diff --git a/backend/src/api/public.rs b/backend/src/api/public.rs new file mode 100644 index 0000000..d844a20 --- /dev/null +++ b/backend/src/api/public.rs @@ -0,0 +1,58 @@ +use std::sync::Arc; + +use axum::{ + routing::get, + Router, +}; + +use crate::models::error::ApiResult; +use crate::AppState; + +pub fn router() -> Router> { + Router::new() + .route("/servers", get(list_public_servers)) + .route("/servers/{id}", get(get_public_server)) + .route("/servers/{id}/wipe-schedule", get(get_wipe_schedule)) + .route("/servers/{id}/countdown", get(get_countdown)) + .route("/servers/{id}/map", get(get_current_map)) + .route("/servers/{id}/players", get(get_players)) + .route("/servers/{id}/mods", get(get_mods)) + .route("/servers/{id}/motd", get(get_motd)) + .route("/servers/{id}/store", get(get_store)) +} + +async fn list_public_servers() -> ApiResult { + todo!() +} + +async fn get_public_server() -> ApiResult { + todo!() +} + +async fn get_wipe_schedule() -> ApiResult { + todo!() +} + +async fn get_countdown() -> ApiResult { + todo!() +} + +async fn get_current_map() -> ApiResult { + todo!() +} + +async fn get_players() -> ApiResult { + todo!() +} + +async fn get_mods() -> ApiResult { + todo!() +} + +async fn get_motd() -> ApiResult { + todo!() +} + +async fn get_store() -> ApiResult { + todo!() +} diff --git a/backend/src/api/schedules.rs b/backend/src/api/schedules.rs new file mode 100644 index 0000000..d458047 --- /dev/null +++ b/backend/src/api/schedules.rs @@ -0,0 +1,53 @@ +use std::sync::Arc; + +use axum::{ + routing::{delete, get, post, put}, + Router, +}; + +use crate::models::error::ApiResult; +use crate::AppState; + +pub fn router() -> Router> { + Router::new() + .route("/", get(list_schedules)) + .route("/", post(create_schedule)) + .route("/{id}", get(get_schedule)) + .route("/{id}", put(update_schedule)) + .route("/{id}", delete(delete_schedule)) + .route("/{id}/toggle", post(toggle_schedule)) + .route("/calendar", get(get_calendar)) + .route("/conflicts", get(get_conflicts)) +} + +async fn list_schedules() -> ApiResult { + todo!() +} + +async fn create_schedule() -> ApiResult { + todo!() +} + +async fn get_schedule() -> ApiResult { + todo!() +} + +async fn update_schedule() -> ApiResult { + todo!() +} + +async fn delete_schedule() -> ApiResult { + todo!() +} + +async fn toggle_schedule() -> ApiResult { + todo!() +} + +async fn get_calendar() -> ApiResult { + todo!() +} + +async fn get_conflicts() -> ApiResult { + todo!() +} diff --git a/backend/src/api/servers.rs b/backend/src/api/servers.rs new file mode 100644 index 0000000..2ed953b --- /dev/null +++ b/backend/src/api/servers.rs @@ -0,0 +1,53 @@ +use std::sync::Arc; + +use axum::{ + routing::{get, post, put}, + Router, +}; + +use crate::models::error::ApiResult; +use crate::AppState; + +pub fn router() -> Router> { + Router::new() + .route("/", get(list_servers)) + .route("/{id}", get(get_server)) + .route("/{id}", put(update_server)) + .route("/{id}/command", post(send_command)) + .route("/{id}/plugins", get(get_server_plugins)) + .route("/{id}/start", post(start_server)) + .route("/{id}/stop", post(stop_server)) + .route("/{id}/restart", post(restart_server)) +} + +async fn list_servers() -> ApiResult { + todo!() +} + +async fn get_server() -> ApiResult { + todo!() +} + +async fn update_server() -> ApiResult { + todo!() +} + +async fn send_command() -> ApiResult { + todo!() +} + +async fn get_server_plugins() -> ApiResult { + todo!() +} + +async fn start_server() -> ApiResult { + todo!() +} + +async fn stop_server() -> ApiResult { + todo!() +} + +async fn restart_server() -> ApiResult { + todo!() +} diff --git a/backend/src/api/store.rs b/backend/src/api/store.rs new file mode 100644 index 0000000..46a34f9 --- /dev/null +++ b/backend/src/api/store.rs @@ -0,0 +1,73 @@ +use std::sync::Arc; + +use axum::{ + routing::{delete, get, post, put}, + Router, +}; + +use crate::models::error::ApiResult; +use crate::AppState; + +pub fn router() -> Router> { + Router::new() + .route("/config", get(get_store_config)) + .route("/config", put(update_store_config)) + .route("/categories", get(list_categories)) + .route("/categories", post(create_category)) + .route("/categories/{id}", put(update_category)) + .route("/categories/{id}", delete(delete_category)) + .route("/items", get(list_items)) + .route("/items", post(create_item)) + .route("/items/{id}", put(update_item)) + .route("/items/{id}", delete(delete_item)) + .route("/transactions", get(list_transactions)) + .route("/webhook/paypal", post(paypal_webhook)) +} + +async fn get_store_config() -> ApiResult { + todo!() +} + +async fn update_store_config() -> ApiResult { + todo!() +} + +async fn list_categories() -> ApiResult { + todo!() +} + +async fn create_category() -> ApiResult { + todo!() +} + +async fn update_category() -> ApiResult { + todo!() +} + +async fn delete_category() -> ApiResult { + todo!() +} + +async fn list_items() -> ApiResult { + todo!() +} + +async fn create_item() -> ApiResult { + todo!() +} + +async fn update_item() -> ApiResult { + todo!() +} + +async fn delete_item() -> ApiResult { + todo!() +} + +async fn list_transactions() -> ApiResult { + todo!() +} + +async fn paypal_webhook() -> ApiResult { + todo!() +} diff --git a/backend/src/api/team.rs b/backend/src/api/team.rs new file mode 100644 index 0000000..0f9a2b0 --- /dev/null +++ b/backend/src/api/team.rs @@ -0,0 +1,53 @@ +use std::sync::Arc; + +use axum::{ + routing::{delete, get, post, put}, + Router, +}; + +use crate::models::error::ApiResult; +use crate::AppState; + +pub fn router() -> Router> { + Router::new() + .route("/", get(list_members)) + .route("/invite", post(invite_member)) + .route("/{id}/role", put(update_member_role)) + .route("/{id}", delete(remove_member)) + .route("/roles", get(list_roles)) + .route("/roles", post(create_role)) + .route("/roles/{id}", put(update_role)) + .route("/roles/{id}", delete(delete_role)) +} + +async fn list_members() -> ApiResult { + todo!() +} + +async fn invite_member() -> ApiResult { + todo!() +} + +async fn update_member_role() -> ApiResult { + todo!() +} + +async fn remove_member() -> ApiResult { + todo!() +} + +async fn list_roles() -> ApiResult { + todo!() +} + +async fn create_role() -> ApiResult { + todo!() +} + +async fn update_role() -> ApiResult { + todo!() +} + +async fn delete_role() -> ApiResult { + todo!() +} diff --git a/backend/src/api/wipes.rs b/backend/src/api/wipes.rs new file mode 100644 index 0000000..8f0982e --- /dev/null +++ b/backend/src/api/wipes.rs @@ -0,0 +1,43 @@ +use std::sync::Arc; + +use axum::{ + routing::{get, post}, + Router, +}; + +use crate::models::error::ApiResult; +use crate::AppState; + +pub fn router() -> Router> { + Router::new() + .route("/{server_id}/trigger", post(trigger_wipe)) + .route("/{server_id}/dry-run", post(dry_run_wipe)) + .route("/{wipe_id}/rollback", post(rollback_wipe)) + .route("/{wipe_id}/status", get(get_wipe_status)) + .route("/history", get(get_wipe_history)) + .route("/history/{id}", get(get_wipe_history_detail)) +} + +async fn trigger_wipe() -> ApiResult { + todo!() +} + +async fn dry_run_wipe() -> ApiResult { + todo!() +} + +async fn rollback_wipe() -> ApiResult { + todo!() +} + +async fn get_wipe_status() -> ApiResult { + todo!() +} + +async fn get_wipe_history() -> ApiResult { + todo!() +} + +async fn get_wipe_history_detail() -> ApiResult { + todo!() +} diff --git a/backend/src/db/chat.rs b/backend/src/db/chat.rs new file mode 100644 index 0000000..7f22077 --- /dev/null +++ b/backend/src/db/chat.rs @@ -0,0 +1,31 @@ +use sqlx::PgPool; +use uuid::Uuid; +use anyhow::Result; + +// TODO: Define ChatMessage struct (id, server_id, steam_id, player_name, message, channel, flagged, timestamp) +// TODO: Define PlayerAction struct (id, server_id, steam_id, action_type, reason, performed_by, created_at) + +/// Store an incoming chat message from the game server. +pub async fn insert_chat_message(pool: &PgPool, server_id: Uuid, steam_id: &str, player_name: &str, message: &str, channel: &str) -> Result { + todo!() +} + +/// Retrieve chat messages for a server with pagination. +pub async fn get_chat_messages(pool: &PgPool, server_id: Uuid, limit: i64, offset: i64) -> Result<()> { + todo!() +} + +/// Flag a chat message for review (toxic, spam, etc.). +pub async fn flag_message(pool: &PgPool, message_id: Uuid, flagged: bool) -> Result<()> { + todo!() +} + +/// Get moderation actions taken against a player. +pub async fn get_player_actions(pool: &PgPool, server_id: Uuid, steam_id: &str) -> Result<()> { + todo!() +} + +/// Record a moderation action (kick, ban, mute, warn). +pub async fn create_player_action(pool: &PgPool, server_id: Uuid, steam_id: &str, action_type: &str, reason: &str, performed_by: Uuid) -> Result { + todo!() +} diff --git a/backend/src/db/licenses.rs b/backend/src/db/licenses.rs new file mode 100644 index 0000000..8b6b073 --- /dev/null +++ b/backend/src/db/licenses.rs @@ -0,0 +1,35 @@ +use sqlx::PgPool; +use uuid::Uuid; +use anyhow::Result; + +// TODO: Define License struct (id, user_id, license_key, status, tier, modules, max_servers, activated_at, expires_at, created_at) + +/// Create a new license record. +pub async fn create_license(pool: &PgPool, user_id: Uuid, license_key: &str, tier: &str) -> Result { + todo!() +} + +/// Look up a license by its key string (used during activation). +pub async fn get_license_by_key(pool: &PgPool, license_key: &str) -> Result<()> { + todo!() +} + +/// Fetch a license by its primary key. +pub async fn get_license_by_id(pool: &PgPool, license_id: Uuid) -> Result<()> { + todo!() +} + +/// Fetch all licenses belonging to a user. +pub async fn get_license_by_user(pool: &PgPool, user_id: Uuid) -> Result<()> { + todo!() +} + +/// Mark a license as activated and record the activation timestamp. +pub async fn activate_license(pool: &PgPool, license_id: Uuid) -> Result<()> { + todo!() +} + +/// Update the status of a license (active, suspended, expired, revoked). +pub async fn update_license_status(pool: &PgPool, license_id: Uuid, status: &str) -> Result<()> { + todo!() +} diff --git a/backend/src/db/maps.rs b/backend/src/db/maps.rs new file mode 100644 index 0000000..859ba01 --- /dev/null +++ b/backend/src/db/maps.rs @@ -0,0 +1,36 @@ +use sqlx::PgPool; +use uuid::Uuid; +use anyhow::Result; + +// TODO: Define Map struct (id, server_id, name, file_path, size_bytes, uploaded_at) +// TODO: Define MapRotation struct (id, server_id, map_ids, current_index, auto_rotate) + +/// Upload/register a new custom map. +pub async fn create_map(pool: &PgPool, server_id: Uuid, name: &str, file_path: &str, size_bytes: i64) -> Result { + todo!() +} + +/// Get all maps for a server. +pub async fn get_maps(pool: &PgPool, server_id: Uuid) -> Result<()> { + todo!() +} + +/// Fetch a single map by ID. +pub async fn get_map_by_id(pool: &PgPool, map_id: Uuid) -> Result<()> { + todo!() +} + +/// Delete a map record (and its associated file reference). +pub async fn delete_map(pool: &PgPool, map_id: Uuid) -> Result<()> { + todo!() +} + +/// Get the current map rotation configuration for a server. +pub async fn get_map_rotation(pool: &PgPool, server_id: Uuid) -> Result<()> { + todo!() +} + +/// Update the map rotation (set map order, current index, toggle auto-rotate). +pub async fn update_map_rotation(pool: &PgPool, server_id: Uuid, map_ids: &[Uuid], auto_rotate: bool) -> Result<()> { + todo!() +} diff --git a/backend/src/db/mod.rs b/backend/src/db/mod.rs new file mode 100644 index 0000000..22e95ac --- /dev/null +++ b/backend/src/db/mod.rs @@ -0,0 +1,14 @@ +// Database query modules — each corresponds to a domain +// Queries use sqlx with compile-time verification against PostgreSQL + +pub mod users; +pub mod licenses; +pub mod servers; +pub mod wipes; +pub mod maps; +pub mod plugins; +pub mod teams; +pub mod notifications; +pub mod chat; +pub mod stats; +pub mod store; diff --git a/backend/src/db/notifications.rs b/backend/src/db/notifications.rs new file mode 100644 index 0000000..37f012e --- /dev/null +++ b/backend/src/db/notifications.rs @@ -0,0 +1,15 @@ +use sqlx::PgPool; +use uuid::Uuid; +use anyhow::Result; + +// TODO: Define NotificationConfig struct (id, server_id, discord_webhook_url, events jsonb, enabled, created_at, updated_at) + +/// Fetch the notification configuration for a server. +pub async fn get_notification_config(pool: &PgPool, server_id: Uuid) -> Result<()> { + todo!() +} + +/// Insert or update the notification configuration for a server. +pub async fn upsert_notification_config(pool: &PgPool, server_id: Uuid, discord_webhook_url: Option<&str>, events: &str, enabled: bool) -> Result<()> { + todo!() +} diff --git a/backend/src/db/plugins.rs b/backend/src/db/plugins.rs new file mode 100644 index 0000000..ee49d2f --- /dev/null +++ b/backend/src/db/plugins.rs @@ -0,0 +1,30 @@ +use sqlx::PgPool; +use uuid::Uuid; +use anyhow::Result; + +// TODO: Define Plugin struct (id, server_id, name, version, author, is_enabled, config_json, wipe_on_map, wipe_on_bp, created_at, updated_at) + +/// Insert or update a plugin record (upsert by server_id + name). +pub async fn upsert_plugin(pool: &PgPool, server_id: Uuid, name: &str, version: &str, author: Option<&str>) -> Result { + todo!() +} + +/// Get all plugins for a server. +pub async fn get_plugins(pool: &PgPool, server_id: Uuid) -> Result<()> { + todo!() +} + +/// Update the JSON configuration blob for a plugin. +pub async fn update_plugin_config(pool: &PgPool, plugin_id: Uuid, config_json: &str) -> Result<()> { + todo!() +} + +/// Update wipe-related settings for a plugin (wipe on map, wipe on BP). +pub async fn update_plugin_wipe_settings(pool: &PgPool, plugin_id: Uuid, wipe_on_map: bool, wipe_on_bp: bool) -> Result<()> { + todo!() +} + +/// Remove a plugin record. +pub async fn delete_plugin(pool: &PgPool, plugin_id: Uuid) -> Result<()> { + todo!() +} diff --git a/backend/src/db/servers.rs b/backend/src/db/servers.rs new file mode 100644 index 0000000..e6422f6 --- /dev/null +++ b/backend/src/db/servers.rs @@ -0,0 +1,52 @@ +use sqlx::PgPool; +use uuid::Uuid; +use anyhow::Result; + +// TODO: Define ServerConnection struct (id, license_id, name, host, rcon_port, rcon_password_encrypted, query_port, created_at) +// TODO: Define ServerConfig struct (id, server_id, seed, world_size, max_players, hostname, description, header_image_url, etc.) +// TODO: Define GameAdmin struct (id, server_id, steam_id, role, added_at) + +/// Register a new server connection (RCON credentials). +pub async fn create_server_connection(pool: &PgPool, license_id: Uuid, name: &str, host: &str, rcon_port: i32) -> Result { + todo!() +} + +/// Fetch a server connection by ID. +pub async fn get_server_connection(pool: &PgPool, server_id: Uuid) -> Result<()> { + todo!() +} + +/// Update server connection details. +pub async fn update_server_connection(pool: &PgPool, server_id: Uuid, name: Option<&str>, host: Option<&str>, rcon_port: Option) -> Result<()> { + todo!() +} + +/// Create the initial server configuration record. +pub async fn create_server_config(pool: &PgPool, server_id: Uuid) -> Result { + todo!() +} + +/// Fetch server configuration. +pub async fn get_server_config(pool: &PgPool, server_id: Uuid) -> Result<()> { + todo!() +} + +/// Update server configuration fields. +pub async fn update_server_config(pool: &PgPool, server_id: Uuid, seed: Option, world_size: Option, max_players: Option) -> Result<()> { + todo!() +} + +/// Get all game admins (moderators/owners) for a server. +pub async fn get_game_admins(pool: &PgPool, server_id: Uuid) -> Result<()> { + todo!() +} + +/// Add a game admin by Steam ID. +pub async fn create_game_admin(pool: &PgPool, server_id: Uuid, steam_id: &str, role: &str) -> Result { + todo!() +} + +/// Remove a game admin. +pub async fn delete_game_admin(pool: &PgPool, admin_id: Uuid) -> Result<()> { + todo!() +} diff --git a/backend/src/db/stats.rs b/backend/src/db/stats.rs new file mode 100644 index 0000000..7c03c38 --- /dev/null +++ b/backend/src/db/stats.rs @@ -0,0 +1,26 @@ +use sqlx::PgPool; +use uuid::Uuid; +use anyhow::Result; + +// TODO: Define ServerStats struct (id, server_id, player_count, fps, memory_usage, entities, timestamp) +// TODO: Define HourlyStats struct (id, server_id, hour, avg_players, avg_fps, avg_memory, peak_players) + +/// Insert a raw stats snapshot from the game server. +pub async fn insert_server_stats(pool: &PgPool, server_id: Uuid, player_count: i32, fps: f64, memory_usage: i64, entities: i32) -> Result { + todo!() +} + +/// Get the most recent stats snapshots for a server. +pub async fn get_recent_stats(pool: &PgPool, server_id: Uuid, limit: i64) -> Result<()> { + todo!() +} + +/// Get hourly aggregated stats for charting. +pub async fn get_hourly_stats(pool: &PgPool, server_id: Uuid, hours: i64) -> Result<()> { + todo!() +} + +/// Roll up raw stats into hourly aggregates (called by a scheduled job). +pub async fn aggregate_hourly_stats(pool: &PgPool, server_id: Uuid) -> Result<()> { + todo!() +} diff --git a/backend/src/db/store.rs b/backend/src/db/store.rs new file mode 100644 index 0000000..e534872 --- /dev/null +++ b/backend/src/db/store.rs @@ -0,0 +1,53 @@ +use sqlx::PgPool; +use uuid::Uuid; +use anyhow::Result; + +// TODO: Define WebstoreConfig struct (id, server_id, enabled, currency, stripe_key_encrypted, tebex_secret_encrypted, created_at, updated_at) +// TODO: Define Category struct (id, server_id, name, sort_order, created_at) +// TODO: Define Item struct (id, category_id, name, description, price, commands, image_url, is_active, created_at) +// TODO: Define Transaction struct (id, server_id, steam_id, item_id, amount, currency, status, provider, provider_ref, created_at, completed_at) + +/// Get the webstore configuration for a server. +pub async fn get_webstore_config(pool: &PgPool, server_id: Uuid) -> Result<()> { + todo!() +} + +/// Insert or update the webstore configuration. +pub async fn upsert_webstore_config(pool: &PgPool, server_id: Uuid, enabled: bool, currency: &str) -> Result<()> { + todo!() +} + +/// Get all item categories for a server's webstore. +pub async fn get_categories(pool: &PgPool, server_id: Uuid) -> Result<()> { + todo!() +} + +/// Create a new item category. +pub async fn create_category(pool: &PgPool, server_id: Uuid, name: &str, sort_order: i32) -> Result { + todo!() +} + +/// Get all items, optionally filtered by category. +pub async fn get_items(pool: &PgPool, server_id: Uuid, category_id: Option) -> Result<()> { + todo!() +} + +/// Create a new store item. +pub async fn create_item(pool: &PgPool, category_id: Uuid, name: &str, description: &str, price: i64, commands: &str) -> Result { + todo!() +} + +/// Create a new purchase transaction record. +pub async fn create_transaction(pool: &PgPool, server_id: Uuid, steam_id: &str, item_id: Uuid, amount: i64, currency: &str, provider: &str) -> Result { + todo!() +} + +/// Update transaction status (pending -> completed/failed/refunded). +pub async fn update_transaction_status(pool: &PgPool, transaction_id: Uuid, status: &str, provider_ref: Option<&str>) -> Result<()> { + todo!() +} + +/// Get transactions for a server with optional status filter. +pub async fn get_transactions(pool: &PgPool, server_id: Uuid, status: Option<&str>, limit: i64, offset: i64) -> Result<()> { + todo!() +} diff --git a/backend/src/db/teams.rs b/backend/src/db/teams.rs new file mode 100644 index 0000000..0d064b1 --- /dev/null +++ b/backend/src/db/teams.rs @@ -0,0 +1,46 @@ +use sqlx::PgPool; +use uuid::Uuid; +use anyhow::Result; + +// TODO: Define TeamMember struct (id, license_id, user_id, role_id, invited_at, accepted_at) +// TODO: Define Role struct (id, license_id, name, permissions jsonb, created_at) + +/// Get all team members for a license. +pub async fn get_team_members(pool: &PgPool, license_id: Uuid) -> Result<()> { + todo!() +} + +/// Add a user to a license team. +pub async fn create_team_member(pool: &PgPool, license_id: Uuid, user_id: Uuid, role_id: Uuid) -> Result { + todo!() +} + +/// Change a team member's role. +pub async fn update_team_member_role(pool: &PgPool, member_id: Uuid, role_id: Uuid) -> Result<()> { + todo!() +} + +/// Remove a team member from the license. +pub async fn remove_team_member(pool: &PgPool, member_id: Uuid) -> Result<()> { + todo!() +} + +/// Get all roles defined for a license. +pub async fn get_roles(pool: &PgPool, license_id: Uuid) -> Result<()> { + todo!() +} + +/// Create a custom role with a permissions set. +pub async fn create_role(pool: &PgPool, license_id: Uuid, name: &str, permissions: &str) -> Result { + todo!() +} + +/// Update a role's name or permissions. +pub async fn update_role(pool: &PgPool, role_id: Uuid, name: Option<&str>, permissions: Option<&str>) -> Result<()> { + todo!() +} + +/// Delete a custom role. +pub async fn delete_role(pool: &PgPool, role_id: Uuid) -> Result<()> { + todo!() +} diff --git a/backend/src/db/users.rs b/backend/src/db/users.rs new file mode 100644 index 0000000..1705bd7 --- /dev/null +++ b/backend/src/db/users.rs @@ -0,0 +1,30 @@ +use sqlx::PgPool; +use uuid::Uuid; +use anyhow::Result; + +// TODO: Define User struct (id, email, password_hash, display_name, avatar_url, created_at, updated_at, last_login_at) + +/// Create a new user record. +pub async fn create_user(pool: &PgPool, email: &str, password_hash: &str, display_name: &str) -> Result { + todo!() +} + +/// Fetch a user by their primary key. +pub async fn get_user_by_id(pool: &PgPool, user_id: Uuid) -> Result<()> { + todo!() +} + +/// Fetch a user by email address (for login lookups). +pub async fn get_user_by_email(pool: &PgPool, email: &str) -> Result<()> { + todo!() +} + +/// Update mutable user profile fields. +pub async fn update_user(pool: &PgPool, user_id: Uuid, display_name: Option<&str>, avatar_url: Option<&str>) -> Result<()> { + todo!() +} + +/// Bump the last_login_at timestamp for a user. +pub async fn update_last_login(pool: &PgPool, user_id: Uuid) -> Result<()> { + todo!() +} diff --git a/backend/src/db/wipes.rs b/backend/src/db/wipes.rs new file mode 100644 index 0000000..6d620ac --- /dev/null +++ b/backend/src/db/wipes.rs @@ -0,0 +1,42 @@ +use sqlx::PgPool; +use uuid::Uuid; +use anyhow::Result; + +// TODO: Define WipeProfile struct (id, server_id, name, wipe_type, commands, plugin_actions, created_at) +// TODO: Define WipeSchedule struct (id, profile_id, cron_expression, next_run_at, enabled) +// TODO: Define WipeHistory struct (id, profile_id, started_at, completed_at, status, log) + +/// Create a new wipe profile (template for a wipe operation). +pub async fn create_wipe_profile(pool: &PgPool, server_id: Uuid, name: &str, wipe_type: &str) -> Result { + todo!() +} + +/// Get all wipe profiles for a server. +pub async fn get_wipe_profiles(pool: &PgPool, server_id: Uuid) -> Result<()> { + todo!() +} + +/// Create a cron-based schedule for a wipe profile. +pub async fn create_wipe_schedule(pool: &PgPool, profile_id: Uuid, cron_expression: &str) -> Result { + todo!() +} + +/// Get all schedules for a wipe profile. +pub async fn get_wipe_schedules(pool: &PgPool, profile_id: Uuid) -> Result<()> { + todo!() +} + +/// Record the start of a wipe execution. +pub async fn create_wipe_history(pool: &PgPool, profile_id: Uuid) -> Result { + todo!() +} + +/// Update a wipe history entry with completion status and log output. +pub async fn update_wipe_history(pool: &PgPool, history_id: Uuid, status: &str, log: Option<&str>) -> Result<()> { + todo!() +} + +/// Get wipe history for a profile, ordered by most recent first. +pub async fn get_wipe_history(pool: &PgPool, profile_id: Uuid, limit: i64) -> Result<()> { + todo!() +} diff --git a/backend/src/middleware/auth.rs b/backend/src/middleware/auth.rs new file mode 100644 index 0000000..3d3cd39 --- /dev/null +++ b/backend/src/middleware/auth.rs @@ -0,0 +1,32 @@ +use axum::extract::FromRequestParts; +use http::request::Parts; +use uuid::Uuid; + +/// Extractor that validates the JWT from the Authorization header +/// and provides the authenticated user's identity to handlers. +#[derive(Debug, Clone)] +pub struct AuthUser { + pub user_id: Uuid, + pub email: String, + pub license_id: Option, + pub role: Option, +} + +// TODO: Implement JWT validation logic +// - Extract Bearer token from Authorization header +// - Decode and verify JWT signature using the app's secret +// - Check token expiration +// - Build AuthUser from claims +// - Return 401 Unauthorized on any failure + +#[axum::async_trait] +impl FromRequestParts for AuthUser +where + S: Send + Sync, +{ + type Rejection = http::StatusCode; + + async fn from_request_parts(_parts: &mut Parts, _state: &S) -> Result { + todo!() + } +} diff --git a/backend/src/middleware/license.rs b/backend/src/middleware/license.rs new file mode 100644 index 0000000..3b05a06 --- /dev/null +++ b/backend/src/middleware/license.rs @@ -0,0 +1,32 @@ +use axum::extract::FromRequestParts; +use http::request::Parts; +use uuid::Uuid; + +/// Extractor that ensures the request is associated with a valid, active license. +/// Should be used after AuthUser — it reads the license_id from the authenticated user's claims +/// and verifies the license exists, is active, and has the required modules enabled. +#[derive(Debug, Clone)] +pub struct ValidLicense { + pub license_id: Uuid, + pub status: String, + pub modules: Vec, +} + +// TODO: Implement license validation logic +// - Retrieve license_id from AuthUser claims (or from request extensions) +// - Query the database to confirm the license exists and is active +// - Check that the license hasn't expired +// - Populate the modules list for downstream permission checks +// - Return 403 Forbidden if the license is invalid, expired, or suspended + +#[axum::async_trait] +impl FromRequestParts for ValidLicense +where + S: Send + Sync, +{ + type Rejection = http::StatusCode; + + async fn from_request_parts(_parts: &mut Parts, _state: &S) -> Result { + todo!() + } +} diff --git a/backend/src/middleware/mod.rs b/backend/src/middleware/mod.rs new file mode 100644 index 0000000..22a98df --- /dev/null +++ b/backend/src/middleware/mod.rs @@ -0,0 +1,3 @@ +pub mod auth; +pub mod license; +pub mod rate_limit; diff --git a/backend/src/middleware/rate_limit.rs b/backend/src/middleware/rate_limit.rs new file mode 100644 index 0000000..e5c4cf2 --- /dev/null +++ b/backend/src/middleware/rate_limit.rs @@ -0,0 +1,17 @@ +// TODO: Implement tower-based rate limiting middleware +// +// Approach options: +// 1. Use tower::limit::RateLimitLayer for simple fixed-window rate limiting +// 2. Use governor crate for more sophisticated token-bucket / sliding-window limiting +// 3. Build a custom tower::Layer + tower::Service that reads client IP + license tier +// and applies different rate limits per tier +// +// Requirements: +// - Per-IP rate limiting for unauthenticated endpoints (login, register) +// - Per-license rate limiting for authenticated API endpoints +// - Higher limits for premium license tiers +// - Return 429 Too Many Requests with Retry-After header +// +// Example integration: +// let rate_limit = RateLimitLayer::new(100, Duration::from_secs(60)); +// Router::new().route("/api/...", get(handler)).layer(rate_limit)