//! Router assembly. Extracted from `main.rs` so integration tests can build the //! exact same app (routes, middleware, layers) in process against a test //! database instead of a live server. use axum::{ routing::{delete, get, post, put}, Router, }; use tower::ServiceBuilder; use tower_http::{cors::CorsLayer, trace::TraceLayer}; use crate::config::AppState; use crate::handlers; use crate::middleware; /// Compose the full Axum app: all public + protected routes, the JWT auth /// layer on protected routes, and the global middleware stack (client-IP /// resolution, security headers, rate limit, tracing, CORS). /// /// `main.rs` calls this and wraps it in `into_make_service_with_connect_info` /// before serving; tests call it and drive it with `oneshot`/`RouterExt`. pub fn build_app(state: AppState) -> Router { // Build public routes (no auth required) let public_routes = Router::new() .route( "/health", get(handlers::health_check).head(handlers::health_check), ) .route("/ready", get(handlers::ready_check)) .route("/api/auth/register", post(handlers::register)) .route("/api/auth/login", post(handlers::login)) .route("/api/auth/refresh", post(handlers::refresh)) .route("/api/auth/logout", post(handlers::logout)) .route( "/api/auth/recover-password", post(handlers::recover_password), ) .route("/api/auth/recovery-info", get(handlers::recovery_info)) // Recipient identity public key lookup (Phase B). Public keys are not // secret, so this is unauthenticated — mirrors recovery-info. .route("/api/users/public-key", get(handlers::get_public_key)); // Build protected routes (auth required) let protected_routes = Router::new() // User profile management .route("/api/users/me", get(handlers::get_account)) .route("/api/users/me", put(handlers::update_account)) .route("/api/users/me", delete(handlers::delete_account)) .route("/api/users/me/change-password", post(handlers::change_password)) // User settings .route("/api/users/me/settings", get(handlers::get_settings)) .route("/api/users/me/settings", put(handlers::update_settings)) // Profile management (Phase A2 multi-profile + Phase B sharing). // GET /api/profiles/:id is share-aware (admits recipients); the other // profile ops remain owner-only. .route("/api/profiles", get(handlers::list_profiles)) .route("/api/profiles", post(handlers::create_profile)) .route("/api/profiles/shared-with-me", get(handlers::list_shared_with_me)) .route("/api/profiles/:id", get(handlers::get_profile_shared_aware)) .route("/api/profiles/:id", put(handlers::update_profile)) .route("/api/profiles/:id", delete(handlers::delete_profile)) // Profile sharing (Phase B): owner manages shares; recipients read via // the gate inside the data handlers. .route( "/api/profiles/:id/shares", get(handlers::list_shares).post(handlers::create_share), ) .route("/api/profiles/:id/shares/:recipient", delete(handlers::delete_share)) // Session management (Phase 2.6) .route("/api/sessions", get(handlers::get_sessions)) .route("/api/sessions/:id", delete(handlers::revoke_session)) .route("/api/sessions/all", delete(handlers::revoke_all_sessions)) // Medication management (Phase 2.7) .route("/api/medications", post(handlers::create_medication)) .route("/api/medications", get(handlers::list_medications)) .route("/api/medications/:id", get(handlers::get_medication)) .route("/api/medications/:id", post(handlers::update_medication)) .route("/api/medications/:id/delete", post(handlers::delete_medication)) .route("/api/medications/:id/log", post(handlers::log_dose)) .route("/api/medications/:id/adherence", get(handlers::get_adherence)) // Health statistics management (Phase 2.7) .route("/api/health-stats", post(handlers::create_health_stat)) .route("/api/health-stats", get(handlers::list_health_stats)) .route("/api/health-stats/:id", get(handlers::get_health_stat)) .route("/api/health-stats/:id", put(handlers::update_health_stat)) .route("/api/health-stats/:id", delete(handlers::delete_health_stat)) // Drug interactions (Phase 2.8) .route("/api/interactions/check", post(handlers::check_interactions)) .route("/api/interactions/check-new", post(handlers::check_new_medication)) // Appointments .route("/api/appointments", post(handlers::create_appointment)) .route("/api/appointments", get(handlers::list_appointments)) .route("/api/appointments/:id", get(handlers::get_appointment)) .route("/api/appointments/:id", post(handlers::update_appointment)) .route("/api/appointments/:id/delete", post(handlers::delete_appointment)) .layer(axum::middleware::from_fn_with_state( state.clone(), middleware::jwt_auth_middleware, )); public_routes .merge(protected_routes) .with_state(state.clone()) .layer( ServiceBuilder::new() // Resolve the client IP once and stash it for handlers/middleware // (audit logging, account-lockout forensics). .layer(axum::middleware::from_fn( middleware::client_ip_middleware, )) // Security headers (applies to all responses) .layer(axum::middleware::from_fn( middleware::security_headers_middleware, )) // IP-based rate limiting (reads ClientIp from extensions). .layer(axum::middleware::from_fn_with_state( state.clone(), middleware::general_rate_limit_middleware, )) .layer(TraceLayer::new_for_http()) .layer(CorsLayer::permissive()), ) }