- Implemented a comprehensive health check utility to monitor system dependencies including NocoDB, SMTP, Represent API, disk space, and memory usage. - Created a logger utility using Winston for structured logging with daily rotation and various log levels. - Developed a metrics utility using Prometheus client to track application performance metrics such as email sends, HTTP requests, and user activity. - Added a backup script for automated backups of NocoDB data, uploaded files, and environment configurations with optional S3 support. - Introduced a toggle script to switch between development (MailHog) and production (ProtonMail) SMTP configurations.
224 lines
6.6 KiB
JavaScript
224 lines
6.6 KiB
JavaScript
// Validate Canadian postal code format
|
|
// Full Canadian postal code validation with proper FSA/LDU rules
|
|
function validatePostalCode(postalCode) {
|
|
// Remove whitespace and convert to uppercase
|
|
const cleaned = postalCode.replace(/\s/g, '').toUpperCase();
|
|
|
|
// Must be exactly 6 characters
|
|
if (cleaned.length !== 6) return false;
|
|
|
|
// Pattern: A1A 1A1 where A is letter and 1 is digit
|
|
const pattern = /^[A-Z]\d[A-Z]\d[A-Z]\d$/;
|
|
if (!pattern.test(cleaned)) return false;
|
|
|
|
// First character cannot be D, F, I, O, Q, U, W, or Z
|
|
const invalidFirstChars = ['D', 'F', 'I', 'O', 'Q', 'U', 'W', 'Z'];
|
|
if (invalidFirstChars.includes(cleaned[0])) return false;
|
|
|
|
// Second position (LDU) cannot be 0
|
|
if (cleaned[1] === '0') return false;
|
|
|
|
// Third character cannot be D, F, I, O, Q, or U
|
|
const invalidThirdChars = ['D', 'F', 'I', 'O', 'Q', 'U'];
|
|
if (invalidThirdChars.includes(cleaned[2])) return false;
|
|
|
|
// Fifth character cannot be D, F, I, O, Q, or U
|
|
if (invalidThirdChars.includes(cleaned[4])) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
// Validate Alberta postal code (starts with T)
|
|
function validateAlbertaPostalCode(postalCode) {
|
|
const formatted = postalCode.replace(/\s/g, '').toUpperCase();
|
|
return formatted.startsWith('T') && validatePostalCode(postalCode);
|
|
}
|
|
|
|
// Validate email format with stricter rules
|
|
function validateEmail(email) {
|
|
// RFC 5322 simplified email validation
|
|
const regex = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
|
|
if (!regex.test(email)) return false;
|
|
|
|
// Additional checks
|
|
const parts = email.split('@');
|
|
if (parts.length !== 2) return false;
|
|
|
|
const [localPart, domain] = parts;
|
|
|
|
// Local part max 64 characters
|
|
if (localPart.length > 64) return false;
|
|
|
|
// Domain must have at least one dot and valid TLD
|
|
if (!domain.includes('.')) return false;
|
|
|
|
// Check for consecutive dots
|
|
if (email.includes('..')) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
// Format postal code to standard format (A1A 1A1)
|
|
function formatPostalCode(postalCode) {
|
|
const cleaned = postalCode.replace(/\s/g, '').toUpperCase();
|
|
if (cleaned.length === 6) {
|
|
return `${cleaned.slice(0, 3)} ${cleaned.slice(3)}`;
|
|
}
|
|
return cleaned;
|
|
}
|
|
|
|
// Sanitize string input to prevent XSS and injection attacks
|
|
function sanitizeString(str) {
|
|
if (typeof str !== 'string') return str;
|
|
|
|
return str
|
|
.replace(/[<>]/g, '') // Remove angle brackets
|
|
.replace(/javascript:/gi, '') // Remove javascript: protocol
|
|
.replace(/on\w+\s*=/gi, '') // Remove event handlers
|
|
.replace(/eval\s*\(/gi, '') // Remove eval calls
|
|
.trim()
|
|
.substring(0, 1000); // Limit length
|
|
}
|
|
|
|
// Sanitize HTML content for email templates
|
|
function sanitizeHtmlContent(html) {
|
|
if (typeof html !== 'string') return html;
|
|
|
|
// Remove dangerous tags and attributes
|
|
let sanitized = html
|
|
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
|
|
.replace(/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object>/gi, '')
|
|
.replace(/<embed[^>]*>/gi, '')
|
|
.replace(/javascript:/gi, '')
|
|
.replace(/on\w+\s*=/gi, '');
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
// Validate SQL/NoSQL injection attempts in where clauses
|
|
function validateWhereClause(whereClause) {
|
|
if (typeof whereClause !== 'string') return false;
|
|
|
|
// Check for SQL injection patterns
|
|
const suspiciousPatterns = [
|
|
/;\s*drop\s+/i,
|
|
/;\s*delete\s+/i,
|
|
/;\s*update\s+/i,
|
|
/;\s*insert\s+/i,
|
|
/union\s+select/i,
|
|
/exec\s*\(/i,
|
|
/execute\s*\(/i,
|
|
/--/,
|
|
/\/\*/,
|
|
/xp_/i,
|
|
/sp_/i
|
|
];
|
|
|
|
return !suspiciousPatterns.some(pattern => pattern.test(whereClause));
|
|
}
|
|
|
|
// Validate required fields in request body
|
|
function validateRequiredFields(body, requiredFields) {
|
|
const errors = [];
|
|
|
|
requiredFields.forEach(field => {
|
|
if (!body[field] || (typeof body[field] === 'string' && body[field].trim() === '')) {
|
|
errors.push(`${field} is required`);
|
|
}
|
|
});
|
|
|
|
return errors;
|
|
}
|
|
|
|
// Check if string contains potentially harmful content
|
|
function containsSuspiciousContent(str) {
|
|
const suspiciousPatterns = [
|
|
/<script/i,
|
|
/javascript:/i,
|
|
/on\w+\s*=/i,
|
|
/<iframe/i,
|
|
/<object/i,
|
|
/<embed/i
|
|
];
|
|
|
|
return suspiciousPatterns.some(pattern => pattern.test(str));
|
|
}
|
|
|
|
// Generate URL-friendly slug from text
|
|
function generateSlug(text) {
|
|
return text
|
|
.toLowerCase()
|
|
.trim()
|
|
.replace(/[^\w\s-]/g, '') // Remove special characters
|
|
.replace(/[\s_-]+/g, '-') // Replace spaces and underscores with hyphens
|
|
.replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
|
|
}
|
|
|
|
// Validate slug format
|
|
function validateSlug(slug) {
|
|
const slugPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
return slugPattern.test(slug) && slug.length >= 3 && slug.length <= 100;
|
|
}
|
|
|
|
// Validate response submission
|
|
function validateResponse(data) {
|
|
const { representative_name, representative_level, response_type, response_text } = data;
|
|
|
|
// Check required fields
|
|
if (!representative_name || representative_name.trim() === '') {
|
|
return { valid: false, error: 'Representative name is required' };
|
|
}
|
|
|
|
if (!representative_level || representative_level.trim() === '') {
|
|
return { valid: false, error: 'Representative level is required' };
|
|
}
|
|
|
|
// Validate representative level
|
|
const validLevels = ['Federal', 'Provincial', 'Municipal', 'School Board'];
|
|
if (!validLevels.includes(representative_level)) {
|
|
return { valid: false, error: 'Invalid representative level' };
|
|
}
|
|
|
|
if (!response_type || response_type.trim() === '') {
|
|
return { valid: false, error: 'Response type is required' };
|
|
}
|
|
|
|
// Validate response type
|
|
const validTypes = ['Email', 'Letter', 'Phone Call', 'Meeting', 'Social Media', 'Other'];
|
|
if (!validTypes.includes(response_type)) {
|
|
return { valid: false, error: 'Invalid response type' };
|
|
}
|
|
|
|
if (!response_text || response_text.trim() === '') {
|
|
return { valid: false, error: 'Response text is required' };
|
|
}
|
|
|
|
// Check for suspicious content
|
|
if (containsSuspiciousContent(response_text)) {
|
|
return { valid: false, error: 'Response contains invalid content' };
|
|
}
|
|
|
|
// Validate email if provided
|
|
if (data.submitted_by_email && !validateEmail(data.submitted_by_email)) {
|
|
return { valid: false, error: 'Invalid email address' };
|
|
}
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
module.exports = {
|
|
validatePostalCode,
|
|
validateAlbertaPostalCode,
|
|
validateEmail,
|
|
formatPostalCode,
|
|
sanitizeString,
|
|
sanitizeHtmlContent,
|
|
validateWhereClause,
|
|
validateRequiredFields,
|
|
containsSuspiciousContent,
|
|
generateSlug,
|
|
validateSlug,
|
|
validateResponse
|
|
}; |