mockforge-ui 0.3.135

Admin UI for MockForge - web-based interface for managing mock servers
//! Pillars: [DevX]
//!
//! # MockForge UI
//!
//! Web-based admin interface for managing mock servers.

pub mod audit;
pub mod auth;
pub mod handlers;
pub mod pillar_tracking_init;
pub mod rbac;
#[cfg(feature = "registry-admin")]
pub mod registry_admin;
pub mod routes;
// Templates module removed; static assets in `static/` are the single source of truth
pub mod models;
pub mod prometheus_client;
pub mod time_travel_handlers;

pub use models::{RequestLog, RouteInfo, ServerStatus, SystemInfo};
pub use routes::create_admin_router;

use std::net::SocketAddr;

/// Start the admin UI server
///
/// # Arguments
/// * `addr` - Address to bind the admin server to
/// * `http_server_addr` - HTTP server address
/// * `ws_server_addr` - WebSocket server address
/// * `grpc_server_addr` - gRPC server address
/// * `graphql_server_addr` - GraphQL server address
/// * `api_enabled` - Whether API endpoints are enabled
/// * `prometheus_url` - Prometheus metrics URL
/// * `chaos_api_state` - Optional chaos API state for hot-reload support
/// * `latency_injector` - Optional latency injector for hot-reload support
/// * `mockai` - Optional MockAI instance for hot-reload support
/// * `continuum_config` - Optional Reality Continuum configuration
/// * `virtual_clock` - Optional virtual clock for time-based progression
/// * `recorder` - Optional traffic recorder
/// * `federation` - Optional federation instance
/// * `vbr_engine` - Optional VBR engine
#[allow(clippy::too_many_arguments)]
pub async fn start_admin_server(
    addr: SocketAddr,
    http_server_addr: Option<SocketAddr>,
    ws_server_addr: Option<SocketAddr>,
    grpc_server_addr: Option<SocketAddr>,
    graphql_server_addr: Option<SocketAddr>,
    api_enabled: bool,
    prometheus_url: String,
    chaos_api_state: Option<std::sync::Arc<mockforge_chaos::api::ChaosApiState>>,
    latency_injector: Option<
        std::sync::Arc<tokio::sync::RwLock<mockforge_foundation::latency::LatencyInjector>>,
    >,
    mockai: Option<
        std::sync::Arc<tokio::sync::RwLock<mockforge_core::intelligent_behavior::MockAI>>,
    >,
    continuum_config: Option<mockforge_core::ContinuumConfig>,
    virtual_clock: Option<std::sync::Arc<mockforge_core::VirtualClock>>,
    recorder: Option<std::sync::Arc<mockforge_recorder::Recorder>>,
    federation: Option<std::sync::Arc<mockforge_federation::Federation>>,
    vbr_engine: Option<std::sync::Arc<mockforge_vbr::VbrEngine>>,
    resilience_api_state: Option<mockforge_chaos::resilience_api::ResilienceApiState>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    start_admin_server_notify(
        addr,
        http_server_addr,
        ws_server_addr,
        grpc_server_addr,
        graphql_server_addr,
        api_enabled,
        prometheus_url,
        chaos_api_state,
        latency_injector,
        mockai,
        continuum_config,
        virtual_clock,
        recorder,
        federation,
        vbr_engine,
        resilience_api_state,
        None,
    )
    .await
}

/// Like [`start_admin_server`], but sends the actual OS-assigned port through
/// `bound_port_tx` once the listener has bound. Required when `addr.port() == 0`
/// (ephemeral), because the OS picks the port and the caller needs to learn it.
#[allow(clippy::too_many_arguments)]
pub async fn start_admin_server_notify(
    addr: SocketAddr,
    http_server_addr: Option<SocketAddr>,
    ws_server_addr: Option<SocketAddr>,
    grpc_server_addr: Option<SocketAddr>,
    graphql_server_addr: Option<SocketAddr>,
    api_enabled: bool,
    prometheus_url: String,
    chaos_api_state: Option<std::sync::Arc<mockforge_chaos::api::ChaosApiState>>,
    latency_injector: Option<
        std::sync::Arc<tokio::sync::RwLock<mockforge_foundation::latency::LatencyInjector>>,
    >,
    mockai: Option<
        std::sync::Arc<tokio::sync::RwLock<mockforge_core::intelligent_behavior::MockAI>>,
    >,
    continuum_config: Option<mockforge_core::ContinuumConfig>,
    virtual_clock: Option<std::sync::Arc<mockforge_core::VirtualClock>>,
    recorder: Option<std::sync::Arc<mockforge_recorder::Recorder>>,
    federation: Option<std::sync::Arc<mockforge_federation::Federation>>,
    vbr_engine: Option<std::sync::Arc<mockforge_vbr::VbrEngine>>,
    resilience_api_state: Option<mockforge_chaos::resilience_api::ResilienceApiState>,
    bound_port_tx: Option<tokio::sync::oneshot::Sender<u16>>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let mut app = create_admin_router(
        http_server_addr,
        ws_server_addr,
        grpc_server_addr,
        graphql_server_addr,
        api_enabled,
        addr.port(),
        prometheus_url,
        chaos_api_state,
        latency_injector,
        mockai,
        continuum_config,
        virtual_clock,
        recorder,
        federation,
        vbr_engine,
        resilience_api_state,
    );

    // Optionally bring up the registry-admin (SQLite) sub-router if the
    // operator points us at a database via MOCKFORGE_REGISTRY_DB_URL.
    // When unset, the /api/admin/registry/* routes simply don't exist and
    // the rest of the admin UI behaves exactly as before.
    #[cfg(feature = "registry-admin")]
    if let Ok(db_url) = std::env::var("MOCKFORGE_REGISTRY_DB_URL") {
        match registry_admin::init_sqlite_registry_store(&db_url).await {
            Ok(store) => {
                // First-run admin bootstrap — if MOCKFORGE_ADMIN_{USERNAME,
                // EMAIL,PASSWORD} are set and no matching user exists yet,
                // seed a verified admin account so the operator can log in
                // without a manual curl dance.
                if let Err(e) = registry_admin::bootstrap_admin_user_from_env(&store).await {
                    tracing::error!("Registry admin bootstrap failed: {} — continuing startup", e);
                }

                let jwt_secret = std::env::var("MOCKFORGE_ADMIN_JWT_SECRET").unwrap_or_else(|_| {
                    tracing::warn!(
                        "MOCKFORGE_ADMIN_JWT_SECRET not set — using empty secret. \
                             Tokens issued by /api/admin/registry/auth/* will be \
                             trivially forgeable. Set the env var in production."
                    );
                    String::new()
                });
                let state = registry_admin::CoreAppState::with_jwt_secret(
                    std::sync::Arc::new(store),
                    jwt_secret,
                );
                app = app.merge(registry_admin::router(state));
                tracing::info!(
                    "Registry admin (SQLite) enabled at /api/admin/registry/* — db: {}",
                    db_url
                );
            }
            Err(e) => {
                tracing::error!(
                    "Failed to initialize registry admin SQLite store at '{}': {} — \
                     /api/admin/registry/* routes will not be available",
                    db_url,
                    e
                );
            }
        }
    }

    tracing::info!("Starting MockForge Admin UI on {}", addr);

    let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
        format!(
            "Failed to bind Admin UI server to port {}: {}\n\
             Hint: The port may already be in use. Try using a different port with --admin-port or check if another process is using this port with: lsof -i :{} or netstat -tulpn | grep {}",
            addr.port(), e, addr.port(), addr.port()
        )
    })?;

    if let Some(tx) = bound_port_tx {
        let actual_port = listener.local_addr().map(|a| a.port()).unwrap_or(addr.port());
        let _ = tx.send(actual_port);
    }

    axum::serve(listener, app).await?;

    Ok(())
}

// Generated by build.rs — embeds real UI assets when available, placeholders otherwise.
include!(concat!(env!("OUT_DIR"), "/ui_content.rs"));

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_admin_html() {
        let html = get_admin_html();
        assert!(!html.is_empty());
        assert!(html.contains("<!DOCTYPE html>") || html.contains("<html"));
    }

    #[test]
    fn test_get_admin_css() {
        let _css = get_admin_css();
        // Content may be a placeholder when UI is not built
    }

    #[test]
    fn test_get_admin_js() {
        let _js = get_admin_js();
        // Content may be a placeholder when UI is not built
    }
}