//! 管理 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, ) -> Result { 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, AxumPath(node_id): AxumPath, ) -> Result { // 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, AxumPath(node_id): AxumPath, ) -> Result { 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, AxumPath(node_id): AxumPath, ) -> Result { 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, AxumPath(node_id): AxumPath, ) -> Result { 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()), } }