Skip to main content

reifydb_sub_server_admin/
handlers.rs

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