refactor: comprehensive audit — fix bugs, harden security, deduplicate providers, add CI/Docker
Phase 1: Fix compilation (config_path Option<PathBuf>, streaming test, stale test cleanup) Phase 2: Fix critical bugs (remove block_on deadlocks in 4 providers, fix broken SQL query builder) Phase 3: Security hardening (session manager, real auth, token masking, Gemini key to header, password policy) Phase 4: Implement stubs (real provider test, /proc health metrics, client/provider/backup endpoints, has_images) Phase 5: Code quality (shared provider helpers, explicit re-exports, all Clippy warnings fixed, unwrap removal, 6 unused deps removed, dashboard split into 7 sub-modules) Phase 6: Infrastructure (GitHub Actions CI, multi-stage Dockerfile, rustfmt.toml, clippy.toml, script fixes)
This commit is contained in:
@@ -95,14 +95,14 @@ pub struct AppConfig {
|
||||
pub providers: ProviderConfig,
|
||||
pub model_mapping: ModelMappingConfig,
|
||||
pub pricing: PricingConfig,
|
||||
pub config_path: PathBuf,
|
||||
pub config_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
pub async fn load() -> Result<Arc<Self>> {
|
||||
Self::load_from_path(None).await
|
||||
}
|
||||
|
||||
|
||||
/// Load configuration from a specific path (for testing)
|
||||
pub async fn load_from_path(config_path: Option<PathBuf>) -> Result<Arc<Self>> {
|
||||
// Load configuration from multiple sources
|
||||
@@ -120,7 +120,10 @@ impl AppConfig {
|
||||
.set_default("providers.openai.default_model", "gpt-4o")?
|
||||
.set_default("providers.openai.enabled", true)?
|
||||
.set_default("providers.gemini.api_key_env", "GEMINI_API_KEY")?
|
||||
.set_default("providers.gemini.base_url", "https://generativelanguage.googleapis.com/v1")?
|
||||
.set_default(
|
||||
"providers.gemini.base_url",
|
||||
"https://generativelanguage.googleapis.com/v1",
|
||||
)?
|
||||
.set_default("providers.gemini.default_model", "gemini-2.0-flash")?
|
||||
.set_default("providers.gemini.enabled", true)?
|
||||
.set_default("providers.deepseek.api_key_env", "DEEPSEEK_API_KEY")?
|
||||
@@ -136,7 +139,11 @@ impl AppConfig {
|
||||
.set_default("providers.ollama.models", Vec::<String>::new())?;
|
||||
|
||||
// Load from config file if exists
|
||||
let config_path = config_path.unwrap_or_else(|| std::env::current_dir().unwrap().join("config.toml"));
|
||||
let config_path = config_path.unwrap_or_else(|| {
|
||||
std::env::current_dir()
|
||||
.unwrap_or_else(|_| PathBuf::from("."))
|
||||
.join("config.toml")
|
||||
});
|
||||
if config_path.exists() {
|
||||
config_builder = config_builder.add_source(File::from(config_path.clone()).format(FileFormat::Toml));
|
||||
}
|
||||
@@ -157,7 +164,7 @@ impl AppConfig {
|
||||
let server: ServerConfig = config.get("server")?;
|
||||
let database: DatabaseConfig = config.get("database")?;
|
||||
let providers: ProviderConfig = config.get("providers")?;
|
||||
|
||||
|
||||
// For now, use empty model mapping and pricing (will be populated later)
|
||||
let model_mapping = ModelMappingConfig { patterns: vec![] };
|
||||
let pricing = PricingConfig {
|
||||
@@ -174,7 +181,7 @@ impl AppConfig {
|
||||
providers,
|
||||
model_mapping,
|
||||
pricing,
|
||||
config_path,
|
||||
config_path: Some(config_path),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -187,48 +194,46 @@ impl AppConfig {
|
||||
_ => return Err(anyhow::anyhow!("Unknown provider: {}", provider)),
|
||||
};
|
||||
|
||||
std::env::var(env_var)
|
||||
.map_err(|_| anyhow::anyhow!("Environment variable {} not set for {}", env_var, provider))
|
||||
}
|
||||
std::env::var(env_var).map_err(|_| anyhow::anyhow!("Environment variable {} not set for {}", env_var, provider))
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to deserialize a Vec<String> from either a sequence or a comma-separated string
|
||||
fn deserialize_vec_or_string<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct VecOrString;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for VecOrString {
|
||||
type Value = Vec<String>;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a sequence or a comma-separated string")
|
||||
}
|
||||
|
||||
/// Helper function to deserialize a Vec<String> from either a sequence or a comma-separated string
|
||||
fn deserialize_vec_or_string<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
E: serde::de::Error,
|
||||
{
|
||||
struct VecOrString;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for VecOrString {
|
||||
type Value = Vec<String>;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a sequence or a comma-separated string")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(value
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
|
||||
where
|
||||
S: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
let mut vec = Vec::new();
|
||||
while let Some(element) = seq.next_element()? {
|
||||
vec.push(element);
|
||||
}
|
||||
Ok(vec)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(VecOrString)
|
||||
Ok(value
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
|
||||
fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
|
||||
where
|
||||
S: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
let mut vec = Vec::new();
|
||||
while let Some(element) = seq.next_element()? {
|
||||
vec.push(element);
|
||||
}
|
||||
Ok(vec)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(VecOrString)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user