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:
227
src/dashboard/clients.rs
Normal file
227
src/dashboard/clients.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::Json,
|
||||
};
|
||||
use chrono;
|
||||
use serde::Deserialize;
|
||||
use serde_json;
|
||||
use sqlx::Row;
|
||||
use tracing::warn;
|
||||
use uuid;
|
||||
|
||||
use super::{ApiResponse, DashboardState};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(super) struct CreateClientRequest {
|
||||
pub(super) name: String,
|
||||
pub(super) client_id: Option<String>,
|
||||
}
|
||||
|
||||
pub(super) async fn handle_get_clients(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
|
||||
let pool = &state.app_state.db_pool;
|
||||
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
client_id as id,
|
||||
name,
|
||||
created_at,
|
||||
total_requests,
|
||||
total_tokens,
|
||||
total_cost,
|
||||
is_active
|
||||
FROM clients
|
||||
ORDER BY created_at DESC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(rows) => {
|
||||
let clients: Vec<serde_json::Value> = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
serde_json::json!({
|
||||
"id": row.get::<String, _>("id"),
|
||||
"name": row.get::<Option<String>, _>("name").unwrap_or_else(|| "Unnamed".to_string()),
|
||||
"created_at": row.get::<chrono::DateTime<chrono::Utc>, _>("created_at"),
|
||||
"requests_count": row.get::<i64, _>("total_requests"),
|
||||
"total_tokens": row.get::<i64, _>("total_tokens"),
|
||||
"total_cost": row.get::<f64, _>("total_cost"),
|
||||
"status": if row.get::<bool, _>("is_active") { "active" } else { "inactive" },
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Json(ApiResponse::success(serde_json::json!(clients)))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to fetch clients: {}", e);
|
||||
Json(ApiResponse::error("Failed to fetch clients".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn handle_create_client(
|
||||
State(state): State<DashboardState>,
|
||||
Json(payload): Json<CreateClientRequest>,
|
||||
) -> Json<ApiResponse<serde_json::Value>> {
|
||||
let pool = &state.app_state.db_pool;
|
||||
|
||||
let client_id = payload
|
||||
.client_id
|
||||
.unwrap_or_else(|| format!("client-{}", &uuid::Uuid::new_v4().to_string()[..8]));
|
||||
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO clients (client_id, name, is_active)
|
||||
VALUES (?, ?, TRUE)
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(&client_id)
|
||||
.bind(&payload.name)
|
||||
.fetch_one(pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(row) => Json(ApiResponse::success(serde_json::json!({
|
||||
"id": row.get::<String, _>("client_id"),
|
||||
"name": row.get::<Option<String>, _>("name"),
|
||||
"created_at": row.get::<chrono::DateTime<chrono::Utc>, _>("created_at"),
|
||||
"status": "active",
|
||||
}))),
|
||||
Err(e) => {
|
||||
warn!("Failed to create client: {}", e);
|
||||
Json(ApiResponse::error(format!("Failed to create client: {}", e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn handle_get_client(
|
||||
State(state): State<DashboardState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Json<ApiResponse<serde_json::Value>> {
|
||||
let pool = &state.app_state.db_pool;
|
||||
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
c.client_id as id,
|
||||
c.name,
|
||||
c.is_active,
|
||||
c.created_at,
|
||||
COALESCE(c.total_tokens, 0) as total_tokens,
|
||||
COALESCE(c.total_cost, 0.0) as total_cost,
|
||||
COUNT(r.id) as total_requests,
|
||||
MAX(r.timestamp) as last_request
|
||||
FROM clients c
|
||||
LEFT JOIN llm_requests r ON c.client_id = r.client_id
|
||||
WHERE c.client_id = ?
|
||||
GROUP BY c.client_id
|
||||
"#,
|
||||
)
|
||||
.bind(&id)
|
||||
.fetch_optional(pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(Some(row)) => Json(ApiResponse::success(serde_json::json!({
|
||||
"id": row.get::<String, _>("id"),
|
||||
"name": row.get::<Option<String>, _>("name").unwrap_or_else(|| "Unnamed".to_string()),
|
||||
"is_active": row.get::<bool, _>("is_active"),
|
||||
"created_at": row.get::<chrono::DateTime<chrono::Utc>, _>("created_at"),
|
||||
"total_tokens": row.get::<i64, _>("total_tokens"),
|
||||
"total_cost": row.get::<f64, _>("total_cost"),
|
||||
"total_requests": row.get::<i64, _>("total_requests"),
|
||||
"last_request": row.get::<Option<chrono::DateTime<chrono::Utc>>, _>("last_request"),
|
||||
"status": if row.get::<bool, _>("is_active") { "active" } else { "inactive" },
|
||||
}))),
|
||||
Ok(None) => Json(ApiResponse::error(format!("Client '{}' not found", id))),
|
||||
Err(e) => {
|
||||
warn!("Failed to fetch client: {}", e);
|
||||
Json(ApiResponse::error(format!("Failed to fetch client: {}", e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn handle_delete_client(
|
||||
State(state): State<DashboardState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Json<ApiResponse<serde_json::Value>> {
|
||||
let pool = &state.app_state.db_pool;
|
||||
|
||||
// Don't allow deleting the default client
|
||||
if id == "default" {
|
||||
return Json(ApiResponse::error("Cannot delete default client".to_string()));
|
||||
}
|
||||
|
||||
let result = sqlx::query("DELETE FROM clients WHERE client_id = ?")
|
||||
.bind(id)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => Json(ApiResponse::success(serde_json::json!({ "message": "Client deleted" }))),
|
||||
Err(e) => Json(ApiResponse::error(format!("Failed to delete client: {}", e))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn handle_client_usage(
|
||||
State(state): State<DashboardState>,
|
||||
Path(id): Path<String>,
|
||||
) -> Json<ApiResponse<serde_json::Value>> {
|
||||
let pool = &state.app_state.db_pool;
|
||||
|
||||
// Get per-model breakdown for this client
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
model,
|
||||
provider,
|
||||
COUNT(*) as request_count,
|
||||
SUM(prompt_tokens) as prompt_tokens,
|
||||
SUM(completion_tokens) as completion_tokens,
|
||||
SUM(total_tokens) as total_tokens,
|
||||
SUM(cost) as total_cost,
|
||||
AVG(duration_ms) as avg_duration_ms
|
||||
FROM llm_requests
|
||||
WHERE client_id = ?
|
||||
GROUP BY model, provider
|
||||
ORDER BY total_cost DESC
|
||||
"#,
|
||||
)
|
||||
.bind(&id)
|
||||
.fetch_all(pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(rows) => {
|
||||
let breakdown: Vec<serde_json::Value> = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
serde_json::json!({
|
||||
"model": row.get::<String, _>("model"),
|
||||
"provider": row.get::<String, _>("provider"),
|
||||
"request_count": row.get::<i64, _>("request_count"),
|
||||
"prompt_tokens": row.get::<i64, _>("prompt_tokens"),
|
||||
"completion_tokens": row.get::<i64, _>("completion_tokens"),
|
||||
"total_tokens": row.get::<i64, _>("total_tokens"),
|
||||
"total_cost": row.get::<f64, _>("total_cost"),
|
||||
"avg_duration_ms": row.get::<f64, _>("avg_duration_ms"),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Json(ApiResponse::success(serde_json::json!({
|
||||
"client_id": id,
|
||||
"breakdown": breakdown,
|
||||
})))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to fetch client usage: {}", e);
|
||||
Json(ApiResponse::error(format!("Failed to fetch client usage: {}", e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user