feat: cut over rust web main shell

This commit is contained in:
lix-2026
2026-04-29 12:24:44 +08:00
parent 7965c6c107
commit 048fe28a4d
97 changed files with 9396 additions and 1263 deletions
+132 -69
View File
@@ -6,15 +6,15 @@ use crate::routes::query_support::{
execute_runtime_query_via_convex, fetch_documents_meta_via_convex,
resolve_effective_workspace_id,
};
use axum::Json;
use axum::extract::{Extension, Query, State};
use axum::http::StatusCode;
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
use axum::Json;
use bridge_runtime::RuntimeQueryEnvelopeWire;
use bridge_runtime::{
RuntimeActorWire, RuntimeCommandEnvelopeWire, RuntimeSourceWire, RuntimeTargetWire,
};
use serde::Deserialize;
use serde_json::{Value, json};
use serde_json::{json, Value};
use std::fs;
use std::time::Duration;
@@ -47,19 +47,34 @@ pub struct DocumentSaveRequest {
}
const NEXT_DOCUMENTS_BASE_URL_ENV: &str = "MNOTE_NEXT_BASE_URL";
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
const HEADER_DOCUMENTS_TRANSPORT: &str = "x-mnote-documents-transport";
fn ok_response(context: &RequestContext, result: Value) -> (StatusCode, Json<Value>) {
fn ok_response(context: &RequestContext, result: Value) -> (StatusCode, HeaderMap, Json<Value>) {
let mut headers = HeaderMap::new();
stamp_documents_headers(&mut headers);
(
StatusCode::OK,
headers,
Json(json!({
"ok": true,
"requestId": context.trace.request_id,
"traceId": context.trace.trace_id,
"owner": "mnote-web",
"result": result,
})),
)
}
fn stamp_documents_headers(headers: &mut HeaderMap) {
if let Ok(name) = HeaderName::from_lowercase(HEADER_MNOTE_WEB_OWNER.as_bytes()) {
headers.insert(name, HeaderValue::from_static("mnote-web"));
}
if let Ok(name) = HeaderName::from_lowercase(HEADER_DOCUMENTS_TRANSPORT.as_bytes()) {
headers.insert(name, HeaderValue::from_static("documents-api"));
}
}
fn read_env_or_dotenv(key: &str) -> Option<String> {
if let Ok(value) = std::env::var(key) {
let trimmed = value.trim().trim_matches('"').to_string();
@@ -99,13 +114,8 @@ fn next_documents_base_url() -> String {
.to_string()
}
fn should_proxy_via_next(context: &RequestContext) -> bool {
context
.auth
.cookie_header
.as_deref()
.map(str::trim)
.is_some_and(|value| !value.is_empty())
fn should_proxy_via_next(_context: &RequestContext) -> bool {
false
}
fn build_next_proxy_headers(
@@ -368,33 +378,80 @@ async fn proxy_next_documents_save(
}))
}
pub async fn load_document_meta_result(
state: &AppState,
context: &RequestContext,
query: DocumentMetaQuery,
) -> Result<Value, WebError> {
let document_id_owned = query.document_id.trim().to_string();
if document_id_owned.is_empty() {
return Err(
WebError::bad_request_code("document_id_required", "缺少有效 documentId")
.with_context(context),
);
}
let effective_workspace_id =
resolve_effective_workspace_id(context, query.workspace_id.as_deref(), false)?;
if should_proxy_via_next(context) {
return proxy_next_documents_meta(
context,
effective_workspace_id.as_deref(),
&document_id_owned,
)
.await;
}
fetch_documents_meta_via_convex(
state.config(),
context,
effective_workspace_id.as_deref(),
&document_id_owned,
)
.await
}
pub async fn load_document_content_result(
state: &AppState,
context: &RequestContext,
query: DocumentContentQuery,
) -> Result<Value, WebError> {
let document_id_owned = query.document_id.trim().to_string();
if document_id_owned.is_empty() {
return Err(
WebError::bad_request_code("document_id_required", "缺少有效 documentId")
.with_context(context),
);
}
let effective_workspace_id =
resolve_effective_workspace_id(context, query.workspace_id.as_deref(), false)?;
if should_proxy_via_next(context) {
return proxy_next_documents_content(
context,
effective_workspace_id.as_deref(),
&document_id_owned,
)
.await;
}
execute_runtime_query_via_convex(
state.config(),
context,
effective_workspace_id.as_deref(),
RuntimeQueryEnvelopeWire {
name: "documents.content.get".into(),
payload: json!({
"documentId": document_id_owned,
"workspaceId": effective_workspace_id,
}),
},
)
.await
}
pub async fn meta(
State(state): State<AppState>,
Extension(context): Extension<RequestContext>,
Query(query): Query<DocumentMetaQuery>,
) -> Result<(StatusCode, Json<Value>), WebError> {
let document_id = query.document_id.trim();
if document_id.is_empty() {
return Err(
WebError::bad_request_code("document_id_required", "缺少有效 documentId")
.with_context(&context),
);
}
let effective_workspace_id =
resolve_effective_workspace_id(&context, query.workspace_id.as_deref(), false)?;
if should_proxy_via_next(&context) {
let result =
proxy_next_documents_meta(&context, effective_workspace_id.as_deref(), document_id)
.await?;
return Ok(ok_response(&context, result));
}
let result = fetch_documents_meta_via_convex(
state.config(),
&context,
effective_workspace_id.as_deref(),
document_id,
)
.await?;
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
let result = load_document_meta_result(&state, &context, query).await?;
Ok(ok_response(&context, result))
}
@@ -402,35 +459,8 @@ pub async fn content(
State(state): State<AppState>,
Extension(context): Extension<RequestContext>,
Query(query): Query<DocumentContentQuery>,
) -> Result<(StatusCode, Json<Value>), WebError> {
let document_id = query.document_id.trim();
if document_id.is_empty() {
return Err(
WebError::bad_request_code("document_id_required", "缺少有效 documentId")
.with_context(&context),
);
}
let effective_workspace_id =
resolve_effective_workspace_id(&context, query.workspace_id.as_deref(), false)?;
if should_proxy_via_next(&context) {
let result =
proxy_next_documents_content(&context, effective_workspace_id.as_deref(), document_id)
.await?;
return Ok(ok_response(&context, result));
}
let result = execute_runtime_query_via_convex(
state.config(),
&context,
effective_workspace_id.as_deref(),
RuntimeQueryEnvelopeWire {
name: "documents.content.get".into(),
payload: json!({
"documentId": document_id,
"workspaceId": effective_workspace_id,
}),
},
)
.await?;
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
let result = load_document_content_result(&state, &context, query).await?;
Ok(ok_response(&context, result))
}
@@ -438,7 +468,7 @@ pub async fn save(
State(state): State<AppState>,
Extension(context): Extension<RequestContext>,
Json(body): Json<DocumentSaveRequest>,
) -> Result<(StatusCode, Json<Value>), WebError> {
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
let document_id = body.document_id.trim();
if document_id.is_empty() {
return Err(
@@ -500,9 +530,9 @@ pub async fn save(
#[cfg(test)]
mod tests {
use crate::app::{AppConfig, AppState, build_app};
use axum::body::{Body, to_bytes};
use axum::http::{Request, StatusCode};
use crate::app::{build_app, AppConfig, AppState};
use axum::body::{to_bytes, Body};
use axum::http::{HeaderMap, HeaderValue, Method, Request, StatusCode, Uri};
use serde_json::Value;
use tower::util::ServiceExt;
@@ -511,6 +541,9 @@ mod tests {
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(),
@@ -579,8 +612,24 @@ mod tests {
}))
}
#[test]
fn convex_auth_cookie_does_not_force_next_proxy() {
let mut headers = HeaderMap::new();
headers.insert(
"cookie",
HeaderValue::from_static("__convexAuthJWT=jwt-demo; foo=bar"),
);
let context = crate::context::RequestContext::from_http_parts(
&Method::GET,
&"/api/documents/meta".parse::<Uri>().expect("uri"),
&headers,
);
assert!(!super::should_proxy_via_next(&context));
}
#[tokio::test]
async fn documents_meta_route_returns_document_metadata() {
async fn documents_api_meta_route_returns_document_metadata() {
let response = app()
.oneshot(
Request::builder()
@@ -592,6 +641,20 @@ mod tests {
.expect("response");
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get("x-mnote-web-owner")
.and_then(|value| value.to_str().ok()),
Some("mnote-web")
);
assert_eq!(
response
.headers()
.get("x-mnote-documents-transport")
.and_then(|value| value.to_str().ok()),
Some("documents-api")
);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body");
@@ -603,7 +666,7 @@ mod tests {
}
#[tokio::test]
async fn documents_content_route_returns_page_subtree() {
async fn documents_api_content_route_returns_page_subtree() {
let response = app()
.oneshot(
Request::builder()