275 lines
8.6 KiB
Go
275 lines
8.6 KiB
Go
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
|
|
}
|