use std::time::Duration; use axum::{ Router, extract::DefaultBodyLimit, routing::{get, get_service, post}, }; use axum_htmx::AutoVaryLayer; use http::{ HeaderName, Method, StatusCode, header::{ACCEPT, ACCEPT_LANGUAGE, CONTENT_TYPE}, }; use tower_http::trace::{self, TraceLayer}; use tower_http::{classify::ServerErrorsFailureClass, timeout::TimeoutLayer}; use tower_http::{cors::CorsLayer, services::ServeDir}; use tracing::Span; use crate::http::handle_view_event::{handle_event_attendees, handle_event_ics}; use crate::http::{ context::WebContext, handle_accept_rsvp::handle_accept_rsvp, handle_admin_content_list::handle_admin_content_list, handle_admin_content_view::{handle_admin_content_nuke, handle_admin_content_view}, handle_admin_denylist::{ handle_admin_denylist, handle_admin_denylist_add, handle_admin_denylist_remove, }, handle_admin_event::{handle_admin_event, handle_admin_event_nuke}, handle_admin_event_index::{ handle_admin_event_index, handle_admin_event_index_delete, handle_admin_event_index_rebuild, }, handle_admin_events::handle_admin_events, handle_admin_handles::{ handle_admin_handles, handle_admin_nuke_identity, handle_admin_tap_sync_all, }, handle_admin_identity_profile::{ handle_admin_identity_profile, handle_admin_identity_profile_ban_did, handle_admin_identity_profile_ban_pds, handle_admin_identity_profile_nuke, }, handle_admin_import_event::handle_admin_import_event, handle_admin_import_rsvp::handle_admin_import_rsvp, handle_admin_index::handle_admin_index, handle_admin_profile_index::{ handle_admin_profile_index, handle_admin_profile_index_delete, handle_admin_profile_index_rebuild, }, handle_admin_rsvp::{handle_admin_rsvp, handle_admin_rsvp_nuke}, handle_admin_rsvp_accept::{handle_admin_rsvp_accept, handle_admin_rsvp_accept_nuke}, handle_admin_rsvp_accepts::handle_admin_rsvp_accepts, handle_admin_rsvps::handle_admin_rsvps, handle_admin_search_index::{ handle_admin_search_index, handle_admin_search_index_delete, handle_admin_search_index_rebuild, }, handle_admin_tap::{handle_admin_tap, handle_admin_tap_info, handle_admin_tap_submit}, handle_api_mcp_configuration::{ handle_api_mcp_configuration_get, handle_api_mcp_configuration_post, }, handle_blob::{ delete_profile_avatar, delete_profile_banner, upload_event_header, upload_event_thumbnail, upload_profile_avatar, upload_profile_banner, }, handle_bulk_accept_rsvps::handle_bulk_accept_rsvps, handle_content::handle_content, handle_create_event::{handle_create_event, handle_create_event_json}, handle_create_rsvp::handle_create_rsvp, handle_delete_event::handle_delete_event, handle_edit_event::handle_edit_event_json, handle_edit_settings::handle_edit_settings, handle_email_confirm::{handle_confirm_email, handle_send_email_confirmation}, handle_export_ics::handle_export_ics, handle_export_rsvps::handle_export_rsvps, handle_finalize_acceptance::handle_finalize_acceptance, handle_geo_aggregation::{handle_geo_aggregation, handle_globe_aggregation}, handle_health::{handle_alive, handle_ready, handle_started}, handle_host_meta::handle_host_meta, handle_import::{handle_import, handle_import_submit}, handle_index::handle_index, handle_lfg::{ handle_lfg_deactivate, handle_lfg_geo_aggregation, handle_lfg_get, handle_lfg_post, handle_lfg_tags_autocomplete, }, handle_location::handle_location, handle_location_suggestions::handle_location_suggestions, handle_mailgun_webhook::handle_mailgun_webhook, handle_manage_event::handle_manage_event, handle_manage_event_content::handle_manage_event_content_save, handle_mcp::{delete_mcp_authenticated, post_mcp_authenticated}, handle_mcp_oauth::{ get_mcp_authorize, get_mcp_authorize_callback, get_mcp_oauth_authorization_server, get_mcp_oauth_protected_resource, post_mcp_authorize, post_mcp_register, post_mcp_token, }, handle_oauth::{ handle_auth_callback, handle_auth_init, handle_auth_logout, handle_auth_refresh, handle_oauth_metadata, }, handle_policy::{ handle_about, handle_acknowledgement, handle_cookie_policy, handle_privacy_policy, handle_terms_of_service, }, handle_preview_description::handle_preview_description, handle_profile::handle_profile_view, handle_quick_event::handle_quick_event, handle_search::handle_search, handle_set_language::handle_set_language, handle_settings::{ handle_email_update, handle_language_update, handle_notification_email_update, handle_notification_preferences_update, handle_profile_update, handle_settings, handle_timezone_update, }, handle_share_rsvp_bluesky::handle_share_rsvp_bluesky, handle_unaccept_rsvp::handle_unaccept_rsvp, handle_unsubscribe::handle_unsubscribe, handle_view_event::handle_view_event, handle_wellknown::handle_wellknown_did_web, handle_xrpc_get_event::handle_xrpc_get_event, handle_xrpc_get_rsvp::handle_xrpc_get_rsvp, handle_xrpc_link_attestation::handle_xrpc_link_attestation, handle_xrpc_search_events::handle_xrpc_search_events, }; pub fn build_router(web_context: WebContext) -> Router { let serve_dir = ServeDir::new(web_context.config.http_static_path.clone()); let router = Router::new() .route("/", get(handle_index)) .route("/robots.txt", get_service(serve_dir.clone())) .route("/favicon.ico", get_service(serve_dir.clone())) .route( "/apple-touch-icon-precomposed.png", get_service(serve_dir.clone()), ) .route("/apple-touch-icon.png", get_service(serve_dir.clone())) .route("/manifest.webmanifest", get_service(serve_dir.clone())) .route("/about", get(handle_about)) .route("/privacy-policy", get(handle_privacy_policy)) .route("/terms-of-service", get(handle_terms_of_service)) .route("/cookie-policy", get(handle_cookie_policy)) .route("/acknowledgement", get(handle_acknowledgement)) .route("/.well-known/did.json", get(handle_wellknown_did_web)) .route("/.well-known/host-meta.json", get(handle_host_meta)) .route( "/.well-known/oauth-protected-resource/mcp", get(get_mcp_oauth_protected_resource), ) .route( "/.well-known/oauth-authorization-server/mcp", get(get_mcp_oauth_authorization_server), ) .route( "/mcp", post(post_mcp_authenticated).delete(delete_mcp_authenticated), ) .route("/mcp/register", post(post_mcp_register)) .route( "/mcp/authorize", get(get_mcp_authorize).post(post_mcp_authorize), ) .route("/mcp/authorize/callback", get(get_mcp_authorize_callback)) .route("/mcp/token", post(post_mcp_token)) .route("/_ready", get(handle_ready)) .route("/_alive", get(handle_alive)) .route("/_started", get(handle_started)) .route( "/xrpc/community.lexicon.calendar.searchEvents", get(handle_xrpc_search_events), ) // legacy endpoint .route( "/xrpc/community.lexicon.calendar.SearchEvents", get(handle_xrpc_search_events), ) .route( "/xrpc/community.lexicon.calendar.getEvent", get(handle_xrpc_get_event), ) // legacy endpoint .route( "/xrpc/community.lexicon.calendar.GetEvent", get(handle_xrpc_get_event), ) .route( "/xrpc/community.lexicon.calendar.getRSVP", get(handle_xrpc_get_rsvp), ) .route( "/xrpc/events.smokesignal.rsvp.linkAttestation", post(handle_xrpc_link_attestation), ) // API endpoints .route("/api/geo-aggregation", get(handle_geo_aggregation)) .route("/api/globe-aggregation", get(handle_globe_aggregation)) .route("/api/lfg/tags", get(handle_lfg_tags_autocomplete)) .route("/api/lfg/geo-aggregation", get(handle_lfg_geo_aggregation)) .route( "/api/mcp/configuration", get(handle_api_mcp_configuration_get).post(handle_api_mcp_configuration_post), ) // LFG routes .route("/lfg", get(handle_lfg_get)) .route("/lfg", post(handle_lfg_post)) .route("/lfg/deactivate", post(handle_lfg_deactivate)) // Location route .route("/l/{location}", get(handle_location)); // Add OAuth routes for AT Protocol router .route("/oauth-client-metadata.json", get(handle_oauth_metadata)) .route("/oauth/login", get(handle_auth_init)) .route("/oauth/login", post(handle_auth_init)) .route("/oauth/callback", get(handle_auth_callback)) .route("/oauth/refresh", post(handle_auth_refresh)) .route("/oauth/logout", post(handle_auth_logout)) .route("/admin", get(handle_admin_index)) .route("/admin/identity_profiles", get(handle_admin_handles)) .route( "/admin/identity_profiles/tap-sync", post(handle_admin_tap_sync_all), ) .route( "/admin/identity_profile", get(handle_admin_identity_profile), ) .route( "/admin/identity_profile/ban-did", post(handle_admin_identity_profile_ban_did), ) .route( "/admin/identity_profile/ban-pds", post(handle_admin_identity_profile_ban_pds), ) .route( "/admin/identity_profile/nuke", post(handle_admin_identity_profile_nuke), ) .route("/admin/handles", get(handle_admin_handles)) .route( "/admin/handles/nuke/{did}", post(handle_admin_nuke_identity), ) // Legacy routes - redirect to new combined view .route("/admin/identities", get(handle_admin_handles)) .route("/admin/identity", get(handle_admin_identity_profile)) .route("/admin/content", get(handle_admin_content_list)) .route("/admin/content/view", get(handle_admin_content_view)) .route("/admin/content/nuke", post(handle_admin_content_nuke)) .route("/admin/denylist", get(handle_admin_denylist)) .route("/admin/denylist/add", post(handle_admin_denylist_add)) .route("/admin/denylist/remove", post(handle_admin_denylist_remove)) .route("/admin/events", get(handle_admin_events)) .route("/admin/events/import", post(handle_admin_import_event)) .route("/admin/event", get(handle_admin_event)) .route("/admin/event/nuke", post(handle_admin_event_nuke)) .route("/admin/rsvps", get(handle_admin_rsvps)) .route("/admin/rsvp", get(handle_admin_rsvp)) .route("/admin/rsvp/nuke", post(handle_admin_rsvp_nuke)) .route("/admin/rsvps/import", post(handle_admin_import_rsvp)) .route("/admin/rsvp-accepts", get(handle_admin_rsvp_accepts)) .route("/admin/rsvp-accept", get(handle_admin_rsvp_accept)) .route( "/admin/rsvp-accept/nuke", post(handle_admin_rsvp_accept_nuke), ) .route("/admin/search-index", get(handle_admin_search_index)) .route( "/admin/search-index/delete", post(handle_admin_search_index_delete), ) .route( "/admin/search-index/rebuild", post(handle_admin_search_index_rebuild), ) .route("/admin/search-index/events", get(handle_admin_event_index)) .route( "/admin/search-index/events/delete", post(handle_admin_event_index_delete), ) .route( "/admin/search-index/events/rebuild", post(handle_admin_event_index_rebuild), ) .route( "/admin/search-index/profiles", get(handle_admin_profile_index), ) .route( "/admin/search-index/profiles/delete", post(handle_admin_profile_index_delete), ) .route( "/admin/search-index/profiles/rebuild", post(handle_admin_profile_index_rebuild), ) .route("/admin/tap", get(handle_admin_tap)) .route("/admin/tap/submit", post(handle_admin_tap_submit)) .route("/admin/tap/info", post(handle_admin_tap_info)) .route("/content/{cid}", get(handle_content)) .route("/logout", get(handle_auth_logout)) .route("/language", post(handle_set_language)) .route("/search", get(handle_search)) .route("/settings", get(handle_settings)) .route("/settings/timezone", post(handle_timezone_update)) .route("/settings/language", post(handle_language_update)) .route("/settings/email", post(handle_email_update)) .route("/settings/profile", post(handle_profile_update)) .route( "/settings/avatar", post(upload_profile_avatar).layer(DefaultBodyLimit::max(5 * 1024 * 1024)), // 5MB limit ) .route("/settings/avatar/delete", post(delete_profile_avatar)) .route( "/settings/banner", post(upload_profile_banner).layer(DefaultBodyLimit::max(5 * 1024 * 1024)), // 5MB limit ) .route("/settings/banner/delete", post(delete_profile_banner)) .route( "/settings/notifications/email", post(handle_notification_email_update), ) .route( "/settings/notifications/preferences", post(handle_notification_preferences_update), ) .route( "/settings/notifications/confirm", post(handle_send_email_confirmation), ) .route( "/settings/confirm-email/{token_sig}", get(handle_confirm_email), ) .route("/unsubscribe/{token}", get(handle_unsubscribe)) .route("/webhooks/mailgun", post(handle_mailgun_webhook)) .route("/import", get(handle_import)) .route("/import", post(handle_import_submit)) .route("/quick-event", get(handle_quick_event)) .route("/event", get(handle_create_event)) .route("/event", post(handle_create_event_json)) .route( "/event/preview-description", post(handle_preview_description), ) .route( "/event/location-suggestions", get(handle_location_suggestions), ) .route("/rsvp", get(handle_create_rsvp)) .route("/rsvp", post(handle_create_rsvp)) .route("/accept_rsvp", post(handle_accept_rsvp)) .route("/bulk_accept_rsvps", post(handle_bulk_accept_rsvps)) .route("/unaccept_rsvp", post(handle_unaccept_rsvp)) .route("/share_rsvp_bluesky", post(handle_share_rsvp_bluesky)) .route("/finalize_acceptance", post(handle_finalize_acceptance)) .route( "/event/upload-header", post(upload_event_header).layer(DefaultBodyLimit::max(5 * 1024 * 1024)), // 5MB limit ) .route( "/event/upload-thumbnail", post(upload_event_thumbnail).layer(DefaultBodyLimit::max(5 * 1024 * 1024)), // 5MB limit ) .route("/ics/{*aturi}", get(handle_export_ics)) .route("/{handle_slug}/ics", get(handle_event_ics)) .route( "/{handle_slug}/{event_rkey}/edit", post(handle_edit_event_json), ) .route( "/{handle_slug}/{event_rkey}/manage", get(handle_manage_event), ) .route( "/{handle_slug}/{event_rkey}/manage/content", post(handle_manage_event_content_save), ) .route( "/{handle_slug}/{event_rkey}/edit-settings", post(handle_edit_settings), ) .route( "/{handle_slug}/{event_rkey}/export-rsvps", get(handle_export_rsvps), ) .route( "/{handle_slug}/{event_rkey}/delete", post(handle_delete_event), ) .route( "/{handle_slug}/{event_rkey}/attendees", get(handle_event_attendees), ) .route("/{handle_slug}/{event_rkey}", get(handle_view_event)) .route("/{handle_slug}", get(handle_profile_view)) .nest_service("/static", serve_dir.clone()) .fallback_service(serve_dir) .layer(( TraceLayer::new_for_http() .make_span_with(trace::DefaultMakeSpan::new().level(tracing::Level::INFO)) .on_failure( |err: ServerErrorsFailureClass, _latency: Duration, _span: &Span| { tracing::error!(error = ?err, "Unhandled error: {err}"); }, ), TimeoutLayer::with_status_code(StatusCode::REQUEST_TIMEOUT, Duration::from_secs(30)), )) .layer( CorsLayer::new() .allow_origin(tower_http::cors::Any) .allow_methods([Method::GET]) .allow_headers([ ACCEPT_LANGUAGE, ACCEPT, CONTENT_TYPE, HeaderName::from_lowercase(b"x-widget-version").unwrap(), ]), ) .layer(AutoVaryLayer) .with_state(web_context.clone()) }