//! Handlers triggered by clicking email links, plus the forms that initiate them. mod account; mod links; mod password; use axum::routing::get; use tower_governor::GovernorLayer; use crate::{ AppState, constants, csrf::{CsrfRouter, with_csrf, with_csrf_skip}, helpers::rate_limiter_ms, }; /// Register email action routes. /// /// Returns a `CsrfRouter` so each mutating route MUST declare a CSRF posture, /// the group can no longer be merged into the page tree as a bare `Router` that /// silently skips the envelope (the Run #16/#17 CHRONIC: `/forgot-password` /// rendered a CSRF token it never validated). Postures: /// - `/forgot-password`, auto-validated. It carries no signed-link token, so /// the session CSRF token IS its protection. /// - `/reset-password`, `/confirm-delete`, `/unsubscribe`, CSRF skip: each /// carries HMAC signed-link fields (user/expires/sig) re-validated in the /// handler, which an attacker cannot forge. That signature is the CSRF /// defense; a session token would add nothing for a logged-out email-link /// flow. /// - `/verify-email`, `/login-link`, GET-only (read-method, no posture). /// /// Every route is unauthenticated (reached from email links or pre-login forms) /// so the whole router carries one per-IP auth rate limiter. The tokens are /// 256-bit CSPRNG / HMAC so this was never a brute-force risk, the cap closes /// the abuse/amplification angle. Burst 5 + 500ms replenish comfortably covers /// the legitimate forgot -> reset -> login click sequence (~3 requests). pub(super) fn email_action_routes() -> CsrfRouter { let auth_rate_limit = rate_limiter_ms( constants::AUTH_RATE_LIMIT_MS, constants::AUTH_RATE_LIMIT_BURST, ); CsrfRouter::new() .route( "/forgot-password", with_csrf(get(password::forgot_password_page).post(password::forgot_password_handler)), ) .route( "/reset-password", with_csrf_skip( "signed link: user/expires/sig HMAC re-validated in handler", get(password::reset_password_page).post(password::reset_password_handler), ), ) .route_get("/verify-email", get(links::verify_email_handler)) .route_get("/login-link", get(links::login_link_handler)) .route( "/confirm-delete", with_csrf_skip( "signed link: user/expires/sig HMAC re-validated in handler", get(account::confirm_delete_page).post(account::confirm_delete_handler), ), ) .route( "/unsubscribe", with_csrf_skip( "signed link: sig HMAC verified in handler", get(account::unsubscribe_page).post(account::unsubscribe_handler), ), ) .layer(GovernorLayer::new(auth_rate_limit)) }