//! GuruConnect Server - WebSocket Relay Server //! //! Handles connections from both agents and dashboard viewers, //! relaying video frames and input events between them. mod config; mod relay; mod session; mod auth; mod api; mod db; mod support_codes; pub mod proto { include!(concat!(env!("OUT_DIR"), "/guruconnect.rs")); } use anyhow::Result; use axum::{ Router, routing::{get, post}, extract::{Path, State, Json}, response::{Html, IntoResponse}, http::StatusCode, }; use std::net::SocketAddr; use tower_http::cors::{Any, CorsLayer}; use tower_http::trace::TraceLayer; use tower_http::services::ServeDir; use tracing::{info, Level}; use tracing_subscriber::FmtSubscriber; use serde::Deserialize; use support_codes::{SupportCodeManager, CreateCodeRequest, SupportCode, CodeValidation}; /// Application state #[derive(Clone)] pub struct AppState { sessions: session::SessionManager, support_codes: SupportCodeManager, } #[tokio::main] async fn main() -> Result<()> { // Initialize logging let _subscriber = FmtSubscriber::builder() .with_max_level(Level::INFO) .with_target(true) .init(); info!("GuruConnect Server v{}", env!("CARGO_PKG_VERSION")); // Load configuration let config = config::Config::load()?; // Use port 3002 for GuruConnect let listen_addr = std::env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0:3002".to_string()); info!("Loaded configuration, listening on {}", listen_addr); // Create application state let state = AppState { sessions: session::SessionManager::new(), support_codes: SupportCodeManager::new(), }; // Build router let app = Router::new() // Health check .route("/health", get(health)) // Portal API - Support codes .route("/api/codes", post(create_code)) .route("/api/codes", get(list_codes)) .route("/api/codes/:code/validate", get(validate_code)) .route("/api/codes/:code/cancel", post(cancel_code)) // WebSocket endpoints .route("/ws/agent", get(relay::agent_ws_handler)) .route("/ws/viewer", get(relay::viewer_ws_handler)) // REST API - Sessions .route("/api/sessions", get(list_sessions)) .route("/api/sessions/:id", get(get_session)) // HTML page routes (clean URLs) .route("/login", get(serve_login)) .route("/dashboard", get(serve_dashboard)) // State .with_state(state) // Serve static files for portal (fallback) .fallback_service(ServeDir::new("static").append_index_html_on_directories(true)) // Middleware .layer(TraceLayer::new_for_http()) .layer( CorsLayer::new() .allow_origin(Any) .allow_methods(Any) .allow_headers(Any), ); // Start server let addr: SocketAddr = listen_addr.parse()?; let listener = tokio::net::TcpListener::bind(addr).await?; info!("Server listening on {}", addr); axum::serve(listener, app).await?; Ok(()) } async fn health() -> &'static str { "OK" } // Support code API handlers async fn create_code( State(state): State, Json(request): Json, ) -> Json { let code = state.support_codes.create_code(request).await; info!("Created support code: {}", code.code); Json(code) } async fn list_codes( State(state): State, ) -> Json> { Json(state.support_codes.list_active_codes().await) } #[derive(Deserialize)] struct ValidateParams { code: String, } async fn validate_code( State(state): State, Path(code): Path, ) -> Json { Json(state.support_codes.validate_code(&code).await) } async fn cancel_code( State(state): State, Path(code): Path, ) -> impl IntoResponse { if state.support_codes.cancel_code(&code).await { (StatusCode::OK, "Code cancelled") } else { (StatusCode::BAD_REQUEST, "Cannot cancel code") } } // Session API handlers (updated to use AppState) async fn list_sessions( State(state): State, ) -> Json> { let sessions = state.sessions.list_sessions().await; Json(sessions.into_iter().map(api::SessionInfo::from).collect()) } async fn get_session( State(state): State, Path(id): Path, ) -> Result, (StatusCode, &'static str)> { let session_id = uuid::Uuid::parse_str(&id) .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid session ID"))?; let session = state.sessions.get_session(session_id).await .ok_or((StatusCode::NOT_FOUND, "Session not found"))?; Ok(Json(api::SessionInfo::from(session))) } // Static page handlers async fn serve_login() -> impl IntoResponse { match tokio::fs::read_to_string("static/login.html").await { Ok(content) => Html(content).into_response(), Err(_) => (StatusCode::NOT_FOUND, "Page not found").into_response(), } } async fn serve_dashboard() -> impl IntoResponse { match tokio::fs::read_to_string("static/dashboard.html").await { Ok(content) => Html(content).into_response(), Err(_) => (StatusCode::NOT_FOUND, "Page not found").into_response(), } }