axum_sql_viewer/
frontend.rs1use axum::{
7 body::Body,
8 extract::{Path, State},
9 http::{header, StatusCode},
10 response::Response,
11 routing::get,
12 Router,
13};
14use include_dir::{include_dir, Dir};
15use std::sync::Arc;
16
17static FRONTEND_DISTRIBUTION: Dir = include_dir!("$CARGO_MANIFEST_DIR/frontend/dist");
19
20#[derive(Clone)]
22pub struct FrontendState {
23 pub base_path: Arc<String>,
24}
25
26impl FrontendState {
27 pub fn new(base_path: String) -> Self {
29 Self {
30 base_path: Arc::new(base_path),
31 }
32 }
33}
34
35pub fn create_frontend_router(base_path: String) -> Router {
45 let state = FrontendState::new(base_path);
46
47 Router::new()
49 .route("/", get(serve_index_page))
50 .route("/assets/{*path}", get(serve_static_asset))
51 .with_state(state)
52}
53
54async fn serve_index_page(State(state): State<FrontendState>) -> Response {
62 if let Some(file) = FRONTEND_DISTRIBUTION.get_file("index.html") {
64 let mut contents = String::from_utf8_lossy(file.contents()).to_string();
65
66 if let Some(head_position) = contents.find("<head>") {
69 let insert_position = head_position + "<head>".len();
70 let base_tag = format!("\n <base href=\"{}/\">", state.base_path);
71 contents.insert_str(insert_position, &base_tag);
72 }
73
74 Response::builder()
75 .status(StatusCode::OK)
76 .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
77 .header(header::CACHE_CONTROL, "public, max-age=3600") .body(Body::from(contents))
79 .unwrap()
80 } else {
81 serve_fallback_page()
82 }
83}
84
85async fn serve_static_asset(Path(path): Path<String>) -> Response {
92 let asset_path = format!("assets/{}", path);
95
96 if let Some(file) = FRONTEND_DISTRIBUTION.get_file(&asset_path) {
98 let contents = file.contents();
99 let mime_type = mime_guess::from_path(&asset_path)
100 .first_or_octet_stream()
101 .to_string();
102
103 Response::builder()
104 .status(StatusCode::OK)
105 .header(header::CONTENT_TYPE, mime_type)
106 .header(header::CACHE_CONTROL, "public, max-age=31536000, immutable") .body(Body::from(contents))
108 .unwrap()
109 } else {
110 Response::builder()
111 .status(StatusCode::NOT_FOUND)
112 .header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
113 .body(Body::from(format!("Asset not found: {}", asset_path)))
114 .unwrap()
115 }
116}
117
118fn serve_fallback_page() -> Response {
124 let html = r#"<!DOCTYPE html>
125<html lang="en">
126<head>
127 <meta charset="UTF-8">
128 <meta name="viewport" content="width=device-width, initial-scale=1.0">
129 <title>axum-sql-viewer - Frontend Not Built</title>
130 <style>
131 body {
132 font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
133 max-width: 800px;
134 margin: 100px auto;
135 padding: 20px;
136 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
137 min-height: 100vh;
138 }
139 .container {
140 background: white;
141 padding: 40px;
142 border-radius: 12px;
143 box-shadow: 0 10px 40px rgba(0,0,0,0.2);
144 }
145 h1 {
146 color: #333;
147 margin-top: 0;
148 font-size: 2em;
149 }
150 h2 {
151 color: #555;
152 font-size: 1.3em;
153 margin-top: 30px;
154 border-bottom: 2px solid #667eea;
155 padding-bottom: 10px;
156 }
157 code {
158 background: #f5f5f5;
159 padding: 3px 8px;
160 border-radius: 4px;
161 font-family: 'Courier New', Consolas, monospace;
162 font-size: 0.9em;
163 color: #e83e8c;
164 }
165 pre {
166 background: #2d2d2d;
167 color: #f8f8f2;
168 padding: 20px;
169 border-radius: 6px;
170 overflow-x: auto;
171 font-family: 'Courier New', Consolas, monospace;
172 line-height: 1.5;
173 }
174 .warning {
175 background: #fff3cd;
176 border-left: 4px solid #ffc107;
177 padding: 15px 20px;
178 margin: 20px 0;
179 border-radius: 4px;
180 }
181 .warning strong {
182 color: #856404;
183 display: block;
184 margin-bottom: 8px;
185 font-size: 1.1em;
186 }
187 .warning p {
188 color: #856404;
189 margin: 0;
190 }
191 ul {
192 line-height: 1.8;
193 }
194 li code {
195 background: #e9ecef;
196 color: #495057;
197 }
198 .info {
199 background: #d1ecf1;
200 border-left: 4px solid #17a2b8;
201 padding: 15px 20px;
202 margin: 20px 0;
203 border-radius: 4px;
204 color: #0c5460;
205 }
206 </style>
207</head>
208<body>
209 <div class="container">
210 <h1>🔍 axum-sql-viewer</h1>
211
212 <div class="warning">
213 <strong>⚠️ Frontend Not Built</strong>
214 <p>The frontend has not been built yet. To use axum-sql-viewer, you need to build the React frontend.</p>
215 </div>
216
217 <h2>📦 Development Setup</h2>
218 <p>To build the frontend during development:</p>
219 <pre>cd axum-sql-viewer/frontend
220pnpm install
221pnpm build</pre>
222
223 <h2>🚀 Using Pre-built Package</h2>
224 <div class="info">
225 <p>If you're using the crate from crates.io, the frontend should already be included. If you see this message, please report it as a bug on GitHub.</p>
226 </div>
227
228 <h2>🔌 API Endpoints</h2>
229 <p>The REST API is still available for direct access:</p>
230 <ul>
231 <li><code>GET /api/tables</code> - List all tables in the database</li>
232 <li><code>GET /api/tables/:name</code> - Get table schema information</li>
233 <li><code>GET /api/tables/:name/rows</code> - Fetch rows with pagination and filtering</li>
234 <li><code>GET /api/tables/:name/count</code> - Get total row count</li>
235 <li><code>POST /api/query</code> - Execute raw SQL queries</li>
236 </ul>
237
238 <h2>📚 Documentation</h2>
239 <p>For more information, visit:</p>
240 <ul>
241 <li><a href="https://docs.rs/axum-sql-viewer" target="_blank">Documentation on docs.rs</a></li>
242 <li><a href="https://github.com/firstdorsal/axum-sql-viewer" target="_blank">GitHub Repository</a></li>
243 </ul>
244
245 <h2>⚠️ Security Warning</h2>
246 <div class="warning">
247 <strong>Development Tool Only</strong>
248 <p>This tool exposes your entire database and should NEVER be used in production or on public networks. It has no authentication or authorization built in.</p>
249 </div>
250 </div>
251</body>
252</html>
253"#;
254
255 Response::builder()
256 .status(StatusCode::OK)
257 .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
258 .header(header::CACHE_CONTROL, "no-cache") .body(Body::from(html))
260 .unwrap()
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_frontend_state_creation() {
269 let state = FrontendState::new("/sql-viewer".to_string());
270 assert_eq!(*state.base_path, "/sql-viewer");
271 }
272
273 #[test]
274 fn test_mime_type_guessing() {
275 use mime_guess::from_path;
276
277 let javascript_mime = from_path("application.js").first_or_octet_stream();
279 assert_eq!(javascript_mime.as_ref(), "text/javascript");
280
281 let css_mime = from_path("styles.css").first_or_octet_stream();
283 assert_eq!(css_mime.as_ref(), "text/css");
284
285 let html_mime = from_path("index.html").first_or_octet_stream();
287 assert_eq!(html_mime.as_ref(), "text/html");
288
289 let png_mime = from_path("image.png").first_or_octet_stream();
291 assert_eq!(png_mime.as_ref(), "image/png");
292
293 let svg_mime = from_path("icon.svg").first_or_octet_stream();
294 assert_eq!(svg_mime.as_ref(), "image/svg+xml");
295
296 let woff2_mime = from_path("font.woff2").first_or_octet_stream();
298 assert_eq!(woff2_mime.as_ref(), "font/woff2");
299 }
300
301 #[test]
302 fn test_fallback_page_has_content() {
303 let response = serve_fallback_page();
304 assert_eq!(response.status(), StatusCode::OK);
305
306 let content_type = response.headers().get(header::CONTENT_TYPE);
308 assert!(content_type.is_some());
309 assert_eq!(content_type.unwrap(), "text/html; charset=utf-8");
310 }
311
312 #[test]
313 fn test_router_creation() {
314 let router = create_frontend_router("/sql-viewer".to_string());
315 drop(router);
317 }
318}