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