fix(proxy): fix RustEmbed derive and error types in search_ui module

- Fixed RustEmbed folder path from "../../static/search/" to "static/search/"
- Changed error type from Result<T> to Result<T, ErrorResponse> for axum compatibility
- Replaced MiroirError::InvalidRequest with ErrorResponse::invalid_request
- Replaced MiroirError::Task with ErrorResponse::internal_error
- Fixed config parameter to pass &config.search_ui instead of &config

The RustEmbed derive was not working because the folder path was relative
to the file location instead of the crate root. Additionally, the error
type needed to be ErrorResponse (which implements IntoResponse) instead
of MiroirError for axum handler compatibility.

Closes: compilation errors in search_ui.rs
This commit is contained in:
jedarden 2026-05-24 15:46:59 -04:00
parent c8bc21bc71
commit a077dc4347
2 changed files with 39 additions and 45 deletions

View file

@ -48,7 +48,7 @@ hex = "0.4"
# HTTP / Tower
tower = "0.5"
rust-embed = { version = "8", features = ["include-exclude"] }
rust-embed = { version = "8", features = ["debug-embed"] }
mime_guess = "2"
# OpenTelemetry (optional - use feature flag to enable)

View file

@ -15,9 +15,10 @@ use axum::{
use miroir_core::{
config::advanced::{SearchUiAuthConfig, SearchUiConfig},
task_store::{SearchUiScopedKey, TaskStore},
MiroirError, Result,
};
use rust_embed::RustEmbed;
use crate::error_response::ErrorResponse;
use rust_embed::RustEmbed as Embed;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tracing::{debug, info};
@ -150,14 +151,12 @@ pub async fn mint_session(
Path(index_uid): Path<String>,
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<impl IntoResponse, MiroirError> {
) -> Result<impl IntoResponse, ErrorResponse> {
let config = &state.config;
// Check if search UI is enabled
if !config.search_ui.enabled {
return Err(MiroirError::InvalidRequest(
"search UI is not enabled".to_string(),
));
return Err(ErrorResponse::invalid_request("search UI is not enabled"));
}
// Validate auth mode
@ -170,18 +169,18 @@ pub async fn mint_session(
"shared_key" => {
// Require X-Search-UI-Key header
let shared_key = std::env::var(&auth_config.shared_key_env).map_err(|_| {
MiroirError::InvalidRequest("search UI shared key not configured".to_string())
ErrorResponse::invalid_request("search UI shared key not configured".to_string())
})?;
let provided_key = headers
.get("X-Search-UI-Key")
.and_then(|v| v.to_str().ok())
.ok_or_else(|| {
MiroirError::InvalidRequest("missing X-Search-UI-Key header".to_string())
ErrorResponse::invalid_request("missing X-Search-UI-Key header".to_string())
})?;
if provided_key != shared_key {
return Err(MiroirError::InvalidRequest(
return Err(ErrorResponse::invalid_request(
"invalid X-Search-UI-Key".to_string(),
));
}
@ -192,7 +191,7 @@ pub async fn mint_session(
.get(&auth_config.oauth_proxy.user_header)
.and_then(|v| v.to_str().ok())
.ok_or_else(|| {
MiroirError::InvalidRequest(format!(
ErrorResponse::invalid_request(format!(
"missing {} header",
auth_config.oauth_proxy.user_header
))
@ -205,7 +204,7 @@ pub async fn mint_session(
debug!(index = %index_uid, "minting oauth_proxy search UI session");
}
_ => {
return Err(MiroirError::InvalidRequest(format!(
return Err(ErrorResponse::invalid_request(format!(
"invalid auth mode: {}",
auth_config.mode
)));
@ -213,7 +212,7 @@ pub async fn mint_session(
}
// Get or create scoped key for this index
let scoped_key = get_or_create_scoped_key(&state, &index_uid, config).await?;
let scoped_key = get_or_create_scoped_key(&state, &index_uid, &config.search_ui).await?;
// Build JWT claims
let now = chrono::Utc::now().timestamp() as u64;
@ -272,7 +271,7 @@ pub async fn mint_session(
// Sign with primary secret
let secret = std::env::var(&auth_config.jwt_secret_env)
.map_err(|_| MiroirError::Task("JWT secret not configured".to_string()))?;
.map_err(|_| ErrorResponse::internal_error("JWT secret not configured".to_string()))?;
let header = JwtHeader {
alg: "HS256".to_string(),
@ -281,7 +280,7 @@ pub async fn mint_session(
};
let token = jwt_encode(&header, &claims, secret.as_bytes())
.map_err(|e| MiroirError::Task(format!("JWT encoding failed: {}", e)))?;
.map_err(|e| ErrorResponse::internal_error(format!("JWT encoding failed: {}", e)))?;
info!(
index = %index_uid,
@ -307,9 +306,9 @@ async fn get_or_create_scoped_key(
state: &AppState,
index_uid: &str,
config: &SearchUiConfig,
) -> Result<SearchUiScopedKey, MiroirError> {
) -> Result<SearchUiScopedKey, ErrorResponse> {
let redis_store = state.redis_store.as_ref().ok_or_else(|| {
MiroirError::TaskStore("Redis store required for search UI scoped keys".to_string())
ErrorResponse::internal_error("Redis store required for search UI scoped keys")
})?;
// Try to get existing key
@ -334,7 +333,7 @@ async fn get_or_create_scoped_key(
// TODO: Create new scoped key via Meilisearch API
// For now, return an error indicating the key needs to be created
Err(MiroirError::Task(format!(
Err(ErrorResponse::internal_error(format!(
"scoped key for index '{}' needs to be created via background rotation",
index_uid
)))
@ -344,17 +343,17 @@ async fn get_or_create_scoped_key(
pub async fn get_config(
Path(index_uid): Path<String>,
State(state): State<AppState>,
) -> Result<impl IntoResponse, MiroirError> {
) -> Result<impl IntoResponse, ErrorResponse> {
let task_store = state
.task_store
.as_ref()
.ok_or_else(|| MiroirError::TaskStore("task store not available".to_string()))?;
.ok_or_else(|| ErrorResponse::internal_error("task store not available".to_string()))?;
// Try to load config from task store
if let Some(row) = task_store.get_search_ui_config(&index_uid)? {
// Parse the config JSON
let config: SearchUiIndexConfig = serde_json::from_str(&row.config_json)
.map_err(|e| MiroirError::Task(format!("failed to parse config: {}", e)))?;
.map_err(|e| ErrorResponse::internal_error(format!("failed to parse config: {}", e)))?;
return Ok(Json(config));
}
@ -380,12 +379,12 @@ pub async fn update_config(
Path(index_uid): Path<String>,
State(state): State<AppState>,
Json(config): Json<SearchUiIndexConfig>,
) -> Result<impl IntoResponse, MiroirError> {
) -> Result<impl IntoResponse, ErrorResponse> {
use miroir_core::task_store::NewSearchUiConfig;
// Serialize config to JSON for storage
let config_json = serde_json::to_string(&config)
.map_err(|e| MiroirError::Task(format!("failed to serialize config: {}", e)))?;
.map_err(|e| ErrorResponse::internal_error(format!("failed to serialize config: {}", e)))?;
// Validate custom template if present (plan §13.21)
if let Some(template) = &config.result_template {
@ -393,7 +392,7 @@ pub async fn update_config(
// Custom templates are stored separately in the config
// The actual template HTML is stored in the custom_template_html field
if config.custom_template_html.is_none() {
return Err(MiroirError::InvalidRequest(
return Err(ErrorResponse::invalid_request(
"custom template requires custom_template_html field".to_string(),
));
}
@ -407,7 +406,7 @@ pub async fn update_config(
let task_store = state
.task_store
.as_ref()
.ok_or_else(|| MiroirError::TaskStore("task store not available".to_string()))?;
.ok_or_else(|| ErrorResponse::internal_error("task store not available".to_string()))?;
let new_config = NewSearchUiConfig {
index_uid: index_uid.clone(),
@ -435,12 +434,12 @@ pub async fn beacon(
Path(index_uid): Path<String>,
State(state): State<AppState>,
Json(mut beacon): Json<BeaconRequest>,
) -> Result<impl IntoResponse, MiroirError> {
) -> Result<impl IntoResponse, ErrorResponse> {
let _ = &state.config;
// Validate event_id is present
if beacon.event_id.is_empty() {
return Err(MiroirError::InvalidRequest(
return Err(ErrorResponse::invalid_request(
"event_id is required".to_string(),
));
}
@ -462,22 +461,17 @@ pub async fn beacon(
}
/// Embedded static assets for the Search UI (plan §13.21).
#[derive(RustEmbed)]
#[folder = "../../static/search/"]
#[exclude = "*.swp"]
#[exclude = "*.DS_Store"]
#[exclude = ".git"]
struct SearchUiAssets;
#[derive(Embed)]
#[folder = "static/search/"]
pub struct SearchUiAssets;
/// Serve the Search UI SPA (plan §13.21).
pub async fn serve_spa(State(state): State<AppState>) -> Result<Response, MiroirError> {
pub async fn serve_spa(State(state): State<AppState>) -> Result<Response, ErrorResponse> {
let config = &state.config;
// Check if search UI is enabled
if !config.search_ui.enabled {
return Err(MiroirError::InvalidRequest(
"search UI is not enabled".to_string(),
));
return Err(ErrorResponse::invalid_request("search UI is not enabled"));
}
// Build CSP header (plan §9)
@ -496,17 +490,17 @@ pub async fn serve_spa(State(state): State<AppState>) -> Result<Response, Miroir
}
/// Serve a static asset from the Search UI (plan §13.21).
pub async fn serve_static_asset(Path(path): Path<String>) -> Result<Response, MiroirError> {
pub async fn serve_static_asset(Path(path): Path<String>) -> Result<Response, ErrorResponse> {
serve_embedded_file(&path, true)
.map_err(|_| MiroirError::InvalidRequest("asset not found".to_string()))
}
/// Serve an embedded file from the Search UI assets (plan §13.21).
fn serve_embedded_file(path: &str, is_static_asset: bool) -> Result<Response, StatusCode> {
fn serve_embedded_file(path: &str, is_static_asset: bool) -> Result<Response, ErrorResponse> {
use axum::body::Body;
use axum::http::header;
let asset = SearchUiAssets::get(path).ok_or(StatusCode::NOT_FOUND)?;
let asset = SearchUiAssets::get(path)
.ok_or_else(|| ErrorResponse::invalid_request("asset not found"))?;
let mime_type = mime_guess::from_path(path)
.first_or_octet_stream()
@ -543,7 +537,7 @@ fn serve_embedded_file(path: &str, is_static_asset: bool) -> Result<Response, St
/// - Properly closed {{ }} tags
/// - Valid {{#if}}...{{/if}} blocks
/// - No unmatched closing tags
fn validate_template(template: &str) -> Result<(), MiroirError> {
fn validate_template(template: &str) -> Result<(), ErrorResponse> {
let mut if_stack = Vec::new();
let mut pos = 0;
@ -561,7 +555,7 @@ fn validate_template(template: &str) -> Result<(), MiroirError> {
// Check for {{/if}} closing
else if tag.starts_with("/if") {
if if_stack.pop() != Some("if") {
return Err(MiroirError::InvalidRequest(
return Err(ErrorResponse::invalid_request(
"unmatched {{/if}} tag in template".to_string(),
));
}
@ -569,14 +563,14 @@ fn validate_template(template: &str) -> Result<(), MiroirError> {
pos += end + 2;
} else {
return Err(MiroirError::InvalidRequest(
return Err(ErrorResponse::invalid_request(
"unclosed {{ tag in template".to_string(),
));
}
}
if !if_stack.is_empty() {
return Err(MiroirError::InvalidRequest(format!(
return Err(ErrorResponse::invalid_request(format!(
"unclosed {{#if}} tag in template"
)));
}