routes.rs 156 lignes · 6511 octets
use axum::{
    Router,
    routing::{delete, get, post, put},
};
use sea_orm::DatabaseConnection;
use tower_http::services::ServeDir;

use crate::handlers::settings::{preferences, profile, security};
use crate::handlers::{
    admin_handler, error_page, health, home_handler, login_handler, root_handler, totp_handler,
    user_handler,
};

/// Build the **browser** family of UI routes (SSR, session-cookie driven).
///
/// These routes are meant to be mounted on the CSRF-protected channel of the
/// application builder (`with_ui` / `merge_browser_routes`). It covers the
/// landing page, public pages (login, password reset…), authenticated UI
/// pages (profile, settings), the admin panel, and the static asset service.
///
/// The machine API (`/api/v1/*`) is intentionally *not* included here; mount
/// it separately via [`api_routes`] on the machine channel.
pub fn browser_routes() -> Router<DatabaseConnection> {
    let public_routes = Router::new()
        .route("/login", get(login_handler::login_page))
        .route(
            "/resend-verification",
            get(login_handler::resend_verification_page),
        )
        .route("/forgot-password", get(login_handler::forgot_password_page))
        .route("/reset-password", get(login_handler::reset_password_page))
        .route(
            "/confirm-password-change",
            get(security::confirm_password_change),
        )
        .route("/health", get(health::health_check))
        .route("/login/2fa", get(login_handler::login_2fa_page));

    let ui_routes = Router::new()
        .route("/home", get(home_handler::home))
        .route("/profile", get(profile::get_profile_page))
        .route("/profile", put(profile::update_profile))
        .route("/settings/password", get(security::get_password_page))
        .route("/settings/password", put(security::change_password))
        .route(
            "/settings/password/pending",
            delete(security::cancel_pending_password_change),
        )
        .route("/settings/sessions", get(security::get_sessions_page))
        .route("/settings/sessions/{id}", delete(security::revoke_session))
        .route(
            "/settings/preferences",
            get(preferences::get_preferences_page),
        )
        .route(
            "/settings/preferences",
            put(preferences::update_preferences),
        )
        .route("/settings/security", get(totp_handler::security_page))
        .route(
            "/settings/security/totp/enable",
            post(totp_handler::enable_totp),
        )
        .route(
            "/settings/security/totp/verify",
            post(totp_handler::verify_totp),
        )
        .route(
            "/settings/security/totp/disable",
            post(totp_handler::disable_totp),
        )
        .route(
            "/settings/security/backup-codes",
            post(totp_handler::regenerate_backup_codes),
        );

    let admin_routes = Router::new()
        .route("/admin", get(admin_handler::admin_dashboard))
        .route("/admin/users", get(admin_handler::list_users))
        .route("/admin/users/create", get(admin_handler::create_user_form))
        .route("/admin/users", post(admin_handler::create_user))
        .route("/admin/users/{id}", get(admin_handler::edit_user_form))
        .route("/admin/users/{id}", put(admin_handler::update_user))
        .route("/admin/users/{id}", delete(admin_handler::delete_user))
        .route(
            "/admin/users/{id}/reset-password",
            post(admin_handler::reset_user_password),
        )
        .route(
            "/admin/users/{id}/reset-password-email",
            post(admin_handler::admin_reset_password_by_email),
        )
        .route("/admin/roles", get(admin_handler::list_roles))
        .route("/admin/roles/create", get(admin_handler::create_role_form))
        .route("/admin/roles", post(admin_handler::create_role))
        .route("/admin/roles/{id}", get(admin_handler::edit_role_form))
        .route("/admin/roles/{id}", put(admin_handler::update_role))
        .route("/admin/roles/{id}", delete(admin_handler::delete_role))
        .route("/admin/settings", get(admin_handler::get_app_settings))
        .route("/admin/settings", put(admin_handler::update_app_settings))
        .route("/admin/errors", get(admin_handler::admin_errors))
        .route("/admin/logs", get(admin_handler::admin_logs))
        .route(
            "/admin/users/{id}/reset-totp",
            post(admin_handler::reset_user_totp),
        );

    let static_dir =
        std::env::var("RUSTWARDEN_STATIC_DIR").unwrap_or_else(|_| "static".to_string());

    tracing::info!("Serving static files from: {}", static_dir);
    let static_service = ServeDir::new(&static_dir);

    Router::new()
        .route("/", get(root_handler::root_redirect))
        .merge(public_routes)
        .merge(ui_routes)
        .merge(admin_routes)
        .nest_service("/static", static_service)
        .fallback(error_page::not_found)
}

/// Build the **machine** family of API routes (`/api/v1/*`).
///
/// These are JSON endpoints authenticated by `Authorization: Bearer` (or
/// pre-session for login/register) and are *not* CSRF-vulnerable. Mount them
/// on the machine channel of the application builder
/// (`merge_machine_routes`), never behind the CSRF layer.
pub fn api_routes() -> Router<DatabaseConnection> {
    // Auth endpoints are provided canonically by the framework core
    // (`rustwarden_core::auth::auth_api_routes`); mount that set and add the
    // UI crate's own user-management API on top.
    let user_routes = Router::new()
        .route("/api/v1/users", post(user_handler::create_user))
        .route("/api/v1/users", get(user_handler::list_users));

    Router::new()
        .merge(rustwarden_core::auth::auth_api_routes())
        .merge(user_routes)
}

/// Create the main application router for Rustwarden.
///
/// Combines [`browser_routes`] and [`api_routes`] into a single router. This
/// puts `/api/v1/*` and the SSR UI in one tree; prefer mounting the two
/// families on their dedicated channels (`with_ui` + `merge_machine_routes`)
/// so the CSRF layer covers the browser family only.
pub fn create_router() -> Router<DatabaseConnection> {
    browser_routes().merge(api_routes())
}

/// Returns a minimal router with only the health-check route, used in tests.
#[allow(dead_code)]
pub fn create_test_router() -> Router {
    Router::new().route("/health", get(health::health_check))
}