This commit is contained in:
@@ -494,6 +494,55 @@ body {
|
|||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 访问记录:过滤栏与分页 */
|
||||||
|
.audit-filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-filters input,
|
||||||
|
.audit-filters select {
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border: 1px solid #adadad;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 13px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-filters #audit-filter-key {
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-filters input[type="datetime-local"] {
|
||||||
|
padding: 0 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-pagination .btn:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-pagination select {
|
||||||
|
height: 26px;
|
||||||
|
border: 1px solid #adadad;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
+26
-1
@@ -311,7 +311,22 @@
|
|||||||
<button class="modal-close" onclick="closeModal('modal-audit')">✕</button>
|
<button class="modal-close" onclick="closeModal('modal-audit')">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div style="overflow:auto;max-height:480px">
|
<div class="audit-filters">
|
||||||
|
<input type="text" id="audit-filter-key" placeholder="键名(模糊匹配)">
|
||||||
|
<select id="audit-filter-action">
|
||||||
|
<option value="">全部操作</option>
|
||||||
|
<option value="read">读取</option>
|
||||||
|
<option value="create">新增</option>
|
||||||
|
<option value="update">更新</option>
|
||||||
|
<option value="delete">删除</option>
|
||||||
|
</select>
|
||||||
|
<input type="datetime-local" id="audit-filter-start">
|
||||||
|
<span style="color:#999">至</span>
|
||||||
|
<input type="datetime-local" id="audit-filter-end">
|
||||||
|
<button class="btn btn-primary" id="btn-audit-query">查询</button>
|
||||||
|
<button class="btn btn-secondary" id="btn-audit-reset">重置</button>
|
||||||
|
</div>
|
||||||
|
<div style="overflow:auto;max-height:52vh">
|
||||||
<table class="config-table" id="audit-table">
|
<table class="config-table" id="audit-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -321,6 +336,16 @@
|
|||||||
<tbody id="audit-tbody"></tbody>
|
<tbody id="audit-tbody"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="audit-pagination">
|
||||||
|
<button class="btn btn-secondary" id="btn-audit-prev">上一页</button>
|
||||||
|
<span id="audit-page-info"></span>
|
||||||
|
<button class="btn btn-secondary" id="btn-audit-next">下一页</button>
|
||||||
|
<select id="audit-page-size" style="width:auto">
|
||||||
|
<option value="20">20 条/页</option>
|
||||||
|
<option value="50">50 条/页</option>
|
||||||
|
<option value="100">100 条/页</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-secondary" onclick="closeModal('modal-audit')">关闭</button>
|
<button class="btn btn-secondary" onclick="closeModal('modal-audit')">关闭</button>
|
||||||
|
|||||||
+9
-3
@@ -93,9 +93,15 @@ const API = {
|
|||||||
return this.request(`/api/projects/${projectId}/configs/${configId}`, { method: "DELETE" });
|
return this.request(`/api/projects/${projectId}/configs/${configId}`, { method: "DELETE" });
|
||||||
},
|
},
|
||||||
|
|
||||||
getAudit(projectId, key) {
|
getAudit(projectId, filters = {}) {
|
||||||
const qs = key ? `?key=${encodeURIComponent(key)}` : "";
|
const qs = new URLSearchParams();
|
||||||
return this.request(`/api/projects/${projectId}/configs/audit${qs}`);
|
for (const k of ["key", "action", "start", "end", "page", "pageSize"]) {
|
||||||
|
if (filters[k] !== undefined && filters[k] !== null && filters[k] !== "") {
|
||||||
|
qs.append(k, filters[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const str = qs.toString();
|
||||||
|
return this.request(`/api/projects/${projectId}/configs/audit${str ? "?" + str : ""}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
// ========== 模式 ==========
|
// ========== 模式 ==========
|
||||||
|
|||||||
+107
-12
@@ -671,9 +671,22 @@ async function copyAddress() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问记录查询状态
|
||||||
|
*/
|
||||||
|
const auditState = {
|
||||||
|
key: "",
|
||||||
|
action: "",
|
||||||
|
start: "",
|
||||||
|
end: "",
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开访问记录弹窗
|
* 打开访问记录弹窗
|
||||||
* @param {string|null} key - 为空时查当前项目全部记录;否则查指定配置键的记录
|
* @param {string|null} key - 传入配置键时作为过滤条件预填;为空时按当前过滤条件查全部记录
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function openAudit(key) {
|
async function openAudit(key) {
|
||||||
@@ -681,32 +694,80 @@ async function openAudit(key) {
|
|||||||
toast("请先选择项目", "error");
|
toast("请先选择项目", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
document.getElementById("audit-title").textContent = key
|
document.getElementById("audit-title").textContent = `(${state.currentProject.name})`;
|
||||||
? `(${state.currentProject.name} · ${key})`
|
if (key) {
|
||||||
: `(${state.currentProject.name})`;
|
auditState.key = key;
|
||||||
|
document.getElementById("audit-filter-key").value = key;
|
||||||
|
}
|
||||||
openModal("modal-audit");
|
openModal("modal-audit");
|
||||||
|
await loadAudit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取当前过滤条件并分页加载访问记录
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function loadAudit() {
|
||||||
|
if (!state.currentProject) return;
|
||||||
|
auditState.key = document.getElementById("audit-filter-key").value.trim();
|
||||||
|
auditState.action = document.getElementById("audit-filter-action").value;
|
||||||
|
auditState.start = toUtcIso(document.getElementById("audit-filter-start").value);
|
||||||
|
auditState.end = toUtcIso(document.getElementById("audit-filter-end").value);
|
||||||
|
auditState.pageSize = parseInt(document.getElementById("audit-page-size").value, 10) || 20;
|
||||||
try {
|
try {
|
||||||
const logs = await API.getAudit(state.currentProject.id, key || "");
|
const data = await API.getAudit(state.currentProject.id, {
|
||||||
renderAudit(logs);
|
key: auditState.key,
|
||||||
|
action: auditState.action,
|
||||||
|
start: auditState.start,
|
||||||
|
end: auditState.end,
|
||||||
|
page: auditState.page,
|
||||||
|
pageSize: auditState.pageSize,
|
||||||
|
});
|
||||||
|
auditState.total = data.total || 0;
|
||||||
|
renderAudit(data.items || []);
|
||||||
|
renderAuditPagination(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast("加载访问记录失败: " + err.message, "error");
|
toast("加载访问记录失败: " + err.message, "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染访问记录表格
|
* datetime-local 输入值转 UTC ISO 字符串
|
||||||
* @param {Array} logs - 访问记录列表
|
* @param {string} value - 形如 "2026-09-21T12:30"(浏览器本地时区)
|
||||||
|
* @returns {string} UTC ISO,空值原样返回
|
||||||
|
*/
|
||||||
|
function toUtcIso(value) {
|
||||||
|
if (!value) return "";
|
||||||
|
const d = new Date(value);
|
||||||
|
return isNaN(d.getTime()) ? "" : d.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染分页信息
|
||||||
|
* @param {Object} data - { total, page, pageSize }
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function renderAudit(logs) {
|
function renderAuditPagination(data) {
|
||||||
|
const totalPages = Math.max(1, Math.ceil((data.total || 0) / (data.pageSize || 1)));
|
||||||
|
document.getElementById("btn-audit-prev").disabled = data.page <= 1;
|
||||||
|
document.getElementById("btn-audit-next").disabled = data.page >= totalPages;
|
||||||
|
document.getElementById("audit-page-info").textContent = `第 ${data.page} / ${totalPages} 页 · 共 ${data.total} 条`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染访问记录表格
|
||||||
|
* @param {Array} items - 当前页访问记录列表
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function renderAudit(items) {
|
||||||
const tbody = document.getElementById("audit-tbody");
|
const tbody = document.getElementById("audit-tbody");
|
||||||
if (!logs || !logs.length) {
|
if (!items || !items.length) {
|
||||||
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#999;padding:24px">暂无访问记录</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#999;padding:24px">暂无访问记录</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ACTION_NAMES = { read: "读取", create: "新增", update: "更新", delete: "删除" };
|
const ACTION_NAMES = { read: "读取", create: "新增", update: "更新", delete: "删除" };
|
||||||
const SOURCE_NAMES = { web: "网页", mcp: "MCP" };
|
const SOURCE_NAMES = { web: "网页", mcp: "MCP" };
|
||||||
tbody.innerHTML = logs
|
tbody.innerHTML = items
|
||||||
.map((l) => {
|
.map((l) => {
|
||||||
const act = ACTION_NAMES[l.action] || l.action;
|
const act = ACTION_NAMES[l.action] || l.action;
|
||||||
const src = SOURCE_NAMES[l.source] || l.source;
|
const src = SOURCE_NAMES[l.source] || l.source;
|
||||||
@@ -867,9 +928,43 @@ function bindEvents() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 操作记录
|
// 访问记录:打开弹窗
|
||||||
document.getElementById("btn-audit").addEventListener("click", () => openAudit(null));
|
document.getElementById("btn-audit").addEventListener("click", () => openAudit(null));
|
||||||
|
|
||||||
|
// 访问记录:过滤与分页
|
||||||
|
document.getElementById("btn-audit-query").addEventListener("click", () => {
|
||||||
|
auditState.page = 1;
|
||||||
|
loadAudit();
|
||||||
|
});
|
||||||
|
document.getElementById("btn-audit-reset").addEventListener("click", () => {
|
||||||
|
document.getElementById("audit-filter-key").value = "";
|
||||||
|
document.getElementById("audit-filter-action").value = "";
|
||||||
|
document.getElementById("audit-filter-start").value = "";
|
||||||
|
document.getElementById("audit-filter-end").value = "";
|
||||||
|
auditState.page = 1;
|
||||||
|
loadAudit();
|
||||||
|
});
|
||||||
|
document.getElementById("btn-audit-prev").addEventListener("click", () => {
|
||||||
|
if (auditState.page > 1) {
|
||||||
|
auditState.page -= 1;
|
||||||
|
loadAudit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById("btn-audit-next").addEventListener("click", () => {
|
||||||
|
auditState.page += 1;
|
||||||
|
loadAudit();
|
||||||
|
});
|
||||||
|
document.getElementById("audit-page-size").addEventListener("change", () => {
|
||||||
|
auditState.page = 1;
|
||||||
|
loadAudit();
|
||||||
|
});
|
||||||
|
document.getElementById("audit-filter-key").addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
auditState.page = 1;
|
||||||
|
loadAudit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 表格操作(事件委托)
|
// 表格操作(事件委托)
|
||||||
document.getElementById("config-tbody").addEventListener("click", (e) => {
|
document.getElementById("config-tbody").addEventListener("click", (e) => {
|
||||||
const btn = e.target.closest(".row-btn");
|
const btn = e.target.closest(".row-btn");
|
||||||
|
|||||||
@@ -322,9 +322,12 @@ async function appendAuditLog(entry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询操作记录
|
* 分页查询访问记录(支持过滤参数)
|
||||||
* @param {Object} filter - { projectId, key }
|
* @param {Object} filter - { projectId, key, action, start, end, page, pageSize }
|
||||||
* @returns {Promise<Array>} 操作记录,按时间倒序
|
* - key: 键名模糊匹配
|
||||||
|
* - action: read/create/update/delete
|
||||||
|
* - start/end: ISO 时间,按 created_at 过滤
|
||||||
|
* @returns {Promise<{items: Array, total: number, page: number, pageSize: number}>}
|
||||||
*/
|
*/
|
||||||
async function loadAuditLogs(filter = {}) {
|
async function loadAuditLogs(filter = {}) {
|
||||||
await init();
|
await init();
|
||||||
@@ -335,20 +338,49 @@ async function loadAuditLogs(filter = {}) {
|
|||||||
params.push(filter.projectId);
|
params.push(filter.projectId);
|
||||||
}
|
}
|
||||||
if (filter.key) {
|
if (filter.key) {
|
||||||
where.push("config_key = ?");
|
where.push("config_key LIKE ?");
|
||||||
params.push(filter.key);
|
params.push(`%${filter.key}%`);
|
||||||
}
|
}
|
||||||
const sql =
|
if (filter.action) {
|
||||||
|
where.push("action = ?");
|
||||||
|
params.push(filter.action);
|
||||||
|
}
|
||||||
|
if (filter.start) {
|
||||||
|
const s = new Date(filter.start);
|
||||||
|
if (!isNaN(s.getTime())) {
|
||||||
|
where.push("created_at >= ?");
|
||||||
|
params.push(s.toISOString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filter.end) {
|
||||||
|
const e = new Date(filter.end);
|
||||||
|
if (!isNaN(e.getTime())) {
|
||||||
|
where.push("created_at <= ?");
|
||||||
|
params.push(e.toISOString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const whereSql = where.length ? ` WHERE ${where.join(" AND ")}` : "";
|
||||||
|
const countReader = await connection.runAndReadAll(
|
||||||
|
`SELECT COUNT(*) AS total FROM audit_logs${whereSql}`,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
const total = Number(countReader.getRowObjects()[0].total) || 0;
|
||||||
|
const page = Math.max(1, parseInt(filter.page, 10) || 1);
|
||||||
|
const pageSize = Math.min(100, Math.max(1, parseInt(filter.pageSize, 10) || 20));
|
||||||
|
const offset = (page - 1) * pageSize;
|
||||||
|
const reader = await connection.runAndReadAll(
|
||||||
`SELECT id, project_id AS projectId, config_key AS key, action, source, actor, ip,
|
`SELECT id, project_id AS projectId, config_key AS key, action, source, actor, ip,
|
||||||
value_text AS value, detail, created_at AS createdAt
|
value_text AS value, detail, created_at AS createdAt
|
||||||
FROM audit_logs` +
|
FROM audit_logs${whereSql}
|
||||||
(where.length ? ` WHERE ${where.join(" AND ")}` : "") +
|
ORDER BY created_at DESC, id DESC
|
||||||
` ORDER BY created_at DESC, id DESC`;
|
LIMIT ? OFFSET ?`,
|
||||||
const reader = await connection.runAndReadAll(sql, params);
|
[...params, pageSize, offset]
|
||||||
return reader.getRowObjects().map((r) => ({
|
);
|
||||||
|
const items = reader.getRowObjects().map((r) => ({
|
||||||
...r,
|
...r,
|
||||||
detail: r.detail ? parseData(r.detail) : null,
|
detail: r.detail ? parseData(r.detail) : null,
|
||||||
}));
|
}));
|
||||||
|
return { items, total, page, pageSize };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
+14
-4
@@ -29,16 +29,26 @@ async function getCurrentMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/projects/:projectId/configs/audit?key=xxx
|
* GET /api/projects/:projectId/configs/audit
|
||||||
* 查询当前项目(或指定配置键)的操作记录
|
* 分页查询当前项目访问记录,支持过滤参数:
|
||||||
|
* key - 键名模糊匹配
|
||||||
|
* action - read/create/update/delete
|
||||||
|
* start - 开始时间(ISO)
|
||||||
|
* end - 结束时间(ISO)
|
||||||
|
* page/pageSize - 分页
|
||||||
*/
|
*/
|
||||||
router.get("/audit", async (req, res) => {
|
router.get("/audit", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const logs = await loadAuditLogs({
|
const result = await loadAuditLogs({
|
||||||
projectId: req.params.projectId,
|
projectId: req.params.projectId,
|
||||||
key: req.query.key || "",
|
key: req.query.key || "",
|
||||||
|
action: req.query.action || "",
|
||||||
|
start: req.query.start || "",
|
||||||
|
end: req.query.end || "",
|
||||||
|
page: req.query.page,
|
||||||
|
pageSize: req.query.pageSize,
|
||||||
});
|
});
|
||||||
res.json({ success: true, data: logs });
|
res.json({ success: true, data: result });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(400).json({ success: false, message: err.message });
|
res.status(400).json({ success: false, message: err.message });
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-8
@@ -33,14 +33,15 @@ test("记录一条配置操作并可按项目/键查询", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const byProject = await duckdbAdapter.loadAuditLogs({ projectId: "proj-a" });
|
const byProject = await duckdbAdapter.loadAuditLogs({ projectId: "proj-a" });
|
||||||
assert.strictEqual(byProject.length, 1);
|
assert.strictEqual(byProject.items.length, 1);
|
||||||
|
assert.strictEqual(byProject.total, 1);
|
||||||
const byKey = await duckdbAdapter.loadAuditLogs({ projectId: "proj-a", key: "db/host" });
|
const byKey = await duckdbAdapter.loadAuditLogs({ projectId: "proj-a", key: "db/host" });
|
||||||
assert.strictEqual(byKey.length, 1);
|
assert.strictEqual(byKey.items.length, 1);
|
||||||
// 其它项目/键查不到
|
// 其它项目/键查不到
|
||||||
assert.strictEqual((await duckdbAdapter.loadAuditLogs({ projectId: "proj-b" })).length, 0);
|
assert.strictEqual((await duckdbAdapter.loadAuditLogs({ projectId: "proj-b" })).items.length, 0);
|
||||||
assert.strictEqual((await duckdbAdapter.loadAuditLogs({ projectId: "proj-a", key: "other" })).length, 0);
|
assert.strictEqual((await duckdbAdapter.loadAuditLogs({ projectId: "proj-a", key: "other" })).items.length, 0);
|
||||||
|
|
||||||
const rec = byKey[0];
|
const rec = byKey.items[0];
|
||||||
assert.strictEqual(rec.source, "web");
|
assert.strictEqual(rec.source, "web");
|
||||||
assert.strictEqual(rec.actor, "admin");
|
assert.strictEqual(rec.actor, "admin");
|
||||||
assert.strictEqual(rec.ip, "203.0.113.9");
|
assert.strictEqual(rec.ip, "203.0.113.9");
|
||||||
@@ -55,9 +56,53 @@ test("记录按时间倒序返回,MCP 来源也正确记录 Token 名", async
|
|||||||
await auditLog({ source: "mcp", actor: "ci-token", ip: "10.0.0.1", projectId: "proj-a", key: "k/1", action: "create", value: "x" });
|
await auditLog({ source: "mcp", actor: "ci-token", ip: "10.0.0.1", projectId: "proj-a", key: "k/1", action: "create", value: "x" });
|
||||||
|
|
||||||
const logs = await duckdbAdapter.loadAuditLogs({ projectId: "proj-a", key: "k/1" });
|
const logs = await duckdbAdapter.loadAuditLogs({ projectId: "proj-a", key: "k/1" });
|
||||||
assert.strictEqual(logs.length, 2);
|
const items = logs.items;
|
||||||
assert.ok(logs[0].createdAt >= logs[1].createdAt, "最新的应在前");
|
assert.strictEqual(items.length, 2);
|
||||||
assert.ok(logs.every((l) => l.source === "mcp" && l.actor === "ci-token"));
|
assert.ok(items[0].createdAt >= items[1].createdAt, "最新的应在前");
|
||||||
|
assert.ok(items.every((l) => l.source === "mcp" && l.actor === "ci-token"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("分页与过滤:action / start-end 时间范围 / 键名模糊匹配 / 页码切片", async () => {
|
||||||
|
const base = Date.now() - 5 * 60000;
|
||||||
|
for (let i = 1; i <= 3; i++) {
|
||||||
|
await duckdbAdapter.appendAuditLog({
|
||||||
|
id: "aud-pg-" + i,
|
||||||
|
source: "mcp",
|
||||||
|
actor: "ci-token",
|
||||||
|
projectId: "proj-p",
|
||||||
|
key: `svc/app-${i}`,
|
||||||
|
action: i === 3 ? "delete" : "create",
|
||||||
|
value: `v${i}`,
|
||||||
|
createdAt: new Date(base + i * 1000).toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页:page=1 取 2 条,total 应为 3
|
||||||
|
const page1 = await duckdbAdapter.loadAuditLogs({ projectId: "proj-p", page: 1, pageSize: 2 });
|
||||||
|
assert.strictEqual(page1.items.length, 2);
|
||||||
|
assert.strictEqual(page1.total, 3);
|
||||||
|
assert.strictEqual(page1.page, 1);
|
||||||
|
assert.strictEqual(page1.pageSize, 2);
|
||||||
|
const page2 = await duckdbAdapter.loadAuditLogs({ projectId: "proj-p", page: 2, pageSize: 2 });
|
||||||
|
assert.strictEqual(page2.items.length, 1);
|
||||||
|
|
||||||
|
// action 过滤
|
||||||
|
const creates = await duckdbAdapter.loadAuditLogs({ projectId: "proj-p", action: "create" });
|
||||||
|
assert.strictEqual(creates.items.length, 2);
|
||||||
|
assert.strictEqual(creates.items.every((l) => l.action === "create"), true);
|
||||||
|
const deletes = await duckdbAdapter.loadAuditLogs({ projectId: "proj-p", action: "delete" });
|
||||||
|
assert.strictEqual(deletes.items.length, 1);
|
||||||
|
|
||||||
|
// key 模糊匹配
|
||||||
|
const fuzzy = await duckdbAdapter.loadAuditLogs({ projectId: "proj-p", key: "app-" });
|
||||||
|
assert.strictEqual(fuzzy.items.length, 3);
|
||||||
|
|
||||||
|
// 时间范围:取中间那条
|
||||||
|
const midStart = new Date(base + 1500).toISOString();
|
||||||
|
const midEnd = new Date(base + 2500).toISOString();
|
||||||
|
const range = await duckdbAdapter.loadAuditLogs({ projectId: "proj-p", start: midStart, end: midEnd });
|
||||||
|
assert.strictEqual(range.items.length, 1);
|
||||||
|
assert.strictEqual(range.items[0].key, "svc/app-2");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getClientIp 优先取 X-Forwarded-For,其次 remoteAddress", () => {
|
test("getClientIp 优先取 X-Forwarded-For,其次 remoteAddress", () => {
|
||||||
|
|||||||
+2
-2
@@ -182,7 +182,7 @@ test("Token 项目范围限制 MCP 配置访问,越权操作被拒绝且不入
|
|||||||
assert.ok(!createdReal.isError);
|
assert.ok(!createdReal.isError);
|
||||||
|
|
||||||
const logsReal = await loadAuditLogs({ projectId: project.id, key: "sql/dsn" });
|
const logsReal = await loadAuditLogs({ projectId: project.id, key: "sql/dsn" });
|
||||||
const createLog = logsReal.find((l) => l.action === "create");
|
const createLog = logsReal.items.find((l) => l.action === "create");
|
||||||
assert.ok(createLog, "应在 audit_logs 中存在 create 记录");
|
assert.ok(createLog, "应在 audit_logs 中存在 create 记录");
|
||||||
assert.strictEqual(createLog.source, "mcp");
|
assert.strictEqual(createLog.source, "mcp");
|
||||||
assert.strictEqual(createLog.actor, "scope-token");
|
assert.strictEqual(createLog.actor, "scope-token");
|
||||||
@@ -191,7 +191,7 @@ test("Token 项目范围限制 MCP 配置访问,越权操作被拒绝且不入
|
|||||||
|
|
||||||
// 越权操作不产生记录(other-proj 无任何记录)
|
// 越权操作不产生记录(other-proj 无任何记录)
|
||||||
const deniedLogs = await loadAuditLogs({ projectId: "other-proj" });
|
const deniedLogs = await loadAuditLogs({ projectId: "other-proj" });
|
||||||
assert.strictEqual(deniedLogs.length, 0);
|
assert.strictEqual(deniedLogs.items.length, 0);
|
||||||
|
|
||||||
// 清理测试项目
|
// 清理测试项目
|
||||||
const delProject = await callTool("delete_project", { projectId: project.id });
|
const delProject = await callTool("delete_project", { projectId: project.id });
|
||||||
|
|||||||
Reference in New Issue
Block a user