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:
@@ -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()),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user