The smokesignal.events web application
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}