Skip to main content

max / makenotwork

18.6 KB · 494 lines History Blame Raw
1 //! The router: every synckit path, and the auth and rate-limit tier each one
2 //! sits behind.
3
4 use axum::routing::get;
5 use tower_governor::GovernorLayer;
6
7 use super::{apps, auth, billing, blobs, groups, keys, subscribe, sync};
8 use crate::{
9 AppState, constants,
10 csrf::{
11 CsrfRouter, delete_csrf, delete_csrf_skip, patch_csrf, post_csrf, post_csrf_skip, put_csrf,
12 put_csrf_skip,
13 },
14 };
15
16 /// Reason strings for synckit CSRF Skip routes. The auth_routes and
17 /// sync_routes blocks use server-to-server or JWT bearer auth with no
18 /// session cookie; CSRF doesn't apply. The app_routes block IS
19 /// session-authed (dashboard-driven) so those use `post_csrf` etc.
20 const SYNCKIT_API_KEY_SKIP: &str = "synckit server-to-server: api_key auth, no session";
21
22 const SYNCKIT_APP_SECRET_SKIP: &str =
23 "synckit server-to-server: keys-endpoint app_secret auth, no session";
24
25 const SYNCKIT_JWT_SKIP: &str = "synckit JWT bearer auth (SyncUser), no session";
26
27 /// Build the SyncKit route tree.
28 ///
29 /// Three route groups with different auth and rate-limiting strategies:
30 ///
31 /// - **Auth routes**: Public, rate-limited per-second (IP) to prevent
32 /// credential stuffing. `/api/sync/auth` and `/api/sync/validate-app` present
33 /// the app's api_key; the three server-to-server SDK key routes
34 /// (`keys/claim`, `keys/release`, `keys/list`) and `app/pricing` present the
35 /// app_secret in the body instead. Read the builder below for the current
36 /// membership.
37 /// - **Sync routes** (push, pull, status, devices, keys, blobs): JWT-based
38 /// auth via `SyncUser` extractor, dual rate-limited: per-IP (prevents single
39 /// client abuse) AND per-app (prevents one developer's app from starving
40 /// others). Per-app limits are higher since an app may have many users.
41 /// - **App management routes** (`/api/sync/apps/...`): Session-based auth
42 /// via `AuthUser` extractor (accessed from the MNW dashboard), no extra
43 /// rate limit beyond the global middleware.
44 ///
45 /// `synckit_jwt_secret` is threaded in (rather than read from a global) so the
46 /// per-app rate limiter's key extractor can verify token signatures; see
47 /// [`crate::rate_limit::SyncAppKeyExtractor`].
48 pub fn synckit_routes(synckit_jwt_secret: Option<std::sync::Arc<String>>) -> CsrfRouter<AppState> {
49 let auth_rate_limit = crate::helpers::rate_limiter_per_sec(
50 constants::SYNCKIT_AUTH_RATE_LIMIT_PER_SEC,
51 constants::SYNCKIT_AUTH_RATE_LIMIT_BURST,
52 );
53
54 let auth_routes = CsrfRouter::new()
55 .route(
56 "/api/sync/auth",
57 post_csrf_skip(SYNCKIT_API_KEY_SKIP, auth::sync_auth),
58 )
59 .route(
60 "/api/v1/sync/auth",
61 post_csrf_skip(SYNCKIT_API_KEY_SKIP, auth::sync_auth),
62 )
63 .route(
64 "/api/sync/validate-app",
65 post_csrf_skip(SYNCKIT_API_KEY_SKIP, auth::validate_app),
66 )
67 .route(
68 "/api/v1/sync/validate-app",
69 post_csrf_skip(SYNCKIT_API_KEY_SKIP, auth::validate_app),
70 )
71 // Server-to-server SDK key claim/release/list (app_secret in body, no JWT).
72 .route(
73 "/api/sync/keys/claim",
74 post_csrf_skip(SYNCKIT_APP_SECRET_SKIP, keys::claim),
75 )
76 .route(
77 "/api/v1/sync/keys/claim",
78 post_csrf_skip(SYNCKIT_APP_SECRET_SKIP, keys::claim),
79 )
80 .route(
81 "/api/sync/keys/release",
82 post_csrf_skip(SYNCKIT_APP_SECRET_SKIP, keys::release),
83 )
84 .route(
85 "/api/v1/sync/keys/release",
86 post_csrf_skip(SYNCKIT_APP_SECRET_SKIP, keys::release),
87 )
88 .route(
89 "/api/sync/keys/list",
90 post_csrf_skip(SYNCKIT_APP_SECRET_SKIP, keys::list),
91 )
92 .route(
93 "/api/v1/sync/keys/list",
94 post_csrf_skip(SYNCKIT_APP_SECRET_SKIP, keys::list),
95 )
96 .route(
97 "/api/sync/app/pricing",
98 post_csrf_skip(SYNCKIT_API_KEY_SKIP, sync::get_app_pricing),
99 )
100 .route(
101 "/api/v1/sync/app/pricing",
102 post_csrf_skip(SYNCKIT_API_KEY_SKIP, sync::get_app_pricing),
103 )
104 .route_layer(GovernorLayer::new(auth_rate_limit));
105
106 let sync_ip_rate_limit = crate::helpers::rate_limiter_ms(
107 constants::SYNCKIT_SYNC_RATE_LIMIT_MS,
108 constants::SYNCKIT_SYNC_RATE_LIMIT_BURST,
109 );
110 let sync_app_rate_limit = crate::helpers::synckit_app_rate_limiter_ms(
111 synckit_jwt_secret,
112 constants::SYNCKIT_APP_RATE_LIMIT_MS,
113 constants::SYNCKIT_APP_RATE_LIMIT_BURST,
114 );
115
116 let sync_routes = CsrfRouter::new()
117 .route(
118 "/api/sync/push",
119 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::sync_push),
120 )
121 .route(
122 "/api/v1/sync/push",
123 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::sync_push),
124 )
125 .route(
126 "/api/sync/pull",
127 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::sync_pull),
128 )
129 .route(
130 "/api/v1/sync/pull",
131 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::sync_pull),
132 )
133 // Group sync: shared changelogs. Membership/admin gating lives inside the
134 // handlers (SyncUser identifies the caller); same JWT auth + dual rate
135 // limit as personal sync. GET+POST on one path merge, as with devices.
136 .route(
137 "/api/sync/groups",
138 post_csrf_skip(SYNCKIT_JWT_SKIP, groups::create_group),
139 )
140 .route(
141 "/api/v1/sync/groups",
142 post_csrf_skip(SYNCKIT_JWT_SKIP, groups::create_group),
143 )
144 .route_get("/api/sync/groups", get(groups::list_groups))
145 .route_get("/api/v1/sync/groups", get(groups::list_groups))
146 .route(
147 "/api/sync/groups/{id}/members",
148 post_csrf_skip(SYNCKIT_JWT_SKIP, groups::add_member),
149 )
150 .route(
151 "/api/v1/sync/groups/{id}/members",
152 post_csrf_skip(SYNCKIT_JWT_SKIP, groups::add_member),
153 )
154 .route_get("/api/sync/groups/{id}/members", get(groups::list_members))
155 .route_get(
156 "/api/v1/sync/groups/{id}/members",
157 get(groups::list_members),
158 )
159 .route(
160 "/api/sync/groups/{id}/members/{user_id}",
161 delete_csrf_skip(SYNCKIT_JWT_SKIP, groups::remove_member),
162 )
163 .route(
164 "/api/v1/sync/groups/{id}/members/{user_id}",
165 delete_csrf_skip(SYNCKIT_JWT_SKIP, groups::remove_member),
166 )
167 // Invitations. The two accept-side routes are not nested under the group:
168 // the caller is not a member yet and cannot be asked for a group id they
169 // have no access to, so the token names the group instead.
170 .route(
171 "/api/sync/groups/{id}/invitations",
172 post_csrf_skip(SYNCKIT_JWT_SKIP, groups::create_invitation),
173 )
174 .route(
175 "/api/v1/sync/groups/{id}/invitations",
176 post_csrf_skip(SYNCKIT_JWT_SKIP, groups::create_invitation),
177 )
178 .route_get(
179 "/api/sync/groups/{id}/invitations",
180 get(groups::list_invitations),
181 )
182 .route_get(
183 "/api/v1/sync/groups/{id}/invitations",
184 get(groups::list_invitations),
185 )
186 .route(
187 "/api/sync/groups/{id}/invitations/{invitation_id}/confirm",
188 post_csrf_skip(SYNCKIT_JWT_SKIP, groups::confirm_invitation),
189 )
190 .route(
191 "/api/v1/sync/groups/{id}/invitations/{invitation_id}/confirm",
192 post_csrf_skip(SYNCKIT_JWT_SKIP, groups::confirm_invitation),
193 )
194 .route(
195 "/api/sync/groups/{id}/invitations/{invitation_id}",
196 delete_csrf_skip(SYNCKIT_JWT_SKIP, groups::revoke_invitation),
197 )
198 .route(
199 "/api/v1/sync/groups/{id}/invitations/{invitation_id}",
200 delete_csrf_skip(SYNCKIT_JWT_SKIP, groups::revoke_invitation),
201 )
202 .route_get(
203 "/api/sync/invitations/{token}",
204 get(groups::preview_invitation),
205 )
206 .route_get(
207 "/api/v1/sync/invitations/{token}",
208 get(groups::preview_invitation),
209 )
210 .route(
211 "/api/sync/invitations/accept",
212 post_csrf_skip(SYNCKIT_JWT_SKIP, groups::accept_invitation),
213 )
214 .route(
215 "/api/v1/sync/invitations/accept",
216 post_csrf_skip(SYNCKIT_JWT_SKIP, groups::accept_invitation),
217 )
218 .route_get("/api/sync/groups/{id}/grant", get(groups::get_grant))
219 .route_get("/api/v1/sync/groups/{id}/grant", get(groups::get_grant))
220 .route_get("/api/sync/groups/{id}/pubkeys", get(groups::list_pubkeys))
221 .route_get(
222 "/api/v1/sync/groups/{id}/pubkeys",
223 get(groups::list_pubkeys),
224 )
225 .route(
226 "/api/sync/groups/{id}/rotate",
227 post_csrf_skip(SYNCKIT_JWT_SKIP, groups::rotate_key),
228 )
229 .route(
230 "/api/v1/sync/groups/{id}/rotate",
231 post_csrf_skip(SYNCKIT_JWT_SKIP, groups::rotate_key),
232 )
233 .route(
234 "/api/sync/groups/{id}/push",
235 post_csrf_skip(SYNCKIT_JWT_SKIP, groups::group_push),
236 )
237 .route(
238 "/api/v1/sync/groups/{id}/push",
239 post_csrf_skip(SYNCKIT_JWT_SKIP, groups::group_push),
240 )
241 .route(
242 "/api/sync/groups/{id}/pull",
243 post_csrf_skip(SYNCKIT_JWT_SKIP, groups::group_pull),
244 )
245 .route(
246 "/api/v1/sync/groups/{id}/pull",
247 post_csrf_skip(SYNCKIT_JWT_SKIP, groups::group_pull),
248 )
249 .route_get("/api/sync/subscribe", get(subscribe::sync_subscribe))
250 .route_get("/api/v1/sync/subscribe", get(subscribe::sync_subscribe))
251 .route_get("/api/sync/status", get(sync::sync_status))
252 .route_get("/api/v1/sync/status", get(sync::sync_status))
253 .route_get("/api/sync/account", get(sync::sync_account))
254 .route_get("/api/v1/sync/account", get(sync::sync_account))
255 .route_get(
256 "/api/sync/subscription",
257 get(sync::sync_subscription_status),
258 )
259 .route_get(
260 "/api/v1/sync/subscription",
261 get(sync::sync_subscription_status),
262 )
263 .route(
264 "/api/sync/subscription/quote",
265 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::quote_subscription_price),
266 )
267 .route(
268 "/api/v1/sync/subscription/quote",
269 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::quote_subscription_price),
270 )
271 .route(
272 "/api/sync/subscription/checkout",
273 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::create_subscription_checkout),
274 )
275 .route(
276 "/api/v1/sync/subscription/checkout",
277 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::create_subscription_checkout),
278 )
279 .route(
280 "/api/sync/subscription/storage-cap",
281 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::queue_storage_cap_change),
282 )
283 .route(
284 "/api/v1/sync/subscription/storage-cap",
285 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::queue_storage_cap_change),
286 )
287 .route(
288 "/api/sync/devices",
289 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::register_device),
290 )
291 .route(
292 "/api/v1/sync/devices",
293 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::register_device),
294 )
295 .route_get("/api/sync/devices", get(sync::list_devices))
296 .route_get("/api/v1/sync/devices", get(sync::list_devices))
297 .route(
298 "/api/sync/devices/{id}",
299 delete_csrf_skip(SYNCKIT_JWT_SKIP, sync::delete_device),
300 )
301 .route(
302 "/api/v1/sync/devices/{id}",
303 delete_csrf_skip(SYNCKIT_JWT_SKIP, sync::delete_device),
304 )
305 .route(
306 "/api/sync/keys",
307 put_csrf_skip(SYNCKIT_JWT_SKIP, sync::put_sync_key),
308 )
309 .route(
310 "/api/v1/sync/keys",
311 put_csrf_skip(SYNCKIT_JWT_SKIP, sync::put_sync_key),
312 )
313 .route_get("/api/sync/keys", get(sync::get_sync_key))
314 .route_get("/api/v1/sync/keys", get(sync::get_sync_key))
315 .route(
316 "/api/sync/keys/rotate",
317 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::begin_rotation),
318 )
319 .route(
320 "/api/v1/sync/keys/rotate",
321 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::begin_rotation),
322 )
323 .route(
324 "/api/sync/keys/rotate",
325 delete_csrf_skip(SYNCKIT_JWT_SKIP, sync::cancel_rotation),
326 )
327 .route(
328 "/api/v1/sync/keys/rotate",
329 delete_csrf_skip(SYNCKIT_JWT_SKIP, sync::cancel_rotation),
330 )
331 .route(
332 "/api/sync/keys/rotate/entries",
333 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::rotation_entries),
334 )
335 .route(
336 "/api/v1/sync/keys/rotate/entries",
337 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::rotation_entries),
338 )
339 .route(
340 "/api/sync/keys/rotate/batch",
341 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::rotation_batch),
342 )
343 .route(
344 "/api/v1/sync/keys/rotate/batch",
345 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::rotation_batch),
346 )
347 .route(
348 "/api/sync/keys/rotate/complete",
349 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::complete_rotation),
350 )
351 .route(
352 "/api/v1/sync/keys/rotate/complete",
353 post_csrf_skip(SYNCKIT_JWT_SKIP, sync::complete_rotation),
354 )
355 .route(
356 "/api/sync/blobs/upload",
357 post_csrf_skip(SYNCKIT_JWT_SKIP, blobs::blob_upload_url),
358 )
359 .route(
360 "/api/v1/sync/blobs/upload",
361 post_csrf_skip(SYNCKIT_JWT_SKIP, blobs::blob_upload_url),
362 )
363 .route(
364 "/api/sync/blobs/multipart/start",
365 post_csrf_skip(SYNCKIT_JWT_SKIP, blobs::blob_multipart_start),
366 )
367 .route(
368 "/api/v1/sync/blobs/multipart/start",
369 post_csrf_skip(SYNCKIT_JWT_SKIP, blobs::blob_multipart_start),
370 )
371 .route(
372 "/api/sync/blobs/multipart/parts",
373 post_csrf_skip(SYNCKIT_JWT_SKIP, blobs::blob_multipart_parts),
374 )
375 .route(
376 "/api/v1/sync/blobs/multipart/parts",
377 post_csrf_skip(SYNCKIT_JWT_SKIP, blobs::blob_multipart_parts),
378 )
379 .route(
380 "/api/sync/blobs/multipart/complete",
381 post_csrf_skip(SYNCKIT_JWT_SKIP, blobs::blob_multipart_complete),
382 )
383 .route(
384 "/api/v1/sync/blobs/multipart/complete",
385 post_csrf_skip(SYNCKIT_JWT_SKIP, blobs::blob_multipart_complete),
386 )
387 .route(
388 "/api/sync/blobs/multipart/abort",
389 post_csrf_skip(SYNCKIT_JWT_SKIP, blobs::blob_multipart_abort),
390 )
391 .route(
392 "/api/v1/sync/blobs/multipart/abort",
393 post_csrf_skip(SYNCKIT_JWT_SKIP, blobs::blob_multipart_abort),
394 )
395 .route(
396 "/api/sync/blobs/confirm",
397 post_csrf_skip(SYNCKIT_JWT_SKIP, blobs::blob_confirm_upload),
398 )
399 .route(
400 "/api/v1/sync/blobs/confirm",
401 post_csrf_skip(SYNCKIT_JWT_SKIP, blobs::blob_confirm_upload),
402 )
403 .route(
404 "/api/sync/blobs/download",
405 post_csrf_skip(SYNCKIT_JWT_SKIP, blobs::blob_download_url),
406 )
407 .route(
408 "/api/v1/sync/blobs/download",
409 post_csrf_skip(SYNCKIT_JWT_SKIP, blobs::blob_download_url),
410 )
411 .route(
412 "/api/sync/blobs/{hash}",
413 delete_csrf_skip(SYNCKIT_JWT_SKIP, blobs::blob_delete),
414 )
415 .route(
416 "/api/v1/sync/blobs/{hash}",
417 delete_csrf_skip(SYNCKIT_JWT_SKIP, blobs::blob_delete),
418 )
419 // Per-app rate limit (inner layer runs first): prevents one developer's
420 // app from starving other apps. Extracts app ID from JWT payload.
421 .route_layer(GovernorLayer::new(sync_app_rate_limit))
422 // Per-IP rate limit (outer layer): prevents a single client from
423 // overwhelming the endpoint regardless of which app they claim.
424 .route_layer(GovernorLayer::new(sync_ip_rate_limit));
425
426 // App management endpoints use session auth (no extra rate limit beyond global)
427 let app_routes = CsrfRouter::new()
428 .route("/api/sync/apps", post_csrf(apps::create_app))
429 .route("/api/v1/sync/apps", post_csrf(apps::create_app))
430 .route_get("/api/sync/apps", get(apps::list_apps))
431 .route_get("/api/v1/sync/apps", get(apps::list_apps))
432 .route(
433 "/api/sync/apps/{id}/regenerate-key",
434 post_csrf(apps::regenerate_app_key),
435 )
436 .route(
437 "/api/v1/sync/apps/{id}/regenerate-key",
438 post_csrf(apps::regenerate_app_key),
439 )
440 .route(
441 "/api/sync/apps/{id}/keys-secret",
442 post_csrf(apps::regenerate_app_keys_secret),
443 )
444 .route(
445 "/api/v1/sync/apps/{id}/keys-secret",
446 post_csrf(apps::regenerate_app_keys_secret),
447 )
448 .route("/api/sync/apps/{id}/link", put_csrf(apps::update_app_link))
449 .route(
450 "/api/v1/sync/apps/{id}/link",
451 put_csrf(apps::update_app_link),
452 )
453 .route("/api/sync/apps/{id}/slug", put_csrf(apps::update_app_slug))
454 .route(
455 "/api/v1/sync/apps/{id}/slug",
456 put_csrf(apps::update_app_slug),
457 )
458 .route("/api/sync/apps/{id}", delete_csrf(apps::delete_app))
459 .route("/api/v1/sync/apps/{id}", delete_csrf(apps::delete_app))
460 // Developer billing (session auth, dashboard-driven).
461 .route(
462 "/api/sync/apps/{id}/billing/setup",
463 post_csrf(billing::setup),
464 )
465 .route(
466 "/api/v1/sync/apps/{id}/billing/setup",
467 post_csrf(billing::setup),
468 )
469 .route(
470 "/api/sync/apps/{id}/billing/activate",
471 post_csrf(billing::activate),
472 )
473 .route(
474 "/api/v1/sync/apps/{id}/billing/activate",
475 post_csrf(billing::activate),
476 )
477 .route("/api/sync/apps/{id}/billing", patch_csrf(billing::patch))
478 .route("/api/v1/sync/apps/{id}/billing", patch_csrf(billing::patch))
479 .route("/api/sync/apps/{id}/billing", delete_csrf(billing::cancel))
480 .route(
481 "/api/v1/sync/apps/{id}/billing",
482 delete_csrf(billing::cancel),
483 )
484 .route_get("/api/sync/apps/{id}/billing", get(billing::get))
485 .route_get("/api/v1/sync/apps/{id}/billing", get(billing::get))
486 .route_get("/api/sync/apps/{id}/billing/portal", get(billing::portal))
487 .route_get(
488 "/api/v1/sync/apps/{id}/billing/portal",
489 get(billing::portal),
490 );
491
492 auth_routes.merge(sync_routes).merge(app_routes)
493 }
494