feat(server,dashboard): 引入多工作流数据隔离、安全中间件与前端 ESM 模块化重构

- server: 实现按 workflow_name 的多工作流数据隔离与旧数据库平滑迁移机制
- server: 新增 API Key 认证(auth)、限流中间件(rate_limit)与运维备份接口(admin)
- server: 统一 AppError 错误处理体系,重构调度器 scheduler 支持工作流级重置与抢占
- node: 节点 ID 缺失时自动生成随机 UUID,原生支持 `docker compose --scale node=N` 动态扩容
- dashboard: 前端模块化重构(state/api/components),升级 CSS 变量设计系统与 Toast 通知
- docker/docs: 更新 /healthz 健康检查、部署脚本 IP 配置及数据库设计文档
This commit is contained in:
fmq
2026-07-28 21:54:02 +08:00
parent 4b4238d702
commit b91f1e4fa5
59 changed files with 7697 additions and 1490 deletions
+135 -8
View File
@@ -1,25 +1,152 @@
use super::AppState;
use axum::{extract::State, response::IntoResponse, Json};
use super::{is_valid_host_name, is_valid_node_id, AppState, AuthenticatedNode};
use axum::{
extract::{Extension, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use common::models::{NodeHeartbeatRequest, NodeRegisterRequest};
use serde_json::json;
use tracing::{info, warn};
pub async fn register_node(
State(state): State<AppState>,
auth_node: Option<Extension<AuthenticatedNode>>,
Json(req): Json<NodeRegisterRequest>,
) -> impl IntoResponse {
) -> Result<impl IntoResponse, crate::api::AppError> {
// 入口白名单校验
if !is_valid_node_id(&req.node_id) {
return Err(crate::api::AppError::BadRequest(
"非法的节点 ID(仅允许字母、数字、点、下划线、连字符,长度 1-128)".to_string(),
));
}
if !is_valid_host_name(&req.host_name) {
return Err(crate::api::AppError::BadRequest(
"非法的主机名(仅允许可打印 ASCII,长度 1-128".to_string(),
));
}
// 已认证已拿到 Token 的节点刷新元数据配置
if let Some(Extension(ref auth)) = auth_node {
if auth.node_id == req.node_id {
let _ = state.db.register_node(&req).await;
info!("已授权节点 {} 刷新配置成功", req.node_id);
return Ok((
StatusCode::OK,
Json(json!({
"status": "approved",
"message": "节点配置更新成功",
"node_token": null,
})),
));
}
}
// 申请注册新节点(免凭据提交申请,进入 pending_approval 状态)
match state.db.register_node(&req).await {
Ok(_) => Json(json!({"status": "ok", "message": "节点注册成功"})),
Err(e) => Json(json!({"status": "error", "message": e.to_string()})),
Ok(true) => {
info!(
"接收到新节点 {} 的注册申请,已加入待审批 (pending_approval) 队列",
req.node_id
);
Ok((
StatusCode::OK,
Json(json!({
"status": "pending_approval",
"message": "节点注册申请已成功提交!请在管理 Dashboard 控制台上点击【同意接入】授权该节点",
"node_token": null,
})),
))
}
Ok(false) => {
// 节点已处于待审批或已存在列表
Ok((
StatusCode::OK,
Json(json!({
"status": "pending_approval",
"message": "节点注册申请等待管理员审批中",
"node_token": null,
})),
))
}
Err(e) => Err(e.into()),
}
}
#[derive(serde::Deserialize)]
pub struct CheckNodeStatusRequest {
pub node_id: String,
}
/// POST /api/node/check_status — Node 端轮询检查审批结果。
pub async fn check_node_status(
State(state): State<AppState>,
Json(req): Json<CheckNodeStatusRequest>,
) -> Result<impl IntoResponse, crate::api::AppError> {
if !is_valid_node_id(&req.node_id) {
return Err(crate::api::AppError::BadRequest(
"非法的节点 ID 参数".to_string(),
));
}
// 尝试拉取取走即焚的暂存明文 Token
match state.db.take_pending_node_token(&req.node_id).await {
Ok(Some(raw_token)) => {
info!(
"节点 {} 的注册申请已被管理员审批同意,下发专属 Token",
req.node_id
);
Ok((
StatusCode::OK,
Json(json!({
"status": "approved",
"message": "节点已通过审批授权",
"node_token": raw_token,
})),
))
}
Ok(None) | Err(_) => {
// 查节点表状态
match state.db.get_node_exists(&req.node_id).await {
Ok(true) => Ok((
StatusCode::OK,
Json(json!({
"status": "pending_approval",
"message": "等待管理员在控制台点击同意",
"node_token": null,
})),
)),
_ => Ok((
StatusCode::OK,
Json(json!({
"status": "rejected",
"message": "节点注册申请未通过或已被移除",
"node_token": null,
})),
)),
}
}
}
}
pub async fn heartbeat_node(
State(state): State<AppState>,
Extension(auth_node): Extension<AuthenticatedNode>,
Json(req): Json<NodeHeartbeatRequest>,
) -> impl IntoResponse {
) -> Result<impl IntoResponse, crate::api::AppError> {
// 身份绑定校验:请求体声称的 node_id 必须与 token 绑定的 node_id 一致,
// 杜绝「持有 A 节点 token 却冒充 B 节点发心跳」的跨节点越权。
if req.node_id != auth_node.node_id {
warn!(
"节点心跳身份校验失败:token 绑定 node={},但请求体声称 node_id={}",
auth_node.node_id, req.node_id
);
return Err(crate::api::AppError::Forbidden(
"node_id 与凭据不匹配".to_string(),
));
}
match state.db.heartbeat_node(&req).await {
Ok(_) => Json(json!({"status": "ok"})),
Err(e) => Json(json!({"status": "error", "message": e.to_string()})),
Ok(_) => Ok(Json(json!({"status": "ok"}))),
Err(e) => Err(e.into()),
}
}