forked from
smokesignal.events/smokesignal
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_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}