package rest import ( "context" "net/http" "time" "github.com/danielgtaylor/huma/v2" "github.com/sarathsp06/sparrow/internal/webhooks" "github.com/sarathsp06/sparrow/internal/webhooks/store" ) type createAlertConfigBody struct { WebhookID string `json:"webhook_id,omitempty" doc:"Scope the alert to one webhook. Omit for a consumer-wide alert covering every webhook."` Email string `json:"email" required:"true" format:"email" doc:"Recipient email address."` EventTypes []string `json:"event_types" required:"true" doc:"Sparrow system event types to alert on, at least one of: sparrow.webhook.health_changed, sparrow.webhook.delivery_failed, sparrow.webhook.disabled."` } type createAlertConfigInput struct { Consumer string `path:"consumer" doc:"Consumer whose webhooks the alert covers."` Body createAlertConfigBody } type alertConfigItem struct { ID string `json:"id" doc:"Alert config id (UUID)."` Consumer string `json:"consumer" doc:"Consumer this alert config belongs to."` WebhookID string `json:"webhook_id,omitempty" doc:"Webhook this alert is scoped to; omitted for consumer-wide alerts."` Email string `json:"email" doc:"Recipient email address."` EventTypes []string `json:"event_types" doc:"Sparrow system event types this recipient is alerted on."` CreatedAt string `json:"created_at" format:"date-time" doc:"Creation timestamp, RFC3339."` } func toAlertConfigItem(c *store.AlertConfig) alertConfigItem { item := alertConfigItem{ ID: c.ID.String(), Consumer: c.Consumer, Email: c.Email, EventTypes: []string(c.EventTypes), CreatedAt: c.CreatedAt.Format(time.RFC3339Nano), } if c.WebhookID != nil { item.WebhookID = c.WebhookID.String() } return item } type alertConfigOutput struct { Body alertConfigItem } type listAlertConfigsInput struct { Consumer string `path:"consumer" doc:"Consumer to list alert configs in."` WebhookID string `query:"webhook_id,omitempty" doc:"Filter to alert configs scoped to one webhook. Omit to list every config in the consumer, including consumer-wide ones."` } type listAlertConfigsOutput struct { Body struct { Items []alertConfigItem `json:"items" doc:"Alert configs, oldest first. Not paginated."` } } type alertConfigIDInput struct { Consumer string `path:"consumer" doc:"Consumer the alert config belongs to."` AlertConfigID string `path:"alert_config_id" doc:"Alert config id (UUID)."` } func registerAlertConfigRoutes(api huma.API, svc webhooks.AlertConfigManager) { huma.Register(api, huma.Operation{ OperationID: "createAlertConfig", Method: http.MethodPost, Path: "/v1/consumers/{consumer}/alert-configs", Summary: "Register an email alert recipient", Description: "Opts an email address into Sparrow's self-generated health-change, permanent-delivery-failure and auto-disable notifications, scoped to one webhook or the whole consumer. Emails are only sent once an alert mailer webhook is registered under the _sparrow consumer (see GET /v1/capabilities alert_delivery.configured). Not idempotent: each call adds a config.\n\n" + "400 for an invalid email or event type, 404 when webhook_id is not in this consumer.\n\nPortal tokens: allowed, as POST /portal/api/alert-configs.", Errors: []int{400, 404}, Tags: []string{"Alert Configs"}, DefaultStatus: http.StatusCreated, }, func(ctx context.Context, in *createAlertConfigInput) (*alertConfigOutput, error) { cfg, err := svc.CreateAlertConfig(ctx, in.Consumer, in.Body.WebhookID, in.Body.Email, in.Body.EventTypes) if err != nil { return nil, mapError(ctx, err, "failed to create alert config") } return &alertConfigOutput{Body: toAlertConfigItem(cfg)}, nil }) huma.Register(api, huma.Operation{ OperationID: "listAlertConfigs", Method: http.MethodGet, Path: "/v1/consumers/{consumer}/alert-configs", Summary: "List email alert recipients", Description: "Lists alert configs in a consumer, oldest first, optionally filtered to one webhook's own configs. Not paginated.\n\nPortal tokens: allowed, as GET /portal/api/alert-configs.", Tags: []string{"Alert Configs"}, }, func(ctx context.Context, in *listAlertConfigsInput) (*listAlertConfigsOutput, error) { configs, err := svc.ListAlertConfigs(ctx, in.Consumer, in.WebhookID) if err != nil { return nil, mapError(ctx, err, "failed to list alert configs") } out := &listAlertConfigsOutput{} out.Body.Items = make([]alertConfigItem, 0, len(configs)) for _, c := range configs { out.Body.Items = append(out.Body.Items, toAlertConfigItem(c)) } return out, nil }) huma.Register(api, huma.Operation{ OperationID: "deleteAlertConfig", Method: http.MethodDelete, Path: "/v1/consumers/{consumer}/alert-configs/{alert_config_id}", Summary: "Delete an email alert recipient", Description: "Removes an alert config. The webhook and its delivery history are unaffected. Returns 204; 404 when it is not in this consumer.\n\nPortal tokens: allowed, as DELETE /portal/api/alert-configs/{alert_config_id}.", Errors: []int{404}, Tags: []string{"Alert Configs"}, DefaultStatus: http.StatusNoContent, }, func(ctx context.Context, in *alertConfigIDInput) (*emptyOutput, error) { if err := svc.DeleteAlertConfig(ctx, in.Consumer, in.AlertConfigID); err != nil { return nil, mapError(ctx, err, "failed to delete alert config") } return &emptyOutput{Status: http.StatusNoContent}, nil }) }