feat: complete tree shell cutover and regression coverage
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
use crate::app::AppConfig;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine;
|
||||
use bridge_runtime::{RuntimeCommandExecutionPlan, RuntimeQueryExecutionPlan};
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
@@ -14,6 +12,7 @@ const HEADER_WORKSPACE_ID: &str = "x-mnote-workspace-id";
|
||||
const HEADER_SOURCE_CHANNEL: &str = "x-mnote-source-channel";
|
||||
const HEADER_SOURCE_CLIENT: &str = "x-mnote-source-client";
|
||||
const HEADER_IDEMPOTENCY_KEY: &str = "x-idempotency-key";
|
||||
const COOKIE_MNOTE_WEB_CONVEX_TOKEN: &str = "mnote_web_convex_token";
|
||||
|
||||
fn read_env_or_dotenv(key: &str) -> Option<String> {
|
||||
if let Ok(value) = std::env::var(key) {
|
||||
@@ -57,6 +56,10 @@ fn build_authorization(config: &AppConfig, context: &RequestContext) -> Result<S
|
||||
return Ok(authorization.to_string());
|
||||
}
|
||||
|
||||
if let Some(convex_token) = extract_cookie_value(context, COOKIE_MNOTE_WEB_CONVEX_TOKEN) {
|
||||
return Ok(format!("Bearer {convex_token}"));
|
||||
}
|
||||
|
||||
let admin_key = config
|
||||
.convex_admin_key
|
||||
.clone()
|
||||
@@ -71,27 +74,32 @@ fn build_authorization(config: &AppConfig, context: &RequestContext) -> Result<S
|
||||
.with_header("x-upstream-service", "convex")
|
||||
})?;
|
||||
|
||||
let effective_actor_id = if context.auth.actor_id.trim().is_empty()
|
||||
|| context.auth.actor_id == "anonymous"
|
||||
{
|
||||
config.dev_user_id.clone()
|
||||
} else {
|
||||
context.auth.actor_id.clone()
|
||||
};
|
||||
// 说明:
|
||||
// - Next 侧 `setAdminAuth(adminKey)` 走的是纯 admin auth,而不是伪造用户身份。
|
||||
// - Convex 业务函数内部会在缺少真实 token 时自行回退到 DEV_USER_ID。
|
||||
// - 这里若强行附带伪造 identity,会让 getAuthUserId(ctx) 命中一个未映射用户,
|
||||
// 反而绕过 DEV_USER_ID fallback,导致 workspace membership 校验失败。
|
||||
Ok(format!("Convex {admin_key}"))
|
||||
}
|
||||
|
||||
let identity = json!({
|
||||
"subject": effective_actor_id,
|
||||
"issuer": "https://mnote.local/dev-auth",
|
||||
"tokenIdentifier": format!("dev-user|{}", effective_actor_id),
|
||||
"name": config.dev_user_name,
|
||||
"email": config.dev_user_email,
|
||||
});
|
||||
let encoded = STANDARD.encode(
|
||||
serde_json::to_string(&identity)
|
||||
.map_err(|error| WebError::internal(format!("开发用户身份序列化失败: {error}")))?,
|
||||
);
|
||||
|
||||
Ok(format!("Convex {admin_key}:{encoded}"))
|
||||
fn extract_cookie_value(context: &RequestContext, name: &str) -> Option<String> {
|
||||
context
|
||||
.auth
|
||||
.cookie_header
|
||||
.as_deref()
|
||||
.and_then(|cookie_header| {
|
||||
cookie_header.split(';').find_map(|segment| {
|
||||
let (key, value) = segment.trim().split_once('=')?;
|
||||
if key.trim() != name {
|
||||
return None;
|
||||
}
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(trimmed.to_string())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn convex_url(config: &AppConfig, context: &RequestContext) -> Result<String, WebError> {
|
||||
@@ -303,7 +311,10 @@ pub async fn execute_convex_command_plan(
|
||||
if plan.function_name.trim().is_empty() || plan.function_name.ends_with(":unknown") {
|
||||
return Err(WebError::bad_request_code(
|
||||
"transport_command_unsupported",
|
||||
format!("mnote-web transport 暂不支持 command: {}", plan.function_name),
|
||||
format!(
|
||||
"mnote-web transport 暂不支持 command: {}",
|
||||
plan.function_name
|
||||
),
|
||||
)
|
||||
.with_context(context)
|
||||
.with_header("x-error-phase", "plan_validation"));
|
||||
@@ -359,7 +370,10 @@ pub async fn execute_convex_command_plan(
|
||||
|
||||
let response = request.send().await.map_err(|error| {
|
||||
let base = if error.is_timeout() {
|
||||
WebError::gateway_timeout_code("convex_timeout", format!("Convex mutation 超时: {error}"))
|
||||
WebError::gateway_timeout_code(
|
||||
"convex_timeout",
|
||||
format!("Convex mutation 超时: {error}"),
|
||||
)
|
||||
} else {
|
||||
WebError::service_unavailable_code(
|
||||
"convex_unavailable",
|
||||
@@ -419,3 +433,77 @@ pub async fn execute_convex_command_plan(
|
||||
.with_header("x-upstream-status", status.as_u16().to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::build_authorization;
|
||||
use crate::app::AppConfig;
|
||||
use crate::context::RequestContext;
|
||||
use axum::http::{HeaderMap, HeaderValue, Method, Uri};
|
||||
|
||||
fn config() -> AppConfig {
|
||||
AppConfig {
|
||||
service_name: "mnote-web".into(),
|
||||
service_version: "0.1.0".into(),
|
||||
bind_addr: "127.0.0.1:3104".into(),
|
||||
hermes_base_path: "/api/hermes".into(),
|
||||
compat_next_base_path: "/api/compat/next".into(),
|
||||
convex_url: Some("http://127.0.0.1:3210".into()),
|
||||
convex_admin_key: Some("admin-demo".into()),
|
||||
allow_dev_fixtures: false,
|
||||
query_fixtures_json: None,
|
||||
mutation_fixtures_json: None,
|
||||
dev_user_id: "dev-user".into(),
|
||||
dev_user_name: "开发用户".into(),
|
||||
dev_user_email: "dev@mnote.local".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn request_context(headers: HeaderMap) -> RequestContext {
|
||||
RequestContext::from_http_parts(
|
||||
&Method::GET,
|
||||
&"/api/compat/next/sidebar?workspaceId=ws_demo"
|
||||
.parse::<Uri>()
|
||||
.expect("uri"),
|
||||
&headers,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_authorization_prefers_forwarded_authorization() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"authorization",
|
||||
HeaderValue::from_static("Bearer real-token"),
|
||||
);
|
||||
|
||||
let authorization =
|
||||
build_authorization(&config(), &request_context(headers)).expect("authorization");
|
||||
|
||||
assert_eq!(authorization, "Bearer real-token");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_authorization_falls_back_to_plain_admin_auth() {
|
||||
let authorization = build_authorization(&config(), &request_context(HeaderMap::new()))
|
||||
.expect("authorization");
|
||||
|
||||
assert_eq!(authorization, "Convex admin-demo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_authorization_reads_convex_token_from_cookie() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"cookie",
|
||||
HeaderValue::from_static(
|
||||
"foo=bar; mnote_web_convex_token=token-from-cookie; theme=light",
|
||||
),
|
||||
);
|
||||
|
||||
let authorization =
|
||||
build_authorization(&config(), &request_context(headers)).expect("authorization");
|
||||
|
||||
assert_eq!(authorization, "Bearer token-from-cookie");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user