fix: Gitea Traefik routing and connection pool optimization
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
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
This commit is contained in:
467
resources/js/modules/validation/Validator.js
Normal file
467
resources/js/modules/validation/Validator.js
Normal file
@@ -0,0 +1,467 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user