package main import ( "errors" "regexp" "strconv" "strings" ) type keycloakThemeField struct { Name string Type string Required bool Options []string Min float64 Max float64 } var keycloakThemeColorPattern = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`) var keycloakThemeFields = []keycloakThemeField{ {Name: "logoAssetId", Type: "image"}, {Name: "faviconAssetId", Type: "image"}, {Name: "logoAssetMimeType", Type: "text"}, {Name: "faviconAssetMimeType", Type: "text"}, {Name: "primaryColor", Type: "color", Required: true}, {Name: "secondaryColor", Type: "color"}, {Name: "backgroundColor", Type: "color"}, {Name: "textColor", Type: "color"}, {Name: "buttonRadius", Type: "number", Min: 0, Max: 32}, {Name: "fontFamily", Type: "select", Options: []string{"Inter", "System UI", "Roboto", "Open Sans", "Lato", "Montserrat"}}, {Name: "loginHeading", Type: "text"}, {Name: "loginSubheading", Type: "textarea"}, {Name: "loginHelpText", Type: "textarea"}, {Name: "registrationHeading", Type: "text"}, {Name: "forgotPasswordHelpText", Type: "textarea"}, {Name: "accountConsoleHeading", Type: "text"}, {Name: "emailTemplateFooter", Type: "textarea"}, {Name: "supportUrl", Type: "url"}, {Name: "privacyUrl", Type: "url"}, {Name: "termsUrl", Type: "url"}, {Name: "footerText", Type: "textarea"}, {Name: "securityText", Type: "textarea"}, } var keycloakThemeFieldNames = func() map[string]struct{} { fields := map[string]struct{}{} for _, field := range keycloakThemeFields { fields[field.Name] = struct{}{} } return fields }() var keycloakThemeAllowedSystemMetadata = map[string]struct{}{ "updatedBy": {}, "updatedAt": {}, "createdBy": {}, "createdAt": {}, "platformApproval": {}, "exportRequested": {}, } var keycloakThemeDefaultFormValues = Doc{ "primaryColor": "#4f46e5", "backgroundColor": "#ffffff", "textColor": "#0f172a", "fontFamily": "Inter", "buttonRadius": 8, "loginHeading": "Welcome back", "registrationHeading": "Create your account", "footerText": "Powered by CMS Platform", "securityText": "Never share your password with anyone.", } func keycloakThemeFormSchemaSections() []Doc { return []Doc{ { "id": "brand", "title": "Brand", "fields": []Doc{ {"type": "image", "name": "logoAssetId", "label": "Logo", "required": false}, {"type": "image", "name": "faviconAssetId", "label": "Favicon", "required": false}, {"type": "text", "name": "logoAssetMimeType", "label": "Logo MIME type", "required": false}, {"type": "text", "name": "faviconAssetMimeType", "label": "Favicon MIME type", "required": false}, {"type": "color", "name": "primaryColor", "label": "Primary colour", "required": true}, {"type": "color", "name": "secondaryColor", "label": "Secondary colour", "required": false}, {"type": "color", "name": "backgroundColor", "label": "Background colour", "required": false}, {"type": "color", "name": "textColor", "label": "Text colour", "required": false}, {"type": "select", "name": "fontFamily", "label": "Font family", "required": false, "options": []string{"Inter", "System UI", "Roboto", "Open Sans", "Lato", "Montserrat"}}, {"type": "number", "name": "buttonRadius", "label": "Button radius", "required": false}, }, }, { "id": "authCopy", "title": "Authentication copy", "fields": []Doc{ {"type": "text", "name": "loginHeading", "label": "Login heading", "required": false}, {"type": "textarea", "name": "loginSubheading", "label": "Login subheading", "required": false}, {"type": "textarea", "name": "loginHelpText", "label": "Login help text", "required": false}, {"type": "text", "name": "registrationHeading", "label": "Registration heading", "required": false}, {"type": "textarea", "name": "forgotPasswordHelpText", "label": "Forgot password help", "required": false}, }, }, { "id": "accountAndEmail", "title": "Account and email templates", "fields": []Doc{ {"type": "text", "name": "accountConsoleHeading", "label": "Account console heading", "required": false}, {"type": "textarea", "name": "emailTemplateFooter", "label": "Email footer text", "required": false}, }, }, { "id": "linksAndLegal", "title": "Links and legal", "fields": []Doc{ {"type": "url", "name": "supportUrl", "label": "Support URL", "required": false}, {"type": "url", "name": "privacyUrl", "label": "Privacy URL", "required": false}, {"type": "url", "name": "termsUrl", "label": "Terms URL", "required": false}, {"type": "textarea", "name": "footerText", "label": "Footer text", "required": false}, {"type": "textarea", "name": "securityText", "label": "Security text", "required": false}, }, }, } } func ensureThemeMetadataDefaults(doc Doc) { metadata := resolveThemeMetadata(nil, doc) if metadata == nil { metadata = Doc{} } for key, value := range keycloakThemeDefaultFormValues { if _, exists := metadata[key]; !exists { metadata[key] = value } } doc["metadata"] = metadata } func validateKeycloakThemeFormValues(metadata Doc) error { if metadata == nil { metadata = Doc{} } fieldMap := map[string]keycloakThemeField{} for _, field := range keycloakThemeFields { fieldMap[field.Name] = field } for key, value := range metadata { field, ok := fieldMap[key] if !ok { if _, allowed := keycloakThemeAllowedSystemMetadata[key]; allowed { continue } return errors.New("theme schema validation failed: unknown field " + key) } if err := validateThemeFieldValue(field, value); err != nil { return err } } for _, field := range keycloakThemeFields { if !field.Required { continue } value, exists := metadata[field.Name] if !exists || isEmptyThemeValue(value) { return errors.New("theme schema validation failed: required field " + field.Name + " is missing") } } return nil } func validateThemeFieldValue(field keycloakThemeField, value any) error { if isEmptyThemeValue(value) { if field.Required { return errors.New("theme schema validation failed: required field " + field.Name + " is missing") } return nil } switch field.Type { case "text", "textarea", "image": str, ok := value.(string) if !ok { return errors.New("theme schema validation failed: field " + field.Name + " must be a string") } trimmed := strings.TrimSpace(str) if field.Type == "image" && strings.Contains(trimmed, " ") { return errors.New("theme schema validation failed: image field " + field.Name + " must not contain spaces") } case "color": str, ok := value.(string) if !ok || !keycloakThemeColorPattern.MatchString(strings.TrimSpace(str)) { return errors.New("theme schema validation failed: field " + field.Name + " must be a hex color") } case "url": str, ok := value.(string) if !ok { return errors.New("theme schema validation failed: field " + field.Name + " must be a string") } if strings.TrimSpace(str) != "" { if err := validateThemeURL(str); err != nil { return errors.New("theme schema validation failed: field " + field.Name + " must be a valid URL") } } case "number": number, ok := normalizeThemeNumber(value) if !ok { return errors.New("theme schema validation failed: field " + field.Name + " must be a number") } if field.Min != 0 || field.Max != 0 { if number < field.Min || number > field.Max { return errors.New("theme schema validation failed: field " + field.Name + " is out of range") } } case "select": str, ok := value.(string) if !ok { return errors.New("theme schema validation failed: field " + field.Name + " must be a string") } if len(field.Options) == 0 { return nil } trimmed := strings.TrimSpace(str) for _, option := range field.Options { if option == trimmed { return nil } } return errors.New("theme schema validation failed: field " + field.Name + " has an unsupported value") } return nil } func normalizeThemeNumber(value any) (float64, bool) { switch v := value.(type) { case float64: return v, true case float32: return float64(v), true case int: return float64(v), true case int32: return float64(v), true case int64: return float64(v), true case string: parsed, err := strconv.ParseFloat(strings.TrimSpace(v), 64) if err != nil { return 0, false } return parsed, true default: return 0, false } } func isEmptyThemeValue(value any) bool { if value == nil { return true } if str, ok := value.(string); ok { return strings.TrimSpace(str) == "" } return false } func isKeycloakThemeSchemaField(name string) bool { _, exists := keycloakThemeFieldNames[name] return exists }