pub mod cleanup; pub mod openapi; pub mod search; pub mod spa; use axum::Router; use export::AppState; use export::middleware::AppTraceLayer; use session_layer::{SessionLayer, store}; use tower_http::limit::RequestBodyLimitLayer; use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer}; use tracing::Level; const MAX_REQUEST_BODY_BYTES: usize = 2 * 1024 * 1024; fn build_cors_layer(web: &config::web::WebConfig) -> tower_http::cors::CorsLayer { use axum::http::{HeaderName, header}; use tower_http::cors::{AllowOrigin, CorsLayer}; if web.cors_allow_origins.is_empty() { return CorsLayer::new(); } let origins: Vec = web .cors_allow_origins .iter() .filter_map(|o| o.parse().ok()) .collect(); CorsLayer::new() .allow_origin(AllowOrigin::list(origins)) .allow_credentials(web.cors_allow_credentials) .allow_methods([ axum::http::Method::GET, axum::http::Method::POST, axum::http::Method::PUT, axum::http::Method::PATCH, axum::http::Method::DELETE, axum::http::Method::OPTIONS, ]) .allow_headers([ header::CONTENT_TYPE, header::AUTHORIZATION, HeaderName::from_static("x-request-id"), ]) } pub async fn router(state: AppState) -> Result { let store = store::connect(state.redis.clone(), &state.config.session).await?; let cors = build_cors_layer(&state.config.web); let auth = auth::services::AuthService::new(state.clone()); let agents = agents::services::service::AgentsService::new(state.clone()); let chat = chat::services::service::ChatService::new(state.clone()); let issues = issues::services::service::IssueService::new(state.clone()); let space = space::services::service::SpaceService::new(state.clone()); let user = user::services::service::UserService::new(state.clone()); let git = git::service::GitService::new(state.clone()); let ci = ci::CiService::new(state.clone()); let pulls = pulls::services::service::PullService::new(state.clone()); let search = ::search::SearchService::from_parts(state.database.clone(), state.config.search.clone()) .expect("validated search client configuration"); let import = ::import::ImportService::new(state.clone()); let health_state = state; // 清理接口需要跨模块调用(space/issues/agents/git),这里的服务都还没被 // 其他 router 的 with_state 消费掉,先构造再合并。 let cleanup_router = cleanup::router(space.clone(), issues.clone(), agents.clone(), git.clone()); let public: Router = Router::new() .merge(auth::routes::router(auth.clone()).with_state(auth.clone())) .merge(agents::routes::router(agents.clone()).with_state(agents)) .merge(chat::routes::router(chat.clone(), space.clone()).with_state(chat)) .merge(issues::routes::router(issues.clone()).with_state(issues)) .merge(space::routes::router(space.clone()).with_state(space)) .merge(user::routes::router(user.clone()).with_state(user)) .merge(git::routes::router(git.clone()).with_state(git.clone())) .merge(pulls::routes::router().with_state(pulls)) .merge(crate::search::router().with_state(search.clone())) .merge(::import::routes::router(import.clone()).with_state(import.clone())) .merge(ci::routes::router().with_state(ci.clone())) .merge(cleanup_router); let internal: Router = Router::new() .merge(git::routes::internal_router(git.clone()).with_state(git)) .merge(crate::search::internal_router().with_state(search)) .merge(::import::routes::internal_router().with_state(import)) .route_layer(axum::middleware::from_fn_with_state( auth.clone(), auth::share::guard::admin_guard, )); let router: Router = Router::new() .nest("/api/v1", public) .nest("/api/v1/internal", internal) .nest("/pages", auth::routes::pages_router(auth.clone())) .merge(export::health::router(health_state)) .fallback(spa::fallback) .layer(cors) .layer(SessionLayer::new(store)) .layer(RequestBodyLimitLayer::new(MAX_REQUEST_BODY_BYTES)) .layer( TraceLayer::new_for_http() .make_span_with( DefaultMakeSpan::new() .level(Level::INFO) .include_headers(false), ) .on_response(DefaultOnResponse::new().level(Level::INFO)), ) .layer(export::telemetry::metrics_layer()) .layer(AppTraceLayer::new()); Ok(router.merge(ci::actions_runtime::router().with_state(ci))) }