feat: cut over rust web main shell
This commit is contained in:
@@ -2,13 +2,14 @@ use crate::app::AppConfig;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use bridge_runtime::{
|
||||
RuntimeBridgeContextWire, RuntimeCommandArtifactPlan, RuntimeCommandEnvelopeWire,
|
||||
RuntimeCommandExecutionPlan, RuntimeQueryExecutionPlan, build_runtime_command_artifact_plan,
|
||||
build_runtime_command_artifact_plan, RuntimeBridgeContextWire, RuntimeCommandArtifactPlan,
|
||||
RuntimeCommandEnvelopeWire, RuntimeCommandExecutionPlan, RuntimeQueryExecutionPlan,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
use std::time::Duration;
|
||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
|
||||
use base64::Engine;
|
||||
|
||||
const HEADER_REQUEST_ID: &str = "x-request-id";
|
||||
const HEADER_TRACE_ID: &str = "x-trace-id";
|
||||
@@ -17,6 +18,7 @@ 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";
|
||||
const COOKIE_CONVEX_AUTH_JWT: &str = "__convexAuthJWT";
|
||||
|
||||
fn read_env_or_dotenv(key: &str) -> Option<String> {
|
||||
if let Ok(value) = std::env::var(key) {
|
||||
@@ -60,6 +62,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_CONVEX_AUTH_JWT) {
|
||||
return Ok(format!("Bearer {convex_token}"));
|
||||
}
|
||||
|
||||
if let Some(convex_token) = extract_cookie_value(context, COOKIE_MNOTE_WEB_CONVEX_TOKEN) {
|
||||
return Ok(format!("Bearer {convex_token}"));
|
||||
}
|
||||
@@ -78,12 +84,23 @@ fn build_authorization(config: &AppConfig, context: &RequestContext) -> Result<S
|
||||
.with_header("x-upstream-service", "convex")
|
||||
})?;
|
||||
|
||||
// 说明:
|
||||
// - 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 dev_user_id = config.dev_user_id.trim();
|
||||
if dev_user_id.is_empty() {
|
||||
return Ok(format!("Convex {admin_key}"));
|
||||
}
|
||||
|
||||
// 说明:Rust Web 直接调用 Convex HTTP API 时没有 Next/Convex Auth cookie。
|
||||
// 自托管开发态用 admin auth 携带 acting identity,让 @convex-dev/auth 的
|
||||
// getAuthUserId(ctx) 能得到 DEV_USER_ID,从而和 Next 开发态免登录语义一致。
|
||||
let identity = json!({
|
||||
"subject": format!("{}|mnote-web-dev-session", dev_user_id),
|
||||
"issuer": "mnote-web-dev",
|
||||
"name": config.dev_user_name,
|
||||
"email": config.dev_user_email,
|
||||
});
|
||||
let identity_encoded = base64::engine::general_purpose::STANDARD
|
||||
.encode(identity.to_string().as_bytes());
|
||||
Ok(format!("Convex {admin_key}:{identity_encoded}"))
|
||||
}
|
||||
|
||||
fn extract_cookie_value(context: &RequestContext, name: &str) -> Option<String> {
|
||||
@@ -459,7 +476,7 @@ fn now_iso_like() -> String {
|
||||
.unwrap_or_else(|_| "1970-01-01T00:00:00Z".into())
|
||||
}
|
||||
|
||||
async fn execute_convex_mutation_by_name(
|
||||
pub async fn execute_convex_mutation_by_name(
|
||||
config: &AppConfig,
|
||||
context: &RequestContext,
|
||||
function_name: &str,
|
||||
@@ -699,12 +716,16 @@ mod tests {
|
||||
use crate::app::AppConfig;
|
||||
use crate::context::RequestContext;
|
||||
use axum::http::{HeaderMap, HeaderValue, Method, Uri};
|
||||
use base64::Engine;
|
||||
|
||||
fn config() -> AppConfig {
|
||||
AppConfig {
|
||||
service_name: "mnote-web".into(),
|
||||
service_version: "0.1.0".into(),
|
||||
bind_addr: "127.0.0.1:0".into(),
|
||||
public_bind_addr: "127.0.0.1:3000".into(),
|
||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
||||
enable_legacy_next_compat: true,
|
||||
enable_debug_shell_routes: false,
|
||||
hermes_base_path: "/api/hermes".into(),
|
||||
compat_next_base_path: "/api/compat/next".into(),
|
||||
@@ -744,11 +765,21 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_authorization_falls_back_to_plain_admin_auth() {
|
||||
fn build_authorization_falls_back_to_dev_admin_identity() {
|
||||
let authorization = build_authorization(&config(), &request_context(HeaderMap::new()))
|
||||
.expect("authorization");
|
||||
|
||||
assert_eq!(authorization, "Convex admin-demo");
|
||||
assert!(authorization.starts_with("Convex admin-demo:"));
|
||||
let encoded = authorization
|
||||
.trim_start_matches("Convex admin-demo:")
|
||||
.trim();
|
||||
let decoded = base64::engine::general_purpose::STANDARD
|
||||
.decode(encoded)
|
||||
.expect("identity base64");
|
||||
let identity: serde_json::Value = serde_json::from_slice(&decoded).expect("identity json");
|
||||
assert_eq!(identity["subject"], "dev-user|mnote-web-dev-session");
|
||||
assert_eq!(identity["issuer"], "mnote-web-dev");
|
||||
assert_eq!(identity["email"], "dev@mnote.local");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -766,4 +797,36 @@ mod tests {
|
||||
|
||||
assert_eq!(authorization, "Bearer token-from-cookie");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_authorization_prefers_convex_auth_jwt_over_legacy_handoff_cookie() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"cookie",
|
||||
HeaderValue::from_static(
|
||||
"mnote_web_convex_token=legacy-token; __convexAuthJWT=jwt-from-convex-auth",
|
||||
),
|
||||
);
|
||||
|
||||
let authorization =
|
||||
build_authorization(&config(), &request_context(headers)).expect("authorization");
|
||||
|
||||
assert_eq!(authorization, "Bearer jwt-from-convex-auth");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_authorization_reads_convex_auth_jwt_cookie() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"cookie",
|
||||
HeaderValue::from_static(
|
||||
"foo=bar; __convexAuthJWT=jwt-from-convex-auth; __convexAuthRefreshToken=refresh",
|
||||
),
|
||||
);
|
||||
|
||||
let authorization =
|
||||
build_authorization(&config(), &request_context(headers)).expect("authorization");
|
||||
|
||||
assert_eq!(authorization, "Bearer jwt-from-convex-auth");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user