feat: advance local-first workspace checklist
- add admin access-policy UI and local access control surfaces - add local markdown conflict resolution UI and smoke coverage - add ACP local agent changed-files audit scaffold and read-only write guard - document current P0-P2 checklist progress and verification evidence
This commit is contained in:
@@ -17,6 +17,8 @@ const HEADER_SESSION_ID: &str = "x-mnote-session-id";
|
||||
const HEADER_SOURCE_CHANNEL: &str = "x-mnote-source-channel";
|
||||
const HEADER_SOURCE_CLIENT: &str = "x-mnote-source-client";
|
||||
const HEADER_IDEMPOTENCY_KEY: &str = "x-idempotency-key";
|
||||
const COOKIE_ACTOR_ID: &str = "mnote_actor_id";
|
||||
const COOKIE_ACTOR_TYPE: &str = "mnote_actor_type";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -79,8 +81,10 @@ impl RequestContext {
|
||||
authorization: header_value(headers, axum::http::header::AUTHORIZATION.as_str()),
|
||||
cookie_header: header_value(headers, axum::http::header::COOKIE.as_str()),
|
||||
actor_id: header_value(headers, HEADER_ACTOR_ID)
|
||||
.or_else(|| cookie_value(headers, COOKIE_ACTOR_ID))
|
||||
.unwrap_or_else(|| "anonymous".into()),
|
||||
actor_type: header_value(headers, HEADER_ACTOR_TYPE)
|
||||
.or_else(|| cookie_value(headers, COOKIE_ACTOR_TYPE))
|
||||
.unwrap_or_else(|| "anonymous".into()),
|
||||
session_id: header_value(headers, HEADER_SESSION_ID),
|
||||
},
|
||||
@@ -106,6 +110,10 @@ impl RequestContext {
|
||||
if let Some(workspace_id) = &self.workspace.workspace_id {
|
||||
insert_header(headers, HEADER_WORKSPACE_ID, workspace_id);
|
||||
}
|
||||
if self.auth.actor_id.trim() != "anonymous" && !self.auth.actor_id.trim().is_empty() {
|
||||
append_cookie(headers, COOKIE_ACTOR_ID, self.auth.actor_id.trim());
|
||||
append_cookie(headers, COOKIE_ACTOR_TYPE, self.auth.actor_type.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +149,14 @@ fn insert_header(headers: &mut HeaderMap, key: &str, value: &str) {
|
||||
headers.insert(name, value);
|
||||
}
|
||||
|
||||
fn append_cookie(headers: &mut HeaderMap, name: &str, value: &str) {
|
||||
let cookie = format!("{name}={value}; Path=/; HttpOnly; SameSite=Lax");
|
||||
let Ok(header_value) = HeaderValue::from_str(&cookie) else {
|
||||
return;
|
||||
};
|
||||
headers.append(axum::http::header::SET_COOKIE, header_value);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -164,4 +180,40 @@ mod tests {
|
||||
assert_eq!(context.workspace.workspace_id.as_deref(), Some("ws_demo"));
|
||||
assert_eq!(context.auth.actor_id, "user_demo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_context_falls_back_to_actor_cookies() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
axum::http::header::COOKIE,
|
||||
HeaderValue::from_static("mnote_actor_id=user_cookie; mnote_actor_type=user"),
|
||||
);
|
||||
|
||||
let context = RequestContext::from_http_parts(
|
||||
&Method::GET,
|
||||
&"/".parse::<Uri>().expect("uri"),
|
||||
&headers,
|
||||
);
|
||||
|
||||
assert_eq!(context.auth.actor_id, "user_cookie");
|
||||
assert_eq!(context.auth.actor_type, "user");
|
||||
}
|
||||
}
|
||||
|
||||
fn cookie_value(headers: &HeaderMap, name: &str) -> Option<String> {
|
||||
let cookie_header = headers
|
||||
.get(axum::http::header::COOKIE)
|
||||
.and_then(|value| value.to_str().ok())?;
|
||||
for part in cookie_header.split(';') {
|
||||
let Some((cookie_name, cookie_value)) = part.trim().split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
if cookie_name.trim() == name {
|
||||
let value = cookie_value.trim();
|
||||
if !value.is_empty() {
|
||||
return Some(value.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user