fix auth registration and access management ui
This commit is contained in:
@@ -8,6 +8,7 @@ pub fn AdminAccessPolicyPage(
|
||||
#[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,
|
||||
) -> impl IntoView {
|
||||
let workspace_name = workspace_name
|
||||
.unwrap_or_else(|| "开发用户 的空间".to_string())
|
||||
@@ -17,12 +18,17 @@ pub fn AdminAccessPolicyPage(
|
||||
.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();
|
||||
view! {
|
||||
<PageLayout current_nav="admin" workspace_name={workspace_name.clone()} topbar_title={"目录授权".to_string()} show_admin_access_policy=true>
|
||||
<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>"目录授权"</h1>
|
||||
<p>"管理员可以查看、验证和管理本地目录授权,普通用户不会看到入口。"</p>
|
||||
<h1>{page_title.clone()}</h1>
|
||||
<p>{if is_admin {
|
||||
"管理员可以管理目录授权和分享授权。"
|
||||
} else {
|
||||
"查看与你相关的分享授权。"
|
||||
}}</p>
|
||||
</header>
|
||||
|
||||
<section class="mnote-admin-policy-summary">
|
||||
@@ -32,7 +38,7 @@ pub fn AdminAccessPolicyPage(
|
||||
</div>
|
||||
<div class="mnote-admin-policy-summary-item">
|
||||
<span class="mnote-admin-policy-summary-label">"策略文件"</span>
|
||||
<code data-testid="mnote-admin-policy-path">{policy_path.clone()}</code>
|
||||
<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>
|
||||
@@ -40,102 +46,149 @@ pub fn AdminAccessPolicyPage(
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mnote-admin-policy-panel">
|
||||
<header class="mnote-admin-policy-panel-header">
|
||||
<h2>"当前策略"</h2>
|
||||
<button type="button" data-testid="mnote-admin-policy-refresh" data-admin-action="refresh-policy">"刷新"</button>
|
||||
</header>
|
||||
<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>
|
||||
{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()
|
||||
}}
|
||||
|
||||
<section class="mnote-admin-policy-grid">
|
||||
<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>
|
||||
{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="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>
|
||||
<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-panel" data-testid="mnote-admin-share-grants-panel">
|
||||
<header class="mnote-admin-policy-panel-header">
|
||||
<h2>"分享授权"</h2>
|
||||
<div>
|
||||
<h2>"分享管理"</h2>
|
||||
<p class="mnote-admin-policy-note">{if is_admin {
|
||||
"查看、创建和撤销分享授权。"
|
||||
} else {
|
||||
"查看你创建或接收的分享授权。"
|
||||
}}</p>
|
||||
</div>
|
||||
<button type="button" data-testid="mnote-admin-share-grants-refresh" data-admin-action="refresh-share-grants">"刷新"</button>
|
||||
</header>
|
||||
<pre class="mnote-admin-policy-json" data-testid="mnote-admin-share-grants-json">{""}</pre>
|
||||
<div class="mnote-admin-policy-table" data-testid="mnote-admin-share-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-share-grants-json">{""}</pre>
|
||||
</details>
|
||||
<div class="mnote-admin-policy-note" data-testid="mnote-admin-share-grants-message"></div>
|
||||
</section>
|
||||
|
||||
<section class="mnote-admin-policy-grid">
|
||||
<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>"ownerUserId"</span>
|
||||
<input data-testid="mnote-admin-share-owner-user-id" name="ownerUserId" type="text" placeholder="owner_123" required />
|
||||
</label>
|
||||
<label>
|
||||
<span>"targetUserId"</span>
|
||||
<input data-testid="mnote-admin-share-target-user-id" name="targetUserId" type="text" placeholder="target_123" required />
|
||||
@@ -181,6 +234,9 @@ pub fn AdminAccessPolicyPage(
|
||||
<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>
|
||||
@@ -202,12 +258,66 @@ const ADMIN_POLICY_SCRIPT: &str = r#"
|
||||
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__');
|
||||
try { return JSON.parse(node ? node.textContent || '{}' : '{}'); } catch (_) { return {}; }
|
||||
})();
|
||||
var isAdmin = pageConfig.isAdmin === true;
|
||||
|
||||
function setText(node, value) {
|
||||
if (!node) return;
|
||||
node.textContent = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value == null ? '' : value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function renderBadge(value) {
|
||||
var text = String(value || '').trim() || 'read';
|
||||
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 renderShareGrants(payload) {
|
||||
if (!shareGrantsList) return;
|
||||
var grants = payload && Array.isArray(payload.grants) ? payload.grants : [];
|
||||
if (!grants.length) {
|
||||
shareGrantsList.innerHTML = '<div class="mnote-admin-policy-empty">暂无分享授权</div>';
|
||||
return;
|
||||
}
|
||||
shareGrantsList.innerHTML = grants.map(function(grant) {
|
||||
var active = grant.active === false ? '已撤销' : '有效';
|
||||
return '<article class="mnote-admin-policy-row">' +
|
||||
'<div><strong>' + escapeHtml(grant.shareId || grant.id || '未命名分享') + '</strong><span>' + escapeHtml(grant.rootPath || grant.rootUri || '') + '</span></div>' +
|
||||
'<div>' + renderBadge(grant.permission) + renderBadge(active) + '</div>' +
|
||||
'<div><span>所有者 ' + escapeHtml(grant.ownerUserId || '') + '</span><span>接收者 ' + escapeHtml(grant.targetUserId || '') + '</span></div>' +
|
||||
'</article>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function formValues(form) {
|
||||
var data = new FormData(form);
|
||||
var capabilities = String(data.get('capabilities') || '')
|
||||
@@ -262,14 +372,17 @@ const ADMIN_POLICY_SCRIPT: &str = r#"
|
||||
}
|
||||
|
||||
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('/api/admin/share-grants', { method: 'GET', headers: {} });
|
||||
var payload = await requestJson(isAdmin ? '/api/admin/share-grants' : '/api/user/share-grants', { method: 'GET', headers: {} });
|
||||
setText(shareGrantsJson, payload);
|
||||
renderShareGrants(payload);
|
||||
setText(shareGrantsMessage, '已刷新分享授权');
|
||||
}
|
||||
|
||||
@@ -283,62 +396,71 @@ const ADMIN_POLICY_SCRIPT: &str = r#"
|
||||
refreshShareGrants().catch(function (error) { setText(shareGrantsMessage, error.message || '刷新失败'); });
|
||||
});
|
||||
|
||||
root.querySelector('[data-admin-form="validate-root"]').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 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 || '验证失败');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
root.querySelector('[data-admin-form="create-grant"]').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 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 || '创建失败');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
root.querySelector('[data-admin-form="delete-grant"]').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 || '删除失败');
|
||||
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('/api/admin/share-grants', {
|
||||
requestJson(isAdmin ? '/api/admin/share-grants' : '/api/user/share-grants', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(values),
|
||||
}).then(function (payload) {
|
||||
@@ -356,7 +478,7 @@ const ADMIN_POLICY_SCRIPT: &str = r#"
|
||||
var data = new FormData(event.currentTarget);
|
||||
var shareId = String(data.get('deleteShareId') || '').trim();
|
||||
setText(deleteShareGrantResult, '正在撤销...');
|
||||
requestJson('/api/admin/share-grants/' + encodeURIComponent(shareId), {
|
||||
requestJson((isAdmin ? '/api/admin/share-grants/' : '/api/user/share-grants/') + encodeURIComponent(shareId), {
|
||||
method: 'DELETE',
|
||||
headers: {},
|
||||
}).then(function (payload) {
|
||||
@@ -369,11 +491,15 @@ const ADMIN_POLICY_SCRIPT: &str = r#"
|
||||
});
|
||||
});
|
||||
|
||||
refreshPolicy().catch(function (error) {
|
||||
setText(message, error.message || '加载策略失败');
|
||||
});
|
||||
if (isAdmin) {
|
||||
refreshPolicy().catch(function (error) {
|
||||
setText(message, error.message || '加载策略失败');
|
||||
renderPolicyGrants(null);
|
||||
});
|
||||
}
|
||||
refreshShareGrants().catch(function (error) {
|
||||
setText(shareGrantsMessage, error.message || '加载分享授权失败');
|
||||
renderShareGrants(null);
|
||||
});
|
||||
})();
|
||||
"#;
|
||||
|
||||
@@ -24,7 +24,7 @@ pub fn AuthPage() -> impl IntoView {
|
||||
<input type="hidden" name="action" value="auth:signIn" />
|
||||
<input type="hidden" name="provider" value="password" />
|
||||
<input type="hidden" name="flow" value="signIn" data-auth-flow />
|
||||
<label class="mnote-auth-field">
|
||||
<label class="mnote-auth-field" data-auth-field="account">
|
||||
<span data-auth-account-label>"邮箱或用户名"</span>
|
||||
<input
|
||||
id="account"
|
||||
@@ -35,6 +35,28 @@ pub fn AuthPage() -> impl IntoView {
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="mnote-auth-field" data-auth-field="email" hidden>
|
||||
<span>"邮箱"</span>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
placeholder="请输入邮箱"
|
||||
/>
|
||||
</label>
|
||||
<label class="mnote-auth-field" data-auth-field="username" hidden>
|
||||
<span>"用户名"</span>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
placeholder="请输入用户名"
|
||||
minlength="2"
|
||||
maxlength="32"
|
||||
/>
|
||||
</label>
|
||||
<label class="mnote-auth-field">
|
||||
<span>"密码"</span>
|
||||
<input
|
||||
@@ -76,6 +98,11 @@ const AUTH_SCRIPT: &str = r#"
|
||||
var form = root.querySelector('form[data-auth-mode="convex-password"]');
|
||||
var flowInput = root.querySelector('[data-auth-flow]');
|
||||
var accountInput = root.querySelector('#account');
|
||||
var emailInput = root.querySelector('#email');
|
||||
var usernameInput = root.querySelector('#username');
|
||||
var accountField = root.querySelector('[data-auth-field="account"]');
|
||||
var emailField = root.querySelector('[data-auth-field="email"]');
|
||||
var usernameField = root.querySelector('[data-auth-field="username"]');
|
||||
var accountLabel = root.querySelector('[data-auth-account-label]');
|
||||
var title = root.querySelector('#mnote-auth-title');
|
||||
var subtitle = root.querySelector('.mnote-auth-heading p');
|
||||
@@ -104,10 +131,20 @@ const AUTH_SCRIPT: &str = r#"
|
||||
if (subtitle) subtitle.textContent = isSignUp ? '创建账号后进入你的工作区' : '登录后进入你的工作区';
|
||||
submit.textContent = isSignUp ? '注册并登录' : '登录';
|
||||
switcher.textContent = isSignUp ? '已有账号?登录' : '没有账号?注册';
|
||||
if (accountLabel) accountLabel.textContent = isSignUp ? '邮箱' : '邮箱或用户名';
|
||||
if (accountLabel) accountLabel.textContent = '邮箱或用户名';
|
||||
if (accountField) accountField.hidden = isSignUp;
|
||||
if (emailField) emailField.hidden = !isSignUp;
|
||||
if (usernameField) usernameField.hidden = !isSignUp;
|
||||
if (accountInput) {
|
||||
accountInput.placeholder = isSignUp ? '请输入邮箱' : '请输入邮箱或用户名';
|
||||
accountInput.autocomplete = isSignUp ? 'email' : 'username';
|
||||
accountInput.required = !isSignUp;
|
||||
accountInput.placeholder = '请输入邮箱或用户名';
|
||||
accountInput.autocomplete = 'username';
|
||||
}
|
||||
if (emailInput) {
|
||||
emailInput.required = isSignUp;
|
||||
}
|
||||
if (usernameInput) {
|
||||
usernameInput.required = isSignUp;
|
||||
}
|
||||
quickLogin.hidden = isSignUp;
|
||||
var passwordInput = root.querySelector('#password');
|
||||
@@ -115,69 +152,42 @@ const AUTH_SCRIPT: &str = r#"
|
||||
setMessage('', '');
|
||||
}
|
||||
|
||||
function accountMap() {
|
||||
try {
|
||||
return JSON.parse(window.localStorage.getItem('mnote.auth.accountEmailByName') || '{}') || {};
|
||||
} catch (_) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function rememberAccount(email, name) {
|
||||
var normalizedEmail = String(email || '').trim();
|
||||
var normalizedName = String(name || '').trim();
|
||||
if (!normalizedEmail || !normalizedName || normalizedName.indexOf('@') !== -1) return;
|
||||
try {
|
||||
var map = accountMap();
|
||||
map[normalizedName.toLowerCase()] = normalizedEmail;
|
||||
window.localStorage.setItem('mnote.auth.accountEmailByName', JSON.stringify(map));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function resolveAccountEmail(account) {
|
||||
var normalized = String(account || '').trim();
|
||||
if (normalized.indexOf('@') !== -1) return normalized;
|
||||
var mapped = accountMap()[normalized.toLowerCase()];
|
||||
return String(mapped || normalized).trim();
|
||||
}
|
||||
|
||||
function defaultNameFromEmail(email) {
|
||||
return String(email || '').split('@')[0].trim();
|
||||
}
|
||||
|
||||
function buildPayload(flow, account, password) {
|
||||
var email = flow === 'signIn' ? resolveAccountEmail(account) : String(account || '').trim();
|
||||
var name = defaultNameFromEmail(email);
|
||||
function buildPayload(flow, values) {
|
||||
var accountValue = String(values.account || '').trim();
|
||||
var emailValue = String(values.email || '').trim();
|
||||
var usernameValue = String(values.username || '').trim();
|
||||
var payload = {
|
||||
action: 'auth:signIn',
|
||||
args: {
|
||||
provider: 'password',
|
||||
params: {
|
||||
email: email,
|
||||
password: password,
|
||||
password: values.password,
|
||||
flow: flow
|
||||
}
|
||||
}
|
||||
};
|
||||
if (name) {
|
||||
payload.args.params.name = name;
|
||||
if (flow === 'signUp') {
|
||||
payload.args.params.email = emailValue;
|
||||
payload.args.params.name = usernameValue;
|
||||
} else {
|
||||
payload.args.params.account = accountValue;
|
||||
if (accountValue.indexOf('@') !== -1) payload.args.params.email = accountValue;
|
||||
if (accountValue) payload.args.params.name = accountValue.indexOf('@') === -1 ? accountValue : accountValue.split('@')[0];
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function requestAuth(flow, account, password) {
|
||||
async function requestAuth(flow, values) {
|
||||
var response = await fetch('/api/auth', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(buildPayload(flow, account, password))
|
||||
body: JSON.stringify(buildPayload(flow, values))
|
||||
});
|
||||
var data = await response.json().catch(function () { return {}; });
|
||||
if (!response.ok || data.error) {
|
||||
throw new Error(data.error || '登录失败,请重试');
|
||||
}
|
||||
var params = buildPayload(flow, account, password).args.params;
|
||||
rememberAccount(params.email, params.name);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -188,20 +198,18 @@ const AUTH_SCRIPT: &str = r#"
|
||||
form.addEventListener('submit', async function (event) {
|
||||
event.preventDefault();
|
||||
var account = String((accountInput || {}).value || '').trim();
|
||||
var email = String((emailInput || {}).value || '').trim();
|
||||
var username = String((usernameInput || {}).value || '').trim();
|
||||
var password = String((root.querySelector('#password') || {}).value || '');
|
||||
var flow = flowInput.value === 'signUp' ? 'signUp' : 'signIn';
|
||||
if (!account || !password) {
|
||||
if ((flow === 'signIn' && !account) || (flow === 'signUp' && (!email || !username)) || !password) {
|
||||
setMessage('请完整填写信息', 'error');
|
||||
return;
|
||||
}
|
||||
if (flow === 'signUp' && account.indexOf('@') === -1) {
|
||||
setMessage('注册时请填写邮箱;登录时可输入邮箱或已记住的用户名', 'error');
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setMessage(flow === 'signUp' ? '正在创建账号...' : '正在登录...', 'info');
|
||||
try {
|
||||
await requestAuth(flow, account, password);
|
||||
await requestAuth(flow, { account: account, email: email, username: username, password: password });
|
||||
setMessage('登录成功,正在进入工作区...', 'success');
|
||||
window.location.assign('/');
|
||||
} catch (error) {
|
||||
@@ -226,8 +234,7 @@ const AUTH_SCRIPT: &str = r#"
|
||||
setBusy(true);
|
||||
setMessage('正在登录测试账号...', 'info');
|
||||
try {
|
||||
await requestAuth('signIn', email, password);
|
||||
rememberAccount(email, username);
|
||||
await requestAuth('signIn', { account: email, password: password });
|
||||
setMessage('登录成功,正在进入工作区...', 'success');
|
||||
window.location.assign('/');
|
||||
return;
|
||||
@@ -236,8 +243,7 @@ const AUTH_SCRIPT: &str = r#"
|
||||
}
|
||||
|
||||
try {
|
||||
await requestAuth('signUp', email, password);
|
||||
rememberAccount(email, username);
|
||||
await requestAuth('signUp', { email: email, username: username, password: password });
|
||||
setMessage('测试账号已创建,正在进入工作区...', 'success');
|
||||
window.location.assign('/');
|
||||
} catch (error) {
|
||||
|
||||
@@ -1222,6 +1222,11 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
});
|
||||
}
|
||||
|
||||
function closeProfileDialog() {
|
||||
var existing = document.querySelector('[data-testid="mnote-profile-dialog"]');
|
||||
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
|
||||
}
|
||||
|
||||
function accountMenuFallbackSession() {
|
||||
var body = document.body instanceof HTMLElement ? document.body : null;
|
||||
var actorId = body ? String(body.getAttribute('data-mnote-actor-id') || '').trim() : '';
|
||||
@@ -1242,8 +1247,24 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
var typeNode = menu.querySelector('[data-account-info="actor-type"]');
|
||||
if (nameNode) nameNode.textContent = session.name || session.userId || '当前用户';
|
||||
if (emailNode) emailNode.textContent = session.email || '未提供邮箱';
|
||||
if (idNode) idNode.textContent = session.userId || 'anonymous';
|
||||
if (idNode) {
|
||||
idNode.textContent = session.userId || 'anonymous';
|
||||
idNode.setAttribute('title', session.userId || 'anonymous');
|
||||
}
|
||||
if (typeNode) typeNode.textContent = session.actorType || session.authMode || 'unknown';
|
||||
var copyButton = menu.querySelector('[data-account-copy-user-id]');
|
||||
if (copyButton instanceof HTMLElement) {
|
||||
copyButton.dataset.copyValue = session.userId || 'anonymous';
|
||||
copyButton.setAttribute('title', '复制用户 ID');
|
||||
}
|
||||
}
|
||||
|
||||
function sessionIsAdmin(session) {
|
||||
return String(session && session.actorType || '').trim() === 'admin';
|
||||
}
|
||||
|
||||
function accessPolicyHrefForSession(session) {
|
||||
return sessionIsAdmin(session) ? '/admin/access-policy' : '/user/access-policy';
|
||||
}
|
||||
|
||||
async function loadAccountInfo(menu) {
|
||||
@@ -1261,6 +1282,70 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAccountSession() {
|
||||
try {
|
||||
var response = await fetch('/api/auth/session', {
|
||||
method: 'GET',
|
||||
headers: { 'accept': 'application/json' },
|
||||
credentials: 'include'
|
||||
});
|
||||
var payload = await response.json().catch(function() { return null; });
|
||||
if (!response.ok || !payload) throw new Error('session_failed_' + response.status);
|
||||
return payload;
|
||||
} catch (_) {
|
||||
return accountMenuFallbackSession();
|
||||
}
|
||||
}
|
||||
|
||||
function openProfileDialog(session) {
|
||||
closeProfileDialog();
|
||||
closeAccountMenu();
|
||||
var dialog = document.createElement('div');
|
||||
dialog.className = 'mnote-profile-dialog';
|
||||
dialog.setAttribute('data-testid', 'mnote-profile-dialog');
|
||||
dialog.setAttribute('role', 'dialog');
|
||||
dialog.setAttribute('aria-modal', 'true');
|
||||
dialog.setAttribute('aria-labelledby', 'mnote-profile-dialog-title');
|
||||
var userId = escapeHtml(session.userId || 'anonymous');
|
||||
dialog.innerHTML =
|
||||
'<div class="mnote-profile-dialog__backdrop" data-profile-dialog-close></div>' +
|
||||
'<section class="mnote-profile-dialog__panel">' +
|
||||
'<header class="mnote-profile-dialog__header">' +
|
||||
'<div>' +
|
||||
'<div class="mnote-profile-dialog__eyebrow">个人信息</div>' +
|
||||
'<h2 id="mnote-profile-dialog-title">个人信息</h2>' +
|
||||
'</div>' +
|
||||
'<button type="button" class="mnote-profile-dialog__close" data-profile-dialog-close aria-label="关闭"><span class="material-symbols-outlined" data-icon="close" aria-hidden="true"></span></button>' +
|
||||
'</header>' +
|
||||
'<div class="mnote-profile-dialog__identity">' +
|
||||
'<div class="mnote-profile-dialog__avatar">' + escapeHtml(String(session.name || session.email || session.userId || '用').slice(0, 1).toUpperCase()) + '</div>' +
|
||||
'<div><strong>' + escapeHtml(session.name || session.userId || '当前用户') + '</strong><span>' + escapeHtml(session.email || '未提供邮箱') + '</span></div>' +
|
||||
'</div>' +
|
||||
'<dl class="mnote-profile-dialog__list">' +
|
||||
'<div><dt>用户名</dt><dd>' + escapeHtml(session.name || '未设置') + '</dd></div>' +
|
||||
'<div><dt>邮箱</dt><dd>' + escapeHtml(session.email || '未提供邮箱') + '</dd></div>' +
|
||||
'<div><dt>用户 ID</dt><dd><code title="' + userId + '">' + userId + '</code><button type="button" data-profile-copy-user-id data-copy-value="' + userId + '"><span class="material-symbols-outlined" data-icon="content_copy" aria-hidden="true"></span></button></dd></div>' +
|
||||
'<div><dt>身份</dt><dd>' + escapeHtml(session.actorType || session.authMode || 'unknown') + '</dd></div>' +
|
||||
'</dl>' +
|
||||
'</section>';
|
||||
dialog.querySelectorAll('[data-profile-dialog-close]').forEach(function(button) {
|
||||
button.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
closeProfileDialog();
|
||||
});
|
||||
});
|
||||
var copyButton = dialog.querySelector('[data-profile-copy-user-id]');
|
||||
if (copyButton) {
|
||||
copyButton.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
void copyTreeContextValue(copyButton.getAttribute('data-copy-value') || '', 'profile-user-id');
|
||||
});
|
||||
}
|
||||
document.body.appendChild(dialog);
|
||||
var closeButton = dialog.querySelector('.mnote-profile-dialog__close');
|
||||
if (closeButton instanceof HTMLElement) closeButton.focus();
|
||||
}
|
||||
|
||||
async function signOutAccount(trigger) {
|
||||
setCommandPending(trigger, true);
|
||||
try {
|
||||
@@ -1296,16 +1381,22 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
menu.setAttribute('data-testid', 'mnote-account-menu');
|
||||
menu.setAttribute('role', 'menu');
|
||||
menu.innerHTML =
|
||||
'<div class="mnote-account-menu__profile" role="group" aria-label="个人信息">' +
|
||||
'<div class="mnote-account-menu__title">个人设置</div>' +
|
||||
'<div class="mnote-account-menu__name" data-account-info="name">正在读取...</div>' +
|
||||
'<div class="mnote-account-menu__row"><span>邮箱</span><strong data-account-info="email">正在读取...</strong></div>' +
|
||||
'<div class="mnote-account-menu__row"><span>用户 ID</span><strong data-account-info="user-id">正在读取...</strong></div>' +
|
||||
'<div class="mnote-account-menu__row"><span>身份</span><strong data-account-info="actor-type">正在读取...</strong></div>' +
|
||||
'</div>' +
|
||||
'<div class="mnote-account-menu__separator" aria-hidden="true"></div>' +
|
||||
'<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__logout" data-testid="mnote-account-sign-out" role="menuitem">退出登录</button>' +
|
||||
'<div class="mnote-account-menu__error" data-account-error hidden></div>';
|
||||
var sessionPromise = fetchAccountSession();
|
||||
var profileButton = menu.querySelector('[data-testid="mnote-account-profile"]');
|
||||
if (profileButton) {
|
||||
profileButton.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
sessionPromise.then(openProfileDialog);
|
||||
});
|
||||
}
|
||||
var accessPolicyLink = menu.querySelector('[data-testid="mnote-account-access-policy"]');
|
||||
sessionPromise.then(function(session) {
|
||||
if (accessPolicyLink instanceof HTMLElement) accessPolicyLink.setAttribute('href', accessPolicyHrefForSession(session));
|
||||
});
|
||||
var signOutButton = menu.querySelector('[data-testid="mnote-account-sign-out"]');
|
||||
if (signOutButton) {
|
||||
signOutButton.addEventListener('click', function(event) {
|
||||
@@ -1315,7 +1406,6 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
var host = trigger.closest('[data-testid="wolai-sidebar-quick-actions"]') || trigger.parentElement;
|
||||
if (host) host.appendChild(menu);
|
||||
void loadAccountInfo(menu);
|
||||
}
|
||||
|
||||
autoOpenRecentLocalRootOnHome();
|
||||
|
||||
Reference in New Issue
Block a user