package notify import "encoding/json" import "fmt" import "strconv" import "strings" import "codit/internal/models" // transportSchemas is the source of truth for each transport type's configurable // fields. The admin UI renders a real input per field (so no raw JSON is typed), // and Create/Update validate + split submitted values against it. Adding a new // transport = add a schema here plus its delivery impl; the UI adapts. var transportSchemas = []models.TransportSchema{ { Type: models.TransportTypeWebhook, Label: "Webhook", AddressLabel: "Webhook URL", AddressHelp: "The endpoint the event JSON is POSTed to (per channel).", Fields: []models.TransportField{ {Key: "method", Label: "HTTP Method", Kind: models.TransportFieldSelect, Default: "POST", Options: []models.TransportFieldOption{{Value: "POST", Label: "POST"}, {Value: "PUT", Label: "PUT"}}}, {Key: "header_name", Label: "Auth Header Name", Kind: models.TransportFieldString, Placeholder: "Authorization", Help: "Header the auth token is sent in (defaults to Authorization)."}, {Key: "token", Label: "Auth Token", Kind: models.TransportFieldSecret, Help: "Sent as the value of the auth header, e.g. \"Bearer abc123\". Optional."}, {Key: "ca_ref", Label: "Verify with CA", Kind: models.TransportFieldSelect, OptionsSource: models.TransportOptionSourcePKICAs, Help: "For HTTPS endpoints signed by an internal CA. Trusted in addition to the system roots. Leave unset to use system roots only."}, {Key: "insecure_skip_verify", Label: "Ignore TLS certificate errors", Kind: models.TransportFieldBool, Help: "Skip server certificate verification entirely. Use only for a self-signed endpoint you control."}, }, }, { Type: models.TransportTypeSMTP, Label: "Email (SMTP)", AddressLabel: "Recipient(s)", AddressHelp: "One or more destination email addresses, comma-separated.", Fields: []models.TransportField{ {Key: "host", Label: "SMTP Host", Kind: models.TransportFieldString, Required: true, Placeholder: "smtp.example.com"}, {Key: "port", Label: "Port", Kind: models.TransportFieldInt, Default: "587"}, {Key: "security", Label: "Security", Kind: models.TransportFieldSelect, Default: "starttls", Options: []models.TransportFieldOption{{Value: "starttls", Label: "STARTTLS"}, {Value: "smtps", Label: "TLS (SMTPS)"}, {Value: "none", Label: "None (plaintext)"}}}, {Key: "ca_ref", Label: "Verify with CA", Kind: models.TransportFieldSelect, OptionsSource: models.TransportOptionSourcePKICAs, Help: "For a mail server whose certificate is signed by an internal CA (STARTTLS/SMTPS). Trusted in addition to system roots; ignored when Security is None."}, {Key: "insecure_skip_verify", Label: "Ignore TLS certificate errors", Kind: models.TransportFieldBool, Help: "Skip server certificate verification (STARTTLS/SMTPS). Leave off unless using a self-signed cert."}, {Key: "from", Label: "From Address", Kind: models.TransportFieldString, Required: true, Placeholder: "alerts@example.com"}, {Key: "username", Label: "Username", Kind: models.TransportFieldString, Help: "Leave blank if the server needs no authentication."}, {Key: "password", Label: "Password", Kind: models.TransportFieldSecret}, }, }, { Type: models.TransportTypeMSGraph, Label: "Email (Microsoft Graph)", AddressLabel: "Recipient(s)", AddressHelp: "One or more destination email addresses, comma-separated.", Fields: []models.TransportField{ {Key: "tenant_id", Label: "Tenant ID", Kind: models.TransportFieldString, Required: true}, {Key: "client_id", Label: "Client ID", Kind: models.TransportFieldString, Required: true}, {Key: "sender", Label: "Sender (mailbox)", Kind: models.TransportFieldString, Required: true, Placeholder: "alerts@example.com", Help: "The mailbox the message is sent as (sendMail on this user)."}, {Key: "cloud", Label: "Cloud", Kind: models.TransportFieldSelect, Default: "commercial", Options: []models.TransportFieldOption{{Value: "commercial", Label: "Commercial (global)"}, {Value: "gcc_high", Label: "US Gov (GCC High / DoD)"}, {Value: "china", Label: "China (21Vianet)"}}, Help: "Sovereign cloud endpoints. Use Commercial unless your tenant is in a national cloud."}, {Key: "client_secret", Label: "Client Secret", Kind: models.TransportFieldSecret, Required: true}, }, }, { Type: models.TransportTypeMSTeams, Label: "Microsoft Teams", AddressLabel: "Incoming Webhook URL", AddressHelp: "The Teams channel incoming-webhook URL (per channel). Self-contained; no provider config needed.", Fields: []models.TransportField{}, }, { Type: models.TransportTypeSlack, Label: "Slack", AddressLabel: "Incoming Webhook URL", AddressHelp: "The Slack incoming-webhook URL (per channel). Self-contained; no provider config needed.", Fields: []models.TransportField{}, }, } // TransportSchemas returns a copy of all known transport type schemas. func TransportSchemas() []models.TransportSchema { return append([]models.TransportSchema(nil), transportSchemas...) } // SchemaFor returns the schema for a transport type. func SchemaFor(t string) (models.TransportSchema, bool) { var s models.TransportSchema for _, s = range transportSchemas { if s.Type == t { return s, true } } return models.TransportSchema{}, false } // SplitTransportValues validates flat string inputs against a type's schema and // returns the typed non-secret config as JSON plus the secret field values that // were provided (non-empty). Required non-secret fields must be present; secret // requiredness is enforced by the caller on create (an update may omit a secret // to keep the stored one). func SplitTransportValues(schemaType string, configValues map[string]string, secretValues map[string]string) (string, map[string]string, error) { var schema models.TransportSchema var ok bool var cfg map[string]any var secrets map[string]string var f models.TransportField var raw string var out []byte var err error schema, ok = SchemaFor(schemaType) if !ok { return "", nil, fmt.Errorf("unknown transport type %q", schemaType) } cfg = map[string]any{} secrets = map[string]string{} for _, f = range schema.Fields { if f.Kind == models.TransportFieldSecret { raw = strings.TrimSpace(secretValues[f.Key]) if raw != "" { secrets[f.Key] = raw } continue } raw = strings.TrimSpace(configValues[f.Key]) if raw == "" { raw = f.Default } if raw == "" && f.Required { return "", nil, fmt.Errorf("%s is required", f.Label) } switch f.Kind { case models.TransportFieldInt: var n int64 if raw == "" { cfg[f.Key] = 0 } else { n, err = strconv.ParseInt(raw, 10, 64) if err != nil { return "", nil, fmt.Errorf("%s must be a number", f.Label) } cfg[f.Key] = n } case models.TransportFieldBool: cfg[f.Key] = raw == "true" || raw == "1" default: cfg[f.Key] = raw } } out, err = json.Marshal(cfg) if err != nil { return "", nil, err } return string(out), secrets, nil }