chore: align sqlite control plane architecture
- replace default Convex control-plane wording with Rust SQLite control-plane across architecture, AGENTS, Reasonix, and design docs - retire root Convex functions source and deploy script into recycle while keeping explicit cloud/compat/sync-replica boundaries - add control-plane migration guard/docs and keep CodeGraph refreshed after the SQLite control-plane cutover
This commit is contained in:
@@ -5,10 +5,12 @@ use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||
use control_plane::session_token_hash;
|
||||
use serde::Serialize;
|
||||
|
||||
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
|
||||
const COOKIE_CONVEX_AUTH_JWT: &str = "__convexAuthJWT";
|
||||
const COOKIE_MNOTE_SESSION: &str = "mnote_session";
|
||||
const COOKIE_ACTOR_EMAIL: &str = "mnote_actor_email";
|
||||
const COOKIE_ACTOR_NAME: &str = "mnote_actor_name";
|
||||
|
||||
@@ -65,6 +67,28 @@ pub async fn refresh_session(
|
||||
}
|
||||
|
||||
fn build_session_response(state: &AppState, context: RequestContext) -> SessionResponse {
|
||||
if let Some(raw_token) = context
|
||||
.auth
|
||||
.cookie_header
|
||||
.as_deref()
|
||||
.and_then(|cookies| raw_cookie_value(cookies, COOKIE_MNOTE_SESSION))
|
||||
{
|
||||
let token_hash = session_token_hash(&raw_token);
|
||||
if let Ok(Some(resolved)) = state.control_plane().get_session_by_token_hash(&token_hash) {
|
||||
return SessionResponse {
|
||||
ok: true,
|
||||
owner: "mnote-web",
|
||||
user_id: resolved.user.id,
|
||||
email: resolved.user.email.unwrap_or_default(),
|
||||
name: resolved.user.display_name,
|
||||
actor_type: "user".to_string(),
|
||||
auth_mode: "sqliteSession",
|
||||
request_id: context.trace.request_id,
|
||||
trace_id: context.trace.trace_id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let actor_id = context.auth.actor_id.trim();
|
||||
let has_forwarded_actor = !actor_id.is_empty() && actor_id != "anonymous";
|
||||
let actor_email = context
|
||||
@@ -179,6 +203,7 @@ mod tests {
|
||||
use crate::app::{build_app, AppConfig, AppState};
|
||||
use axum::body::{to_bytes, Body};
|
||||
use axum::http::{Request, StatusCode};
|
||||
use control_plane::{session_token_hash, CreateSessionInput, UpsertUserInput};
|
||||
use tower::util::ServiceExt;
|
||||
|
||||
fn app() -> axum::Router {
|
||||
@@ -282,6 +307,75 @@ mod tests {
|
||||
assert!(payload.get("convexAdminKey").is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_prefers_sqlite_cookie_identity_over_dev_fallback() {
|
||||
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("shujuan".into()),
|
||||
email: Some("shujuan@163.com".into()),
|
||||
username: "shujuan".into(),
|
||||
display_name: "shujuan".into(),
|
||||
role: None,
|
||||
password_hash: None,
|
||||
})
|
||||
.expect("upsert user");
|
||||
state
|
||||
.control_plane()
|
||||
.create_session(CreateSessionInput {
|
||||
id: None,
|
||||
user_id: "shujuan".into(),
|
||||
token_hash: session_token_hash("raw-session-token"),
|
||||
user_agent: None,
|
||||
ip_hash: None,
|
||||
expires_at: None,
|
||||
})
|
||||
.expect("create session");
|
||||
|
||||
let response = build_app(state)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/auth/session")
|
||||
.header("cookie", "mnote_session=raw-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["owner"], "mnote-web");
|
||||
assert_eq!(payload["userId"], "shujuan");
|
||||
assert_eq!(payload["email"], "shujuan@163.com");
|
||||
assert_eq!(payload["name"], "shujuan");
|
||||
assert_eq!(payload["actorType"], "user");
|
||||
assert_eq!(payload["authMode"], "sqliteSession");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_whoami_alias_prefers_forwarded_actor_identity() {
|
||||
let response = app()
|
||||
|
||||
Reference in New Issue
Block a user