Compare commits

..

5 Commits

Author SHA1 Message Date
voson
7bd0603dc6 chore(secrets-mcp): bump version to 0.1.9
All checks were successful
Secrets MCP — Build & Release / 检查 / 构建 / 发版 (push) Successful in 2m47s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Successful in 5s
Made-with: Cursor
2026-03-21 12:25:38 +08:00
voson
17a95bea5b refactor(audit): 移除旧格式兼容,user_id 统一走列字段
Some checks failed
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Has been cancelled
Secrets MCP — Build & Release / 检查 / 构建 / 发版 (push) Has been cancelled
- audit_log 查询去掉 detail->>'user_id' 回退分支
- login_detail 不再冗余写入 user_id 到 detail JSON
- 迁移 SQL 去掉多余的 ALTER TABLE ADD COLUMN

Made-with: Cursor
2026-03-21 12:24:00 +08:00
voson
a42db62702 style(secrets-mcp): rustfmt web.rs audit mapping
All checks were successful
Secrets MCP — Build & Release / 检查 / 构建 / 发版 (push) Successful in 5m20s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Successful in 6s
Made-with: Cursor
2026-03-21 12:06:29 +08:00
voson
2edb970cba chore(secrets-mcp): bump version to 0.1.8
Some checks failed
Secrets MCP — Build & Release / 检查 / 构建 / 发版 (push) Failing after 19s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Has been skipped
Made-with: Cursor
2026-03-21 12:05:22 +08:00
voson
17f8ac0dbc web: 审计页时间按浏览器本地时区显示
Some checks failed
Secrets MCP — Build & Release / 检查 / 构建 / 发版 (push) Failing after 25s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Has been skipped
Made-with: Cursor
2026-03-21 12:03:44 +08:00
7 changed files with 24 additions and 19 deletions

2
Cargo.lock generated
View File

@@ -1949,7 +1949,7 @@ dependencies = [
[[package]]
name = "secrets-mcp"
version = "0.1.7"
version = "0.1.9"
dependencies = [
"anyhow",
"askama",

View File

@@ -10,14 +10,8 @@ pub fn current_actor() -> String {
std::env::var("USER").unwrap_or_default()
}
fn login_detail(
user_id: Uuid,
provider: &str,
client_ip: Option<&str>,
user_agent: Option<&str>,
) -> Value {
fn login_detail(provider: &str, client_ip: Option<&str>, user_agent: Option<&str>) -> Value {
json!({
"user_id": user_id,
"provider": provider,
"client_ip": client_ip,
"user_agent": user_agent,
@@ -34,7 +28,7 @@ pub async fn log_login(
user_agent: Option<&str>,
) {
let actor = current_actor();
let detail = login_detail(user_id, provider, client_ip, user_agent);
let detail = login_detail(provider, client_ip, user_agent);
let result: Result<_, sqlx::Error> = sqlx::query(
"INSERT INTO audit_log (user_id, action, namespace, kind, name, detail, actor) \
VALUES ($1, $2, $3, $4, $5, $6, $7)",
@@ -94,10 +88,8 @@ mod tests {
#[test]
fn login_detail_includes_expected_fields() {
let user_id = Uuid::nil();
let detail = login_detail(user_id, "google", Some("127.0.0.1"), Some("Mozilla/5.0"));
let detail = login_detail("google", Some("127.0.0.1"), Some("Mozilla/5.0"));
assert_eq!(detail["user_id"], json!(user_id));
assert_eq!(detail["provider"], "google");
assert_eq!(detail["client_ip"], "127.0.0.1");
assert_eq!(detail["user_agent"], "Mozilla/5.0");

View File

@@ -77,7 +77,6 @@ pub async fn migrate(pool: &PgPool) -> Result<()> {
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS user_id UUID;
CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_audit_log_ns_kind ON audit_log(namespace, kind);
CREATE INDEX IF NOT EXISTS idx_audit_log_user_id ON audit_log(user_id) WHERE user_id IS NOT NULL;

View File

@@ -10,7 +10,7 @@ pub async fn list_for_user(pool: &PgPool, user_id: Uuid, limit: i64) -> Result<V
let rows = sqlx::query_as(
"SELECT id, user_id, action, namespace, kind, name, detail, actor, created_at \
FROM audit_log \
WHERE user_id = $1 OR (user_id IS NULL AND detail->>'user_id' = $1::text) \
WHERE user_id = $1 \
ORDER BY created_at DESC, id DESC \
LIMIT $2",
)

View File

@@ -1,6 +1,6 @@
[package]
name = "secrets-mcp"
version = "0.1.7"
version = "0.1.9"
edition.workspace = true
[[bin]]

View File

@@ -1,4 +1,5 @@
use askama::Template;
use chrono::SecondsFormat;
use std::net::SocketAddr;
use axum::{
@@ -61,7 +62,8 @@ struct AuditPageTemplate {
}
struct AuditEntryView {
created_at: String,
/// RFC3339 UTC for `<time datetime>`; rendered as browser-local in audit.html.
created_at_iso: String,
action: String,
target: String,
detail: String,
@@ -408,7 +410,7 @@ async fn audit_page(
let entries = rows
.into_iter()
.map(|row| AuditEntryView {
created_at: row.created_at.format("%Y-%m-%d %H:%M:%S UTC").to_string(),
created_at_iso: row.created_at.to_rfc3339_opts(SecondsFormat::Secs, true),
action: row.action,
target: format_audit_target(&row.namespace, &row.kind, &row.name),
detail: serde_json::to_string_pretty(&row.detail).unwrap_or_else(|_| "{}".to_string()),

View File

@@ -108,7 +108,7 @@
<main class="main">
<section class="card">
<div class="card-title">我的审计</div>
<div class="card-subtitle">展示最近 100 条与当前用户相关的新审计记录。</div>
<div class="card-subtitle">展示最近 100 条与当前用户相关的新审计记录。时间为浏览器本地时区。</div>
{% if entries.is_empty() %}
<div class="empty">暂无审计记录。</div>
@@ -125,7 +125,7 @@
<tbody>
{% for entry in entries %}
<tr>
<td class="col-time mono">{{ entry.created_at }}</td>
<td class="col-time mono"><time class="audit-local-time" datetime="{{ entry.created_at_iso }}">{{ entry.created_at_iso }}</time></td>
<td class="col-action mono">{{ entry.action }}</td>
<td class="col-target mono">{{ entry.target }}</td>
<td class="col-detail"><pre class="detail">{{ entry.detail }}</pre></td>
@@ -138,5 +138,17 @@
</main>
</div>
</div>
<script>
(function () {
document.querySelectorAll('time.audit-local-time[datetime]').forEach(function (el) {
var raw = el.getAttribute('datetime');
var d = raw ? new Date(raw) : null;
if (d && !isNaN(d.getTime())) {
el.textContent = d.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'medium' });
el.title = raw + ' (UTC)';
}
});
})();
</script>
</body>
</html>