chore: align local-first control plane and editor fixes

- wire SQLite control-plane access/session paths into Rust web local-folder routes

- preserve local Markdown attachment semantics across upload, reload, and secondary-pane resource tabs

- refresh design governance docs, Reasonix task templates, and bug records

- retire root .mcp.json local MCP config
This commit is contained in:
lix-2026
2026-05-23 23:38:42 +08:00
parent 42fb58310c
commit 5f97800489
110 changed files with 5344 additions and 889 deletions
+165 -3
View File
@@ -78,10 +78,10 @@ fn build_session_response(state: &AppState, context: RequestContext) -> SessionR
return SessionResponse {
ok: true,
owner: "mnote-web",
user_id: resolved.user.id,
user_id: resolved.user.id.clone(),
email: resolved.user.email.unwrap_or_default(),
name: resolved.user.display_name,
actor_type: "user".to_string(),
actor_type: effective_actor_type_for_user(&resolved.user.id, &resolved.user.role),
auth_mode: "sqliteSession",
request_id: context.trace.request_id,
trace_id: context.trace.trace_id,
@@ -109,7 +109,7 @@ fn build_session_response(state: &AppState, context: RequestContext) -> SessionR
state.config().dev_user_id.clone()
};
let actor_type = if has_forwarded_actor {
context.auth.actor_type
effective_actor_type_for_user(actor_id, &context.auth.actor_type)
} else {
"devFallback".to_string()
};
@@ -139,6 +139,19 @@ fn build_session_response(state: &AppState, context: RequestContext) -> SessionR
}
}
fn effective_actor_type_for_user(user_id: &str, stored_role: &str) -> String {
let role = stored_role.trim();
let fallback_role = if role.is_empty() { "user" } else { role };
if crate::routes::local_folder_source::is_local_access_policy_admin_actor(
user_id,
fallback_role,
) {
"admin".to_string()
} else {
fallback_role.to_string()
}
}
fn jwt_cookie_claim(context: &RequestContext, keys: &[&str]) -> Option<String> {
let token = context
.auth
@@ -376,6 +389,155 @@ mod tests {
assert_eq!(payload["authMode"], "sqliteSession");
}
#[tokio::test]
async fn session_returns_admin_for_sqlite_admin_user() {
let state = 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("http://127.0.0.1:3100".into()),
enable_legacy_next_compat: true,
enable_debug_shell_routes: false,
enable_editor_actor: true,
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(),
});
state
.control_plane()
.upsert_user(UpsertUserInput {
id: Some("liaibo".into()),
email: Some("liaibo@yeah.net".into()),
username: "liaibo".into(),
display_name: "liaibo".into(),
role: Some("admin".into()),
password_hash: None,
})
.expect("upsert admin user");
state
.control_plane()
.create_session(CreateSessionInput {
id: None,
user_id: "liaibo".into(),
token_hash: session_token_hash("raw-admin-session-token"),
user_agent: None,
ip_hash: None,
expires_at: None,
})
.expect("create admin session");
let response = build_app(state)
.oneshot(
Request::builder()
.uri("/api/auth/session")
.header("cookie", "mnote_session=raw-admin-session-token")
.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 payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
assert_eq!(payload["userId"], "liaibo");
assert_eq!(payload["actorType"], "admin");
assert_eq!(payload["authMode"], "sqliteSession");
}
#[tokio::test]
async fn session_returns_admin_for_local_access_policy_admin() {
let _guard = crate::test_support::hermes_env_lock()
.lock()
.expect("env lock");
let policy_root = std::env::temp_dir().join(format!(
"mnote-session-access-policy-admin-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&policy_root);
std::fs::create_dir_all(&policy_root).expect("create policy root");
let policy_file = policy_root.join("access-policy.json");
std::fs::write(&policy_file, r#"{"admins":["liaibo"],"grants":[]}"#)
.expect("write access policy");
std::env::set_var("MNOTE_LOCAL_ACCESS_POLICY_FILE", &policy_file);
let state = 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("http://127.0.0.1:3100".into()),
enable_legacy_next_compat: true,
enable_debug_shell_routes: false,
enable_editor_actor: true,
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(),
});
state
.control_plane()
.upsert_user(UpsertUserInput {
id: Some("liaibo".into()),
email: Some("liaibo@yeah.net".into()),
username: "liaibo".into(),
display_name: "liaibo".into(),
role: None,
password_hash: None,
})
.expect("upsert policy admin user");
state
.control_plane()
.create_session(CreateSessionInput {
id: None,
user_id: "liaibo".into(),
token_hash: session_token_hash("raw-policy-admin-session-token"),
user_agent: None,
ip_hash: None,
expires_at: None,
})
.expect("create policy admin session");
let response = build_app(state)
.oneshot(
Request::builder()
.uri("/api/auth/session")
.header("cookie", "mnote_session=raw-policy-admin-session-token")
.body(Body::empty())
.expect("request"),
)
.await
.expect("response");
std::env::remove_var("MNOTE_LOCAL_ACCESS_POLICY_FILE");
let _ = std::fs::remove_dir_all(&policy_root);
assert_eq!(response.status(), StatusCode::OK);
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["userId"], "liaibo");
assert_eq!(payload["actorType"], "admin");
assert_eq!(payload["authMode"], "sqliteSession");
}
#[tokio::test]
async fn legacy_whoami_alias_prefers_forwarded_actor_identity() {
let response = app()