use std::ops::{Div, Mul}; use axum::Json; use axum::extract::{Path, State}; use sea_orm::{ ColumnTrait, Condition, EntityTrait, IntoActiveModel, ModelTrait, Order, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, Set, TryIntoModel, }; use serde::Deserialize; use validator::Validate; use crate::api::{ error::{ApiError, ApiResult}, models::*, }; use crate::db::entity::{self, health_records, shared_nodes}; use crate::db::{Db, operations::*}; use crate::health_checker_manager::HealthCheckerManager; use axum_extra::extract::Query; use std::sync::Arc; #[derive(Clone)] pub struct AppState { pub db: Db, pub health_checker_manager: Arc, } pub async fn health_check() -> Json> { Json(ApiResponse::message("Service is healthy".to_string())) } pub async fn get_nodes( State(app_state): State, Query(pagination): Query, Query(filters): Query, ) -> ApiResult>>> { let page = pagination.page.unwrap_or(1); let per_page = pagination.per_page.unwrap_or(20); let offset = (page - 1) * per_page; let mut query = entity::shared_nodes::Entity::find(); // 普通用户只能看到已审核的节点 query = query.filter(entity::shared_nodes::Column::IsApproved.eq(true)); if let Some(is_active) = filters.is_active { query = query.filter(entity::shared_nodes::Column::IsActive.eq(is_active)); } if let Some(protocol) = filters.protocol { query = query.filter(entity::shared_nodes::Column::Protocol.eq(protocol)); } if let Some(search) = filters.search { query = query.filter( sea_orm::Condition::any() .add(entity::shared_nodes::Column::Name.contains(&search)) .add(entity::shared_nodes::Column::Host.contains(&search)) .add(entity::shared_nodes::Column::Description.contains(&search)), ); } // 标签过滤(支持单标签与多标签 OR) let mut filtered_ids: Option> = None; if !filters.tags.is_empty() { let ids_any = NodeOperations::filter_node_ids_by_tags_any(&app_state.db, &filters.tags).await?; filtered_ids = match filtered_ids { Some(mut existing) => { // 合并去重 existing.extend(ids_any); existing.sort(); existing.dedup(); Some(existing) } None => Some(ids_any), }; } if let Some(ids) = filtered_ids { if ids.is_empty() { return Ok(Json(ApiResponse::success(PaginatedResponse { items: vec![], total: 0, page, per_page, total_pages: 0, }))); } query = query.filter(entity::shared_nodes::Column::Id.is_in(ids)); } let total = query.clone().count(app_state.db.orm_db()).await?; let nodes = query .order_by_asc(entity::shared_nodes::Column::Id) .limit(Some(per_page as u64)) .offset(Some(offset as u64)) .all(app_state.db.orm_db()) .await?; let mut node_responses: Vec = nodes.into_iter().map(NodeResponse::from).collect(); let total_pages = total.div_ceil(per_page as u64); // 补充标签 let ids: Vec = node_responses.iter().map(|n| n.id).collect(); let tags_map = NodeOperations::get_nodes_tags_map(&app_state.db, &ids).await?; for n in &mut node_responses { n.tags = tags_map.get(&n.id).cloned().unwrap_or_default(); } // 为每个节点添加健康状态信息 for node_response in &mut node_responses { if let Some(mut health_record) = app_state .health_checker_manager .get_node_memory_record(node_response.id) { node_response.current_health_status = Some(health_record.get_current_health_status().to_string()); node_response.last_check_time = Some(health_record.get_last_check_time()); node_response.last_response_time = health_record.get_last_response_time(); // 获取24小时健康统计 if let Some(stats) = app_state .health_checker_manager .get_node_health_stats(node_response.id, 24) { node_response.health_percentage_24h = Some(stats.health_percentage); } let (total_ring, healthy_ring) = health_record.get_counter_ring(); node_response.health_record_total_counter_ring = total_ring; node_response.health_record_healthy_counter_ring = healthy_ring; node_response.ring_granularity = health_record.get_ring_granularity(); } } // remove sensitive information node_responses.iter_mut().for_each(|node| { node.network_name = None; node.network_secret = None; // make cur connection and max conn round to percentage if node.max_connections != 0 { node.current_connections = node.current_connections.mul(100).div(node.max_connections); node.max_connections = 100; } else { node.current_connections = 0; node.max_connections = 0; } node.wechat = None; node.qq_number = None; node.mail = None; }); Ok(Json(ApiResponse::success(PaginatedResponse { items: node_responses, total, page, per_page, total_pages: total_pages as u32, }))) } pub async fn create_node( State(app_state): State, Json(request): Json, ) -> ApiResult>> { request.validate()?; let node = NodeOperations::create_node(&app_state.db, request).await?; Ok(Json(ApiResponse::success(NodeResponse::from(node)))) } pub async fn test_connection( State(app_state): State, Json(request): Json, ) -> ApiResult>> { let mut node = NodeOperations::create_node_model(request); node.id = Set(0); let node = node.try_into_model()?; app_state .health_checker_manager .test_connection(&node, std::time::Duration::from_secs(5)) .await .map_err(|e| ApiError::Internal(e.to_string()))?; Ok(Json(ApiResponse::success(NodeResponse::from(node)))) } pub async fn get_node( State(app_state): State, Path(id): Path, ) -> ApiResult>> { let node = NodeOperations::get_node_by_id(&app_state.db, id) .await? .ok_or_else(|| ApiError::NotFound(format!("Node with id {} not found", id)))?; let mut resp = NodeResponse::from(node); resp.tags = NodeOperations::get_node_tags(&app_state.db, resp.id).await?; Ok(Json(ApiResponse::success(resp))) } pub async fn get_node_health( State(app_state): State, Path(node_id): Path, Query(pagination): Query, Query(filters): Query, ) -> ApiResult>>> { let page = pagination.page.unwrap_or(1); let per_page = pagination.per_page.unwrap_or(20); let offset = (page - 1) * per_page; let mut query = entity::health_records::Entity::find() .filter(entity::health_records::Column::NodeId.eq(node_id)); if let Some(status) = filters.status { query = query.filter(entity::health_records::Column::Status.eq(status)); } if let Some(since) = filters.since { query = query.filter(entity::health_records::Column::CheckedAt.gte(since.naive_utc())); } let total = query.clone().count(app_state.db.orm_db()).await?; let records = query .order_by_desc(entity::health_records::Column::CheckedAt) .limit(Some(per_page as u64)) .offset(Some(offset as u64)) .all(app_state.db.orm_db()) .await?; let record_responses: Vec = records .into_iter() .map(HealthRecordResponse::from) .collect(); let total_pages = total.div_ceil(per_page as u64); Ok(Json(ApiResponse::success(PaginatedResponse { items: record_responses, total, page, per_page, total_pages: total_pages as u32, }))) } pub async fn get_node_health_stats( State(app_state): State, Path(node_id): Path, Query(params): Query, ) -> ApiResult>> { let hours = params.hours.unwrap_or(24); let stats = HealthOperations::get_health_stats(&app_state.db, node_id, hours).await?; Ok(Json(ApiResponse::success(HealthStatsResponse::from(stats)))) } #[derive(Debug, Deserialize)] pub struct HealthStatsParams { pub hours: Option, } #[derive(Debug, Deserialize)] pub struct InstanceFilterParams { pub node_id: Option, pub status: Option, } // 管理员相关处理器 use crate::config::AppConfig; use axum::http::{HeaderMap, StatusCode}; use chrono::{Duration, Utc}; use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode}; use serde::Serialize; #[derive(Debug, Serialize, Deserialize)] struct AdminClaims { sub: String, exp: usize, iat: usize, } pub async fn get_node_connect_url( State(app_state): State, Path(id): Path, ) -> ApiResult { let node = NodeOperations::get_node_by_id(&app_state.db, id) .await? .ok_or_else(|| ApiError::NotFound(format!("Node with id {} not found", id)))?; let connect_url = format!("{}://{}:{}", node.protocol, node.host, node.port); Ok(connect_url) } pub async fn admin_login( Json(request): Json, ) -> ApiResult>> { request .validate() .map_err(|e| ApiError::Validation(e.to_string()))?; let config = AppConfig::default(); if request.password != config.security.admin_password { return Err(ApiError::Unauthorized("Invalid password".to_string())); } let now = Utc::now(); let expires_at = now + Duration::hours(24); let claims = AdminClaims { sub: "admin".to_string(), exp: expires_at.timestamp() as usize, iat: now.timestamp() as usize, }; let token = encode( &Header::default(), &claims, &EncodingKey::from_secret(config.security.jwt_secret.as_ref()), ) .map_err(|e| ApiError::Internal(format!("Token generation failed: {}", e)))?; Ok(Json(ApiResponse::success(AdminLoginResponse { token, expires_at, }))) } pub async fn admin_get_nodes( State(app_state): State, Query(pagination): Query, Query(filters): Query, headers: HeaderMap, ) -> ApiResult>>> { verify_admin_token(&headers)?; let page = pagination.page.unwrap_or(1); let per_page = pagination.per_page.unwrap_or(200); let offset = (page - 1) * per_page; let mut query = entity::shared_nodes::Entity::find(); if let Some(is_active) = filters.is_active { query = query.filter(entity::shared_nodes::Column::IsActive.eq(is_active)); } if let Some(is_approved) = filters.is_approved { query = query.filter(entity::shared_nodes::Column::IsApproved.eq(is_approved)); } if let Some(protocol) = filters.protocol { query = query.filter(entity::shared_nodes::Column::Protocol.eq(protocol)); } if let Some(search) = filters.search { query = query.filter( sea_orm::Condition::any() .add(entity::shared_nodes::Column::Name.contains(&search)) .add(entity::shared_nodes::Column::Host.contains(&search)) .add(entity::shared_nodes::Column::Description.contains(&search)), ); } // 标签过滤(支持单标签与多标签 OR) let mut filtered_ids: Option> = None; if let Some(tag) = filters.tag { let ids = NodeOperations::filter_node_ids_by_tag(&app_state.db, &tag).await?; filtered_ids = Some(ids); } if let Some(tags) = filters.tags && !tags.is_empty() { let ids_any = NodeOperations::filter_node_ids_by_tags_any(&app_state.db, &tags).await?; filtered_ids = match filtered_ids { Some(mut existing) => { existing.extend(ids_any); existing.sort(); existing.dedup(); Some(existing) } None => Some(ids_any), }; } if let Some(ids) = filtered_ids { if ids.is_empty() { return Ok(Json(ApiResponse::success(PaginatedResponse { items: vec![], total: 0, page, per_page, total_pages: 0, }))); } query = query.filter(entity::shared_nodes::Column::Id.is_in(ids)); } let total = query.clone().count(app_state.db.orm_db()).await?; let nodes = query .order_by(entity::shared_nodes::Column::CreatedAt, Order::Desc) .offset(offset as u64) .limit(per_page as u64) .all(app_state.db.orm_db()) .await?; let mut node_responses: Vec = nodes.into_iter().map(NodeResponse::from).collect(); // 补充标签 let ids: Vec = node_responses.iter().map(|n| n.id).collect(); let tags_map = NodeOperations::get_nodes_tags_map(&app_state.db, &ids).await?; for n in &mut node_responses { n.tags = tags_map.get(&n.id).cloned().unwrap_or_default(); } let total_pages = (total as f64 / per_page as f64).ceil() as u32; Ok(Json(ApiResponse::success(PaginatedResponse { items: node_responses, total, page, per_page, total_pages, }))) } pub async fn admin_approve_node( State(app_state): State, Path(id): Path, headers: HeaderMap, ) -> ApiResult>> { verify_admin_token(&headers)?; let node = entity::shared_nodes::Entity::find_by_id(id) .one(app_state.db.orm_db()) .await? .ok_or_else(|| ApiError::NotFound("Node not found".to_string()))?; let mut active_model = node.into_active_model(); active_model.is_approved = sea_orm::Set(true); let updated_node = entity::shared_nodes::Entity::update(active_model) .exec(app_state.db.orm_db()) .await?; let mut resp = NodeResponse::from(updated_node); resp.tags = NodeOperations::get_node_tags(&app_state.db, resp.id).await?; Ok(Json(ApiResponse::success(resp))) } pub async fn admin_update_node( State(app_state): State, Path(id): Path, headers: HeaderMap, Json(request): Json, ) -> ApiResult>> { verify_admin_token(&headers)?; request.validate()?; let mut node = NodeOperations::get_node_by_id(&app_state.db, id) .await? .ok_or_else(|| ApiError::NotFound(format!("Node with id {} not found", id)))?; let mut node = node.into_active_model(); if let Some(name) = request.name { node.name = Set(name); } if let Some(host) = request.host { node.host = Set(host); } if let Some(port) = request.port { node.port = Set(port); } if let Some(protocol) = request.protocol { node.protocol = Set(protocol); } if let Some(description) = request.description { node.description = Set(description); } if let Some(max_connections) = request.max_connections { node.max_connections = Set(max_connections); } if let Some(is_active) = request.is_active { node.is_active = Set(is_active); } if let Some(allow_relay) = request.allow_relay { node.allow_relay = Set(allow_relay); } if let Some(network_name) = request.network_name { node.network_name = Set(network_name); } if let Some(network_secret) = request.network_secret { node.network_secret = Set(network_secret); } if let Some(wechat) = request.wechat { node.wechat = Set(wechat); } if let Some(mail) = request.mail { node.mail = Set(mail); } if let Some(qq_number) = request.qq_number { node.qq_number = Set(qq_number); } node.updated_at = Set(chrono::Utc::now().fixed_offset()); tracing::info!("updated node: {:?}", node); let updated_node = entity::shared_nodes::Entity::update(node) .exec(app_state.db.orm_db()) .await?; // 更新标签 if let Some(tags) = request.tags { NodeOperations::set_node_tags(&app_state.db, updated_node.id, tags).await?; } let mut resp = NodeResponse::from(updated_node); resp.tags = NodeOperations::get_node_tags(&app_state.db, resp.id).await?; Ok(Json(ApiResponse::success(resp))) } pub async fn admin_revoke_approval( State(app_state): State, Path(id): Path, headers: HeaderMap, ) -> ApiResult>> { verify_admin_token(&headers)?; let node = entity::shared_nodes::Entity::find_by_id(id) .one(app_state.db.orm_db()) .await? .ok_or_else(|| ApiError::NotFound("Node not found".to_string()))?; let mut active_model = node.into_active_model(); active_model.is_approved = sea_orm::Set(false); let updated_node = entity::shared_nodes::Entity::update(active_model) .exec(app_state.db.orm_db()) .await?; let mut resp = NodeResponse::from(updated_node); resp.tags = NodeOperations::get_node_tags(&app_state.db, resp.id).await?; Ok(Json(ApiResponse::success(resp))) } pub async fn admin_delete_node( State(app_state): State, Path(id): Path, headers: HeaderMap, ) -> ApiResult>> { verify_admin_token(&headers)?; let node = entity::shared_nodes::Entity::find_by_id(id) .one(app_state.db.orm_db()) .await? .ok_or_else(|| ApiError::NotFound("Node not found".to_string()))?; node.delete(app_state.db.orm_db()).await?; Ok(Json(ApiResponse::message( "Node deleted successfully".to_string(), ))) } pub async fn admin_verify_token(headers: HeaderMap) -> ApiResult>> { verify_admin_token(&headers)?; Ok(Json(ApiResponse::message("Token is valid".to_string()))) } fn verify_admin_token(headers: &HeaderMap) -> ApiResult<()> { let config = AppConfig::default(); let auth_header = headers .get("authorization") .ok_or_else(|| ApiError::Unauthorized("Missing authorization header".to_string()))?; let auth_str = auth_header .to_str() .map_err(|_| ApiError::Unauthorized("Invalid authorization header".to_string()))?; let token = auth_str .strip_prefix("Bearer ") .ok_or_else(|| ApiError::Unauthorized("Invalid authorization format".to_string()))?; let _claims = decode::( token, &DecodingKey::from_secret(config.security.jwt_secret.as_ref()), &Validation::default(), ) .map_err(|_| ApiError::Unauthorized("Invalid token".to_string()))?; Ok(()) } pub async fn get_all_tags( State(app_state): State, ) -> ApiResult>>> { let tags = NodeOperations::get_all_tags(&app_state.db).await?; Ok(Json(ApiResponse::success(tags))) }