拆分大文件

This commit is contained in:
2026-04-23 23:38:00 +08:00
parent 53a9cdd791
commit 8df502b2a7
506 changed files with 11312 additions and 13069 deletions

View File

@@ -1,4 +1,7 @@
use std::collections::BTreeSet;
use std::{
collections::BTreeSet,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
};
use axum::{
Json,
@@ -21,10 +24,13 @@ use shared_contracts::admin::{
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
use crate::{
api_response::json_success_body, http_error::AppError, request_context::RequestContext,
api_response::json_success_body,
http_error::AppError,
request_context::RequestContext,
state::{AdminRuntime, AppState},
};
// 首版调试台只允许有限大小的请求体,避免把后台当作通用代理大包转发器。
const MAX_DEBUG_BODY_BYTES: usize = 128 * 1024;
const BLOCKED_DEBUG_HEADERS: &[&str] = &[
"host",
@@ -33,6 +39,7 @@ const BLOCKED_DEBUG_HEADERS: &[&str] = &[
"transfer-encoding",
"expect",
];
// 数据库概览首版只统计受控白名单表,禁止后台页面直接输入任意 SQL。
const DATABASE_OVERVIEW_TABLES: &[&str] = &[
"runtime_setting",
"runtime_snapshot",
@@ -124,9 +131,9 @@ pub async fn admin_login(
Extension(request_context): Extension<RequestContext>,
Json(payload): Json<AdminLoginRequest>,
) -> Result<Json<Value>, AppError> {
let runtime = state
.admin_runtime()
.ok_or_else(|| AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_message("后台管理未启用"))?;
let runtime = state.admin_runtime().ok_or_else(|| {
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_message("后台管理未启用")
})?;
let expected_username = runtime.username().trim();
let expected_password = runtime.password().trim();
@@ -139,16 +146,18 @@ pub async fn admin_login(
}
if submitted_username != expected_username || submitted_password != expected_password {
return Err(AppError::from_status(StatusCode::UNAUTHORIZED).with_message("管理员用户名或密码错误"));
return Err(
AppError::from_status(StatusCode::UNAUTHORIZED).with_message("管理员用户名或密码错误")
);
}
let now = OffsetDateTime::now_utc();
let claims = runtime
.build_claims(now)
.map_err(|error| AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error))?;
let token = runtime
.sign_token(&claims)
.map_err(|error| AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error))?;
let claims = runtime.build_claims(now).map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error)
})?;
let token = runtime.sign_token(&claims).map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error)
})?;
Ok(json_success_body(
Some(&request_context),
@@ -176,9 +185,9 @@ pub async fn admin_overview(
Extension(request_context): Extension<RequestContext>,
Extension(_admin): Extension<AuthenticatedAdmin>,
) -> Result<Json<Value>, AppError> {
let runtime = state
.admin_runtime()
.ok_or_else(|| AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_message("后台管理未启用"))?;
let runtime = state.admin_runtime().ok_or_else(|| {
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_message("后台管理未启用")
})?;
let overview = build_admin_overview(&state, runtime).await?;
Ok(json_success_body(Some(&request_context), overview))
@@ -199,9 +208,10 @@ pub async fn require_admin_auth(
mut request: Request,
next: Next,
) -> Result<Response, AppError> {
let runtime = state
.admin_runtime()
.ok_or_else(|| AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_message("后台管理未启用"))?;
// 后台鉴权必须同时满足令牌验签通过、主体匹配当前管理员、roles 含 admin。
let runtime = state.admin_runtime().ok_or_else(|| {
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_message("后台管理未启用")
})?;
let bearer_token = extract_bearer_token(request.headers())?;
let claims = runtime
.verify_token(&bearer_token)
@@ -213,7 +223,9 @@ pub async fn require_admin_auth(
request
.extensions_mut()
.insert(AuthenticatedAdmin::new(build_admin_session_payload(admin_session)));
.insert(AuthenticatedAdmin::new(build_admin_session_payload(
admin_session,
)));
Ok(next.run(request).await)
}
@@ -252,10 +264,16 @@ async fn build_admin_overview(
}
async fn fetch_database_overview(state: &AppState) -> AdminDatabaseOverviewPayload {
// 概览直接读取 SpacetimeDB HTTP API保证后台看到的是真实数据库元信息而不是本地缓存。
let client = Client::new();
let server_root = state.config.spacetime_server_url.trim_end_matches('/');
let database = state.config.spacetime_database.trim();
let token = state.config.spacetime_token.as_deref().map(str::trim).filter(|value| !value.is_empty());
let token = state
.config
.spacetime_token
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty());
let mut fetch_errors = Vec::new();
let database_info = fetch_spacetime_json::<SpacetimeDatabaseInfoResponse>(
@@ -321,9 +339,15 @@ async fn fetch_database_overview(state: &AppState) -> AdminDatabaseOverviewPaylo
schema_table_names.sort();
AdminDatabaseOverviewPayload {
database_identity: database_info.as_ref().and_then(|value| value.database_identity.clone()),
owner_identity: database_info.as_ref().and_then(|value| value.owner_identity.clone()),
host_type: database_info.as_ref().and_then(|value| value.host_type.clone()),
database_identity: database_info
.as_ref()
.and_then(|value| value.database_identity.clone()),
owner_identity: database_info
.as_ref()
.and_then(|value| value.owner_identity.clone()),
host_type: database_info
.as_ref()
.and_then(|value| value.host_type.clone()),
schema_table_names,
table_stats,
fetch_errors,
@@ -426,15 +450,12 @@ async fn execute_admin_debug_http(
state: &AppState,
payload: AdminDebugHttpRequest,
) -> Result<AdminDebugHttpResponse, AppError> {
// 调试请求始终回打当前 api-server同源受控不允许作为外部代理使用。
let method = Method::from_bytes(payload.method.trim().as_bytes()).map_err(|_| {
AppError::from_status(StatusCode::BAD_REQUEST).with_message("HTTP 方法不合法")
})?;
let path = normalize_debug_path(&payload.path)?;
let base_url = format!(
"http://{}:{}",
state.config.bind_host.trim(),
state.config.bind_port
);
let base_url = build_debug_base_url(&state.config.bind_host, state.config.bind_port);
let target_url = format!("{base_url}{path}");
let body_text = payload.body.unwrap_or_default();
if body_text.len() > MAX_DEBUG_BODY_BYTES {
@@ -451,7 +472,10 @@ async fn execute_admin_debug_http(
for header in payload.headers.unwrap_or_default() {
let header_name = header.name.trim().to_ascii_lowercase();
if BLOCKED_DEBUG_HEADERS.iter().any(|blocked| *blocked == header_name) {
if BLOCKED_DEBUG_HEADERS
.iter()
.any(|blocked| *blocked == header_name)
{
continue;
}
let name = HeaderName::from_bytes(header_name.as_bytes()).map_err(|_| {
@@ -464,7 +488,8 @@ async fn execute_admin_debug_http(
}
let response = request.send().await.map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_message(format!("调试请求失败:{error}"))
AppError::from_status(StatusCode::BAD_GATEWAY)
.with_message(format!("调试请求失败:{error}"))
})?;
let status = response.status();
let headers = response
@@ -476,7 +501,8 @@ async fn execute_admin_debug_http(
})
.collect::<Vec<_>>();
let response_body = response.bytes().await.map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_message(format!("调试响应读取失败:{error}"))
AppError::from_status(StatusCode::BAD_GATEWAY)
.with_message(format!("调试响应读取失败:{error}"))
})?;
let body_preview = build_body_preview(&response_body);
let body_json = serde_json::from_slice::<Value>(&response_body).ok();
@@ -490,19 +516,56 @@ async fn execute_admin_debug_http(
})
}
fn build_debug_base_url(bind_host: &str, bind_port: u16) -> String {
let debug_host = resolve_debug_host(bind_host);
let authority_host = format_http_authority_host(&debug_host);
format!("http://{authority_host}:{bind_port}")
}
fn resolve_debug_host(bind_host: &str) -> String {
let trimmed = bind_host.trim();
if trimmed.is_empty() {
return Ipv4Addr::LOCALHOST.to_string();
}
match trimmed.parse::<IpAddr>() {
Ok(IpAddr::V4(ip)) if ip.is_unspecified() => Ipv4Addr::LOCALHOST.to_string(),
Ok(IpAddr::V6(ip)) if ip.is_unspecified() => Ipv6Addr::LOCALHOST.to_string(),
Ok(ip) => ip.to_string(),
Err(_) => trimmed.to_string(),
}
}
fn format_http_authority_host(host: &str) -> String {
if host.starts_with('[') && host.ends_with(']') {
return host.to_string();
}
if host.parse::<Ipv6Addr>().is_ok() {
return format!("[{host}]");
}
host.to_string()
}
fn normalize_debug_path(path: &str) -> Result<String, AppError> {
// 只允许 `/xxx` 形式的同源相对路径,明确拒绝绝对 URL 与后台登录接口。
let trimmed = path.trim();
if trimmed.is_empty() {
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_message("调试路径不能为空"));
}
if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_message("只允许调试同源相对路径"));
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_message("只允许调试同源相对路径")
);
}
if !trimmed.starts_with('/') {
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_message("调试路径必须以 / 开头"));
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_message("调试路径必须以 / 开头")
);
}
if trimmed == "/admin/api/login" {
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_message("禁止调试后台登录接口"));
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_message("禁止调试后台登录接口")
);
}
Ok(trimmed.to_string())
}
@@ -540,6 +603,7 @@ fn build_admin_session_payload(session: crate::state::AdminSession) -> AdminSess
}
}
// 首版后台页面内嵌在 api-server避免新增独立前端工程与静态资源发布链。
static ADMIN_CONSOLE_HTML: &str = r#"<!doctype html>
<html lang="zh-CN">
<head>
@@ -1051,23 +1115,46 @@ static ADMIN_CONSOLE_HTML: &str = r#"<!doctype html>
#[cfg(test)]
mod tests {
use super::{build_body_preview, normalize_debug_path, trim_preview};
use axum::http::StatusCode;
use super::{build_body_preview, build_debug_base_url, normalize_debug_path, trim_preview};
use axum::{http::StatusCode, response::IntoResponse};
#[test]
fn normalize_debug_path_rejects_absolute_url() {
let error = normalize_debug_path("https://example.com/api").expect_err("absolute url should fail");
let error =
normalize_debug_path("https://example.com/api").expect_err("absolute url should fail");
assert_eq!(error.into_response().status(), StatusCode::BAD_REQUEST);
}
#[test]
fn normalize_debug_path_rejects_admin_login_route() {
let error = normalize_debug_path("/admin/api/login").expect_err("admin login route should fail");
let error =
normalize_debug_path("/admin/api/login").expect_err("admin login route should fail");
assert_eq!(error.into_response().status(), StatusCode::BAD_REQUEST);
}
#[test]
fn normalize_debug_path_accepts_healthz() {
let path = normalize_debug_path("/healthz").expect("healthz path should pass validation");
assert_eq!(path, "/healthz");
}
#[test]
fn build_debug_base_url_rewrites_wildcard_ipv4_to_loopback() {
let url = build_debug_base_url("0.0.0.0", 3200);
assert_eq!(url, "http://127.0.0.1:3200");
}
#[test]
fn build_debug_base_url_wraps_ipv6_host() {
let url = build_debug_base_url("::1", 3200);
assert_eq!(url, "http://[::1]:3200");
}
#[test]
fn trim_preview_limits_length() {
let text = "a".repeat(5000);