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