mod acl;
mod admin;
#[cfg(feature = "admin-ui")]
mod admin_ui;
mod audit;
mod auth;
mod community;
mod config;
pub(crate) mod did_log;
mod endorsement_types;
mod endorsements;
mod health;
pub(crate) mod install;
pub(crate) mod join_requests;
pub(crate) mod members;
pub(crate) mod policies;
pub mod recognise;
mod relationships;
pub(crate) mod status_lists;
#[cfg(feature = "website")]
mod website;
use std::sync::Arc;
use axum::Router;
use axum::extract::DefaultBodyLimit;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::routing::{any, delete, get, post};
use tower_governor::GovernorLayer;
use tower_governor::governor::GovernorConfigBuilder;
use tower_governor::key_extractor::SmartIpKeyExtractor;
use vti_common::trust_task::{TrustTask, TrustTaskRouter};
use crate::config::RoutingConfig;
use crate::server::AppState;
pub const MAX_BODY_SIZE: usize = 1024 * 1024;
pub const UNAUTH_BODY_SIZE: usize = 64 * 1024;
pub fn router() -> Router<AppState> {
#[cfg(feature = "website")]
{
router_with(&RoutingConfig::default(), None)
}
#[cfg(not(feature = "website"))]
{
router_with(&RoutingConfig::default())
}
}
#[cfg(feature = "website")]
pub fn router_with(
routing: &RoutingConfig,
website_state: Option<crate::website::WebsiteState>,
) -> Router<AppState> {
router_with_inner(routing, website_state, false)
}
#[cfg(feature = "website")]
pub fn router_with_xff(
routing: &RoutingConfig,
website_state: Option<crate::website::WebsiteState>,
trust_xff: bool,
) -> Router<AppState> {
router_with_inner(routing, website_state, trust_xff)
}
#[cfg(not(feature = "website"))]
pub fn router_with(routing: &RoutingConfig) -> Router<AppState> {
router_with_inner(routing, false)
}
#[cfg(not(feature = "website"))]
pub fn router_with_xff(routing: &RoutingConfig, trust_xff: bool) -> Router<AppState> {
router_with_inner(routing, trust_xff)
}
#[cfg(not(feature = "website"))]
fn router_with_inner(routing: &RoutingConfig, trust_xff: bool) -> Router<AppState> {
let api_chain = build_api_chain(routing, trust_xff);
assemble(routing, api_chain)
}
#[cfg(feature = "website")]
fn router_with_inner(
routing: &RoutingConfig,
website_state: Option<crate::website::WebsiteState>,
trust_xff: bool,
) -> Router<AppState> {
let api_chain = build_api_chain(routing, trust_xff);
assemble_with_website(routing, api_chain, website_state)
}
fn build_api_chain(_routing: &RoutingConfig, trust_xff: bool) -> Router<AppState> {
let auth_sessions_manage = TrustTask::new("https://trusttasks.org/spec/auth/sessions/list/0.1")
.expect("static Trust-Task URL");
let auth_sessions_revoke =
TrustTask::new("https://trusttasks.org/spec/auth/revoke-session/0.1")
.expect("static Trust-Task URL");
let auth_whoami = TrustTask::new("https://trusttasks.org/spec/auth/whoami/0.1")
.expect("static Trust-Task URL");
let auth_sign_out = TrustTask::new("https://trusttasks.org/spec/auth/revoke-session/0.1")
.expect("static Trust-Task URL");
let audit_list = TrustTask::new("https://trusttasks.org/openvtc/vtc/audit/list/1.0")
.expect("static Trust-Task URL");
let config_manage =
TrustTask::new("https://trusttasks.org/openvtc/vtc/config/legacy/manage/1.0")
.expect("static Trust-Task URL");
let acl_manage = TrustTask::new("https://trusttasks.org/openvtc/vtc/acl/legacy/manage/1.0")
.expect("static Trust-Task URL");
let acl_entry = TrustTask::new("https://trusttasks.org/openvtc/vtc/acl/legacy/entry/1.0")
.expect("static Trust-Task URL");
let community_profile =
TrustTask::new("https://trusttasks.org/openvtc/vtc/community/profile/manage/1.0")
.expect("static Trust-Task URL");
let admin_config = TrustTask::new("https://trusttasks.org/openvtc/vtc/admin/config/manage/1.0")
.expect("static Trust-Task URL");
let admin_config_reload =
TrustTask::new("https://trusttasks.org/openvtc/vtc/admin/config/reload/1.0")
.expect("static Trust-Task URL");
let admin_config_restart =
TrustTask::new("https://trusttasks.org/openvtc/vtc/admin/config/restart/1.0")
.expect("static Trust-Task URL");
let admin_config_export =
TrustTask::new("https://trusttasks.org/openvtc/vtc/admin/config/export/1.0")
.expect("static Trust-Task URL");
let admin_config_import =
TrustTask::new("https://trusttasks.org/openvtc/vtc/admin/config/import/1.0")
.expect("static Trust-Task URL");
let admin_bootstrap = TrustTask::new("https://trusttasks.org/openvtc/vtc/admin/bootstrap/1.0")
.expect("static Trust-Task URL");
let admin_passkeys_list =
TrustTask::new("https://trusttasks.org/openvtc/vtc/admin/passkeys/list/1.0")
.expect("static Trust-Task URL");
let admin_passkeys_register =
TrustTask::new("https://trusttasks.org/openvtc/vtc/admin/passkeys/register/1.0")
.expect("static Trust-Task URL");
let admin_passkeys_revoke =
TrustTask::new("https://trusttasks.org/openvtc/vtc/admin/passkeys/revoke/1.0")
.expect("static Trust-Task URL");
let admin_invites_manage =
TrustTask::new("https://trusttasks.org/openvtc/vtc/admin/invites/manage/1.0")
.expect("static Trust-Task URL");
let admin_invites_revoke =
TrustTask::new("https://trusttasks.org/openvtc/vtc/admin/invites/revoke/1.0")
.expect("static Trust-Task URL");
let members_list = TrustTask::new("https://trusttasks.org/openvtc/vtc/members/list/1.0")
.expect("static Trust-Task URL");
let members_show = TrustTask::new("https://trusttasks.org/openvtc/vtc/members/show/1.0")
.expect("static Trust-Task URL");
let members_promote =
TrustTask::new("https://trusttasks.org/openvtc/vtc/members/promote-to-admin/1.0")
.expect("static Trust-Task URL");
let members_self_remove =
TrustTask::new("https://trusttasks.org/openvtc/vtc/members/self-remove/1.0")
.expect("static Trust-Task URL");
let join_submit = TrustTask::new("https://trusttasks.org/openvtc/vtc/join-requests/submit/1.0")
.expect("static Trust-Task URL");
let join_show = TrustTask::new("https://trusttasks.org/openvtc/vtc/join-requests/show/1.0")
.expect("static Trust-Task URL");
let join_approve =
TrustTask::new("https://trusttasks.org/openvtc/vtc/join-requests/approve/1.0")
.expect("static Trust-Task URL");
let join_reject = TrustTask::new("https://trusttasks.org/openvtc/vtc/join-requests/reject/1.0")
.expect("static Trust-Task URL");
let policies_upload = TrustTask::new("https://trusttasks.org/openvtc/vtc/policies/upload/1.0")
.expect("static Trust-Task URL");
let policies_activate =
TrustTask::new("https://trusttasks.org/openvtc/vtc/policies/activate/1.0")
.expect("static Trust-Task URL");
let policies_test = TrustTask::new("https://trusttasks.org/openvtc/vtc/policies/test/1.0")
.expect("static Trust-Task URL");
let members_renew = TrustTask::new("https://trusttasks.org/openvtc/vtc/members/renew/1.0")
.expect("static Trust-Task URL");
let members_rotate_challenge =
TrustTask::new("https://trusttasks.org/openvtc/vtc/members/rotate-challenge/1.0")
.expect("static Trust-Task URL");
let members_rotate = TrustTask::new("https://trusttasks.org/openvtc/vtc/members/rotate/1.0")
.expect("static Trust-Task URL");
let members_personhood_challenge =
TrustTask::new("https://trusttasks.org/openvtc/vtc/members/personhood/challenge/1.0")
.expect("static Trust-Task URL");
let members_personhood_assert =
TrustTask::new("https://trusttasks.org/openvtc/vtc/members/personhood/assert/1.0")
.expect("static Trust-Task URL");
let _members_personhood_revoke =
TrustTask::new("https://trusttasks.org/openvtc/vtc/members/personhood/revoke/1.0")
.expect("static Trust-Task URL");
let relationships_publish =
TrustTask::new("https://trusttasks.org/openvtc/vtc/relationships/publish/1.0")
.expect("static Trust-Task URL");
let relationships_list =
TrustTask::new("https://trusttasks.org/openvtc/vtc/relationships/list/1.0")
.expect("static Trust-Task URL");
let relationships_revoke =
TrustTask::new("https://trusttasks.org/openvtc/vtc/relationships/revoke/1.0")
.expect("static Trust-Task URL");
let endorsement_types_register =
TrustTask::new("https://trusttasks.org/openvtc/vtc/endorsement-types/register/1.0")
.expect("static Trust-Task URL");
let _endorsement_types_list =
TrustTask::new("https://trusttasks.org/openvtc/vtc/endorsement-types/list/1.0")
.expect("static Trust-Task URL");
let endorsement_types_delete =
TrustTask::new("https://trusttasks.org/openvtc/vtc/endorsement-types/delete/1.0")
.expect("static Trust-Task URL");
let endorsements_issue =
TrustTask::new("https://trusttasks.org/openvtc/vtc/credentials/endorsements/issue/1.0")
.expect("static Trust-Task URL");
let _endorsements_list =
TrustTask::new("https://trusttasks.org/openvtc/vtc/credentials/endorsements/list/1.0")
.expect("static Trust-Task URL");
let _endorsements_show =
TrustTask::new("https://trusttasks.org/openvtc/vtc/credentials/endorsements/show/1.0")
.expect("static Trust-Task URL");
let _endorsements_revoke =
TrustTask::new("https://trusttasks.org/openvtc/vtc/credentials/endorsements/revoke/1.0")
.expect("static Trust-Task URL");
let endorsements_show_revoke =
TrustTask::new("https://trusttasks.org/openvtc/vtc/credentials/endorsements/show/1.0")
.expect("static Trust-Task URL");
let health_diagnostics =
TrustTask::new("https://trusttasks.org/openvtc/vtc/health/diagnostics/1.0")
.expect("static Trust-Task URL");
let api = TrustTaskRouter::<AppState>::new()
.route_with_task(
"/health/diagnostics",
get(health::diagnostics),
health_diagnostics,
)
.route_exempt("/{scid}/did.jsonl", get(did_log::did_log))
.route_exempt("/status-lists/{purpose}", get(status_lists::show))
.route_with_task(
"/auth/sessions",
get(auth::session_list).delete(auth::revoke_sessions_by_did),
auth_sessions_manage,
)
.route_with_task(
"/auth/sessions/{session_id}",
delete(auth::revoke_session),
auth_sessions_revoke,
)
.route_with_task("/auth/whoami", get(auth::whoami), auth_whoami)
.route_with_task("/auth/sign-out", post(auth::sign_out), auth_sign_out)
.route_with_task("/audit", get(audit::list_audit), audit_list)
.route_with_task(
"/config",
get(config::get_config).patch(config::update_config),
config_manage,
)
.route_with_task("/acl", get(acl::list_acl).post(acl::create_acl), acl_manage)
.route_with_task(
"/acl/{did}",
get(acl::get_acl)
.patch(acl::update_acl)
.delete(acl::delete_acl),
acl_entry,
)
.route_with_task(
"/community/profile",
get(community::profile::get_profile).put(community::profile::put_profile),
community_profile,
)
.route_exempt(
"/community/public-profile",
get(community::profile::get_public_profile),
)
.route_with_task(
"/admin/config",
get(admin::config::get_config).patch(admin::config::patch_config),
admin_config,
)
.route_with_task(
"/admin/config/reload",
post(admin::config::reload_config),
admin_config_reload,
)
.route_with_task(
"/admin/config/restart",
post(admin::config::restart_config),
admin_config_restart,
)
.route_with_task(
"/admin/config/export",
post(admin::config::export_config),
admin_config_export,
)
.route_with_task(
"/admin/config/import",
post(admin::config::import_config),
admin_config_import,
)
.route_with_task(
"/admin/bootstrap",
post(admin::bootstrap::bootstrap),
admin_bootstrap,
)
.route_with_task(
"/admin/passkeys",
get(admin::passkeys::list),
admin_passkeys_list,
)
.route_with_task(
"/admin/passkeys/register/start",
post(admin::passkeys::register_start),
admin_passkeys_register.clone(),
)
.route_with_task(
"/admin/passkeys/register/finish",
post(admin::passkeys::register_finish),
admin_passkeys_register,
)
.route_with_task(
"/admin/passkeys/revoke/start",
post(admin::passkeys::revoke_start),
admin_passkeys_revoke.clone(),
)
.route_with_task(
"/admin/passkeys/revoke/finish",
post(admin::passkeys::revoke_finish),
admin_passkeys_revoke,
)
.route_with_task(
"/admin/invites",
get(admin::invites::list_invites).post(admin::invites::create_invite),
admin_invites_manage,
)
.route_with_task(
"/admin/invites/{jti}",
axum::routing::delete(admin::invites::revoke_invite),
admin_invites_revoke,
)
.route_with_task("/members", get(members::read::list_members), members_list)
.route_with_task(
"/members/me",
axum::routing::delete(members::remove::self_remove),
members_self_remove,
)
.route_with_task(
"/members/me/renew",
post(members::renew::renew),
members_renew,
)
.route_with_task(
"/members/me/rotate/challenge",
post(members::rotate::challenge),
members_rotate_challenge,
)
.route_with_task(
"/members/me/rotate",
post(members::rotate::rotate),
members_rotate,
)
.route_with_task(
"/members/{did}/personhood/challenge",
post(members::personhood::challenge),
members_personhood_challenge,
)
.route_with_task(
"/members/{did}/personhood",
post(members::personhood::assert).delete(members::personhood::revoke),
members_personhood_assert,
)
.route_with_task(
"/members/{did}/relationships",
get(members::relationships::list),
relationships_list,
)
.route_with_task(
"/relationships",
post(relationships::publish),
relationships_publish,
)
.route_with_task(
"/relationships/{id}",
delete(relationships::revoke),
relationships_revoke,
)
.route_with_task(
"/endorsement-types",
post(endorsement_types::register).get(endorsement_types::list),
endorsement_types_register,
)
.route_with_task(
"/endorsement-types/{type_uri}",
delete(endorsement_types::delete),
endorsement_types_delete,
)
.route_with_task(
"/credentials/endorsements",
post(endorsements::issue).get(endorsements::list),
endorsements_issue,
)
.route_with_task(
"/credentials/endorsements/{id}",
axum::routing::get(endorsements::show).delete(endorsements::revoke),
endorsements_show_revoke,
)
.route_with_task(
"/members/{did}",
get(members::read::show_member)
.patch(members::update::update_member)
.delete(members::remove::admin_remove),
members_show,
)
.route_with_task(
"/members/{did}/promote-to-admin/start",
post(members::promote::promote_start),
members_promote.clone(),
)
.route_with_task(
"/members/{did}/promote-to-admin/finish",
post(members::promote::promote_finish),
members_promote,
)
.route_with_task(
"/join-requests",
post(join_requests::submit::submit).get(join_requests::read::list_join_requests),
join_submit,
)
.route_with_task(
"/join-requests/{id}",
get(join_requests::read::show_join_request),
join_show,
)
.route_with_task(
"/join-requests/{id}/approve",
post(join_requests::decide::approve),
join_approve,
)
.route_with_task(
"/join-requests/{id}/reject",
post(join_requests::decide::reject),
join_reject,
)
.route_with_task(
"/policies",
get(policies::read::list_policies).post(policies::admin::upload),
policies_upload.clone(),
)
.route_with_task(
"/policies/{id}",
get(policies::read::show_policy),
policies_upload.clone(),
)
.route_with_task(
"/policies/{id}/activate",
post(policies::admin::activate),
policies_activate,
)
.route_with_task(
"/policies/{id}/test",
post(policies::admin::test),
policies_test,
);
#[cfg(feature = "website")]
let api = {
use axum::extract::DefaultBodyLimit;
let website_files_list =
TrustTask::new("https://trusttasks.org/openvtc/vtc/website/files/list/1.0")
.expect("static Trust-Task URL");
let website_files_show =
TrustTask::new("https://trusttasks.org/openvtc/vtc/website/files/show/1.0")
.expect("static Trust-Task URL");
let _website_files_write =
TrustTask::new("https://trusttasks.org/openvtc/vtc/website/files/write/1.0")
.expect("static Trust-Task URL");
let _website_files_delete =
TrustTask::new("https://trusttasks.org/openvtc/vtc/website/files/delete/1.0")
.expect("static Trust-Task URL");
let website_deploy =
TrustTask::new("https://trusttasks.org/openvtc/vtc/website/deploy/1.0")
.expect("static Trust-Task URL");
let website_gens_list =
TrustTask::new("https://trusttasks.org/openvtc/vtc/website/generations/list/1.0")
.expect("static Trust-Task URL");
let website_rollback =
TrustTask::new("https://trusttasks.org/openvtc/vtc/website/rollback/1.0")
.expect("static Trust-Task URL");
const WEBSITE_ROUTE_CAP: usize = 64 * 1024 * 1024;
api.route_with_task(
"/website/files",
get(website::files::list),
website_files_list,
)
.route_with_task(
"/website/files/{*path}",
get(website::files::show)
.put(website::files::write)
.delete(website::files::delete)
.layer(DefaultBodyLimit::max(WEBSITE_ROUTE_CAP)),
website_files_show,
)
.route_with_task(
"/website/deploy",
post(website::deploy::deploy).layer(DefaultBodyLimit::max(WEBSITE_ROUTE_CAP)),
website_deploy,
)
.route_with_task(
"/website/generations",
get(website::generations::list),
website_gens_list,
)
.route_with_task(
"/website/rollback/{gen_num}",
post(website::generations::rollback),
website_rollback,
)
};
let api = api
.into_router()
.layer(DefaultBodyLimit::max(MAX_BODY_SIZE));
let unauth = build_unauth_routes(trust_xff);
api.merge(unauth)
}
fn build_unauth_routes(trust_xff: bool) -> Router<AppState> {
let auth_challenge = TrustTask::new("https://trusttasks.org/spec/auth/challenge/0.1")
.expect("static Trust-Task URL");
let auth_authenticate = TrustTask::new("https://trusttasks.org/spec/auth/authenticate/0.1")
.expect("static Trust-Task URL");
let auth_refresh = TrustTask::new("https://trusttasks.org/spec/auth/refresh/0.1")
.expect("static Trust-Task URL");
let auth_admin_login =
TrustTask::new("https://trusttasks.org/openvtc/vtc/auth/admin-login/1.0")
.expect("static Trust-Task URL");
let auth_passkey_login_start =
TrustTask::new("https://trusttasks.org/spec/auth/passkey/login/start/0.1")
.expect("static Trust-Task URL");
let auth_passkey_login_finish =
TrustTask::new("https://trusttasks.org/spec/auth/passkey/login/finish/0.1")
.expect("static Trust-Task URL");
let install_claim_start =
TrustTask::new("https://trusttasks.org/openvtc/vtc/install/claim/start/1.0")
.expect("static Trust-Task URL");
let install_claim_finish =
TrustTask::new("https://trusttasks.org/openvtc/vtc/install/claim/finish/1.0")
.expect("static Trust-Task URL");
let auth_recognise = TrustTask::new("https://trusttasks.org/openvtc/vtc/auth/recognise/1.0")
.expect("static Trust-Task URL");
let _ = trust_xff;
let synth_connect_info = axum::middleware::from_fn(insert_default_connect_info_if_missing);
let unauth_router = TrustTaskRouter::<AppState>::new()
.route_with_task("/auth/challenge", post(auth::challenge), auth_challenge)
.route_with_task("/auth/", post(auth::authenticate), auth_authenticate)
.route_with_task("/auth/refresh", post(auth::refresh), auth_refresh)
.route_with_task(
"/auth/admin-login",
post(auth::admin_login),
auth_admin_login,
)
.route_with_task(
"/auth/passkey-login/start",
post(auth::passkey_login_start),
auth_passkey_login_start,
)
.route_with_task(
"/auth/passkey-login/finish",
post(auth::passkey_login_finish),
auth_passkey_login_finish,
)
.route_with_task(
"/install/claim/start",
post(install::claim_start),
install_claim_start,
)
.route_with_task(
"/install/claim/finish",
post(install::claim_finish),
install_claim_finish,
)
.route_with_task(
"/auth/recognise",
post(recognise::recognise),
auth_recognise,
)
.into_router()
.layer(DefaultBodyLimit::max(UNAUTH_BODY_SIZE));
let unauth_router = if trust_xff {
let cfg = Arc::new(
GovernorConfigBuilder::default()
.per_second(5)
.burst_size(10)
.key_extractor(SmartIpKeyExtractor)
.finish()
.expect("governor config values are static and non-zero"),
);
unauth_router.layer(GovernorLayer::new(cfg))
} else {
let cfg = Arc::new(
GovernorConfigBuilder::default()
.per_second(5)
.burst_size(10)
.key_extractor(tower_governor::key_extractor::PeerIpKeyExtractor)
.finish()
.expect("governor config values are static and non-zero"),
);
unauth_router.layer(GovernorLayer::new(cfg))
};
unauth_router.layer(synth_connect_info)
}
async fn insert_default_connect_info_if_missing(
mut request: axum::extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use axum::extract::ConnectInfo;
if request
.extensions()
.get::<ConnectInfo<SocketAddr>>()
.is_none()
{
let synthetic =
ConnectInfo::<SocketAddr>(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0));
request.extensions_mut().insert(synthetic);
}
next.run(request).await
}
#[cfg_attr(feature = "website", allow(dead_code))]
fn assemble(routing: &RoutingConfig, api: Router<AppState>) -> Router<AppState> {
use axum::middleware::from_fn;
use crate::routing::security_headers::security_headers;
let admin_placeholder: Router<AppState> = Router::new()
.fallback(any(placeholder_503))
.layer(from_fn(security_headers));
let website_placeholder: Router<AppState> = Router::new()
.fallback(any(placeholder_503))
.layer(from_fn(security_headers));
let mut app: Router<AppState> = Router::new()
.route("/health", get(health::health))
.nest(&routing.api.mount, api);
app = app.nest(&routing.admin_ui.mount, admin_placeholder);
if routing.website.mount == "/" {
app = app.merge(website_placeholder);
} else {
app = app.nest(&routing.website.mount, website_placeholder);
}
app
}
#[cfg(feature = "website")]
pub fn assemble_with_website(
routing: &RoutingConfig,
api: Router<AppState>,
website_state: Option<crate::website::WebsiteState>,
) -> Router<AppState> {
use axum::middleware::from_fn;
use crate::routing::security_headers::security_headers;
#[cfg(feature = "admin-ui")]
let admin: Router<AppState> = Router::new()
.route("/build-info.json", get(admin_ui::build_info))
.route("/plugins.json", get(admin_ui::plugins_manifest))
.route("/plugins/{id}/{*rel_path}", get(admin_ui::plugin_asset))
.route("/", get(admin_ui::serve_spa))
.route("/{*path}", get(admin_ui::serve_spa))
.layer(from_fn(security_headers));
#[cfg(not(feature = "admin-ui"))]
let admin: Router<AppState> = Router::new()
.route("/", any(placeholder_503))
.route("/{*path}", any(placeholder_503))
.layer(from_fn(security_headers));
let website: axum::Router<()> = match website_state {
Some(state) => Router::new()
.route("/", get(crate::website::serve))
.route("/{*path}", get(crate::website::serve))
.layer(from_fn(security_headers))
.with_state(state),
None => Router::new()
.route("/", get(crate::website::default_site::serve))
.route("/{*path}", get(crate::website::default_site::serve))
.layer(from_fn(security_headers)),
};
let mut app: Router<AppState> = Router::new()
.route("/health", get(health::health))
.nest(&routing.api.mount, api);
app = app.nest(&routing.admin_ui.mount, admin);
let admin_slash = format!("{}/", routing.admin_ui.mount.trim_end_matches('/'));
#[cfg(feature = "admin-ui")]
{
app = app.route(admin_slash.as_str(), get(admin_ui::serve_spa));
}
#[cfg(not(feature = "admin-ui"))]
{
app = app.route(admin_slash.as_str(), any(placeholder_503));
}
if routing.website.mount == "/" {
app = app.fallback_service(website);
} else {
app = app.nest_service(&routing.website.mount, website);
}
app
}
#[cfg_attr(all(feature = "website", feature = "admin-ui"), allow(dead_code))]
async fn placeholder_503() -> impl IntoResponse {
(
StatusCode::SERVICE_UNAVAILABLE,
"surface not yet implemented",
)
}