postrust_proxy/admin/
ui.rs1use crate::ProxyState;
4use axum::{extract::State, response::Html, routing::get, Router};
5use std::sync::Arc;
6
7pub fn admin_ui_router() -> Router<Arc<ProxyState>> {
9 Router::new()
10 .route("/", get(proxy_dashboard))
11 .route("/routes", get(routes_page))
12 .route("/upstreams", get(upstreams_page))
13 .route("/health", get(health_page))
14 .route("/certificates", get(certificates_page))
15}
16
17async fn proxy_dashboard(State(state): State<Arc<ProxyState>>) -> Html<String> {
18 let config = state.config.read().await;
19
20 let backends_count: usize = config.upstreams.iter().map(|u| u.backends.len()).sum();
21
22 let html = format!(
23 r#"<!DOCTYPE html>
24<html>
25<head>
26 <title>Proxy Dashboard - Postrust</title>
27 <style>
28 body {{ font-family: system-ui, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }}
29 .container {{ max-width: 1200px; margin: 0 auto; }}
30 h1 {{ color: #333; }}
31 .stats {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 20px 0; }}
32 .stat-card {{ background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
33 .stat-card h3 {{ margin: 0 0 10px 0; color: #666; font-size: 14px; }}
34 .stat-card .value {{ font-size: 36px; font-weight: bold; color: #333; }}
35 .nav {{ margin: 20px 0; }}
36 .nav a {{ display: inline-block; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 4px; margin-right: 10px; }}
37 .nav a:hover {{ background: #0056b3; }}
38 </style>
39</head>
40<body>
41 <div class="container">
42 <h1>Proxy Dashboard</h1>
43
44 <div class="nav">
45 <a href="/admin/proxy/routes">Routes</a>
46 <a href="/admin/proxy/upstreams">Upstreams</a>
47 <a href="/admin/proxy/health">Health</a>
48 <a href="/admin/proxy/certificates">Certificates</a>
49 </div>
50
51 <div class="stats">
52 <div class="stat-card">
53 <h3>Routes</h3>
54 <div class="value">{}</div>
55 </div>
56 <div class="stat-card">
57 <h3>Upstreams</h3>
58 <div class="value">{}</div>
59 </div>
60 <div class="stat-card">
61 <h3>Backends</h3>
62 <div class="value">{}</div>
63 </div>
64 </div>
65 </div>
66</body>
67</html>"#,
68 config.routes.len(),
69 config.upstreams.len(),
70 backends_count,
71 );
72
73 Html(html)
74}
75
76async fn routes_page(State(state): State<Arc<ProxyState>>) -> Html<String> {
77 let config = state.config.read().await;
78
79 let routes_html: String = config
80 .routes
81 .iter()
82 .map(|r| {
83 format!(
84 r#"<tr>
85 <td>{}</td>
86 <td>{}</td>
87 <td>{}</td>
88 <td>{}</td>
89 <td>{}</td>
90 </tr>"#,
91 r.id.map(|id| id.to_string()).unwrap_or_default(),
92 r.name,
93 r.match_.host.as_deref().unwrap_or("*"),
94 r.match_.path.as_deref().unwrap_or("/"),
95 r.priority,
96 )
97 })
98 .collect();
99
100 let html = format!(
101 r#"<!DOCTYPE html>
102<html>
103<head>
104 <title>Routes - Postrust Proxy</title>
105 <style>
106 body {{ font-family: system-ui, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }}
107 .container {{ max-width: 1200px; margin: 0 auto; }}
108 h1 {{ color: #333; }}
109 table {{ width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
110 th, td {{ padding: 12px 15px; text-align: left; border-bottom: 1px solid #eee; }}
111 th {{ background: #f8f9fa; font-weight: 600; }}
112 .nav {{ margin: 20px 0; }}
113 .nav a {{ display: inline-block; padding: 10px 20px; background: #6c757d; color: white; text-decoration: none; border-radius: 4px; margin-right: 10px; }}
114 .nav a:hover {{ background: #5a6268; }}
115 </style>
116</head>
117<body>
118 <div class="container">
119 <h1>Routes</h1>
120
121 <div class="nav">
122 <a href="/admin/proxy/">Back to Dashboard</a>
123 </div>
124
125 <table>
126 <thead>
127 <tr>
128 <th>ID</th>
129 <th>Name</th>
130 <th>Host</th>
131 <th>Path Prefix</th>
132 <th>Priority</th>
133 </tr>
134 </thead>
135 <tbody>
136 {}
137 </tbody>
138 </table>
139 </div>
140</body>
141</html>"#,
142 routes_html
143 );
144
145 Html(html)
146}
147
148async fn upstreams_page(State(state): State<Arc<ProxyState>>) -> Html<String> {
149 let config = state.config.read().await;
150
151 let upstreams_html: String = config
152 .upstreams
153 .iter()
154 .map(|u| {
155 format!(
156 r#"<tr>
157 <td>{}</td>
158 <td>{}</td>
159 <td>{:?}</td>
160 <td>{}</td>
161 </tr>"#,
162 u.id.map(|id| id.to_string()).unwrap_or_default(),
163 u.name,
164 u.lb_strategy,
165 u.backends.len(),
166 )
167 })
168 .collect();
169
170 let html = format!(
171 r#"<!DOCTYPE html>
172<html>
173<head>
174 <title>Upstreams - Postrust Proxy</title>
175 <style>
176 body {{ font-family: system-ui, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }}
177 .container {{ max-width: 1200px; margin: 0 auto; }}
178 h1 {{ color: #333; }}
179 table {{ width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
180 th, td {{ padding: 12px 15px; text-align: left; border-bottom: 1px solid #eee; }}
181 th {{ background: #f8f9fa; font-weight: 600; }}
182 .nav {{ margin: 20px 0; }}
183 .nav a {{ display: inline-block; padding: 10px 20px; background: #6c757d; color: white; text-decoration: none; border-radius: 4px; margin-right: 10px; }}
184 .nav a:hover {{ background: #5a6268; }}
185 </style>
186</head>
187<body>
188 <div class="container">
189 <h1>Upstreams</h1>
190
191 <div class="nav">
192 <a href="/admin/proxy/">Back to Dashboard</a>
193 </div>
194
195 <table>
196 <thead>
197 <tr>
198 <th>ID</th>
199 <th>Name</th>
200 <th>Load Balance</th>
201 <th>Backends</th>
202 </tr>
203 </thead>
204 <tbody>
205 {}
206 </tbody>
207 </table>
208 </div>
209</body>
210</html>"#,
211 upstreams_html
212 );
213
214 Html(html)
215}
216
217async fn health_page(State(state): State<Arc<ProxyState>>) -> Html<String> {
218 let config = state.config.read().await;
219
220 let mut health_html = String::new();
221 for upstream in &config.upstreams {
222 for backend in &upstream.backends {
223 let (status, status_class) = if let Some(id) = backend.id {
224 if state.health_checker.is_healthy(id) {
225 ("Healthy", "healthy")
226 } else {
227 ("Unhealthy", "unhealthy")
228 }
229 } else {
230 ("Unknown", "unknown")
231 };
232
233 health_html.push_str(&format!(
234 r#"<tr>
235 <td>{}</td>
236 <td>{}</td>
237 <td>{}</td>
238 <td class="{}">{}</td>
239 </tr>"#,
240 upstream.name, backend.address, backend.scheme, status_class, status,
241 ));
242 }
243 }
244
245 let html = format!(
246 r#"<!DOCTYPE html>
247<html>
248<head>
249 <title>Health Status - Postrust Proxy</title>
250 <style>
251 body {{ font-family: system-ui, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }}
252 .container {{ max-width: 1200px; margin: 0 auto; }}
253 h1 {{ color: #333; }}
254 table {{ width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
255 th, td {{ padding: 12px 15px; text-align: left; border-bottom: 1px solid #eee; }}
256 th {{ background: #f8f9fa; font-weight: 600; }}
257 .healthy {{ color: #28a745; font-weight: bold; }}
258 .unhealthy {{ color: #dc3545; font-weight: bold; }}
259 .unknown {{ color: #6c757d; }}
260 .nav {{ margin: 20px 0; }}
261 .nav a {{ display: inline-block; padding: 10px 20px; background: #6c757d; color: white; text-decoration: none; border-radius: 4px; margin-right: 10px; }}
262 .nav a:hover {{ background: #5a6268; }}
263 </style>
264</head>
265<body>
266 <div class="container">
267 <h1>Backend Health Status</h1>
268
269 <div class="nav">
270 <a href="/admin/proxy/">Back to Dashboard</a>
271 </div>
272
273 <table>
274 <thead>
275 <tr>
276 <th>Upstream</th>
277 <th>Backend Address</th>
278 <th>Scheme</th>
279 <th>Status</th>
280 </tr>
281 </thead>
282 <tbody>
283 {}
284 </tbody>
285 </table>
286 </div>
287</body>
288</html>"#,
289 health_html
290 );
291
292 Html(html)
293}
294
295async fn certificates_page(State(_state): State<Arc<ProxyState>>) -> Html<String> {
296 let html = r#"<!DOCTYPE html>
298<html>
299<head>
300 <title>Certificates - Postrust Proxy</title>
301 <style>
302 body { font-family: system-ui, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
303 .container { max-width: 1200px; margin: 0 auto; }
304 h1 { color: #333; }
305 .placeholder { background: white; padding: 40px; text-align: center; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); color: #666; }
306 .nav { margin: 20px 0; }
307 .nav a { display: inline-block; padding: 10px 20px; background: #6c757d; color: white; text-decoration: none; border-radius: 4px; margin-right: 10px; }
308 .nav a:hover { background: #5a6268; }
309 </style>
310</head>
311<body>
312 <div class="container">
313 <h1>SSL/TLS Certificates</h1>
314
315 <div class="nav">
316 <a href="/admin/proxy/">Back to Dashboard</a>
317 </div>
318
319 <div class="placeholder">
320 <p>Certificate management will be available here.</p>
321 <p>Certificates are automatically managed via Let's Encrypt ACME.</p>
322 </div>
323 </div>
324</body>
325</html>"#;
326
327 Html(html.to_string())
328}