Compare commits
13 Commits
feature/up
...
1efc4ffc5a
| Author | SHA1 | Date | |
|---|---|---|---|
| 1efc4ffc5a | |||
| 3216cbbd5e | |||
| 7f0040b668 | |||
| d67a9f6f0d | |||
|
|
4fe03b245e | ||
|
|
b4a36dbe4c | ||
|
|
7ae76f622c | ||
|
|
9b8232d94d | ||
|
|
83e02acb22 | ||
|
|
eb1afc87cc | ||
|
|
1c0053207c | ||
|
|
ab840126b3 | ||
| 1ed065e08e |
80
.devcontainer/devcontainer.json
Normal file
80
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/rust
|
||||||
|
{
|
||||||
|
"name": "YANPM",
|
||||||
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/rust:2-1-trixie",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/java:1": {
|
||||||
|
"version": "latest",
|
||||||
|
"jdkDistro": "open",
|
||||||
|
"gradleVersion": "latest",
|
||||||
|
"mavenVersion": "latest",
|
||||||
|
"antVersion": "latest",
|
||||||
|
"groovyVersion": "latest"
|
||||||
|
},
|
||||||
|
"ghcr.io/dhoeric/features/act:1": {},
|
||||||
|
"ghcr.io/guiyomh/features/just:0": {
|
||||||
|
"version": "latest"
|
||||||
|
},
|
||||||
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
|
"nodeGypDependencies": true,
|
||||||
|
"version": "22",
|
||||||
|
"pnpmVersion": "latest",
|
||||||
|
"nvmVersion": "latest"
|
||||||
|
},
|
||||||
|
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||||
|
"moby": false,
|
||||||
|
"azureDnsAutoDetection": true,
|
||||||
|
"installDockerBuildx": true,
|
||||||
|
"installDockerComposeSwitch": true,
|
||||||
|
"disableIp6tables": true,
|
||||||
|
"version": "latest",
|
||||||
|
"dockerDashComposeVersion": "v2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Use 'mounts' to make the cargo cache persistent in a Docker Volume.
|
||||||
|
// "mounts": [
|
||||||
|
// {
|
||||||
|
// "source": "devcontainer-cargo-cache-${devcontainerId}",
|
||||||
|
// "target": "/usr/local/cargo",
|
||||||
|
// "type": "volume"
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
|
||||||
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
|
// "features": {},
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
// "forwardPorts": [],
|
||||||
|
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
"postCreateCommand": "bash .devcontainer/start.sh",
|
||||||
|
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"mikestead.dotenv",
|
||||||
|
"usernamehw.errorlens",
|
||||||
|
"streetsidesoftware.code-spell-checker",
|
||||||
|
"mhutchie.git-graph",
|
||||||
|
"yzhang.markdown-all-in-one",
|
||||||
|
"christian-kohler.npm-intellisense",
|
||||||
|
"42Crunch.vscode-openapi",
|
||||||
|
"christian-kohler.path-intellisense",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"rust-lang.rust-analyzer",
|
||||||
|
"nefrob.vscode-just-syntax",
|
||||||
|
"redhat.vscode-yaml",
|
||||||
|
// optional
|
||||||
|
"SirTori.indenticator",
|
||||||
|
"oderwat.indent-rainbow"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "root"
|
||||||
|
}
|
||||||
19
.devcontainer/start.sh
Normal file
19
.devcontainer/start.sh
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Starting devcontainer setup..."
|
||||||
|
|
||||||
|
echo "Installing dependencies..."
|
||||||
|
# install dependencies
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
cargo install sea-orm-cli
|
||||||
|
|
||||||
|
echo "building agent image..."
|
||||||
|
# build agent image
|
||||||
|
cd apps/agent
|
||||||
|
just build-docker
|
||||||
|
cd -
|
||||||
|
echo "Agent image built."
|
||||||
|
|
||||||
|
echo "Devcontainer setup complete."
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -29,3 +29,4 @@ target
|
|||||||
generated-config.yaml
|
generated-config.yaml
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
|
.pnpm-store/
|
||||||
|
|||||||
@@ -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,362 @@
|
|||||||
|
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)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum CreateLocationRequestBody {
|
||||||
|
// #[serde(rename = "upstream_id")]
|
||||||
|
UpstreamId(CreateLocationRequestBodyByUpstreamId),
|
||||||
|
// #[serde(rename = "proxy_pass")]
|
||||||
|
ProxyPass(CreateLocationRequestBodyByProxyPass),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, utoipa::ToSchema, serde::Serialize)]
|
||||||
|
pub struct CreateLocationRequestBodyByUpstreamId {
|
||||||
|
pub path: String,
|
||||||
|
pub match_type: String,
|
||||||
|
pub order: i64,
|
||||||
|
pub upstream_id: uuid::Uuid,
|
||||||
|
pub preserve_host_header: Option<bool>,
|
||||||
|
pub allowed_methods: Option<Vec<String>>,
|
||||||
|
pub custom_config: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, utoipa::ToSchema, serde::Serialize)]
|
||||||
|
pub struct CreateLocationRequestBodyByProxyPass {
|
||||||
|
pub path: String,
|
||||||
|
pub match_type: String,
|
||||||
|
pub order: i64,
|
||||||
|
pub proxy_pass_protocol: String,
|
||||||
|
pub proxy_pass_host: String,
|
||||||
|
pub proxy_pass_port: i64,
|
||||||
|
pub preserve_host_header: Option<bool>,
|
||||||
|
pub allowed_methods: Option<Vec<String>>,
|
||||||
|
pub custom_config: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(uuid::Uuid, CreateLocationRequestBody)> for CreateLocationInfo {
|
||||||
|
fn from(val: (uuid::Uuid, CreateLocationRequestBody)) -> Self {
|
||||||
|
match val.1 {
|
||||||
|
CreateLocationRequestBody::UpstreamId(body) => Self::from((val.0, body)),
|
||||||
|
CreateLocationRequestBody::ProxyPass(body) => Self::from((val.0, body)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(uuid::Uuid, CreateLocationRequestBodyByUpstreamId)> for CreateLocationInfo {
|
||||||
|
fn from((proxy_id, payload): (uuid::Uuid, CreateLocationRequestBodyByUpstreamId)) -> Self {
|
||||||
|
Self {
|
||||||
|
host_id: proxy_id,
|
||||||
|
path: payload.path,
|
||||||
|
match_type: payload.match_type,
|
||||||
|
order: payload.order,
|
||||||
|
upstream_id: Some(payload.upstream_id),
|
||||||
|
proxy_pass_protocol: None,
|
||||||
|
proxy_pass_host: None,
|
||||||
|
proxy_pass_port: None,
|
||||||
|
preserve_host_header: payload.preserve_host_header,
|
||||||
|
allowed_methods: payload.allowed_methods,
|
||||||
|
custom_config: payload.custom_config,
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(uuid::Uuid, CreateLocationRequestBodyByProxyPass)> for CreateLocationInfo {
|
||||||
|
fn from((proxy_id, payload): (uuid::Uuid, CreateLocationRequestBodyByProxyPass)) -> Self {
|
||||||
|
Self {
|
||||||
|
host_id: proxy_id,
|
||||||
|
path: payload.path,
|
||||||
|
match_type: payload.match_type,
|
||||||
|
order: payload.order,
|
||||||
|
upstream_id: None,
|
||||||
|
proxy_pass_protocol: Some(payload.proxy_pass_protocol),
|
||||||
|
proxy_pass_host: Some(payload.proxy_pass_host),
|
||||||
|
proxy_pass_port: Some(payload.proxy_pass_port),
|
||||||
|
preserve_host_header: payload.preserve_host_header,
|
||||||
|
allowed_methods: payload.allowed_methods,
|
||||||
|
custom_config: payload.custom_config,
|
||||||
|
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()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum_test::TestServer;
|
||||||
|
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase, MockRow};
|
||||||
|
|
||||||
|
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::CreateLocationRequestBodyByProxyPass,
|
||||||
|
create_location::CreateLocationRequestBodyByUpstreamId, 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 dummy_row: Vec<MockRow> = vec![];
|
||||||
|
|
||||||
|
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()))]])
|
||||||
|
.append_query_results(vec![dummy_row])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let payload = CreateLocationRequestBodyByUpstreamId {
|
||||||
|
path: "/".to_string(),
|
||||||
|
match_type: "prefix".to_string(),
|
||||||
|
order: 1,
|
||||||
|
upstream_id: up_id,
|
||||||
|
preserve_host_header: None,
|
||||||
|
allowed_methods: None,
|
||||||
|
custom_config: 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_proxy_pass_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: Some("http".to_string()),
|
||||||
|
proxy_pass_host: Some("127.0.0.1".to_string()),
|
||||||
|
proxy_pass_port: Some(8080),
|
||||||
|
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 dummy_row: Vec<MockRow> = vec![];
|
||||||
|
|
||||||
|
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()))]])
|
||||||
|
.append_query_results(vec![dummy_row])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let router = get_router_with_state(db.clone());
|
||||||
|
let server = TestServer::new(router).expect("failed to create test server");
|
||||||
|
|
||||||
|
let payload = CreateLocationRequestBodyByProxyPass {
|
||||||
|
path: "/".to_string(),
|
||||||
|
match_type: "prefix".to_string(),
|
||||||
|
order: 1,
|
||||||
|
proxy_pass_protocol: "http".to_string(),
|
||||||
|
proxy_pass_host: "127.0.0.1".to_string(),
|
||||||
|
proxy_pass_port: 8080,
|
||||||
|
preserve_host_header: None,
|
||||||
|
allowed_methods: None,
|
||||||
|
custom_config: 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 = CreateLocationRequestBodyByUpstreamId {
|
||||||
|
path: "/".to_string(),
|
||||||
|
match_type: "prefix".to_string(),
|
||||||
|
order: 1,
|
||||||
|
upstream_id: uuid::Uuid::new_v4(),
|
||||||
|
preserve_host_header: None,
|
||||||
|
allowed_methods: None,
|
||||||
|
custom_config: 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
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, MockRow};
|
||||||
|
|
||||||
|
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 dummy_row: Vec<MockRow> = vec![];
|
||||||
|
|
||||||
|
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()))]])
|
||||||
|
.append_query_results(vec![dummy_row])
|
||||||
|
.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,159 @@
|
|||||||
|
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, MockRow};
|
||||||
|
|
||||||
|
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 dummy_row: Vec<MockRow> = vec![];
|
||||||
|
|
||||||
|
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()))]])
|
||||||
|
.append_query_results(vec![dummy_row])
|
||||||
|
.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,182 @@
|
|||||||
|
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, MockRow};
|
||||||
|
|
||||||
|
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 dummy_row: Vec<MockRow> = vec![];
|
||||||
|
|
||||||
|
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()))]])
|
||||||
|
.append_query_results(vec![dummy_row])
|
||||||
|
.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,220 @@
|
|||||||
|
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, MockRow};
|
||||||
|
|
||||||
|
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 dummy_row: Vec<MockRow> = vec![];
|
||||||
|
|
||||||
|
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()))]])
|
||||||
|
.append_query_results(vec![dummy_row])
|
||||||
|
.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,227 @@
|
|||||||
|
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, MockRow};
|
||||||
|
|
||||||
|
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 dummy_row: Vec<MockRow> = vec![];
|
||||||
|
|
||||||
|
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()))]])
|
||||||
|
.append_query_results(vec![dummy_row])
|
||||||
|
.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()))
|
||||||
|
}
|
||||||
@@ -131,7 +131,7 @@ pub async fn create_upstream(
|
|||||||
state
|
state
|
||||||
.service
|
.service
|
||||||
.nginx
|
.nginx
|
||||||
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
|
.regenerate_and_apply_config(state.service.agent_client.clone(), &Some(&mut tx))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
@@ -145,7 +145,7 @@ mod tests {
|
|||||||
|
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum_test::TestServer;
|
use axum_test::TestServer;
|
||||||
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase, MockRow};
|
||||||
|
|
||||||
use database::generated::entities::{upstream, upstream_target};
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
|
|
||||||
@@ -204,6 +204,7 @@ mod tests {
|
|||||||
created_at: chrono::Utc::now(),
|
created_at: chrono::Utc::now(),
|
||||||
updated_at: chrono::Utc::now(),
|
updated_at: chrono::Utc::now(),
|
||||||
};
|
};
|
||||||
|
let dummy_row: Vec<MockRow> = vec![];
|
||||||
|
|
||||||
// service will likely perform an insert and then query to return created models
|
// service will likely perform an insert and then query to return created models
|
||||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
@@ -213,6 +214,7 @@ mod tests {
|
|||||||
// `find_with_related` returns rows of `(upstream, Option<target>)` which
|
// `find_with_related` returns rows of `(upstream, Option<target>)` which
|
||||||
// the mock DB expects as `(Model, Option<Model>)` per row.
|
// the mock DB expects as `(Model, Option<Model>)` per row.
|
||||||
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
|
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
|
||||||
|
.append_query_results(vec![dummy_row])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let router = get_router_with_state(db.clone());
|
let router = get_router_with_state(db.clone());
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ pub async fn add_upstream_target(
|
|||||||
state
|
state
|
||||||
.service
|
.service
|
||||||
.nginx
|
.nginx
|
||||||
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
|
.regenerate_and_apply_config(state.service.agent_client.clone(), &Some(&mut tx))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
@@ -100,7 +100,7 @@ mod tests {
|
|||||||
|
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum_test::TestServer;
|
use axum_test::TestServer;
|
||||||
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase, MockRow};
|
||||||
|
|
||||||
use database::generated::entities::{upstream, upstream_target};
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
|
|
||||||
@@ -158,6 +158,7 @@ mod tests {
|
|||||||
created_at: chrono::Utc::now(),
|
created_at: chrono::Utc::now(),
|
||||||
updated_at: chrono::Utc::now(),
|
updated_at: chrono::Utc::now(),
|
||||||
};
|
};
|
||||||
|
let dummy_row: Vec<MockRow> = vec![];
|
||||||
|
|
||||||
// configure mock agent to return an error on apply
|
// configure mock agent to return an error on apply
|
||||||
let mut mock = MockAgentService::new();
|
let mut mock = MockAgentService::new();
|
||||||
@@ -175,6 +176,7 @@ mod tests {
|
|||||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
.append_query_results(vec![vec![target_model.clone()]])
|
.append_query_results(vec![vec![target_model.clone()]])
|
||||||
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
|
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
|
||||||
|
.append_query_results(vec![dummy_row])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let program_settings = ProgramSettings::mock();
|
let program_settings = ProgramSettings::mock();
|
||||||
@@ -235,11 +237,13 @@ mod tests {
|
|||||||
created_at: chrono::Utc::now(),
|
created_at: chrono::Utc::now(),
|
||||||
updated_at: chrono::Utc::now(),
|
updated_at: chrono::Utc::now(),
|
||||||
};
|
};
|
||||||
|
let dummy_row: Vec<MockRow> = vec![];
|
||||||
|
|
||||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
.append_query_results(vec![vec![target_model.clone()]])
|
.append_query_results(vec![vec![target_model.clone()]])
|
||||||
// additional query result for regenerate_and_apply_config -> generate_config
|
// additional query result for regenerate_and_apply_config -> generate_config
|
||||||
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
|
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
|
||||||
|
.append_query_results(vec![dummy_row])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let router = get_router_with_state(db.clone());
|
let router = get_router_with_state(db.clone());
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ pub async fn remove_upstream(
|
|||||||
state
|
state
|
||||||
.service
|
.service
|
||||||
.nginx
|
.nginx
|
||||||
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
|
.regenerate_and_apply_config(state.service.agent_client.clone(), &Some(&mut tx))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
@@ -54,7 +54,7 @@ mod tests {
|
|||||||
|
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum_test::TestServer;
|
use axum_test::TestServer;
|
||||||
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase, MockExecResult};
|
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase, MockExecResult, MockRow};
|
||||||
|
|
||||||
use database::generated::entities::{upstream, upstream_target};
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
|
|
||||||
@@ -109,6 +109,7 @@ mod tests {
|
|||||||
created_at: chrono::Utc::now(),
|
created_at: chrono::Utc::now(),
|
||||||
updated_at: chrono::Utc::now(),
|
updated_at: chrono::Utc::now(),
|
||||||
};
|
};
|
||||||
|
let dummy_row: Vec<MockRow> = vec![];
|
||||||
|
|
||||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
.append_query_results(vec![vec![existing.clone()]])
|
.append_query_results(vec![vec![existing.clone()]])
|
||||||
@@ -124,6 +125,7 @@ mod tests {
|
|||||||
])
|
])
|
||||||
// additional query result for regenerate_and_apply_config -> generate_config
|
// additional query result for regenerate_and_apply_config -> generate_config
|
||||||
.append_query_results(vec![vec![(existing.clone(), Some(target_model.clone()))]])
|
.append_query_results(vec![vec![(existing.clone(), Some(target_model.clone()))]])
|
||||||
|
.append_query_results(vec![dummy_row])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let router = get_router_with_state(db.clone());
|
let router = get_router_with_state(db.clone());
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ pub async fn remove_upstream_target(
|
|||||||
state
|
state
|
||||||
.service
|
.service
|
||||||
.nginx
|
.nginx
|
||||||
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
|
.regenerate_and_apply_config(state.service.agent_client.clone(), &Some(&mut tx))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
@@ -54,7 +54,7 @@ mod tests {
|
|||||||
|
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum_test::TestServer;
|
use axum_test::TestServer;
|
||||||
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase, MockExecResult};
|
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase, MockExecResult, MockRow};
|
||||||
|
|
||||||
use database::generated::entities::{upstream, upstream_target};
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
|
|
||||||
@@ -110,6 +110,7 @@ mod tests {
|
|||||||
created_at: chrono::Utc::now(),
|
created_at: chrono::Utc::now(),
|
||||||
updated_at: chrono::Utc::now(),
|
updated_at: chrono::Utc::now(),
|
||||||
};
|
};
|
||||||
|
let dummy_row: Vec<MockRow> = vec![];
|
||||||
|
|
||||||
let first: Vec<Vec<upstream_target::Model>> = vec![vec![current_model.clone()]];
|
let first: Vec<Vec<upstream_target::Model>> = vec![vec![current_model.clone()]];
|
||||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
@@ -120,6 +121,7 @@ mod tests {
|
|||||||
}])
|
}])
|
||||||
// additional query result for regenerate_and_apply_config -> generate_config
|
// additional query result for regenerate_and_apply_config -> generate_config
|
||||||
.append_query_results(vec![vec![(up_model.clone(), Some(current_model.clone()))]])
|
.append_query_results(vec![vec![(up_model.clone(), Some(current_model.clone()))]])
|
||||||
|
.append_query_results(vec![dummy_row])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let router = get_router_with_state(db.clone());
|
let router = get_router_with_state(db.clone());
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ pub async fn update_upstream(
|
|||||||
state
|
state
|
||||||
.service
|
.service
|
||||||
.nginx
|
.nginx
|
||||||
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
|
.regenerate_and_apply_config(state.service.agent_client.clone(), &Some(&mut tx))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
@@ -92,7 +92,7 @@ mod tests {
|
|||||||
|
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum_test::TestServer;
|
use axum_test::TestServer;
|
||||||
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase, MockRow};
|
||||||
|
|
||||||
use database::generated::entities::{upstream, upstream_target};
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
|
|
||||||
@@ -152,6 +152,7 @@ mod tests {
|
|||||||
let up_model = current_model.clone();
|
let up_model = current_model.clone();
|
||||||
let first: Vec<Vec<upstream::Model>> = vec![vec![current_model.clone()]];
|
let first: Vec<Vec<upstream::Model>> = vec![vec![current_model.clone()]];
|
||||||
let second: Vec<Vec<upstream::Model>> = vec![vec![updated_model.clone()]];
|
let second: Vec<Vec<upstream::Model>> = vec![vec![updated_model.clone()]];
|
||||||
|
let dummy_row: Vec<MockRow> = vec![];
|
||||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
.append_query_results(first)
|
.append_query_results(first)
|
||||||
.append_query_results(second)
|
.append_query_results(second)
|
||||||
@@ -160,6 +161,7 @@ mod tests {
|
|||||||
up_model.clone(),
|
up_model.clone(),
|
||||||
Option::<upstream_target::Model>::None,
|
Option::<upstream_target::Model>::None,
|
||||||
)]])
|
)]])
|
||||||
|
.append_query_results(vec![dummy_row])
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let router = get_router_with_state(db.clone());
|
let router = get_router_with_state(db.clone());
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ pub async fn update_upstream_target(
|
|||||||
state
|
state
|
||||||
.service
|
.service
|
||||||
.nginx
|
.nginx
|
||||||
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
|
.regenerate_and_apply_config(state.service.agent_client.clone(), &Some(&mut tx))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
tx.commit().await?;
|
tx.commit().await?;
|
||||||
@@ -165,11 +165,14 @@ mod tests {
|
|||||||
// additional query result for regenerate_and_apply_config -> generate_config
|
// additional query result for regenerate_and_apply_config -> generate_config
|
||||||
let third: Vec<Vec<(upstream::Model, Option<upstream_target::Model>)>> =
|
let third: Vec<Vec<(upstream::Model, Option<upstream_target::Model>)>> =
|
||||||
vec![vec![(up_model.clone(), Some(updated_model.clone()))]];
|
vec![vec![(up_model.clone(), Some(updated_model.clone()))]];
|
||||||
|
// placeholder for other queries called by regenerate_and_apply_config
|
||||||
|
let fourth: Vec<Vec<upstream::Model>> = vec![vec![]];
|
||||||
|
|
||||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
.append_query_results(first)
|
.append_query_results(first)
|
||||||
.append_query_results(second)
|
.append_query_results(second)
|
||||||
.append_query_results(third)
|
.append_query_results(third)
|
||||||
|
.append_query_results(fourth)
|
||||||
.into_connection();
|
.into_connection();
|
||||||
|
|
||||||
let router = get_router_with_state(db.clone());
|
let router = get_router_with_state(db.clone());
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ pub mod builder;
|
|||||||
pub mod info;
|
pub mod info;
|
||||||
pub mod traits;
|
pub mod traits;
|
||||||
|
|
||||||
|
pub mod location;
|
||||||
|
pub mod proxy_host;
|
||||||
pub mod upstream;
|
pub mod upstream;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -14,6 +16,8 @@ use crate::{
|
|||||||
agent_client::AgentService,
|
agent_client::AgentService,
|
||||||
nginx::{
|
nginx::{
|
||||||
builder::{NginxConfigBuilder, NginxConfigProvider},
|
builder::{NginxConfigBuilder, NginxConfigProvider},
|
||||||
|
location::{LocationService, LocationServiceImpl},
|
||||||
|
proxy_host::{ProxyService, ProxyServiceImpl},
|
||||||
upstream::{UpstreamService, UpstreamServiceImpl},
|
upstream::{UpstreamService, UpstreamServiceImpl},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -23,7 +27,12 @@ pub struct NginxService {
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
connection: Arc<DatabaseConnection>,
|
connection: Arc<DatabaseConnection>,
|
||||||
//
|
//
|
||||||
|
#[allow(dead_code)]
|
||||||
upstream_service: Arc<dyn UpstreamService>,
|
upstream_service: Arc<dyn UpstreamService>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
proxy_service: Arc<dyn ProxyService>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
location_service: Arc<dyn LocationService>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NginxService {
|
impl NginxService {
|
||||||
@@ -32,6 +41,8 @@ impl NginxService {
|
|||||||
connection: connection.clone(),
|
connection: connection.clone(),
|
||||||
//
|
//
|
||||||
upstream_service: Arc::new(UpstreamServiceImpl::new(connection.clone())),
|
upstream_service: Arc::new(UpstreamServiceImpl::new(connection.clone())),
|
||||||
|
proxy_service: Arc::new(ProxyServiceImpl::new(connection.clone())),
|
||||||
|
location_service: Arc::new(LocationServiceImpl::new(connection.clone())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +50,14 @@ impl NginxService {
|
|||||||
self.upstream_service.clone()
|
self.upstream_service.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_proxy_service(&self) -> Arc<dyn ProxyService> {
|
||||||
|
self.proxy_service.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_location_service(&self) -> Arc<dyn LocationService> {
|
||||||
|
self.location_service.clone()
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub async fn validate_config(
|
pub async fn validate_config(
|
||||||
&self,
|
&self,
|
||||||
@@ -62,20 +81,21 @@ impl NginxService {
|
|||||||
|
|
||||||
pub async fn generate_config(
|
pub async fn generate_config(
|
||||||
&self,
|
&self,
|
||||||
tx: Option<&mut DatabaseTransaction>,
|
tx: &Option<&mut DatabaseTransaction>,
|
||||||
) -> Result<String, ServiceError> {
|
) -> Result<String, ServiceError> {
|
||||||
let mut builder = NginxConfigBuilder::default();
|
let mut builder = NginxConfigBuilder::default();
|
||||||
self.upstream_service
|
self.upstream_service
|
||||||
.generate_config(&mut builder, tx)
|
.generate_config(&mut builder, tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
self.proxy_service.generate_config(&mut builder, tx).await?;
|
||||||
|
|
||||||
Ok(builder.to_nginx_config(None))
|
builder.to_nginx_config(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn regenerate_and_apply_config(
|
pub async fn regenerate_and_apply_config(
|
||||||
&self,
|
&self,
|
||||||
agent: Arc<dyn AgentService>,
|
agent: Arc<dyn AgentService>,
|
||||||
tx: Option<&mut DatabaseTransaction>,
|
tx: &Option<&mut DatabaseTransaction>,
|
||||||
) -> Result<(), ServiceError> {
|
) -> Result<(), ServiceError> {
|
||||||
let config = self.generate_config(tx).await?;
|
let config = self.generate_config(tx).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
use crate::services::nginx::info::upstream::UpstreamInfo;
|
use crate::{
|
||||||
|
errors::service_error::ServiceError,
|
||||||
|
services::nginx::info::{proxy_host::ProxyHostInfo, upstream::UpstreamInfo},
|
||||||
|
};
|
||||||
|
|
||||||
pub const INDENT_SIZE: usize = 2;
|
pub const INDENT_SIZE: usize = 2;
|
||||||
|
|
||||||
pub trait NginxConfigProvider {
|
pub trait NginxConfigProvider {
|
||||||
fn to_nginx_config(&self, indent: Option<usize>) -> String;
|
fn to_nginx_config(&self, indent: Option<usize>) -> Result<String, ServiceError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct NginxConfigBuilder {
|
pub struct NginxConfigBuilder {
|
||||||
upstreams: Vec<UpstreamInfo>,
|
upstreams: Vec<UpstreamInfo>,
|
||||||
|
proxy_hosts: Vec<ProxyHostInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NginxConfigBuilder {
|
impl NginxConfigBuilder {
|
||||||
@@ -21,10 +25,20 @@ impl NginxConfigBuilder {
|
|||||||
self.add_upstream(upstream);
|
self.add_upstream(upstream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_proxy_host(&mut self, proxy_host: ProxyHostInfo) {
|
||||||
|
self.proxy_hosts.push(proxy_host);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_proxy_hosts(&mut self, proxy_hosts: Vec<ProxyHostInfo>) {
|
||||||
|
for proxy_host in proxy_hosts {
|
||||||
|
self.add_proxy_host(proxy_host);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NginxConfigProvider for NginxConfigBuilder {
|
impl NginxConfigProvider for NginxConfigBuilder {
|
||||||
fn to_nginx_config(&self, indent: Option<usize>) -> String {
|
fn to_nginx_config(&self, indent: Option<usize>) -> Result<String, ServiceError> {
|
||||||
let mut config = format!(
|
let mut config = format!(
|
||||||
"# Nginx Config Generated by YANPM at {}",
|
"# Nginx Config Generated by YANPM at {}",
|
||||||
chrono::Utc::now()
|
chrono::Utc::now()
|
||||||
@@ -32,12 +46,16 @@ impl NginxConfigProvider for NginxConfigBuilder {
|
|||||||
|
|
||||||
for upstream in &self.upstreams {
|
for upstream in &self.upstreams {
|
||||||
config.push('\n');
|
config.push('\n');
|
||||||
config.push_str(&upstream.to_nginx_config(indent));
|
config.push_str(&upstream.to_nginx_config(indent)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for proxy_host in &self.proxy_hosts {
|
||||||
|
config.push('\n');
|
||||||
|
config.push_str(&proxy_host.to_nginx_config(indent)?);
|
||||||
|
}
|
||||||
// TODO: Add other sections like servers, locations, etc.
|
// TODO: Add other sections like servers, locations, etc.
|
||||||
// trailing newline for file ending
|
// trailing newline for file ending
|
||||||
config.push('\n');
|
config.push('\n');
|
||||||
config
|
Ok(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
|
pub mod location;
|
||||||
|
pub mod proxy_host;
|
||||||
pub mod upstream;
|
pub mod upstream;
|
||||||
pub mod upstream_target;
|
pub mod upstream_target;
|
||||||
|
|||||||
296
apps/api/src/services/nginx/info/location.rs
Normal file
296
apps/api/src/services/nginx/info/location.rs
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use database::generated::entities::{location, proxy_host, upstream};
|
||||||
|
use sea_orm::ActiveValue::{Set, Unchanged};
|
||||||
|
use tracing::warn;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::service_error::ServiceError,
|
||||||
|
services::nginx::{builder::NginxConfigProvider, traits::indentable::Indentable},
|
||||||
|
set_if_some,
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ProxyPassInfo {
|
||||||
|
pub protocol: String,
|
||||||
|
pub host: String,
|
||||||
|
pub port: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct LocationInfo {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub host_id: Uuid,
|
||||||
|
pub path: String,
|
||||||
|
pub match_type: String,
|
||||||
|
pub order: i64,
|
||||||
|
pub upstream_id: Option<Uuid>,
|
||||||
|
pub proxy_pass_info: Option<ProxyPassInfo>,
|
||||||
|
pub preserve_host_header: Option<bool>,
|
||||||
|
pub allowed_methods: Option<Vec<String>>,
|
||||||
|
pub custom_config: Option<String>,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
//
|
||||||
|
pub upstream: Option<super::upstream::UpstreamInfo>,
|
||||||
|
pub proxy_host: Option<super::proxy_host::ProxyHostInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CreateLocationInfo {
|
||||||
|
pub host_id: Uuid,
|
||||||
|
pub path: String,
|
||||||
|
pub match_type: String,
|
||||||
|
pub order: i64,
|
||||||
|
pub upstream_id: Option<Uuid>,
|
||||||
|
pub proxy_pass_protocol: Option<String>,
|
||||||
|
pub proxy_pass_host: Option<String>,
|
||||||
|
pub proxy_pass_port: Option<i64>,
|
||||||
|
pub preserve_host_header: Option<bool>,
|
||||||
|
pub allowed_methods: Option<Vec<String>>,
|
||||||
|
pub custom_config: Option<String>,
|
||||||
|
pub enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct UpdateLocationInfo {
|
||||||
|
pub path: Option<String>,
|
||||||
|
pub match_type: Option<String>,
|
||||||
|
pub order: Option<i64>,
|
||||||
|
pub upstream_id: Option<Option<Uuid>>,
|
||||||
|
pub proxy_pass_protocol: Option<Option<String>>,
|
||||||
|
pub proxy_pass_host: Option<Option<String>>,
|
||||||
|
pub proxy_pass_port: Option<Option<i64>>,
|
||||||
|
pub preserve_host_header: Option<Option<bool>>,
|
||||||
|
pub allowed_methods: Option<Option<Vec<String>>>,
|
||||||
|
pub custom_config: Option<Option<String>>,
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<location::Model> for LocationInfo {
|
||||||
|
fn from(model: location::Model) -> Self {
|
||||||
|
let allowed_methods: Option<Vec<String>> = match model.allowed_methods {
|
||||||
|
Some(JsonValue::Array(arr)) => {
|
||||||
|
let v: Vec<String> = arr
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|val| val.as_str().map(|s| s.to_string()))
|
||||||
|
.collect();
|
||||||
|
if v.is_empty() { None } else { Some(v) }
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id: model.id,
|
||||||
|
host_id: model.host_id,
|
||||||
|
path: model.path,
|
||||||
|
match_type: model.match_type,
|
||||||
|
order: model.order,
|
||||||
|
upstream_id: model.upstream_id,
|
||||||
|
proxy_pass_info: match (
|
||||||
|
model.proxy_pass_protocol,
|
||||||
|
model.proxy_pass_host,
|
||||||
|
model.proxy_pass_port,
|
||||||
|
) {
|
||||||
|
(Some(protocol), Some(host), Some(port)) => Some(ProxyPassInfo {
|
||||||
|
protocol,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
}),
|
||||||
|
(Some(_), _, _) | (_, Some(_), _) | (_, _, Some(_)) => {
|
||||||
|
warn!("Incomplete proxy_pass_info for location {}", model.id);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
preserve_host_header: model.preserve_host_header,
|
||||||
|
allowed_methods,
|
||||||
|
custom_config: model.custom_config,
|
||||||
|
enabled: model.enabled,
|
||||||
|
created_at: model.created_at,
|
||||||
|
updated_at: model.updated_at,
|
||||||
|
upstream: None,
|
||||||
|
proxy_host: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(location::Model, Option<proxy_host::Model>)> for LocationInfo {
|
||||||
|
fn from(data: (location::Model, Option<proxy_host::Model>)) -> Self {
|
||||||
|
let (location_model, proxy_host_model_opt) = data;
|
||||||
|
(location_model, proxy_host_model_opt, None).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl
|
||||||
|
From<(
|
||||||
|
location::Model,
|
||||||
|
Option<proxy_host::Model>,
|
||||||
|
Option<upstream::Model>,
|
||||||
|
)> for LocationInfo
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
data: (
|
||||||
|
location::Model,
|
||||||
|
Option<proxy_host::Model>,
|
||||||
|
Option<upstream::Model>,
|
||||||
|
),
|
||||||
|
) -> Self {
|
||||||
|
let (location_model, proxy_host_model_opt, upstream_model_opt) = data;
|
||||||
|
let mut location_info = LocationInfo::from(location_model);
|
||||||
|
|
||||||
|
if let Some(upstream_model) = upstream_model_opt {
|
||||||
|
location_info.upstream = Some(super::upstream::UpstreamInfo::from(upstream_model));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(proxy_host_model) = proxy_host_model_opt {
|
||||||
|
location_info.proxy_host =
|
||||||
|
Some(super::proxy_host::ProxyHostInfo::from(proxy_host_model));
|
||||||
|
}
|
||||||
|
|
||||||
|
location_info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CreateLocationInfo> for location::ActiveModel {
|
||||||
|
fn from(val: CreateLocationInfo) -> Self {
|
||||||
|
location::ActiveModel {
|
||||||
|
id: Set(Uuid::new_v4()),
|
||||||
|
host_id: Set(val.host_id),
|
||||||
|
path: Set(val.path),
|
||||||
|
match_type: Set(val.match_type),
|
||||||
|
order: Set(val.order),
|
||||||
|
upstream_id: Set(val.upstream_id),
|
||||||
|
proxy_pass_protocol: Set(val.proxy_pass_protocol),
|
||||||
|
proxy_pass_host: Set(val.proxy_pass_host),
|
||||||
|
proxy_pass_port: Set(val.proxy_pass_port),
|
||||||
|
preserve_host_header: Set(val.preserve_host_header),
|
||||||
|
allowed_methods: Set(val
|
||||||
|
.allowed_methods
|
||||||
|
.map(|v| JsonValue::Array(v.into_iter().map(JsonValue::String).collect()))),
|
||||||
|
custom_config: Set(val.custom_config),
|
||||||
|
enabled: Set(val.enabled),
|
||||||
|
created_at: Set(chrono::Utc::now()),
|
||||||
|
updated_at: Set(chrono::Utc::now()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateLocationInfo {
|
||||||
|
pub fn apply_to_model(self, current_model: location::Model) -> location::ActiveModel {
|
||||||
|
location::ActiveModel {
|
||||||
|
id: Unchanged(current_model.id),
|
||||||
|
host_id: Unchanged(current_model.host_id),
|
||||||
|
path: set_if_some!(self.path),
|
||||||
|
match_type: set_if_some!(self.match_type),
|
||||||
|
order: set_if_some!(self.order),
|
||||||
|
upstream_id: match self.upstream_id {
|
||||||
|
Some(inner) => Set(inner),
|
||||||
|
None => Unchanged(current_model.upstream_id),
|
||||||
|
},
|
||||||
|
proxy_pass_protocol: match self.proxy_pass_protocol {
|
||||||
|
Some(inner) => Set(inner),
|
||||||
|
None => Unchanged(current_model.proxy_pass_protocol),
|
||||||
|
},
|
||||||
|
proxy_pass_host: match self.proxy_pass_host {
|
||||||
|
Some(inner) => Set(inner),
|
||||||
|
None => Unchanged(current_model.proxy_pass_host),
|
||||||
|
},
|
||||||
|
proxy_pass_port: match self.proxy_pass_port {
|
||||||
|
Some(inner) => Set(inner),
|
||||||
|
None => Unchanged(current_model.proxy_pass_port),
|
||||||
|
},
|
||||||
|
preserve_host_header: match self.preserve_host_header {
|
||||||
|
Some(inner) => Set(inner),
|
||||||
|
None => Unchanged(current_model.preserve_host_header),
|
||||||
|
},
|
||||||
|
allowed_methods: match self.allowed_methods {
|
||||||
|
Some(inner) => {
|
||||||
|
let json_opt = inner
|
||||||
|
.map(|v| JsonValue::Array(v.into_iter().map(JsonValue::String).collect()));
|
||||||
|
Set(json_opt)
|
||||||
|
}
|
||||||
|
None => Unchanged(current_model.allowed_methods),
|
||||||
|
},
|
||||||
|
custom_config: match self.custom_config {
|
||||||
|
Some(inner) => Set(inner),
|
||||||
|
None => Unchanged(current_model.custom_config),
|
||||||
|
},
|
||||||
|
enabled: set_if_some!(self.enabled),
|
||||||
|
created_at: Unchanged(current_model.created_at),
|
||||||
|
updated_at: Set(chrono::Utc::now()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NginxConfigProvider for LocationInfo {
|
||||||
|
fn to_nginx_config(&self, indent: Option<usize>) -> Result<String, ServiceError> {
|
||||||
|
let indent = indent.unwrap_or(0);
|
||||||
|
|
||||||
|
let selector = match self.match_type.as_str() {
|
||||||
|
"exact" => format!("location = {} ", self.path),
|
||||||
|
"regex" => format!("location ~ {} ", self.path),
|
||||||
|
_ => format!("location {} ", self.path),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut body_lines: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
if let Some(methods) = &self.allowed_methods
|
||||||
|
&& !methods.is_empty()
|
||||||
|
{
|
||||||
|
body_lines.push(format!(
|
||||||
|
"limit_except {} {{ deny all; }}",
|
||||||
|
methods.join(" ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(upstream) = &self.upstream {
|
||||||
|
body_lines.push(format!(
|
||||||
|
"proxy_pass {}://{};",
|
||||||
|
upstream.protocol, upstream.name
|
||||||
|
));
|
||||||
|
} else if let Some(host) = &self.proxy_pass_info {
|
||||||
|
body_lines.push(format!(
|
||||||
|
"proxy_pass {}://{}:{};",
|
||||||
|
host.protocol, host.host, host.port
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Location {} has neither upstream nor proxy_pass_host defined",
|
||||||
|
self.id
|
||||||
|
);
|
||||||
|
return Err(ServiceError::InternalError(
|
||||||
|
"Location must have either an upstream or a proxy_pass_host defined".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(preserve) = self.preserve_host_header {
|
||||||
|
if preserve {
|
||||||
|
body_lines.push("proxy_set_header Host $host;".to_string());
|
||||||
|
} else {
|
||||||
|
body_lines.push("proxy_set_header Host $proxy_host;".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(cfg) = &self.custom_config
|
||||||
|
&& !cfg.trim().is_empty()
|
||||||
|
{
|
||||||
|
body_lines.push(cfg.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let inner = if body_lines.is_empty() {
|
||||||
|
"# location has no config".to_string()
|
||||||
|
} else {
|
||||||
|
body_lines
|
||||||
|
.into_iter()
|
||||||
|
.map(|l| l.indent(indent + 2))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\n")
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(format!("{}{{\n{}\n}}", selector.trim_end(), inner).indent(indent))
|
||||||
|
}
|
||||||
|
}
|
||||||
251
apps/api/src/services/nginx/info/proxy_host.rs
Normal file
251
apps/api/src/services/nginx/info/proxy_host.rs
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use database::generated::entities::{location, proxy_host};
|
||||||
|
use sea_orm::ActiveValue::{Set, Unchanged};
|
||||||
|
use serde_json::Value as JsonValue;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::service_error::ServiceError,
|
||||||
|
services::nginx::{
|
||||||
|
builder::{INDENT_SIZE, NginxConfigProvider},
|
||||||
|
traits::indentable::Indentable,
|
||||||
|
},
|
||||||
|
set_if_some,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ProxyHostInfo {
|
||||||
|
pub id: 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 meta: Option<JsonValue>,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
//
|
||||||
|
pub upstream: Option<super::upstream::UpstreamInfo>,
|
||||||
|
pub locations: Vec<super::location::LocationInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProxyHostCreateInfo {
|
||||||
|
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<JsonValue>,
|
||||||
|
pub default_upstream_id: Option<Uuid>,
|
||||||
|
pub created_by: Option<Uuid>,
|
||||||
|
//
|
||||||
|
pub locations: Vec<super::location::CreateLocationInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct UpdateProxyHostInfo {
|
||||||
|
pub name: Option<Option<String>>,
|
||||||
|
pub domain: Option<String>,
|
||||||
|
pub scheme: Option<String>,
|
||||||
|
pub listen_port: Option<i64>,
|
||||||
|
pub forward_scheme: Option<String>,
|
||||||
|
pub forward_host: Option<Option<String>>,
|
||||||
|
pub forward_port: Option<Option<i64>>,
|
||||||
|
pub preserve_host_header: Option<bool>,
|
||||||
|
pub enable_websocket: Option<bool>,
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
pub meta: Option<Option<JsonValue>>,
|
||||||
|
pub default_upstream_id: Option<Option<Uuid>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<proxy_host::Model> for ProxyHostInfo {
|
||||||
|
fn from(model: proxy_host::Model) -> Self {
|
||||||
|
Self {
|
||||||
|
id: model.id,
|
||||||
|
name: model.name,
|
||||||
|
domain: model.domain,
|
||||||
|
scheme: model.scheme,
|
||||||
|
listen_port: model.listen_port,
|
||||||
|
forward_scheme: model.forward_scheme,
|
||||||
|
forward_host: model.forward_host,
|
||||||
|
forward_port: model.forward_port,
|
||||||
|
preserve_host_header: model.preserve_host_header,
|
||||||
|
enable_websocket: model.enable_websocket,
|
||||||
|
meta: model.meta,
|
||||||
|
enabled: model.enabled,
|
||||||
|
created_at: model.created_at,
|
||||||
|
updated_at: model.updated_at,
|
||||||
|
upstream: None,
|
||||||
|
locations: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(proxy_host::Model, Vec<location::Model>)> for ProxyHostInfo {
|
||||||
|
fn from(data: (proxy_host::Model, Vec<location::Model>)) -> Self {
|
||||||
|
let (proxy_model, location_models) = data;
|
||||||
|
let mut proxy_info = ProxyHostInfo::from(proxy_model);
|
||||||
|
let locations_info: Vec<super::location::LocationInfo> =
|
||||||
|
location_models.into_iter().map(|m| m.into()).collect();
|
||||||
|
proxy_info.locations = locations_info;
|
||||||
|
proxy_info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ProxyHostCreateInfo> for (proxy_host::ActiveModel, Vec<location::ActiveModel>) {
|
||||||
|
fn from(val: ProxyHostCreateInfo) -> Self {
|
||||||
|
let proxy_host = proxy_host::ActiveModel {
|
||||||
|
id: Set(Uuid::new_v4()),
|
||||||
|
name: Set(val.name),
|
||||||
|
domain: Set(val.domain),
|
||||||
|
scheme: Set(val.scheme),
|
||||||
|
listen_port: Set(val.listen_port),
|
||||||
|
forward_scheme: Set(val.forward_scheme),
|
||||||
|
forward_host: Set(val.forward_host),
|
||||||
|
forward_port: Set(val.forward_port),
|
||||||
|
preserve_host_header: Set(val.preserve_host_header),
|
||||||
|
enable_websocket: Set(val.enable_websocket),
|
||||||
|
enabled: Set(val.enabled),
|
||||||
|
meta: Set(val.meta),
|
||||||
|
default_upstream_id: Set(val.default_upstream_id),
|
||||||
|
created_by: Set(val.created_by),
|
||||||
|
created_at: Set(chrono::Utc::now()),
|
||||||
|
updated_at: Set(chrono::Utc::now()),
|
||||||
|
};
|
||||||
|
let location_models = val.locations.into_iter().map(|loc| loc.into()).collect();
|
||||||
|
(proxy_host, location_models)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ProxyHostInfo> for proxy_host::ActiveModel {
|
||||||
|
fn from(val: ProxyHostInfo) -> Self {
|
||||||
|
proxy_host::ActiveModel {
|
||||||
|
id: Set(val.id),
|
||||||
|
name: Set(val.name),
|
||||||
|
domain: Set(val.domain),
|
||||||
|
scheme: Set(val.scheme),
|
||||||
|
listen_port: Set(val.listen_port),
|
||||||
|
forward_scheme: Set(val.forward_scheme),
|
||||||
|
forward_host: Set(val.forward_host),
|
||||||
|
forward_port: Set(val.forward_port),
|
||||||
|
preserve_host_header: Set(val.preserve_host_header),
|
||||||
|
enable_websocket: Set(val.enable_websocket),
|
||||||
|
enabled: Set(val.enabled),
|
||||||
|
meta: Set(val.meta),
|
||||||
|
default_upstream_id: Set(val.upstream.as_ref().map(|u| u.id)),
|
||||||
|
created_by: Set(None),
|
||||||
|
created_at: Set(val.created_at),
|
||||||
|
updated_at: Set(val.updated_at),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateProxyHostInfo {
|
||||||
|
pub fn apply_to_model(self, current_model: proxy_host::Model) -> proxy_host::ActiveModel {
|
||||||
|
proxy_host::ActiveModel {
|
||||||
|
id: Unchanged(current_model.id),
|
||||||
|
name: match self.name {
|
||||||
|
Some(inner) => Set(inner),
|
||||||
|
None => Unchanged(current_model.name),
|
||||||
|
},
|
||||||
|
domain: set_if_some!(self.domain),
|
||||||
|
scheme: set_if_some!(self.scheme),
|
||||||
|
listen_port: set_if_some!(self.listen_port),
|
||||||
|
forward_scheme: set_if_some!(self.forward_scheme),
|
||||||
|
forward_host: match self.forward_host {
|
||||||
|
Some(inner) => Set(inner),
|
||||||
|
None => Unchanged(current_model.forward_host),
|
||||||
|
},
|
||||||
|
forward_port: match self.forward_port {
|
||||||
|
Some(inner) => Set(inner),
|
||||||
|
None => Unchanged(current_model.forward_port),
|
||||||
|
},
|
||||||
|
preserve_host_header: set_if_some!(self.preserve_host_header),
|
||||||
|
enable_websocket: set_if_some!(self.enable_websocket),
|
||||||
|
enabled: set_if_some!(self.enabled),
|
||||||
|
meta: set_if_some!(self.meta),
|
||||||
|
default_upstream_id: match self.default_upstream_id {
|
||||||
|
Some(inner) => Set(inner),
|
||||||
|
None => Unchanged(current_model.default_upstream_id),
|
||||||
|
},
|
||||||
|
created_by: Unchanged(current_model.created_by),
|
||||||
|
created_at: Unchanged(current_model.created_at),
|
||||||
|
updated_at: Set(chrono::Utc::now()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NginxConfigProvider for ProxyHostInfo {
|
||||||
|
fn to_nginx_config(&self, indent: Option<usize>) -> Result<String, ServiceError> {
|
||||||
|
let indent = indent.unwrap_or(0);
|
||||||
|
|
||||||
|
let mut body: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
// default location or fallback
|
||||||
|
let default_pass = if let Some(up) = &self.upstream {
|
||||||
|
format!("proxy_pass http://{};", up.name)
|
||||||
|
} else if let Some(host) = &self.forward_host {
|
||||||
|
if let Some(port) = self.forward_port {
|
||||||
|
format!("proxy_pass http://{}:{};", host, port)
|
||||||
|
} else {
|
||||||
|
format!("proxy_pass http://{};", host)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
// get locations's index sorted by order to prevent mutable borrow issues
|
||||||
|
let mut index_list: Vec<usize> = (0..self.locations.len()).collect();
|
||||||
|
index_list.sort_by(|&a, &b| {
|
||||||
|
let order_a = self.locations[a].order;
|
||||||
|
let order_b = self.locations[b].order;
|
||||||
|
order_a.cmp(&order_b)
|
||||||
|
});
|
||||||
|
|
||||||
|
for &index in &index_list {
|
||||||
|
let loc = &self.locations[index];
|
||||||
|
body.push(loc.to_nginx_config(Some(indent + INDENT_SIZE))?);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is a default proxy_pass and no root location for `/`, add it
|
||||||
|
if !default_pass.is_empty() {
|
||||||
|
body.insert(
|
||||||
|
0,
|
||||||
|
format!(
|
||||||
|
"location / {{\n{}\n}}",
|
||||||
|
default_pass.indent(indent + INDENT_SIZE)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.enable_websocket {
|
||||||
|
body.push("proxy_set_header Upgrade $http_upgrade;".to_string());
|
||||||
|
body.push("proxy_set_header Connection \"upgrade\";".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let inner = if body.is_empty() {
|
||||||
|
"# server has no config".to_string()
|
||||||
|
} else {
|
||||||
|
body.into_iter()
|
||||||
|
.map(|l| l.indent(indent + INDENT_SIZE))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\n")
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(format!(
|
||||||
|
"server {{\n listen {};\n server_name {};\n{}\n}}",
|
||||||
|
self.listen_port, self.domain, inner
|
||||||
|
)
|
||||||
|
.indent(indent))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ use tracing::warn;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
errors::service_error::ServiceError,
|
||||||
services::nginx::{
|
services::nginx::{
|
||||||
builder::{INDENT_SIZE, NginxConfigProvider},
|
builder::{INDENT_SIZE, NginxConfigProvider},
|
||||||
info::upstream_target as upstream_target_info,
|
info::upstream_target as upstream_target_info,
|
||||||
@@ -51,12 +52,12 @@ pub struct UpdateUpstreamInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl NginxConfigProvider for UpstreamInfo {
|
impl NginxConfigProvider for UpstreamInfo {
|
||||||
fn to_nginx_config(&self, indent: Option<usize>) -> String {
|
fn to_nginx_config(&self, indent: Option<usize>) -> Result<String, ServiceError> {
|
||||||
let targets_config: Vec<String> = self
|
let targets_config: Vec<String> = self
|
||||||
.upstream_targets
|
.upstream_targets
|
||||||
.iter()
|
.iter()
|
||||||
.map(|target| target.to_nginx_config(Some(indent.unwrap_or(0) + INDENT_SIZE)))
|
.map(|target| target.to_nginx_config(Some(indent.unwrap_or(0) + INDENT_SIZE)))
|
||||||
.collect();
|
.collect::<Result<Vec<String>, ServiceError>>()?;
|
||||||
|
|
||||||
let mut targets_config_str = {
|
let mut targets_config_str = {
|
||||||
let config_str = match self.algorithm.as_str() {
|
let config_str = match self.algorithm.as_str() {
|
||||||
@@ -104,7 +105,10 @@ impl NginxConfigProvider for UpstreamInfo {
|
|||||||
.push_str(&PLACEHOLDER_TARGET.indent(indent.unwrap_or(0) + INDENT_SIZE));
|
.push_str(&PLACEHOLDER_TARGET.indent(indent.unwrap_or(0) + INDENT_SIZE));
|
||||||
}
|
}
|
||||||
|
|
||||||
format!("upstream {} {{\n{}\n}}", self.name, targets_config_str).indent(indent.unwrap_or(0))
|
Ok(
|
||||||
|
format!("upstream {} {{\n{}\n}}", self.name, targets_config_str)
|
||||||
|
.indent(indent.unwrap_or(0)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use uuid::Uuid;
|
|||||||
use database::generated::entities::{upstream, upstream_target};
|
use database::generated::entities::{upstream, upstream_target};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
errors::service_error::ServiceError,
|
||||||
services::nginx::{builder::NginxConfigProvider, traits::indentable::Indentable},
|
services::nginx::{builder::NginxConfigProvider, traits::indentable::Indentable},
|
||||||
set_if_some,
|
set_if_some,
|
||||||
};
|
};
|
||||||
@@ -128,8 +129,8 @@ impl From<UpstreamTargetCreateInfo> for upstream_target::ActiveModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl NginxConfigProvider for UpstreamTargetInfo {
|
impl NginxConfigProvider for UpstreamTargetInfo {
|
||||||
fn to_nginx_config(&self, indent: Option<usize>) -> String {
|
fn to_nginx_config(&self, indent: Option<usize>) -> Result<String, ServiceError> {
|
||||||
format!(
|
Ok(format!(
|
||||||
"server {}:{} weight={}{}{};",
|
"server {}:{} weight={}{}{};",
|
||||||
self.target_host,
|
self.target_host,
|
||||||
self.target_port,
|
self.target_port,
|
||||||
@@ -137,7 +138,7 @@ impl NginxConfigProvider for UpstreamTargetInfo {
|
|||||||
if self.is_backup { " backup" } else { "" },
|
if self.is_backup { " backup" } else { "" },
|
||||||
if !self.enabled { " down" } else { "" },
|
if !self.enabled { " down" } else { "" },
|
||||||
)
|
)
|
||||||
.indent(indent.unwrap_or(0))
|
.indent(indent.unwrap_or(0)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
512
apps/api/src/services/nginx/location.rs
Normal file
512
apps/api/src/services/nginx/location.rs
Normal file
@@ -0,0 +1,512 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, ColumnTrait, DatabaseConnection, DatabaseTransaction, EntityTrait,
|
||||||
|
ModelTrait, QueryFilter, QuerySelect, QueryTrait,
|
||||||
|
};
|
||||||
|
|
||||||
|
use database::generated::entities::{location, proxy_host, upstream};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::service_error::ServiceError,
|
||||||
|
helpers::database::PaginationFilter,
|
||||||
|
services::nginx::info::location::{CreateLocationInfo, LocationInfo, UpdateLocationInfo},
|
||||||
|
with_conn,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait LocationService: Send + Sync {
|
||||||
|
async fn create_location(
|
||||||
|
&self,
|
||||||
|
create_info: CreateLocationInfo,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<LocationInfo, ServiceError>;
|
||||||
|
#[allow(dead_code)]
|
||||||
|
async fn get_locations(
|
||||||
|
&self,
|
||||||
|
pagination: Option<PaginationFilter>,
|
||||||
|
options: Option<GetLocationOptions>,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<Vec<LocationInfo>, ServiceError>;
|
||||||
|
async fn get_location(
|
||||||
|
&self,
|
||||||
|
location_id: uuid::Uuid,
|
||||||
|
options: Option<GetLocationOptions>,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<LocationInfo, ServiceError>;
|
||||||
|
async fn update_location(
|
||||||
|
&self,
|
||||||
|
location_id: uuid::Uuid,
|
||||||
|
update: UpdateLocationInfo,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<LocationInfo, ServiceError>;
|
||||||
|
async fn delete_location(
|
||||||
|
&self,
|
||||||
|
location_id: uuid::Uuid,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<(), ServiceError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LocationServiceImpl {
|
||||||
|
connection: Arc<DatabaseConnection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct LocationTotalCountOptions {}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct GetLocationOptions {
|
||||||
|
pub include_upstream: bool,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub filter_by_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LocationServiceImpl {
|
||||||
|
pub fn new(connection: Arc<DatabaseConnection>) -> Self {
|
||||||
|
Self { connection }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl LocationService for LocationServiceImpl {
|
||||||
|
async fn create_location(
|
||||||
|
&self,
|
||||||
|
create_info: CreateLocationInfo,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<LocationInfo, ServiceError> {
|
||||||
|
let model: location::ActiveModel = create_info.into();
|
||||||
|
|
||||||
|
let r = with_conn!(&*self.connection, tx, conn, { model.insert(*conn).await? });
|
||||||
|
Ok(r.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_locations(
|
||||||
|
&self,
|
||||||
|
pagination: Option<PaginationFilter>,
|
||||||
|
options: Option<GetLocationOptions>,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<Vec<LocationInfo>, ServiceError> {
|
||||||
|
let r = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
let mut find_query = location::Entity::find();
|
||||||
|
if let Some(pagination) = pagination {
|
||||||
|
let (offset, limit) = pagination.get_offset_limit();
|
||||||
|
find_query = find_query.offset(offset).limit(limit);
|
||||||
|
}
|
||||||
|
let find_query = find_query
|
||||||
|
.apply_if(
|
||||||
|
options
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|v| v.filter_by_enabled)
|
||||||
|
.then_some(true),
|
||||||
|
|q, _v| q.filter(location::Column::Enabled.eq(true)),
|
||||||
|
)
|
||||||
|
.find_also_related(proxy_host::Entity);
|
||||||
|
|
||||||
|
let r: Vec<LocationInfo> = if options.as_ref().is_some_and(|v| v.include_upstream) {
|
||||||
|
find_query
|
||||||
|
.find_also_related(upstream::Entity)
|
||||||
|
.all(*conn)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| v.into())
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
find_query
|
||||||
|
.all(*conn)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| m.into())
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
r
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_location(
|
||||||
|
&self,
|
||||||
|
location_id: uuid::Uuid,
|
||||||
|
options: Option<GetLocationOptions>,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<LocationInfo, ServiceError> {
|
||||||
|
let r = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
let find_query =
|
||||||
|
location::Entity::find_by_id(location_id).find_also_related(proxy_host::Entity);
|
||||||
|
|
||||||
|
let r: Option<LocationInfo> = if options.as_ref().is_some_and(|v| v.include_upstream) {
|
||||||
|
find_query
|
||||||
|
.find_also_related(upstream::Entity)
|
||||||
|
.one(*conn)
|
||||||
|
.await?
|
||||||
|
.map(|v| v.into())
|
||||||
|
} else {
|
||||||
|
find_query.one(*conn).await?.map(|m| m.into())
|
||||||
|
};
|
||||||
|
r
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(r.ok_or(ServiceError::NotFound(format!(
|
||||||
|
"Location with id {} not found",
|
||||||
|
location_id
|
||||||
|
)))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_location(
|
||||||
|
&self,
|
||||||
|
location_id: uuid::Uuid,
|
||||||
|
update: UpdateLocationInfo,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<LocationInfo, ServiceError> {
|
||||||
|
let current_model = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
location::Entity::find_by_id(location_id)
|
||||||
|
.one(*conn)
|
||||||
|
.await?
|
||||||
|
.ok_or(ServiceError::NotFound(format!(
|
||||||
|
"Location with id {} not found",
|
||||||
|
location_id
|
||||||
|
)))?
|
||||||
|
});
|
||||||
|
let active_model = update.apply_to_model(current_model);
|
||||||
|
|
||||||
|
let r = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
active_model.update(*conn).await?
|
||||||
|
});
|
||||||
|
Ok(r.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_location(
|
||||||
|
&self,
|
||||||
|
location_id: uuid::Uuid,
|
||||||
|
tx: Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<(), ServiceError> {
|
||||||
|
let model = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
location::Entity::find_by_id(location_id)
|
||||||
|
.one(*conn)
|
||||||
|
.await?
|
||||||
|
.ok_or(ServiceError::NotFound(format!(
|
||||||
|
"Location with id {} not found",
|
||||||
|
location_id
|
||||||
|
)))?
|
||||||
|
});
|
||||||
|
with_conn!(&*self.connection, tx, conn, {
|
||||||
|
model.delete(*conn).await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use sea_orm::MockExecResult;
|
||||||
|
use sea_orm::{DatabaseBackend, MockDatabase};
|
||||||
|
|
||||||
|
use database::generated::entities::{location, proxy_host};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn create_location_returns_info() {
|
||||||
|
let host_id = uuid::Uuid::new_v4();
|
||||||
|
let created = location::Model {
|
||||||
|
id: uuid::Uuid::new_v4(),
|
||||||
|
host_id,
|
||||||
|
path: "/test".to_string(),
|
||||||
|
match_type: "prefix".to_string(),
|
||||||
|
order: 0,
|
||||||
|
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![created.clone()]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = LocationServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
|
let create_info = CreateLocationInfo {
|
||||||
|
host_id,
|
||||||
|
path: "/test".to_string(),
|
||||||
|
match_type: "prefix".to_string(),
|
||||||
|
order: 0,
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = svc.create_location(create_info, None).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
let info = res.expect("Failed to create location");
|
||||||
|
assert_eq!(info.path, "/test");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_locations_returns_list() {
|
||||||
|
let host_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let l1 = location::Model {
|
||||||
|
id: uuid::Uuid::new_v4(),
|
||||||
|
host_id,
|
||||||
|
path: "/a".to_string(),
|
||||||
|
match_type: "prefix".to_string(),
|
||||||
|
order: 0,
|
||||||
|
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 l2 = location::Model {
|
||||||
|
id: uuid::Uuid::new_v4(),
|
||||||
|
host_id,
|
||||||
|
path: "/b".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![
|
||||||
|
(l1.clone(), None::<proxy_host::Model>),
|
||||||
|
(l2.clone(), None::<proxy_host::Model>),
|
||||||
|
]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = LocationServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
|
let res = svc.get_locations(None, None, None).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
let list = res.expect("Failed to get locations");
|
||||||
|
assert_eq!(list.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_location_with_upstream_returns_upstream() {
|
||||||
|
let host_id = uuid::Uuid::new_v4();
|
||||||
|
let up_id = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
let loc = location::Model {
|
||||||
|
id: uuid::Uuid::new_v4(),
|
||||||
|
host_id,
|
||||||
|
path: "/up".to_string(),
|
||||||
|
match_type: "prefix".to_string(),
|
||||||
|
order: 0,
|
||||||
|
upstream_id: Some(up_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,
|
||||||
|
created_at: chrono::Utc::now(),
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![vec![(loc.clone(), None::<proxy_host::Model>)]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = LocationServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
|
let res = svc
|
||||||
|
.get_location(
|
||||||
|
loc.id,
|
||||||
|
Some(GetLocationOptions {
|
||||||
|
include_upstream: false,
|
||||||
|
filter_by_enabled: false,
|
||||||
|
}),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(res.is_ok());
|
||||||
|
let info = res.expect("Failed to get location");
|
||||||
|
assert_eq!(info.id, loc.id);
|
||||||
|
assert_eq!(info.upstream_id, Some(up_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn 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 svc = LocationServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
|
let res = svc.get_location(uuid::Uuid::new_v4(), None, None).await;
|
||||||
|
|
||||||
|
assert!(matches!(res, Err(ServiceError::NotFound(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn update_location_success() {
|
||||||
|
let id = uuid::Uuid::new_v4();
|
||||||
|
let host_id = uuid::Uuid::new_v4();
|
||||||
|
let existing = location::Model {
|
||||||
|
id,
|
||||||
|
host_id,
|
||||||
|
path: "/old".to_string(),
|
||||||
|
match_type: "prefix".to_string(),
|
||||||
|
order: 0,
|
||||||
|
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 {
|
||||||
|
id,
|
||||||
|
host_id,
|
||||||
|
path: "/new".to_string(),
|
||||||
|
match_type: "prefix".to_string(),
|
||||||
|
order: 0,
|
||||||
|
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: existing.created_at,
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![vec![existing.clone()]]) // find_by_id
|
||||||
|
.append_query_results(vec![vec![updated.clone()]]) // update result
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = LocationServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
|
let update_info = UpdateLocationInfo {
|
||||||
|
path: Some("/new".to_string()),
|
||||||
|
match_type: None,
|
||||||
|
order: None,
|
||||||
|
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: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = svc.update_location(id, update_info, None).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
let got = res.expect("Failed to update location");
|
||||||
|
assert_eq!(got.path, "/new");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn update_location_not_found() {
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = LocationServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
|
let res = svc
|
||||||
|
.update_location(
|
||||||
|
uuid::Uuid::new_v4(),
|
||||||
|
UpdateLocationInfo {
|
||||||
|
path: None,
|
||||||
|
match_type: None,
|
||||||
|
order: None,
|
||||||
|
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: None,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(matches!(res, Err(ServiceError::NotFound(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn delete_location_success() {
|
||||||
|
let id = uuid::Uuid::new_v4();
|
||||||
|
let existing = location::Model {
|
||||||
|
id,
|
||||||
|
host_id: uuid::Uuid::new_v4(),
|
||||||
|
path: "/del".to_string(),
|
||||||
|
match_type: "prefix".to_string(),
|
||||||
|
order: 0,
|
||||||
|
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![existing.clone()]])
|
||||||
|
.append_exec_results(vec![MockExecResult {
|
||||||
|
rows_affected: 1,
|
||||||
|
last_insert_id: 0,
|
||||||
|
}])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = LocationServiceImpl::new(Arc::new(db));
|
||||||
|
let res = svc.delete_location(id, None).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn delete_location_not_found() {
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = LocationServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
|
let res = svc.delete_location(uuid::Uuid::new_v4(), None).await;
|
||||||
|
assert!(matches!(res, Err(ServiceError::NotFound(_))));
|
||||||
|
}
|
||||||
|
}
|
||||||
622
apps/api/src/services/nginx/proxy_host.rs
Normal file
622
apps/api/src/services/nginx/proxy_host.rs
Normal file
@@ -0,0 +1,622 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, ActiveValue::Set, ColumnTrait, DatabaseConnection, DatabaseTransaction,
|
||||||
|
EntityTrait, FromQueryResult, JoinType, ModelTrait, QueryFilter, QuerySelect, QueryTrait,
|
||||||
|
RelationTrait, TransactionTrait,
|
||||||
|
};
|
||||||
|
|
||||||
|
use database::generated::entities::{location, proxy_host};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::service_error::ServiceError,
|
||||||
|
helpers::database::PaginationFilter,
|
||||||
|
services::nginx::{
|
||||||
|
builder::NginxConfigBuilder,
|
||||||
|
info::proxy_host::{ProxyHostCreateInfo, ProxyHostInfo, UpdateProxyHostInfo},
|
||||||
|
},
|
||||||
|
with_conn,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait ProxyService: Send + Sync {
|
||||||
|
async fn create_proxy(
|
||||||
|
&self,
|
||||||
|
create_info: ProxyHostCreateInfo,
|
||||||
|
tx: &Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<ProxyHostInfo, ServiceError>;
|
||||||
|
async fn get_total_proxies(
|
||||||
|
&self,
|
||||||
|
options: Option<ProxyTotalCountOptions>,
|
||||||
|
tx: &Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<u64, ServiceError>;
|
||||||
|
async fn get_proxies(
|
||||||
|
&self,
|
||||||
|
pagination: Option<PaginationFilter>,
|
||||||
|
options: Option<ProxyHostListOptions>,
|
||||||
|
tx: &Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<Vec<ProxyHostInfo>, ServiceError>;
|
||||||
|
async fn get_proxy(
|
||||||
|
&self,
|
||||||
|
proxy_id: uuid::Uuid,
|
||||||
|
options: Option<ProxyHostGetOptions>,
|
||||||
|
tx: &Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<ProxyHostInfo, ServiceError>;
|
||||||
|
async fn update_proxy(
|
||||||
|
&self,
|
||||||
|
proxy_id: uuid::Uuid,
|
||||||
|
update: UpdateProxyHostInfo,
|
||||||
|
tx: &Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<ProxyHostInfo, ServiceError>;
|
||||||
|
async fn delete_proxy(
|
||||||
|
&self,
|
||||||
|
proxy_id: uuid::Uuid,
|
||||||
|
tx: &Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<(), ServiceError>;
|
||||||
|
async fn generate_config(
|
||||||
|
&self,
|
||||||
|
builder: &mut NginxConfigBuilder,
|
||||||
|
tx: &Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<(), ServiceError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProxyServiceImpl {
|
||||||
|
connection: Arc<DatabaseConnection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct ProxyTotalCountOptions {}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ProxyHostGetOptions {
|
||||||
|
pub include_upstream: bool,
|
||||||
|
pub filter_by_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type ProxyHostListOptions = ProxyHostGetOptions;
|
||||||
|
|
||||||
|
impl ProxyServiceImpl {
|
||||||
|
pub fn new(connection: Arc<DatabaseConnection>) -> Self {
|
||||||
|
Self { connection }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl ProxyService for ProxyServiceImpl {
|
||||||
|
async fn create_proxy(
|
||||||
|
&self,
|
||||||
|
create_info: ProxyHostCreateInfo,
|
||||||
|
tx: &Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<ProxyHostInfo, ServiceError> {
|
||||||
|
let (proxy_host, location_models): (proxy_host::ActiveModel, Vec<location::ActiveModel>) =
|
||||||
|
create_info.into();
|
||||||
|
|
||||||
|
let owned_tx = match tx {
|
||||||
|
Some(_) => None,
|
||||||
|
None => Some(self.connection.begin().await.map_err(ServiceError::from)?),
|
||||||
|
};
|
||||||
|
|
||||||
|
let tx_ref = owned_tx.as_ref().or(tx.as_deref());
|
||||||
|
|
||||||
|
let r = with_conn!(&*self.connection, tx_ref, conn, {
|
||||||
|
let inserted_proxy = proxy_host.insert(*conn).await?;
|
||||||
|
let mut inserted_location_models: Vec<location::Model> =
|
||||||
|
Vec::with_capacity(location_models.len());
|
||||||
|
|
||||||
|
for mut loc_model in location_models {
|
||||||
|
loc_model.host_id = Set(inserted_proxy.id);
|
||||||
|
let r = loc_model.insert(*conn).await?;
|
||||||
|
inserted_location_models.push(r);
|
||||||
|
}
|
||||||
|
(inserted_proxy, inserted_location_models)
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(r.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_total_proxies(
|
||||||
|
&self,
|
||||||
|
_options: Option<ProxyTotalCountOptions>,
|
||||||
|
tx: &Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<u64, ServiceError> {
|
||||||
|
#[derive(Debug, FromQueryResult)]
|
||||||
|
struct CountResult {
|
||||||
|
count: i64,
|
||||||
|
}
|
||||||
|
let count_info = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
proxy_host::Entity::find()
|
||||||
|
.select_only()
|
||||||
|
.column_as(proxy_host::Column::Id.count(), "count")
|
||||||
|
.into_model::<CountResult>()
|
||||||
|
.one(*conn)
|
||||||
|
.await?
|
||||||
|
});
|
||||||
|
Ok(count_info.map_or(0, |c| c.count) as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_proxies(
|
||||||
|
&self,
|
||||||
|
pagination: Option<PaginationFilter>,
|
||||||
|
options: Option<ProxyHostListOptions>,
|
||||||
|
tx: &Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<Vec<ProxyHostInfo>, ServiceError> {
|
||||||
|
let r = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
let mut find_query = proxy_host::Entity::find();
|
||||||
|
if let Some(pagination) = pagination {
|
||||||
|
let (offset, limit) = pagination.get_offset_limit();
|
||||||
|
find_query = find_query.offset(offset).limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
let find_query = find_query
|
||||||
|
.apply_if(
|
||||||
|
options
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|v| v.filter_by_enabled)
|
||||||
|
.then_some(true),
|
||||||
|
|q, _v| q.filter(location::Column::Enabled.eq(true)),
|
||||||
|
)
|
||||||
|
.find_with_related(location::Entity);
|
||||||
|
|
||||||
|
let r: Vec<ProxyHostInfo> = if options.as_ref().is_some_and(|v| v.include_upstream) {
|
||||||
|
find_query
|
||||||
|
.join(JoinType::LeftJoin, proxy_host::Relation::Upstream.def())
|
||||||
|
.all(*conn)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| v.into())
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
find_query
|
||||||
|
.all(*conn)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| m.into())
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
r
|
||||||
|
});
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_proxy(
|
||||||
|
&self,
|
||||||
|
proxy_id: uuid::Uuid,
|
||||||
|
options: Option<ProxyHostGetOptions>,
|
||||||
|
tx: &Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<ProxyHostInfo, ServiceError> {
|
||||||
|
let r: ProxyHostInfo = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
let find_query = proxy_host::Entity::find_by_id(proxy_id)
|
||||||
|
.apply_if(
|
||||||
|
options
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|v| v.filter_by_enabled)
|
||||||
|
.then_some(true),
|
||||||
|
|q, _v| q.filter(location::Column::Enabled.eq(true)),
|
||||||
|
)
|
||||||
|
.find_with_related(location::Entity);
|
||||||
|
let r: Option<ProxyHostInfo> = if options.as_ref().is_some_and(|v| v.include_upstream) {
|
||||||
|
find_query
|
||||||
|
.join(JoinType::LeftJoin, proxy_host::Relation::Upstream.def())
|
||||||
|
.all(*conn)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.map(|v| v.into())
|
||||||
|
} else {
|
||||||
|
find_query
|
||||||
|
.all(*conn)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| m.into())
|
||||||
|
.next()
|
||||||
|
};
|
||||||
|
r.ok_or(ServiceError::NotFound(format!(
|
||||||
|
"Proxy host with id {} not found",
|
||||||
|
proxy_id
|
||||||
|
)))?
|
||||||
|
});
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_proxy(
|
||||||
|
&self,
|
||||||
|
proxy_id: uuid::Uuid,
|
||||||
|
update: UpdateProxyHostInfo,
|
||||||
|
tx: &Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<ProxyHostInfo, ServiceError> {
|
||||||
|
let current_model = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
proxy_host::Entity::find_by_id(proxy_id)
|
||||||
|
.one(*conn)
|
||||||
|
.await?
|
||||||
|
.ok_or(ServiceError::NotFound(format!(
|
||||||
|
"Proxy host with id {} not found",
|
||||||
|
proxy_id
|
||||||
|
)))?
|
||||||
|
});
|
||||||
|
let active_model = update.apply_to_model(current_model);
|
||||||
|
|
||||||
|
let r = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
active_model.update(*conn).await?
|
||||||
|
});
|
||||||
|
Ok(r.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_proxy(
|
||||||
|
&self,
|
||||||
|
proxy_id: uuid::Uuid,
|
||||||
|
tx: &Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<(), ServiceError> {
|
||||||
|
let model = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
proxy_host::Entity::find_by_id(proxy_id)
|
||||||
|
.one(*conn)
|
||||||
|
.await?
|
||||||
|
.ok_or(ServiceError::NotFound(format!(
|
||||||
|
"Proxy host with id {} not found",
|
||||||
|
proxy_id
|
||||||
|
)))?
|
||||||
|
});
|
||||||
|
with_conn!(&*self.connection, tx, conn, {
|
||||||
|
model.delete(*conn).await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_config(
|
||||||
|
&self,
|
||||||
|
builder: &mut NginxConfigBuilder,
|
||||||
|
tx: &Option<&mut DatabaseTransaction>,
|
||||||
|
) -> Result<(), ServiceError> {
|
||||||
|
let proxies = self
|
||||||
|
.get_proxies(
|
||||||
|
None,
|
||||||
|
Some(ProxyHostListOptions {
|
||||||
|
include_upstream: true,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
tx,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
builder.add_proxy_hosts(proxies);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use sea_orm::MockExecResult;
|
||||||
|
use sea_orm::{DatabaseBackend, MockDatabase};
|
||||||
|
|
||||||
|
use database::generated::entities::{location, proxy_host};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn create_proxy_returns_info() {
|
||||||
|
let id = uuid::Uuid::new_v4();
|
||||||
|
let created = proxy_host::Model {
|
||||||
|
id,
|
||||||
|
name: Some("test_proxy".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 = location::Model {
|
||||||
|
id: uuid::Uuid::new_v4(),
|
||||||
|
host_id: id,
|
||||||
|
path: "/".to_string(),
|
||||||
|
match_type: "prefix".to_string(),
|
||||||
|
order: 0,
|
||||||
|
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![created.clone()]])
|
||||||
|
.append_query_results(vec![vec![loc.clone()]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = ProxyServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
|
let create_info = crate::services::nginx::info::proxy_host::ProxyHostCreateInfo {
|
||||||
|
name: Some("test_proxy".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,
|
||||||
|
locations: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = svc.create_proxy(create_info, &None).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
let info = res.expect("Failed to create proxy");
|
||||||
|
assert_eq!(info.domain, "example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_total_proxies_returns_count() {
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = ProxyServiceImpl::new(Arc::new(db));
|
||||||
|
let res = svc
|
||||||
|
.get_total_proxies(None, &None)
|
||||||
|
.await
|
||||||
|
.expect("Failed to get total proxies");
|
||||||
|
assert_eq!(res, 0u64);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_proxies_returns_list() {
|
||||||
|
let p1 = proxy_host::Model {
|
||||||
|
id: uuid::Uuid::new_v4(),
|
||||||
|
name: Some("p1".to_string()),
|
||||||
|
domain: "d1".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: "d2".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>),
|
||||||
|
]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = ProxyServiceImpl::new(Arc::new(db));
|
||||||
|
let res = svc.get_proxies(None, None, &None).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
let list = res.expect("Failed to get proxies");
|
||||||
|
assert_eq!(list.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_proxy_returns_info() {
|
||||||
|
let id = uuid::Uuid::new_v4();
|
||||||
|
let p = proxy_host::Model {
|
||||||
|
id,
|
||||||
|
name: Some("proxy".to_string()),
|
||||||
|
domain: "ex.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![(p.clone(), None::<location::Model>)]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = ProxyServiceImpl::new(Arc::new(db));
|
||||||
|
let res = svc.get_proxy(id, None, &None).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
let got = res.expect("Failed to get proxy");
|
||||||
|
assert_eq!(got.id, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_proxy_not_found() {
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = ProxyServiceImpl::new(Arc::new(db));
|
||||||
|
let res = svc.get_proxy(uuid::Uuid::new_v4(), None, &None).await;
|
||||||
|
assert!(matches!(res, Err(ServiceError::NotFound(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn update_proxy_success() {
|
||||||
|
let id = uuid::Uuid::new_v4();
|
||||||
|
let existing = proxy_host::Model {
|
||||||
|
id,
|
||||||
|
name: Some("old".to_string()),
|
||||||
|
domain: "d".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 {
|
||||||
|
id,
|
||||||
|
name: Some("new".to_string()),
|
||||||
|
domain: existing.domain.clone(),
|
||||||
|
scheme: existing.scheme.clone(),
|
||||||
|
listen_port: existing.listen_port,
|
||||||
|
forward_scheme: existing.forward_scheme.clone(),
|
||||||
|
forward_host: existing.forward_host.clone(),
|
||||||
|
forward_port: existing.forward_port,
|
||||||
|
preserve_host_header: existing.preserve_host_header,
|
||||||
|
enable_websocket: existing.enable_websocket,
|
||||||
|
enabled: existing.enabled,
|
||||||
|
meta: existing.meta.clone(),
|
||||||
|
default_upstream_id: existing.default_upstream_id,
|
||||||
|
created_by: existing.created_by,
|
||||||
|
created_at: existing.created_at,
|
||||||
|
updated_at: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![vec![existing.clone()]])
|
||||||
|
.append_query_results(vec![vec![updated.clone()]])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = ProxyServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
|
let update_info = crate::services::nginx::info::proxy_host::UpdateProxyHostInfo {
|
||||||
|
name: None,
|
||||||
|
domain: None,
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = svc.update_proxy(id, update_info, &None).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
let got = res.expect("Failed to update proxy");
|
||||||
|
assert_eq!(got.name.expect("Name should be present"), "new");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn update_proxy_not_found() {
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = ProxyServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
|
let res = svc
|
||||||
|
.update_proxy(
|
||||||
|
uuid::Uuid::new_v4(),
|
||||||
|
crate::services::nginx::info::proxy_host::UpdateProxyHostInfo {
|
||||||
|
name: None,
|
||||||
|
domain: None,
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
&None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(matches!(res, Err(ServiceError::NotFound(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn delete_proxy_success() {
|
||||||
|
let id = uuid::Uuid::new_v4();
|
||||||
|
let existing = proxy_host::Model {
|
||||||
|
id,
|
||||||
|
name: Some("to-delete".to_string()),
|
||||||
|
domain: "d".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![existing.clone()]])
|
||||||
|
.append_exec_results(vec![MockExecResult {
|
||||||
|
rows_affected: 1,
|
||||||
|
last_insert_id: 0,
|
||||||
|
}])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = ProxyServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
|
let res = svc.delete_proxy(id, &None).await;
|
||||||
|
assert!(res.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn delete_proxy_not_found() {
|
||||||
|
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||||
|
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||||
|
.into_connection();
|
||||||
|
|
||||||
|
let svc = ProxyServiceImpl::new(Arc::new(db));
|
||||||
|
|
||||||
|
let res = svc.delete_proxy(uuid::Uuid::new_v4(), &None).await;
|
||||||
|
assert!(matches!(res, Err(ServiceError::NotFound(_))));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@ pub trait UpstreamService: Send + Sync {
|
|||||||
async fn generate_config(
|
async fn generate_config(
|
||||||
&self,
|
&self,
|
||||||
builder: &mut NginxConfigBuilder,
|
builder: &mut NginxConfigBuilder,
|
||||||
tx: Option<&mut DatabaseTransaction>,
|
tx: &Option<&mut DatabaseTransaction>,
|
||||||
) -> Result<(), ServiceError>;
|
) -> Result<(), ServiceError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ impl UpstreamService for UpstreamServiceImpl {
|
|||||||
let count_info = with_conn!(&*self.connection, tx, conn, {
|
let count_info = with_conn!(&*self.connection, tx, conn, {
|
||||||
upstream::Entity::find()
|
upstream::Entity::find()
|
||||||
.select_only()
|
.select_only()
|
||||||
.column_as(upstream::Column::Id, "count")
|
.column_as(upstream::Column::Id.count(), "count")
|
||||||
.into_model::<CountResult>()
|
.into_model::<CountResult>()
|
||||||
.one(*conn)
|
.one(*conn)
|
||||||
.await?
|
.await?
|
||||||
@@ -199,9 +199,10 @@ impl UpstreamService for UpstreamServiceImpl {
|
|||||||
)))?;
|
)))?;
|
||||||
let targets = upstream_target::Entity::find()
|
let targets = upstream_target::Entity::find()
|
||||||
.filter(upstream_target::Column::UpstreamId.eq(upstream_id))
|
.filter(upstream_target::Column::UpstreamId.eq(upstream_id))
|
||||||
.apply_if(Some(concrete_options.filter_by_enabled), |query, _v| {
|
.apply_if(
|
||||||
query.filter(upstream_target::Column::Enabled.eq(true))
|
concrete_options.filter_by_enabled.then_some(true),
|
||||||
})
|
|query, _v| query.filter(upstream_target::Column::Enabled.eq(true)),
|
||||||
|
)
|
||||||
.all(*conn)
|
.all(*conn)
|
||||||
.await?;
|
.await?;
|
||||||
(up, targets)
|
(up, targets)
|
||||||
@@ -456,7 +457,7 @@ impl UpstreamService for UpstreamServiceImpl {
|
|||||||
async fn generate_config(
|
async fn generate_config(
|
||||||
&self,
|
&self,
|
||||||
builder: &mut NginxConfigBuilder,
|
builder: &mut NginxConfigBuilder,
|
||||||
tx: Option<&mut DatabaseTransaction>,
|
tx: &Option<&mut DatabaseTransaction>,
|
||||||
) -> Result<(), ServiceError> {
|
) -> Result<(), ServiceError> {
|
||||||
// get all upstreams and their targets
|
// get all upstreams and their targets
|
||||||
let upstreams = with_conn!(&*self.connection, tx, conn, {
|
let upstreams = with_conn!(&*self.connection, tx, conn, {
|
||||||
|
|||||||
@@ -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,213 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/CreateLocationRequestBodyByUpstreamId"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/CreateLocationRequestBodyByProxyPass"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"CreateLocationRequestBodyByProxyPass": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"path",
|
||||||
|
"match_type",
|
||||||
|
"order",
|
||||||
|
"proxy_pass_protocol",
|
||||||
|
"proxy_pass_host",
|
||||||
|
"proxy_pass_port"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"allowed_methods": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"custom_config": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"match_type": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"preserve_host_header": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"proxy_pass_host": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"proxy_pass_port": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"proxy_pass_protocol": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CreateLocationRequestBodyByUpstreamId": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"path",
|
||||||
|
"match_type",
|
||||||
|
"order",
|
||||||
|
"upstream_id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"allowed_methods": {
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"custom_config": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"match_type": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"preserve_host_header": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"upstream_id": {
|
||||||
|
"type": "string",
|
||||||
|
"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 +1209,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 +1312,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": [
|
||||||
|
|||||||
@@ -1,6 +1,48 @@
|
|||||||
export namespace Schemas {
|
export namespace Schemas {
|
||||||
// <Schemas>
|
// <Schemas>
|
||||||
export type AdminInitRequest = { password: string; setup_secret: string; username: string };
|
export type AdminInitRequest = { password: string; setup_secret: string; username: string };
|
||||||
|
export type CreateLocationReq = {
|
||||||
|
match_type: string;
|
||||||
|
order: number;
|
||||||
|
path: string;
|
||||||
|
upstream_id?: (string | null) | undefined;
|
||||||
|
};
|
||||||
|
export type CreateLocationRequestBodyByUpstreamId = {
|
||||||
|
allowed_methods?: (Array<string> | null) | undefined;
|
||||||
|
custom_config?: (string | null) | undefined;
|
||||||
|
match_type: string;
|
||||||
|
order: number;
|
||||||
|
path: string;
|
||||||
|
preserve_host_header?: (boolean | null) | undefined;
|
||||||
|
upstream_id: string;
|
||||||
|
};
|
||||||
|
export type CreateLocationRequestBodyByProxyPass = {
|
||||||
|
allowed_methods?: (Array<string> | null) | undefined;
|
||||||
|
custom_config?: (string | null) | undefined;
|
||||||
|
match_type: string;
|
||||||
|
order: number;
|
||||||
|
path: string;
|
||||||
|
preserve_host_header?: (boolean | null) | undefined;
|
||||||
|
proxy_pass_host: string;
|
||||||
|
proxy_pass_port: number;
|
||||||
|
proxy_pass_protocol: string;
|
||||||
|
};
|
||||||
|
export type CreateLocationRequestBody = CreateLocationRequestBodyByUpstreamId | CreateLocationRequestBodyByProxyPass;
|
||||||
|
export type CreateProxyRequestBody = {
|
||||||
|
default_upstream_id?: (string | null) | undefined;
|
||||||
|
domain: string;
|
||||||
|
enable_websocket: boolean;
|
||||||
|
enabled: boolean;
|
||||||
|
forward_host?: (string | null) | undefined;
|
||||||
|
forward_port?: (number | null) | undefined;
|
||||||
|
forward_scheme: string;
|
||||||
|
listen_port: number;
|
||||||
|
locations: Array<CreateLocationReq>;
|
||||||
|
meta?: unknown | undefined;
|
||||||
|
name?: (string | null) | undefined;
|
||||||
|
preserve_host_header: boolean;
|
||||||
|
scheme: string;
|
||||||
|
};
|
||||||
export type UpstreamBasicInfo = {
|
export type UpstreamBasicInfo = {
|
||||||
created_at: string;
|
created_at: string;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -44,8 +86,45 @@ export namespace Schemas {
|
|||||||
up_since: string;
|
up_since: string;
|
||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
|
export type LocationInfoResponse = {
|
||||||
|
created_at: string;
|
||||||
|
enabled: boolean;
|
||||||
|
host_id: string;
|
||||||
|
id: string;
|
||||||
|
match_type: string;
|
||||||
|
order: number;
|
||||||
|
path: string;
|
||||||
|
updated_at: string;
|
||||||
|
upstream_id?: (string | null) | undefined;
|
||||||
|
};
|
||||||
export type LoginRequest = { password: string; username: string };
|
export type LoginRequest = { password: string; username: string };
|
||||||
export type PaginationInfo = { current_page: number; per_page: number; total_items: number; total_pages: number };
|
export type PaginationInfo = { current_page: number; per_page: number; total_items: number; total_pages: number };
|
||||||
|
export type ProxyHostUpstreamBasic = { id: string; name: string; protocol: string };
|
||||||
|
export type ProxyHostInfoResponse = {
|
||||||
|
created_at: string;
|
||||||
|
domain: string;
|
||||||
|
enable_websocket: boolean;
|
||||||
|
enabled: boolean;
|
||||||
|
forward_host?: (string | null) | undefined;
|
||||||
|
forward_port?: (number | null) | undefined;
|
||||||
|
forward_scheme: string;
|
||||||
|
id: string;
|
||||||
|
listen_port: number;
|
||||||
|
locations: Array<LocationInfoResponse>;
|
||||||
|
name?: (string | null) | undefined;
|
||||||
|
preserve_host_header: boolean;
|
||||||
|
scheme: string;
|
||||||
|
updated_at: string;
|
||||||
|
upstream?: (null | ProxyHostUpstreamBasic) | undefined;
|
||||||
|
};
|
||||||
|
export type ProxyListResponse = { items: Array<ProxyHostInfoResponse>; pagination: PaginationInfo };
|
||||||
|
export type UpdateLocationRequestBody = Partial<{
|
||||||
|
match_type: string | null;
|
||||||
|
order: number | null;
|
||||||
|
path: string | null;
|
||||||
|
upstream_id: string | null;
|
||||||
|
}>;
|
||||||
|
export type UpdateProxyRequestBody = Partial<{ domain: string | null; name: string | null }>;
|
||||||
export type UpstreamTargetBasicInfo = {
|
export type UpstreamTargetBasicInfo = {
|
||||||
created_at: string;
|
created_at: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -149,6 +228,91 @@ export namespace Endpoints {
|
|||||||
parameters: never;
|
parameters: never;
|
||||||
responses: { 200: Schemas.HealthInfo; 404: unknown };
|
responses: { 200: Schemas.HealthInfo; 404: unknown };
|
||||||
};
|
};
|
||||||
|
export type get_Get_location = {
|
||||||
|
method: "GET";
|
||||||
|
path: "/api/nginx/locations/{location_id}";
|
||||||
|
requestFormat: "json";
|
||||||
|
parameters: {
|
||||||
|
path: { location_id: string };
|
||||||
|
};
|
||||||
|
responses: { 200: Schemas.LocationInfoResponse; 404: unknown; 500: unknown };
|
||||||
|
};
|
||||||
|
export type delete_Remove_location = {
|
||||||
|
method: "DELETE";
|
||||||
|
path: "/api/nginx/locations/{location_id}";
|
||||||
|
requestFormat: "json";
|
||||||
|
parameters: {
|
||||||
|
path: { location_id: string };
|
||||||
|
};
|
||||||
|
responses: { 200: unknown; 401: unknown; 404: unknown; 500: unknown };
|
||||||
|
};
|
||||||
|
export type patch_Update_location = {
|
||||||
|
method: "PATCH";
|
||||||
|
path: "/api/nginx/locations/{location_id}";
|
||||||
|
requestFormat: "json";
|
||||||
|
parameters: {
|
||||||
|
path: { location_id: string };
|
||||||
|
|
||||||
|
body: Schemas.UpdateLocationRequestBody;
|
||||||
|
};
|
||||||
|
responses: { 200: Schemas.LocationInfoResponse; 401: unknown; 404: unknown; 422: unknown; 500: unknown };
|
||||||
|
};
|
||||||
|
export type get_Get_proxy_list = {
|
||||||
|
method: "GET";
|
||||||
|
path: "/api/nginx/proxy_hosts";
|
||||||
|
requestFormat: "json";
|
||||||
|
parameters: never;
|
||||||
|
responses: { 200: Schemas.ProxyListResponse; 500: unknown };
|
||||||
|
};
|
||||||
|
export type post_Create_proxy = {
|
||||||
|
method: "POST";
|
||||||
|
path: "/api/nginx/proxy_hosts";
|
||||||
|
requestFormat: "json";
|
||||||
|
parameters: {
|
||||||
|
body: Schemas.CreateProxyRequestBody;
|
||||||
|
};
|
||||||
|
responses: { 200: Schemas.ProxyHostInfoResponse; 401: unknown; 422: unknown; 500: unknown };
|
||||||
|
};
|
||||||
|
export type get_Get_proxy = {
|
||||||
|
method: "GET";
|
||||||
|
path: "/api/nginx/proxy_hosts/{proxy_id}";
|
||||||
|
requestFormat: "json";
|
||||||
|
parameters: {
|
||||||
|
path: { proxy_id: string };
|
||||||
|
};
|
||||||
|
responses: { 200: Schemas.ProxyHostInfoResponse; 404: unknown; 500: unknown };
|
||||||
|
};
|
||||||
|
export type delete_Remove_proxy = {
|
||||||
|
method: "DELETE";
|
||||||
|
path: "/api/nginx/proxy_hosts/{proxy_id}";
|
||||||
|
requestFormat: "json";
|
||||||
|
parameters: {
|
||||||
|
path: { proxy_id: string };
|
||||||
|
};
|
||||||
|
responses: { 200: unknown; 401: unknown; 404: unknown; 500: unknown };
|
||||||
|
};
|
||||||
|
export type patch_Update_proxy = {
|
||||||
|
method: "PATCH";
|
||||||
|
path: "/api/nginx/proxy_hosts/{proxy_id}";
|
||||||
|
requestFormat: "json";
|
||||||
|
parameters: {
|
||||||
|
path: { proxy_id: string };
|
||||||
|
|
||||||
|
body: Schemas.UpdateProxyRequestBody;
|
||||||
|
};
|
||||||
|
responses: { 200: Schemas.ProxyHostInfoResponse; 401: unknown; 422: unknown; 500: unknown };
|
||||||
|
};
|
||||||
|
export type post_Create_location = {
|
||||||
|
method: "POST";
|
||||||
|
path: "/api/nginx/proxy_hosts/{proxy_id}/locations";
|
||||||
|
requestFormat: "json";
|
||||||
|
parameters: {
|
||||||
|
path: { proxy_id: string };
|
||||||
|
|
||||||
|
body: Schemas.CreateLocationRequestBody;
|
||||||
|
};
|
||||||
|
responses: { 200: Schemas.LocationInfoResponse; 401: unknown; 422: unknown; 500: unknown };
|
||||||
|
};
|
||||||
export type get_Get_upstream_target = {
|
export type get_Get_upstream_target = {
|
||||||
method: "GET";
|
method: "GET";
|
||||||
path: "/api/nginx/upstream_targets/{upstream_target_id}";
|
path: "/api/nginx/upstream_targets/{upstream_target_id}";
|
||||||
@@ -254,21 +418,30 @@ export type EndpointByMethod = {
|
|||||||
post: {
|
post: {
|
||||||
"/api/auth/init_admin": Endpoints.post_Init_admin;
|
"/api/auth/init_admin": Endpoints.post_Init_admin;
|
||||||
"/api/auth/login": Endpoints.post_Login;
|
"/api/auth/login": Endpoints.post_Login;
|
||||||
|
"/api/nginx/proxy_hosts": Endpoints.post_Create_proxy;
|
||||||
|
"/api/nginx/proxy_hosts/{proxy_id}/locations": Endpoints.post_Create_location;
|
||||||
"/api/nginx/upstreams": Endpoints.post_Create_upstream;
|
"/api/nginx/upstreams": Endpoints.post_Create_upstream;
|
||||||
"/api/nginx/upstreams/{upstream_id}/targets": Endpoints.post_Add_upstream_target;
|
"/api/nginx/upstreams/{upstream_id}/targets": Endpoints.post_Add_upstream_target;
|
||||||
};
|
};
|
||||||
get: {
|
get: {
|
||||||
"/api/health/info": Endpoints.get_Get_health_info;
|
"/api/health/info": Endpoints.get_Get_health_info;
|
||||||
|
"/api/nginx/locations/{location_id}": Endpoints.get_Get_location;
|
||||||
|
"/api/nginx/proxy_hosts": Endpoints.get_Get_proxy_list;
|
||||||
|
"/api/nginx/proxy_hosts/{proxy_id}": Endpoints.get_Get_proxy;
|
||||||
"/api/nginx/upstream_targets/{upstream_target_id}": Endpoints.get_Get_upstream_target;
|
"/api/nginx/upstream_targets/{upstream_target_id}": Endpoints.get_Get_upstream_target;
|
||||||
"/api/nginx/upstreams": Endpoints.get_Get_upstream_list;
|
"/api/nginx/upstreams": Endpoints.get_Get_upstream_list;
|
||||||
"/api/nginx/upstreams/{upstream_id}": Endpoints.get_Get_upstream;
|
"/api/nginx/upstreams/{upstream_id}": Endpoints.get_Get_upstream;
|
||||||
"/api/user/me": Endpoints.get_Get_user_info;
|
"/api/user/me": Endpoints.get_Get_user_info;
|
||||||
};
|
};
|
||||||
delete: {
|
delete: {
|
||||||
|
"/api/nginx/locations/{location_id}": Endpoints.delete_Remove_location;
|
||||||
|
"/api/nginx/proxy_hosts/{proxy_id}": Endpoints.delete_Remove_proxy;
|
||||||
"/api/nginx/upstream_targets/{upstream_target_id}": Endpoints.delete_Remove_upstream_target;
|
"/api/nginx/upstream_targets/{upstream_target_id}": Endpoints.delete_Remove_upstream_target;
|
||||||
"/api/nginx/upstreams/{upstream_id}": Endpoints.delete_Remove_upstream;
|
"/api/nginx/upstreams/{upstream_id}": Endpoints.delete_Remove_upstream;
|
||||||
};
|
};
|
||||||
patch: {
|
patch: {
|
||||||
|
"/api/nginx/locations/{location_id}": Endpoints.patch_Update_location;
|
||||||
|
"/api/nginx/proxy_hosts/{proxy_id}": Endpoints.patch_Update_proxy;
|
||||||
"/api/nginx/upstream_targets/{upstream_target_id}": Endpoints.patch_Update_upstream_target;
|
"/api/nginx/upstream_targets/{upstream_target_id}": Endpoints.patch_Update_upstream_target;
|
||||||
"/api/nginx/upstreams/{upstream_id}": Endpoints.patch_Update_upstream;
|
"/api/nginx/upstreams/{upstream_id}": Endpoints.patch_Update_upstream;
|
||||||
};
|
};
|
||||||
|
|||||||
46
public/database/src/generated/entities/location.rs
Normal file
46
public/database/src/generated/entities/location.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.18
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[sea_orm::model]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "location")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub id: Uuid,
|
||||||
|
pub host_id: Uuid,
|
||||||
|
pub path: String,
|
||||||
|
pub match_type: String,
|
||||||
|
pub order: i64,
|
||||||
|
pub upstream_id: Option<Uuid>,
|
||||||
|
pub proxy_pass_protocol: Option<String>,
|
||||||
|
pub proxy_pass_host: Option<String>,
|
||||||
|
pub proxy_pass_port: Option<i64>,
|
||||||
|
pub preserve_host_header: Option<bool>,
|
||||||
|
#[sea_orm(column_type = "JsonBinary", nullable)]
|
||||||
|
pub allowed_methods: Option<Json>,
|
||||||
|
#[sea_orm(column_type = "Text", nullable)]
|
||||||
|
pub custom_config: Option<String>,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub created_at: DateTimeUtc,
|
||||||
|
pub updated_at: DateTimeUtc,
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to,
|
||||||
|
from = "host_id",
|
||||||
|
to = "id",
|
||||||
|
on_update = "Cascade",
|
||||||
|
on_delete = "Cascade"
|
||||||
|
)]
|
||||||
|
pub proxy_host: HasOne<super::proxy_host::Entity>,
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to,
|
||||||
|
from = "upstream_id",
|
||||||
|
to = "id",
|
||||||
|
on_update = "Cascade",
|
||||||
|
on_delete = "SetNull"
|
||||||
|
)]
|
||||||
|
pub upstream: HasOne<super::upstream::Entity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod location;
|
||||||
|
pub mod proxy_host;
|
||||||
pub mod upstream;
|
pub mod upstream;
|
||||||
pub mod upstream_target;
|
pub mod upstream_target;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.18
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.18
|
||||||
|
|
||||||
pub use super::config::Entity as Config;
|
pub use super::config::Entity as Config;
|
||||||
|
pub use super::location::Entity as Location;
|
||||||
|
pub use super::proxy_host::Entity as ProxyHost;
|
||||||
pub use super::upstream::Entity as Upstream;
|
pub use super::upstream::Entity as Upstream;
|
||||||
pub use super::upstream_target::Entity as UpstreamTarget;
|
pub use super::upstream_target::Entity as UpstreamTarget;
|
||||||
pub use super::user::Entity as User;
|
pub use super::user::Entity as User;
|
||||||
|
|||||||
48
public/database/src/generated/entities/proxy_host.rs
Normal file
48
public/database/src/generated/entities/proxy_host.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.18
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[sea_orm::model]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "proxy_host")]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub id: 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,
|
||||||
|
#[sea_orm(column_type = "JsonBinary", nullable)]
|
||||||
|
pub meta: Option<Json>,
|
||||||
|
pub default_upstream_id: Option<Uuid>,
|
||||||
|
pub created_by: Option<Uuid>,
|
||||||
|
pub created_at: DateTimeUtc,
|
||||||
|
pub updated_at: DateTimeUtc,
|
||||||
|
#[sea_orm(has_many)]
|
||||||
|
pub locations: HasMany<super::location::Entity>,
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to,
|
||||||
|
from = "default_upstream_id",
|
||||||
|
to = "id",
|
||||||
|
on_update = "Cascade",
|
||||||
|
on_delete = "SetNull"
|
||||||
|
)]
|
||||||
|
pub upstream: HasOne<super::upstream::Entity>,
|
||||||
|
#[sea_orm(
|
||||||
|
belongs_to,
|
||||||
|
from = "created_by",
|
||||||
|
to = "id",
|
||||||
|
on_update = "Cascade",
|
||||||
|
on_delete = "SetNull"
|
||||||
|
)]
|
||||||
|
pub user: HasOne<super::user::Entity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
@@ -17,6 +17,10 @@ pub struct Model {
|
|||||||
pub created_at: DateTimeUtc,
|
pub created_at: DateTimeUtc,
|
||||||
pub updated_at: DateTimeUtc,
|
pub updated_at: DateTimeUtc,
|
||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
|
pub locations: HasMany<super::location::Entity>,
|
||||||
|
#[sea_orm(has_many)]
|
||||||
|
pub proxy_hosts: HasMany<super::proxy_host::Entity>,
|
||||||
|
#[sea_orm(has_many)]
|
||||||
pub upstream_targets: HasMany<super::upstream_target::Entity>,
|
pub upstream_targets: HasMany<super::upstream_target::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ pub struct Model {
|
|||||||
pub last_login_at: Option<DateTimeUtc>,
|
pub last_login_at: Option<DateTimeUtc>,
|
||||||
pub deleted_at: Option<DateTimeUtc>,
|
pub deleted_at: Option<DateTimeUtc>,
|
||||||
#[sea_orm(has_many)]
|
#[sea_orm(has_many)]
|
||||||
|
pub proxy_hosts: HasMany<super::proxy_host::Entity>,
|
||||||
|
#[sea_orm(has_many)]
|
||||||
pub user_identities: HasMany<super::user_identity::Entity>,
|
pub user_identities: HasMany<super::user_identity::Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20251011_000003_create_user_identity_table::Migration),
|
Box::new(m20251011_000003_create_user_identity_table::Migration),
|
||||||
Box::new(m20251223_000004_create_upstream_table::Migration),
|
Box::new(m20251223_000004_create_upstream_table::Migration),
|
||||||
Box::new(m20251223_000005_create_upstream_target_table::Migration),
|
Box::new(m20251223_000005_create_upstream_target_table::Migration),
|
||||||
|
Box::new(m20260102_000006_create_proxy_table::Migration),
|
||||||
|
Box::new(m20260102_000007_create_location_table::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ pub mod m20251011_000002_create_user_table;
|
|||||||
pub mod m20251011_000003_create_user_identity_table;
|
pub mod m20251011_000003_create_user_identity_table;
|
||||||
pub mod m20251223_000004_create_upstream_table;
|
pub mod m20251223_000004_create_upstream_table;
|
||||||
pub mod m20251223_000005_create_upstream_target_table;
|
pub mod m20251223_000005_create_upstream_target_table;
|
||||||
|
pub mod m20260102_000006_create_proxy_table;
|
||||||
|
pub mod m20260102_000007_create_location_table;
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
use sea_orm_migration::{prelude::*, schema::*};
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[forbid(dead_code)]
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
pub enum ProxyHost {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
Name,
|
||||||
|
Domain,
|
||||||
|
Scheme,
|
||||||
|
ListenPort,
|
||||||
|
ForwardScheme,
|
||||||
|
ForwardHost,
|
||||||
|
ForwardPort,
|
||||||
|
PreserveHostHeader,
|
||||||
|
EnableWebsocket,
|
||||||
|
Enabled,
|
||||||
|
Meta,
|
||||||
|
DefaultUpstreamId,
|
||||||
|
CreatedBy,
|
||||||
|
CreatedAt,
|
||||||
|
UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(ProxyHost::Table)
|
||||||
|
.if_not_exists()
|
||||||
|
.col(pk_uuid(ProxyHost::Id))
|
||||||
|
.col(ColumnDef::new(ProxyHost::Name).string().null())
|
||||||
|
.col(ColumnDef::new(ProxyHost::Domain).string().not_null())
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(ProxyHost::Scheme)
|
||||||
|
.string()
|
||||||
|
.default("http")
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(ProxyHost::ListenPort)
|
||||||
|
.integer()
|
||||||
|
.default(80)
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(ProxyHost::ForwardScheme)
|
||||||
|
.string()
|
||||||
|
.default("http")
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(ProxyHost::ForwardHost).string().null())
|
||||||
|
.col(ColumnDef::new(ProxyHost::ForwardPort).integer().null())
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(ProxyHost::PreserveHostHeader)
|
||||||
|
.boolean()
|
||||||
|
.default(false)
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(ProxyHost::EnableWebsocket)
|
||||||
|
.boolean()
|
||||||
|
.default(false)
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(ProxyHost::Enabled)
|
||||||
|
.boolean()
|
||||||
|
.default(true)
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(ProxyHost::Meta).json_binary().null())
|
||||||
|
.col(ColumnDef::new(ProxyHost::DefaultUpstreamId).uuid().null())
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk-proxy-host-default-upstream-id")
|
||||||
|
.from(ProxyHost::Table, ProxyHost::DefaultUpstreamId)
|
||||||
|
.to(
|
||||||
|
super::m20251223_000004_create_upstream_table::Upstream::Table,
|
||||||
|
super::m20251223_000004_create_upstream_table::Upstream::Id,
|
||||||
|
)
|
||||||
|
.on_delete(ForeignKeyAction::SetNull)
|
||||||
|
.on_update(ForeignKeyAction::Cascade),
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(ProxyHost::CreatedBy).uuid().null())
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk-proxy-host-created-by")
|
||||||
|
.from(ProxyHost::Table, ProxyHost::CreatedBy)
|
||||||
|
.to(
|
||||||
|
super::m20251011_000002_create_user_table::User::Table,
|
||||||
|
super::m20251011_000002_create_user_table::User::Id,
|
||||||
|
)
|
||||||
|
.on_delete(ForeignKeyAction::SetNull)
|
||||||
|
.on_update(ForeignKeyAction::Cascade),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(ProxyHost::CreatedAt)
|
||||||
|
.timestamp()
|
||||||
|
.default(SimpleExpr::Keyword(Keyword::CurrentTimestamp))
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(ProxyHost::UpdatedAt)
|
||||||
|
.timestamp()
|
||||||
|
.default(SimpleExpr::Keyword(Keyword::CurrentTimestamp))
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(ProxyHost::Table).to_owned())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
use sea_orm_migration::{prelude::*, schema::*};
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[forbid(dead_code)]
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
pub enum Location {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
HostId,
|
||||||
|
Path,
|
||||||
|
MatchType,
|
||||||
|
Order,
|
||||||
|
UpstreamId,
|
||||||
|
ProxyPassProtocol,
|
||||||
|
ProxyPassHost,
|
||||||
|
ProxyPassPort,
|
||||||
|
PreserveHostHeader,
|
||||||
|
AllowedMethods,
|
||||||
|
CustomConfig,
|
||||||
|
Enabled,
|
||||||
|
CreatedAt,
|
||||||
|
UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Location::Table)
|
||||||
|
.if_not_exists()
|
||||||
|
.col(pk_uuid(Location::Id))
|
||||||
|
.col(ColumnDef::new(Location::HostId).uuid().not_null())
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk-location-host-id")
|
||||||
|
.from(Location::Table, Location::HostId)
|
||||||
|
.to(
|
||||||
|
super::m20260102_000006_create_proxy_table::ProxyHost::Table,
|
||||||
|
super::m20260102_000006_create_proxy_table::ProxyHost::Id,
|
||||||
|
)
|
||||||
|
.on_delete(ForeignKeyAction::Cascade)
|
||||||
|
.on_update(ForeignKeyAction::Cascade),
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(Location::Path).string().not_null())
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Location::MatchType)
|
||||||
|
.string()
|
||||||
|
.default("prefix")
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Location::Order)
|
||||||
|
.integer()
|
||||||
|
.default(0)
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(Location::UpstreamId).uuid().null())
|
||||||
|
.foreign_key(
|
||||||
|
ForeignKey::create()
|
||||||
|
.name("fk-location-upstream-id")
|
||||||
|
.from(Location::Table, Location::UpstreamId)
|
||||||
|
.to(
|
||||||
|
super::m20251223_000004_create_upstream_table::Upstream::Table,
|
||||||
|
super::m20251223_000004_create_upstream_table::Upstream::Id,
|
||||||
|
)
|
||||||
|
.on_delete(ForeignKeyAction::SetNull)
|
||||||
|
.on_update(ForeignKeyAction::Cascade),
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(Location::ProxyPassProtocol).string().null())
|
||||||
|
.col(ColumnDef::new(Location::ProxyPassHost).string().null())
|
||||||
|
.col(ColumnDef::new(Location::ProxyPassPort).integer().null())
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Location::PreserveHostHeader)
|
||||||
|
.boolean()
|
||||||
|
.null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Location::AllowedMethods)
|
||||||
|
.json_binary()
|
||||||
|
.null(),
|
||||||
|
)
|
||||||
|
.col(ColumnDef::new(Location::CustomConfig).text().null())
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Location::Enabled)
|
||||||
|
.boolean()
|
||||||
|
.default(true)
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Location::CreatedAt)
|
||||||
|
.timestamp()
|
||||||
|
.default(SimpleExpr::Keyword(Keyword::CurrentTimestamp))
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.col(
|
||||||
|
ColumnDef::new(Location::UpdatedAt)
|
||||||
|
.timestamp()
|
||||||
|
.default(SimpleExpr::Keyword(Keyword::CurrentTimestamp))
|
||||||
|
.not_null(),
|
||||||
|
)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(Location::Table).to_owned())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user