182 lines
6.6 KiB
Rust
182 lines
6.6 KiB
Rust
//! 配置迁移 HTTP 处理器
|
||
|
||
use axum::{
|
||
extract::{Extension, Path, Query, State},
|
||
http::StatusCode, Json,
|
||
};
|
||
use crate::state::AppState;
|
||
use crate::error::SaasResult;
|
||
use crate::auth::types::AuthContext;
|
||
use crate::auth::handlers::{check_permission, log_operation};
|
||
use crate::common::PaginatedResponse;
|
||
use super::{types::*, service};
|
||
|
||
/// GET /api/v1/config/items?category=xxx&source=xxx&page=1&page_size=20
|
||
pub async fn list_config_items(
|
||
State(state): State<AppState>,
|
||
Query(query): Query<ConfigQuery>,
|
||
_ctx: Extension<AuthContext>,
|
||
) -> SaasResult<Json<PaginatedResponse<ConfigItemInfo>>> {
|
||
let filter_query = ConfigQuery {
|
||
category: query.category.clone(),
|
||
source: query.source.clone(),
|
||
page: None,
|
||
page_size: None,
|
||
};
|
||
service::list_config_items(&state.db, &filter_query, query.page, query.page_size).await.map(Json)
|
||
}
|
||
|
||
/// GET /api/v1/config/items/:id
|
||
pub async fn get_config_item(
|
||
State(state): State<AppState>,
|
||
Path(id): Path<String>,
|
||
_ctx: Extension<AuthContext>,
|
||
) -> SaasResult<Json<ConfigItemInfo>> {
|
||
service::get_config_item(&state.db, &id).await.map(Json)
|
||
}
|
||
|
||
/// POST /api/v1/config/items (admin only)
|
||
pub async fn create_config_item(
|
||
State(state): State<AppState>,
|
||
Extension(ctx): Extension<AuthContext>,
|
||
Json(req): Json<CreateConfigItemRequest>,
|
||
) -> SaasResult<(StatusCode, Json<ConfigItemInfo>)> {
|
||
check_permission(&ctx, "config:write")?;
|
||
let item = service::create_config_item(&state.db, &req).await?;
|
||
log_operation(&state.db, &ctx.account_id, "config.create", "config_item", &item.id, None, ctx.client_ip.as_deref()).await?;
|
||
Ok((StatusCode::CREATED, Json(item)))
|
||
}
|
||
|
||
/// PATCH /api/v1/config/items/:id (admin only)
|
||
pub async fn update_config_item(
|
||
State(state): State<AppState>,
|
||
Path(id): Path<String>,
|
||
Extension(ctx): Extension<AuthContext>,
|
||
Json(req): Json<UpdateConfigItemRequest>,
|
||
) -> SaasResult<Json<ConfigItemInfo>> {
|
||
check_permission(&ctx, "config:write")?;
|
||
let item = service::update_config_item(&state.db, &id, &req).await?;
|
||
log_operation(&state.db, &ctx.account_id, "config.update", "config_item", &id, None, ctx.client_ip.as_deref()).await?;
|
||
Ok(Json(item))
|
||
}
|
||
|
||
/// DELETE /api/v1/config/items/:id (admin only)
|
||
pub async fn delete_config_item(
|
||
State(state): State<AppState>,
|
||
Path(id): Path<String>,
|
||
Extension(ctx): Extension<AuthContext>,
|
||
) -> SaasResult<Json<serde_json::Value>> {
|
||
check_permission(&ctx, "config:write")?;
|
||
service::delete_config_item(&state.db, &id).await?;
|
||
log_operation(&state.db, &ctx.account_id, "config.delete", "config_item", &id, None, ctx.client_ip.as_deref()).await?;
|
||
Ok(Json(serde_json::json!({"ok": true})))
|
||
}
|
||
|
||
/// GET /api/v1/config/analysis
|
||
pub async fn analyze_config(
|
||
State(state): State<AppState>,
|
||
_ctx: Extension<AuthContext>,
|
||
) -> SaasResult<Json<ConfigAnalysis>> {
|
||
service::analyze_config(&state.db).await.map(Json)
|
||
}
|
||
|
||
/// POST /api/v1/config/seed (admin only)
|
||
pub async fn seed_config(
|
||
State(state): State<AppState>,
|
||
Extension(ctx): Extension<AuthContext>,
|
||
) -> SaasResult<Json<serde_json::Value>> {
|
||
check_permission(&ctx, "config:write")?;
|
||
let count = service::seed_default_config_items(&state.db).await?;
|
||
log_operation(&state.db, &ctx.account_id, "config.seed", "config_item", "batch", Some(serde_json::json!({"count": count})), ctx.client_ip.as_deref()).await?;
|
||
Ok(Json(serde_json::json!({"created": count})))
|
||
}
|
||
|
||
/// POST /api/v1/config/sync (需要 config:write 权限)
|
||
pub async fn sync_config(
|
||
State(state): State<AppState>,
|
||
Extension(ctx): Extension<AuthContext>,
|
||
Json(req): Json<SyncConfigRequest>,
|
||
) -> SaasResult<Json<super::service::ConfigSyncResult>> {
|
||
// 权限检查:仅 config:write 可推送配置
|
||
check_permission(&ctx, "config:write")?;
|
||
|
||
let result = super::service::sync_config(&state.db, &ctx.account_id, &req).await?;
|
||
|
||
// 审计日志
|
||
log_operation(
|
||
&state.db,
|
||
&ctx.account_id,
|
||
"config.sync",
|
||
"config",
|
||
"batch",
|
||
Some(serde_json::json!({
|
||
"client_fingerprint": req.client_fingerprint,
|
||
"action": req.action,
|
||
"config_count": req.config_keys.len(),
|
||
})),
|
||
ctx.client_ip.as_deref(),
|
||
).await.ok();
|
||
|
||
Ok(Json(result))
|
||
}
|
||
|
||
/// POST /api/v1/config/diff
|
||
/// 计算客户端与 SaaS 端的配置差异 (不修改数据)
|
||
pub async fn config_diff(
|
||
State(state): State<AppState>,
|
||
Extension(ctx): Extension<AuthContext>,
|
||
Json(req): Json<SyncConfigRequest>,
|
||
) -> SaasResult<Json<ConfigDiffResponse>> {
|
||
// diff 操作虽然不修改数据,但涉及敏感配置信息,仍需认证用户
|
||
service::compute_config_diff(&state.db, &req).await.map(Json)
|
||
}
|
||
|
||
/// GET /api/v1/config/sync-logs?page=1&page_size=20
|
||
pub async fn list_sync_logs(
|
||
State(state): State<AppState>,
|
||
Extension(ctx): Extension<AuthContext>,
|
||
Query(params): Query<std::collections::HashMap<String, String>>,
|
||
) -> SaasResult<Json<crate::common::PaginatedResponse<ConfigSyncLogInfo>>> {
|
||
let page: u32 = params.get("page").and_then(|v| v.parse().ok()).unwrap_or(1).max(1);
|
||
let page_size: u32 = params.get("page_size").and_then(|v| v.parse().ok()).unwrap_or(20).min(100);
|
||
service::list_sync_logs(&state.db, &ctx.account_id, page, page_size).await.map(Json)
|
||
}
|
||
|
||
/// GET /api/v1/config/pull?since=2026-03-28T00:00:00Z
|
||
/// 批量拉取配置(供桌面端启动时一次性拉取)
|
||
/// 返回扁平的 key-value map,可选 since 参数过滤仅返回该时间之后更新的配置
|
||
pub async fn pull_config(
|
||
State(state): State<AppState>,
|
||
_ctx: Extension<AuthContext>,
|
||
Query(params): Query<std::collections::HashMap<String, String>>,
|
||
) -> SaasResult<Json<serde_json::Value>> {
|
||
let since = params.get("since").cloned();
|
||
let items = service::fetch_all_config_items(
|
||
&state.db,
|
||
&ConfigQuery { category: None, source: None, page: None, page_size: None },
|
||
).await?;
|
||
|
||
let mut configs: Vec<serde_json::Value> = Vec::new();
|
||
for item in items {
|
||
// 如果指定了 since,只返回 updated_at > since 的配置
|
||
if let Some(ref since_val) = since {
|
||
if item.updated_at <= *since_val {
|
||
continue;
|
||
}
|
||
}
|
||
configs.push(serde_json::json!({
|
||
"key": item.key_path,
|
||
"category": item.category,
|
||
"value": item.current_value,
|
||
"value_type": item.value_type,
|
||
"default": item.default_value,
|
||
"updated_at": item.updated_at,
|
||
}));
|
||
}
|
||
|
||
Ok(Json(serde_json::json!({
|
||
"configs": configs,
|
||
"pulled_at": chrono::Utc::now().to_rfc3339(),
|
||
})))
|
||
}
|