P0 安全修复: - 修复 account update 自角色提升漏洞: 非 admin 用户更新自己时剥离 role 字段 - 添加 Admin 引导机制: accounts 表为空时自动从环境变量创建 super_admin P1 功能补全: - 所有 17 个 log_operation 调用点传入真实客户端 IP (ConnectInfo + X-Forwarded-For) - AuthContext 新增 client_ip 字段, middleware 层自动提取 - main.rs 使用 into_make_service_with_connect_info 启用 SocketAddr 注入 - 新增 PUT /api/v1/auth/password 密码修改端点 (验证旧密码 + argon2 哈希) - 桌面端 SaaS 设置页添加密码修改 UI (折叠式表单) - SaaSClient 添加 changePassword() 方法 - 集成测试修复: 注入模拟 ConnectInfo 适配 onshot 测试模式
87 lines
3.0 KiB
Rust
87 lines
3.0 KiB
Rust
//! ZCLAW SaaS 服务入口
|
||
|
||
use tracing::info;
|
||
use zclaw_saas::{config::SaaSConfig, db::init_db, state::AppState};
|
||
|
||
#[tokio::main]
|
||
async fn main() -> anyhow::Result<()> {
|
||
tracing_subscriber::fmt()
|
||
.with_env_filter(
|
||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||
.unwrap_or_else(|_| "zclaw_saas=debug,tower_http=debug".into()),
|
||
)
|
||
.init();
|
||
|
||
let config = SaaSConfig::load()?;
|
||
info!("SaaS config loaded: {}:{}", config.server.host, config.server.port);
|
||
|
||
let db = init_db(&config.database.url).await?;
|
||
info!("Database initialized");
|
||
|
||
let state = AppState::new(db, config.clone())?;
|
||
let app = build_router(state);
|
||
|
||
let listener = tokio::net::TcpListener::bind(format!("{}:{}", config.server.host, config.server.port))
|
||
.await?;
|
||
info!("SaaS server listening on {}:{}", config.server.host, config.server.port);
|
||
|
||
axum::serve(listener, app.into_make_service_with_connect_info::<std::net::SocketAddr>()).await?;
|
||
Ok(())
|
||
}
|
||
|
||
fn build_router(state: AppState) -> axum::Router {
|
||
use axum::middleware;
|
||
use tower_http::cors::{Any, CorsLayer};
|
||
use tower_http::trace::TraceLayer;
|
||
|
||
use axum::http::HeaderValue;
|
||
let cors = {
|
||
let config = state.config.blocking_read();
|
||
let is_dev = std::env::var("ZCLAW_SAAS_DEV")
|
||
.map(|v| v == "true" || v == "1")
|
||
.unwrap_or(false);
|
||
if config.server.cors_origins.is_empty() {
|
||
if is_dev {
|
||
CorsLayer::new()
|
||
.allow_origin(Any)
|
||
.allow_methods(Any)
|
||
.allow_headers(Any)
|
||
} else {
|
||
tracing::error!("生产环境必须配置 server.cors_origins,不能使用 allow_origin(Any)");
|
||
panic!("生产环境必须配置 server.cors_origins 白名单。开发环境可设置 ZCLAW_SAAS_DEV=true 绕过。");
|
||
}
|
||
} else {
|
||
let origins: Vec<HeaderValue> = config.server.cors_origins.iter()
|
||
.filter_map(|o: &String| o.parse::<HeaderValue>().ok())
|
||
.collect();
|
||
CorsLayer::new()
|
||
.allow_origin(origins)
|
||
.allow_methods(Any)
|
||
.allow_headers(Any)
|
||
}
|
||
};
|
||
|
||
let public_routes = zclaw_saas::auth::routes();
|
||
|
||
let protected_routes = zclaw_saas::auth::protected_routes()
|
||
.merge(zclaw_saas::account::routes())
|
||
.merge(zclaw_saas::model_config::routes())
|
||
.merge(zclaw_saas::relay::routes())
|
||
.merge(zclaw_saas::migration::routes())
|
||
.layer(middleware::from_fn_with_state(
|
||
state.clone(),
|
||
zclaw_saas::middleware::rate_limit_middleware,
|
||
))
|
||
.layer(middleware::from_fn_with_state(
|
||
state.clone(),
|
||
zclaw_saas::auth::auth_middleware,
|
||
));
|
||
|
||
axum::Router::new()
|
||
.merge(public_routes)
|
||
.merge(protected_routes)
|
||
.layer(TraceLayer::new_for_http())
|
||
.layer(cors)
|
||
.with_state(state)
|
||
}
|