lmrc-cli 0.3.16

CLI tool for scaffolding LMRC Stack infrastructure projects
Documentation
//! Gateway - Unified API Gateway with Hexagonal Architecture
//!
//! ## Architecture
//!
//! **Hexagonal (Ports & Adapters)**:
//! - **Inbound Ports**: HTTP handlers receive requests
//! - **Core Domain**: Routing logic, authentication, proxying
//! - **Outbound Ports**: Authentication providers, proxy clients, database access
//! - **Adapters**: Concrete implementations for different auth methods, backends
//!
//! This design allows:
//! - Easy addition of new subdomains (just add route config)
//! - Easy addition of new auth methods (implement AuthProvider trait)
//! - Easy backend integration (implement ProxyTarget trait)

mod config;
mod routing;
mod auth;
mod proxy;
mod middleware;
mod state;
mod error;

use axum::{
    middleware as axum_middleware,
    routing::{any, get, post},
    Router,
};
use lmrc_http_common::middleware::cors_with_origins;
use sea_orm::Database;
use state::AppState;
use std::net::SocketAddr;
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Initialize tracing
    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "gateway=debug,tower_http=debug".into()),
        )
        .with(tracing_subscriber::fmt::layer())
        .init();

    // Load environment variables
    dotenvy::dotenv().ok();

    // Load configuration
    let config = config::Config::from_env()?;

    tracing::info!("Starting gateway on {}:{}", config.server.host, config.server.port);
    tracing::info!("Infrastructure database: connected");
    tracing::info!("User routes configured: {}", config.routing.user_routes.len());

    // Connect to infrastructure database (for infra auth)
    tracing::info!("Connecting to infrastructure database...");
    let infra_db = Database::connect(&config.database.infra_url).await?;
    tracing::info!("Infrastructure database connected");

    // Create application state
    let state = AppState::new(config.clone(), infra_db);

    // Build router
    let app = create_router(state);

    // Start server
    let addr = SocketAddr::from(([0, 0, 0, 0], config.server.port));
    tracing::info!("Listening on {}", addr);

    let listener = tokio::net::TcpListener::bind(addr).await?;
    axum::serve(listener, app).await?;

    Ok(())
}

/// Create the application router with hexagonal architecture
fn create_router(state: AppState) -> Router {
    // Infrastructure routes (infra.* subdomain)
    let infra_routes = Router::new()
        // Auth endpoints (handled locally by gateway)
        .route("/auth/login", post(auth::handlers::infra_login))
        .route("/auth/logout", post(auth::handlers::infra_logout))
        .route("/auth/me", get(auth::handlers::infra_me))
        // Protected routes (require infra auth)
        .route("/api/*path", any(proxy::handlers::proxy_to_infra_api))
        .layer(axum_middleware::from_fn_with_state(
            state.clone(),
            middleware::infra_auth::infra_auth_middleware,
        ))
        // Frontend (static, no auth required)
        .route("/*path", any(proxy::handlers::proxy_to_infra_front));

    // User service routes (app.* subdomains)
    // These are dynamically routed based on subdomain
    let user_routes = Router::new()
        .route("/*path", any(routing::handlers::route_user_service));

    // Root router with subdomain detection
    Router::new()
        .nest("/", Router::new()
            .fallback(routing::handlers::subdomain_router))
        .layer(TraceLayer::new_for_http())
        .layer(axum_middleware::from_fn(middleware::subdomain::extract_subdomain))
        .layer(cors_with_origins(state.config.server.cors_origins.clone()))
        .with_state(state)
}