learn-platform-deploy/Backend/cmd/api/keycloak_theme_form_schema.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
}