diff --git a/cmd/system_webhooklog.go b/cmd/system_webhooklog.go new file mode 100644 index 0000000..ca461ac --- /dev/null +++ b/cmd/system_webhooklog.go @@ -0,0 +1,146 @@ +package cmd + +import ( + "fmt" + "os" + "strings" + "text/tabwriter" + + "github.com/pepabo/xpoint-cli/internal/xpoint" + "github.com/spf13/cobra" +) + +var ( + systemWebhooklogListFrom string + systemWebhooklogListTo string + systemWebhooklogListDocID int + systemWebhooklogListFormCode string + systemWebhooklogListRouteCode string + systemWebhooklogListStatus string + systemWebhooklogListURL string + systemWebhooklogListLimit int + systemWebhooklogListOffset int + systemWebhooklogListOutput string + systemWebhooklogListJQ string + + systemWebhooklogShowJQ string +) + +var systemWebhooklogCmd = &cobra.Command{ + Use: "webhooklog", + Short: "Inspect X-point webhook delivery logs (admin)", +} + +var systemWebhooklogListCmd = &cobra.Command{ + Use: "list", + Short: "List webhook delivery logs", + Long: `List webhook delivery logs via GET /api/v1/system/webhooklog. + +Filters are all optional. --status accepts all | success | failed. +Requires an administrator account.`, + RunE: runSystemWebhooklogList, +} + +var systemWebhooklogShowCmd = &cobra.Command{ + Use: "show ", + Short: "Show webhook delivery log detail (request/response)", + Long: `Fetch detailed request/response data for a single webhook log entry via +GET /api/v1/system/webhooklog/{uuid}. Requires an administrator account. + +The response is emitted as JSON because the request/response body shape +depends on the remote endpoint. Use --jq to filter the output.`, + Args: cobra.ExactArgs(1), + RunE: runSystemWebhooklogShow, +} + +func init() { + systemCmd.AddCommand(systemWebhooklogCmd) + systemWebhooklogCmd.AddCommand(systemWebhooklogListCmd) + systemWebhooklogCmd.AddCommand(systemWebhooklogShowCmd) + + lf := systemWebhooklogListCmd.Flags() + lf.StringVar(&systemWebhooklogListFrom, "from", "", "send date from (e.g. 2022/10/01)") + lf.StringVar(&systemWebhooklogListTo, "to", "", "send date to (e.g. 2022/12/01)") + lf.IntVar(&systemWebhooklogListDocID, "docid", 0, "document id (0 = omit)") + lf.StringVar(&systemWebhooklogListFormCode, "form-code", "", "form code") + lf.StringVar(&systemWebhooklogListRouteCode, "route-code", "", "approval route code") + lf.StringVar(&systemWebhooklogListStatus, "status", "", "status: all | success | failed") + lf.StringVar(&systemWebhooklogListURL, "url", "", "destination URL (partial match)") + lf.IntVar(&systemWebhooklogListLimit, "limit", 0, "fetch count (0 = omit; server default 50)") + lf.IntVar(&systemWebhooklogListOffset, "offset", 0, "offset (0 = omit; server default 0)") + lf.StringVarP(&systemWebhooklogListOutput, "output", "o", "", "output format: table|json (default: table on TTY, json otherwise)") + lf.StringVar(&systemWebhooklogListJQ, "jq", "", "apply a gojq filter to the JSON response (forces JSON output)") + + sf := systemWebhooklogShowCmd.Flags() + sf.StringVar(&systemWebhooklogShowJQ, "jq", "", "apply a gojq filter to the JSON response") +} + +func runSystemWebhooklogList(cmd *cobra.Command, _ []string) error { + p := xpoint.WebhooklogListParams{ + From: systemWebhooklogListFrom, + To: systemWebhooklogListTo, + FormCode: systemWebhooklogListFormCode, + RouteCode: systemWebhooklogListRouteCode, + Status: systemWebhooklogListStatus, + URL: systemWebhooklogListURL, + } + if cmd.Flags().Changed("docid") { + v := systemWebhooklogListDocID + p.DocID = &v + } + if cmd.Flags().Changed("limit") { + v := systemWebhooklogListLimit + p.Limit = &v + } + if cmd.Flags().Changed("offset") { + v := systemWebhooklogListOffset + p.Offset = &v + } + if p.Status != "" { + switch strings.ToLower(p.Status) { + case "all", "success", "failed": + default: + return fmt.Errorf("--status must be all|success|failed, got %q", p.Status) + } + } + + client, err := newClientFromFlags(cmd.Context()) + if err != nil { + return err + } + res, err := client.ListWebhooklog(cmd.Context(), p) + if err != nil { + return err + } + + return render(res, resolveOutputFormat(systemWebhooklogListOutput), systemWebhooklogListJQ, func() error { + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + defer w.Flush() + fmt.Fprintln(w, "SEND_DATE\tSTATUS\tDOCID\tFORM_CODE\tROUTE_CODE\tTITLE\tURL\tUUID") + for _, e := range res.Data { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + e.SendDate, e.StatusCode, e.DocID, e.FormCode, e.RouteCode, e.Title1, e.URL, e.UUID, + ) + } + return nil + }) +} + +func runSystemWebhooklogShow(cmd *cobra.Command, args []string) error { + uuid := strings.TrimSpace(args[0]) + if uuid == "" { + return fmt.Errorf("uuid is required") + } + client, err := newClientFromFlags(cmd.Context()) + if err != nil { + return err + } + out, err := client.GetWebhooklog(cmd.Context(), uuid) + if err != nil { + return err + } + if systemWebhooklogShowJQ != "" { + return runJQ(out, systemWebhooklogShowJQ) + } + return writeJSON(os.Stdout, out) +} diff --git a/internal/schema/schema_test.go b/internal/schema/schema_test.go index f2a6a3e..74b098d 100644 --- a/internal/schema/schema_test.go +++ b/internal/schema/schema_test.go @@ -43,6 +43,8 @@ func TestAliases_Sorted(t *testing.T) { "system.master.list", "system.master.show", "system.master.upload", + "system.webhooklog.list", + "system.webhooklog.show", } if len(got) != len(want) { t.Fatalf("aliases = %v", got) diff --git a/internal/schema/system.webhooklog.list.json b/internal/schema/system.webhooklog.list.json new file mode 100644 index 0000000..7a90bc2 --- /dev/null +++ b/internal/schema/system.webhooklog.list.json @@ -0,0 +1,85 @@ +{ + "method": "GET", + "path": "/api/v1/system/webhooklog", + "summary": "Webhookログ取得", + "description": "Webhook の送信日時等を指定して Webhook 送信ログを検索・取得する。\n管理者権限が必要。\n", + "parameters": [ + { + "name": "from", + "in": "query", + "type": "string", + "description": "送信日時 From (例: 2022/10/01)" + }, + { + "name": "to", + "in": "query", + "type": "string", + "description": "送信日時 To (例: 2022/12/01)" + }, + { + "name": "docid", + "in": "query", + "type": "integer", + "description": "書類ID" + }, + { + "name": "form_code", + "in": "query", + "type": "string", + "description": "フォームコード" + }, + { + "name": "route_code", + "in": "query", + "type": "string", + "description": "承認ルートコード" + }, + { + "name": "status", + "in": "query", + "type": "string", + "description": "ステータス: all | success | failed" + }, + { + "name": "url", + "in": "query", + "type": "string", + "description": "送信先URL (部分一致)" + }, + { + "name": "limit", + "in": "query", + "type": "integer", + "description": "取得件数 (default 50, 推奨 1000 以下)" + }, + { + "name": "offset", + "in": "query", + "type": "integer", + "description": "取得開始位置 (default 0)" + } + ], + "response": { + "type": "object", + "properties": { + "data": { + "type": "array", + "description": "ログデータ (存在しない場合は空配列)", + "items": { + "type": "object", + "properties": { + "domain_code": { "type": "string", "description": "ドメインコード" }, + "docid": { "type": "string", "description": "書類ID" }, + "form_code": { "type": "string", "description": "フォームコード" }, + "route_code": { "type": "string", "description": "承認ルートコード" }, + "title1": { "type": "string", "description": "件名" }, + "url": { "type": "string", "description": "送信先URL" }, + "status_code": { "type": "string", "description": "ステータスコード" }, + "send_date": { "type": "string", "description": "送信日時" }, + "uuid": { "type": "string", "description": "UUID (詳細取得に使用)" } + } + } + } + } + } +} diff --git a/internal/schema/system.webhooklog.show.json b/internal/schema/system.webhooklog.show.json new file mode 100644 index 0000000..d122d88 --- /dev/null +++ b/internal/schema/system.webhooklog.show.json @@ -0,0 +1,33 @@ +{ + "method": "GET", + "path": "/api/v1/system/webhooklog/{uuid}", + "summary": "Webhook詳細ログ取得", + "description": "Webhook 送信ログの詳細情報 (リクエスト情報、レスポンス情報) を取得する。\n管理者権限が必要。\n", + "parameters": [ + { + "name": "uuid", + "in": "path", + "required": true, + "type": "string", + "description": "Webhookログの UUID" + } + ], + "response": { + "type": "object", + "properties": { + "domain_code": { "type": "string", "description": "ドメインコード" }, + "docid": { "type": "string", "description": "書類ID" }, + "url": { "type": "string", "description": "送信先URL" }, + "status_code": { "type": "string", "description": "ステータスコード" }, + "uuid": { "type": "string", "description": "UUID" }, + "request": { + "type": "object", + "description": "リクエスト情報 (header/body 等)" + }, + "response": { + "type": "object", + "description": "レスポンス情報 (header/body 等。body は送信先応答により文字列等になる場合がある)" + } + } + } +} diff --git a/internal/xpoint/client.go b/internal/xpoint/client.go index c02a6ad..84077e3 100644 --- a/internal/xpoint/client.go +++ b/internal/xpoint/client.go @@ -374,6 +374,88 @@ func (c *Client) UploadUserMasterCSV(ctx context.Context, tableName, fileName st return &out, nil } +type WebhooklogEntry struct { + DomainCode string `json:"domain_code"` + DocID string `json:"docid"` + FormCode string `json:"form_code"` + RouteCode string `json:"route_code"` + Title1 string `json:"title1"` + URL string `json:"url"` + StatusCode string `json:"status_code"` + SendDate string `json:"send_date"` + UUID string `json:"uuid"` +} + +type WebhooklogListResponse struct { + Data []WebhooklogEntry `json:"data"` +} + +// WebhooklogListParams holds query parameters for GET /api/v1/system/webhooklog. +type WebhooklogListParams struct { + From string // from + To string // to + DocID *int // docid + FormCode string // form_code + RouteCode string // route_code + Status string // status: all | success | failed + URL string // url + Limit *int // limit + Offset *int // offset +} + +func (p WebhooklogListParams) query() url.Values { + v := url.Values{} + if p.From != "" { + v.Set("from", p.From) + } + if p.To != "" { + v.Set("to", p.To) + } + if p.DocID != nil { + v.Set("docid", strconv.Itoa(*p.DocID)) + } + if p.FormCode != "" { + v.Set("form_code", p.FormCode) + } + if p.RouteCode != "" { + v.Set("route_code", p.RouteCode) + } + if p.Status != "" { + v.Set("status", p.Status) + } + if p.URL != "" { + v.Set("url", p.URL) + } + if p.Limit != nil { + v.Set("limit", strconv.Itoa(*p.Limit)) + } + if p.Offset != nil { + v.Set("offset", strconv.Itoa(*p.Offset)) + } + return v +} + +// ListWebhooklog calls GET /api/v1/system/webhooklog (admin). +func (c *Client) ListWebhooklog(ctx context.Context, p WebhooklogListParams) (*WebhooklogListResponse, error) { + var out WebhooklogListResponse + if err := c.do(ctx, http.MethodGet, "/api/v1/system/webhooklog", p.query(), nil, &out); err != nil { + return nil, err + } + return &out, nil +} + +// GetWebhooklog calls GET /api/v1/system/webhooklog/{uuid} (admin). The +// response contains request/response detail whose body may be an arbitrary +// type, so it is returned as raw JSON. +func (c *Client) GetWebhooklog(ctx context.Context, uuid string) (json.RawMessage, error) { + path := fmt.Sprintf("/api/v1/system/webhooklog/%s", url.PathEscape(uuid)) + var out json.RawMessage + if err := c.do(ctx, http.MethodGet, path, nil, nil, &out); err != nil { + return nil, err + } + return out, nil +} + type Approval struct { DocID int `json:"docid"` Hidden *bool `json:"hidden,omitempty"`