Files
bear-cli/lib/bear_cli/audit_logs.ex
T
queena 464db21745 feat: 實作管理端指令群 jwks/accounts/audit-logs(issue #12)
- docs/commands.md 新增 §3.8 jwks、§3.9 accounts、§3.10 audit-logs 規格
- Api 新增 jwks/accounts/audit-logs 端點(共用 api_request)
- 新增 Jwks/Accounts/AuditLogs/Admin 模組;CLI 接線三個指令群
- 403 → 退出碼 8 提示需 admin;一次性密碼只在成功當下輸出
- 測試 100 例全綠(fake API 注入,比照 cli_test.exs)
2026-09-10 03:07:40 +08:00

104 lines
2.9 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
defmodule BearCli.AuditLogs do
@moduledoc """
`bear audit-logs` 指令群(稽核日誌;docs/commands.md §3.10、issue #12)。
對應伺服器端稽核日誌 JSON API(alterminal/bear#46,`/api/v1/audit-logs`、
PAT Bearer、admin 限定)。認證沿用既有 PAT 憑證(`BEAR_TOKEN` 或憑證檔)。
退出碼:0 成功;2 用法錯誤;3 未登入或 401;6 網路/伺服器錯誤;
8 權限不足(403,僅限 admin)。
"""
import BearCli.Admin, only: [context: 1, fail: 3, format_api_error: 2]
alias BearCli.Api
# -- 入口 --
@doc """
執行 `bear audit-logs <動詞>`,回傳退出碼。`opts[:audit_logs_api]` 可注入
假 API 模組供測試(需實作 `audit_logs_list/3`)。
"""
def run(:list, opts) do
api = opts[:audit_logs_api] || Api
with {:ok, ctx} <- context(opts) do
params =
[page: opts[:page] || 1, per_page: opts[:per_page] || 50]
|> maybe_put(:category, opts[:category])
case api.audit_logs_list(ctx.issuer, ctx.token, params) do
{:ok, body} ->
entries = body["data"] || []
if opts[:json] do
IO.puts(
Jason.encode!(
Map.take(body, ["page", "per_page", "total", "total_pages", "emails"])
|> Map.merge(%{ok: true, entries: entries})
)
)
else
print_table(entries, body["emails"] || %{})
end
0
{:error, _kind, _detail} = error ->
{message, code} = format_api_error(ctx, error)
fail(opts, message, code)
end
else
{:error, message, code} -> fail(opts, message, code)
end
end
defp maybe_put(params, _key, nil), do: params
defp maybe_put(params, _key, ""), do: params
defp maybe_put(params, key, value), do: Keyword.put(params, key, value)
# -- 輸出 --
@table_headers ["TIME", "CATEGORY", "EVENT", "ACTOR", "ACCOUNT"]
defp print_table(entries, emails) do
rows =
Enum.map(entries, fn entry ->
[
entry["created_at"] || "",
entry["category"] || "",
entry["event"] || "",
email_of(emails, entry["actor_id"]),
email_of(emails, entry["account_id"])
]
end)
widths =
Enum.with_index(@table_headers, fn _header, i ->
Enum.max([
String.length(Enum.at(@table_headers, i))
| Enum.map(rows, &String.length(Enum.at(&1, i)))
])
end)
render_row = fn cells ->
cells
|> Enum.with_index()
|> Enum.map(fn {cell, i} -> String.pad_trailing(cell, Enum.at(widths, i)) end)
|> Enum.join(" ")
|> String.trim_trailing()
end
IO.puts(render_row.(@table_headers))
Enum.each(rows, fn cells -> IO.puts(render_row.(cells)) end)
end
defp email_of(_emails, nil), do: "-"
defp email_of(emails, id) do
case Map.get(emails, id) do
nil -> String.slice(id || "", 0, 8)
email -> email
end
end
end