The smokesignal.events web application
at main 423 lines 18 kB view raw
1use std::time::Duration; 2 3use axum::{ 4 Router, 5 extract::DefaultBodyLimit, 6 routing::{get, get_service, post}, 7}; 8use axum_htmx::AutoVaryLayer; 9use http::{ 10 HeaderName, Method, StatusCode, 11 header::{ACCEPT, ACCEPT_LANGUAGE, CONTENT_TYPE}, 12}; 13use tower_http::trace::{self, TraceLayer}; 14use tower_http::{classify::ServerErrorsFailureClass, timeout::TimeoutLayer}; 15use tower_http::{cors::CorsLayer, services::ServeDir}; 16use tracing::Span; 17 18use crate::http::handle_view_event::{handle_event_attendees, handle_event_ics}; 19use crate::http::{ 20 context::WebContext, 21 handle_accept_rsvp::handle_accept_rsvp, 22 handle_admin_content_list::handle_admin_content_list, 23 handle_admin_content_view::{handle_admin_content_nuke, handle_admin_content_view}, 24 handle_admin_denylist::{ 25 handle_admin_denylist, handle_admin_denylist_add, handle_admin_denylist_remove, 26 }, 27 handle_admin_event::{handle_admin_event, handle_admin_event_nuke}, 28 handle_admin_event_index::{ 29 handle_admin_event_index, handle_admin_event_index_delete, handle_admin_event_index_rebuild, 30 }, 31 handle_admin_events::handle_admin_events, 32 handle_admin_handles::{ 33 handle_admin_handles, handle_admin_nuke_identity, handle_admin_tap_sync_all, 34 }, 35 handle_admin_identity_profile::{ 36 handle_admin_identity_profile, handle_admin_identity_profile_ban_did, 37 handle_admin_identity_profile_ban_pds, handle_admin_identity_profile_nuke, 38 }, 39 handle_admin_import_event::handle_admin_import_event, 40 handle_admin_import_rsvp::handle_admin_import_rsvp, 41 handle_admin_index::handle_admin_index, 42 handle_admin_profile_index::{ 43 handle_admin_profile_index, handle_admin_profile_index_delete, 44 handle_admin_profile_index_rebuild, 45 }, 46 handle_admin_rsvp::{handle_admin_rsvp, handle_admin_rsvp_nuke}, 47 handle_admin_rsvp_accept::{handle_admin_rsvp_accept, handle_admin_rsvp_accept_nuke}, 48 handle_admin_rsvp_accepts::handle_admin_rsvp_accepts, 49 handle_admin_rsvps::handle_admin_rsvps, 50 handle_admin_search_index::{ 51 handle_admin_search_index, handle_admin_search_index_delete, 52 handle_admin_search_index_rebuild, 53 }, 54 handle_admin_tap::{handle_admin_tap, handle_admin_tap_info, handle_admin_tap_submit}, 55 handle_api_mcp_configuration::{ 56 handle_api_mcp_configuration_get, handle_api_mcp_configuration_post, 57 }, 58 handle_blob::{ 59 delete_profile_avatar, delete_profile_banner, upload_event_header, upload_event_thumbnail, 60 upload_profile_avatar, upload_profile_banner, 61 }, 62 handle_bulk_accept_rsvps::handle_bulk_accept_rsvps, 63 handle_content::handle_content, 64 handle_create_event::{handle_create_event, handle_create_event_json}, 65 handle_create_rsvp::handle_create_rsvp, 66 handle_delete_event::handle_delete_event, 67 handle_edit_event::handle_edit_event_json, 68 handle_edit_settings::handle_edit_settings, 69 handle_email_confirm::{handle_confirm_email, handle_send_email_confirmation}, 70 handle_export_ics::handle_export_ics, 71 handle_export_rsvps::handle_export_rsvps, 72 handle_finalize_acceptance::handle_finalize_acceptance, 73 handle_geo_aggregation::{handle_geo_aggregation, handle_globe_aggregation}, 74 handle_health::{handle_alive, handle_ready, handle_started}, 75 handle_host_meta::handle_host_meta, 76 handle_import::{handle_import, handle_import_submit}, 77 handle_index::handle_index, 78 handle_lfg::{ 79 handle_lfg_deactivate, handle_lfg_geo_aggregation, handle_lfg_get, handle_lfg_post, 80 handle_lfg_tags_autocomplete, 81 }, 82 handle_location::handle_location, 83 handle_location_suggestions::handle_location_suggestions, 84 handle_mailgun_webhook::handle_mailgun_webhook, 85 handle_manage_event::handle_manage_event, 86 handle_manage_event_content::handle_manage_event_content_save, 87 handle_mcp::{delete_mcp_authenticated, post_mcp_authenticated}, 88 handle_mcp_oauth::{ 89 get_mcp_authorize, get_mcp_authorize_callback, get_mcp_oauth_authorization_server, 90 get_mcp_oauth_protected_resource, post_mcp_authorize, post_mcp_register, post_mcp_token, 91 }, 92 handle_oauth::{ 93 handle_auth_callback, handle_auth_init, handle_auth_logout, handle_auth_refresh, 94 handle_oauth_metadata, 95 }, 96 handle_policy::{ 97 handle_about, handle_acknowledgement, handle_cookie_policy, handle_privacy_policy, 98 handle_terms_of_service, 99 }, 100 handle_preview_description::handle_preview_description, 101 handle_profile::handle_profile_view, 102 handle_quick_event::handle_quick_event, 103 handle_search::handle_search, 104 handle_set_language::handle_set_language, 105 handle_settings::{ 106 handle_email_update, handle_language_update, handle_notification_email_update, 107 handle_notification_preferences_update, handle_profile_update, handle_settings, 108 handle_timezone_update, 109 }, 110 handle_share_rsvp_bluesky::handle_share_rsvp_bluesky, 111 handle_unaccept_rsvp::handle_unaccept_rsvp, 112 handle_unsubscribe::handle_unsubscribe, 113 handle_view_event::handle_view_event, 114 handle_wellknown::handle_wellknown_did_web, 115 handle_xrpc_configure_event::handle_xrpc_configure_event, 116 handle_xrpc_get_event::handle_xrpc_get_event, 117 handle_xrpc_get_rsvp::handle_xrpc_get_rsvp, 118 handle_xrpc_link_attestation::handle_xrpc_link_attestation, 119 handle_xrpc_search_events::handle_xrpc_search_events, 120}; 121 122pub fn build_router(web_context: WebContext) -> Router { 123 let serve_dir = ServeDir::new(web_context.config.http_static_path.clone()); 124 125 let router = Router::new() 126 .route("/", get(handle_index)) 127 .route("/robots.txt", get_service(serve_dir.clone())) 128 .route("/favicon.ico", get_service(serve_dir.clone())) 129 .route( 130 "/apple-touch-icon-precomposed.png", 131 get_service(serve_dir.clone()), 132 ) 133 .route("/apple-touch-icon.png", get_service(serve_dir.clone())) 134 .route("/manifest.webmanifest", get_service(serve_dir.clone())) 135 .route("/about", get(handle_about)) 136 .route("/privacy-policy", get(handle_privacy_policy)) 137 .route("/terms-of-service", get(handle_terms_of_service)) 138 .route("/cookie-policy", get(handle_cookie_policy)) 139 .route("/acknowledgement", get(handle_acknowledgement)) 140 .route("/.well-known/did.json", get(handle_wellknown_did_web)) 141 .route("/.well-known/host-meta.json", get(handle_host_meta)) 142 .route( 143 "/.well-known/oauth-protected-resource/mcp", 144 get(get_mcp_oauth_protected_resource), 145 ) 146 .route( 147 "/.well-known/oauth-authorization-server/mcp", 148 get(get_mcp_oauth_authorization_server), 149 ) 150 .route( 151 "/mcp", 152 post(post_mcp_authenticated).delete(delete_mcp_authenticated), 153 ) 154 .route("/mcp/register", post(post_mcp_register)) 155 .route( 156 "/mcp/authorize", 157 get(get_mcp_authorize).post(post_mcp_authorize), 158 ) 159 .route("/mcp/authorize/callback", get(get_mcp_authorize_callback)) 160 .route("/mcp/token", post(post_mcp_token)) 161 .route("/_ready", get(handle_ready)) 162 .route("/_alive", get(handle_alive)) 163 .route("/_started", get(handle_started)) 164 .route( 165 "/xrpc/community.lexicon.calendar.searchEvents", 166 get(handle_xrpc_search_events), 167 ) 168 // legacy endpoint 169 .route( 170 "/xrpc/community.lexicon.calendar.SearchEvents", 171 get(handle_xrpc_search_events), 172 ) 173 .route( 174 "/xrpc/community.lexicon.calendar.getEvent", 175 get(handle_xrpc_get_event), 176 ) 177 // legacy endpoint 178 .route( 179 "/xrpc/community.lexicon.calendar.GetEvent", 180 get(handle_xrpc_get_event), 181 ) 182 .route( 183 "/xrpc/community.lexicon.calendar.getRSVP", 184 get(handle_xrpc_get_rsvp), 185 ) 186 .route( 187 "/xrpc/events.smokesignal.rsvp.linkAttestation", 188 post(handle_xrpc_link_attestation), 189 ) 190 .route( 191 "/xrpc/events.smokesignal.event.configure", 192 post(handle_xrpc_configure_event), 193 ) 194 // API endpoints 195 .route("/api/geo-aggregation", get(handle_geo_aggregation)) 196 .route("/api/globe-aggregation", get(handle_globe_aggregation)) 197 .route("/api/lfg/tags", get(handle_lfg_tags_autocomplete)) 198 .route("/api/lfg/geo-aggregation", get(handle_lfg_geo_aggregation)) 199 .route( 200 "/api/mcp/configuration", 201 get(handle_api_mcp_configuration_get).post(handle_api_mcp_configuration_post), 202 ) 203 // LFG routes 204 .route("/lfg", get(handle_lfg_get)) 205 .route("/lfg", post(handle_lfg_post)) 206 .route("/lfg/deactivate", post(handle_lfg_deactivate)) 207 // Location route 208 .route("/l/{location}", get(handle_location)); 209 210 // Add OAuth routes for AT Protocol 211 router 212 .route("/oauth-client-metadata.json", get(handle_oauth_metadata)) 213 .route("/oauth/login", get(handle_auth_init)) 214 .route("/oauth/login", post(handle_auth_init)) 215 .route("/oauth/callback", get(handle_auth_callback)) 216 .route("/oauth/refresh", post(handle_auth_refresh)) 217 .route("/oauth/logout", post(handle_auth_logout)) 218 .route("/admin", get(handle_admin_index)) 219 .route("/admin/identity_profiles", get(handle_admin_handles)) 220 .route( 221 "/admin/identity_profiles/tap-sync", 222 post(handle_admin_tap_sync_all), 223 ) 224 .route( 225 "/admin/identity_profile", 226 get(handle_admin_identity_profile), 227 ) 228 .route( 229 "/admin/identity_profile/ban-did", 230 post(handle_admin_identity_profile_ban_did), 231 ) 232 .route( 233 "/admin/identity_profile/ban-pds", 234 post(handle_admin_identity_profile_ban_pds), 235 ) 236 .route( 237 "/admin/identity_profile/nuke", 238 post(handle_admin_identity_profile_nuke), 239 ) 240 .route("/admin/handles", get(handle_admin_handles)) 241 .route( 242 "/admin/handles/nuke/{did}", 243 post(handle_admin_nuke_identity), 244 ) 245 // Legacy routes - redirect to new combined view 246 .route("/admin/identities", get(handle_admin_handles)) 247 .route("/admin/identity", get(handle_admin_identity_profile)) 248 .route("/admin/content", get(handle_admin_content_list)) 249 .route("/admin/content/view", get(handle_admin_content_view)) 250 .route("/admin/content/nuke", post(handle_admin_content_nuke)) 251 .route("/admin/denylist", get(handle_admin_denylist)) 252 .route("/admin/denylist/add", post(handle_admin_denylist_add)) 253 .route("/admin/denylist/remove", post(handle_admin_denylist_remove)) 254 .route("/admin/events", get(handle_admin_events)) 255 .route("/admin/events/import", post(handle_admin_import_event)) 256 .route("/admin/event", get(handle_admin_event)) 257 .route("/admin/event/nuke", post(handle_admin_event_nuke)) 258 .route("/admin/rsvps", get(handle_admin_rsvps)) 259 .route("/admin/rsvp", get(handle_admin_rsvp)) 260 .route("/admin/rsvp/nuke", post(handle_admin_rsvp_nuke)) 261 .route("/admin/rsvps/import", post(handle_admin_import_rsvp)) 262 .route("/admin/rsvp-accepts", get(handle_admin_rsvp_accepts)) 263 .route("/admin/rsvp-accept", get(handle_admin_rsvp_accept)) 264 .route( 265 "/admin/rsvp-accept/nuke", 266 post(handle_admin_rsvp_accept_nuke), 267 ) 268 .route("/admin/search-index", get(handle_admin_search_index)) 269 .route( 270 "/admin/search-index/delete", 271 post(handle_admin_search_index_delete), 272 ) 273 .route( 274 "/admin/search-index/rebuild", 275 post(handle_admin_search_index_rebuild), 276 ) 277 .route("/admin/search-index/events", get(handle_admin_event_index)) 278 .route( 279 "/admin/search-index/events/delete", 280 post(handle_admin_event_index_delete), 281 ) 282 .route( 283 "/admin/search-index/events/rebuild", 284 post(handle_admin_event_index_rebuild), 285 ) 286 .route( 287 "/admin/search-index/profiles", 288 get(handle_admin_profile_index), 289 ) 290 .route( 291 "/admin/search-index/profiles/delete", 292 post(handle_admin_profile_index_delete), 293 ) 294 .route( 295 "/admin/search-index/profiles/rebuild", 296 post(handle_admin_profile_index_rebuild), 297 ) 298 .route("/admin/tap", get(handle_admin_tap)) 299 .route("/admin/tap/submit", post(handle_admin_tap_submit)) 300 .route("/admin/tap/info", post(handle_admin_tap_info)) 301 .route("/content/{cid}", get(handle_content)) 302 .route("/logout", get(handle_auth_logout)) 303 .route("/language", post(handle_set_language)) 304 .route("/search", get(handle_search)) 305 .route("/settings", get(handle_settings)) 306 .route("/settings/timezone", post(handle_timezone_update)) 307 .route("/settings/language", post(handle_language_update)) 308 .route("/settings/email", post(handle_email_update)) 309 .route("/settings/profile", post(handle_profile_update)) 310 .route( 311 "/settings/avatar", 312 post(upload_profile_avatar).layer(DefaultBodyLimit::max(5 * 1024 * 1024)), // 5MB limit 313 ) 314 .route("/settings/avatar/delete", post(delete_profile_avatar)) 315 .route( 316 "/settings/banner", 317 post(upload_profile_banner).layer(DefaultBodyLimit::max(5 * 1024 * 1024)), // 5MB limit 318 ) 319 .route("/settings/banner/delete", post(delete_profile_banner)) 320 .route( 321 "/settings/notifications/email", 322 post(handle_notification_email_update), 323 ) 324 .route( 325 "/settings/notifications/preferences", 326 post(handle_notification_preferences_update), 327 ) 328 .route( 329 "/settings/notifications/confirm", 330 post(handle_send_email_confirmation), 331 ) 332 .route( 333 "/settings/confirm-email/{token_sig}", 334 get(handle_confirm_email), 335 ) 336 .route("/unsubscribe/{token}", get(handle_unsubscribe)) 337 .route("/webhooks/mailgun", post(handle_mailgun_webhook)) 338 .route("/import", get(handle_import)) 339 .route("/import", post(handle_import_submit)) 340 .route("/quick-event", get(handle_quick_event)) 341 .route("/event", get(handle_create_event)) 342 .route("/event", post(handle_create_event_json)) 343 .route( 344 "/event/preview-description", 345 post(handle_preview_description), 346 ) 347 .route( 348 "/event/location-suggestions", 349 get(handle_location_suggestions), 350 ) 351 .route("/rsvp", get(handle_create_rsvp)) 352 .route("/rsvp", post(handle_create_rsvp)) 353 .route("/accept_rsvp", post(handle_accept_rsvp)) 354 .route("/bulk_accept_rsvps", post(handle_bulk_accept_rsvps)) 355 .route("/unaccept_rsvp", post(handle_unaccept_rsvp)) 356 .route("/share_rsvp_bluesky", post(handle_share_rsvp_bluesky)) 357 .route("/finalize_acceptance", post(handle_finalize_acceptance)) 358 .route( 359 "/event/upload-header", 360 post(upload_event_header).layer(DefaultBodyLimit::max(5 * 1024 * 1024)), // 5MB limit 361 ) 362 .route( 363 "/event/upload-thumbnail", 364 post(upload_event_thumbnail).layer(DefaultBodyLimit::max(5 * 1024 * 1024)), // 5MB limit 365 ) 366 .route("/ics/{*aturi}", get(handle_export_ics)) 367 .route("/{handle_slug}/ics", get(handle_event_ics)) 368 .route( 369 "/{handle_slug}/{event_rkey}/edit", 370 post(handle_edit_event_json), 371 ) 372 .route( 373 "/{handle_slug}/{event_rkey}/manage", 374 get(handle_manage_event), 375 ) 376 .route( 377 "/{handle_slug}/{event_rkey}/manage/content", 378 post(handle_manage_event_content_save), 379 ) 380 .route( 381 "/{handle_slug}/{event_rkey}/edit-settings", 382 post(handle_edit_settings), 383 ) 384 .route( 385 "/{handle_slug}/{event_rkey}/export-rsvps", 386 get(handle_export_rsvps), 387 ) 388 .route( 389 "/{handle_slug}/{event_rkey}/delete", 390 post(handle_delete_event), 391 ) 392 .route( 393 "/{handle_slug}/{event_rkey}/attendees", 394 get(handle_event_attendees), 395 ) 396 .route("/{handle_slug}/{event_rkey}", get(handle_view_event)) 397 .route("/{handle_slug}", get(handle_profile_view)) 398 .nest_service("/static", serve_dir.clone()) 399 .fallback_service(serve_dir) 400 .layer(( 401 TraceLayer::new_for_http() 402 .make_span_with(trace::DefaultMakeSpan::new().level(tracing::Level::INFO)) 403 .on_failure( 404 |err: ServerErrorsFailureClass, _latency: Duration, _span: &Span| { 405 tracing::error!(error = ?err, "Unhandled error: {err}"); 406 }, 407 ), 408 TimeoutLayer::with_status_code(StatusCode::REQUEST_TIMEOUT, Duration::from_secs(30)), 409 )) 410 .layer( 411 CorsLayer::new() 412 .allow_origin(tower_http::cors::Any) 413 .allow_methods([Method::GET]) 414 .allow_headers([ 415 ACCEPT_LANGUAGE, 416 ACCEPT, 417 CONTENT_TYPE, 418 HeaderName::from_lowercase(b"x-widget-version").unwrap(), 419 ]), 420 ) 421 .layer(AutoVaryLayer) 422 .with_state(web_context.clone()) 423}