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:
@@ -1,267 +1,118 @@
|
||||
//! MNOTE 管理员目录授权页面组件
|
||||
//! MNOTE 文件夹授权管理组件
|
||||
|
||||
use crate::ssr::pages::layout::PageLayout;
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn AdminAccessPolicyPage(
|
||||
pub fn AdminAccessPolicyPanel(
|
||||
#[prop(optional)] workspace_name: Option<String>,
|
||||
#[prop(optional)] policy_path: Option<String>,
|
||||
#[prop(optional)] share_grants_path: Option<String>,
|
||||
#[prop(optional, default = true)] is_admin: bool,
|
||||
#[prop(optional, default = true)] boot_script: bool,
|
||||
) -> impl IntoView {
|
||||
let workspace_name = workspace_name
|
||||
.unwrap_or_else(|| "开发用户 的空间".to_string())
|
||||
.trim()
|
||||
.to_string();
|
||||
let policy_path = policy_path
|
||||
.unwrap_or_else(|| "/mnt/Data1T/Mnote_data/control-plane/access-policy.json".to_string());
|
||||
let share_grants_path = share_grants_path
|
||||
.unwrap_or_else(|| "/mnt/Data1T/Mnote_data/control-plane/share-grants.json".to_string());
|
||||
let page_title = "授权管理".to_string();
|
||||
.unwrap_or_else(|| "/mnt/Data1T/Mnote_data/control-plane/control-plane.db".to_string());
|
||||
let scope_text = if is_admin {
|
||||
"管理员可以授权任意本地文件夹。"
|
||||
} else {
|
||||
"普通用户只能授权自己空间下的文件夹。"
|
||||
};
|
||||
view! {
|
||||
<PageLayout current_nav={if is_admin { "admin" } else { "home" }} workspace_name={workspace_name.clone()} topbar_title={page_title.clone()} show_admin_access_policy={is_admin}>
|
||||
<main class="mnote-admin-policy-page" data-testid="mnote-admin-access-policy-page">
|
||||
<header class="mnote-admin-policy-header">
|
||||
<h1>{page_title.clone()}</h1>
|
||||
<p>{if is_admin {
|
||||
"管理员可以管理目录授权和分享授权。"
|
||||
} else {
|
||||
"查看与你相关的分享授权。"
|
||||
}}</p>
|
||||
</header>
|
||||
<div
|
||||
class="mnote-admin-policy-dialog__content"
|
||||
data-testid="mnote-admin-access-policy-page"
|
||||
data-access-policy-role={if is_admin { "admin" } else { "user" }}
|
||||
>
|
||||
<header class="mnote-admin-policy-header">
|
||||
<div>
|
||||
<div class="mnote-admin-policy-eyebrow">"授权管理"</div>
|
||||
<h2 id="mnote-admin-policy-dialog-title">"文件夹授权"</h2>
|
||||
<p>{scope_text}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="mnote-admin-policy-summary">
|
||||
<div class="mnote-admin-policy-summary-item">
|
||||
<span class="mnote-admin-policy-summary-label">"工作区"</span>
|
||||
<strong data-testid="mnote-admin-workspace-name">{workspace_name.clone()}</strong>
|
||||
</div>
|
||||
<div class="mnote-admin-policy-summary-item">
|
||||
<span class="mnote-admin-policy-summary-label">"策略文件"</span>
|
||||
<code data-testid="mnote-admin-policy-path">{if is_admin { policy_path.clone() } else { "仅管理员可见".to_string() }}</code>
|
||||
</div>
|
||||
<div class="mnote-admin-policy-summary-item">
|
||||
<span class="mnote-admin-policy-summary-label">"分享授权文件"</span>
|
||||
<code data-testid="mnote-admin-share-grants-path">{share_grants_path.clone()}</code>
|
||||
</div>
|
||||
</section>
|
||||
<section class="mnote-admin-policy-summary">
|
||||
<div class="mnote-admin-policy-summary-item">
|
||||
<span class="mnote-admin-policy-summary-label">"工作区"</span>
|
||||
<strong data-testid="mnote-admin-workspace-name">{workspace_name.clone()}</strong>
|
||||
</div>
|
||||
<div class="mnote-admin-policy-summary-item">
|
||||
<span class="mnote-admin-policy-summary-label">"授权记录"</span>
|
||||
<code data-testid="mnote-admin-share-grants-path">{share_grants_path.clone()}</code>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{if is_admin {
|
||||
view! {
|
||||
<section class="mnote-admin-policy-panel" data-admin-section="directory">
|
||||
<header class="mnote-admin-policy-panel-header">
|
||||
<div>
|
||||
<h2>"目录授权"</h2>
|
||||
<p class="mnote-admin-policy-note">"给指定用户授权可访问的本地目录。"</p>
|
||||
</div>
|
||||
<button type="button" data-testid="mnote-admin-policy-refresh" data-admin-action="refresh-policy">"刷新"</button>
|
||||
</header>
|
||||
<div class="mnote-admin-policy-table" data-testid="mnote-admin-policy-grants-list">
|
||||
<div class="mnote-admin-policy-empty">"正在读取目录授权..."</div>
|
||||
</div>
|
||||
<details class="mnote-admin-policy-debug">
|
||||
<summary>"查看策略 JSON"</summary>
|
||||
<pre class="mnote-admin-policy-json" data-testid="mnote-admin-policy-json">{""}</pre>
|
||||
</details>
|
||||
<div class="mnote-admin-policy-note" data-testid="mnote-admin-policy-message"></div>
|
||||
</section>
|
||||
}.into_any()
|
||||
} else {
|
||||
view! {
|
||||
<section class="mnote-admin-policy-panel" data-admin-section="directory" hidden>
|
||||
<pre class="mnote-admin-policy-json" data-testid="mnote-admin-policy-json">{""}</pre>
|
||||
<div class="mnote-admin-policy-note" data-testid="mnote-admin-policy-message"></div>
|
||||
</section>
|
||||
}.into_any()
|
||||
}}
|
||||
|
||||
{if is_admin {
|
||||
view! {
|
||||
<section class="mnote-admin-policy-grid" data-admin-only="true">
|
||||
<form class="mnote-admin-policy-form" data-admin-form="validate-root">
|
||||
<header><h2>"验证目录"</h2></header>
|
||||
<label>
|
||||
<span>"rootUri"</span>
|
||||
<input data-testid="mnote-admin-root-uri" name="rootUri" type="text" placeholder="file:///mnt/Data1T/Mnote_data/users/..." />
|
||||
</label>
|
||||
<label>
|
||||
<span>"rootPath"</span>
|
||||
<input data-testid="mnote-admin-root-path" name="rootPath" type="text" placeholder="/mnt/Data1T/Mnote_data/..." />
|
||||
</label>
|
||||
<button type="submit" data-testid="mnote-admin-validate-root-submit">"验证"</button>
|
||||
<pre class="mnote-admin-policy-json" data-testid="mnote-admin-validate-result"></pre>
|
||||
</form>
|
||||
|
||||
<form class="mnote-admin-policy-form" data-admin-form="create-grant">
|
||||
<header><h2>"新增授权"</h2></header>
|
||||
<label>
|
||||
<span>"grantId"</span>
|
||||
<input data-testid="mnote-admin-grant-id" name="grantId" type="text" placeholder="可留空自动生成" />
|
||||
</label>
|
||||
<label>
|
||||
<span>"userId"</span>
|
||||
<input data-testid="mnote-admin-grant-user-id" name="userId" type="text" placeholder="user_123" required />
|
||||
</label>
|
||||
<label>
|
||||
<span>"rootUri"</span>
|
||||
<input data-testid="mnote-admin-grant-root-uri" name="rootUri" type="text" />
|
||||
</label>
|
||||
<label>
|
||||
<span>"rootPath"</span>
|
||||
<input data-testid="mnote-admin-grant-root-path" name="rootPath" type="text" />
|
||||
</label>
|
||||
<label>
|
||||
<span>"permission"</span>
|
||||
<select data-testid="mnote-admin-grant-permission" name="permission">
|
||||
<option value="read">"read"</option>
|
||||
<option value="write">"write"</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>"recursive"</span>
|
||||
<input data-testid="mnote-admin-grant-recursive" name="recursive" type="checkbox" checked=true />
|
||||
</label>
|
||||
<label>
|
||||
<span>"capabilities"</span>
|
||||
<input data-testid="mnote-admin-grant-capabilities" name="capabilities" type="text" placeholder="ai,share" />
|
||||
</label>
|
||||
<button type="submit" data-testid="mnote-admin-create-grant-submit">"创建"</button>
|
||||
<pre class="mnote-admin-policy-json" data-testid="mnote-admin-create-result"></pre>
|
||||
</form>
|
||||
|
||||
<form class="mnote-admin-policy-form" data-admin-form="delete-grant">
|
||||
<header><h2>"删除授权"</h2></header>
|
||||
<label>
|
||||
<span>"grantId"</span>
|
||||
<input data-testid="mnote-admin-delete-grant-id" name="grantId" type="text" placeholder="grant_xxx" required />
|
||||
</label>
|
||||
<button type="submit" data-testid="mnote-admin-delete-grant-submit">"删除"</button>
|
||||
<pre class="mnote-admin-policy-json" data-testid="mnote-admin-delete-result"></pre>
|
||||
</form>
|
||||
</section>
|
||||
}.into_any()
|
||||
} else {
|
||||
view! {}.into_any()
|
||||
}}
|
||||
<section class="mnote-admin-policy-grid">
|
||||
<form class="mnote-admin-policy-form" data-admin-form="create-share-grant">
|
||||
<header><h3>"新增文件夹授权"</h3></header>
|
||||
<label>
|
||||
<span>"文件夹地址"</span>
|
||||
<input data-testid="mnote-admin-share-root-path" name="rootPath" type="text" placeholder="/mnt/Data1T/Mnote_data/users/..." required=true />
|
||||
</label>
|
||||
<label>
|
||||
<span>"授权用户 ID"</span>
|
||||
<input data-testid="mnote-admin-share-target-user-id" name="targetUserId" type="text" placeholder="user_123" required=true />
|
||||
</label>
|
||||
<label>
|
||||
<span>"权限"</span>
|
||||
<select data-testid="mnote-admin-share-permission" name="sharePermission">
|
||||
<option value="read">"read"</option>
|
||||
<option value="write">"write"</option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit" data-testid="mnote-admin-create-share-grant-submit">"创建授权"</button>
|
||||
<pre class="mnote-admin-policy-json" data-testid="mnote-admin-create-share-grant-result"></pre>
|
||||
</form>
|
||||
|
||||
<section class="mnote-admin-policy-panel" data-testid="mnote-admin-share-grants-panel">
|
||||
<header class="mnote-admin-policy-panel-header">
|
||||
<div>
|
||||
<h2>"分享管理"</h2>
|
||||
<p class="mnote-admin-policy-note">{if is_admin {
|
||||
"查看、创建和撤销分享授权。"
|
||||
} else {
|
||||
"查看你创建或接收的分享授权。"
|
||||
}}</p>
|
||||
<h3>"授权列表"</h3>
|
||||
<p class="mnote-admin-policy-note">"查看当前可管理的文件夹授权,并撤销不再需要的授权。"</p>
|
||||
</div>
|
||||
<button type="button" data-testid="mnote-admin-share-grants-refresh" data-admin-action="refresh-share-grants">"刷新"</button>
|
||||
</header>
|
||||
<div class="mnote-admin-policy-table" data-testid="mnote-admin-share-grants-list">
|
||||
<div class="mnote-admin-policy-empty">"正在读取分享授权..."</div>
|
||||
<div class="mnote-admin-policy-empty">"正在读取授权..."</div>
|
||||
</div>
|
||||
<details class="mnote-admin-policy-debug">
|
||||
<summary>"查看分享授权 JSON"</summary>
|
||||
<summary>"查看授权 JSON"</summary>
|
||||
<pre class="mnote-admin-policy-json" data-testid="mnote-admin-share-grants-json">{""}</pre>
|
||||
</details>
|
||||
<div class="mnote-admin-policy-note" data-testid="mnote-admin-share-grants-message"></div>
|
||||
<pre class="mnote-admin-policy-json" data-testid="mnote-admin-delete-share-grant-result" hidden></pre>
|
||||
</section>
|
||||
|
||||
<section class="mnote-admin-policy-grid" data-admin-only={if is_admin { "true" } else { "false" }}>
|
||||
<form class="mnote-admin-policy-form" data-admin-form="create-share-grant">
|
||||
<header><h2>"新增分享授权"</h2></header>
|
||||
<label>
|
||||
<span>"grantId"</span>
|
||||
<input data-testid="mnote-admin-share-grant-id" name="shareGrantId" type="text" placeholder="可留空自动生成" />
|
||||
</label>
|
||||
{if is_admin {
|
||||
view! {
|
||||
<label>
|
||||
<span>"ownerUserId"</span>
|
||||
<input data-testid="mnote-admin-share-owner-user-id" name="ownerUserId" type="text" placeholder="owner_123" required />
|
||||
</label>
|
||||
}.into_any()
|
||||
} else {
|
||||
view! {
|
||||
<input type="hidden" data-testid="mnote-admin-share-owner-user-id" name="ownerUserId" value="" />
|
||||
}.into_any()
|
||||
}}
|
||||
<label>
|
||||
<span>"shareId"</span>
|
||||
<input data-testid="mnote-admin-share-id" name="shareId" type="text" placeholder="share_xxx" required />
|
||||
</label>
|
||||
<label>
|
||||
<span>"targetUserId"</span>
|
||||
<input data-testid="mnote-admin-share-target-user-id" name="targetUserId" type="text" placeholder="target_123" required />
|
||||
</label>
|
||||
<label>
|
||||
<span>"rootUri"</span>
|
||||
<input data-testid="mnote-admin-share-root-uri" name="shareRootUri" type="text" />
|
||||
</label>
|
||||
<label>
|
||||
<span>"rootPath"</span>
|
||||
<input data-testid="mnote-admin-share-root-path" name="shareRootPath" type="text" />
|
||||
</label>
|
||||
<label>
|
||||
<span>"documentId"</span>
|
||||
<input data-testid="mnote-admin-share-document-id" name="documentId" type="text" placeholder="doc_123" />
|
||||
</label>
|
||||
<label>
|
||||
<span>"allowedResourceIds"</span>
|
||||
<input data-testid="mnote-admin-share-resource-ids" name="allowedResourceIds" type="text" placeholder="doc_123,mindmap_456" />
|
||||
</label>
|
||||
<label>
|
||||
<span>"permission"</span>
|
||||
<select data-testid="mnote-admin-share-permission" name="sharePermission">
|
||||
<option value="read">"read"</option>
|
||||
<option value="write">"write"</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>"capabilities"</span>
|
||||
<input data-testid="mnote-admin-share-capabilities" name="shareCapabilities" type="text" placeholder="ai,share" />
|
||||
</label>
|
||||
<button type="submit" data-testid="mnote-admin-create-share-grant-submit">"创建分享授权"</button>
|
||||
<pre class="mnote-admin-policy-json" data-testid="mnote-admin-create-share-grant-result"></pre>
|
||||
</form>
|
||||
|
||||
<form class="mnote-admin-policy-form" data-admin-form="delete-share-grant">
|
||||
<header><h2>"撤销分享授权"</h2></header>
|
||||
<label>
|
||||
<span>"shareId 或 grantId"</span>
|
||||
<input data-testid="mnote-admin-delete-share-id" name="deleteShareId" type="text" placeholder="share_xxx" required />
|
||||
</label>
|
||||
<button type="submit" data-testid="mnote-admin-delete-share-grant-submit">"撤销"</button>
|
||||
<pre class="mnote-admin-policy-json" data-testid="mnote-admin-delete-share-grant-result"></pre>
|
||||
</form>
|
||||
</section>
|
||||
<script id="__MNOTE_ACCESS_POLICY_PAGE__" type="application/json">
|
||||
{format!(r#"{{"isAdmin":{}}}"#, if is_admin { "true" } else { "false" })}
|
||||
</script>
|
||||
<script>{ADMIN_POLICY_SCRIPT}</script>
|
||||
</main>
|
||||
</PageLayout>
|
||||
</section>
|
||||
<script id="__MNOTE_ACCESS_POLICY_PAGE__" type="application/json">
|
||||
{format!(r#"{{"isAdmin":{}}}"#, if is_admin { "true" } else { "false" })}
|
||||
</script>
|
||||
{if boot_script {
|
||||
view! { <script>{ADMIN_POLICY_SCRIPT}</script> }.into_any()
|
||||
} else {
|
||||
view! {}.into_any()
|
||||
}}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const ADMIN_POLICY_SCRIPT: &str = r#"
|
||||
pub(crate) const ADMIN_POLICY_SCRIPT: &str = r#"
|
||||
(function () {
|
||||
var root = document.querySelector('[data-testid="mnote-admin-access-policy-page"]');
|
||||
function initAccessPolicyPanel(scope) {
|
||||
var root = (scope || document).querySelector('[data-testid="mnote-admin-access-policy-page"]');
|
||||
if (!root) return;
|
||||
var message = root.querySelector('[data-testid="mnote-admin-policy-message"]');
|
||||
var policyJson = root.querySelector('[data-testid="mnote-admin-policy-json"]');
|
||||
if (root.getAttribute('data-access-policy-ready') === 'true') return;
|
||||
root.setAttribute('data-access-policy-ready', 'true');
|
||||
var shareGrantsJson = root.querySelector('[data-testid="mnote-admin-share-grants-json"]');
|
||||
var validateResult = root.querySelector('[data-testid="mnote-admin-validate-result"]');
|
||||
var createResult = root.querySelector('[data-testid="mnote-admin-create-result"]');
|
||||
var deleteResult = root.querySelector('[data-testid="mnote-admin-delete-result"]');
|
||||
var shareGrantsMessage = root.querySelector('[data-testid="mnote-admin-share-grants-message"]');
|
||||
var createShareGrantResult = root.querySelector('[data-testid="mnote-admin-create-share-grant-result"]');
|
||||
var deleteShareGrantResult = root.querySelector('[data-testid="mnote-admin-delete-share-grant-result"]');
|
||||
var refreshButton = root.querySelector('[data-admin-action="refresh-policy"]');
|
||||
var refreshShareGrantsButton = root.querySelector('[data-admin-action="refresh-share-grants"]');
|
||||
var policyGrantsList = root.querySelector('[data-testid="mnote-admin-policy-grants-list"]');
|
||||
var shareGrantsList = root.querySelector('[data-testid="mnote-admin-share-grants-list"]');
|
||||
var pageConfig = (function () {
|
||||
var node = document.getElementById('__MNOTE_ACCESS_POLICY_PAGE__');
|
||||
var node = root.querySelector('#__MNOTE_ACCESS_POLICY_PAGE__');
|
||||
try { return JSON.parse(node ? node.textContent || '{}' : '{}'); } catch (_) { return {}; }
|
||||
})();
|
||||
var isAdmin = pageConfig.isAdmin === true;
|
||||
@@ -285,76 +136,43 @@ const ADMIN_POLICY_SCRIPT: &str = r#"
|
||||
return '<span class="mnote-admin-policy-badge" data-value="' + escapeHtml(text) + '">' + escapeHtml(text) + '</span>';
|
||||
}
|
||||
|
||||
function renderPolicyGrants(payload) {
|
||||
if (!policyGrantsList) return;
|
||||
var grants = payload && payload.policy && Array.isArray(payload.policy.grants) ? payload.policy.grants : [];
|
||||
if (!grants.length) {
|
||||
policyGrantsList.innerHTML = '<div class="mnote-admin-policy-empty">暂无目录授权</div>';
|
||||
return;
|
||||
}
|
||||
policyGrantsList.innerHTML = grants.map(function(grant) {
|
||||
return '<article class="mnote-admin-policy-row">' +
|
||||
'<div><strong>' + escapeHtml(grant.userId || grant.user_id || '未知用户') + '</strong><span>' + escapeHtml(grant.rootPath || grant.root_path || grant.rootUri || grant.root_uri || '') + '</span></div>' +
|
||||
'<div>' + renderBadge(grant.permission || grant.access) + '</div>' +
|
||||
'<div><span>' + escapeHtml((grant.capabilities || []).join(', ') || '无能力标记') + '</span></div>' +
|
||||
'</article>';
|
||||
}).join('');
|
||||
function readDirectoryGrants(payload) {
|
||||
if (payload && Array.isArray(payload.grants)) return payload.grants;
|
||||
if (payload && payload.policy && Array.isArray(payload.policy.grants)) return payload.policy.grants;
|
||||
return [];
|
||||
}
|
||||
|
||||
function accessPolicyUrl(suffix) {
|
||||
var base = isAdmin ? '/api/admin/access-policy' : '/api/user/access-policy';
|
||||
return suffix ? base + suffix : base;
|
||||
}
|
||||
|
||||
function renderShareGrants(payload) {
|
||||
if (!shareGrantsList) return;
|
||||
var grants = payload && Array.isArray(payload.grants) ? payload.grants : [];
|
||||
var grants = readDirectoryGrants(payload);
|
||||
if (!grants.length) {
|
||||
shareGrantsList.innerHTML = '<div class="mnote-admin-policy-empty">暂无分享授权</div>';
|
||||
shareGrantsList.innerHTML = '<div class="mnote-admin-policy-empty">暂无文件夹授权</div>';
|
||||
return;
|
||||
}
|
||||
shareGrantsList.innerHTML = grants.map(function(grant) {
|
||||
var active = grant.active === false ? '已撤销' : '有效';
|
||||
var active = grant.active === false || grant.status === 'revoked' ? '已撤销' : '有效';
|
||||
var revokeButton = grant.active === false ? '' :
|
||||
'<button type="button" data-admin-action="revoke-access-grant" data-grant-id="' + escapeHtml(grant.id || '') + '">撤销</button>';
|
||||
return '<article class="mnote-admin-policy-row">' +
|
||||
'<div><strong>' + escapeHtml(grant.shareId || grant.id || '未命名分享') + '</strong><span>' + escapeHtml(grant.rootPath || grant.rootUri || '') + '</span></div>' +
|
||||
'<div><strong>' + escapeHtml(grant.rootPath || grant.rootUri || '') + '</strong><span>授权用户 ' + escapeHtml(grant.targetUserId || grant.userId || '') + '</span></div>' +
|
||||
'<div>' + renderBadge(grant.permission) + renderBadge(active) + '</div>' +
|
||||
'<div><span>所有者 ' + escapeHtml(grant.ownerUserId || '') + '</span><span>接收者 ' + escapeHtml(grant.targetUserId || '') + '</span></div>' +
|
||||
'<div><span>创建者 ' + escapeHtml(grant.createdBy || grant.ownerUserId || '') + '</span><span>授权 ID ' + escapeHtml(grant.id || '') + '</span>' + revokeButton + '</div>' +
|
||||
'</article>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function formValues(form) {
|
||||
var data = new FormData(form);
|
||||
var capabilities = String(data.get('capabilities') || '')
|
||||
.split(/[,\s]+/)
|
||||
.map(function (item) { return item.trim(); })
|
||||
.filter(Boolean);
|
||||
return {
|
||||
id: String(data.get('grantId') || '').trim(),
|
||||
userId: String(data.get('userId') || '').trim(),
|
||||
rootUri: String(data.get('rootUri') || '').trim(),
|
||||
rootPath: String(data.get('rootPath') || '').trim(),
|
||||
permission: String(data.get('permission') || 'read').trim(),
|
||||
recursive: data.get('recursive') === 'on' || data.get('recursive') === 'true',
|
||||
capabilities: capabilities,
|
||||
};
|
||||
}
|
||||
|
||||
function splitList(value) {
|
||||
return String(value || '')
|
||||
.split(/[,\s]+/)
|
||||
.map(function (item) { return item.trim(); })
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function shareGrantFormValues(form) {
|
||||
var data = new FormData(form);
|
||||
return {
|
||||
id: String(data.get('shareGrantId') || '').trim(),
|
||||
shareId: String(data.get('shareId') || '').trim(),
|
||||
ownerUserId: String(data.get('ownerUserId') || '').trim(),
|
||||
userId: String(data.get('targetUserId') || '').trim(),
|
||||
targetUserId: String(data.get('targetUserId') || '').trim(),
|
||||
rootUri: String(data.get('shareRootUri') || '').trim(),
|
||||
rootPath: String(data.get('shareRootPath') || '').trim(),
|
||||
documentId: String(data.get('documentId') || '').trim(),
|
||||
allowedResourceIds: splitList(data.get('allowedResourceIds')),
|
||||
rootPath: String(data.get('rootPath') || '').trim(),
|
||||
permission: String(data.get('sharePermission') || 'read').trim(),
|
||||
capabilities: splitList(data.get('shareCapabilities')),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -371,101 +189,29 @@ const ADMIN_POLICY_SCRIPT: &str = r#"
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function refreshPolicy() {
|
||||
if (!isAdmin) return;
|
||||
var payload = await requestJson('/api/admin/access-policy', { method: 'GET', headers: {} });
|
||||
setText(policyJson, payload);
|
||||
renderPolicyGrants(payload);
|
||||
setText(message, '已刷新策略');
|
||||
}
|
||||
|
||||
async function refreshShareGrants() {
|
||||
var payload = await requestJson(isAdmin ? '/api/admin/share-grants' : '/api/user/share-grants', { method: 'GET', headers: {} });
|
||||
var payload = await requestJson(accessPolicyUrl(''), { method: 'GET', headers: {} });
|
||||
setText(shareGrantsJson, payload);
|
||||
renderShareGrants(payload);
|
||||
setText(shareGrantsMessage, '已刷新分享授权');
|
||||
setText(shareGrantsMessage, '已刷新授权列表');
|
||||
}
|
||||
|
||||
refreshButton && refreshButton.addEventListener('click', function () {
|
||||
setText(message, '正在刷新策略...');
|
||||
refreshPolicy().catch(function (error) { setText(message, error.message || '刷新失败'); });
|
||||
});
|
||||
|
||||
refreshShareGrantsButton && refreshShareGrantsButton.addEventListener('click', function () {
|
||||
setText(shareGrantsMessage, '正在刷新分享授权...');
|
||||
setText(shareGrantsMessage, '正在刷新授权列表...');
|
||||
refreshShareGrants().catch(function (error) { setText(shareGrantsMessage, error.message || '刷新失败'); });
|
||||
});
|
||||
|
||||
var validateRootForm = root.querySelector('[data-admin-form="validate-root"]');
|
||||
if (validateRootForm) {
|
||||
validateRootForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
var values = formValues(event.currentTarget);
|
||||
setText(validateResult, '正在验证...');
|
||||
requestJson('/api/admin/access-policy/validate-root', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ rootUri: values.rootUri, rootPath: values.rootPath }),
|
||||
}).then(function (payload) {
|
||||
setText(validateResult, payload);
|
||||
setText(message, '目录验证完成');
|
||||
}).catch(function (error) {
|
||||
setText(validateResult, { ok: false, error: error.message || '验证失败' });
|
||||
setText(message, error.message || '验证失败');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var createGrantForm = root.querySelector('[data-admin-form="create-grant"]');
|
||||
if (createGrantForm) {
|
||||
createGrantForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
var values = formValues(event.currentTarget);
|
||||
setText(createResult, '正在创建...');
|
||||
requestJson('/api/admin/access-policy/grants', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(values),
|
||||
}).then(function (payload) {
|
||||
setText(createResult, payload);
|
||||
setText(message, '授权已创建');
|
||||
return refreshPolicy();
|
||||
}).catch(function (error) {
|
||||
setText(createResult, { ok: false, error: error.message || '创建失败' });
|
||||
setText(message, error.message || '创建失败');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var deleteGrantForm = root.querySelector('[data-admin-form="delete-grant"]');
|
||||
if (deleteGrantForm) {
|
||||
deleteGrantForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
var values = formValues(event.currentTarget);
|
||||
var grantId = values.id;
|
||||
setText(deleteResult, '正在删除...');
|
||||
requestJson('/api/admin/access-policy/grants/' + encodeURIComponent(grantId), {
|
||||
method: 'DELETE',
|
||||
headers: {},
|
||||
}).then(function (payload) {
|
||||
setText(deleteResult, payload);
|
||||
setText(message, '授权已删除');
|
||||
return refreshPolicy();
|
||||
}).catch(function (error) {
|
||||
setText(deleteResult, { ok: false, error: error.message || '删除失败' });
|
||||
setText(message, error.message || '删除失败');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
root.querySelector('[data-admin-form="create-share-grant"]').addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
var values = shareGrantFormValues(event.currentTarget);
|
||||
setText(createShareGrantResult, '正在创建...');
|
||||
requestJson(isAdmin ? '/api/admin/share-grants' : '/api/user/share-grants', {
|
||||
requestJson(accessPolicyUrl('/grants'), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(values),
|
||||
}).then(function (payload) {
|
||||
setText(createShareGrantResult, payload);
|
||||
setText(shareGrantsMessage, '分享授权已创建');
|
||||
setText(shareGrantsMessage, '文件夹授权已创建');
|
||||
event.currentTarget.reset();
|
||||
return refreshShareGrants();
|
||||
}).catch(function (error) {
|
||||
setText(createShareGrantResult, { ok: false, error: error.message || '创建失败' });
|
||||
@@ -473,17 +219,19 @@ const ADMIN_POLICY_SCRIPT: &str = r#"
|
||||
});
|
||||
});
|
||||
|
||||
root.querySelector('[data-admin-form="delete-share-grant"]').addEventListener('submit', function (event) {
|
||||
shareGrantsList && shareGrantsList.addEventListener('click', function(event) {
|
||||
var button = event.target && event.target.closest ? event.target.closest('[data-admin-action="revoke-access-grant"]') : null;
|
||||
if (!button) return;
|
||||
event.preventDefault();
|
||||
var data = new FormData(event.currentTarget);
|
||||
var shareId = String(data.get('deleteShareId') || '').trim();
|
||||
var grantId = String(button.getAttribute('data-grant-id') || '').trim();
|
||||
if (!grantId) return;
|
||||
setText(deleteShareGrantResult, '正在撤销...');
|
||||
requestJson((isAdmin ? '/api/admin/share-grants/' : '/api/user/share-grants/') + encodeURIComponent(shareId), {
|
||||
requestJson(accessPolicyUrl('/grants/') + encodeURIComponent(grantId), {
|
||||
method: 'DELETE',
|
||||
headers: {},
|
||||
}).then(function (payload) {
|
||||
setText(deleteShareGrantResult, payload);
|
||||
setText(shareGrantsMessage, '分享授权已撤销');
|
||||
setText(shareGrantsMessage, '文件夹授权已撤销');
|
||||
return refreshShareGrants();
|
||||
}).catch(function (error) {
|
||||
setText(deleteShareGrantResult, { ok: false, error: error.message || '撤销失败' });
|
||||
@@ -491,16 +239,16 @@ const ADMIN_POLICY_SCRIPT: &str = r#"
|
||||
});
|
||||
});
|
||||
|
||||
if (isAdmin) {
|
||||
refreshPolicy().catch(function (error) {
|
||||
setText(message, error.message || '加载策略失败');
|
||||
renderPolicyGrants(null);
|
||||
});
|
||||
}
|
||||
refreshShareGrants().catch(function (error) {
|
||||
setText(shareGrantsMessage, error.message || '加载分享授权失败');
|
||||
setText(shareGrantsMessage, error.message || '加载授权失败');
|
||||
renderShareGrants(null);
|
||||
});
|
||||
}
|
||||
window.MNOTEInitAccessPolicyPanel = initAccessPolicyPanel;
|
||||
var shell = document.body && document.body.getAttribute('data-mnote-shell');
|
||||
if (shell === 'admin' || shell === 'user-access-policy') {
|
||||
initAccessPolicyPanel(document);
|
||||
}
|
||||
})();
|
||||
"#;
|
||||
|
||||
@@ -510,21 +258,38 @@ mod tests {
|
||||
use crate::ssr::render_view;
|
||||
|
||||
#[test]
|
||||
fn admin_access_policy_page_renders_admin_controls() {
|
||||
fn admin_access_policy_panel_renders_minimal_folder_grant_controls() {
|
||||
let html = render_view(view! {
|
||||
<AdminAccessPolicyPage
|
||||
<AdminAccessPolicyPanel
|
||||
workspace_name={"我的空间".to_string()}
|
||||
policy_path={"/mnt/Data1T/Mnote_data/control-plane/access-policy.json".to_string()}
|
||||
is_admin=true
|
||||
/>
|
||||
});
|
||||
assert!(html.contains("mnote-admin-access-policy-page"));
|
||||
assert!(html.contains("mnote-admin-policy-json"));
|
||||
assert!(html.contains("文件夹授权"));
|
||||
assert!(html.contains("文件夹地址"));
|
||||
assert!(html.contains("授权用户 ID"));
|
||||
assert!(html.contains("read"));
|
||||
assert!(html.contains("write"));
|
||||
assert!(html.contains("mnote-admin-share-grants-panel"));
|
||||
assert!(html.contains("mnote-admin-share-grants-json"));
|
||||
assert!(html.contains("mnote-admin-validate-root-submit"));
|
||||
assert!(html.contains("mnote-admin-create-grant-submit"));
|
||||
assert!(html.contains("mnote-admin-delete-grant-submit"));
|
||||
assert!(html.contains("mnote-admin-create-share-grant-submit"));
|
||||
assert!(html.contains("mnote-admin-delete-share-grant-submit"));
|
||||
assert!(html.contains("/api/admin/access-policy"));
|
||||
assert!(html.contains("/api/user/access-policy"));
|
||||
assert!(html.contains("data-mnote-shell"));
|
||||
assert!(!html.contains("/api/admin/share-grants"));
|
||||
assert!(!html.contains("/api/user/share-grants"));
|
||||
assert!(
|
||||
html.contains("data-admin-action="revoke-access-grant"")
|
||||
|| html.contains("revoke-access-grant")
|
||||
);
|
||||
assert!(!html.contains("name=\"grantId\""));
|
||||
assert!(!html.contains("name=\"shareId\""));
|
||||
assert!(!html.contains("share_xxx"));
|
||||
assert!(!html.contains("name=\"ownerUserId\""));
|
||||
assert!(!html.contains("documentId"));
|
||||
assert!(!html.contains("allowedResourceIds"));
|
||||
assert!(!html.contains("capabilities"));
|
||||
assert!(!html.contains("mnote-admin-validate-root-submit"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! MNOTE 通用页面布局组件(Wolai / Notion 风格)
|
||||
|
||||
use crate::ssr::pages::admin::AdminAccessPolicyPanel;
|
||||
use leptos::prelude::*;
|
||||
|
||||
const SIDEBAR_TREE_JS: &str = r##"
|
||||
@@ -1227,6 +1228,12 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
|
||||
}
|
||||
|
||||
function closeAdminAccessPolicyDialog() {
|
||||
var existing = document.querySelector('[data-testid="mnote-admin-access-policy-modal"]');
|
||||
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
|
||||
document.documentElement.removeAttribute('data-mnote-admin-access-policy-modal-open');
|
||||
}
|
||||
|
||||
function accountMenuFallbackSession() {
|
||||
var body = document.body instanceof HTMLElement ? document.body : null;
|
||||
var actorId = body ? String(body.getAttribute('data-mnote-actor-id') || '').trim() : '';
|
||||
@@ -1263,10 +1270,6 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
return String(session && session.actorType || '').trim() === 'admin';
|
||||
}
|
||||
|
||||
function accessPolicyHrefForSession(session) {
|
||||
return sessionIsAdmin(session) ? '/admin/access-policy' : '/user/access-policy';
|
||||
}
|
||||
|
||||
async function loadAccountInfo(menu) {
|
||||
try {
|
||||
var response = await fetch('/api/auth/session', {
|
||||
@@ -1346,6 +1349,43 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
if (closeButton instanceof HTMLElement) closeButton.focus();
|
||||
}
|
||||
|
||||
function openAdminAccessPolicyDialog(trigger) {
|
||||
closeAdminAccessPolicyDialog();
|
||||
closeAccountMenu();
|
||||
if (!(trigger instanceof HTMLElement)) return;
|
||||
var role = trigger.getAttribute('data-access-policy-role') === 'admin' ? 'admin' : 'user';
|
||||
var template = document.querySelector('[data-testid="mnote-admin-access-policy-template-' + role + '"]');
|
||||
var dialog = document.createElement('div');
|
||||
dialog.className = 'mnote-admin-policy-modal';
|
||||
dialog.setAttribute('data-testid', 'mnote-admin-access-policy-modal');
|
||||
dialog.setAttribute('role', 'dialog');
|
||||
dialog.setAttribute('aria-modal', 'true');
|
||||
dialog.setAttribute('aria-labelledby', 'mnote-admin-policy-dialog-title');
|
||||
dialog.innerHTML =
|
||||
'<div class="mnote-admin-policy-modal__backdrop" data-admin-access-policy-close></div>' +
|
||||
'<section class="mnote-admin-policy-modal__panel">' +
|
||||
'<button type="button" class="mnote-admin-policy-modal__close" data-admin-access-policy-close aria-label="关闭授权管理"><span class="material-symbols-outlined" data-icon="close" aria-hidden="true"></span></button>' +
|
||||
'<div class="mnote-admin-policy-modal__content" data-testid="mnote-admin-access-policy-modal-content"></div>' +
|
||||
'</section>';
|
||||
var content = dialog.querySelector('[data-testid="mnote-admin-access-policy-modal-content"]');
|
||||
if (content) {
|
||||
content.innerHTML = template ? template.innerHTML : '<div class="mnote-admin-policy-empty">授权管理面板加载失败</div>';
|
||||
}
|
||||
if (typeof window.MNOTEInitAccessPolicyPanel === 'function') {
|
||||
window.MNOTEInitAccessPolicyPanel(dialog);
|
||||
}
|
||||
dialog.querySelectorAll('[data-admin-access-policy-close]').forEach(function(button) {
|
||||
button.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
closeAdminAccessPolicyDialog();
|
||||
});
|
||||
});
|
||||
document.body.appendChild(dialog);
|
||||
document.documentElement.setAttribute('data-mnote-admin-access-policy-modal-open', 'true');
|
||||
var closeButton = dialog.querySelector('.mnote-admin-policy-modal__close');
|
||||
if (closeButton instanceof HTMLElement) closeButton.focus();
|
||||
}
|
||||
|
||||
async function signOutAccount(trigger) {
|
||||
setCommandPending(trigger, true);
|
||||
try {
|
||||
@@ -1382,7 +1422,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
menu.setAttribute('role', 'menu');
|
||||
menu.innerHTML =
|
||||
'<button type="button" class="mnote-workspace-source-menu__item mnote-account-menu__item" data-testid="mnote-account-profile" role="menuitem"><span class="material-symbols-outlined" data-icon="account_circle" aria-hidden="true"></span><span>个人信息</span></button>' +
|
||||
'<a class="mnote-workspace-source-menu__item mnote-account-menu__item" data-testid="mnote-account-access-policy" role="menuitem" href="/user/access-policy"><span class="material-symbols-outlined" data-icon="admin_panel_settings" aria-hidden="true"></span><span>授权管理</span></a>' +
|
||||
'<button type="button" class="mnote-workspace-source-menu__item mnote-account-menu__item" data-testid="mnote-account-access-policy" role="menuitem" hidden><span class="material-symbols-outlined" data-icon="admin_panel_settings" aria-hidden="true"></span><span>授权管理</span></button>' +
|
||||
'<button type="button" class="mnote-workspace-source-menu__item mnote-account-menu__logout" data-testid="mnote-account-sign-out" role="menuitem">退出登录</button>' +
|
||||
'<div class="mnote-account-menu__error" data-account-error hidden></div>';
|
||||
var sessionPromise = fetchAccountSession();
|
||||
@@ -1395,8 +1435,16 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
var accessPolicyLink = menu.querySelector('[data-testid="mnote-account-access-policy"]');
|
||||
sessionPromise.then(function(session) {
|
||||
if (accessPolicyLink instanceof HTMLElement) accessPolicyLink.setAttribute('href', accessPolicyHrefForSession(session));
|
||||
if (!(accessPolicyLink instanceof HTMLElement)) return;
|
||||
accessPolicyLink.hidden = false;
|
||||
accessPolicyLink.setAttribute('data-access-policy-role', sessionIsAdmin(session) ? 'admin' : 'user');
|
||||
});
|
||||
if (accessPolicyLink) {
|
||||
accessPolicyLink.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
openAdminAccessPolicyDialog(accessPolicyLink);
|
||||
});
|
||||
}
|
||||
var signOutButton = menu.querySelector('[data-testid="mnote-account-sign-out"]');
|
||||
if (signOutButton) {
|
||||
signOutButton.addEventListener('click', function(event) {
|
||||
@@ -9996,13 +10044,14 @@ pub fn PageLayout(
|
||||
/// 顶栏当前页面标题(可选)
|
||||
#[prop(optional)]
|
||||
topbar_title: Option<String>,
|
||||
/// 是否显示管理员授权入口
|
||||
/// 是否显示管理员授权能力
|
||||
#[prop(optional)]
|
||||
show_admin_access_policy: bool,
|
||||
/// 是否启用树实时流
|
||||
#[prop(optional, default = true)]
|
||||
enable_tree_live: bool,
|
||||
) -> impl IntoView {
|
||||
let _ = show_admin_access_policy;
|
||||
let sidebar_tree_html = sidebar_tree_html.unwrap_or_default();
|
||||
|
||||
let ws_name = workspace_name
|
||||
@@ -10042,6 +10091,12 @@ pub fn PageLayout(
|
||||
"views": ["page-tree", "file-tree"]
|
||||
})
|
||||
.to_string();
|
||||
let admin_access_policy_template = crate::ssr::render_view(leptos::view! {
|
||||
<AdminAccessPolicyPanel workspace_name={ws_name.clone()} is_admin=true boot_script=false />
|
||||
});
|
||||
let user_access_policy_template = crate::ssr::render_view(leptos::view! {
|
||||
<AdminAccessPolicyPanel workspace_name={ws_name.clone()} is_admin=false boot_script=false />
|
||||
});
|
||||
|
||||
view! {
|
||||
<div class="mnote-shell wolai-workspace-shell" data-mnote-web-owner="mnote-web" data-mnote-shell="workspace">
|
||||
@@ -10068,21 +10123,6 @@ pub fn PageLayout(
|
||||
<a href="/help" title="帮助" aria-label="帮助"><span class="material-symbols-outlined nav-icon" data-icon="help" aria-hidden="true"></span></a>
|
||||
<a href="/files" title="文件" aria-label="文件"><span class="material-symbols-outlined nav-icon" data-icon="inventory_2" aria-hidden="true"></span></a>
|
||||
<button type="button" title="打开本地文件夹" aria-label="打开本地文件夹" data-mnote-action="open-local-folder"><span class="material-symbols-outlined nav-icon" data-icon="folder_open" aria-hidden="true"></span></button>
|
||||
{if show_admin_access_policy {
|
||||
view! {
|
||||
<a
|
||||
href="/admin/access-policy"
|
||||
class:active={current_nav == "admin"}
|
||||
title="目录授权"
|
||||
aria-label="目录授权"
|
||||
data-testid="mnote-admin-access-policy-entry"
|
||||
>
|
||||
<span class="material-symbols-outlined nav-icon" data-icon="admin_panel_settings" aria-hidden="true"></span>
|
||||
</a>
|
||||
}.into_any()
|
||||
} else {
|
||||
view! {}.into_any()
|
||||
}}
|
||||
<button
|
||||
type="button"
|
||||
title="更多"
|
||||
@@ -10097,6 +10137,9 @@ pub fn PageLayout(
|
||||
</nav>
|
||||
<div class="wolai-sidebar-body" inner_html={sidebar_sections_html}></div>
|
||||
<script id="__MNOTE_TREE_LIVE_BOOTSTRAP__" type="application/json" inner_html={tree_live_bootstrap}></script>
|
||||
<div hidden data-testid="mnote-admin-access-policy-template-admin" inner_html={admin_access_policy_template}></div>
|
||||
<div hidden data-testid="mnote-admin-access-policy-template-user" inner_html={user_access_policy_template}></div>
|
||||
<script inner_html={crate::ssr::pages::admin::ADMIN_POLICY_SCRIPT.to_string()}></script>
|
||||
<script inner_html={SIDEBAR_TREE_JS.to_string()}></script>
|
||||
<script id="mnote-tree-live-controller" inner_html={TREE_LIVE_CONTROLLER_JS.to_string()}></script>
|
||||
</aside>
|
||||
|
||||
Reference in New Issue
Block a user