104 lines
3.8 KiB
TypeScript
104 lines
3.8 KiB
TypeScript
import Link from "next/link";
|
|
import { redirect } from "next/navigation";
|
|
import { apiGet } from "@/lib/server-api";
|
|
import { fmtDateTime } from "@/lib/format";
|
|
import { isRestricted } from "@/lib/access";
|
|
import type { AuditLog, Me } from "@/lib/types";
|
|
|
|
type Search = Promise<Record<string, string | string[] | undefined>>;
|
|
|
|
const FILTERS = ["from", "to", "source", "event_type", "success", "target_user_id", "actor_account"];
|
|
|
|
export default async function AuditPage({ searchParams }: { searchParams: Search }) {
|
|
const me = await apiGet<Me>("/auth/me");
|
|
if (isRestricted(me.account)) redirect("/");
|
|
const sp = await searchParams;
|
|
const params = new URLSearchParams();
|
|
for (const key of [...FILTERS, "before"]) {
|
|
const v = sp[key];
|
|
if (typeof v === "string" && v) params.set(key, v);
|
|
}
|
|
const { logs, next_before } = await apiGet<{ logs: AuditLog[]; next_before: number | null }>(`/audit?${params}`);
|
|
const value = (key: string) => (typeof sp[key] === "string" ? (sp[key] as string) : "");
|
|
|
|
const nextParams = new URLSearchParams(params);
|
|
if (next_before) nextParams.set("before", String(next_before));
|
|
|
|
return (
|
|
<>
|
|
<h1>Auditoria</h1>
|
|
<form className="filters" method="get">
|
|
<label>
|
|
De <input type="date" name="from" defaultValue={value("from")} />
|
|
</label>
|
|
<label>
|
|
Até <input type="date" name="to" defaultValue={value("to")} />
|
|
</label>
|
|
<select name="source" defaultValue={value("source")}>
|
|
<option value="">Todas as origens</option>
|
|
<option value="telegram">Telegram</option>
|
|
<option value="web">Painel</option>
|
|
<option value="worker">Worker</option>
|
|
<option value="cli">Terminal</option>
|
|
</select>
|
|
<input name="event_type" placeholder="Evento (ex.: user_activated)" defaultValue={value("event_type")} />
|
|
<select name="success" defaultValue={value("success")}>
|
|
<option value="">Qualquer resultado</option>
|
|
<option value="true">Sucesso</option>
|
|
<option value="false">Falha</option>
|
|
</select>
|
|
{value("target_user_id") && <input type="hidden" name="target_user_id" value={value("target_user_id")} />}
|
|
<button className="btn secondary" type="submit">
|
|
Filtrar
|
|
</button>
|
|
<Link href="/auditoria">Limpar</Link>
|
|
</form>
|
|
|
|
<div className="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Quando</th>
|
|
<th>Origem</th>
|
|
<th>Quem</th>
|
|
<th>Evento</th>
|
|
<th>Alvo</th>
|
|
<th>Resultado</th>
|
|
<th>Detalhes</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{logs.length === 0 && (
|
|
<tr>
|
|
<td colSpan={7} className="muted">
|
|
Nenhum evento encontrado.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
{logs.map((l) => (
|
|
<tr key={l.id}>
|
|
<td className="nowrap">{fmtDateTime(l.occurred_at)}</td>
|
|
<td>{l.source}</td>
|
|
<td>{l.actor_username ?? l.actor_user_name ?? "—"}</td>
|
|
<td>
|
|
<code>{l.event_type}</code>
|
|
</td>
|
|
<td>{l.target_user_id ? <Link href={`/usuarios/${l.target_user_id}`}>{l.target_user_name}</Link> : "—"}</td>
|
|
<td>{l.success ? <span className="badge ok">sucesso</span> : <span className="badge error">falha</span>}</td>
|
|
<td className="small">
|
|
{Object.keys(l.details).length > 0 && <code className="details">{JSON.stringify(l.details)}</code>}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{next_before && (
|
|
<p>
|
|
<Link href={`/auditoria?${nextParams}`}>Mais antigos →</Link>
|
|
</p>
|
|
)}
|
|
</>
|
|
);
|
|
}
|