1use crate::config::{Backend, HealthCheckConfig, LoadBalanceStrategy, Route, RouteMatch, Upstream};
4use crate::ProxyState;
5use axum::{
6 extract::{Path, State},
7 http::StatusCode,
8 response::IntoResponse,
9 routing::{delete, get, post, put},
10 Json, Router,
11};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::sync::Arc;
15use uuid::Uuid;
16
17#[derive(Serialize)]
19pub struct ApiResponse<T> {
20 pub success: bool,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub data: Option<T>,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub error: Option<String>,
25}
26
27impl<T: Serialize> ApiResponse<T> {
28 pub fn success(data: T) -> Self {
29 Self {
30 success: true,
31 data: Some(data),
32 error: None,
33 }
34 }
35
36 pub fn error(message: impl Into<String>) -> ApiResponse<()> {
37 ApiResponse {
38 success: false,
39 data: None,
40 error: Some(message.into()),
41 }
42 }
43}
44
45pub fn admin_router() -> Router<Arc<ProxyState>> {
47 Router::new()
48 .route("/routes", get(list_routes))
50 .route("/routes", post(create_route))
51 .route("/routes/:id", get(get_route))
52 .route("/routes/:id", put(update_route))
53 .route("/routes/:id", delete(delete_route))
54 .route("/upstreams", get(list_upstreams))
56 .route("/upstreams", post(create_upstream))
57 .route("/upstreams/:id", get(get_upstream))
58 .route("/upstreams/:id", put(update_upstream))
59 .route("/upstreams/:id", delete(delete_upstream))
60 .route("/upstreams/:id/backends", get(list_backends))
62 .route("/upstreams/:id/backends", post(add_backend))
63 .route(
64 "/upstreams/:id/backends/:backend_id",
65 delete(remove_backend),
66 )
67 .route("/health", get(health_status))
69 .route("/health/:backend_id", get(backend_health))
70 .route("/config/reload", post(reload_config))
72 .route("/stats", get(get_stats))
74}
75
76async fn list_routes(State(state): State<Arc<ProxyState>>) -> impl IntoResponse {
79 let config = state.config.read().await;
80 let routes = config.routes.clone();
81 Json(ApiResponse::success(routes))
82}
83
84async fn get_route(
85 State(state): State<Arc<ProxyState>>,
86 Path(id): Path<Uuid>,
87) -> impl IntoResponse {
88 let config = state.config.read().await;
89 match config.routes.iter().find(|r| r.id == Some(id)) {
90 Some(route) => Json(ApiResponse::success(route)).into_response(),
91 None => (
92 StatusCode::NOT_FOUND,
93 Json(ApiResponse::<()>::error("Route not found")),
94 )
95 .into_response(),
96 }
97}
98
99#[derive(Deserialize)]
100pub struct CreateRouteRequest {
101 pub name: String,
102 pub host: Option<String>,
103 pub path: Option<String>,
104 pub upstream: String,
105 pub strip_path: Option<bool>,
106 pub priority: Option<i32>,
107 pub add_headers: Option<HashMap<String, String>>,
108 pub remove_headers: Option<Vec<String>>,
109}
110
111async fn create_route(
112 State(state): State<Arc<ProxyState>>,
113 Json(req): Json<CreateRouteRequest>,
114) -> impl IntoResponse {
115 let route = Route {
116 id: Some(Uuid::new_v4()),
117 name: req.name,
118 description: None,
119 match_: RouteMatch {
120 host: req.host,
121 path: req.path,
122 path_type: Default::default(),
123 headers: HashMap::new(),
124 methods: None,
125 },
126 priority: req.priority.unwrap_or(100),
127 upstream: req.upstream,
128 strip_path: req.strip_path.unwrap_or(false),
129 add_headers: req.add_headers.unwrap_or_default(),
130 remove_headers: req.remove_headers.unwrap_or_default(),
131 rate_limit: None,
132 timeout_secs: 30,
133 retry_count: 0,
134 enabled: true,
135 };
136
137 let mut config = state.config.write().await;
139 config.routes.push(route.clone());
140
141 (StatusCode::CREATED, Json(ApiResponse::success(route)))
142}
143
144async fn update_route(
145 State(state): State<Arc<ProxyState>>,
146 Path(id): Path<Uuid>,
147 Json(req): Json<CreateRouteRequest>,
148) -> impl IntoResponse {
149 let mut config = state.config.write().await;
150
151 if let Some(route) = config.routes.iter_mut().find(|r| r.id == Some(id)) {
152 route.name = req.name;
153 route.match_.host = req.host;
154 route.match_.path = req.path;
155 route.upstream = req.upstream;
156 route.strip_path = req.strip_path.unwrap_or(false);
157 route.priority = req.priority.unwrap_or(100);
158 if let Some(add_headers) = req.add_headers {
159 route.add_headers = add_headers;
160 }
161 if let Some(remove_headers) = req.remove_headers {
162 route.remove_headers = remove_headers;
163 }
164
165 Json(ApiResponse::success(route.clone())).into_response()
167 } else {
168 (
169 StatusCode::NOT_FOUND,
170 Json(ApiResponse::<()>::error("Route not found")),
171 )
172 .into_response()
173 }
174}
175
176async fn delete_route(
177 State(state): State<Arc<ProxyState>>,
178 Path(id): Path<Uuid>,
179) -> impl IntoResponse {
180 let mut config = state.config.write().await;
181 let len_before = config.routes.len();
182 config.routes.retain(|r| r.id != Some(id));
183
184 if config.routes.len() < len_before {
185 Json(ApiResponse::success(())).into_response()
187 } else {
188 (
189 StatusCode::NOT_FOUND,
190 Json(ApiResponse::<()>::error("Route not found")),
191 )
192 .into_response()
193 }
194}
195
196async fn list_upstreams(State(state): State<Arc<ProxyState>>) -> impl IntoResponse {
199 let config = state.config.read().await;
200 let upstreams = config.upstreams.clone();
201 Json(ApiResponse::success(upstreams))
202}
203
204async fn get_upstream(
205 State(state): State<Arc<ProxyState>>,
206 Path(id): Path<Uuid>,
207) -> impl IntoResponse {
208 let config = state.config.read().await;
209 match config.upstreams.iter().find(|u| u.id == Some(id)) {
210 Some(upstream) => Json(ApiResponse::success(upstream)).into_response(),
211 None => (
212 StatusCode::NOT_FOUND,
213 Json(ApiResponse::<()>::error("Upstream not found")),
214 )
215 .into_response(),
216 }
217}
218
219#[derive(Deserialize)]
220pub struct CreateUpstreamRequest {
221 pub name: String,
222 pub lb_strategy: Option<LoadBalanceStrategy>,
223 pub backends: Vec<BackendRequest>,
224}
225
226#[derive(Deserialize)]
227pub struct BackendRequest {
228 pub address: String,
229 pub weight: Option<u32>,
230 pub scheme: Option<String>,
231}
232
233async fn create_upstream(
234 State(state): State<Arc<ProxyState>>,
235 Json(req): Json<CreateUpstreamRequest>,
236) -> impl IntoResponse {
237 let backends: Vec<Backend> = req
238 .backends
239 .into_iter()
240 .map(|b| Backend {
241 id: Some(Uuid::new_v4()),
242 address: b.address,
243 weight: b.weight.unwrap_or(100),
244 scheme: b.scheme.unwrap_or_else(|| "http".to_string()),
245 enabled: true,
246 http_version: Default::default(),
247 })
248 .collect();
249
250 let upstream = Upstream {
251 id: Some(Uuid::new_v4()),
252 name: req.name,
253 description: None,
254 lb_strategy: req.lb_strategy.unwrap_or_default(),
255 backends,
256 health_check: HealthCheckConfig::default(),
257 enabled: true,
258 };
259
260 let mut config = state.config.write().await;
262 config.upstreams.push(upstream.clone());
263
264 (StatusCode::CREATED, Json(ApiResponse::success(upstream)))
265}
266
267async fn update_upstream(
268 State(state): State<Arc<ProxyState>>,
269 Path(id): Path<Uuid>,
270 Json(req): Json<CreateUpstreamRequest>,
271) -> impl IntoResponse {
272 let mut config = state.config.write().await;
273
274 if let Some(upstream) = config.upstreams.iter_mut().find(|u| u.id == Some(id)) {
275 upstream.name = req.name;
276 upstream.lb_strategy = req.lb_strategy.unwrap_or_default();
277 Json(ApiResponse::success(upstream.clone())).into_response()
281 } else {
282 (
283 StatusCode::NOT_FOUND,
284 Json(ApiResponse::<()>::error("Upstream not found")),
285 )
286 .into_response()
287 }
288}
289
290async fn delete_upstream(
291 State(state): State<Arc<ProxyState>>,
292 Path(id): Path<Uuid>,
293) -> impl IntoResponse {
294 let mut config = state.config.write().await;
295 let len_before = config.upstreams.len();
296 config.upstreams.retain(|u| u.id != Some(id));
297
298 if config.upstreams.len() < len_before {
299 Json(ApiResponse::success(())).into_response()
301 } else {
302 (
303 StatusCode::NOT_FOUND,
304 Json(ApiResponse::<()>::error("Upstream not found")),
305 )
306 .into_response()
307 }
308}
309
310async fn list_backends(
313 State(state): State<Arc<ProxyState>>,
314 Path(upstream_id): Path<Uuid>,
315) -> impl IntoResponse {
316 let config = state.config.read().await;
317 match config.upstreams.iter().find(|u| u.id == Some(upstream_id)) {
318 Some(upstream) => {
319 let backends = upstream.backends.clone();
320 Json(ApiResponse::success(backends)).into_response()
321 }
322 None => (
323 StatusCode::NOT_FOUND,
324 Json(ApiResponse::<()>::error("Upstream not found")),
325 )
326 .into_response(),
327 }
328}
329
330async fn add_backend(
331 State(state): State<Arc<ProxyState>>,
332 Path(upstream_id): Path<Uuid>,
333 Json(req): Json<BackendRequest>,
334) -> impl IntoResponse {
335 let mut config = state.config.write().await;
336
337 if let Some(upstream) = config
338 .upstreams
339 .iter_mut()
340 .find(|u| u.id == Some(upstream_id))
341 {
342 let backend = Backend {
343 id: Some(Uuid::new_v4()),
344 address: req.address,
345 weight: req.weight.unwrap_or(100),
346 scheme: req.scheme.unwrap_or_else(|| "http".to_string()),
347 enabled: true,
348 http_version: Default::default(),
349 };
350
351 upstream.backends.push(backend.clone());
352
353 (StatusCode::CREATED, Json(ApiResponse::success(backend))).into_response()
355 } else {
356 (
357 StatusCode::NOT_FOUND,
358 Json(ApiResponse::<()>::error("Upstream not found")),
359 )
360 .into_response()
361 }
362}
363
364async fn remove_backend(
365 State(state): State<Arc<ProxyState>>,
366 Path((upstream_id, backend_id)): Path<(Uuid, Uuid)>,
367) -> impl IntoResponse {
368 let mut config = state.config.write().await;
369
370 if let Some(upstream) = config
371 .upstreams
372 .iter_mut()
373 .find(|u| u.id == Some(upstream_id))
374 {
375 let len_before = upstream.backends.len();
376 upstream.backends.retain(|b| b.id != Some(backend_id));
377
378 if upstream.backends.len() < len_before {
379 Json(ApiResponse::success(())).into_response()
381 } else {
382 (
383 StatusCode::NOT_FOUND,
384 Json(ApiResponse::<()>::error("Backend not found")),
385 )
386 .into_response()
387 }
388 } else {
389 (
390 StatusCode::NOT_FOUND,
391 Json(ApiResponse::<()>::error("Upstream not found")),
392 )
393 .into_response()
394 }
395}
396
397#[derive(Serialize)]
400pub struct HealthSummary {
401 pub total_backends: usize,
402 pub healthy_backends: usize,
403 pub unhealthy_backends: usize,
404}
405
406async fn health_status(State(state): State<Arc<ProxyState>>) -> impl IntoResponse {
407 let config = state.config.read().await;
408
409 let mut total = 0;
410 let mut healthy = 0;
411
412 for upstream in &config.upstreams {
413 for backend in &upstream.backends {
414 total += 1;
415 if let Some(id) = backend.id {
416 if state.health_checker.is_healthy(id) {
417 healthy += 1;
418 }
419 }
420 }
421 }
422
423 Json(ApiResponse::success(HealthSummary {
424 total_backends: total,
425 healthy_backends: healthy,
426 unhealthy_backends: total - healthy,
427 }))
428}
429
430async fn backend_health(
431 State(state): State<Arc<ProxyState>>,
432 Path(backend_id): Path<Uuid>,
433) -> impl IntoResponse {
434 match state.health_checker.get_health(backend_id) {
435 Some(health) => Json(ApiResponse::success(health)).into_response(),
436 None => (
437 StatusCode::NOT_FOUND,
438 Json(ApiResponse::<()>::error("Backend not found")),
439 )
440 .into_response(),
441 }
442}
443
444async fn reload_config(State(state): State<Arc<ProxyState>>) -> impl IntoResponse {
447 state.config_reloader.request_reload().await;
448 Json(ApiResponse::success("Configuration reload requested"))
449}
450
451#[derive(Serialize)]
454pub struct ProxyStats {
455 pub routes_count: usize,
456 pub upstreams_count: usize,
457 pub backends_count: usize,
458}
459
460async fn get_stats(State(state): State<Arc<ProxyState>>) -> impl IntoResponse {
461 let config = state.config.read().await;
462
463 let backends_count: usize = config.upstreams.iter().map(|u| u.backends.len()).sum();
464
465 Json(ApiResponse::success(ProxyStats {
466 routes_count: config.routes.len(),
467 upstreams_count: config.upstreams.len(),
468 backends_count,
469 }))
470}