- 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 配置及数据库设计文档
153 lines
5.3 KiB
Rust
153 lines
5.3 KiB
Rust
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()),
|
||
}
|
||
}
|