use crate::middleware::request_context::inject_request_context; use crate::routes::build_router; use axum::Router; use std::env; use std::fs; use std::sync::Arc; use tower_http::trace::TraceLayer; #[derive(Debug, Clone)] pub struct AppConfig { pub service_name: String, pub service_version: String, pub bind_addr: String, pub enable_debug_shell_routes: bool, pub hermes_base_path: String, pub compat_next_base_path: String, pub convex_url: Option, pub convex_admin_key: Option, pub allow_dev_fixtures: bool, pub query_fixtures_json: Option, pub mutation_fixtures_json: Option, pub dev_user_id: String, pub dev_user_name: String, pub dev_user_email: String, } impl AppConfig { pub fn from_env() -> Self { Self { service_name: env::var("MNOTE_WEB_SERVICE_NAME").unwrap_or_else(|_| "mnote-web".into()), service_version: env::var("MNOTE_WEB_SERVICE_VERSION") .unwrap_or_else(|_| env!("CARGO_PKG_VERSION").into()), bind_addr: env::var("MNOTE_WEB_BIND").unwrap_or_else(|_| "127.0.0.1:0".into()), enable_debug_shell_routes: env::var("MNOTE_WEB_ENABLE_DEBUG_SHELL_ROUTES") .ok() .map(|value| matches!(value.trim(), "1" | "true" | "TRUE" | "yes" | "YES")) .unwrap_or(false), hermes_base_path: env::var("MNOTE_WEB_HERMES_BASE_PATH") .unwrap_or_else(|_| "/api/hermes".into()), compat_next_base_path: env::var("MNOTE_WEB_COMPAT_NEXT_BASE_PATH") .unwrap_or_else(|_| "/api/compat/next".into()), convex_url: env::var("CONVEX_SELF_HOSTED_URL") .ok() .or_else(|| env::var("NEXT_PUBLIC_CONVEX_URL").ok()) .or_else(|| read_env_or_dotenv("CONVEX_SELF_HOSTED_URL")) .or_else(|| read_env_or_dotenv("NEXT_PUBLIC_CONVEX_URL")) .map(|value| value.trim().trim_end_matches('/').to_string()) .filter(|value| !value.is_empty()), convex_admin_key: env::var("CONVEX_SELF_HOSTED_ADMIN_KEY") .ok() .or_else(|| read_env_or_dotenv("CONVEX_SELF_HOSTED_ADMIN_KEY")) .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()), allow_dev_fixtures: env::var("MNOTE_WEB_ALLOW_DEV_FIXTURES") .ok() .map(|value| matches!(value.trim(), "1" | "true" | "TRUE" | "yes" | "YES")) .unwrap_or(false), query_fixtures_json: env::var("MNOTE_WEB_QUERY_FIXTURES_JSON") .ok() .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()), mutation_fixtures_json: env::var("MNOTE_WEB_MUTATION_FIXTURES_JSON") .ok() .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()), dev_user_id: env::var("DEV_USER_ID") .ok() .or_else(|| read_env_or_dotenv("DEV_USER_ID")) .unwrap_or_else(|| "dev-user".into()), dev_user_name: env::var("DEV_USER_NAME") .ok() .or_else(|| read_env_or_dotenv("DEV_USER_NAME")) .unwrap_or_else(|| "开发用户".into()), dev_user_email: env::var("DEV_USER_EMAIL") .ok() .or_else(|| read_env_or_dotenv("DEV_USER_EMAIL")) .unwrap_or_else(|| "dev@mnote.local".into()), } } } fn read_env_or_dotenv(key: &str) -> Option { if let Ok(value) = env::var(key) { let trimmed = value.trim().trim_matches('"').to_string(); if !trimmed.is_empty() { return Some(trimmed); } } let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .join("../../..") .join(".env.all"); let content = fs::read_to_string(root).ok()?; for line in content.lines() { let line = line.trim_end_matches('\r'); if line.starts_with('#') || line.trim().is_empty() { continue; } let Some((k, v)) = line.split_once('=') else { continue; }; if k.trim() != key { continue; } let trimmed = v.trim().trim_matches('"').to_string(); if !trimmed.is_empty() { return Some(trimmed); } } None } #[derive(Debug, Clone)] pub struct AppState { config: Arc, } impl AppState { pub fn new(config: AppConfig) -> Self { Self { config: Arc::new(config), } } pub fn config(&self) -> &AppConfig { self.config.as_ref() } } pub fn build_app(state: AppState) -> Router { build_router(state) .layer(TraceLayer::new_for_http()) .layer(axum::middleware::from_fn(inject_request_context)) }