Skip to main content

max / makenotwork

1.9 KB · 50 lines History Blame Raw
1 //! Handlers triggered by clicking email links, plus the forms that initiate them.
2
3 mod account;
4 mod links;
5 mod password;
6
7 use axum::{routing::get, Router};
8 use tower_governor::GovernorLayer;
9
10 use crate::{constants, helpers::rate_limiter_ms, AppState};
11
12 /// Register email action routes.
13 ///
14 /// Every route here is unauthenticated (they're reached from email links or the
15 /// pre-login forms that trigger them), so the whole router carries one per-IP
16 /// auth rate limiter applied via `.layer`. Previously only `/forgot-password`
17 /// was capped; `/reset-password`, `/login-link`, `/verify-email`,
18 /// `/confirm-delete`, and `/unsubscribe` were uncapped, leaving a
19 /// DoS/email-amplification surface (Run #11 Security MINOR). The tokens are
20 /// 256-bit CSPRNG / HMAC so this was never a brute-force risk — the cap closes
21 /// the abuse/amplification angle. Burst 5 + 500ms replenish comfortably covers
22 /// the legitimate forgot -> reset -> login click sequence (~3 requests).
23 pub fn email_action_routes() -> Router<AppState> {
24 let auth_rate_limit =
25 rate_limiter_ms(constants::AUTH_RATE_LIMIT_MS, constants::AUTH_RATE_LIMIT_BURST);
26
27 Router::new()
28 .route(
29 "/forgot-password",
30 get(password::forgot_password_page).post(password::forgot_password_handler),
31 )
32 .route(
33 "/reset-password",
34 get(password::reset_password_page).post(password::reset_password_handler),
35 )
36 .route("/verify-email", get(links::verify_email_handler))
37 .route("/login-link", get(links::login_link_handler))
38 .route(
39 "/confirm-delete",
40 get(account::confirm_delete_page).post(account::confirm_delete_handler),
41 )
42 .route(
43 "/unsubscribe",
44 get(account::unsubscribe_page).post(account::unsubscribe_handler),
45 )
46 .layer(GovernorLayer {
47 config: auth_rate_limit,
48 })
49 }
50