Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
468 lines
13 KiB
JavaScript
468 lines
13 KiB
JavaScript
/**
|
|
* Validation Module
|
|
*
|
|
* Provides standalone validation system for fields, forms, and data.
|
|
* Features:
|
|
* - Schema-based validation
|
|
* - Field-level validation
|
|
* - Async validation
|
|
* - Custom validation rules
|
|
* - Integration with form-handling
|
|
* - Integration with LiveComponents
|
|
*/
|
|
|
|
import { Logger } from '../../core/logger.js';
|
|
|
|
/**
|
|
* Built-in validation rules
|
|
*/
|
|
const builtInRules = {
|
|
required: (value, options = {}) => {
|
|
if (value === null || value === undefined || value === '') {
|
|
return options.message || 'This field is required';
|
|
}
|
|
return true;
|
|
},
|
|
|
|
email: (value, options = {}) => {
|
|
if (!value) return true; // Optional if not required
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(value)) {
|
|
return options.message || 'Invalid email address';
|
|
}
|
|
return true;
|
|
},
|
|
|
|
url: (value, options = {}) => {
|
|
if (!value) return true;
|
|
try {
|
|
new URL(value);
|
|
return true;
|
|
} catch {
|
|
return options.message || 'Invalid URL';
|
|
}
|
|
},
|
|
|
|
min: (value, options = {}) => {
|
|
if (!value && value !== 0) return true;
|
|
const min = parseFloat(options.value);
|
|
const numValue = typeof value === 'string' ? parseFloat(value) : value;
|
|
if (isNaN(numValue) || numValue < min) {
|
|
return options.message || `Value must be at least ${min}`;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
max: (value, options = {}) => {
|
|
if (!value && value !== 0) return true;
|
|
const max = parseFloat(options.value);
|
|
const numValue = typeof value === 'string' ? parseFloat(value) : value;
|
|
if (isNaN(numValue) || numValue > max) {
|
|
return options.message || `Value must be at most ${max}`;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
minLength: (value, options = {}) => {
|
|
if (!value) return true;
|
|
const min = parseInt(options.value, 10);
|
|
if (typeof value !== 'string' || value.length < min) {
|
|
return options.message || `Must be at least ${min} characters`;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
maxLength: (value, options = {}) => {
|
|
if (!value) return true;
|
|
const max = parseInt(options.value, 10);
|
|
if (typeof value !== 'string' || value.length > max) {
|
|
return options.message || `Must be at most ${max} characters`;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
pattern: (value, options = {}) => {
|
|
if (!value) return true;
|
|
const regex = new RegExp(options.value);
|
|
if (!regex.test(value)) {
|
|
return options.message || 'Invalid format';
|
|
}
|
|
return true;
|
|
},
|
|
|
|
number: (value, options = {}) => {
|
|
if (!value && value !== 0) return true;
|
|
if (isNaN(parseFloat(value))) {
|
|
return options.message || 'Must be a number';
|
|
}
|
|
return true;
|
|
},
|
|
|
|
integer: (value, options = {}) => {
|
|
if (!value && value !== 0) return true;
|
|
if (!Number.isInteger(parseFloat(value))) {
|
|
return options.message || 'Must be an integer';
|
|
}
|
|
return true;
|
|
},
|
|
|
|
phone: (value, options = {}) => {
|
|
if (!value) return true;
|
|
const phoneRegex = /^[\d\s\-\+\(\)]+$/;
|
|
if (!phoneRegex.test(value)) {
|
|
return options.message || 'Invalid phone number';
|
|
}
|
|
return true;
|
|
},
|
|
|
|
postalCode: (value, options = {}) => {
|
|
if (!value) return true;
|
|
const country = options.country || 'DE';
|
|
const patterns = {
|
|
DE: /^\d{5}$/,
|
|
US: /^\d{5}(-\d{4})?$/,
|
|
UK: /^[A-Z]{1,2}\d{1,2}[A-Z]?\s?\d[A-Z]{2}$/i,
|
|
FR: /^\d{5}$/
|
|
};
|
|
const pattern = patterns[country] || patterns.DE;
|
|
if (!pattern.test(value)) {
|
|
return options.message || `Invalid postal code for ${country}`;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
custom: (value, options = {}) => {
|
|
if (!options.validator || typeof options.validator !== 'function') {
|
|
return 'Custom validator function required';
|
|
}
|
|
return options.validator(value, options);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Validator - Schema-based validation
|
|
*/
|
|
export class Validator {
|
|
constructor(schema = {}) {
|
|
this.schema = schema;
|
|
this.customRules = new Map();
|
|
this.errors = {};
|
|
this.validatedFields = new Set();
|
|
}
|
|
|
|
/**
|
|
* Create a new Validator instance
|
|
*/
|
|
static create(schema = {}) {
|
|
return new Validator(schema);
|
|
}
|
|
|
|
/**
|
|
* Register a custom validation rule
|
|
*/
|
|
registerRule(name, rule) {
|
|
if (typeof rule !== 'function') {
|
|
throw new Error('Validation rule must be a function');
|
|
}
|
|
this.customRules.set(name, rule);
|
|
}
|
|
|
|
/**
|
|
* Validate a single field
|
|
*/
|
|
async validateField(fieldName, value, schema = null) {
|
|
const fieldSchema = schema || this.schema[fieldName];
|
|
|
|
if (!fieldSchema) {
|
|
return { valid: true, errors: [] };
|
|
}
|
|
|
|
const errors = [];
|
|
|
|
// Handle array of rules
|
|
const rules = Array.isArray(fieldSchema) ? fieldSchema : [fieldSchema];
|
|
|
|
for (const ruleConfig of rules) {
|
|
const result = await this.validateRule(value, ruleConfig);
|
|
if (result !== true) {
|
|
errors.push(result);
|
|
}
|
|
}
|
|
|
|
// Store errors
|
|
if (errors.length > 0) {
|
|
this.errors[fieldName] = errors;
|
|
} else {
|
|
delete this.errors[fieldName];
|
|
}
|
|
|
|
this.validatedFields.add(fieldName);
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate a single rule
|
|
*/
|
|
async validateRule(value, ruleConfig) {
|
|
if (typeof ruleConfig === 'function') {
|
|
// Custom validator function
|
|
const result = await ruleConfig(value);
|
|
return result === true ? true : (result || 'Validation failed');
|
|
}
|
|
|
|
if (typeof ruleConfig === 'string') {
|
|
// Rule name only
|
|
return this.executeRule(ruleConfig, value, {});
|
|
}
|
|
|
|
if (typeof ruleConfig === 'object' && ruleConfig !== null) {
|
|
// Rule with options
|
|
const ruleName = ruleConfig.rule || ruleConfig.type || Object.keys(ruleConfig)[0];
|
|
const options = ruleConfig.options || ruleConfig[ruleName] || {};
|
|
|
|
// Check for async validation
|
|
if (ruleConfig.async && typeof ruleConfig.validator === 'function') {
|
|
const result = await ruleConfig.validator(value, options);
|
|
return result === true ? true : (result || options.message || 'Validation failed');
|
|
}
|
|
|
|
return this.executeRule(ruleName, value, options);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Execute a validation rule
|
|
*/
|
|
executeRule(ruleName, value, options) {
|
|
// Check custom rules first
|
|
if (this.customRules.has(ruleName)) {
|
|
const result = this.customRules.get(ruleName)(value, options);
|
|
return result === true ? true : (result || options.message || 'Validation failed');
|
|
}
|
|
|
|
// Check built-in rules
|
|
if (builtInRules[ruleName]) {
|
|
const result = builtInRules[ruleName](value, options);
|
|
return result === true ? true : (result || options.message || 'Validation failed');
|
|
}
|
|
|
|
Logger.warn(`[Validator] Unknown validation rule: ${ruleName}`);
|
|
return true; // Unknown rules pass by default
|
|
}
|
|
|
|
/**
|
|
* Validate entire schema
|
|
*/
|
|
async validate(data) {
|
|
this.errors = {};
|
|
this.validatedFields.clear();
|
|
|
|
const results = {};
|
|
let isValid = true;
|
|
|
|
for (const fieldName in this.schema) {
|
|
const value = data[fieldName];
|
|
const result = await this.validateField(fieldName, value);
|
|
results[fieldName] = result;
|
|
|
|
if (!result.valid) {
|
|
isValid = false;
|
|
}
|
|
}
|
|
|
|
return {
|
|
valid: isValid,
|
|
errors: this.errors,
|
|
results
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate specific fields
|
|
*/
|
|
async validateFields(data, fieldNames) {
|
|
this.errors = {};
|
|
|
|
const results = {};
|
|
let isValid = true;
|
|
|
|
for (const fieldName of fieldNames) {
|
|
if (!(fieldName in this.schema)) {
|
|
continue;
|
|
}
|
|
|
|
const value = data[fieldName];
|
|
const result = await this.validateField(fieldName, value);
|
|
results[fieldName] = result;
|
|
|
|
if (!result.valid) {
|
|
isValid = false;
|
|
}
|
|
}
|
|
|
|
return {
|
|
valid: isValid,
|
|
errors: this.errors,
|
|
results
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get errors for a specific field
|
|
*/
|
|
getFieldErrors(fieldName) {
|
|
return this.errors[fieldName] || [];
|
|
}
|
|
|
|
/**
|
|
* Get all errors
|
|
*/
|
|
getErrors() {
|
|
return { ...this.errors };
|
|
}
|
|
|
|
/**
|
|
* Check if field is valid
|
|
*/
|
|
isFieldValid(fieldName) {
|
|
return !this.errors[fieldName] || this.errors[fieldName].length === 0;
|
|
}
|
|
|
|
/**
|
|
* Check if all fields are valid
|
|
*/
|
|
isValid() {
|
|
return Object.keys(this.errors).length === 0;
|
|
}
|
|
|
|
/**
|
|
* Clear errors
|
|
*/
|
|
clearErrors(fieldName = null) {
|
|
if (fieldName) {
|
|
delete this.errors[fieldName];
|
|
} else {
|
|
this.errors = {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset validator
|
|
*/
|
|
reset() {
|
|
this.errors = {};
|
|
this.validatedFields.clear();
|
|
}
|
|
|
|
/**
|
|
* Create validator from HTML form
|
|
*/
|
|
static fromForm(form) {
|
|
const schema = {};
|
|
const fields = form.querySelectorAll('input, textarea, select');
|
|
|
|
fields.forEach(field => {
|
|
if (!field.name) return;
|
|
|
|
const rules = [];
|
|
|
|
// Required
|
|
if (field.hasAttribute('required')) {
|
|
rules.push('required');
|
|
}
|
|
|
|
// Type-based validation
|
|
if (field.type === 'email') {
|
|
rules.push('email');
|
|
} else if (field.type === 'url') {
|
|
rules.push('url');
|
|
} else if (field.type === 'number') {
|
|
rules.push('number');
|
|
}
|
|
|
|
// Min/Max length
|
|
if (field.hasAttribute('minlength')) {
|
|
rules.push({
|
|
rule: 'minLength',
|
|
options: { value: field.getAttribute('minlength') }
|
|
});
|
|
}
|
|
if (field.hasAttribute('maxlength')) {
|
|
rules.push({
|
|
rule: 'maxLength',
|
|
options: { value: field.getAttribute('maxlength') }
|
|
});
|
|
}
|
|
|
|
// Min/Max (for numbers)
|
|
if (field.hasAttribute('min')) {
|
|
rules.push({
|
|
rule: 'min',
|
|
options: { value: field.getAttribute('min') }
|
|
});
|
|
}
|
|
if (field.hasAttribute('max')) {
|
|
rules.push({
|
|
rule: 'max',
|
|
options: { value: field.getAttribute('max') }
|
|
});
|
|
}
|
|
|
|
// Pattern
|
|
if (field.hasAttribute('pattern')) {
|
|
rules.push({
|
|
rule: 'pattern',
|
|
options: {
|
|
value: field.getAttribute('pattern'),
|
|
message: field.getAttribute('data-error-pattern') || 'Invalid format'
|
|
}
|
|
});
|
|
}
|
|
|
|
// Custom validation
|
|
if (field.hasAttribute('data-validate')) {
|
|
const validateAttr = field.getAttribute('data-validate');
|
|
try {
|
|
const customRule = JSON.parse(validateAttr);
|
|
rules.push(customRule);
|
|
} catch {
|
|
// Treat as rule name
|
|
rules.push(validateAttr);
|
|
}
|
|
}
|
|
|
|
if (rules.length > 0) {
|
|
schema[field.name] = rules;
|
|
}
|
|
});
|
|
|
|
return new Validator(schema);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ValidationRule - Individual validation rule
|
|
*/
|
|
export class ValidationRule {
|
|
constructor(name, validator, options = {}) {
|
|
this.name = name;
|
|
this.validator = validator;
|
|
this.options = options;
|
|
}
|
|
|
|
async validate(value) {
|
|
if (typeof this.validator === 'function') {
|
|
const result = await this.validator(value, this.options);
|
|
return result === true ? true : (result || this.options.message || 'Validation failed');
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|