release(secrets-mcp): 0.5.3 — 审计日志分页与 Web;CONTRIBUTING;文档与模板修正
This commit is contained in:
@@ -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, ¶ms)
|
||||
.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"),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user