Files

149 lines
6.9 KiB
Go

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
}