advance 1-8 post-mvp execution batches

This commit is contained in:
lix-2026
2026-05-21 23:53:39 +08:00
parent 3ebcbff728
commit fdb20300e9
67 changed files with 4378 additions and 275 deletions
+117 -7
View File
@@ -41,6 +41,8 @@ const COOKIE_MNOTE_WEB_CONVEX_TOKEN: &str = "mnote_web_convex_token";
const COOKIE_MNOTE_WEB_DEV_SESSION: &str = "mnote_web_dev_session";
const COOKIE_MNOTE_ACTOR_ID: &str = "mnote_actor_id";
const COOKIE_MNOTE_ACTOR_TYPE: &str = "mnote_actor_type";
const COOKIE_MNOTE_ACTOR_EMAIL: &str = "mnote_actor_email";
const COOKIE_MNOTE_ACTOR_NAME: &str = "mnote_actor_name";
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
@@ -121,9 +123,12 @@ pub async fn auth_api(
.with_context(&context)
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web"));
}
if action == "auth:signOut" {
return Ok(build_sign_out_response(&context));
}
let convex_response = run_convex_auth_action(&state, &context, &payload).await?;
Ok(build_auth_proxy_response(&convex_response, &context))
Ok(build_auth_proxy_response(&convex_response, &context, &payload))
}
pub async fn auth_entry(
@@ -1625,6 +1630,7 @@ async fn run_convex_auth_action(
fn build_auth_proxy_response(
convex_response: &serde_json::Value,
context: &RequestContext,
request_payload: &serde_json::Value,
) -> Response {
if convex_response
.get("status")
@@ -1639,7 +1645,7 @@ fn build_auth_proxy_response(
*response.status_mut() = StatusCode::BAD_REQUEST;
clear_auth_cookies(response.headers_mut());
stamp_gateway_headers(response.headers_mut(), false);
context.apply_response_headers(response.headers_mut());
apply_trace_response_headers(context, response.headers_mut());
return response;
}
@@ -1679,14 +1685,46 @@ fn build_auth_proxy_response(
set_literal_cookie(response.headers_mut(), COOKIE_MNOTE_ACTOR_ID, &actor_id);
set_literal_cookie(response.headers_mut(), COOKIE_MNOTE_ACTOR_TYPE, "user");
}
if let Some(email) = resolve_mnote_actor_email(&value, tokens, request_payload) {
set_encoded_cookie(response.headers_mut(), COOKIE_MNOTE_ACTOR_EMAIL, &email);
}
if let Some(name) = resolve_mnote_actor_name(&value, tokens, request_payload) {
set_encoded_cookie(response.headers_mut(), COOKIE_MNOTE_ACTOR_NAME, &name);
}
expire_cookie(response.headers_mut(), COOKIE_MNOTE_WEB_DEV_SESSION);
}
}
stamp_gateway_headers(response.headers_mut(), false);
context.apply_response_headers(response.headers_mut());
apply_trace_response_headers(context, response.headers_mut());
response
}
fn build_sign_out_response(context: &RequestContext) -> Response {
let mut response = axum::Json(json!({ "ok": true, "signedOut": true })).into_response();
clear_auth_cookies(response.headers_mut());
stamp_gateway_headers(response.headers_mut(), false);
apply_trace_response_headers(context, response.headers_mut());
response
}
fn apply_trace_response_headers(context: &RequestContext, headers: &mut axum::http::HeaderMap) {
insert_response_header(headers, "x-request-id", &context.trace.request_id);
insert_response_header(headers, "x-trace-id", &context.trace.trace_id);
if let Some(workspace_id) = &context.workspace.workspace_id {
insert_response_header(headers, "x-mnote-workspace-id", workspace_id);
}
}
fn insert_response_header(headers: &mut axum::http::HeaderMap, name: &str, value: &str) {
let Ok(name) = HeaderName::from_lowercase(name.as_bytes()) else {
return;
};
let Ok(value) = HeaderValue::from_str(value) else {
return;
};
headers.insert(name, value);
}
fn resolve_mnote_actor_id(
value: &serde_json::Value,
tokens: &serde_json::Value,
@@ -1735,6 +1773,69 @@ fn extract_actor_id_from_jwt(token: &str) -> Option<String> {
})
}
fn resolve_mnote_actor_email(
value: &serde_json::Value,
tokens: &serde_json::Value,
request_payload: &serde_json::Value,
) -> Option<String> {
[
"/email",
"/user/email",
"/profile/email",
"/args/params/email",
]
.iter()
.find_map(|pointer| non_empty_json_string(value.pointer(pointer)))
.or_else(|| {
tokens
.get("token")
.and_then(Value::as_str)
.and_then(extract_email_from_jwt)
})
.or_else(|| non_empty_json_string(request_payload.pointer("/args/params/email")))
}
fn resolve_mnote_actor_name(
value: &serde_json::Value,
tokens: &serde_json::Value,
request_payload: &serde_json::Value,
) -> Option<String> {
["/name", "/user/name", "/profile/name", "/args/params/name"]
.iter()
.find_map(|pointer| non_empty_json_string(value.pointer(pointer)))
.or_else(|| {
tokens
.get("token")
.and_then(Value::as_str)
.and_then(extract_name_from_jwt)
})
.or_else(|| non_empty_json_string(request_payload.pointer("/args/params/name")))
}
fn extract_email_from_jwt(token: &str) -> Option<String> {
jwt_string_claim(token, &["email", "preferred_username"])
}
fn extract_name_from_jwt(token: &str) -> Option<String> {
jwt_string_claim(token, &["name", "username"])
}
fn jwt_string_claim(token: &str, keys: &[&str]) -> Option<String> {
let payload_segment = token.split('.').nth(1)?;
let decoded = URL_SAFE_NO_PAD.decode(payload_segment.as_bytes()).ok()?;
let payload: Value = serde_json::from_slice(&decoded).ok()?;
keys.iter()
.find_map(|key| non_empty_json_string(payload.get(key)))
}
fn non_empty_json_string(value: Option<&serde_json::Value>) -> Option<String> {
value
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
fn set_literal_cookie(headers: &mut axum::http::HeaderMap, name: &'static str, value: &str) {
let cookie = format!("{name}={value}; Path=/; HttpOnly; SameSite=Lax");
if let Ok(value) = HeaderValue::from_str(&cookie) {
@@ -1742,6 +1843,11 @@ fn set_literal_cookie(headers: &mut axum::http::HeaderMap, name: &'static str, v
}
}
fn set_encoded_cookie(headers: &mut axum::http::HeaderMap, name: &'static str, value: &str) {
let encoded = URL_SAFE_NO_PAD.encode(value.as_bytes());
set_literal_cookie(headers, name, &encoded);
}
fn set_auth_cookie_from_value(
headers: &mut axum::http::HeaderMap,
name: &'static str,
@@ -1762,6 +1868,8 @@ fn clear_auth_cookies(headers: &mut axum::http::HeaderMap) {
expire_cookie(headers, COOKIE_MNOTE_WEB_DEV_SESSION);
expire_cookie(headers, COOKIE_MNOTE_ACTOR_ID);
expire_cookie(headers, COOKIE_MNOTE_ACTOR_TYPE);
expire_cookie(headers, COOKIE_MNOTE_ACTOR_EMAIL);
expire_cookie(headers, COOKIE_MNOTE_ACTOR_NAME);
}
fn expire_cookie(headers: &mut axum::http::HeaderMap, name: &'static str) {
@@ -2623,9 +2731,9 @@ mod tests {
assert!(html.contains("local_folder"));
assert!(html.contains(r#"data-mnote-source-kind="local_folder""#));
assert!(html.contains(r#"data-mnote-root-uri="file://"#));
assert!(html.contains(r#"data-testid="mnote-workspace-empty-state""#));
assert!(html.contains("当前还没有可显示的本地工作区"));
assert!(html.contains(r#"data-testid="mnote-empty-create-page""#));
assert!(!html.contains(r#"data-testid="mnote-workspace-empty-state""#));
assert!(!html.contains("当前还没有可显示的本地工作区"));
assert!(!html.contains(r#"data-testid="mnote-empty-create-page""#));
assert!(html.contains(r#""transport":"disabled""#));
let _ = std::fs::remove_dir_all(&base);
@@ -2797,7 +2905,9 @@ mod tests {
.expect("body");
let html = String::from_utf8(body.to_vec()).expect("utf8");
assert!(html.contains(r#"data-mnote-shell="auth""#));
assert!(html.contains("邮箱登录"));
assert!(html.contains("账号登录"));
assert!(html.contains("邮箱或用户名"));
assert!(!html.contains(r#"<span>"用户名"</span>"#));
assert!(html.contains("测试账号快速登录"));
}