Skip to main content

reifydb_sub_server_admin/
handlers.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4//! HTTP endpoint handler for the admin server.
5//!
6//! This module provides handler for:
7//! - `/health` - Health check
8//! - `/v1/auth/*` - Authentication endpoints
9//! - `/v1/config` - Configuration endpoints
10//! - `/v1/execute` - Query execution
11//! - `/v1/metrics` - System metrics
12//! - Static file serving for the admin UI
13
14use axum::{
15	Json,
16	body::Body,
17	extract::{Path, State},
18	http::{Response, StatusCode, header},
19	response::IntoResponse,
20};
21use serde::{Deserialize, Serialize};
22use serde_json::json;
23
24use crate::{assets, state::AdminState};
25
26/// Login request body.
27#[derive(Debug, Deserialize)]
28pub struct LoginRequest {
29	pub token: String,
30}
31
32/// Login response.
33#[derive(Debug, Serialize)]
34pub struct LoginResponse {
35	pub success: bool,
36	pub message: Option<String>,
37	pub session_token: Option<String>,
38}
39
40/// Auth status response.
41#[derive(Debug, Serialize)]
42pub struct AuthStatusResponse {
43	pub auth_required: bool,
44	pub authenticated: bool,
45}
46
47/// Handle login request.
48pub async fn handle_login(State(state): State<AdminState>, Json(request): Json<LoginRequest>) -> impl IntoResponse {
49	if !state.auth_required() {
50		return (
51			StatusCode::OK,
52			Json(LoginResponse {
53				success: true,
54				message: Some("Auth not required".to_string()),
55				session_token: None,
56			}),
57		);
58	}
59
60	if state.auth_token() == Some(&request.token) {
61		// TODO: Generate proper session token
62		(
63			StatusCode::OK,
64			Json(LoginResponse {
65				success: true,
66				message: None,
67				session_token: Some("temp_session_token".to_string()),
68			}),
69		)
70	} else {
71		(
72			StatusCode::BAD_REQUEST,
73			Json(LoginResponse {
74				success: false,
75				message: Some("Invalid token".to_string()),
76				session_token: None,
77			}),
78		)
79	}
80}
81
82/// Handle logout request.
83pub async fn handle_logout() -> impl IntoResponse {
84	(
85		StatusCode::OK,
86		Json(json!({
87			"success": true,
88			"message": "Logged out"
89		})),
90	)
91}
92
93/// Get authentication status.
94pub async fn handle_auth_status(State(state): State<AdminState>) -> impl IntoResponse {
95	(
96		StatusCode::OK,
97		Json(AuthStatusResponse {
98			auth_required: state.auth_required(),
99			// TODO: Check actual auth status from session
100			authenticated: !state.auth_required(),
101		}),
102	)
103}
104
105/// Execute request body.
106#[derive(Debug, Deserialize)]
107pub struct ExecuteRequest {
108	pub query: String,
109}
110
111/// Execute a query (placeholder).
112pub async fn handle_execute(
113	State(_state): State<AdminState>,
114	Json(request): Json<ExecuteRequest>,
115) -> impl IntoResponse {
116	// TODO: Execute query using the engine
117	(
118		StatusCode::OK,
119		Json(json!({
120			"success": true,
121			"message": "Query execution not yet implemented",
122			"query": request.query
123		})),
124	)
125}
126
127const FALLBACK_HTML: &str = r#"<!DOCTYPE html>
128<html>
129<head>
130    <title>ReifyDB Admin</title>
131    <style>
132        body { font-family: system-ui; max-width: 800px; margin: 50px auto; padding: 20px; }
133        .error { background: #fee; padding: 20px; border-radius: 5px; }
134    </style>
135</head>
136<body>
137    <h1>ReifyDB Admin Console</h1>
138    <div class="error">
139        <p>React app not found. Please build the webapp first.</p>
140    </div>
141</body>
142</html>"#;
143
144/// Serve the index.html file.
145pub async fn serve_index() -> impl IntoResponse {
146	if let Some(file) = assets::get_embedded_file("index.html") {
147		Response::builder()
148			.status(StatusCode::OK)
149			.header(header::CONTENT_TYPE, file.mime_type)
150			.body(Body::from(file.content.to_vec()))
151			.unwrap()
152	} else {
153		Response::builder()
154			.status(StatusCode::OK)
155			.header(header::CONTENT_TYPE, "text/html")
156			.body(Body::from(FALLBACK_HTML))
157			.unwrap()
158	}
159}
160
161/// Serve static assets.
162pub async fn serve_static(Path(path): Path<String>) -> impl IntoResponse {
163	// The router extracts path without "assets/" prefix (e.g., "index.js")
164	// but the manifest stores files with full path (e.g., "assets/index.js")
165	let clean_path = path.strip_prefix('/').unwrap_or(&path);
166	let full_path = format!("assets/{}", clean_path);
167
168	if let Some(file) = assets::get_embedded_file(&full_path) {
169		Response::builder()
170			.status(StatusCode::OK)
171			.header(header::CONTENT_TYPE, file.mime_type)
172			.header(header::CACHE_CONTROL, "public, max-age=31536000")
173			.body(Body::from(file.content.to_vec()))
174			.unwrap()
175	} else {
176		Response::builder()
177			.status(StatusCode::NOT_FOUND)
178			.header(header::CONTENT_TYPE, "text/plain")
179			.body(Body::from(format!("Static file not found: {}", full_path)))
180			.unwrap()
181	}
182}