DCTS/crates/server/src/api/admin.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

155 lines
5.7 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.

//! 管理 APIAdmin 角色)。
//!
//! 提供 node 凭据的可视化与运维操作,供 Dashboard 管理界面调用:
//! - 列出所有节点及其凭据状态(在线/token 是否有效/吊销/颁发时间)
//! - 吊销指定节点的专属 token立即失效不影响其他节点
//! - 重新颁发指定节点的专属 token返回新明文旧 token 失效)
//!
//! 这些端点均要求 Admin 角色(见 mod.rs 授权矩阵node 自身无权操作他人或自身凭据,
//! 从而保证「吊销/重发」是管理员主动行为,避免被攻陷节点篡改凭据体系。
use super::{is_valid_node_id, AppState};
use axum::{
extract::{Path as AxumPath, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use serde_json::json;
use tracing::{info, warn};
/// GET /api/admin/nodes — 列出全部节点及凭据状态。
pub async fn list_nodes(
State(state): State<AppState>,
) -> Result<impl IntoResponse, crate::api::AppError> {
match state.db.list_nodes_with_credentials().await {
Ok(list) => Ok((
StatusCode::OK,
Json(json!({ "success": true, "message": "成功获取节点列表", "data": list })),
)),
Err(e) => Err(e.into()),
}
}
/// POST /api/admin/nodes/:node_id/revoke — 吊销指定节点的专属 token。
///
/// 吊销后该 node 的现有 token 立即失效,须重新走注册流程领取新 token。
/// 操作幂等:对无凭据记录或已吊销的节点调用不会报错。
pub async fn revoke_node(
State(state): State<AppState>,
AxumPath(node_id): AxumPath<String>,
) -> Result<impl IntoResponse, crate::api::AppError> {
// node_id 白名单校验,防止注入或异常输入(与 register_node 的 node_id 来源口径一致)
if !is_valid_node_id(&node_id) {
return Err(crate::api::AppError::BadRequest(
"非法的节点 ID 参数".to_string(),
));
}
match state.db.revoke_node_token(&node_id).await {
Ok(_) => {
info!("管理员已吊销节点 {} 的专属 token", node_id);
Ok((
StatusCode::OK,
Json(
json!({ "success": true, "message": format!("节点 '{}' 的 token 已吊销", node_id) }),
),
))
}
Err(e) => {
warn!("吊销节点 {} token 失败: {}", node_id, e);
Err(e.into())
}
}
}
/// POST /api/admin/nodes/:node_id/reissue — 重新颁发指定节点的专属 token。
///
/// 旧 token 立即失效,返回新 token 明文仅此一次DB 只存 hash
/// 节点需用新 token 重新注册或由管理员手动同步到节点本地 `.node_token`。
pub async fn reissue_node(
State(state): State<AppState>,
AxumPath(node_id): AxumPath<String>,
) -> Result<impl IntoResponse, crate::api::AppError> {
if !is_valid_node_id(&node_id) {
return Err(crate::api::AppError::BadRequest(
"非法的节点 ID 参数".to_string(),
));
}
// 仅允许对已注册的节点重发 token防止凭据表被写入幽灵 node_id
match state.db.get_node_exists(&node_id).await {
Ok(false) => {
return Err(crate::api::AppError::NotFound(format!(
"节点 '{}' 不存在,请先注册",
node_id
)));
}
Ok(true) => {}
Err(e) => return Err(e.into()),
}
match state.db.issue_node_token(&node_id).await {
Ok(new_token) => {
info!("管理员已为节点 {} 重新颁发专属 token", node_id);
Ok((
StatusCode::OK,
Json(json!({
"success": true,
"message": format!("节点 '{}' 的 token 已重新颁发,请将新 token 同步到该节点", node_id),
"node_token": new_token,
})),
))
}
Err(e) => {
warn!("为节点 {} 重新颁发 token 失败: {}", node_id, e);
Err(e.into())
}
}
}
/// POST /api/admin/nodes/:node_id/approve — 管理员同意节点接入申请。
pub async fn approve_node(
State(state): State<AppState>,
AxumPath(node_id): AxumPath<String>,
) -> Result<impl IntoResponse, crate::api::AppError> {
if !is_valid_node_id(&node_id) {
return Err(crate::api::AppError::BadRequest(
"非法的节点 ID 参数".to_string(),
));
}
match state.db.approve_node(&node_id).await {
Ok(_token) => {
info!("管理员已同意节点 {} 的接入申请并生成专属 Token", node_id);
Ok((
StatusCode::OK,
Json(
json!({ "success": true, "message": format!("节点 '{}' 已授权加入集群", node_id) }),
),
))
}
Err(e) => Err(e.into()),
}
}
/// POST /api/admin/nodes/:node_id/reject — 管理员拒绝节点接入申请。
pub async fn reject_node(
State(state): State<AppState>,
AxumPath(node_id): AxumPath<String>,
) -> Result<impl IntoResponse, crate::api::AppError> {
if !is_valid_node_id(&node_id) {
return Err(crate::api::AppError::BadRequest(
"非法的节点 ID 参数".to_string(),
));
}
match state.db.reject_node(&node_id).await {
Ok(_) => {
info!("管理员已拒绝节点 {} 的接入申请并移除", node_id);
Ok((
StatusCode::OK,
Json(
json!({ "success": true, "message": format!("已拒绝节点 '{}' 的接入申请", node_id) }),
),
))
}
Err(e) => Err(e.into()),
}
}