release(secrets-mcp): 0.5.3 — 审计日志分页与 Web;CONTRIBUTING;文档与模板修正

This commit is contained in:
voson
2026-04-05 10:45:33 +08:00
parent dd24f7cc44
commit 1860cce86c
12 changed files with 405 additions and 59 deletions

View File

@@ -20,9 +20,9 @@ use secrets_core::crypto::hex;
use secrets_core::error::AppError;
use secrets_core::service::{
api_key::{ensure_api_key, regenerate_api_key},
audit_log::list_for_user,
audit_log::{count_for_user, list_for_user},
delete::delete_by_id,
search::{SearchParams, fetch_secret_schemas, ilike_pattern, list_entries},
search::{SearchParams, count_entries, fetch_secret_schemas, ilike_pattern, list_entries},
update::{UpdateEntryFieldsByIdParams, update_fields_by_id},
user::{
OAuthProfile, bind_oauth_account, find_or_create_user, get_user_by_id,
@@ -72,6 +72,9 @@ struct AuditPageTemplate {
user_name: String,
user_email: String,
entries: Vec<AuditEntryView>,
current_page: u32,
total_pages: u32,
total_count: i64,
version: &'static str,
}
@@ -95,6 +98,9 @@ struct EntriesPageTemplate {
filter_folder: String,
filter_name: String,
filter_type: String,
current_page: u32,
total_pages: u32,
total_count: i64,
version: &'static str,
}
@@ -131,7 +137,8 @@ struct FolderTabView {
}
/// Cap for HTML list (avoids loading unbounded rows into memory).
const ENTRIES_PAGE_LIMIT: u32 = 5_000;
const ENTRIES_PAGE_LIMIT: u32 = 50;
const AUDIT_PAGE_LIMIT: i64 = 10;
#[derive(Deserialize)]
struct EntriesQuery {
@@ -140,6 +147,7 @@ struct EntriesQuery {
/// URL query key is `type` (maps to DB column `entries.type`).
#[serde(rename = "type")]
entry_type: Option<String>,
page: Option<u32>,
}
// ── App state helpers ─────────────────────────────────────────────────────────
@@ -596,6 +604,8 @@ async fn entries_page(
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(|s| s.to_string());
let page = q.page.unwrap_or(1).max(1);
let offset = (page - 1) * ENTRIES_PAGE_LIMIT;
let params = SearchParams {
folder: folder_filter.as_deref(),
entry_type: type_filter.as_deref(),
@@ -605,10 +615,17 @@ async fn entries_page(
query: None,
sort: "updated",
limit: ENTRIES_PAGE_LIMIT,
offset: 0,
offset,
user_id: Some(user_id),
};
let total_count = count_entries(&state.pool, &params)
.await
.inspect_err(|e| tracing::warn!(error = %e, "count_entries failed for web entries page"))
.unwrap_or(0);
let total_pages = (total_count as u32).div_ceil(ENTRIES_PAGE_LIMIT).max(1);
let current_page = page.min(total_pages);
let rows = list_entries(&state.pool, params).await.map_err(|e| {
tracing::error!(error = %e, "failed to load entries list for web");
StatusCode::INTERNAL_SERVER_ERROR
@@ -681,7 +698,12 @@ async fn entries_page(
type_options.sort_unstable();
}
fn entries_href(folder: Option<&str>, entry_type: Option<&str>, name: Option<&str>) -> String {
fn entries_href(
folder: Option<&str>,
entry_type: Option<&str>,
name: Option<&str>,
page: Option<u32>,
) -> String {
let mut pairs: Vec<String> = Vec::new();
if let Some(f) = folder
&& !f.is_empty()
@@ -698,6 +720,9 @@ async fn entries_page(
{
pairs.push(format!("name={}", urlencoding::encode(n)));
}
if let Some(p) = page {
pairs.push(format!("page={}", p));
}
if pairs.is_empty() {
"/entries".to_string()
} else {
@@ -710,13 +735,23 @@ async fn entries_page(
folder_tabs.push(FolderTabView {
name: "全部".to_string(),
count: all_count,
href: entries_href(None, type_filter.as_deref(), name_filter.as_deref()),
href: entries_href(
None,
type_filter.as_deref(),
name_filter.as_deref(),
Some(1),
),
active: folder_filter.is_none(),
});
for r in folder_rows {
let name = r.folder;
folder_tabs.push(FolderTabView {
href: entries_href(Some(&name), type_filter.as_deref(), name_filter.as_deref()),
href: entries_href(
Some(&name),
type_filter.as_deref(),
name_filter.as_deref(),
Some(1),
),
active: folder_filter.as_deref() == Some(name.as_str()),
name,
count: r.count,
@@ -773,15 +808,24 @@ async fn entries_page(
filter_folder: folder_filter.unwrap_or_default(),
filter_name: name_filter.unwrap_or_default(),
filter_type: type_filter.unwrap_or_default(),
current_page,
total_pages,
total_count,
version: env!("CARGO_PKG_VERSION"),
};
render_template(tmpl)
}
#[derive(Deserialize)]
struct AuditQuery {
page: Option<u32>,
}
async fn audit_page(
State(state): State<AppState>,
session: Session,
Query(aq): Query<AuditQuery>,
) -> Result<Response, StatusCode> {
let Some(user_id) = current_user_id(&session).await else {
return Ok(Redirect::to("/login").into_response());
@@ -795,7 +839,20 @@ async fn audit_page(
None => return Ok(Redirect::to("/login").into_response()),
};
let rows = list_for_user(&state.pool, user_id, 100)
let page = aq.page.unwrap_or(1).max(1);
let total_count = count_for_user(&state.pool, user_id).await.map_err(|e| {
tracing::error!(error = %e, "failed to count audit log for user");
StatusCode::INTERNAL_SERVER_ERROR
})?;
let total_pages = (total_count as u32)
.div_ceil(AUDIT_PAGE_LIMIT as u32)
.max(1);
let current_page = page.min(total_pages);
let actual_offset = ((current_page - 1) as i64) * AUDIT_PAGE_LIMIT;
let rows = list_for_user(&state.pool, user_id, AUDIT_PAGE_LIMIT, actual_offset)
.await
.map_err(|e| {
tracing::error!(error = %e, "failed to load audit log for user");
@@ -816,6 +873,9 @@ async fn audit_page(
user_name: user.name.clone(),
user_email: user.email.clone().unwrap_or_default(),
entries,
current_page,
total_pages,
total_count,
version: env!("CARGO_PKG_VERSION"),
};