feat: cut over rust web main shell
This commit is contained in:
@@ -0,0 +1,652 @@
|
||||
use crate::app::AppState;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::routes::web_shell::{load_sidebar_tree_html, load_workspace_shell_projection};
|
||||
use crate::workspace_shell::render_workspace_shell_sidebar_html;
|
||||
use axum::body::Body;
|
||||
use axum::extract::{Extension, Query, State};
|
||||
use axum::http::{header, HeaderName, HeaderValue, Request, StatusCode};
|
||||
use axum::response::{Html, IntoResponse, Response};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
|
||||
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
|
||||
const HEADER_MNOTE_LEGACY_UPSTREAM: &str = "x-mnote-legacy-upstream";
|
||||
const COOKIE_RECENT_PAGE_ID: &str = "mnote_recent_page_id";
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GatewayManifest {
|
||||
ok: bool,
|
||||
owner: &'static str,
|
||||
public_entry: String,
|
||||
legacy_next_base_url: Option<String>,
|
||||
legacy_next_compat_enabled: bool,
|
||||
notes: Vec<&'static str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct RootEntryQuery {
|
||||
page_id: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn gateway_health(State(state): State<AppState>) -> Response {
|
||||
let mut response = axum::Json(GatewayManifest {
|
||||
ok: true,
|
||||
owner: "mnote-web",
|
||||
public_entry: state.config().public_bind_addr.clone(),
|
||||
legacy_next_base_url: state.config().legacy_next_base_url.clone(),
|
||||
legacy_next_compat_enabled: state.config().enable_legacy_next_compat,
|
||||
notes: vec![
|
||||
"3000 公开入口默认由 mnote-web gateway 拥有。",
|
||||
"Next App Router 只作为 legacy compat upstream 使用。",
|
||||
],
|
||||
})
|
||||
.into_response();
|
||||
stamp_gateway_headers(response.headers_mut(), false);
|
||||
response
|
||||
}
|
||||
|
||||
pub async fn auth_entry(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
request: Request<Body>,
|
||||
) -> Result<Response, WebError> {
|
||||
if state.config().enable_legacy_next_compat && state.config().legacy_next_base_url.is_some() {
|
||||
return legacy_next_proxy(State(state), Extension(context), request).await;
|
||||
}
|
||||
|
||||
let content = crate::ssr::render_view(crate::ssr::pages::auth::AuthPage());
|
||||
let mut response = Html(format!(
|
||||
r#"<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>MNOTE Auth</title>
|
||||
<style>{}</style>
|
||||
</head>
|
||||
<body data-mnote-web-owner="mnote-web" data-mnote-shell="auth">
|
||||
{}
|
||||
</body>
|
||||
</html>"#,
|
||||
crate::ssr::MNOTE_CSS,
|
||||
content
|
||||
))
|
||||
.into_response();
|
||||
stamp_gateway_headers(response.headers_mut(), false);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn root_entry(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Query(query): Query<RootEntryQuery>,
|
||||
) -> Response {
|
||||
let workspace_id = context
|
||||
.workspace
|
||||
.workspace_id
|
||||
.as_deref()
|
||||
.unwrap_or("ws_demo");
|
||||
let requested_page_id = query
|
||||
.page_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty());
|
||||
let recent_page_id = extract_cookie_value(&context, COOKIE_RECENT_PAGE_ID);
|
||||
let active_page_id = requested_page_id.or(recent_page_id.as_deref());
|
||||
let default_workspace_name = format!("{} 的空间", state.config().dev_user_name);
|
||||
let workspace_projection = load_workspace_shell_projection(
|
||||
state.config(),
|
||||
&context,
|
||||
workspace_id,
|
||||
active_page_id,
|
||||
&default_workspace_name,
|
||||
)
|
||||
.await;
|
||||
let sidebar_tree_html =
|
||||
load_sidebar_tree_html(
|
||||
state.config(),
|
||||
&context,
|
||||
workspace_id,
|
||||
workspace_projection.active_page_id.as_deref(),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let workspace_sidebar_html = render_workspace_shell_sidebar_html(
|
||||
&workspace_projection,
|
||||
Some(sidebar_tree_html.as_str()),
|
||||
);
|
||||
let workspace_name = workspace_projection.workspace_name.clone();
|
||||
let content = crate::ssr::render_view(leptos::view! {
|
||||
<crate::ssr::pages::home::HomePage sidebar_tree_html={sidebar_tree_html} workspace_name={workspace_name} workspace_sidebar_html={workspace_sidebar_html} />
|
||||
});
|
||||
let mut response = Html(format!(
|
||||
r#"<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>MNOTE</title>
|
||||
<style>{}</style>
|
||||
</head>
|
||||
<body data-mnote-web-owner="mnote-web" data-mnote-shell="workspace">
|
||||
{}
|
||||
</body>
|
||||
</html>"#,
|
||||
crate::ssr::MNOTE_CSS,
|
||||
content
|
||||
))
|
||||
.into_response();
|
||||
stamp_gateway_headers(response.headers_mut(), false);
|
||||
response
|
||||
}
|
||||
|
||||
pub async fn legacy_next_proxy(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
request: Request<Body>,
|
||||
) -> Result<Response, WebError> {
|
||||
if !state.config().enable_legacy_next_compat {
|
||||
return Err(WebError::service_unavailable_code(
|
||||
"legacy_next_compat_disabled",
|
||||
"Next App Router legacy compat 已关闭,当前路径未迁到 Rust Web gateway。",
|
||||
)
|
||||
.with_context(&context)
|
||||
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web"));
|
||||
}
|
||||
|
||||
let Some(base_url) = state.config().legacy_next_base_url.as_deref() else {
|
||||
return Err(WebError::service_unavailable_code(
|
||||
"legacy_next_upstream_missing",
|
||||
"未配置 MNOTE_WEB_LEGACY_NEXT_BASE_URL,无法代理 legacy Next 路径。",
|
||||
)
|
||||
.with_context(&context)
|
||||
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web"));
|
||||
};
|
||||
|
||||
let path_and_query = request
|
||||
.uri()
|
||||
.path_and_query()
|
||||
.map(|value| value.as_str())
|
||||
.unwrap_or("/");
|
||||
let upstream_url = reqwest::Url::parse(&format!("{base_url}{path_and_query}"))
|
||||
.map_err(|error| WebError::internal(format!("legacy Next upstream URL 非法: {error}")))?;
|
||||
|
||||
let method = request.method().clone();
|
||||
let headers = request.headers().clone();
|
||||
let body = axum::body::to_bytes(request.into_body(), 10 * 1024 * 1024)
|
||||
.await
|
||||
.map_err(|error| WebError::internal(format!("读取 legacy proxy 请求体失败: {error}")))?;
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()
|
||||
.map_err(|error| WebError::internal(format!("legacy Next HTTP 客户端创建失败: {error}")))?;
|
||||
|
||||
let upstream_origin = upstream_origin(&upstream_url);
|
||||
let mut upstream_request = client.request(method, upstream_url);
|
||||
for (name, value) in headers.iter() {
|
||||
if is_hop_by_hop_header(name.as_str()) || name == header::HOST {
|
||||
continue;
|
||||
}
|
||||
if name == header::ORIGIN {
|
||||
upstream_request = upstream_request.header(name, upstream_origin.as_str());
|
||||
continue;
|
||||
}
|
||||
if name == header::REFERER {
|
||||
let normalized_referer = normalize_legacy_referer(value, &upstream_origin);
|
||||
upstream_request = upstream_request.header(name, normalized_referer);
|
||||
continue;
|
||||
}
|
||||
upstream_request = upstream_request.header(name, value);
|
||||
}
|
||||
|
||||
let upstream_response = upstream_request
|
||||
.body(body.to_vec())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
WebError::bad_gateway_code(
|
||||
"legacy_next_proxy_error",
|
||||
format!("legacy Next 请求失败: {error}"),
|
||||
)
|
||||
.with_context(&context)
|
||||
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.with_header(HEADER_MNOTE_LEGACY_UPSTREAM, "next-app-router")
|
||||
})?;
|
||||
|
||||
let status = upstream_response.status();
|
||||
let upstream_headers = upstream_response.headers().clone();
|
||||
let body = upstream_response.bytes().await.map_err(|error| {
|
||||
WebError::bad_gateway_code(
|
||||
"legacy_next_proxy_body_error",
|
||||
format!("legacy Next 响应读取失败: {error}"),
|
||||
)
|
||||
.with_context(&context)
|
||||
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
.with_header(HEADER_MNOTE_LEGACY_UPSTREAM, "next-app-router")
|
||||
})?;
|
||||
|
||||
let mut response = Response::builder()
|
||||
.status(StatusCode::from_u16(status.as_u16()).unwrap_or(StatusCode::BAD_GATEWAY))
|
||||
.body(Body::from(body))
|
||||
.map_err(|error| WebError::internal(format!("legacy proxy 响应构造失败: {error}")))?;
|
||||
for (name, value) in upstream_headers.iter() {
|
||||
if is_hop_by_hop_header(name.as_str()) {
|
||||
continue;
|
||||
}
|
||||
response.headers_mut().append(name, value.clone());
|
||||
}
|
||||
stamp_gateway_headers(response.headers_mut(), true);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
|
||||
fn extract_cookie_value(context: &RequestContext, name: &str) -> Option<String> {
|
||||
context.auth.cookie_header.as_deref()?.split(';').find_map(|part| {
|
||||
let (cookie_name, cookie_value) = part.trim().split_once('=')?;
|
||||
if cookie_name.trim() == name {
|
||||
let value = cookie_value.trim();
|
||||
if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value.to_string())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn stamp_gateway_headers(headers: &mut axum::http::HeaderMap, legacy: bool) {
|
||||
if let Ok(name) = HeaderName::from_lowercase(HEADER_MNOTE_WEB_OWNER.as_bytes()) {
|
||||
headers.insert(name, HeaderValue::from_static("mnote-web"));
|
||||
}
|
||||
if legacy {
|
||||
if let Ok(name) = HeaderName::from_lowercase(HEADER_MNOTE_LEGACY_UPSTREAM.as_bytes()) {
|
||||
headers.insert(name, HeaderValue::from_static("next-app-router"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_hop_by_hop_header(name: &str) -> bool {
|
||||
matches!(
|
||||
name.to_ascii_lowercase().as_str(),
|
||||
"connection"
|
||||
| "keep-alive"
|
||||
| "proxy-authenticate"
|
||||
| "proxy-authorization"
|
||||
| "te"
|
||||
| "trailers"
|
||||
| "transfer-encoding"
|
||||
| "upgrade"
|
||||
)
|
||||
}
|
||||
|
||||
fn upstream_origin(url: &reqwest::Url) -> String {
|
||||
let host = url.host_str().unwrap_or("127.0.0.1");
|
||||
match url.port() {
|
||||
Some(port) => format!("{}://{}:{}", url.scheme(), host, port),
|
||||
None => format!("{}://{}", url.scheme(), host),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_legacy_referer(value: &HeaderValue, upstream_origin: &str) -> String {
|
||||
let referer = value.to_str().unwrap_or_default();
|
||||
let Ok(parsed) = reqwest::Url::parse(referer) else {
|
||||
return upstream_origin.to_string();
|
||||
};
|
||||
let path = parsed.path();
|
||||
let query = parsed
|
||||
.query()
|
||||
.map(|query| format!("?{query}"))
|
||||
.unwrap_or_default();
|
||||
format!("{upstream_origin}{path}{query}")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::{build_app, AppConfig, AppState};
|
||||
use axum::body::{to_bytes, Body};
|
||||
use axum::http::{header, HeaderMap, HeaderValue, Request, StatusCode};
|
||||
use axum::response::{Html, IntoResponse};
|
||||
use axum::routing::{get, post};
|
||||
use tokio::net::TcpListener;
|
||||
use tower::util::ServiceExt;
|
||||
|
||||
fn app() -> axum::Router {
|
||||
app_with_legacy_next_base_url("http://127.0.0.1:3100".into())
|
||||
}
|
||||
|
||||
fn app_with_legacy_next_base_url(legacy_next_base_url: String) -> axum::Router {
|
||||
app_with_config(legacy_next_base_url, true)
|
||||
}
|
||||
|
||||
fn app_with_config(
|
||||
legacy_next_base_url: String,
|
||||
enable_legacy_next_compat: bool,
|
||||
) -> axum::Router {
|
||||
build_app(AppState::new(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(legacy_next_base_url),
|
||||
enable_legacy_next_compat,
|
||||
enable_debug_shell_routes: false,
|
||||
hermes_base_path: "/api/hermes".into(),
|
||||
compat_next_base_path: "/api/compat/next".into(),
|
||||
convex_url: None,
|
||||
convex_admin_key: None,
|
||||
allow_dev_fixtures: true,
|
||||
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(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn spawn_legacy_auth_upstream() -> String {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("legacy listener");
|
||||
let addr = listener.local_addr().expect("legacy addr");
|
||||
let app = axum::Router::new().route(
|
||||
"/auth",
|
||||
get(|| async {
|
||||
Html(r#"<html><body><button>测试账号快速登录</button></body></html>"#)
|
||||
})
|
||||
.post(|| async { "auth-post-ok" }),
|
||||
);
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.expect("legacy server");
|
||||
});
|
||||
format!("http://{addr}")
|
||||
}
|
||||
|
||||
async fn spawn_legacy_origin_checked_upstream() -> String {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("legacy listener");
|
||||
let addr = listener.local_addr().expect("legacy addr");
|
||||
let expected_origin = format!("http://{addr}");
|
||||
let app = axum::Router::new().route(
|
||||
"/api/auth",
|
||||
post(move |headers: HeaderMap| {
|
||||
let expected_origin = expected_origin.clone();
|
||||
async move {
|
||||
let origin = headers
|
||||
.get("origin")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or_default();
|
||||
if origin != expected_origin {
|
||||
return (StatusCode::FORBIDDEN, "Invalid origin");
|
||||
}
|
||||
(StatusCode::OK, "ok")
|
||||
}
|
||||
}),
|
||||
);
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.expect("legacy server");
|
||||
});
|
||||
format!("http://{addr}")
|
||||
}
|
||||
|
||||
async fn spawn_legacy_cookie_upstream() -> String {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("legacy listener");
|
||||
let addr = listener.local_addr().expect("legacy addr");
|
||||
let app = axum::Router::new().route(
|
||||
"/api/auth",
|
||||
post(|| async {
|
||||
let mut response = "ok".into_response();
|
||||
response.headers_mut().append(
|
||||
header::SET_COOKIE,
|
||||
HeaderValue::from_static("__convexAuthJWT=jwt-demo; Path=/; HttpOnly"),
|
||||
);
|
||||
response.headers_mut().append(
|
||||
header::SET_COOKIE,
|
||||
HeaderValue::from_static(
|
||||
"__convexAuthRefreshToken=refresh-demo; Path=/; HttpOnly",
|
||||
),
|
||||
);
|
||||
response
|
||||
}),
|
||||
);
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.expect("legacy server");
|
||||
});
|
||||
format!("http://{addr}")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_health_declares_mnote_web_owner() {
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/gateway/health")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.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")
|
||||
);
|
||||
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["owner"], "mnote-web");
|
||||
assert_eq!(payload["publicEntry"], "127.0.0.1:3000");
|
||||
assert_eq!(payload["legacyNextBaseUrl"], "http://127.0.0.1:3100");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn root_entry_returns_wolai_workspace_layout_contract() {
|
||||
let response = app_with_config("http://127.0.0.1:3100".into(), false)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.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")
|
||||
);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let html = String::from_utf8(body.to_vec()).expect("utf8");
|
||||
assert!(html.contains(r#"<body data-mnote-web-owner="mnote-web" data-mnote-shell="workspace">"#));
|
||||
assert!(html.contains(r#"data-testid="wolai-sidebar""#));
|
||||
assert!(html.contains(r#"data-testid="wolai-topbar""#));
|
||||
assert!(html.contains(r#"data-testid="wolai-floating-ai""#));
|
||||
assert!(html.contains("星标置顶"));
|
||||
assert!(html.contains("我的页面"));
|
||||
assert!(html.contains("垃圾箱"));
|
||||
assert!(html.contains("模板中心"));
|
||||
assert!(!html.contains("欢迎使用 MNOTE 知识管理平台"));
|
||||
assert!(!html.contains(r#"<a href="/documents">文档</a>"#));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn root_entry_uses_recent_page_cookie_as_active_page() {
|
||||
let response = app_with_config("http://127.0.0.1:3100".into(), false)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/")
|
||||
.header("cookie", "mnote_recent_page_id=page_child")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let html = String::from_utf8(body.to_vec()).expect("utf8");
|
||||
assert!(html.contains(r#"data-node-id="page_child""#));
|
||||
assert!(html.contains(r#"class="wolai-page-row wolai-active-row" href="/documents/page_child?workspaceId=ws_demo" data-node-id="page_child""#));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_entry_returns_gateway_fallback_shell_when_compat_disabled() {
|
||||
let response = app_with_config("http://127.0.0.1:3100".into(), false)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/auth")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.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!(response
|
||||
.headers()
|
||||
.get("content-type")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or_default()
|
||||
.contains("text/html"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_entry_uses_legacy_next_login_ui_when_compat_enabled() {
|
||||
let legacy_base_url = spawn_legacy_auth_upstream().await;
|
||||
let response = app_with_legacy_next_base_url(legacy_base_url)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/auth")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-mnote-legacy-upstream")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("next-app-router")
|
||||
);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let html = String::from_utf8(body.to_vec()).expect("utf8");
|
||||
assert!(html.contains("测试账号快速登录"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_entry_proxies_post_to_legacy_next_when_compat_enabled() {
|
||||
let legacy_base_url = spawn_legacy_auth_upstream().await;
|
||||
let response = app_with_legacy_next_base_url(legacy_base_url)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/auth")
|
||||
.header("origin", "http://127.0.0.1:3000")
|
||||
.header("referer", "http://127.0.0.1:3000/auth")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-mnote-legacy-upstream")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("next-app-router")
|
||||
);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let text = String::from_utf8(body.to_vec()).expect("utf8");
|
||||
assert_eq!(text, "auth-post-ok");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_proxy_normalizes_auth_post_origin_to_upstream_origin() {
|
||||
let legacy_base_url = spawn_legacy_origin_checked_upstream().await;
|
||||
let response = app_with_legacy_next_base_url(legacy_base_url)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth")
|
||||
.header("origin", "http://127.0.0.1:3000")
|
||||
.header("referer", "http://127.0.0.1:3000/auth")
|
||||
.body(Body::from(r#"{"action":"auth:signIn"}"#))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-mnote-legacy-upstream")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("next-app-router")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_proxy_preserves_multiple_set_cookie_headers() {
|
||||
let legacy_base_url = spawn_legacy_cookie_upstream().await;
|
||||
let response = app_with_legacy_next_base_url(legacy_base_url)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let cookies = response.headers().get_all("set-cookie");
|
||||
let values = cookies
|
||||
.iter()
|
||||
.map(|value| value.to_str().unwrap_or_default())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(values
|
||||
.iter()
|
||||
.any(|value| value.contains("__convexAuthJWT=jwt-demo")));
|
||||
assert!(values
|
||||
.iter()
|
||||
.any(|value| value.contains("__convexAuthRefreshToken=refresh-demo")));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user