learn-platform-deploy/Frontend/src/app/keycloak-theme-form.ts

160 lines
5.2 KiB
TypeScript

export type ThemeFieldType = 'text' | 'textarea' | 'url' | 'number' | 'select' | 'color' | 'image'
export type ThemeFieldSchema = {
type: ThemeFieldType
name: string
label: string
required?: boolean
options?: string[]
}
export type ThemeSectionSchema = {
id: string
title: string
fields: ThemeFieldSchema[]
}
const HEX_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/
const UNSAFE_TOKENS = ['<script', '</script', 'javascript:', 'onerror=', 'onload=', '<iframe', 'srcdoc=', 'data:text/html', 'vbscript:']
export const DEFAULT_THEME_FORM_SCHEMA: ThemeSectionSchema[] = [
{
id: 'brand',
title: 'Brand',
fields: [
{ type: 'image', name: 'logoAssetId', label: 'Logo' },
{ type: 'image', name: 'faviconAssetId', label: 'Favicon' },
{ type: 'text', name: 'logoAssetMimeType', label: 'Logo MIME type' },
{ type: 'text', name: 'faviconAssetMimeType', label: 'Favicon MIME type' },
{ type: 'color', name: 'primaryColor', label: 'Primary colour', required: true },
{ type: 'color', name: 'secondaryColor', label: 'Secondary colour' },
{ type: 'color', name: 'backgroundColor', label: 'Background colour' },
{ type: 'color', name: 'textColor', label: 'Text colour' },
{ type: 'select', name: 'fontFamily', label: 'Font family', options: ['Inter', 'System UI', 'Roboto', 'Open Sans', 'Lato', 'Montserrat'] },
{ type: 'number', name: 'buttonRadius', label: 'Button radius' },
],
},
{
id: 'authCopy',
title: 'Authentication copy',
fields: [
{ type: 'text', name: 'loginHeading', label: 'Login heading' },
{ type: 'textarea', name: 'loginSubheading', label: 'Login subheading' },
{ type: 'textarea', name: 'loginHelpText', label: 'Login help text' },
{ type: 'text', name: 'registrationHeading', label: 'Registration heading' },
{ type: 'textarea', name: 'forgotPasswordHelpText', label: 'Forgot password help' },
],
},
{
id: 'accountAndEmail',
title: 'Account and email templates',
fields: [
{ type: 'text', name: 'accountConsoleHeading', label: 'Account console heading' },
{ type: 'textarea', name: 'emailTemplateFooter', label: 'Email footer text' },
],
},
{
id: 'linksAndLegal',
title: 'Links and legal',
fields: [
{ type: 'url', name: 'supportUrl', label: 'Support URL' },
{ type: 'url', name: 'privacyUrl', label: 'Privacy URL' },
{ type: 'url', name: 'termsUrl', label: 'Terms URL' },
{ type: 'textarea', name: 'footerText', label: 'Footer text' },
{ type: 'textarea', name: 'securityText', label: 'Security text' },
],
},
]
export const DEFAULT_THEME_FORM_VALUES: Record<string, string | number | boolean> = {
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.',
}
export type ThemeFormValidationResult = {
valid: boolean
errors: Record<string, string>
}
function hasUnsafeToken(value: string): boolean {
const normalized = value.toLowerCase()
return UNSAFE_TOKENS.some(token => normalized.includes(token))
}
function isValidUrl(value: string): boolean {
try {
const parsed = new URL(value)
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
} catch {
return false
}
}
export function validateThemeFormValues(
schema: ThemeSectionSchema[],
values: Record<string, unknown>,
): ThemeFormValidationResult {
const errors: Record<string, string> = {}
for (const section of schema) {
for (const field of section.fields) {
const value = values[field.name]
const textValue = typeof value === 'string' ? value.trim() : ''
if (field.required && (value === undefined || value === null || textValue === '')) {
errors[field.name] = `${field.label} is required`
continue
}
if (value === undefined || value === null || textValue === '') {
continue
}
if (typeof value === 'string' && hasUnsafeToken(value)) {
errors[field.name] = `${field.label} contains unsafe content`
continue
}
if (field.type === 'color' && typeof value === 'string' && !HEX_COLOR_PATTERN.test(value.trim())) {
errors[field.name] = `${field.label} must be a hex color`
continue
}
if (field.type === 'url' && typeof value === 'string' && !isValidUrl(value.trim())) {
errors[field.name] = `${field.label} must be a valid URL`
continue
}
if (field.type === 'number') {
const numeric = typeof value === 'number' ? value : Number(value)
if (Number.isNaN(numeric)) {
errors[field.name] = `${field.label} must be a number`
continue
}
}
if (field.type === 'select' && field.options && !field.options.includes(String(value).trim())) {
errors[field.name] = `${field.label} has an unsupported value`
continue
}
if (field.type === 'image' && typeof value === 'string' && value.includes(' ')) {
errors[field.name] = `${field.label} must not contain spaces`
}
}
}
return {
valid: Object.keys(errors).length === 0,
errors,
}
}