//! XRPC API server. //! //! Serves XRPC endpoints via axum routers built with `jacquard-axum`'s //! `IntoRouter` helper. mod admin; mod get_repo_status; mod hello; mod list_repos; mod list_repos_by_collection; use admin::{admin_page, admin_status}; use get_repo_status::get_repo_status; use list_repos::list_repos; use list_repos_by_collection::list_repos_by_collection; use std::net::SocketAddr; use jacquard_common::url::Host; use crate::com_atproto::sync::list_repos_by_collection::ListReposByCollectionRequest; use jacquard_api::com_atproto::sync::{ get_repo_status::GetRepoStatusRequest, list_repos::ListReposRequest, }; use jacquard_axum::IntoRouter; use crate::error::Result; use crate::http::ThrottledClient; use crate::storage::DbRef; use crate::sync::resync::dispatcher::DispatcherState; /// Config for the admin endpoints. Only constructed when `--admin-password` is /// set; when absent the `/admin/*` routes are not registered at all. #[derive(Clone)] pub struct AdminConfig { /// The relay/PDS host that lightrail is subscribed to and backfilling from. pub subscribe_host: Host, /// Required Bearer token for all admin endpoints. pub admin_password: String, } /// Build and serve the axum application on `addr`. /// /// Routes always registered: /// GET /xrpc/com.atproto.sync.getRepoStatus /// GET /xrpc/com.atproto.sync.listRepos /// GET /xrpc/com.atproto.sync.listReposByCollection /// /// Registered only when `admin_config` is `Some`: /// GET /admin/status pub async fn serve( addr: SocketAddr, db: DbRef, token: tokio_util::sync::CancellationToken, admin_config: Option, dispatcher_state: Option, client: Option, ) -> Result<()> { let base = GetRepoStatusRequest::into_router(get_repo_status) .merge(ListReposRequest::into_router(list_repos)) .merge(ListReposByCollectionRequest::into_router( list_repos_by_collection, )); let app = if let Some(config) = admin_config { let mut app = base .route("/admin", axum::routing::get(admin_page)) .route("/admin/status", axum::routing::get(admin_status)) .route("/", axum::routing::get(hello::hello)) .with_state(db) .layer(axum::Extension(config)); if let Some(ds) = dispatcher_state { app = app.layer(axum::Extension(ds)); } if let Some(c) = client { app = app.layer(axum::Extension(c)); } app } else { base.route("/", axum::routing::get(hello::hello)) .with_state(db) }; let listener = tokio::net::TcpListener::bind(addr).await?; axum::serve(listener, app) .with_graceful_shutdown(token.cancelled_owned()) .await?; Ok(()) }