DCTS/crates/server/src/api/node.rs
Asfmq b91f1e4fa5 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 配置及数据库设计文档
2026-07-28 21:54:02 +08:00

153 lines
5.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>,
) -> 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(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>,
) -> 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(_) => Ok(Json(json!({"status": "ok"}))),
Err(e) => Err(e.into()),
}
}