feat: ✨ connection audit shows last 7 days
This commit is contained in:
@@ -1195,22 +1195,36 @@ def delete_host(hid: int):
|
|||||||
@require_login
|
@require_login
|
||||||
def list_connection_audit():
|
def list_connection_audit():
|
||||||
raw_limit = request.args.get("limit") or "200"
|
raw_limit = request.args.get("limit") or "200"
|
||||||
|
raw_days = request.args.get("days_back")
|
||||||
try:
|
try:
|
||||||
limit = int(raw_limit)
|
limit = int(raw_limit)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
limit = 200
|
limit = 200
|
||||||
limit = max(1, min(limit, 500))
|
limit = max(1, min(limit, 500))
|
||||||
|
|
||||||
|
# Build the where clause for days filtering
|
||||||
|
where_clause = ""
|
||||||
|
params: list[Any] = []
|
||||||
|
if raw_days is not None:
|
||||||
|
try:
|
||||||
|
days = int(raw_days)
|
||||||
|
if days > 0:
|
||||||
|
where_clause = "WHERE started_at >= DATE_SUB(NOW(), INTERVAL %s DAY)"
|
||||||
|
params = [days]
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
with db_cursor() as (_, cur):
|
with db_cursor() as (_, cur):
|
||||||
cur.execute(
|
query = f"""
|
||||||
"""
|
|
||||||
SELECT id, host_id, host_label, hostname, port, jump_host_id,
|
SELECT id, host_id, host_label, hostname, port, jump_host_id,
|
||||||
started_at, ended_at, duration_seconds
|
started_at, ended_at, duration_seconds
|
||||||
FROM ssh_connection_audit
|
FROM ssh_connection_audit
|
||||||
|
{where_clause}
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
LIMIT %s
|
LIMIT %s
|
||||||
""",
|
"""
|
||||||
(limit,),
|
params.append(limit)
|
||||||
)
|
cur.execute(query, tuple(params))
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
return jsonify({"items": rows})
|
return jsonify({"items": rows})
|
||||||
|
|
||||||
|
|||||||
+27
-2
@@ -46,6 +46,7 @@ const showAuditLog = ref(false);
|
|||||||
const auditLoading = ref(false);
|
const auditLoading = ref(false);
|
||||||
const auditErr = ref("");
|
const auditErr = ref("");
|
||||||
const auditRows = ref<ConnectionAuditRow[]>([]);
|
const auditRows = ref<ConnectionAuditRow[]>([]);
|
||||||
|
const auditShowAll = ref(false);
|
||||||
const deleteIdentityErr = ref("");
|
const deleteIdentityErr = ref("");
|
||||||
const deleteIdentityErrId = ref<number | null>(null);
|
const deleteIdentityErrId = ref<number | null>(null);
|
||||||
const newFolderLabel = ref("");
|
const newFolderLabel = ref("");
|
||||||
@@ -208,10 +209,24 @@ function fmtDuration(totalSeconds: number | null): string {
|
|||||||
|
|
||||||
async function openAuditLog() {
|
async function openAuditLog() {
|
||||||
showAuditLog.value = true;
|
showAuditLog.value = true;
|
||||||
|
auditShowAll.value = false;
|
||||||
auditLoading.value = true;
|
auditLoading.value = true;
|
||||||
auditErr.value = "";
|
auditErr.value = "";
|
||||||
try {
|
try {
|
||||||
auditRows.value = await api.listConnectionAudit(250);
|
auditRows.value = await api.listConnectionAudit(250, 7);
|
||||||
|
} catch (e) {
|
||||||
|
auditErr.value = e instanceof Error ? e.message : "Failed to load audit log";
|
||||||
|
} finally {
|
||||||
|
auditLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAllAuditLog() {
|
||||||
|
auditShowAll.value = true;
|
||||||
|
auditLoading.value = true;
|
||||||
|
auditErr.value = "";
|
||||||
|
try {
|
||||||
|
auditRows.value = await api.listConnectionAudit(500);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
auditErr.value = e instanceof Error ? e.message : "Failed to load audit log";
|
auditErr.value = e instanceof Error ? e.message : "Failed to load audit log";
|
||||||
} finally {
|
} finally {
|
||||||
@@ -778,6 +793,15 @@ async function deleteIdentityRow(id: number) {
|
|||||||
>
|
>
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<h2 class="text-lg font-semibold text-white">Connection audit</h2>
|
<h2 class="text-lg font-semibold text-white">Connection audit</h2>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
v-if="!auditShowAll"
|
||||||
|
type="button"
|
||||||
|
class="rounded-lg bg-slate-800 px-3 py-1.5 text-xs hover:bg-slate-700"
|
||||||
|
@click="loadAllAuditLog"
|
||||||
|
>
|
||||||
|
Show all
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-lg px-3 py-1.5 text-xs text-slate-400 hover:bg-slate-800 hover:text-white"
|
class="rounded-lg px-3 py-1.5 text-xs text-slate-400 hover:bg-slate-800 hover:text-white"
|
||||||
@@ -786,8 +810,9 @@ async function deleteIdentityRow(id: number) {
|
|||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<p class="mt-1 text-xs text-slate-500">
|
<p class="mt-1 text-xs text-slate-500">
|
||||||
Recent SSH sessions and how long they lasted.
|
Recent SSH sessions from the last 7 days and how long they lasted.
|
||||||
</p>
|
</p>
|
||||||
<p v-if="auditErr" class="mt-3 text-xs text-red-400">{{ auditErr }}</p>
|
<p v-if="auditErr" class="mt-3 text-xs text-red-400">{{ auditErr }}</p>
|
||||||
<p v-else-if="auditLoading" class="mt-3 text-xs text-slate-400">
|
<p v-else-if="auditLoading" class="mt-3 text-xs text-slate-400">
|
||||||
|
|||||||
+4
-1
@@ -166,8 +166,11 @@ export const api = {
|
|||||||
await handle(res);
|
await handle(res);
|
||||||
},
|
},
|
||||||
|
|
||||||
async listConnectionAudit(limit = 200): Promise<ConnectionAuditRow[]> {
|
async listConnectionAudit(limit = 200, daysBack?: number): Promise<ConnectionAuditRow[]> {
|
||||||
const q = new URLSearchParams({ limit: String(limit) });
|
const q = new URLSearchParams({ limit: String(limit) });
|
||||||
|
if (daysBack !== undefined) {
|
||||||
|
q.set("days_back", String(daysBack));
|
||||||
|
}
|
||||||
const res = await fetch(`/api/audit/connections?${q.toString()}`, {
|
const res = await fetch(`/api/audit/connections?${q.toString()}`, {
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user