Skip to main content

postrust_proxy/admin/
ui.rs

1//! HTML admin UI for proxy management.
2
3use crate::ProxyState;
4use axum::{extract::State, response::Html, routing::get, Router};
5use std::sync::Arc;
6
7/// Create the admin UI router.
8pub 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    // TODO: Integrate with CertificateStore
297    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}