AkurAI Build
Menu

Public repository / 228b5716ba0b7bcc87cce261ea6df891133a35bb

AkurAI-CRM / crates/crm-cli/src/cmd_serve.rs

Latest change 3ca078e63808e39f59571f8562e9a829f261ae13 - Restore CSRF guard, admin-gated merge, IdP logout and login redirects by Ólafur Búi Ólafsson

use akurai_http::{Method, Request, Response, Server};
use crm_api::{build_router_with_mcp, r, CrmState, Route};
use crm_mcp::{McpConfig, McpServer};
use std::io;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

pub struct Config {
    pub host: String,
    pub port: u16,
    pub dir: PathBuf,
    pub db: String,
}

pub fn run(cfg: Config) -> io::Result<()> {
    crm_api::IdpConfig::from_env().map_err(io::Error::other)?;
    let state = CrmState::new(&cfg.db, cfg.dir.clone()).map_err(io::Error::other)?;
    let state = Arc::new(Mutex::new(state));

    let mcp = Arc::new(
        McpServer::new(McpConfig {
            db_path: cfg.db.clone(),
        })
        .map_err(io::Error::other)?,
    );
    let mut routes = build_router_with_mcp(state, Arc::clone(&mcp).http_handler());
    for path in [
        "/.well-known/oauth-protected-resource",
        "/.well-known/oauth-protected-resource/mcp",
    ] {
        routes.push(r("GET", path, Arc::clone(&mcp).metadata_handler()));
    }

    let addr = format!("{}:{}", cfg.host, cfg.port);
    let server = Server::bind(&addr)?;
    let local = server.local_addr()?;

    println!("AkurAI-CRM");
    println!("  database: {}", cfg.db);
    println!("  frontend: {}", cfg.dir.display());
    println!("  → http://{local}");
    tracing::info!(listen = %local, db = %cfg.db, "akurai-crm serving");

    let public_url = crm_api::auth::idp_client::public_url_from_env();
    server.run(move |req: &Request| -> Response {
        match crm_api::auth::csrf::check(req, &public_url) {
            Ok(()) => dispatch(&routes, req),
            Err(rejection) => rejection,
        }
    })
}

fn dispatch(routes: &[Route], req: &Request) -> Response {
    let mut best: Option<(Response, (usize, usize, usize))> = None;

    for route in routes {
        if !method_matches(route.method, &req.method) {
            continue;
        }
        let pattern = akurai_router::Pattern::parse(route.pattern);
        if pattern.match_path(&req.path).is_some() {
            let score = pattern.specificity();
            if best.as_ref().is_none_or(|(_, b)| score > *b) {
                let resp = (route.handler)(req);
                best = Some((resp, score));
            }
        }
    }

    match best {
        Some((resp, _)) => resp,
        None => Response::not_found(),
    }
}

fn method_matches(pattern: &str, method: &Method) -> bool {
    match method {
        Method::Get => pattern.eq_ignore_ascii_case("GET"),
        Method::Post => pattern.eq_ignore_ascii_case("POST"),
        Method::Put => pattern.eq_ignore_ascii_case("PUT"),
        Method::Patch => pattern.eq_ignore_ascii_case("PATCH"),
        Method::Delete => pattern.eq_ignore_ascii_case("DELETE"),
        Method::Head => pattern.eq_ignore_ascii_case("HEAD"),
        Method::Options => pattern.eq_ignore_ascii_case("OPTIONS"),
        Method::Other(s) => pattern.eq_ignore_ascii_case(s),
    }
}