feature/proxy-service #14
@@ -25,6 +25,17 @@ pub mod tag {
|
||||
crate::routes::api::restricted::nginx::upstream::update_upstream_target::update_upstream_target,
|
||||
crate::routes::api::restricted::nginx::upstream::remove_upstream::remove_upstream,
|
||||
crate::routes::api::restricted::nginx::upstream::remove_upstream_target::remove_upstream_target,
|
||||
// Nginx proxy host management
|
||||
crate::routes::api::restricted::nginx::proxy_host::get_proxy::get_proxy_list,
|
||||
crate::routes::api::restricted::nginx::proxy_host::get_proxy::get_proxy,
|
||||
crate::routes::api::restricted::nginx::proxy_host::create_proxy::create_proxy,
|
||||
crate::routes::api::restricted::nginx::proxy_host::update_proxy::update_proxy,
|
||||
crate::routes::api::restricted::nginx::proxy_host::remove_proxy::remove_proxy,
|
||||
// Proxy host locations
|
||||
crate::routes::api::restricted::nginx::proxy_host::create_location::create_location,
|
||||
crate::routes::api::restricted::nginx::proxy_host::get_location::get_location,
|
||||
crate::routes::api::restricted::nginx::proxy_host::update_location::update_location,
|
||||
crate::routes::api::restricted::nginx::proxy_host::remove_location::remove_location,
|
||||
),
|
||||
components(
|
||||
schemas(crate::routes::api::health::info::HealthInfo),
|
||||
@@ -46,6 +57,15 @@ pub mod tag {
|
||||
schemas(crate::routes::api::restricted::nginx::upstream::update_upstream_target::UpdateUpstreamTargetRequestBody),
|
||||
schemas(crate::routes::api::restricted::nginx::upstream::info::response::UpdateUpstreamInfoResponse),
|
||||
schemas(crate::routes::api::restricted::nginx::upstream::info::response::UpdateUpstreamTargetInfoResponse),
|
||||
// Nginx proxy host schemas
|
||||
schemas(crate::routes::api::restricted::nginx::proxy_host::create_proxy::CreateLocationReq),
|
||||
schemas(crate::routes::api::restricted::nginx::proxy_host::create_proxy::CreateProxyRequestBody),
|
||||
schemas(crate::routes::api::restricted::nginx::proxy_host::create_location::CreateLocationRequestBody),
|
||||
schemas(crate::routes::api::restricted::nginx::proxy_host::update_proxy::UpdateProxyRequestBody),
|
||||
schemas(crate::routes::api::restricted::nginx::proxy_host::update_location::UpdateLocationRequestBody),
|
||||
schemas(crate::routes::api::restricted::nginx::proxy_host::info::response::LocationInfoResponse),
|
||||
schemas(crate::routes::api::restricted::nginx::proxy_host::info::response::ProxyHostInfoResponse),
|
||||
schemas(crate::routes::api::restricted::nginx::proxy_host::info::response::ProxyListResponse),
|
||||
),
|
||||
tags(
|
||||
(name = tag::HEALTH_TAG, description = "Health information API"),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod proxy_host;
|
||||
pub mod upstream;
|
||||
|
||||
use std::sync::Arc;
|
||||
@@ -7,5 +8,7 @@ use axum::Router;
|
||||
use crate::routes::AppState;
|
||||
|
||||
pub fn get_nginx_router(state: Arc<AppState>) -> Router {
|
||||
Router::new().merge(upstream::get_upstream_router(state.clone()))
|
||||
Router::new()
|
||||
.merge(proxy_host::get_proxy_router(state.clone()))
|
||||
.merge(upstream::get_upstream_router(state.clone()))
|
||||
}
|
||||
|
||||
43
apps/api/src/routes/api/restricted/nginx/proxy_host.rs
Normal file
43
apps/api/src/routes/api/restricted/nginx/proxy_host.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
pub mod create_location;
|
||||
pub mod create_proxy;
|
||||
pub mod get_location;
|
||||
pub mod get_proxy;
|
||||
pub mod info;
|
||||
pub mod remove_location;
|
||||
pub mod remove_proxy;
|
||||
pub mod update_location;
|
||||
pub mod update_proxy;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, post},
|
||||
};
|
||||
|
||||
use crate::routes::AppState;
|
||||
|
||||
pub fn get_proxy_router(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/proxy_hosts",
|
||||
get(get_proxy::get_proxy_list).post(create_proxy::create_proxy),
|
||||
)
|
||||
.route(
|
||||
"/proxy_hosts/{proxy_id}",
|
||||
get(get_proxy::get_proxy)
|
||||
.patch(update_proxy::update_proxy)
|
||||
.delete(remove_proxy::remove_proxy),
|
||||
)
|
||||
.route(
|
||||
"/proxy_hosts/{proxy_id}/locations",
|
||||
post(create_location::create_location),
|
||||
)
|
||||
.route(
|
||||
"/locations/{location_id}",
|
||||
get(get_location::get_location)
|
||||
.patch(update_location::update_location)
|
||||
.delete(remove_location::remove_location),
|
||||
)
|
||||
.with_state(state)
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{Json, extract::State, response::Result as AxumResult};
|
||||
use sea_orm::TransactionTrait;
|
||||
|
||||
use crate::{
|
||||
errors::api_error::ApiError,
|
||||
middlewares::request_info::AuthenticatedRequestInfo,
|
||||
routes::{
|
||||
AppState,
|
||||
api::{
|
||||
openapi::tag::NGINX_TAG,
|
||||
restricted::nginx::proxy_host::info::response::LocationInfoResponse,
|
||||
},
|
||||
},
|
||||
services::nginx::info::location::CreateLocationInfo,
|
||||
};
|
||||
|
||||
#[derive(serde::Deserialize, utoipa::ToSchema, serde::Serialize)]
|
||||
pub struct CreateLocationRequestBody {
|
||||
pub path: String,
|
||||
pub match_type: String,
|
||||
pub order: i64,
|
||||
pub upstream_id: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum_test::TestServer;
|
||||
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
||||
|
||||
use database::generated::entities::{location, upstream, upstream_target};
|
||||
|
||||
use crate::{
|
||||
configs::{FromConfig, ProgramSettings},
|
||||
middlewares::require_auth::mock::REQUEST_AUTH_USER_INVALID_HEADER,
|
||||
routes::api::restricted::nginx::proxy_host::create_location::CreateLocationRequestBody,
|
||||
routes::api::restricted::nginx::proxy_host::get_proxy_router,
|
||||
services::{agent_client::MockAgentService, get_mock_app_service},
|
||||
};
|
||||
|
||||
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||
let program_settings = ProgramSettings::mock();
|
||||
let mut mock = MockAgentService::new();
|
||||
mock.expect_validate().returning(|_cfg| Ok(()));
|
||||
mock.expect_apply().returning(|_cfg| Ok(()));
|
||||
let mock_agent = Arc::new(mock);
|
||||
let app_service =
|
||||
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
|
||||
let state = Arc::new(crate::routes::AppState {
|
||||
database_connection: Arc::new(db),
|
||||
service: Arc::new(app_service),
|
||||
config: Arc::new(program_settings),
|
||||
});
|
||||
get_proxy_router(state).layer(axum::middleware::from_fn(
|
||||
crate::middlewares::require_auth::mock::mock_require_auth,
|
||||
))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_create_location_succeeds_returns_created() {
|
||||
let ph_id = uuid::Uuid::new_v4();
|
||||
let loc_id = uuid::Uuid::new_v4();
|
||||
|
||||
let loc_model = location::Model {
|
||||
id: loc_id,
|
||||
host_id: ph_id,
|
||||
path: "/".to_string(),
|
||||
match_type: "prefix".to_string(),
|
||||
order: 1,
|
||||
upstream_id: None,
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let up_id = uuid::Uuid::new_v4();
|
||||
let up_model = upstream::Model {
|
||||
id: up_id,
|
||||
name: "u1".to_string(),
|
||||
protocol: "http".to_string(),
|
||||
algorithm: "rr".to_string(),
|
||||
sticky_session: false,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
let target_model = upstream_target::Model {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
upstream_id: up_id,
|
||||
target_host: "127.0.0.1".to_string(),
|
||||
target_port: 8080,
|
||||
weight: 1,
|
||||
is_backup: false,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![vec![loc_model.clone()]])
|
||||
// additional query result for regenerate_and_apply_config -> generate_config
|
||||
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
|
||||
.into_connection();
|
||||
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let payload = CreateLocationRequestBody {
|
||||
path: "/".to_string(),
|
||||
match_type: "prefix".to_string(),
|
||||
order: 1,
|
||||
upstream_id: None,
|
||||
};
|
||||
|
||||
let res = server
|
||||
.post(&format!("/proxy_hosts/{}/locations", ph_id))
|
||||
.json(&payload)
|
||||
.await;
|
||||
res.assert_status_ok();
|
||||
let body = res.json::<crate::routes::api::restricted::nginx::proxy_host::info::response::LocationInfoResponse>();
|
||||
assert_eq!(body.id, loc_id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_create_location_invalid_payload_returns_bad_request() {
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite).into_connection();
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let res = server
|
||||
.post(&format!("/proxy_hosts/{}/locations", uuid::Uuid::new_v4()))
|
||||
.json(&serde_json::json!({}))
|
||||
.await;
|
||||
res.assert_status(StatusCode::UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_create_location_unauthenticated_returns_unauthorized() {
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite).into_connection();
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let payload = CreateLocationRequestBody {
|
||||
path: "/".to_string(),
|
||||
match_type: "prefix".to_string(),
|
||||
order: 1,
|
||||
upstream_id: None,
|
||||
};
|
||||
|
||||
let res = server
|
||||
.post(&format!("/proxy_hosts/{}/locations", uuid::Uuid::new_v4()))
|
||||
.add_header(REQUEST_AUTH_USER_INVALID_HEADER, "true")
|
||||
.json(&payload)
|
||||
.await;
|
||||
|
||||
res.assert_status(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(uuid::Uuid, CreateLocationRequestBody)> for CreateLocationInfo {
|
||||
fn from(val: (uuid::Uuid, CreateLocationRequestBody)) -> Self {
|
||||
Self {
|
||||
host_id: val.0,
|
||||
path: val.1.path,
|
||||
match_type: val.1.match_type,
|
||||
order: val.1.order,
|
||||
upstream_id: val.1.upstream_id,
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/nginx/proxy_hosts/{proxy_id}/locations",
|
||||
request_body = CreateLocationRequestBody,
|
||||
responses(
|
||||
(status = 200, description = "Location created", body = LocationInfoResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 422, description = "Invalid request"),
|
||||
(status = 500, description = "Internal server error"),
|
||||
),
|
||||
tag = NGINX_TAG,
|
||||
)]
|
||||
pub async fn create_location(
|
||||
_request_info: AuthenticatedRequestInfo,
|
||||
axum::extract::Path(proxy_id): axum::extract::Path<uuid::Uuid>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<CreateLocationRequestBody>,
|
||||
) -> AxumResult<Json<LocationInfoResponse>, ApiError> {
|
||||
let svc = &state.service.nginx.get_location_service();
|
||||
let create_info: CreateLocationInfo = (proxy_id, payload).into();
|
||||
|
||||
let mut tx = state.database_connection.begin().await?;
|
||||
let info = svc.create_location(create_info, Some(&mut tx)).await?;
|
||||
state
|
||||
.service
|
||||
.nginx
|
||||
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Json(info.into()))
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{Json, extract::State, response::Result as AxumResult};
|
||||
use sea_orm::TransactionTrait;
|
||||
|
||||
use crate::{
|
||||
errors::api_error::ApiError,
|
||||
middlewares::request_info::AuthenticatedRequestInfo,
|
||||
routes::{
|
||||
AppState,
|
||||
api::{
|
||||
openapi::tag::NGINX_TAG,
|
||||
restricted::nginx::proxy_host::info::response::ProxyHostInfoResponse,
|
||||
},
|
||||
},
|
||||
services::nginx::info::proxy_host::ProxyHostCreateInfo,
|
||||
};
|
||||
|
||||
#[derive(serde::Deserialize, utoipa::ToSchema, serde::Serialize)]
|
||||
pub struct CreateLocationReq {
|
||||
pub path: String,
|
||||
pub match_type: String,
|
||||
pub order: i64,
|
||||
pub upstream_id: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum_test::TestServer;
|
||||
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
||||
|
||||
use database::generated::entities::{location, proxy_host, upstream, upstream_target};
|
||||
|
||||
use crate::{
|
||||
configs::{FromConfig, ProgramSettings},
|
||||
middlewares::require_auth::mock::REQUEST_AUTH_USER_INVALID_HEADER,
|
||||
routes::api::restricted::nginx::proxy_host::create_proxy::CreateLocationReq as ReqLocation,
|
||||
routes::api::restricted::nginx::proxy_host::create_proxy::CreateProxyRequestBody,
|
||||
routes::api::restricted::nginx::proxy_host::get_proxy_router,
|
||||
services::{agent_client::MockAgentService, get_mock_app_service},
|
||||
};
|
||||
|
||||
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||
let program_settings = ProgramSettings::mock();
|
||||
let mut mock = MockAgentService::new();
|
||||
mock.expect_validate().returning(|_cfg| Ok(()));
|
||||
mock.expect_apply().returning(|_cfg| Ok(()));
|
||||
let mock_agent = Arc::new(mock);
|
||||
let app_service =
|
||||
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
|
||||
let state = Arc::new(crate::routes::AppState {
|
||||
database_connection: Arc::new(db),
|
||||
service: Arc::new(app_service),
|
||||
config: Arc::new(program_settings),
|
||||
});
|
||||
get_proxy_router(state).layer(axum::middleware::from_fn(
|
||||
crate::middlewares::require_auth::mock::mock_require_auth,
|
||||
))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_create_proxy_succeeds_returns_created() {
|
||||
let ph_id = uuid::Uuid::new_v4();
|
||||
|
||||
let ph_model = proxy_host::Model {
|
||||
id: ph_id,
|
||||
name: Some("myproxy".to_string()),
|
||||
domain: "example.com".to_string(),
|
||||
scheme: "http".to_string(),
|
||||
listen_port: 80,
|
||||
forward_scheme: "http".to_string(),
|
||||
forward_host: None,
|
||||
forward_port: None,
|
||||
preserve_host_header: false,
|
||||
enable_websocket: false,
|
||||
enabled: true,
|
||||
meta: None,
|
||||
default_upstream_id: None,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let loc_id = uuid::Uuid::new_v4();
|
||||
let loc_model = location::Model {
|
||||
id: loc_id,
|
||||
host_id: ph_id,
|
||||
path: "/".to_string(),
|
||||
match_type: "prefix".to_string(),
|
||||
order: 1,
|
||||
upstream_id: None,
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let up_id = uuid::Uuid::new_v4();
|
||||
let up_model = upstream::Model {
|
||||
id: up_id,
|
||||
name: "u1".to_string(),
|
||||
protocol: "http".to_string(),
|
||||
algorithm: "rr".to_string(),
|
||||
sticky_session: false,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
let target_model = upstream_target::Model {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
upstream_id: up_id,
|
||||
target_host: "127.0.0.1".to_string(),
|
||||
target_port: 8080,
|
||||
weight: 1,
|
||||
is_backup: false,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![vec![ph_model.clone()]])
|
||||
.append_query_results(vec![vec![loc_model.clone()]])
|
||||
// additional query result for regenerate_and_apply_config -> generate_config
|
||||
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
|
||||
.into_connection();
|
||||
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let payload = CreateProxyRequestBody {
|
||||
name: Some("myproxy".to_string()),
|
||||
domain: "example.com".to_string(),
|
||||
scheme: "http".to_string(),
|
||||
listen_port: 80,
|
||||
forward_scheme: "http".to_string(),
|
||||
forward_host: None,
|
||||
forward_port: None,
|
||||
preserve_host_header: false,
|
||||
enable_websocket: false,
|
||||
enabled: true,
|
||||
meta: None,
|
||||
default_upstream_id: None,
|
||||
locations: vec![ReqLocation {
|
||||
path: "/".to_string(),
|
||||
match_type: "prefix".to_string(),
|
||||
order: 1,
|
||||
upstream_id: None,
|
||||
}],
|
||||
};
|
||||
|
||||
let res = server.post("/proxy_hosts").json(&payload).await;
|
||||
res.assert_status_ok();
|
||||
let body: crate::routes::api::restricted::nginx::proxy_host::info::response::ProxyHostInfoResponse =
|
||||
res.json();
|
||||
assert_eq!(body.id, ph_id);
|
||||
assert_eq!(body.domain, "example.com");
|
||||
assert_eq!(body.locations.len(), 1);
|
||||
assert_eq!(body.locations[0].id, loc_id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_create_proxy_invalid_payload_returns_bad_request() {
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite).into_connection();
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let res = server
|
||||
.post("/proxy_hosts")
|
||||
.json(&serde_json::json!({}))
|
||||
.await;
|
||||
res.assert_status(StatusCode::UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_create_proxy_unauthenticated_returns_unauthorized() {
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite).into_connection();
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let payload = CreateProxyRequestBody {
|
||||
name: Some("myproxy".to_string()),
|
||||
domain: "example.com".to_string(),
|
||||
scheme: "http".to_string(),
|
||||
listen_port: 80,
|
||||
forward_scheme: "http".to_string(),
|
||||
forward_host: None,
|
||||
forward_port: None,
|
||||
preserve_host_header: false,
|
||||
enable_websocket: false,
|
||||
enabled: true,
|
||||
meta: None,
|
||||
default_upstream_id: None,
|
||||
locations: vec![],
|
||||
};
|
||||
|
||||
let res = server
|
||||
.post("/proxy_hosts")
|
||||
.add_header(REQUEST_AUTH_USER_INVALID_HEADER, "true")
|
||||
.json(&payload)
|
||||
.await;
|
||||
|
||||
res.assert_status(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, utoipa::ToSchema, serde::Serialize)]
|
||||
pub struct CreateProxyRequestBody {
|
||||
pub name: Option<String>,
|
||||
pub domain: String,
|
||||
pub scheme: String,
|
||||
pub listen_port: i64,
|
||||
pub forward_scheme: String,
|
||||
pub forward_host: Option<String>,
|
||||
pub forward_port: Option<i64>,
|
||||
pub preserve_host_header: bool,
|
||||
pub enable_websocket: bool,
|
||||
pub enabled: bool,
|
||||
pub meta: Option<serde_json::Value>,
|
||||
pub default_upstream_id: Option<uuid::Uuid>,
|
||||
pub locations: Vec<CreateLocationReq>,
|
||||
}
|
||||
|
||||
impl From<CreateProxyRequestBody> for ProxyHostCreateInfo {
|
||||
fn from(val: CreateProxyRequestBody) -> Self {
|
||||
Self {
|
||||
name: val.name,
|
||||
domain: val.domain,
|
||||
scheme: val.scheme,
|
||||
listen_port: val.listen_port,
|
||||
forward_scheme: val.forward_scheme,
|
||||
forward_host: val.forward_host,
|
||||
forward_port: val.forward_port,
|
||||
preserve_host_header: val.preserve_host_header,
|
||||
enable_websocket: val.enable_websocket,
|
||||
enabled: val.enabled,
|
||||
meta: val.meta,
|
||||
default_upstream_id: val.default_upstream_id,
|
||||
created_by: None,
|
||||
locations: val
|
||||
.locations
|
||||
.into_iter()
|
||||
.map(
|
||||
|l| crate::services::nginx::info::location::CreateLocationInfo {
|
||||
host_id: uuid::Uuid::nil(),
|
||||
path: l.path,
|
||||
match_type: l.match_type,
|
||||
order: l.order,
|
||||
upstream_id: l.upstream_id,
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: true,
|
||||
},
|
||||
)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/nginx/proxy_hosts",
|
||||
request_body = CreateProxyRequestBody,
|
||||
responses(
|
||||
(status = 200, description = "Proxy created successfully", body = ProxyHostInfoResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 422, description = "Invalid request"),
|
||||
(status = 500, description = "Internal server error"),
|
||||
),
|
||||
tag = NGINX_TAG,
|
||||
)]
|
||||
pub async fn create_proxy(
|
||||
request_info: AuthenticatedRequestInfo,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<CreateProxyRequestBody>,
|
||||
) -> AxumResult<Json<ProxyHostInfoResponse>, ApiError> {
|
||||
let proxy_service = &state.service.nginx.get_proxy_service();
|
||||
let mut create_info: ProxyHostCreateInfo = payload.into();
|
||||
create_info.created_by = Some(request_info.user_id);
|
||||
|
||||
let mut tx = state.database_connection.begin().await?;
|
||||
let info = proxy_service
|
||||
.create_proxy(create_info, Some(&mut tx))
|
||||
.await?;
|
||||
|
||||
state
|
||||
.service
|
||||
.nginx
|
||||
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Json(info.into()))
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::services::nginx::location::GetLocationOptions;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, Query, State},
|
||||
response::Result as AxumResult,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
errors::api_error::ApiError,
|
||||
routes::{
|
||||
AppState,
|
||||
api::{
|
||||
openapi::tag::NGINX_TAG,
|
||||
restricted::nginx::proxy_host::info::response::LocationInfoResponse,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct GetLocationParams {
|
||||
pub include_upstream: Option<bool>,
|
||||
}
|
||||
|
||||
pub struct ConcreteGetLocationParams {
|
||||
pub include_upstream: bool,
|
||||
}
|
||||
|
||||
impl From<GetLocationParams> for ConcreteGetLocationParams {
|
||||
fn from(params: GetLocationParams) -> Self {
|
||||
Self {
|
||||
include_upstream: params.include_upstream.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/nginx/locations/{location_id}",
|
||||
responses(
|
||||
(status = 200, description = "Get location info", body = LocationInfoResponse),
|
||||
(status = 404, description = "Not found"),
|
||||
(status = 500, description = "Internal server error"),
|
||||
),
|
||||
tag = NGINX_TAG,
|
||||
)]
|
||||
#[axum::debug_handler]
|
||||
pub async fn get_location(
|
||||
Path(location_id): Path<uuid::Uuid>,
|
||||
Query(params): Query<GetLocationParams>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> AxumResult<Json<LocationInfoResponse>, ApiError> {
|
||||
let concrete_params: ConcreteGetLocationParams = params.into();
|
||||
let svc = &state.service.nginx.get_location_service();
|
||||
let info = if concrete_params.include_upstream {
|
||||
svc.get_location(
|
||||
location_id,
|
||||
Some(GetLocationOptions {
|
||||
include_upstream: true,
|
||||
filter_by_enabled: false,
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
svc.get_location(location_id, None, None).await?
|
||||
};
|
||||
Ok(Json(info.into()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum_test::TestServer;
|
||||
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
||||
|
||||
use database::generated::entities::{location, proxy_host};
|
||||
|
||||
use crate::{
|
||||
configs::{FromConfig, ProgramSettings},
|
||||
routes::api::restricted::nginx::proxy_host::get_proxy_router,
|
||||
services::get_app_service,
|
||||
};
|
||||
|
||||
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||
let program_settings = ProgramSettings::mock();
|
||||
let app_service = get_app_service(&Arc::new(db.clone()), &program_settings);
|
||||
let state = Arc::new(crate::routes::AppState {
|
||||
database_connection: Arc::new(db),
|
||||
service: Arc::new(app_service),
|
||||
config: Arc::new(program_settings),
|
||||
});
|
||||
get_proxy_router(state)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_get_location_returns_info() {
|
||||
let loc_id = uuid::Uuid::new_v4();
|
||||
let loc_model = location::Model {
|
||||
id: loc_id,
|
||||
host_id: uuid::Uuid::new_v4(),
|
||||
path: "/".to_string(),
|
||||
match_type: "prefix".to_string(),
|
||||
order: 1,
|
||||
upstream_id: None,
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![vec![(
|
||||
loc_model.clone(),
|
||||
Option::<proxy_host::Model>::None,
|
||||
)]])
|
||||
.into_connection();
|
||||
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let res = server.get(&format!("/locations/{}", loc_id)).await;
|
||||
res.assert_status_ok();
|
||||
let body = res.json::<crate::routes::api::restricted::nginx::proxy_host::info::response::LocationInfoResponse>();
|
||||
assert_eq!(body.id, loc_id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_get_location_not_found_returns_not_found() {
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||
.into_connection();
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let res = server
|
||||
.get(&format!("/locations/{}", uuid::Uuid::new_v4()))
|
||||
.await;
|
||||
res.assert_status(StatusCode::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
281
apps/api/src/routes/api/restricted/nginx/proxy_host/get_proxy.rs
Normal file
281
apps/api/src/routes/api/restricted/nginx/proxy_host/get_proxy.rs
Normal file
@@ -0,0 +1,281 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, Query, State},
|
||||
response::Result as AxumResult,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::services::nginx::proxy_host::{ProxyHostGetOptions, ProxyHostListOptions};
|
||||
use crate::{
|
||||
errors::{api_error::ApiError, service_error::ServiceError},
|
||||
routes::{
|
||||
AppState,
|
||||
api::restricted::nginx::proxy_host::info::response::{
|
||||
ProxyHostInfoResponse, ProxyListResponse,
|
||||
},
|
||||
api::{
|
||||
helper::pagination::{ExtractPagination, PaginationInfo},
|
||||
openapi::tag::NGINX_TAG,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/nginx/proxy_hosts",
|
||||
responses(
|
||||
(status = 200, description = "List proxies", body = ProxyListResponse),
|
||||
(status = 500, description = "Internal server error"),
|
||||
),
|
||||
tag = NGINX_TAG,
|
||||
)]
|
||||
#[axum::debug_handler]
|
||||
pub async fn get_proxy_list(
|
||||
ExtractPagination(pagination): ExtractPagination,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> AxumResult<Json<ProxyListResponse>, ServiceError> {
|
||||
let svc = &state.service.nginx.get_proxy_service();
|
||||
|
||||
let (proxies_res, proxies_count_res) = tokio::join!(
|
||||
svc.get_proxies(
|
||||
Some(pagination.clone().into()),
|
||||
Some(ProxyHostListOptions {
|
||||
include_upstream: true,
|
||||
filter_by_enabled: false,
|
||||
}),
|
||||
None,
|
||||
),
|
||||
svc.get_total_proxies(None, None),
|
||||
);
|
||||
|
||||
let proxies = proxies_res?;
|
||||
let proxies_count = proxies_count_res?;
|
||||
|
||||
let items: Vec<ProxyHostInfoResponse> = proxies.into_iter().map(|i| i.into()).collect();
|
||||
|
||||
Ok(Json(ProxyListResponse {
|
||||
items,
|
||||
pagination: PaginationInfo {
|
||||
total_items: proxies_count,
|
||||
total_pages: if proxies_count == 0 {
|
||||
0
|
||||
} else {
|
||||
(proxies_count as f32 / pagination.per_page as f32).ceil() as u32
|
||||
},
|
||||
current_page: pagination.page,
|
||||
per_page: pagination.per_page,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum_test::TestServer;
|
||||
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase, Value};
|
||||
|
||||
use database::generated::entities::{location, proxy_host};
|
||||
|
||||
use crate::{
|
||||
configs::{FromConfig, ProgramSettings},
|
||||
routes::api::restricted::nginx::proxy_host::get_proxy_router,
|
||||
services::get_app_service,
|
||||
};
|
||||
|
||||
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||
let program_settings = ProgramSettings::mock();
|
||||
let app_service = get_app_service(&Arc::new(db.clone()), &program_settings);
|
||||
let state = Arc::new(crate::routes::AppState {
|
||||
database_connection: Arc::new(db),
|
||||
service: Arc::new(app_service),
|
||||
config: Arc::new(program_settings),
|
||||
});
|
||||
get_proxy_router(state)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_get_proxy_list_returns_list() {
|
||||
let p1 = proxy_host::Model {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
name: Some("p1".to_string()),
|
||||
domain: "a.com".to_string(),
|
||||
scheme: "http".to_string(),
|
||||
listen_port: 80,
|
||||
forward_scheme: "http".to_string(),
|
||||
forward_host: None,
|
||||
forward_port: None,
|
||||
preserve_host_header: false,
|
||||
enable_websocket: false,
|
||||
enabled: true,
|
||||
meta: None,
|
||||
default_upstream_id: None,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
let p2 = proxy_host::Model {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
name: Some("p2".to_string()),
|
||||
domain: "b.com".to_string(),
|
||||
scheme: "http".to_string(),
|
||||
listen_port: 80,
|
||||
forward_scheme: "http".to_string(),
|
||||
forward_host: None,
|
||||
forward_port: None,
|
||||
preserve_host_header: false,
|
||||
enable_websocket: false,
|
||||
enabled: true,
|
||||
meta: None,
|
||||
default_upstream_id: None,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![vec![
|
||||
(p1.clone(), None::<location::Model>),
|
||||
(p2.clone(), None::<location::Model>),
|
||||
]])
|
||||
.append_query_results(vec![vec![std::collections::BTreeMap::from([(
|
||||
"count".to_string(),
|
||||
Value::BigInt(Some(2)),
|
||||
)])]])
|
||||
.into_connection();
|
||||
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let res = server.get("/proxy_hosts").await;
|
||||
res.assert_status_ok();
|
||||
let body = res.json::<crate::routes::api::restricted::nginx::proxy_host::info::response::ProxyListResponse>();
|
||||
assert_eq!(body.items.len(), 2);
|
||||
assert_eq!(body.pagination.current_page, 1u32);
|
||||
assert_eq!(body.pagination.total_pages, 1u32);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_get_proxy_with_locations_returns_locations() {
|
||||
let ph_id = uuid::Uuid::new_v4();
|
||||
let ph_model = proxy_host::Model {
|
||||
id: ph_id,
|
||||
name: Some("with_locations".to_string()),
|
||||
domain: "with.com".to_string(),
|
||||
scheme: "http".to_string(),
|
||||
listen_port: 80,
|
||||
forward_scheme: "http".to_string(),
|
||||
forward_host: None,
|
||||
forward_port: None,
|
||||
preserve_host_header: false,
|
||||
enable_websocket: false,
|
||||
enabled: true,
|
||||
meta: None,
|
||||
default_upstream_id: None,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let loc_model = location::Model {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
host_id: ph_id,
|
||||
path: "/path".to_string(),
|
||||
match_type: "prefix".to_string(),
|
||||
order: 1,
|
||||
upstream_id: None,
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![vec![(ph_model.clone(), Some(loc_model.clone()))]])
|
||||
.into_connection();
|
||||
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let url = format!("/proxy_hosts/{}", ph_id);
|
||||
let res = server.get(&url).await;
|
||||
res.assert_status_ok();
|
||||
let body = res.json::<crate::routes::api::restricted::nginx::proxy_host::info::response::ProxyHostInfoResponse>();
|
||||
assert_eq!(body.id, ph_id);
|
||||
assert_eq!(body.locations.len(), 1);
|
||||
assert_eq!(body.locations[0].path, "/path");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_get_proxy_not_found_returns_not_found() {
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||
.into_connection();
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let res = server
|
||||
.get(&format!("/proxy_hosts/{}", uuid::Uuid::new_v4()))
|
||||
.await;
|
||||
res.assert_status(StatusCode::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct GetProxyParams {
|
||||
pub include_upstream: Option<bool>,
|
||||
}
|
||||
|
||||
pub struct ConcreteGetProxyParams {
|
||||
pub include_upstream: bool,
|
||||
}
|
||||
|
||||
impl From<GetProxyParams> for ConcreteGetProxyParams {
|
||||
fn from(params: GetProxyParams) -> Self {
|
||||
Self {
|
||||
include_upstream: params.include_upstream.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/nginx/proxy_hosts/{proxy_id}",
|
||||
responses(
|
||||
(status = 200, description = "Get proxy info", body = ProxyHostInfoResponse),
|
||||
(status = 404, description = "Not found"),
|
||||
(status = 500, description = "Internal server error"),
|
||||
),
|
||||
tag = NGINX_TAG,
|
||||
)]
|
||||
pub async fn get_proxy(
|
||||
Path(proxy_id): Path<Uuid>,
|
||||
Query(params): Query<GetProxyParams>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> AxumResult<Json<ProxyHostInfoResponse>, ApiError> {
|
||||
let concrete_params: ConcreteGetProxyParams = params.into();
|
||||
let svc = &state.service.nginx.get_proxy_service();
|
||||
let info = if concrete_params.include_upstream {
|
||||
svc.get_proxy(
|
||||
proxy_id,
|
||||
Some(ProxyHostGetOptions {
|
||||
include_upstream: true,
|
||||
filter_by_enabled: false,
|
||||
}),
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
svc.get_proxy(proxy_id, None, None).await?
|
||||
};
|
||||
Ok(Json(info.into()))
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod response;
|
||||
@@ -0,0 +1,91 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::routes::api::helper::pagination::PaginationInfo;
|
||||
|
||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct LocationInfoResponse {
|
||||
pub id: uuid::Uuid,
|
||||
pub host_id: uuid::Uuid,
|
||||
pub path: String,
|
||||
pub match_type: String,
|
||||
pub order: i64,
|
||||
pub upstream_id: Option<uuid::Uuid>,
|
||||
pub enabled: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl From<crate::services::nginx::info::location::LocationInfo> for LocationInfoResponse {
|
||||
fn from(info: crate::services::nginx::info::location::LocationInfo) -> Self {
|
||||
Self {
|
||||
id: info.id,
|
||||
host_id: info.host_id,
|
||||
path: info.path,
|
||||
match_type: info.match_type,
|
||||
order: info.order,
|
||||
upstream_id: info.upstream_id,
|
||||
enabled: info.enabled,
|
||||
created_at: info.created_at,
|
||||
updated_at: info.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ProxyHostInfoResponse {
|
||||
pub id: uuid::Uuid,
|
||||
pub name: Option<String>,
|
||||
pub domain: String,
|
||||
pub scheme: String,
|
||||
pub listen_port: i64,
|
||||
pub forward_scheme: String,
|
||||
pub forward_host: Option<String>,
|
||||
pub forward_port: Option<i64>,
|
||||
pub preserve_host_header: bool,
|
||||
pub enable_websocket: bool,
|
||||
pub enabled: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub upstream: Option<ProxyHostUpstreamBasic>,
|
||||
pub locations: Vec<LocationInfoResponse>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ProxyHostUpstreamBasic {
|
||||
pub id: uuid::Uuid,
|
||||
pub name: String,
|
||||
pub protocol: String,
|
||||
}
|
||||
|
||||
impl From<crate::services::nginx::info::proxy_host::ProxyHostInfo> for ProxyHostInfoResponse {
|
||||
fn from(info: crate::services::nginx::info::proxy_host::ProxyHostInfo) -> Self {
|
||||
Self {
|
||||
id: info.id,
|
||||
name: info.name,
|
||||
domain: info.domain,
|
||||
scheme: info.scheme,
|
||||
listen_port: info.listen_port,
|
||||
forward_scheme: info.forward_scheme,
|
||||
forward_host: info.forward_host,
|
||||
forward_port: info.forward_port,
|
||||
preserve_host_header: info.preserve_host_header,
|
||||
enable_websocket: info.enable_websocket,
|
||||
enabled: info.enabled,
|
||||
created_at: info.created_at,
|
||||
updated_at: info.updated_at,
|
||||
upstream: info.upstream.map(|u| ProxyHostUpstreamBasic {
|
||||
id: u.id,
|
||||
name: u.name,
|
||||
protocol: u.protocol,
|
||||
}),
|
||||
locations: info.locations.into_iter().map(|l| l.into()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
pub struct ProxyListResponse {
|
||||
pub items: Vec<ProxyHostInfoResponse>,
|
||||
pub pagination: PaginationInfo,
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
response::Result as AxumResult,
|
||||
};
|
||||
use sea_orm::TransactionTrait;
|
||||
|
||||
use crate::{
|
||||
errors::api_error::ApiError,
|
||||
middlewares::request_info::AuthenticatedRequestInfo,
|
||||
routes::{AppState, api::openapi::tag::NGINX_TAG},
|
||||
};
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/nginx/locations/{location_id}",
|
||||
responses(
|
||||
(status = 200, description = "Location removed successfully", body = ()),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Not found"),
|
||||
(status = 500, description = "Internal server error"),
|
||||
),
|
||||
tag = NGINX_TAG,
|
||||
)]
|
||||
#[axum::debug_handler]
|
||||
pub async fn remove_location(
|
||||
_request_info: AuthenticatedRequestInfo,
|
||||
Path(location_id): Path<uuid::Uuid>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> AxumResult<Json<()>, ApiError> {
|
||||
let svc = &state.service.nginx.get_location_service();
|
||||
|
||||
let mut tx = state.database_connection.begin().await?;
|
||||
svc.delete_location(location_id, Some(&mut tx)).await?;
|
||||
state
|
||||
.service
|
||||
.nginx
|
||||
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum_test::TestServer;
|
||||
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
||||
|
||||
use database::generated::entities::{location, upstream, upstream_target};
|
||||
|
||||
use crate::{
|
||||
configs::{FromConfig, ProgramSettings},
|
||||
routes::api::restricted::nginx::proxy_host::get_proxy_router,
|
||||
services::{agent_client::MockAgentService, get_mock_app_service},
|
||||
};
|
||||
|
||||
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||
let program_settings = ProgramSettings::mock();
|
||||
let mut mock = MockAgentService::new();
|
||||
mock.expect_validate().returning(|_cfg| Ok(()));
|
||||
mock.expect_apply().returning(|_cfg| Ok(()));
|
||||
let mock_agent = Arc::new(mock);
|
||||
let app_service =
|
||||
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
|
||||
let state = Arc::new(crate::routes::AppState {
|
||||
database_connection: Arc::new(db),
|
||||
service: Arc::new(app_service),
|
||||
config: Arc::new(program_settings),
|
||||
});
|
||||
get_proxy_router(state).layer(axum::middleware::from_fn(
|
||||
crate::middlewares::require_auth::mock::mock_require_auth,
|
||||
))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_remove_location_succeeds_returns_ok() {
|
||||
let loc_id = uuid::Uuid::new_v4();
|
||||
|
||||
let existing = location::Model {
|
||||
id: loc_id,
|
||||
host_id: uuid::Uuid::new_v4(),
|
||||
path: "/".to_string(),
|
||||
match_type: "prefix".to_string(),
|
||||
order: 1,
|
||||
upstream_id: None,
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let up_id = uuid::Uuid::new_v4();
|
||||
let up_model = upstream::Model {
|
||||
id: up_id,
|
||||
name: "u1".to_string(),
|
||||
protocol: "http".to_string(),
|
||||
algorithm: "rr".to_string(),
|
||||
sticky_session: false,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
let target_model = upstream_target::Model {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
upstream_id: up_id,
|
||||
target_host: "127.0.0.1".to_string(),
|
||||
target_port: 8080,
|
||||
weight: 1,
|
||||
is_backup: false,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![vec![existing.clone()]])
|
||||
.append_exec_results(vec![sea_orm::MockExecResult {
|
||||
rows_affected: 1,
|
||||
last_insert_id: 0,
|
||||
}])
|
||||
// additional query result for regenerate_and_apply_config -> generate_config
|
||||
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
|
||||
.into_connection();
|
||||
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let res = server.delete(&format!("/locations/{}", loc_id)).await;
|
||||
res.assert_status_ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_remove_location_not_found_returns_not_found() {
|
||||
let empty_results: Vec<Vec<location::Model>> = vec![Vec::<location::Model>::new()];
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(empty_results)
|
||||
.into_connection();
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let res = server
|
||||
.delete(&format!("/locations/{}", uuid::Uuid::new_v4()))
|
||||
.await;
|
||||
res.assert_status(StatusCode::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
response::Result as AxumResult,
|
||||
};
|
||||
use sea_orm::TransactionTrait;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
errors::api_error::ApiError,
|
||||
middlewares::request_info::AuthenticatedRequestInfo,
|
||||
routes::{AppState, api::openapi::tag::NGINX_TAG},
|
||||
};
|
||||
|
||||
#[axum::debug_handler]
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/nginx/proxy_hosts/{proxy_id}",
|
||||
responses(
|
||||
(status = 200, description = "Proxy removed successfully", body = ()),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Not found"),
|
||||
(status = 500, description = "Internal server error"),
|
||||
),
|
||||
tag = NGINX_TAG,
|
||||
)]
|
||||
pub async fn remove_proxy(
|
||||
_request_info: AuthenticatedRequestInfo,
|
||||
Path(proxy_id): Path<Uuid>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> AxumResult<Json<()>, ApiError> {
|
||||
let svc = &state.service.nginx.get_proxy_service();
|
||||
|
||||
let mut tx = state.database_connection.begin().await?;
|
||||
svc.delete_proxy(proxy_id, Some(&mut tx)).await?;
|
||||
state
|
||||
.service
|
||||
.nginx
|
||||
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum_test::TestServer;
|
||||
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase, MockExecResult};
|
||||
|
||||
use database::generated::entities::{proxy_host, upstream, upstream_target};
|
||||
|
||||
use crate::{
|
||||
configs::{FromConfig, ProgramSettings},
|
||||
middlewares::require_auth::mock::REQUEST_AUTH_USER_INVALID_HEADER,
|
||||
routes::api::restricted::nginx::proxy_host::get_proxy_router,
|
||||
services::{agent_client::MockAgentService, get_mock_app_service},
|
||||
};
|
||||
|
||||
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||
let program_settings = ProgramSettings::mock();
|
||||
let mut mock = MockAgentService::new();
|
||||
mock.expect_validate().returning(|_cfg| Ok(()));
|
||||
mock.expect_apply().returning(|_cfg| Ok(()));
|
||||
let mock_agent = Arc::new(mock);
|
||||
let app_service =
|
||||
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
|
||||
let state = Arc::new(crate::routes::AppState {
|
||||
database_connection: Arc::new(db),
|
||||
service: Arc::new(app_service),
|
||||
config: Arc::new(program_settings),
|
||||
});
|
||||
get_proxy_router(state).layer(axum::middleware::from_fn(
|
||||
crate::middlewares::require_auth::mock::mock_require_auth,
|
||||
))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_remove_proxy_succeeds_returns_ok() {
|
||||
let ph_id = uuid::Uuid::new_v4();
|
||||
|
||||
let existing = proxy_host::Model {
|
||||
id: ph_id,
|
||||
name: Some("todelete".to_string()),
|
||||
domain: "d.com".to_string(),
|
||||
scheme: "http".to_string(),
|
||||
listen_port: 80,
|
||||
forward_scheme: "http".to_string(),
|
||||
forward_host: None,
|
||||
forward_port: None,
|
||||
preserve_host_header: false,
|
||||
enable_websocket: false,
|
||||
enabled: true,
|
||||
meta: None,
|
||||
default_upstream_id: None,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let up_id = uuid::Uuid::new_v4();
|
||||
let up_model = upstream::Model {
|
||||
id: up_id,
|
||||
name: "u1".to_string(),
|
||||
protocol: "http".to_string(),
|
||||
algorithm: "rr".to_string(),
|
||||
sticky_session: false,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
let target_model = upstream_target::Model {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
upstream_id: up_id,
|
||||
target_host: "127.0.0.1".to_string(),
|
||||
target_port: 8080,
|
||||
weight: 1,
|
||||
is_backup: false,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![vec![existing.clone()]])
|
||||
.append_exec_results(vec![
|
||||
MockExecResult {
|
||||
rows_affected: 1,
|
||||
last_insert_id: 0,
|
||||
},
|
||||
MockExecResult {
|
||||
rows_affected: 1,
|
||||
last_insert_id: 0,
|
||||
},
|
||||
])
|
||||
// additional query result for regenerate_and_apply_config -> generate_config
|
||||
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
|
||||
.into_connection();
|
||||
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let res = server.delete(&format!("/proxy_hosts/{}", ph_id)).await;
|
||||
res.assert_status_ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_remove_proxy_unauthenticated_returns_unauthorized() {
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite).into_connection();
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let res = server
|
||||
.delete(&format!("/proxy_hosts/{}", uuid::Uuid::new_v4()))
|
||||
.add_header(REQUEST_AUTH_USER_INVALID_HEADER, "true")
|
||||
.await;
|
||||
|
||||
res.assert_status(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_remove_proxy_not_found_returns_not_found() {
|
||||
let empty_results: Vec<Vec<proxy_host::Model>> = vec![Vec::<proxy_host::Model>::new()];
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(empty_results)
|
||||
.into_connection();
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let res = server
|
||||
.delete(&format!("/proxy_hosts/{}", uuid::Uuid::new_v4()))
|
||||
.await;
|
||||
res.assert_status(StatusCode::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
response::Result as AxumResult,
|
||||
};
|
||||
use sea_orm::TransactionTrait;
|
||||
|
||||
use crate::{
|
||||
errors::api_error::ApiError,
|
||||
middlewares::request_info::AuthenticatedRequestInfo,
|
||||
routes::{
|
||||
AppState,
|
||||
api::{
|
||||
openapi::tag::NGINX_TAG,
|
||||
restricted::nginx::proxy_host::info::response::LocationInfoResponse,
|
||||
},
|
||||
},
|
||||
services::nginx::info::location::UpdateLocationInfo,
|
||||
};
|
||||
|
||||
#[derive(serde::Deserialize, utoipa::ToSchema, serde::Serialize)]
|
||||
pub struct UpdateLocationRequestBody {
|
||||
pub path: Option<String>,
|
||||
pub match_type: Option<String>,
|
||||
pub order: Option<i64>,
|
||||
pub upstream_id: Option<Option<uuid::Uuid>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum_test::TestServer;
|
||||
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
||||
|
||||
use database::generated::entities::{location, upstream, upstream_target};
|
||||
|
||||
use super::UpdateLocationRequestBody;
|
||||
use crate::{
|
||||
configs::{FromConfig, ProgramSettings},
|
||||
routes::api::restricted::nginx::proxy_host::get_proxy_router,
|
||||
services::{agent_client::MockAgentService, get_mock_app_service},
|
||||
};
|
||||
|
||||
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||
let program_settings = ProgramSettings::mock();
|
||||
let mut mock = MockAgentService::new();
|
||||
mock.expect_validate().returning(|_cfg| Ok(()));
|
||||
mock.expect_apply().returning(|_cfg| Ok(()));
|
||||
let mock_agent = Arc::new(mock);
|
||||
let app_service =
|
||||
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
|
||||
let state = Arc::new(crate::routes::AppState {
|
||||
database_connection: Arc::new(db),
|
||||
service: Arc::new(app_service),
|
||||
config: Arc::new(program_settings),
|
||||
});
|
||||
get_proxy_router(state).layer(axum::middleware::from_fn(
|
||||
crate::middlewares::require_auth::mock::mock_require_auth,
|
||||
))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_update_location_succeeds_returns_ok() {
|
||||
let loc_id = uuid::Uuid::new_v4();
|
||||
|
||||
let current = location::Model {
|
||||
id: loc_id,
|
||||
host_id: uuid::Uuid::new_v4(),
|
||||
path: "/old".to_string(),
|
||||
match_type: "prefix".to_string(),
|
||||
order: 1,
|
||||
upstream_id: None,
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let updated = location::Model { ..current.clone() };
|
||||
|
||||
let first: Vec<Vec<location::Model>> = vec![vec![current.clone()]];
|
||||
let second: Vec<Vec<location::Model>> = vec![vec![updated.clone()]];
|
||||
let up_id = uuid::Uuid::new_v4();
|
||||
let up_model = upstream::Model {
|
||||
id: up_id,
|
||||
name: "u1".to_string(),
|
||||
protocol: "http".to_string(),
|
||||
algorithm: "rr".to_string(),
|
||||
sticky_session: false,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
let target_model = upstream_target::Model {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
upstream_id: up_id,
|
||||
target_host: "127.0.0.1".to_string(),
|
||||
target_port: 8080,
|
||||
weight: 1,
|
||||
is_backup: false,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(first)
|
||||
.append_query_results(second)
|
||||
// additional query result for regenerate_and_apply_config -> generate_config
|
||||
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
|
||||
.into_connection();
|
||||
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let payload = UpdateLocationRequestBody {
|
||||
path: Some("/new".to_string()),
|
||||
match_type: None,
|
||||
order: None,
|
||||
upstream_id: None,
|
||||
};
|
||||
|
||||
let res = server
|
||||
.patch(&format!("/locations/{}", loc_id))
|
||||
.json(&payload)
|
||||
.await;
|
||||
res.assert_status_ok();
|
||||
let body = res.json::<crate::routes::api::restricted::nginx::proxy_host::info::response::LocationInfoResponse>();
|
||||
assert_eq!(body.id, loc_id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_update_location_not_found_returns_not_found() {
|
||||
let empty_results: Vec<Vec<location::Model>> = vec![Vec::<location::Model>::new()];
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(empty_results)
|
||||
.into_connection();
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let payload = UpdateLocationRequestBody {
|
||||
path: Some("/new".to_string()),
|
||||
match_type: None,
|
||||
order: None,
|
||||
upstream_id: None,
|
||||
};
|
||||
|
||||
let res = server
|
||||
.patch(&format!("/locations/{}", uuid::Uuid::new_v4()))
|
||||
.json(&payload)
|
||||
.await;
|
||||
res.assert_status(StatusCode::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UpdateLocationRequestBody> for UpdateLocationInfo {
|
||||
fn from(val: UpdateLocationRequestBody) -> Self {
|
||||
Self {
|
||||
path: val.path,
|
||||
match_type: val.match_type,
|
||||
order: val.order,
|
||||
upstream_id: val.upstream_id,
|
||||
proxy_pass_protocol: None,
|
||||
proxy_pass_host: None,
|
||||
proxy_pass_port: None,
|
||||
preserve_host_header: None,
|
||||
allowed_methods: None,
|
||||
custom_config: None,
|
||||
enabled: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/api/nginx/locations/{location_id}",
|
||||
request_body = UpdateLocationRequestBody,
|
||||
responses(
|
||||
(status = 200, description = "Location updated successfully", body = LocationInfoResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "Not found"),
|
||||
(status = 422, description = "Invalid request"),
|
||||
(status = 500, description = "Internal server error"),
|
||||
),
|
||||
tag = NGINX_TAG,
|
||||
)]
|
||||
pub async fn update_location(
|
||||
_request_info: AuthenticatedRequestInfo,
|
||||
Path(location_id): Path<uuid::Uuid>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<UpdateLocationRequestBody>,
|
||||
) -> AxumResult<Json<LocationInfoResponse>, ApiError> {
|
||||
let svc = &state.service.nginx.get_location_service();
|
||||
let update: UpdateLocationInfo = payload.into();
|
||||
|
||||
let mut tx = state.database_connection.begin().await?;
|
||||
let info = svc
|
||||
.update_location(location_id, update, Some(&mut tx))
|
||||
.await?;
|
||||
state
|
||||
.service
|
||||
.nginx
|
||||
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Json(info.into()))
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
response::Result as AxumResult,
|
||||
};
|
||||
use sea_orm::TransactionTrait;
|
||||
|
||||
use crate::{
|
||||
errors::api_error::ApiError,
|
||||
middlewares::request_info::AuthenticatedRequestInfo,
|
||||
routes::{AppState, api::restricted::nginx::proxy_host::info::response::ProxyHostInfoResponse},
|
||||
services::nginx::info::proxy_host::UpdateProxyHostInfo,
|
||||
};
|
||||
|
||||
#[derive(serde::Deserialize, utoipa::ToSchema, serde::Serialize)]
|
||||
pub struct UpdateProxyRequestBody {
|
||||
pub name: Option<Option<String>>,
|
||||
pub domain: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum_test::TestServer;
|
||||
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
||||
|
||||
use database::generated::entities::{proxy_host, upstream, upstream_target};
|
||||
|
||||
use super::UpdateProxyRequestBody;
|
||||
use crate::{
|
||||
configs::{FromConfig, ProgramSettings},
|
||||
middlewares::require_auth::mock::REQUEST_AUTH_USER_INVALID_HEADER,
|
||||
routes::api::restricted::nginx::proxy_host::get_proxy_router,
|
||||
services::{agent_client::MockAgentService, get_mock_app_service},
|
||||
};
|
||||
|
||||
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||
let program_settings = ProgramSettings::mock();
|
||||
let mut mock = MockAgentService::new();
|
||||
mock.expect_validate().returning(|_cfg| Ok(()));
|
||||
mock.expect_apply().returning(|_cfg| Ok(()));
|
||||
let mock_agent = Arc::new(mock);
|
||||
let app_service =
|
||||
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
|
||||
let state = Arc::new(crate::routes::AppState {
|
||||
database_connection: Arc::new(db),
|
||||
service: Arc::new(app_service),
|
||||
config: Arc::new(program_settings),
|
||||
});
|
||||
get_proxy_router(state).layer(axum::middleware::from_fn(
|
||||
crate::middlewares::require_auth::mock::mock_require_auth,
|
||||
))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_update_proxy_succeeds_returns_ok() {
|
||||
let ph_id = uuid::Uuid::new_v4();
|
||||
|
||||
let current = proxy_host::Model {
|
||||
id: ph_id,
|
||||
name: Some("oldname".to_string()),
|
||||
domain: "a.com".to_string(),
|
||||
scheme: "http".to_string(),
|
||||
listen_port: 80,
|
||||
forward_scheme: "http".to_string(),
|
||||
forward_host: None,
|
||||
forward_port: None,
|
||||
preserve_host_header: false,
|
||||
enable_websocket: false,
|
||||
enabled: true,
|
||||
meta: None,
|
||||
default_upstream_id: None,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let updated = proxy_host::Model { ..current.clone() };
|
||||
|
||||
let first: Vec<Vec<proxy_host::Model>> = vec![vec![current.clone()]];
|
||||
let second: Vec<Vec<proxy_host::Model>> = vec![vec![updated.clone()]];
|
||||
let up_id = uuid::Uuid::new_v4();
|
||||
let up_model = upstream::Model {
|
||||
id: up_id,
|
||||
name: "u1".to_string(),
|
||||
protocol: "http".to_string(),
|
||||
algorithm: "rr".to_string(),
|
||||
sticky_session: false,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
let target_model = upstream_target::Model {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
upstream_id: up_id,
|
||||
target_host: "127.0.0.1".to_string(),
|
||||
target_port: 8080,
|
||||
weight: 1,
|
||||
is_backup: false,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(first)
|
||||
.append_query_results(second)
|
||||
// additional query result for regenerate_and_apply_config -> generate_config
|
||||
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
|
||||
.into_connection();
|
||||
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let payload = UpdateProxyRequestBody {
|
||||
name: Some(Some("newname".to_string())),
|
||||
domain: Some("a.com".to_string()),
|
||||
};
|
||||
|
||||
let res = server
|
||||
.patch(&format!("/proxy_hosts/{}", ph_id))
|
||||
.json(&payload)
|
||||
.await;
|
||||
res.assert_status_ok();
|
||||
let body: crate::routes::api::restricted::nginx::proxy_host::info::response::ProxyHostInfoResponse = res.json();
|
||||
assert_eq!(body.id, ph_id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_update_proxy_unauthenticated_returns_unauthorized() {
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite).into_connection();
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let payload = UpdateProxyRequestBody {
|
||||
name: Some(Some("newname".to_string())),
|
||||
domain: None,
|
||||
};
|
||||
|
||||
let res = server
|
||||
.patch(&format!("/proxy_hosts/{}", uuid::Uuid::new_v4()))
|
||||
.add_header(REQUEST_AUTH_USER_INVALID_HEADER, "true")
|
||||
.json(&payload)
|
||||
.await;
|
||||
|
||||
res.assert_status(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_update_proxy_not_found_returns_not_found() {
|
||||
let empty_results: Vec<Vec<proxy_host::Model>> = vec![Vec::<proxy_host::Model>::new()];
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(empty_results)
|
||||
.into_connection();
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let payload = UpdateProxyRequestBody {
|
||||
name: Some(Some("newname".to_string())),
|
||||
domain: None,
|
||||
};
|
||||
|
||||
let res = server
|
||||
.patch(&format!("/proxy_hosts/{}", uuid::Uuid::new_v4()))
|
||||
.json(&payload)
|
||||
.await;
|
||||
res.assert_status(StatusCode::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UpdateProxyRequestBody> for UpdateProxyHostInfo {
|
||||
fn from(val: UpdateProxyRequestBody) -> Self {
|
||||
Self {
|
||||
name: val.name,
|
||||
domain: val.domain,
|
||||
scheme: None,
|
||||
listen_port: None,
|
||||
forward_scheme: None,
|
||||
forward_host: None,
|
||||
forward_port: None,
|
||||
preserve_host_header: None,
|
||||
enable_websocket: None,
|
||||
enabled: None,
|
||||
meta: None,
|
||||
default_upstream_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/api/nginx/proxy_hosts/{proxy_id}",
|
||||
request_body = UpdateProxyRequestBody,
|
||||
responses(
|
||||
(status = 200, description = "Proxy updated successfully", body = ProxyHostInfoResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 422, description = "Invalid request"),
|
||||
(status = 500, description = "Internal server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn update_proxy(
|
||||
_request_info: AuthenticatedRequestInfo,
|
||||
Path(proxy_id): Path<uuid::Uuid>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<UpdateProxyRequestBody>,
|
||||
) -> AxumResult<Json<ProxyHostInfoResponse>, ApiError> {
|
||||
let svc = &state.service.nginx.get_proxy_service();
|
||||
let update: UpdateProxyHostInfo = payload.into();
|
||||
|
||||
let mut tx = state.database_connection.begin().await?;
|
||||
let info = svc.update_proxy(proxy_id, update, Some(&mut tx)).await?;
|
||||
state
|
||||
.service
|
||||
.nginx
|
||||
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Json(info.into()))
|
||||
}
|
||||
@@ -106,6 +106,365 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/nginx/locations/{location_id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Nginx"
|
||||
],
|
||||
"operationId": "get_location",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "location_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Get location info",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LocationInfoResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Nginx"
|
||||
],
|
||||
"operationId": "remove_location",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "location_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Location removed successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"default": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"tags": [
|
||||
"Nginx"
|
||||
],
|
||||
"operationId": "update_location",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "location_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateLocationRequestBody"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Location updated successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LocationInfoResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found"
|
||||
},
|
||||
"422": {
|
||||
"description": "Invalid request"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/nginx/proxy_hosts": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Nginx"
|
||||
],
|
||||
"operationId": "get_proxy_list",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List proxies",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProxyListResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Nginx"
|
||||
],
|
||||
"operationId": "create_proxy",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateProxyRequestBody"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Proxy created successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProxyHostInfoResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"422": {
|
||||
"description": "Invalid request"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/nginx/proxy_hosts/{proxy_id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Nginx"
|
||||
],
|
||||
"operationId": "get_proxy",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "proxy_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Get proxy info",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProxyHostInfoResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Nginx"
|
||||
],
|
||||
"operationId": "remove_proxy",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "proxy_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Proxy removed successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"default": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"tags": [
|
||||
"crate::routes::api::restricted::nginx::proxy_host::update_proxy"
|
||||
],
|
||||
"operationId": "update_proxy",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "proxy_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateProxyRequestBody"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Proxy updated successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProxyHostInfoResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"422": {
|
||||
"description": "Invalid request"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/nginx/proxy_hosts/{proxy_id}/locations": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Nginx"
|
||||
],
|
||||
"operationId": "create_location",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "proxy_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateLocationRequestBody"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Location created",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LocationInfoResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"422": {
|
||||
"description": "Invalid request"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/nginx/upstream_targets/{upstream_target_id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -508,6 +867,130 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateLocationReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"path",
|
||||
"match_type",
|
||||
"order"
|
||||
],
|
||||
"properties": {
|
||||
"match_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"order": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"upstream_id": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateLocationRequestBody": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"path",
|
||||
"match_type",
|
||||
"order"
|
||||
],
|
||||
"properties": {
|
||||
"match_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"order": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"upstream_id": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateProxyRequestBody": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"domain",
|
||||
"scheme",
|
||||
"listen_port",
|
||||
"forward_scheme",
|
||||
"preserve_host_header",
|
||||
"enable_websocket",
|
||||
"enabled",
|
||||
"locations"
|
||||
],
|
||||
"properties": {
|
||||
"default_upstream_id": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "uuid"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string"
|
||||
},
|
||||
"enable_websocket": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"forward_host": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"forward_port": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int64"
|
||||
},
|
||||
"forward_scheme": {
|
||||
"type": "string"
|
||||
},
|
||||
"listen_port": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"locations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/CreateLocationReq"
|
||||
}
|
||||
},
|
||||
"meta": {},
|
||||
"name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"preserve_host_header": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"scheme": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateUpstreamRequestBody": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -643,6 +1126,57 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"LocationInfoResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"host_id",
|
||||
"path",
|
||||
"match_type",
|
||||
"order",
|
||||
"enabled",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
],
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"host_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"match_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"order": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"upstream_id": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoginRequest": {
|
||||
"type": "object",
|
||||
"description": "Login request payload",
|
||||
@@ -695,6 +1229,179 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProxyHostInfoResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"domain",
|
||||
"scheme",
|
||||
"listen_port",
|
||||
"forward_scheme",
|
||||
"preserve_host_header",
|
||||
"enable_websocket",
|
||||
"enabled",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"locations"
|
||||
],
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string"
|
||||
},
|
||||
"enable_websocket": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"forward_host": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"forward_port": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int64"
|
||||
},
|
||||
"forward_scheme": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"listen_port": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"locations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/LocationInfoResponse"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"preserve_host_header": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"scheme": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"upstream": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/ProxyHostUpstreamBasic"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProxyHostUpstreamBasic": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"protocol"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"protocol": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProxyListResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"items",
|
||||
"pagination"
|
||||
],
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ProxyHostInfoResponse"
|
||||
}
|
||||
},
|
||||
"pagination": {
|
||||
"$ref": "#/components/schemas/PaginationInfo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateLocationRequestBody": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"match_type": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"order": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int64"
|
||||
},
|
||||
"path": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"upstream_id": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateProxyRequestBody": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateUpstreamInfoResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
Reference in New Issue
Block a user