From 6f560c981bc114153cf036d05f48aa98a0cdd98f Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Sun, 5 Jul 2026 05:29:22 +0000 Subject: [PATCH] feat(api): add API routes with full state wiring and integration test fixtures - Add /api routes for agents and proxy resources - Wire all services into ApiState in service/mod.rs - Update route tests with all mock services --- .../src/routes/api/agents/add_agent.rs | 41 ++++ .../src/routes/api/agents/delete_agent.rs | 27 +++ .../src/routes/api/agents/dto.rs | 39 +++ .../src/routes/api/agents/get_agent.rs | 52 ++++ .../src/routes/api/agents/mod.rs | 223 ++++++++++++++++++ .../src/routes/api/agents/update_agent.rs | 62 +++++ apps/nxmesh-master/src/routes/api/error.rs | 55 +++++ apps/nxmesh-master/src/routes/api/mod.rs | 148 ++++++++++++ .../src/routes/api/proxy/access_rules.rs | 164 +++++++++++++ .../src/routes/api/proxy/agents.rs | 82 +++++++ .../src/routes/api/proxy/cache_zones.rs | 133 +++++++++++ .../routes/api/proxy/config_inheritance.rs | 107 +++++++++ .../src/routes/api/proxy/configs.rs | 147 ++++++++++++ .../src/routes/api/proxy/limit_rules.rs | 144 +++++++++++ .../src/routes/api/proxy/limit_zones.rs | 133 +++++++++++ .../src/routes/api/proxy/locations.rs | 142 +++++++++++ .../src/routes/api/proxy/log_settings.rs | 140 +++++++++++ .../nxmesh-master/src/routes/api/proxy/mod.rs | 37 +++ .../src/routes/api/proxy/proxy_settings.rs | 154 ++++++++++++ .../src/routes/api/proxy/rewrite_rules.rs | 148 ++++++++++++ .../src/routes/api/proxy/server_blocks.rs | 142 +++++++++++ .../src/routes/api/proxy/ssl_certificates.rs | 133 +++++++++++ .../src/routes/api/proxy/test_builder.rs | 174 ++++++++++++++ .../src/routes/api/proxy/upstreams.rs | 144 +++++++++++ apps/nxmesh-master/src/routes/mod.rs | 46 +++- apps/nxmesh-master/src/service/mod.rs | 64 ++++- 26 files changed, 2877 insertions(+), 4 deletions(-) create mode 100644 apps/nxmesh-master/src/routes/api/agents/add_agent.rs create mode 100644 apps/nxmesh-master/src/routes/api/agents/delete_agent.rs create mode 100644 apps/nxmesh-master/src/routes/api/agents/dto.rs create mode 100644 apps/nxmesh-master/src/routes/api/agents/get_agent.rs create mode 100644 apps/nxmesh-master/src/routes/api/agents/mod.rs create mode 100644 apps/nxmesh-master/src/routes/api/agents/update_agent.rs create mode 100644 apps/nxmesh-master/src/routes/api/error.rs create mode 100644 apps/nxmesh-master/src/routes/api/mod.rs create mode 100644 apps/nxmesh-master/src/routes/api/proxy/access_rules.rs create mode 100644 apps/nxmesh-master/src/routes/api/proxy/agents.rs create mode 100644 apps/nxmesh-master/src/routes/api/proxy/cache_zones.rs create mode 100644 apps/nxmesh-master/src/routes/api/proxy/config_inheritance.rs create mode 100644 apps/nxmesh-master/src/routes/api/proxy/configs.rs create mode 100644 apps/nxmesh-master/src/routes/api/proxy/limit_rules.rs create mode 100644 apps/nxmesh-master/src/routes/api/proxy/limit_zones.rs create mode 100644 apps/nxmesh-master/src/routes/api/proxy/locations.rs create mode 100644 apps/nxmesh-master/src/routes/api/proxy/log_settings.rs create mode 100644 apps/nxmesh-master/src/routes/api/proxy/mod.rs create mode 100644 apps/nxmesh-master/src/routes/api/proxy/proxy_settings.rs create mode 100644 apps/nxmesh-master/src/routes/api/proxy/rewrite_rules.rs create mode 100644 apps/nxmesh-master/src/routes/api/proxy/server_blocks.rs create mode 100644 apps/nxmesh-master/src/routes/api/proxy/ssl_certificates.rs create mode 100644 apps/nxmesh-master/src/routes/api/proxy/test_builder.rs create mode 100644 apps/nxmesh-master/src/routes/api/proxy/upstreams.rs diff --git a/apps/nxmesh-master/src/routes/api/agents/add_agent.rs b/apps/nxmesh-master/src/routes/api/agents/add_agent.rs new file mode 100644 index 0000000..9490f4c --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/agents/add_agent.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; + +use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; +use serde::{Deserialize, Serialize}; +use tracing::error; + +use crate::{ + routes::api::{agents::dto::AgentInfo, error::AppError}, + service::agent::{AgentService, CreateAgentRecord}, +}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct CreateAgentRequest { + pub name: String, + #[serde(default)] + pub ip_address: Option, +} + +pub async fn add_agent_handler( + State(agent_service): State>, + Json(body): Json, +) -> Result { + if body.name.trim().is_empty() { + return Err(AppError::BadRequest("name is required".to_string())); + } + + let rec = CreateAgentRecord { + name: body.name, + ip_address: body.ip_address, + }; + + let agent = agent_service.create(&rec).await.map_err(|err| { + error!("Failed to create agent: {}", err); + AppError::InternalServerError + })?; + + Ok(( + StatusCode::CREATED, + Json(serde_json::json!({"agent": AgentInfo::from(agent)})), + )) +} diff --git a/apps/nxmesh-master/src/routes/api/agents/delete_agent.rs b/apps/nxmesh-master/src/routes/api/agents/delete_agent.rs new file mode 100644 index 0000000..d808347 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/agents/delete_agent.rs @@ -0,0 +1,27 @@ +use std::sync::Arc; + +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use tracing::{error, info}; + +use crate::{routes::api::error::AppError, service::agent::AgentService}; + +pub async fn delete_agent_handler( + State(agent_service): State>, + Path(id): Path, +) -> Result { + let deleted = agent_service.delete(id).await.map_err(|err| { + error!("Failed to delete agent: {}", err); + AppError::InternalServerError + })?; + + if deleted { + info!("Agent {} deleted", id); + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} diff --git a/apps/nxmesh-master/src/routes/api/agents/dto.rs b/apps/nxmesh-master/src/routes/api/agents/dto.rs new file mode 100644 index 0000000..6a1158f --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/agents/dto.rs @@ -0,0 +1,39 @@ +use serde::{Deserialize, Serialize}; + +use crate::service::agent::{AgentRecord, State}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentInfo { + pub id: String, + pub name: String, + pub state: State, + // + pub deployment_mode: Option, + pub ip_address: Option, + pub last_seen_at: Option, + pub labels: Option, + // + pub number_of_routes: usize, + // + pub created_at: String, + pub updated_at: String, + pub is_disabled: bool, +} + +impl From for AgentInfo { + fn from(record: AgentRecord) -> Self { + AgentInfo { + id: record.id.to_string(), + name: record.name, + ip_address: record.ip_address, + state: record.state, + deployment_mode: record.deployment_mode, + last_seen_at: record.last_seen_at, + labels: record.labels, + number_of_routes: 0, // This will be populated later + created_at: record.created_at, + updated_at: record.updated_at, + is_disabled: matches!(record.state, State::Disabled), + } + } +} diff --git a/apps/nxmesh-master/src/routes/api/agents/get_agent.rs b/apps/nxmesh-master/src/routes/api/agents/get_agent.rs new file mode 100644 index 0000000..683ac6c --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/agents/get_agent.rs @@ -0,0 +1,52 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, +}; +use serde::Serialize; +use tracing::error; + +use crate::{ + routes::api::{agents::dto::AgentInfo, error::AppError}, + service::agent::AgentService, +}; + +#[derive(Debug, Clone, Serialize)] +pub struct GetAgentsResponse { + agents: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct GetAgentResponse { + agent: AgentInfo, +} + +pub async fn get_agents_handler( + State(agent_service): State>, +) -> Result, AppError> { + let agents = agent_service.list().await.map_err(|err| { + error!("Failed to get agents: {}", err); + AppError::InternalServerError + })?; + + Ok(Json(GetAgentsResponse { + agents: agents.into_iter().map(AgentInfo::from).collect(), + })) +} + +pub async fn get_agent_handler( + State(agent_service): State>, + Path(id): Path, +) -> Result, AppError> { + let agent = agent_service.get(id).await.map_err(|err| { + error!("Failed to get agent: {}", err); + AppError::InternalServerError + })?; + match agent { + Some(agent) => Ok(Json(GetAgentResponse { + agent: AgentInfo::from(agent), + })), + None => Err(AppError::NotFound), + } +} diff --git a/apps/nxmesh-master/src/routes/api/agents/mod.rs b/apps/nxmesh-master/src/routes/api/agents/mod.rs new file mode 100644 index 0000000..5aeed05 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/agents/mod.rs @@ -0,0 +1,223 @@ +use crate::routes::api::{ + ApiRouter, + agents::{ + add_agent::add_agent_handler, + delete_agent::delete_agent_handler, + get_agent::{get_agent_handler, get_agents_handler}, + update_agent::update_agent_handler, + }, +}; + +mod add_agent; +mod delete_agent; +mod dto; +mod get_agent; +mod update_agent; + +pub async fn get_router() -> ApiRouter { + ApiRouter::new() + .route( + "/agents", + axum::routing::get(get_agents_handler).post(add_agent_handler), + ) + .route( + "/agents/{id}", + axum::routing::get(get_agent_handler) + .put(update_agent_handler) + .delete(delete_agent_handler), + ) +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use std::sync::Arc; + + use crate::service::agent::{AgentRecord, MockAgentService, State}; + + use super::*; + use axum_test::TestServer; + + async fn make_state(mock: MockAgentService) -> axum::Router { + use crate::service::proxy::*; + let state = crate::routes::api::LocalApiState::from(crate::routes::api::ApiState { + agent_service: Arc::new(mock), + proxy_service: Arc::new(MockProxyServiceTrait::new()), + server_block_service: Arc::new(server_block::MockServerBlockService::new()), + upstream_service: Arc::new(upstream::MockUpstreamService::new()), + location_block_service: Arc::new(location_block::MockLocationBlockService::new()), + access_rule_service: Arc::new(access_rule::MockAccessRuleService::new()), + cache_zone_service: Arc::new(cache_zone::MockCacheZoneService::new()), + limit_rule_service: Arc::new(limit_rule::MockLimitRuleService::new()), + limit_zone_service: Arc::new(limit_zone::MockLimitZoneService::new()), + log_setting_service: Arc::new(log_setting::MockLogSettingService::new()), + proxy_setting_service: Arc::new(proxy_setting::MockProxySettingService::new()), + rewrite_rule_service: Arc::new(rewrite_rule::MockRewriteRuleService::new()), + ssl_certificate_service: Arc::new(ssl_certificate::MockSslCertificateService::new()), + config_inheritance_service: Arc::new(config_inheritance::MockConfigInheritanceService::new()), + }); + get_router().await.with_state(state) + } + + fn make_record(id: uuid::Uuid) -> AgentRecord { + AgentRecord { + id, + name: "agent1".to_string(), + ip_address: Some("127.0.0.1".to_string()), + state: State::Active, + deployment_mode: None, + last_seen_at: None, + labels: None, + created_at: "created".to_string(), + updated_at: "updated".to_string(), + } + } + + #[tokio::test] + async fn test_get_agents() { + let mut agent_service_mock = MockAgentService::new(); + agent_service_mock.expect_list().returning(|| Ok(vec![])); + let server = TestServer::new(make_state(agent_service_mock).await); + let response = server.get("/agents").await; + assert_eq!(response.status_code(), 200); + assert_eq!(response.text(), r#"{"agents":[]}"#); + } + + #[tokio::test] + async fn test_get_agent_not_found() { + let mut agent_service_mock = MockAgentService::new(); + agent_service_mock.expect_get().returning(|_| Ok(None)); + let server = TestServer::new(make_state(agent_service_mock).await); + let id = uuid::Uuid::new_v4(); + let response = server.get(&format!("/agents/{}", id)).await; + assert_eq!(response.status_code(), 404); + } + + #[tokio::test] + async fn test_get_agent_found() { + let id = uuid::Uuid::new_v4(); + let mut agent_service_mock = MockAgentService::new(); + agent_service_mock + .expect_get() + .returning(move |_| Ok(Some(make_record(id)))); + let server = TestServer::new(make_state(agent_service_mock).await); + let expected = format!( + r#"{{"agent":{{"id":"{}","name":"agent1","state":"Active","deployment_mode":null,"ip_address":"127.0.0.1","last_seen_at":null,"labels":null,"number_of_routes":0,"created_at":"created","updated_at":"updated","is_disabled":false}}}}"#, + id + ); + let response = server.get(&format!("/agents/{}", id)).await; + assert_eq!(response.status_code(), 200); + assert_eq!(response.text(), expected); + } + + #[tokio::test] + async fn test_create_agent() { + let id = uuid::Uuid::new_v4(); + let mut agent_service_mock = MockAgentService::new(); + agent_service_mock.expect_create().returning(move |rec| { + Ok(AgentRecord { + id, + name: rec.name.clone(), + ip_address: rec.ip_address.clone(), + state: State::Active, + deployment_mode: None, + last_seen_at: None, + labels: None, + created_at: "created".to_string(), + updated_at: "updated".to_string(), + }) + }); + let server = TestServer::new(make_state(agent_service_mock).await); + let response = server + .post("/agents") + .json(&serde_json::json!({"name": "new-agent", "ip_address": "10.0.0.1"})) + .await; + assert_eq!(response.status_code(), 201); + let body: serde_json::Value = serde_json::from_str(&response.text()).unwrap(); + assert_eq!(body["agent"]["name"], "new-agent"); + assert_eq!(body["agent"]["ip_address"], "10.0.0.1"); + assert_eq!(body["agent"]["id"], id.to_string()); + } + + #[tokio::test] + async fn test_create_agent_empty_name() { + let agent_service_mock = MockAgentService::new(); + let server = TestServer::new(make_state(agent_service_mock).await); + let response = server + .post("/agents") + .json(&serde_json::json!({"name": ""})) + .await; + assert_eq!(response.status_code(), 400); + } + + #[tokio::test] + async fn test_create_agent_missing_name() { + let agent_service_mock = MockAgentService::new(); + let server = TestServer::new(make_state(agent_service_mock).await); + let response = server.post("/agents").json(&serde_json::json!({})).await; + assert_eq!(response.status_code(), 422); + } + + #[tokio::test] + async fn test_update_agent() { + let id = uuid::Uuid::new_v4(); + let mut agent_service_mock = MockAgentService::new(); + agent_service_mock.expect_update().returning(move |_, _| { + Ok(Some(AgentRecord { + id, + name: "updated-agent".to_string(), + ip_address: Some("10.0.0.2".to_string()), + state: State::Inactive, + deployment_mode: None, + last_seen_at: None, + labels: None, + created_at: "created".to_string(), + updated_at: "updated".to_string(), + })) + }); + let server = TestServer::new(make_state(agent_service_mock).await); + let response = server + .put(&format!("/agents/{}", id)) + .json(&serde_json::json!({"name": "updated-agent", "state": "Inactive"})) + .await; + assert_eq!(response.status_code(), 200); + let body: serde_json::Value = serde_json::from_str(&response.text()).unwrap(); + assert_eq!(body["agent"]["name"], "updated-agent"); + assert_eq!(body["agent"]["state"], "Inactive"); + } + + #[tokio::test] + async fn test_update_agent_not_found() { + let mut agent_service_mock = MockAgentService::new(); + agent_service_mock + .expect_update() + .returning(|_, _| Ok(None)); + let server = TestServer::new(make_state(agent_service_mock).await); + let id = uuid::Uuid::new_v4(); + let response = server + .put(&format!("/agents/{}", id)) + .json(&serde_json::json!({"name": "updated-agent"})) + .await; + assert_eq!(response.status_code(), 404); + } + + #[tokio::test] + async fn test_delete_agent() { + let mut agent_service_mock = MockAgentService::new(); + agent_service_mock.expect_delete().returning(|_| Ok(true)); + let server = TestServer::new(make_state(agent_service_mock).await); + let id = uuid::Uuid::new_v4(); + let response = server.delete(&format!("/agents/{}", id)).await; + assert_eq!(response.status_code(), 204); + } + + #[tokio::test] + async fn test_delete_agent_not_found() { + let mut agent_service_mock = MockAgentService::new(); + agent_service_mock.expect_delete().returning(|_| Ok(false)); + let server = TestServer::new(make_state(agent_service_mock).await); + let id = uuid::Uuid::new_v4(); + let response = server.delete(&format!("/agents/{}", id)).await; + assert_eq!(response.status_code(), 404); + } +} diff --git a/apps/nxmesh-master/src/routes/api/agents/update_agent.rs b/apps/nxmesh-master/src/routes/api/agents/update_agent.rs new file mode 100644 index 0000000..e4383f2 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/agents/update_agent.rs @@ -0,0 +1,62 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::Deserialize; +use tracing::error; + +use crate::{ + routes::api::{agents::dto::AgentInfo, error::AppError}, + service::agent::{AgentService, State as AgentState, UpdateAgentRecord}, +}; + +#[derive(Debug, Deserialize)] +pub struct UpdateAgentRequest { + #[serde(default)] + pub name: Option, + #[serde(default)] + pub ip_address: Option, + #[serde(default)] + pub state: Option, + #[serde(default)] + pub deployment_mode: Option, + #[serde(default)] + pub labels: Option, +} + +pub async fn update_agent_handler( + State(agent_service): State>, + Path(id): Path, + Json(body): Json, +) -> Result { + if let Some(ref name) = body.name { + if name.trim().is_empty() { + return Err(AppError::BadRequest("name must not be empty".to_string())); + } + } + + let rec = UpdateAgentRecord { + name: body.name, + ip_address: body.ip_address, + state: body.state, + deployment_mode: body.deployment_mode, + labels: body.labels, + }; + + let agent = agent_service.update(id, &rec).await.map_err(|err| { + error!("Failed to update agent: {}", err); + AppError::InternalServerError + })?; + + match agent { + Some(agent) => Ok(( + StatusCode::OK, + Json(serde_json::json!({"agent": AgentInfo::from(agent)})), + )), + None => Err(AppError::NotFound), + } +} diff --git a/apps/nxmesh-master/src/routes/api/error.rs b/apps/nxmesh-master/src/routes/api/error.rs new file mode 100644 index 0000000..3ac159f --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/error.rs @@ -0,0 +1,55 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; +use serde_json::json; +use tracing::error; + +use crate::service::proxy::types::ProxyServiceError; + +impl From for AppError { + fn from(e: ProxyServiceError) -> Self { + match e { + ProxyServiceError::ConfigNotFound => AppError::NotFound, + ProxyServiceError::InvalidConfig(msg) => AppError::BadRequest(msg), + ProxyServiceError::RendererNotFound => AppError::InternalServerError, + ProxyServiceError::DatabaseError(_) => AppError::InternalServerError, + } + } +} + +pub enum AppError { + NotFound, + InternalServerError, + BadRequest(String), +} + +fn make_error_response(status: StatusCode, code: &str, msg: &str) -> Response { + let body = json!({"code": code, "message": msg}); + Response::builder() + .status(status) + .header("Content-Type", "application/json") + .body(serde_json::to_string(&body).unwrap_or_default().into()) + .unwrap_or_else(|err| { + error!("Failed to build error response: {}", err); + StatusCode::INTERNAL_SERVER_ERROR.into_response() + }) +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + match self { + AppError::BadRequest(msg) => { + make_error_response(StatusCode::BAD_REQUEST, "BAD_REQUEST", &msg) + } + AppError::NotFound => { + make_error_response(StatusCode::NOT_FOUND, "NOT_FOUND", "Not Found") + } + AppError::InternalServerError => make_error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "INTERNAL_ERROR", + "Internal Server Error", + ), + } + } +} diff --git a/apps/nxmesh-master/src/routes/api/mod.rs b/apps/nxmesh-master/src/routes/api/mod.rs new file mode 100644 index 0000000..18fe8aa --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/mod.rs @@ -0,0 +1,148 @@ +use std::sync::Arc; + +use axum::{Router, extract::FromRef}; + +use crate::service::agent::AgentService; +use crate::service::proxy::ProxyServiceTrait; +use crate::service::proxy::access_rule::AccessRuleService; +use crate::service::proxy::cache_zone::CacheZoneService; +use crate::service::proxy::config_inheritance::ConfigInheritanceService; +use crate::service::proxy::limit_rule::LimitRuleService; +use crate::service::proxy::limit_zone::LimitZoneService; +use crate::service::proxy::location_block::LocationBlockService; +use crate::service::proxy::log_setting::LogSettingService; +use crate::service::proxy::proxy_setting::ProxySettingService; +use crate::service::proxy::rewrite_rule::RewriteRuleService; +use crate::service::proxy::server_block::ServerBlockService; +use crate::service::proxy::ssl_certificate::SslCertificateService; +use crate::service::proxy::upstream::UpstreamService; + +mod agents; +pub mod error; +pub use error::AppError; +pub(crate) mod proxy; + +pub struct ApiState { + pub agent_service: Arc, + pub proxy_service: Arc, + pub server_block_service: Arc, + pub upstream_service: Arc, + pub location_block_service: Arc, + pub access_rule_service: Arc, + pub cache_zone_service: Arc, + pub limit_rule_service: Arc, + pub limit_zone_service: Arc, + pub log_setting_service: Arc, + pub proxy_setting_service: Arc, + pub rewrite_rule_service: Arc, + pub ssl_certificate_service: Arc, + pub config_inheritance_service: Arc, +} + +#[derive(Clone)] +pub struct LocalApiState(pub Arc); + +impl From for LocalApiState { + fn from(api_state: ApiState) -> Self { + LocalApiState(Arc::new(api_state)) + } +} + +impl From> for LocalApiState { + fn from(api_state: Arc) -> Self { + LocalApiState(api_state) + } +} + +pub type ApiRouter = Router; + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.agent_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.proxy_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.server_block_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.upstream_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.location_block_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.access_rule_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.cache_zone_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.limit_rule_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.limit_zone_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.log_setting_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.proxy_setting_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.rewrite_rule_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.ssl_certificate_service.clone() + } +} + +impl FromRef for Arc { + fn from_ref(api_state: &LocalApiState) -> Arc { + api_state.0.config_inheritance_service.clone() + } +} + +pub async fn get_router(state: impl Into) -> Router { + ApiRouter::new() + .nest("/agents", agents::get_router().await) + .nest("/proxy", proxy::get_router().await) + .with_state(state.into()) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/access_rules.rs b/apps/nxmesh-master/src/routes/api/proxy/access_rules.rs new file mode 100644 index 0000000..218922e --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/access_rules.rs @@ -0,0 +1,164 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{AppError, ApiRouter}; +use crate::service::proxy::access_rule::{ + AccessRuleService, CreateAccessRuleParams, UpdateAccessRuleParams, +}; +use crate::service::proxy::types::AccessRuleConfig; + +#[derive(Serialize)] +pub(crate) struct AccessRuleResponse { + pub id: Uuid, + pub r#type: String, + pub ip_cidr: String, + pub description: Option, + pub priority: i32, + pub override_of_id: Option, +} + +impl From for AccessRuleResponse { + fn from(c: AccessRuleConfig) -> Self { + Self { + id: c.id, + r#type: c.r#type, + ip_cidr: c.ip_cidr, + description: c.description, + priority: c.priority, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateAccessRuleRequest { + pub server_id: Option, + pub location_id: Option, + pub r#type: String, + pub ip_cidr: String, + pub description: Option, + pub priority: i32, + pub override_of_id: Option, +} + +impl From for CreateAccessRuleParams { + fn from(r: CreateAccessRuleRequest) -> Self { + Self { + server_id: r.server_id, + location_id: r.location_id, + r#type: r.r#type, + ip_cidr: r.ip_cidr, + description: r.description, + priority: r.priority, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateAccessRuleRequest { + pub server_id: Option>, + pub location_id: Option>, + pub r#type: Option, + pub ip_cidr: Option, + pub description: Option>, + pub priority: Option, + pub override_of_id: Option>, +} + +impl From for UpdateAccessRuleParams { + fn from(r: UpdateAccessRuleRequest) -> Self { + Self { + server_id: r.server_id, + location_id: r.location_id, + r#type: r.r#type, + ip_cidr: r.ip_cidr, + description: r.description, + priority: r.priority, + override_of_id: r.override_of_id, + } + } +} + +async fn list_access_rules_by_server( + State(svc): State>, + Path(server_id): Path, +) -> Result>, AppError> { + let rules = svc.list_by_server(server_id).await?; + Ok(Json(rules.into_iter().map(Into::into).collect())) +} + +async fn list_access_rules_by_location( + State(svc): State>, + Path(location_id): Path, +) -> Result>, AppError> { + let rules = svc.list_by_location(location_id).await?; + Ok(Json(rules.into_iter().map(Into::into).collect())) +} + +async fn create_access_rule( + State(svc): State>, + Json(body): Json, +) -> Result { + let rule = svc.create(body.into()).await?; + Ok((StatusCode::CREATED, Json(AccessRuleResponse::from(rule)))) +} + +async fn get_access_rule( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let rule = svc.get(id).await?; + Ok(Json(rule.into())) +} + +async fn update_access_rule( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let rule = svc.update(id, body.into()).await?; + Ok(Json(rule.into())) +} + +async fn delete_access_rule( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/server-blocks/{server_id}/access-rules", + axum::routing::get(list_access_rules_by_server), + ) + .route( + "/locations/{location_id}/access-rules", + axum::routing::get(list_access_rules_by_location), + ) + .route( + "/access-rules", + axum::routing::post(create_access_rule), + ) + .route( + "/access-rules/{id}", + axum::routing::get(get_access_rule) + .put(update_access_rule) + .delete(delete_access_rule), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/agents.rs b/apps/nxmesh-master/src/routes/api/proxy/agents.rs new file mode 100644 index 0000000..dad36cc --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/agents.rs @@ -0,0 +1,82 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{AppError, ApiRouter}; +use crate::service::proxy::{ProxyServiceTrait, types::AgentConfigBinding}; + +use super::configs::ProxyConfigResponse; + +#[derive(Serialize)] +pub(crate) struct AgentConfigResponse { + pub id: Uuid, + pub agent_id: Option, + pub group_id: Option, + pub config_id: Uuid, + pub is_active: bool, + pub applied_at: String, +} + +impl From for AgentConfigResponse { + fn from(b: AgentConfigBinding) -> Self { + Self { + id: b.id, + agent_id: b.agent_id, + group_id: b.group_id, + config_id: b.config_id, + is_active: b.is_active, + applied_at: b.applied_at.to_rfc3339(), + } + } +} + +#[derive(Deserialize)] +pub(crate) struct BindAgentRequest { + pub config_id: Uuid, +} + +async fn get_active_agent_config( + State(svc): State>, + Path(agent_id): Path, +) -> Result>, AppError> { + let config = svc.get_active_agent_config(agent_id).await?; + Ok(Json(config.map(Into::into))) +} + +async fn bind_agent( + State(svc): State>, + Path(agent_id): Path, + Json(body): Json, +) -> Result { + let binding = svc.bind_agent(agent_id, body.config_id).await?; + Ok((StatusCode::CREATED, Json(AgentConfigResponse::from(binding)))) +} + +async fn unbind_agent( + State(svc): State>, + Path(agent_id): Path, +) -> Result { + let deleted = svc.unbind_agent(agent_id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/agents/{agent_id}/config", + axum::routing::get(get_active_agent_config) + .post(bind_agent) + .delete(unbind_agent), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/cache_zones.rs b/apps/nxmesh-master/src/routes/api/proxy/cache_zones.rs new file mode 100644 index 0000000..839b8dc --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/cache_zones.rs @@ -0,0 +1,133 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{AppError, ApiRouter}; +use crate::service::proxy::cache_zone::{ + CacheZoneService, CreateCacheZoneParams, UpdateCacheZoneParams, +}; +use crate::service::proxy::types::CacheZoneConfig; + +#[derive(Serialize)] +pub(crate) struct CacheZoneResponse { + pub id: Uuid, + pub name: String, + pub path: String, + pub size: String, + pub override_of_id: Option, +} + +impl From for CacheZoneResponse { + fn from(c: CacheZoneConfig) -> Self { + Self { + id: c.id, + name: c.name, + path: c.path, + size: c.size, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateCacheZoneRequest { + pub name: String, + pub path: String, + pub size_limit: String, + pub override_of_id: Option, +} + +impl From for CreateCacheZoneParams { + fn from(r: CreateCacheZoneRequest) -> Self { + Self { + name: r.name, + path: r.path, + size_limit: r.size_limit, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateCacheZoneRequest { + pub name: Option, + pub path: Option, + pub size_limit: Option, + pub override_of_id: Option>, +} + +impl From for UpdateCacheZoneParams { + fn from(r: UpdateCacheZoneRequest) -> Self { + Self { + name: r.name, + path: r.path, + size_limit: r.size_limit, + override_of_id: r.override_of_id, + } + } +} + +async fn list_cache_zones( + State(svc): State>, +) -> Result>, AppError> { + let zones = svc.list().await?; + Ok(Json(zones.into_iter().map(Into::into).collect())) +} + +async fn create_cache_zone( + State(svc): State>, + Json(body): Json, +) -> Result { + let zone = svc.create(body.into()).await?; + Ok((StatusCode::CREATED, Json(CacheZoneResponse::from(zone)))) +} + +async fn get_cache_zone( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let zone = svc.get(id).await?; + Ok(Json(zone.into())) +} + +async fn update_cache_zone( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let zone = svc.update(id, body.into()).await?; + Ok(Json(zone.into())) +} + +async fn delete_cache_zone( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/cache-zones", + axum::routing::get(list_cache_zones).post(create_cache_zone), + ) + .route( + "/cache-zones/{id}", + axum::routing::get(get_cache_zone) + .put(update_cache_zone) + .delete(delete_cache_zone), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/config_inheritance.rs b/apps/nxmesh-master/src/routes/api/proxy/config_inheritance.rs new file mode 100644 index 0000000..045f34a --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/config_inheritance.rs @@ -0,0 +1,107 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{AppError, ApiRouter}; +use crate::service::proxy::config_inheritance::{ + AddInheritanceParams, ConfigInheritanceRecord, ConfigInheritanceService, +}; + +#[derive(Deserialize)] +pub(crate) struct AddParentRequest { + pub parent_config_id: Uuid, + #[serde(default)] + pub priority: Option, +} + +#[derive(Serialize)] +pub(crate) struct InheritanceRecordResponse { + pub id: Uuid, + pub child_config_id: Uuid, + pub parent_config_id: Uuid, + pub priority: Option, + pub applied_at: String, +} + +impl From for InheritanceRecordResponse { + fn from(r: ConfigInheritanceRecord) -> Self { + Self { + id: r.id, + child_config_id: r.child_config_id, + parent_config_id: r.parent_config_id, + priority: r.priority, + applied_at: r.applied_at.and_utc().to_rfc3339(), + } + } +} + +async fn list_parents( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let records = svc.list_parents(id).await?; + let response: Vec = records.into_iter().map(Into::into).collect(); + Ok(Json(serde_json::json!(response))) +} + +async fn add_parent( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result { + let record = svc + .add(AddInheritanceParams { + child_config_id: id, + parent_config_id: body.parent_config_id, + priority: body.priority, + }) + .await?; + Ok(( + StatusCode::CREATED, + Json(InheritanceRecordResponse::from(record)), + )) +} + +async fn remove_parent( + State(svc): State>, + Path((id, parent_id)): Path<(Uuid, Uuid)>, +) -> Result { + let deleted = svc.remove(id, parent_id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +async fn list_children( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let records = svc.list_children(id).await?; + let response: Vec = records.into_iter().map(Into::into).collect(); + Ok(Json(serde_json::json!(response))) +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/configs/{id}/parents", + axum::routing::get(list_parents).post(add_parent), + ) + .route( + "/configs/{id}/parents/{parent_id}", + axum::routing::delete(remove_parent), + ) + .route( + "/configs/{id}/children", + axum::routing::get(list_children), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/configs.rs b/apps/nxmesh-master/src/routes/api/proxy/configs.rs new file mode 100644 index 0000000..37f2f61 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/configs.rs @@ -0,0 +1,147 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{AppError, ApiRouter}; +use crate::service::proxy::{ + ProxyServiceTrait, + types::{CreateProxyConfigParams, ProxyConfigSummary, UpdateProxyConfigParams}, +}; + +#[derive(Serialize)] +pub(crate) struct ProxyConfigResponse { + pub id: Uuid, + pub name: String, + pub description: Option, + pub is_template: bool, + pub created_at: String, + pub updated_at: String, +} + +impl From for ProxyConfigResponse { + fn from(s: ProxyConfigSummary) -> Self { + Self { + id: s.id, + name: s.name, + description: s.description, + is_template: s.is_template, + created_at: s.created_at.to_rfc3339(), + updated_at: s.updated_at.to_rfc3339(), + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateConfigRequest { + pub name: String, + #[serde(default)] + pub description: Option, + #[serde(default)] + pub is_template: bool, +} + +#[derive(Deserialize)] +pub(crate) struct UpdateConfigRequest { + #[serde(default)] + pub name: Option, + #[serde(default)] + pub description: Option, + #[serde(default)] + pub is_template: Option, +} + +#[derive(Serialize)] +pub(crate) struct ListConfigsResponse { + pub configs: Vec, +} + +async fn list_configs( + State(svc): State>, +) -> Result, AppError> { + let configs = svc.list_configs().await?; + Ok(Json(ListConfigsResponse { + configs: configs.into_iter().map(Into::into).collect(), + })) +} + +async fn create_config( + State(svc): State>, + Json(body): Json, +) -> Result { + if body.name.trim().is_empty() { + return Err(AppError::BadRequest("name is required".to_string())); + } + let config = svc + .create_config(CreateProxyConfigParams { + name: body.name, + description: body.description, + is_template: body.is_template, + }) + .await?; + Ok((StatusCode::CREATED, Json(ProxyConfigResponse::from(config)))) +} + +async fn get_config( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let config = svc.get_proxy_config(id).await?; + Ok(Json(serde_json::to_value(&config.id).unwrap_or_default())) +} + +async fn update_config( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let config = svc + .update_config( + id, + UpdateProxyConfigParams { + name: body.name, + description: body.description, + is_template: body.is_template, + }, + ) + .await?; + Ok(Json(config.into())) +} + +async fn delete_config( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete_config(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +async fn render_config( + State(svc): State>, + Path(id): Path, +) -> Result { + let output = svc.render_config(id).await?; + Ok(output) +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route("/configs", axum::routing::get(list_configs).post(create_config)) + .route( + "/configs/{id}", + axum::routing::get(get_config) + .put(update_config) + .delete(delete_config), + ) + .route("/configs/{id}/render", axum::routing::get(render_config)) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/limit_rules.rs b/apps/nxmesh-master/src/routes/api/proxy/limit_rules.rs new file mode 100644 index 0000000..c2d3339 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/limit_rules.rs @@ -0,0 +1,144 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{AppError, ApiRouter}; +use crate::service::proxy::limit_rule::{ + CreateLimitRuleParams, LimitRuleService, UpdateLimitRuleParams, +}; +use crate::service::proxy::types::LimitRuleConfig; + +#[derive(Serialize)] +pub(crate) struct LimitRuleResponse { + pub id: Uuid, + pub location_id: Uuid, + pub zone_id: Uuid, + pub burst: Option, + pub nodelay: Option, + pub override_of_id: Option, +} + +impl From for LimitRuleResponse { + fn from(c: LimitRuleConfig) -> Self { + Self { + id: c.id, + location_id: c.location_id, + zone_id: c.zone_id, + burst: c.burst, + nodelay: c.nodelay, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateLimitRuleRequest { + pub location_id: Uuid, + pub zone_id: Uuid, + pub burst: Option, + pub nodelay: Option, + pub override_of_id: Option, +} + +impl From for CreateLimitRuleParams { + fn from(r: CreateLimitRuleRequest) -> Self { + Self { + location_id: r.location_id, + zone_id: r.zone_id, + burst: r.burst, + nodelay: r.nodelay, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateLimitRuleRequest { + pub location_id: Option, + pub zone_id: Option, + pub burst: Option>, + pub nodelay: Option>, + pub override_of_id: Option>, +} + +impl From for UpdateLimitRuleParams { + fn from(r: UpdateLimitRuleRequest) -> Self { + Self { + location_id: r.location_id, + zone_id: r.zone_id, + burst: r.burst, + nodelay: r.nodelay, + override_of_id: r.override_of_id, + } + } +} + +async fn list_limit_rules_by_location( + State(svc): State>, + Path(location_id): Path, +) -> Result>, AppError> { + let rules = svc.list_by_location(location_id).await?; + Ok(Json(rules.into_iter().map(Into::into).collect())) +} + +async fn create_limit_rule( + State(svc): State>, + Json(body): Json, +) -> Result { + let rule = svc.create(body.into()).await?; + Ok((StatusCode::CREATED, Json(LimitRuleResponse::from(rule)))) +} + +async fn get_limit_rule( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let rule = svc.get(id).await?; + Ok(Json(rule.into())) +} + +async fn update_limit_rule( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let rule = svc.update(id, body.into()).await?; + Ok(Json(rule.into())) +} + +async fn delete_limit_rule( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/locations/{location_id}/limit-rules", + axum::routing::get(list_limit_rules_by_location), + ) + .route( + "/limit-rules", + axum::routing::post(create_limit_rule), + ) + .route( + "/limit-rules/{id}", + axum::routing::get(get_limit_rule) + .put(update_limit_rule) + .delete(delete_limit_rule), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/limit_zones.rs b/apps/nxmesh-master/src/routes/api/proxy/limit_zones.rs new file mode 100644 index 0000000..4bd07e1 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/limit_zones.rs @@ -0,0 +1,133 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{AppError, ApiRouter}; +use crate::service::proxy::limit_zone::{ + CreateLimitZoneParams, LimitZoneService, UpdateLimitZoneParams, +}; +use crate::service::proxy::types::LimitZoneConfig; + +#[derive(Serialize)] +pub(crate) struct LimitZoneResponse { + pub id: Uuid, + pub name: String, + pub key: String, + pub rate: String, + pub override_of_id: Option, +} + +impl From for LimitZoneResponse { + fn from(c: LimitZoneConfig) -> Self { + Self { + id: c.id, + name: c.name, + key: c.key, + rate: c.rate, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateLimitZoneRequest { + pub name: String, + pub key: String, + pub rate: String, + pub override_of_id: Option, +} + +impl From for CreateLimitZoneParams { + fn from(r: CreateLimitZoneRequest) -> Self { + Self { + name: r.name, + key: r.key, + rate: r.rate, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateLimitZoneRequest { + pub name: Option, + pub key: Option, + pub rate: Option, + pub override_of_id: Option>, +} + +impl From for UpdateLimitZoneParams { + fn from(r: UpdateLimitZoneRequest) -> Self { + Self { + name: r.name, + key: r.key, + rate: r.rate, + override_of_id: r.override_of_id, + } + } +} + +async fn list_limit_zones( + State(svc): State>, +) -> Result>, AppError> { + let zones = svc.list().await?; + Ok(Json(zones.into_iter().map(Into::into).collect())) +} + +async fn create_limit_zone( + State(svc): State>, + Json(body): Json, +) -> Result { + let zone = svc.create(body.into()).await?; + Ok((StatusCode::CREATED, Json(LimitZoneResponse::from(zone)))) +} + +async fn get_limit_zone( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let zone = svc.get(id).await?; + Ok(Json(zone.into())) +} + +async fn update_limit_zone( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let zone = svc.update(id, body.into()).await?; + Ok(Json(zone.into())) +} + +async fn delete_limit_zone( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/limit-zones", + axum::routing::get(list_limit_zones).post(create_limit_zone), + ) + .route( + "/limit-zones/{id}", + axum::routing::get(get_limit_zone) + .put(update_limit_zone) + .delete(delete_limit_zone), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/locations.rs b/apps/nxmesh-master/src/routes/api/proxy/locations.rs new file mode 100644 index 0000000..dd8d742 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/locations.rs @@ -0,0 +1,142 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{AppError, ApiRouter}; +use crate::service::proxy::location_block::{ + CreateLocationBlockParams, LocationBlockService, UpdateLocationBlockParams, +}; +use crate::service::proxy::types::LocationBlockConfig; + +#[derive(Serialize)] +pub(crate) struct LocationBlockResponse { + pub id: Uuid, + pub server_id: Uuid, + pub path_pattern: String, + pub proxy_pass_upstream_id: Option, + pub metadata: Option, + pub override_of_id: Option, +} + +impl From for LocationBlockResponse { + fn from(c: LocationBlockConfig) -> Self { + Self { + id: c.id, + server_id: c.server_id, + path_pattern: c.path_pattern, + proxy_pass_upstream_id: c.proxy_pass_upstream_id, + metadata: c.metadata, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateLocationBlockRequest { + pub path_pattern: String, + pub proxy_pass_upstream_id: Option, + pub metadata: Option, + pub override_of_id: Option, +} + +impl From for CreateLocationBlockParams { + fn from(r: CreateLocationBlockRequest) -> Self { + Self { + server_id: Uuid::nil(), + path_pattern: r.path_pattern, + proxy_pass_upstream_id: r.proxy_pass_upstream_id, + metadata: r.metadata, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateLocationBlockRequest { + pub server_id: Option, + pub path_pattern: Option, + pub proxy_pass_upstream_id: Option>, + pub metadata: Option>, + pub override_of_id: Option>, +} + +impl From for UpdateLocationBlockParams { + fn from(r: UpdateLocationBlockRequest) -> Self { + Self { + server_id: r.server_id, + path_pattern: r.path_pattern, + proxy_pass_upstream_id: r.proxy_pass_upstream_id, + metadata: r.metadata, + override_of_id: r.override_of_id, + } + } +} + +async fn list_locations( + State(svc): State>, + Path(server_id): Path, +) -> Result>, AppError> { + let locations = svc.list_by_server(server_id).await?; + Ok(Json(locations.into_iter().map(Into::into).collect())) +} + +async fn create_location( + State(svc): State>, + Path(server_id): Path, + Json(body): Json, +) -> Result { + let mut params = CreateLocationBlockParams::from(body); + params.server_id = server_id; + let location = svc.create(params).await?; + Ok((StatusCode::CREATED, Json(LocationBlockResponse::from(location)))) +} + +async fn get_location( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let location = svc.get(id).await?; + Ok(Json(location.into())) +} + +async fn update_location( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let location = svc.update(id, body.into()).await?; + Ok(Json(location.into())) +} + +async fn delete_location( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/server-blocks/{server_id}/locations", + axum::routing::get(list_locations).post(create_location), + ) + .route( + "/locations/{id}", + axum::routing::get(get_location) + .put(update_location) + .delete(delete_location), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/log_settings.rs b/apps/nxmesh-master/src/routes/api/proxy/log_settings.rs new file mode 100644 index 0000000..6b4259d --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/log_settings.rs @@ -0,0 +1,140 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{AppError, ApiRouter}; +use crate::service::proxy::log_setting::{ + CreateLogSettingParams, LogSettingService, UpdateLogSettingParams, +}; +use crate::service::proxy::types::LogSettingConfig; + +#[derive(Serialize)] +pub(crate) struct LogSettingResponse { + pub id: Uuid, + pub access_log_path: Option, + pub error_log_path: Option, + pub log_level: Option, + pub override_of_id: Option, +} + +impl From for LogSettingResponse { + fn from(c: LogSettingConfig) -> Self { + Self { + id: c.id, + access_log_path: c.access_log_path, + error_log_path: c.error_log_path, + log_level: c.log_level, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateLogSettingRequest { + pub access_log_path: Option, + pub error_log_path: Option, + pub log_level: Option, + pub override_of_id: Option, +} + +impl From for CreateLogSettingParams { + fn from(r: CreateLogSettingRequest) -> Self { + Self { + server_id: Uuid::nil(), + access_log_path: r.access_log_path, + error_log_path: r.error_log_path, + log_level: r.log_level, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateLogSettingRequest { + pub server_id: Option, + pub access_log_path: Option>, + pub error_log_path: Option>, + pub log_level: Option>, + pub override_of_id: Option>, +} + +impl From for UpdateLogSettingParams { + fn from(r: UpdateLogSettingRequest) -> Self { + Self { + server_id: r.server_id, + access_log_path: r.access_log_path, + error_log_path: r.error_log_path, + log_level: r.log_level, + override_of_id: r.override_of_id, + } + } +} + +async fn list_log_settings( + State(svc): State>, + Path(server_id): Path, +) -> Result>, AppError> { + let settings = svc.list_by_server(server_id).await?; + Ok(Json(settings.into_iter().map(Into::into).collect())) +} + +async fn create_log_setting( + State(svc): State>, + Path(server_id): Path, + Json(body): Json, +) -> Result { + let mut params = CreateLogSettingParams::from(body); + params.server_id = server_id; + let setting = svc.create(params).await?; + Ok((StatusCode::CREATED, Json(LogSettingResponse::from(setting)))) +} + +async fn get_log_setting( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let setting = svc.get(id).await?; + Ok(Json(setting.into())) +} + +async fn update_log_setting( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let setting = svc.update(id, body.into()).await?; + Ok(Json(setting.into())) +} + +async fn delete_log_setting( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/server-blocks/{server_id}/log-settings", + axum::routing::get(list_log_settings).post(create_log_setting), + ) + .route( + "/log-settings/{id}", + axum::routing::get(get_log_setting) + .put(update_log_setting) + .delete(delete_log_setting), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/mod.rs b/apps/nxmesh-master/src/routes/api/proxy/mod.rs new file mode 100644 index 0000000..3d83237 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/mod.rs @@ -0,0 +1,37 @@ +use crate::routes::api::ApiRouter; + +pub(crate) mod access_rules; +pub(crate) mod agents; +pub(crate) mod cache_zones; +pub(crate) mod config_inheritance; +pub(crate) mod configs; +pub(crate) mod limit_rules; +pub(crate) mod limit_zones; +pub(crate) mod locations; +pub(crate) mod log_settings; +pub(crate) mod proxy_settings; +pub(crate) mod rewrite_rules; +pub(crate) mod server_blocks; +pub(crate) mod ssl_certificates; +pub(crate) mod upstreams; + +#[cfg(test)] +pub(crate) mod test_builder; + +pub async fn get_router() -> ApiRouter { + ApiRouter::new() + .merge(configs::routes()) + .merge(agents::routes()) + .merge(config_inheritance::routes()) + .merge(server_blocks::routes()) + .merge(upstreams::routes()) + .merge(locations::routes()) + .merge(access_rules::routes()) + .merge(cache_zones::routes()) + .merge(limit_rules::routes()) + .merge(limit_zones::routes()) + .merge(log_settings::routes()) + .merge(proxy_settings::routes()) + .merge(rewrite_rules::routes()) + .merge(ssl_certificates::routes()) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/proxy_settings.rs b/apps/nxmesh-master/src/routes/api/proxy/proxy_settings.rs new file mode 100644 index 0000000..3aeb41b --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/proxy_settings.rs @@ -0,0 +1,154 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{AppError, ApiRouter}; +use crate::service::proxy::proxy_setting::{ + CreateProxySettingParams, ProxySettingService, UpdateProxySettingParams, +}; +use crate::service::proxy::types::ProxySettingConfig; + +#[derive(Serialize)] +pub(crate) struct ProxySettingResponse { + pub id: Uuid, + pub location_id: Uuid, + pub read_timeout: Option, + pub connect_timeout: Option, + pub buffer_size: Option, + pub cache_enabled: Option, + pub cache_zone: Option, + pub override_of_id: Option, +} + +impl From for ProxySettingResponse { + fn from(c: ProxySettingConfig) -> Self { + Self { + id: c.id, + location_id: c.location_id, + read_timeout: c.read_timeout, + connect_timeout: c.connect_timeout, + buffer_size: c.buffer_size, + cache_enabled: c.cache_enabled, + cache_zone: c.cache_zone, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateProxySettingRequest { + pub read_timeout: Option, + pub connect_timeout: Option, + pub buffer_size: Option, + pub cache_enabled: Option, + pub cache_zone: Option, + pub override_of_id: Option, +} + +impl From for CreateProxySettingParams { + fn from(r: CreateProxySettingRequest) -> Self { + Self { + location_id: Uuid::nil(), + read_timeout: r.read_timeout, + connect_timeout: r.connect_timeout, + buffer_size: r.buffer_size, + cache_enabled: r.cache_enabled, + cache_zone: r.cache_zone, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateProxySettingRequest { + pub location_id: Option, + pub read_timeout: Option>, + pub connect_timeout: Option>, + pub buffer_size: Option>, + pub cache_enabled: Option>, + pub cache_zone: Option>, + pub override_of_id: Option>, +} + +impl From for UpdateProxySettingParams { + fn from(r: UpdateProxySettingRequest) -> Self { + Self { + location_id: r.location_id, + read_timeout: r.read_timeout, + connect_timeout: r.connect_timeout, + buffer_size: r.buffer_size, + cache_enabled: r.cache_enabled, + cache_zone: r.cache_zone, + override_of_id: r.override_of_id, + } + } +} + +async fn list_proxy_settings( + State(svc): State>, + Path(location_id): Path, +) -> Result>, AppError> { + let settings = svc.list_by_location(location_id).await?; + Ok(Json(settings.into_iter().map(Into::into).collect())) +} + +async fn create_proxy_setting( + State(svc): State>, + Path(location_id): Path, + Json(body): Json, +) -> Result { + let mut params = CreateProxySettingParams::from(body); + params.location_id = location_id; + let setting = svc.create(params).await?; + Ok((StatusCode::CREATED, Json(ProxySettingResponse::from(setting)))) +} + +async fn get_proxy_setting( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let setting = svc.get(id).await?; + Ok(Json(setting.into())) +} + +async fn update_proxy_setting( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let setting = svc.update(id, body.into()).await?; + Ok(Json(setting.into())) +} + +async fn delete_proxy_setting( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/locations/{location_id}/proxy-settings", + axum::routing::get(list_proxy_settings).post(create_proxy_setting), + ) + .route( + "/proxy-settings/{id}", + axum::routing::get(get_proxy_setting) + .put(update_proxy_setting) + .delete(delete_proxy_setting), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/rewrite_rules.rs b/apps/nxmesh-master/src/routes/api/proxy/rewrite_rules.rs new file mode 100644 index 0000000..aa36586 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/rewrite_rules.rs @@ -0,0 +1,148 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{AppError, ApiRouter}; +use crate::service::proxy::rewrite_rule::{ + CreateRewriteRuleParams, RewriteRuleService, UpdateRewriteRuleParams, +}; +use crate::service::proxy::types::RewriteRuleConfig; + +#[derive(Serialize)] +pub(crate) struct RewriteRuleResponse { + pub id: Uuid, + pub location_id: Uuid, + pub pattern: String, + pub replacement: String, + pub flag: Option, + pub priority: i32, + pub override_of_id: Option, +} + +impl From for RewriteRuleResponse { + fn from(c: RewriteRuleConfig) -> Self { + Self { + id: c.id, + location_id: c.location_id, + pattern: c.pattern, + replacement: c.replacement, + flag: c.flag, + priority: c.priority, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateRewriteRuleRequest { + pub pattern: String, + pub replacement: String, + pub flag: Option, + pub priority: i32, + pub override_of_id: Option, +} + +impl From for CreateRewriteRuleParams { + fn from(r: CreateRewriteRuleRequest) -> Self { + Self { + location_id: Uuid::nil(), + pattern: r.pattern, + replacement: r.replacement, + flag: r.flag, + priority: r.priority, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateRewriteRuleRequest { + pub location_id: Option, + pub pattern: Option, + pub replacement: Option, + pub flag: Option>, + pub priority: Option, + pub override_of_id: Option>, +} + +impl From for UpdateRewriteRuleParams { + fn from(r: UpdateRewriteRuleRequest) -> Self { + Self { + location_id: r.location_id, + pattern: r.pattern, + replacement: r.replacement, + flag: r.flag, + priority: r.priority, + override_of_id: r.override_of_id, + } + } +} + +async fn list_rewrite_rules( + State(svc): State>, + Path(location_id): Path, +) -> Result>, AppError> { + let rules = svc.list_by_location(location_id).await?; + Ok(Json(rules.into_iter().map(Into::into).collect())) +} + +async fn create_rewrite_rule( + State(svc): State>, + Path(location_id): Path, + Json(body): Json, +) -> Result { + let mut params = CreateRewriteRuleParams::from(body); + params.location_id = location_id; + let rule = svc.create(params).await?; + Ok((StatusCode::CREATED, Json(RewriteRuleResponse::from(rule)))) +} + +async fn get_rewrite_rule( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let rule = svc.get(id).await?; + Ok(Json(rule.into())) +} + +async fn update_rewrite_rule( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let rule = svc.update(id, body.into()).await?; + Ok(Json(rule.into())) +} + +async fn delete_rewrite_rule( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/locations/{location_id}/rewrite-rules", + axum::routing::get(list_rewrite_rules).post(create_rewrite_rule), + ) + .route( + "/rewrite-rules/{id}", + axum::routing::get(get_rewrite_rule) + .put(update_rewrite_rule) + .delete(delete_rewrite_rule), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/server_blocks.rs b/apps/nxmesh-master/src/routes/api/proxy/server_blocks.rs new file mode 100644 index 0000000..24d126a --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/server_blocks.rs @@ -0,0 +1,142 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{AppError, ApiRouter}; +use crate::service::proxy::server_block::{ + CreateServerBlockParams, ServerBlockService, UpdateServerBlockParams, +}; +use crate::service::proxy::types::ServerBlockConfig; + +#[derive(Serialize)] +pub(crate) struct ServerBlockResponse { + pub id: Uuid, + pub server_name: Option>, + pub listen_port: i32, + pub ssl_enabled: Option, + pub override_of_id: Option, +} + +impl From for ServerBlockResponse { + fn from(c: ServerBlockConfig) -> Self { + Self { + id: c.id, + server_name: c.server_name, + listen_port: c.listen_port, + ssl_enabled: c.ssl_enabled, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateServerBlockRequest { + pub server_name: Option>, + pub listen_port: i32, + pub ssl_enabled: Option, + pub ssl_cert_id: Option, + pub override_of_id: Option, +} + +impl From for CreateServerBlockParams { + fn from(r: CreateServerBlockRequest) -> Self { + Self { + config_id: Uuid::nil(), + server_name: r.server_name, + listen_port: r.listen_port, + ssl_enabled: r.ssl_enabled, + ssl_cert_id: r.ssl_cert_id, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateServerBlockRequest { + pub server_name: Option>>, + pub listen_port: Option, + pub ssl_enabled: Option>, + pub ssl_cert_id: Option>, + pub override_of_id: Option>, +} + +impl From for UpdateServerBlockParams { + fn from(r: UpdateServerBlockRequest) -> Self { + Self { + server_name: r.server_name, + listen_port: r.listen_port, + ssl_enabled: r.ssl_enabled, + ssl_cert_id: r.ssl_cert_id, + override_of_id: r.override_of_id, + } + } +} + +async fn list_server_blocks( + State(svc): State>, + Path(config_id): Path, +) -> Result>, AppError> { + let blocks = svc.list_by_config(config_id).await?; + Ok(Json(blocks.into_iter().map(Into::into).collect())) +} + +async fn create_server_block( + State(svc): State>, + Path(config_id): Path, + Json(body): Json, +) -> Result { + let mut params = CreateServerBlockParams::from(body); + params.config_id = config_id; + let block = svc.create(params).await?; + Ok((StatusCode::CREATED, Json(ServerBlockResponse::from(block)))) +} + +async fn get_server_block( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let block = svc.get(id).await?; + Ok(Json(block.into())) +} + +async fn update_server_block( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let block = svc.update(id, body.into()).await?; + Ok(Json(block.into())) +} + +async fn delete_server_block( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/configs/{config_id}/server-blocks", + axum::routing::get(list_server_blocks).post(create_server_block), + ) + .route( + "/server-blocks/{id}", + axum::routing::get(get_server_block) + .put(update_server_block) + .delete(delete_server_block), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/ssl_certificates.rs b/apps/nxmesh-master/src/routes/api/proxy/ssl_certificates.rs new file mode 100644 index 0000000..5331e76 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/ssl_certificates.rs @@ -0,0 +1,133 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{AppError, ApiRouter}; +use crate::service::proxy::ssl_certificate::{ + CreateSslCertificateParams, SslCertificateService, UpdateSslCertificateParams, +}; +use crate::service::proxy::types::SslCertificateConfig; + +#[derive(Serialize)] +pub(crate) struct SslCertificateResponse { + pub id: Uuid, + pub name: String, + pub cert_path: String, + pub key_path: String, + pub expiry_date: String, +} + +impl From for SslCertificateResponse { + fn from(c: SslCertificateConfig) -> Self { + Self { + id: c.id, + name: c.name, + cert_path: c.cert_path, + key_path: c.key_path, + expiry_date: c.expiry_date.to_rfc3339(), + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateSslCertificateRequest { + pub name: String, + pub cert_path: String, + pub key_path: String, + pub expiry_date: chrono::DateTime, +} + +impl From for CreateSslCertificateParams { + fn from(r: CreateSslCertificateRequest) -> Self { + Self { + name: r.name, + cert_path: r.cert_path, + key_path: r.key_path, + expiry_date: r.expiry_date, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateSslCertificateRequest { + pub name: Option, + pub cert_path: Option, + pub key_path: Option, + pub expiry_date: Option>, +} + +impl From for UpdateSslCertificateParams { + fn from(r: UpdateSslCertificateRequest) -> Self { + Self { + name: r.name, + cert_path: r.cert_path, + key_path: r.key_path, + expiry_date: r.expiry_date, + } + } +} + +async fn list_ssl_certificates( + State(svc): State>, +) -> Result>, AppError> { + let certs = svc.list().await?; + Ok(Json(certs.into_iter().map(Into::into).collect())) +} + +async fn create_ssl_certificate( + State(svc): State>, + Json(body): Json, +) -> Result { + let cert = svc.create(body.into()).await?; + Ok((StatusCode::CREATED, Json(SslCertificateResponse::from(cert)))) +} + +async fn get_ssl_certificate( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let cert = svc.get(id).await?; + Ok(Json(cert.into())) +} + +async fn update_ssl_certificate( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let cert = svc.update(id, body.into()).await?; + Ok(Json(cert.into())) +} + +async fn delete_ssl_certificate( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/ssl-certificates", + axum::routing::get(list_ssl_certificates).post(create_ssl_certificate), + ) + .route( + "/ssl-certificates/{id}", + axum::routing::get(get_ssl_certificate) + .put(update_ssl_certificate) + .delete(delete_ssl_certificate), + ) +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/test_builder.rs b/apps/nxmesh-master/src/routes/api/proxy/test_builder.rs new file mode 100644 index 0000000..47ff21c --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/test_builder.rs @@ -0,0 +1,174 @@ +use std::sync::Arc; + +use axum_test::TestServer; + +use crate::routes::api::{ApiState, LocalApiState}; +use crate::service::proxy::*; +use crate::service::proxy::{ + access_rule::MockAccessRuleService, + cache_zone::MockCacheZoneService, + config_inheritance::MockConfigInheritanceService, + limit_rule::MockLimitRuleService, + limit_zone::MockLimitZoneService, + location_block::MockLocationBlockService, + log_setting::MockLogSettingService, + proxy_setting::MockProxySettingService, + rewrite_rule::MockRewriteRuleService, + server_block::MockServerBlockService, + ssl_certificate::MockSslCertificateService, + upstream::MockUpstreamService, +}; + +pub(crate) struct TestProxyApiBuilder { + proxy_service: Option, + server_block_service: Option, + upstream_service: Option, + location_block_service: Option, + access_rule_service: Option, + cache_zone_service: Option, + limit_rule_service: Option, + limit_zone_service: Option, + log_setting_service: Option, + proxy_setting_service: Option, + rewrite_rule_service: Option, + ssl_certificate_service: Option, + config_inheritance_service: Option, +} + +impl TestProxyApiBuilder { + pub fn new() -> Self { + Self { + proxy_service: None, + server_block_service: None, + upstream_service: None, + location_block_service: None, + access_rule_service: None, + cache_zone_service: None, + limit_rule_service: None, + limit_zone_service: None, + log_setting_service: None, + proxy_setting_service: None, + rewrite_rule_service: None, + ssl_certificate_service: None, + config_inheritance_service: None, + } + } + + pub fn with_proxy(mut self, mock: MockProxyServiceTrait) -> Self { + self.proxy_service = Some(mock); + self + } + + pub fn with_server_block(mut self, mock: MockServerBlockService) -> Self { + self.server_block_service = Some(mock); + self + } + + pub fn with_upstream(mut self, mock: MockUpstreamService) -> Self { + self.upstream_service = Some(mock); + self + } + + pub fn with_location_block(mut self, mock: MockLocationBlockService) -> Self { + self.location_block_service = Some(mock); + self + } + + pub fn with_access_rule(mut self, mock: MockAccessRuleService) -> Self { + self.access_rule_service = Some(mock); + self + } + + pub fn with_cache_zone(mut self, mock: MockCacheZoneService) -> Self { + self.cache_zone_service = Some(mock); + self + } + + pub fn with_limit_rule(mut self, mock: MockLimitRuleService) -> Self { + self.limit_rule_service = Some(mock); + self + } + + pub fn with_limit_zone(mut self, mock: MockLimitZoneService) -> Self { + self.limit_zone_service = Some(mock); + self + } + + pub fn with_log_setting(mut self, mock: MockLogSettingService) -> Self { + self.log_setting_service = Some(mock); + self + } + + pub fn with_proxy_setting(mut self, mock: MockProxySettingService) -> Self { + self.proxy_setting_service = Some(mock); + self + } + + pub fn with_rewrite_rule(mut self, mock: MockRewriteRuleService) -> Self { + self.rewrite_rule_service = Some(mock); + self + } + + pub fn with_ssl_certificate(mut self, mock: MockSslCertificateService) -> Self { + self.ssl_certificate_service = Some(mock); + self + } + + pub fn with_config_inheritance(mut self, mock: MockConfigInheritanceService) -> Self { + self.config_inheritance_service = Some(mock); + self + } + + pub async fn build(self) -> TestServer { + let state = ApiState { + proxy_service: Arc::new(self.proxy_service.unwrap_or_else(MockProxyServiceTrait::new)), + server_block_service: Arc::new( + self.server_block_service.unwrap_or_else(MockServerBlockService::new), + ), + upstream_service: Arc::new( + self.upstream_service.unwrap_or_else(MockUpstreamService::new), + ), + location_block_service: Arc::new( + self.location_block_service + .unwrap_or_else(MockLocationBlockService::new), + ), + access_rule_service: Arc::new( + self.access_rule_service.unwrap_or_else(MockAccessRuleService::new), + ), + cache_zone_service: Arc::new( + self.cache_zone_service.unwrap_or_else(MockCacheZoneService::new), + ), + limit_rule_service: Arc::new( + self.limit_rule_service.unwrap_or_else(MockLimitRuleService::new), + ), + limit_zone_service: Arc::new( + self.limit_zone_service.unwrap_or_else(MockLimitZoneService::new), + ), + log_setting_service: Arc::new( + self.log_setting_service.unwrap_or_else(MockLogSettingService::new), + ), + proxy_setting_service: Arc::new( + self.proxy_setting_service + .unwrap_or_else(MockProxySettingService::new), + ), + rewrite_rule_service: Arc::new( + self.rewrite_rule_service + .unwrap_or_else(MockRewriteRuleService::new), + ), + ssl_certificate_service: Arc::new( + self.ssl_certificate_service + .unwrap_or_else(MockSslCertificateService::new), + ), + config_inheritance_service: Arc::new( + self.config_inheritance_service + .unwrap_or_else(MockConfigInheritanceService::new), + ), + // Keep agent_service for ApiState completeness; not used by proxy routes + agent_service: Arc::new(crate::service::agent::MockAgentService::new()), + }; + let app = super::get_router() + .await + .with_state(LocalApiState(Arc::new(state))); + TestServer::new(app) + } +} diff --git a/apps/nxmesh-master/src/routes/api/proxy/upstreams.rs b/apps/nxmesh-master/src/routes/api/proxy/upstreams.rs new file mode 100644 index 0000000..5d8e5c6 --- /dev/null +++ b/apps/nxmesh-master/src/routes/api/proxy/upstreams.rs @@ -0,0 +1,144 @@ +use std::sync::Arc; + +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::routes::api::{AppError, ApiRouter}; +use crate::service::proxy::types::UpstreamConfig; +use crate::service::proxy::upstream::{ + CreateUpstreamParams, UpstreamService, UpdateUpstreamParams, +}; + +#[derive(Serialize)] +pub(crate) struct UpstreamResponse { + pub id: Uuid, + pub name: String, + pub target_host: String, + pub target_port: i32, + pub metadata: Option, + pub override_of_id: Option, +} + +impl From for UpstreamResponse { + fn from(c: UpstreamConfig) -> Self { + Self { + id: c.id, + name: c.name, + target_host: c.target_host, + target_port: c.target_port, + metadata: c.metadata, + override_of_id: c.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct CreateUpstreamRequest { + pub name: String, + pub target_host: String, + pub target_port: i32, + pub metadata: Option, + pub override_of_id: Option, +} + +impl From for CreateUpstreamParams { + fn from(r: CreateUpstreamRequest) -> Self { + Self { + config_id: Uuid::nil(), + name: r.name, + target_host: r.target_host, + target_port: r.target_port, + metadata: r.metadata, + override_of_id: r.override_of_id, + } + } +} + +#[derive(Deserialize)] +pub(crate) struct UpdateUpstreamRequest { + pub name: Option, + pub target_host: Option, + pub target_port: Option, + pub metadata: Option>, + pub override_of_id: Option>, +} + +impl From for UpdateUpstreamParams { + fn from(r: UpdateUpstreamRequest) -> Self { + Self { + name: r.name, + target_host: r.target_host, + target_port: r.target_port, + metadata: r.metadata, + override_of_id: r.override_of_id, + } + } +} + +async fn list_upstreams( + State(svc): State>, + Path(config_id): Path, +) -> Result>, AppError> { + let upstreams = svc.list_by_config(config_id).await?; + Ok(Json(upstreams.into_iter().map(Into::into).collect())) +} + +async fn create_upstream( + State(svc): State>, + Path(config_id): Path, + Json(body): Json, +) -> Result { + let mut params = CreateUpstreamParams::from(body); + params.config_id = config_id; + let upstream = svc.create(params).await?; + Ok((StatusCode::CREATED, Json(UpstreamResponse::from(upstream)))) +} + +async fn get_upstream( + State(svc): State>, + Path(id): Path, +) -> Result, AppError> { + let upstream = svc.get(id).await?; + Ok(Json(upstream.into())) +} + +async fn update_upstream( + State(svc): State>, + Path(id): Path, + Json(body): Json, +) -> Result, AppError> { + let upstream = svc.update(id, body.into()).await?; + Ok(Json(upstream.into())) +} + +async fn delete_upstream( + State(svc): State>, + Path(id): Path, +) -> Result { + let deleted = svc.delete(id).await?; + if deleted { + Ok((StatusCode::NO_CONTENT,)) + } else { + Err(AppError::NotFound) + } +} + +pub(super) fn routes() -> ApiRouter { + ApiRouter::new() + .route( + "/configs/{config_id}/upstreams", + axum::routing::get(list_upstreams).post(create_upstream), + ) + .route( + "/upstreams/{id}", + axum::routing::get(get_upstream) + .put(update_upstream) + .delete(delete_upstream), + ) +} diff --git a/apps/nxmesh-master/src/routes/mod.rs b/apps/nxmesh-master/src/routes/mod.rs index 0583ef2..9f9e863 100644 --- a/apps/nxmesh-master/src/routes/mod.rs +++ b/apps/nxmesh-master/src/routes/mod.rs @@ -1,21 +1,44 @@ +use std::sync::Arc; + use axum::Router; +pub mod api; mod frontend; -pub async fn get_root_router() -> Router { +pub async fn get_root_router(api_state: impl Into>) -> Router { Router::new() .merge(frontend::get_router().await) + .nest("/api", api::get_router(api_state.into()).await) .fallback(frontend::get_fallback_handler().await) } #[cfg(test)] mod tests { + use crate::service::agent::MockAgentService; + use super::*; use axum_test::TestServer; #[tokio::test] async fn test_should_return_index_html_for_root_path() { - let router = get_root_router().await; + use crate::service::proxy::*; + let state = Arc::new(api::ApiState { + agent_service: Arc::new(MockAgentService::new()), + proxy_service: Arc::new(MockProxyServiceTrait::new()), + server_block_service: Arc::new(server_block::MockServerBlockService::new()), + upstream_service: Arc::new(upstream::MockUpstreamService::new()), + location_block_service: Arc::new(location_block::MockLocationBlockService::new()), + access_rule_service: Arc::new(access_rule::MockAccessRuleService::new()), + cache_zone_service: Arc::new(cache_zone::MockCacheZoneService::new()), + limit_rule_service: Arc::new(limit_rule::MockLimitRuleService::new()), + limit_zone_service: Arc::new(limit_zone::MockLimitZoneService::new()), + log_setting_service: Arc::new(log_setting::MockLogSettingService::new()), + proxy_setting_service: Arc::new(proxy_setting::MockProxySettingService::new()), + rewrite_rule_service: Arc::new(rewrite_rule::MockRewriteRuleService::new()), + ssl_certificate_service: Arc::new(ssl_certificate::MockSslCertificateService::new()), + config_inheritance_service: Arc::new(config_inheritance::MockConfigInheritanceService::new()), + }); + let router = get_root_router(state).await; let server = TestServer::new(router); let response = server.get("/").await; assert_eq!(response.status_code(), 200); @@ -23,7 +46,24 @@ mod tests { #[tokio::test] async fn test_should_return_index_html_for_nonexistent_path() { - let router = get_root_router().await; + use crate::service::proxy::*; + let state = Arc::new(api::ApiState { + agent_service: Arc::new(MockAgentService::new()), + proxy_service: Arc::new(MockProxyServiceTrait::new()), + server_block_service: Arc::new(server_block::MockServerBlockService::new()), + upstream_service: Arc::new(upstream::MockUpstreamService::new()), + location_block_service: Arc::new(location_block::MockLocationBlockService::new()), + access_rule_service: Arc::new(access_rule::MockAccessRuleService::new()), + cache_zone_service: Arc::new(cache_zone::MockCacheZoneService::new()), + limit_rule_service: Arc::new(limit_rule::MockLimitRuleService::new()), + limit_zone_service: Arc::new(limit_zone::MockLimitZoneService::new()), + log_setting_service: Arc::new(log_setting::MockLogSettingService::new()), + proxy_setting_service: Arc::new(proxy_setting::MockProxySettingService::new()), + rewrite_rule_service: Arc::new(rewrite_rule::MockRewriteRuleService::new()), + ssl_certificate_service: Arc::new(ssl_certificate::MockSslCertificateService::new()), + config_inheritance_service: Arc::new(config_inheritance::MockConfigInheritanceService::new()), + }); + let router = get_root_router(state).await; let server = TestServer::new(router); let fallback_response = server.get("/nonexistent").await; assert_eq!(fallback_response.status_code(), 200); diff --git a/apps/nxmesh-master/src/service/mod.rs b/apps/nxmesh-master/src/service/mod.rs index c0e1601..7880c9c 100644 --- a/apps/nxmesh-master/src/service/mod.rs +++ b/apps/nxmesh-master/src/service/mod.rs @@ -6,6 +6,7 @@ use crate::{connector::agent::AgentConnectorTrait, service::certificate::Certifi pub mod agent; pub mod certificate; +pub mod error; pub mod proxy; pub async fn start_master_server( @@ -51,7 +52,68 @@ pub async fn start_master_server( } }); - let axum_router = crate::routes::get_root_router().await; + let api_state = crate::routes::api::ApiState { + agent_service: Arc::new(crate::service::agent::AgentServiceImpl::new( + db_connection.clone(), + )), + proxy_service: Arc::new(crate::service::proxy::service::ProxyServiceImpl::new( + db_connection.clone(), + )), + server_block_service: Arc::new( + crate::service::proxy::server_block::ServerBlockServiceImpl::new( + db_connection.clone(), + ), + ), + upstream_service: Arc::new( + crate::service::proxy::upstream::UpstreamServiceImpl::new(db_connection.clone()), + ), + location_block_service: Arc::new( + crate::service::proxy::location_block::LocationBlockServiceImpl::new( + db_connection.clone(), + ), + ), + access_rule_service: Arc::new( + crate::service::proxy::access_rule::AccessRuleServiceImpl::new( + db_connection.clone(), + ), + ), + cache_zone_service: Arc::new( + crate::service::proxy::cache_zone::CacheZoneServiceImpl::new(db_connection.clone()), + ), + limit_rule_service: Arc::new( + crate::service::proxy::limit_rule::LimitRuleServiceImpl::new(db_connection.clone()), + ), + limit_zone_service: Arc::new( + crate::service::proxy::limit_zone::LimitZoneServiceImpl::new(db_connection.clone()), + ), + log_setting_service: Arc::new( + crate::service::proxy::log_setting::LogSettingServiceImpl::new( + db_connection.clone(), + ), + ), + proxy_setting_service: Arc::new( + crate::service::proxy::proxy_setting::ProxySettingServiceImpl::new( + db_connection.clone(), + ), + ), + rewrite_rule_service: Arc::new( + crate::service::proxy::rewrite_rule::RewriteRuleServiceImpl::new( + db_connection.clone(), + ), + ), + ssl_certificate_service: Arc::new( + crate::service::proxy::ssl_certificate::SslCertificateServiceImpl::new( + db_connection.clone(), + ), + ), + config_inheritance_service: Arc::new( + crate::service::proxy::config_inheritance::ConfigInheritanceServiceImpl::new( + db_connection.clone(), + ), + ), + }; + + let axum_router = crate::routes::get_root_router(Arc::new(api_state)).await; // Start the HTTP server let addr = format!("{}:{}", settings.server.bind_address, settings.server.port)