routes.rs 101 lignes · 3987 octets
//! Default federation UI router.
//!
//! Mounts every public, authenticated and admin endpoint shipped by
//! this crate. Mirrors the previous `cfg(feature = "fediverse")` blocks
//! that lived in `rustwarden-ui` so wiring stays a one-liner:
//! `builder.merge_routes(rustwarden_fediverse_ui::default_routes())`.

use axum::Router;
use axum::extract::DefaultBodyLimit;
use axum::routing::{get, post};
use sea_orm::DatabaseConnection;
use tower::limit::ConcurrencyLimitLayer;

use crate::handlers::{admin, user_auth};

/// Maximum concurrent SSE connections to `/login/fediverse/sse/{id}`
/// (ANSSI FUI-DOS-1). The endpoint is unauthenticated and holds a DB
/// connection for up to 5 minutes per challenge — capping concurrency
/// prevents pool exhaustion by a single attacker.
const SSE_MAX_CONCURRENT: usize = 100;

/// Maximum body size accepted on `POST /login/fediverse` (ANSSI
/// FUI-NET-1). 8 KiB is far above any legitimate login form payload
/// (handle field ~100 bytes + CSRF token ~64 bytes + a few hidden
/// inputs) and bounds the DoS surface on this unauthenticated route.
const LOGIN_FORM_BODY_LIMIT_BYTES: usize = 8 * 1024;

/// Returns the default federation UI routes (public, authenticated and
/// admin) wired against `DatabaseConnection` state.
pub fn default_routes() -> Router<DatabaseConnection> {
    Router::new()
        // Federation admin hub (entry point for the six sub-sections).
        .route("/admin/fediverse", get(admin::get_hub))
        .route("/admin/fediverse/teams", get(admin::get_teams_for_invites))
        // Public routes (RFC 0023 Slice B).
        .route("/login/fediverse/error", get(user_auth::error))
        .route(
            "/login/fediverse",
            get(user_auth::login_form)
                .post(user_auth::login_initiate)
                .layer(DefaultBodyLimit::max(LOGIN_FORM_BODY_LIMIT_BYTES)),
        )
        .route(
            "/login/fediverse/waiting/{challenge_id}",
            get(user_auth::waiting),
        )
        .route(
            "/login/fediverse/sse/{challenge_id}",
            get(user_auth::sse_status).layer(ConcurrencyLimitLayer::new(SSE_MAX_CONCURRENT)),
        )
        // Authenticated routes (consent, invite, connected apps).
        .route(
            "/fediverse/consent/{challenge_id}",
            get(user_auth::consent_form).post(user_auth::consent_submit),
        )
        .route("/profile/connected_apps", get(user_auth::connected_apps))
        .route(
            "/fediverse/invite/{invite_id}",
            get(user_auth::invite_landing_form).post(user_auth::invite_landing_submit),
        )
        // Admin routes (RFC 0021 Slice A).
        .route(
            "/admin/fediverse/settings",
            get(admin::get_settings).post(admin::update_settings),
        )
        .route("/admin/fediverse/trust_list", get(admin::get_trust_list))
        .route(
            "/admin/fediverse/trust_list/add",
            post(admin::add_trust_entry),
        )
        .route(
            "/admin/fediverse/trust_list/delete",
            post(admin::delete_trust_entry),
        )
        .route("/admin/fediverse/queue", get(admin::get_queue))
        .route(
            "/admin/fediverse/sessions_remote",
            get(admin::get_sessions_remote),
        )
        .route(
            "/admin/fediverse/sessions_remote/revoke",
            post(admin::revoke_remote_session),
        )
        .route(
            "/admin/fediverse/sessions_remote/kill_domain",
            post(admin::kill_domain),
        )
        .route("/admin/fediverse/nodeinfo", get(admin::get_nodeinfo))
        .route(
            "/admin/fediverse/invites/{team_id}",
            get(admin::get_team_invites),
        )
        .route(
            "/admin/fediverse/invites/{team_id}/add",
            post(admin::create_team_invite),
        )
        .route(
            "/admin/fediverse/invites/{team_id}/revoke",
            post(admin::revoke_team_invite),
        )
}