This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
//! a follow-up if the operator outgrows this view.
|
||||
|
||||
use super::shared::{fmt_unix, html_escape, require_admin};
|
||||
use crate::api::admin::i18n::{t, tf1, Lang};
|
||||
use crate::api::error::ApiError;
|
||||
use crate::api::middleware::AuthedUser;
|
||||
use crate::api::state::AppState;
|
||||
@@ -23,14 +24,15 @@ pub struct TabQuery {
|
||||
pub async fn index(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
admin: AuthedUser,
|
||||
lang: Lang,
|
||||
Query(q): Query<TabQuery>,
|
||||
) -> Result<Html<String>, ApiError> {
|
||||
require_admin(&admin)?;
|
||||
let tab = q.tab.as_deref().unwrap_or("conn");
|
||||
let body = match tab {
|
||||
"file" => render_file(&state).await?,
|
||||
"alarm" => render_alarm(&state).await?,
|
||||
_ => render_conn(&state).await?,
|
||||
"file" => render_file(&state, lang).await?,
|
||||
"alarm" => render_alarm(&state, lang).await?,
|
||||
_ => render_conn(&state, lang).await?,
|
||||
};
|
||||
let pill = |id: &str, label: &str| {
|
||||
let active = id == tab;
|
||||
@@ -49,42 +51,50 @@ pub async fn index(
|
||||
Ok(Html(format!(
|
||||
r##"<div class="space-y-4">
|
||||
<header class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold">Audit log</h2>
|
||||
<p class="text-xs text-slate-500">Latest {n} rows.</p>
|
||||
<h2 class="text-lg font-semibold">{heading}</h2>
|
||||
<p class="text-xs text-slate-500">{latest}</p>
|
||||
</header>
|
||||
<div class="flex gap-2 text-xs">{pill_conn}{pill_file}{pill_alarm}</div>
|
||||
{body}
|
||||
</div>"##,
|
||||
n = PAGE_SIZE,
|
||||
pill_conn = pill("conn", "Connections"),
|
||||
pill_file = pill("file", "File transfers"),
|
||||
pill_alarm = pill("alarm", "Alarms"),
|
||||
heading = t(lang, "audit.heading"),
|
||||
latest = tf1(lang, "audit.latest", &PAGE_SIZE.to_string()),
|
||||
pill_conn = pill("conn", t(lang, "audit.tab_conn")),
|
||||
pill_file = pill("file", t(lang, "audit.tab_file")),
|
||||
pill_alarm = pill("alarm", t(lang, "audit.tab_alarm")),
|
||||
body = body,
|
||||
)))
|
||||
}
|
||||
|
||||
async fn render_conn(state: &Arc<AppState>) -> Result<String, ApiError> {
|
||||
async fn render_conn(state: &Arc<AppState>, lang: Lang) -> Result<String, ApiError> {
|
||||
let rows = state
|
||||
.db
|
||||
.audit_conn_list(PAGE_SIZE)
|
||||
.await
|
||||
.map_err(|e| ApiError::Internal(e.to_string()))?;
|
||||
if rows.is_empty() {
|
||||
return Ok(empty_table("No connection audit rows yet."));
|
||||
return Ok(empty_table(t(lang, "audit.no_conn")));
|
||||
}
|
||||
let mut s = String::new();
|
||||
s.push_str(
|
||||
let _ = write!(
|
||||
s,
|
||||
r##"<div class="rounded-md border border-slate-800 bg-slate-900 overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="text-xs uppercase text-slate-500 bg-slate-950"><tr>
|
||||
<th class="text-left font-medium px-3 py-2">When</th>
|
||||
<th class="text-left font-medium px-3 py-2">Peer</th>
|
||||
<th class="text-left font-medium px-3 py-2">Conn / Session</th>
|
||||
<th class="text-left font-medium px-3 py-2">IP</th>
|
||||
<th class="text-left font-medium px-3 py-2">Action</th>
|
||||
<th class="text-left font-medium px-3 py-2">Note</th>
|
||||
<th class="text-left font-medium px-3 py-2">{c_when}</th>
|
||||
<th class="text-left font-medium px-3 py-2">{c_peer}</th>
|
||||
<th class="text-left font-medium px-3 py-2">{c_conn}</th>
|
||||
<th class="text-left font-medium px-3 py-2">{c_ip}</th>
|
||||
<th class="text-left font-medium px-3 py-2">{c_action}</th>
|
||||
<th class="text-left font-medium px-3 py-2">{c_note}</th>
|
||||
</tr></thead>
|
||||
<tbody class="divide-y divide-slate-800">"##,
|
||||
c_when = t(lang, "audit.col_when"),
|
||||
c_peer = t(lang, "audit.col_peer"),
|
||||
c_conn = t(lang, "audit.col_conn_session"),
|
||||
c_ip = t(lang, "audit.col_ip"),
|
||||
c_action = t(lang, "audit.col_action"),
|
||||
c_note = t(lang, "audit.col_note"),
|
||||
);
|
||||
for r in &rows {
|
||||
let _ = write!(
|
||||
@@ -110,32 +120,38 @@ async fn render_conn(state: &Arc<AppState>) -> Result<String, ApiError> {
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
async fn render_file(state: &Arc<AppState>) -> Result<String, ApiError> {
|
||||
async fn render_file(state: &Arc<AppState>, lang: Lang) -> Result<String, ApiError> {
|
||||
let rows = state
|
||||
.db
|
||||
.audit_file_list(PAGE_SIZE)
|
||||
.await
|
||||
.map_err(|e| ApiError::Internal(e.to_string()))?;
|
||||
if rows.is_empty() {
|
||||
return Ok(empty_table("No file-transfer audit rows yet."));
|
||||
return Ok(empty_table(t(lang, "audit.no_file")));
|
||||
}
|
||||
let mut s = String::new();
|
||||
s.push_str(
|
||||
let _ = write!(
|
||||
s,
|
||||
r##"<div class="rounded-md border border-slate-800 bg-slate-900 overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="text-xs uppercase text-slate-500 bg-slate-950"><tr>
|
||||
<th class="text-left font-medium px-3 py-2">When</th>
|
||||
<th class="text-left font-medium px-3 py-2">Peer</th>
|
||||
<th class="text-left font-medium px-3 py-2">Direction</th>
|
||||
<th class="text-left font-medium px-3 py-2">Path</th>
|
||||
<th class="text-left font-medium px-3 py-2">Remote</th>
|
||||
<th class="text-left font-medium px-3 py-2">{c_when}</th>
|
||||
<th class="text-left font-medium px-3 py-2">{c_peer}</th>
|
||||
<th class="text-left font-medium px-3 py-2">{c_dir}</th>
|
||||
<th class="text-left font-medium px-3 py-2">{c_path}</th>
|
||||
<th class="text-left font-medium px-3 py-2">{c_remote}</th>
|
||||
</tr></thead>
|
||||
<tbody class="divide-y divide-slate-800">"##,
|
||||
c_when = t(lang, "audit.col_when"),
|
||||
c_peer = t(lang, "audit.col_peer"),
|
||||
c_dir = t(lang, "audit.col_direction"),
|
||||
c_path = t(lang, "audit.col_path"),
|
||||
c_remote = t(lang, "audit.col_remote"),
|
||||
);
|
||||
for r in &rows {
|
||||
let dir = match r.direction {
|
||||
0 => "→ remote",
|
||||
1 => "← remote",
|
||||
0 => t(lang, "audit.dir_to_remote"),
|
||||
1 => t(lang, "audit.dir_from_remote"),
|
||||
_ => "?",
|
||||
};
|
||||
let _ = write!(
|
||||
@@ -158,26 +174,31 @@ async fn render_file(state: &Arc<AppState>) -> Result<String, ApiError> {
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
async fn render_alarm(state: &Arc<AppState>) -> Result<String, ApiError> {
|
||||
async fn render_alarm(state: &Arc<AppState>, lang: Lang) -> Result<String, ApiError> {
|
||||
let rows = state
|
||||
.db
|
||||
.audit_alarm_list(PAGE_SIZE)
|
||||
.await
|
||||
.map_err(|e| ApiError::Internal(e.to_string()))?;
|
||||
if rows.is_empty() {
|
||||
return Ok(empty_table("No alarm audit rows yet."));
|
||||
return Ok(empty_table(t(lang, "audit.no_alarm")));
|
||||
}
|
||||
let mut s = String::new();
|
||||
s.push_str(
|
||||
let _ = write!(
|
||||
s,
|
||||
r##"<div class="rounded-md border border-slate-800 bg-slate-900 overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="text-xs uppercase text-slate-500 bg-slate-950"><tr>
|
||||
<th class="text-left font-medium px-3 py-2">When</th>
|
||||
<th class="text-left font-medium px-3 py-2">Peer</th>
|
||||
<th class="text-left font-medium px-3 py-2">Type</th>
|
||||
<th class="text-left font-medium px-3 py-2">Info</th>
|
||||
<th class="text-left font-medium px-3 py-2">{c_when}</th>
|
||||
<th class="text-left font-medium px-3 py-2">{c_peer}</th>
|
||||
<th class="text-left font-medium px-3 py-2">{c_type}</th>
|
||||
<th class="text-left font-medium px-3 py-2">{c_info}</th>
|
||||
</tr></thead>
|
||||
<tbody class="divide-y divide-slate-800">"##,
|
||||
c_when = t(lang, "audit.col_when"),
|
||||
c_peer = t(lang, "audit.col_peer"),
|
||||
c_type = t(lang, "audit.col_type"),
|
||||
c_info = t(lang, "audit.col_info"),
|
||||
);
|
||||
for r in &rows {
|
||||
let typ = match r.typ {
|
||||
|
||||
Reference in New Issue
Block a user