// SPDX-License-Identifier: AGPL-3.0-or-later use crate::{ api::client::ApiResultExt, middleware::{auth::AuthContext, csrf::CsrfToken}, state::AppState, templates, }; use axum::{ Router, extract::{Query, State}, response::{Html, IntoResponse, Response}, routing::get, }; use serde::Deserialize; use super::system_actions; #[derive(Deserialize)] struct GatewayQuery { leaderboard_limit: Option, node_stats: Option, } #[derive(Deserialize)] struct AuditLogsQuery { q: Option, admin_user_id: Option, target_id: Option, target_type: Option, access: Option, sort_by: Option, sort_order: Option, limit: Option, page: Option, } #[derive(Deserialize)] struct SearchIndexQuery { job_id: Option, } #[derive(Deserialize)] struct LimitConfigQuery { rule: Option, } pub fn router() -> Router { Router::new() .route( "/gateway", get(gateway_page).post(system_actions::gateway_post), ) .route("/audit-logs", get(audit_logs_page)) .route( "/search-index", get(search_index_page).post(system_actions::search_index_post), ) .route( "/instance-config", get(instance_config_page).post(system_actions::instance_config_post), ) .route( "/limit-config", get(limit_config_page).post(system_actions::limit_config_post), ) .route("/strange-place", get(strange_place_page)) } async fn gateway_page( State(state): State, auth: axum::Extension, csrf: axum::Extension, Query(query): Query, ) -> Response { let config = state.config(); let node_stats_expanded = query.node_stats.as_deref() == Some("expanded"); let leaderboard_limit: u32 = query .leaderboard_limit .as_deref() .and_then(|s| s.parse().ok()) .unwrap_or(100); let client = crate::api::client::AdminApiClient::new(state.http_client(), config, &auth.0.session); let node_stats = client .get_node_stats() .await .log_error("load gateway node stats"); let guild_stats = client .get_guild_memory_stats(leaderboard_limit) .await .log_error("load gateway guild memory stats"); let voice_counts = client .get_gateway_voice_state_counts() .await .log_error("load gateway voice state counts"); let gw_params = templates::pages::gateway::GatewayPageParams { csrf_token: &csrf.0.0, node_stats: node_stats.as_ref(), voice_counts: voice_counts.as_ref(), guild_stats: guild_stats.as_ref(), reload_result: None, node_stats_expanded, leaderboard_limit, }; let markup = templates::pages::gateway::gateway_page(config, &auth.0, &gw_params); Html(markup.into_string()).into_response() } async fn audit_logs_page( State(state): State, auth: axum::Extension, Query(query): Query, ) -> Response { let config = state.config(); let limit = query.limit.unwrap_or(50); let current_page = query.page.unwrap_or(0); let params = templates::pages::audit_logs::AuditLogsParams { query: query.q.as_deref().unwrap_or(""), admin_user_id: query.admin_user_id.as_deref().unwrap_or(""), target_id: query.target_id.as_deref().unwrap_or(""), target_type: query.target_type.as_deref().unwrap_or(""), access: query.access.as_deref().unwrap_or(""), sort_by: query.sort_by.as_deref().unwrap_or("createdAt"), sort_order: query.sort_order.as_deref().unwrap_or("desc"), limit, current_page, }; let client = crate::api::client::AdminApiClient::new(state.http_client(), config, &auth.0.session); let search_params = crate::api::audit::SearchAuditLogsParams { query: nonempty(params.query), admin_user_id: nonempty(params.admin_user_id), target_id: nonempty(params.target_id), target_type: nonempty(params.target_type), access: nonempty(params.access), sort_by: Some(params.sort_by.to_owned()), sort_order: Some(params.sort_order.to_owned()), limit, offset: u64::from(current_page) * u64::from(limit), }; let result = client .search_audit_logs(&search_params) .await .log_error("load audit logs"); let markup = templates::pages::audit_logs::audit_logs_page(config, &auth.0, ¶ms, result.as_ref()); Html(markup.into_string()).into_response() } async fn search_index_page( State(state): State, auth: axum::Extension, csrf: axum::Extension, Query(query): Query, ) -> Response { let config = state.config(); let job_id_str = query.job_id.unwrap_or_default(); let mut status: Option = None; let mut total: Option = None; let mut indexed: Option = None; if !job_id_str.is_empty() { let client = crate::api::client::AdminApiClient::new(state.http_client(), config, &auth.0.session); match client.get_index_refresh_status(&job_id_str).await { Ok(resp) => match resp { crate::api::types::IndexRefreshStatusResponse::NotFound { status: s } => { status = Some(s); } crate::api::types::IndexRefreshStatusResponse::Progress { status: s, total: t, indexed: i, .. } => { status = Some(s); total = t; indexed = i; } }, Err(error) => { tracing::warn!(%error, job_id = job_id_str, "admin API request failed: get index refresh status"); } } } let refresh = if !job_id_str.is_empty() { Some(templates::pages::search_index::RefreshStatus { job_id: &job_id_str, status: status.as_deref(), total, indexed, started_at: None, completed_at: None, error: None, }) } else { None }; let markup = templates::pages::search_index::search_index_page( config, &auth.0, &csrf.0.0, refresh.as_ref(), ); Html(markup.into_string()).into_response() } async fn instance_config_page( State(state): State, auth: axum::Extension, csrf: axum::Extension, ) -> Response { let config = state.config(); let client = crate::api::client::AdminApiClient::new(state.http_client(), config, &auth.0.session); let instance_config = client .get_instance_config() .await .log_error("load instance config"); if let Some(instance_config) = &instance_config { state.remember_premium_branding(crate::api::types::PremiumBranding::from_instance_config( instance_config, )); } let limit_config = client .get_limit_config() .await .log_error("load limit config"); let markup = templates::pages::instance_config::instance_config_page( config, &auth.0, &csrf.0.0, instance_config.as_ref(), limit_config.as_ref(), ); Html(markup.into_string()).into_response() } async fn limit_config_page( State(state): State, auth: axum::Extension, csrf: axum::Extension, Query(query): Query, ) -> Response { let config = state.config(); let client = crate::api::client::AdminApiClient::new(state.http_client(), config, &auth.0.session); let limit_config = client .get_limit_config() .await .log_error("load limit config page"); let markup = templates::pages::limit_config::limit_config_page( config, &auth.0, &csrf.0.0, query.rule.as_deref(), limit_config.as_ref(), ); Html(markup.into_string()).into_response() } async fn strange_place_page( State(state): State, auth: axum::Extension, ) -> Response { let config = state.config(); let markup = templates::pages::strange_place::strange_place_page(config, &auth.0); Html(markup.into_string()).into_response() } fn nonempty(s: &str) -> Option { if s.is_empty() { None } else { Some(s.to_owned()) } }