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 })
247 .collect();
248
249 let upstream = Upstream {
250 id: Some(Uuid::new_v4()),
251 name: req.name,
252 description: None,
253 lb_strategy: req.lb_strategy.unwrap_or_default(),
254 backends,
255 health_check: HealthCheckConfig::default(),
256 enabled: true,
257 };
258
259 let mut config = state.config.write().await;
261 config.upstreams.push(upstream.clone());
262
263 (StatusCode::CREATED, Json(ApiResponse::success(upstream)))
264}
265
266async fn update_upstream(
267 State(state): State<Arc<ProxyState>>,
268 Path(id): Path<Uuid>,
269 Json(req): Json<CreateUpstreamRequest>,
270) -> impl IntoResponse {
271 let mut config = state.config.write().await;
272
273 if let Some(upstream) = config.upstreams.iter_mut().find(|u| u.id == Some(id)) {
274 upstream.name = req.name;
275 upstream.lb_strategy = req.lb_strategy.unwrap_or_default();
276 Json(ApiResponse::success(upstream.clone())).into_response()
280 } else {
281 (
282 StatusCode::NOT_FOUND,
283 Json(ApiResponse::<()>::error("Upstream not found")),
284 )
285 .into_response()
286 }
287}
288
289async fn delete_upstream(
290 State(state): State<Arc<ProxyState>>,
291 Path(id): Path<Uuid>,
292) -> impl IntoResponse {
293 let mut config = state.config.write().await;
294 let len_before = config.upstreams.len();
295 config.upstreams.retain(|u| u.id != Some(id));
296
297 if config.upstreams.len() < len_before {
298 Json(ApiResponse::success(())).into_response()
300 } else {
301 (
302 StatusCode::NOT_FOUND,
303 Json(ApiResponse::<()>::error("Upstream not found")),
304 )
305 .into_response()
306 }
307}
308
309async fn list_backends(
312 State(state): State<Arc<ProxyState>>,
313 Path(upstream_id): Path<Uuid>,
314) -> impl IntoResponse {
315 let config = state.config.read().await;
316 match config.upstreams.iter().find(|u| u.id == Some(upstream_id)) {
317 Some(upstream) => {
318 let backends = upstream.backends.clone();
319 Json(ApiResponse::success(backends)).into_response()
320 }
321 None => (
322 StatusCode::NOT_FOUND,
323 Json(ApiResponse::<()>::error("Upstream not found")),
324 )
325 .into_response(),
326 }
327}
328
329async fn add_backend(
330 State(state): State<Arc<ProxyState>>,
331 Path(upstream_id): Path<Uuid>,
332 Json(req): Json<BackendRequest>,
333) -> impl IntoResponse {
334 let mut config = state.config.write().await;
335
336 if let Some(upstream) = config
337 .upstreams
338 .iter_mut()
339 .find(|u| u.id == Some(upstream_id))
340 {
341 let backend = Backend {
342 id: Some(Uuid::new_v4()),
343 address: req.address,
344 weight: req.weight.unwrap_or(100),
345 scheme: req.scheme.unwrap_or_else(|| "http".to_string()),
346 enabled: true,
347 };
348
349 upstream.backends.push(backend.clone());
350
351 (StatusCode::CREATED, Json(ApiResponse::success(backend))).into_response()
353 } else {
354 (
355 StatusCode::NOT_FOUND,
356 Json(ApiResponse::<()>::error("Upstream not found")),
357 )
358 .into_response()
359 }
360}
361
362async fn remove_backend(
363 State(state): State<Arc<ProxyState>>,
364 Path((upstream_id, backend_id)): Path<(Uuid, Uuid)>,
365) -> impl IntoResponse {
366 let mut config = state.config.write().await;
367
368 if let Some(upstream) = config
369 .upstreams
370 .iter_mut()
371 .find(|u| u.id == Some(upstream_id))
372 {
373 let len_before = upstream.backends.len();
374 upstream.backends.retain(|b| b.id != Some(backend_id));
375
376 if upstream.backends.len() < len_before {
377 Json(ApiResponse::success(())).into_response()
379 } else {
380 (
381 StatusCode::NOT_FOUND,
382 Json(ApiResponse::<()>::error("Backend not found")),
383 )
384 .into_response()
385 }
386 } else {
387 (
388 StatusCode::NOT_FOUND,
389 Json(ApiResponse::<()>::error("Upstream not found")),
390 )
391 .into_response()
392 }
393}
394
395#[derive(Serialize)]
398pub struct HealthSummary {
399 pub total_backends: usize,
400 pub healthy_backends: usize,
401 pub unhealthy_backends: usize,
402}
403
404async fn health_status(State(state): State<Arc<ProxyState>>) -> impl IntoResponse {
405 let config = state.config.read().await;
406
407 let mut total = 0;
408 let mut healthy = 0;
409
410 for upstream in &config.upstreams {
411 for backend in &upstream.backends {
412 total += 1;
413 if let Some(id) = backend.id {
414 if state.health_checker.is_healthy(id) {
415 healthy += 1;
416 }
417 }
418 }
419 }
420
421 Json(ApiResponse::success(HealthSummary {
422 total_backends: total,
423 healthy_backends: healthy,
424 unhealthy_backends: total - healthy,
425 }))
426}
427
428async fn backend_health(
429 State(state): State<Arc<ProxyState>>,
430 Path(backend_id): Path<Uuid>,
431) -> impl IntoResponse {
432 match state.health_checker.get_health(backend_id) {
433 Some(health) => Json(ApiResponse::success(health)).into_response(),
434 None => (
435 StatusCode::NOT_FOUND,
436 Json(ApiResponse::<()>::error("Backend not found")),
437 )
438 .into_response(),
439 }
440}
441
442async fn reload_config(State(state): State<Arc<ProxyState>>) -> impl IntoResponse {
445 state.config_reloader.request_reload().await;
446 Json(ApiResponse::success("Configuration reload requested"))
447}
448
449#[derive(Serialize)]
452pub struct ProxyStats {
453 pub routes_count: usize,
454 pub upstreams_count: usize,
455 pub backends_count: usize,
456}
457
458async fn get_stats(State(state): State<Arc<ProxyState>>) -> impl IntoResponse {
459 let config = state.config.read().await;
460
461 let backends_count: usize = config.upstreams.iter().map(|u| u.backends.len()).sum();
462
463 Json(ApiResponse::success(ProxyStats {
464 routes_count: config.routes.len(),
465 upstreams_count: config.upstreams.len(),
466 backends_count,
467 }))
468}