- 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 配置及数据库设计文档
155 lines
5.7 KiB
Rust
155 lines
5.7 KiB
Rust
//! 管理 API(Admin 角色)。
|
||
//!
|
||
//! 提供 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()),
|
||
}
|
||
}
|