use dioxus::prelude::*; use axum::routing::get; use axum::{middleware, Extension}; use time::Duration; use tower_sessions::{cookie::Key, MemoryStore, SessionManagerLayer}; use crate::infrastructure::{ auth_callback, auth_login, config::{KeycloakConfig, LlmProvidersConfig, ServiceUrls, SmtpConfig, StripeConfig}, database::Database, logout, require_auth, server_state::{ServerState, ServerStateInner}, PendingOAuthStore, }; /// Start the Axum server with Dioxus fullstack, session management, /// MongoDB, and Keycloak OAuth routes. /// /// Loads all configuration from environment variables once, connects /// to MongoDB, and builds a [`ServerState`] shared across every request. /// /// # Errors /// /// Returns `Error` if the tokio runtime, config loading, DB connection, /// or TCP listener fails. pub fn server_start(app: fn() -> Element) -> Result<(), super::Error> { tokio::runtime::Runtime::new()?.block_on(async move { // Load .env once at startup. dotenvy::dotenv().ok(); // ---- Load and leak config structs for 'static lifetime ---- let keycloak: &'static KeycloakConfig = Box::leak(Box::new(KeycloakConfig::from_env()?)); let smtp: &'static SmtpConfig = Box::leak(Box::new(SmtpConfig::from_env()?)); let services: &'static ServiceUrls = Box::leak(Box::new(ServiceUrls::from_env()?)); let stripe: &'static StripeConfig = Box::leak(Box::new(StripeConfig::from_env()?)); let llm_providers: &'static LlmProvidersConfig = Box::leak(Box::new(LlmProvidersConfig::from_env()?)); tracing::info!("Configuration loaded"); // ---- Connect to MongoDB ---- let mongo_uri = std::env::var("MONGODB_URI").unwrap_or_else(|_| "mongodb://localhost:27017".into()); let mongo_db = std::env::var("MONGODB_DATABASE").unwrap_or_else(|_| "certifai".into()); let db = Database::connect(&mongo_uri, &mongo_db).await?; tracing::info!("Connected to MongoDB (database: {mongo_db})"); // ---- Build ServerState ---- let server_state: ServerState = ServerStateInner { db, keycloak, smtp, services, stripe, llm_providers, } .into(); // ---- Session layer ---- let key = Key::generate(); let store = MemoryStore::default(); let session = SessionManagerLayer::new(store) .with_secure(false) // Lax is required so the browser sends the session cookie // on the redirect back from Keycloak (cross-origin GET). .with_same_site(tower_sessions::cookie::SameSite::Lax) .with_expiry(tower_sessions::Expiry::OnInactivity(Duration::hours(24))) .with_signed(key); // ---- Build router ---- let addr = dioxus_cli_config::fullstack_address_or_localhost(); let listener = tokio::net::TcpListener::bind(addr).await?; // Layers wrap in reverse order: session (outermost) -> auth // middleware -> extensions -> route handlers. The session layer // must be outermost so the `Session` extractor is available to // the auth middleware, which gates all `/api/` server function // routes (except `check-auth`). let router = axum::Router::new() .route("/auth", get(auth_login)) .route("/auth/callback", get(auth_callback)) .route("/logout", get(logout)) .serve_dioxus_application(ServeConfig::new(), app) .layer(Extension(PendingOAuthStore::default())) .layer(Extension(server_state)) .layer(middleware::from_fn(require_auth)) .layer(session); tracing::info!("Serving at {addr}"); axum::serve(listener, router.into_make_service()).await?; Ok(()) }) }