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::update_upstream_target::update_upstream_target,
|
||||||
crate::routes::api::restricted::nginx::upstream::remove_upstream::remove_upstream,
|
crate::routes::api::restricted::nginx::upstream::remove_upstream::remove_upstream,
|
||||||
crate::routes::api::restricted::nginx::upstream::remove_upstream_target::remove_upstream_target,
|
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(
|
components(
|
||||||
schemas(crate::routes::api::health::info::HealthInfo),
|
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::update_upstream_target::UpdateUpstreamTargetRequestBody),
|
||||||
schemas(crate::routes::api::restricted::nginx::upstream::info::response::UpdateUpstreamInfoResponse),
|
schemas(crate::routes::api::restricted::nginx::upstream::info::response::UpdateUpstreamInfoResponse),
|
||||||
schemas(crate::routes::api::restricted::nginx::upstream::info::response::UpdateUpstreamTargetInfoResponse),
|
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(
|
tags(
|
||||||
(name = tag::HEALTH_TAG, description = "Health information API"),
|
(name = tag::HEALTH_TAG, description = "Health information API"),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod proxy_host;
|
||||||
pub mod upstream;
|
pub mod upstream;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -7,5 +8,7 @@ use axum::Router;
|
|||||||
use crate::routes::AppState;
|
use crate::routes::AppState;
|
||||||
|
|
||||||
pub fn get_nginx_router(state: Arc<AppState>) -> Router {
|
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}": {
|
"/api/nginx/upstream_targets/{upstream_target_id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"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": {
|
"CreateUpstreamRequestBody": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"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": {
|
"LoginRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Login request payload",
|
"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": {
|
"UpdateUpstreamInfoResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
Reference in New Issue
Block a user