thatcher 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +398 -0
- package/package.json +73 -0
- package/src/adapters/google-auth.js +148 -0
- package/src/adapters/google-drive.js +209 -0
- package/src/app/api/[entity]/[[...path]]/route.js +33 -0
- package/src/app/api/audit/dashboard/route.js +77 -0
- package/src/app/api/audit/logs/route.js +46 -0
- package/src/app/api/audit/permissions/[id]/route.js +37 -0
- package/src/app/api/audit/permissions/route.js +93 -0
- package/src/app/api/audit/permissions/stats/route.js +29 -0
- package/src/app/api/audit/route.js +79 -0
- package/src/app/api/audit/stats/route.js +18 -0
- package/src/app/api/auth/google/callback/route.js +94 -0
- package/src/app/api/auth/google/route.js +58 -0
- package/src/app/api/auth/login/route.js +121 -0
- package/src/app/api/auth/logout/route.js +52 -0
- package/src/app/api/auth/me/route.js +16 -0
- package/src/app/api/auth/mwr-bridge/route.js +77 -0
- package/src/app/api/auth/password-reset/route.js +65 -0
- package/src/app/api/cron/trigger/route.js +58 -0
- package/src/app/api/csrf-token/route.js +8 -0
- package/src/app/api/debug/config/route.js +22 -0
- package/src/app/api/debug/hooks/route.js +16 -0
- package/src/app/api/debug/plugins/route.js +20 -0
- package/src/app/api/debug/sqlite/route.js +21 -0
- package/src/app/api/debug/sync/route.js +29 -0
- package/src/app/api/debug/workflow/route.js +20 -0
- package/src/app/api/domains/[domain]/route.js +26 -0
- package/src/app/api/domains/route.js +21 -0
- package/src/app/api/email/allocate/batch/route.js +106 -0
- package/src/app/api/email/allocate/route.js +141 -0
- package/src/app/api/email/receive/route.js +158 -0
- package/src/app/api/email/route.js +3 -0
- package/src/app/api/email/send/route.js +77 -0
- package/src/app/api/email/unallocated/route.js +47 -0
- package/src/app/api/files/[id]/route.js +38 -0
- package/src/app/api/health/route.js +95 -0
- package/src/app/api/metrics/route.js +73 -0
- package/src/app/api/monitoring/dashboard/route.js +18 -0
- package/src/cli.js +243 -0
- package/src/config/config-loader.js +112 -0
- package/src/config/constants.js +127 -0
- package/src/config/env.js +164 -0
- package/src/config/spec-helpers.js +232 -0
- package/src/engine.server.js +212 -0
- package/src/index.js +368 -0
- package/src/lib/accessibility.js +162 -0
- package/src/lib/action-factory.js +34 -0
- package/src/lib/action-utils.js +21 -0
- package/src/lib/alert-manager.js +189 -0
- package/src/lib/api-error-wrapper.js +125 -0
- package/src/lib/api-helpers.js +53 -0
- package/src/lib/api.js +82 -0
- package/src/lib/audit-logger-enhanced.js +117 -0
- package/src/lib/audit-logger.js +193 -0
- package/src/lib/auth-middleware.js +102 -0
- package/src/lib/auth-route-helpers.js +83 -0
- package/src/lib/business-rules-engine.js +86 -0
- package/src/lib/compression.js +44 -0
- package/src/lib/config-field-helpers.js +91 -0
- package/src/lib/config-generator-engine.js +445 -0
- package/src/lib/config-helpers.js +120 -0
- package/src/lib/connection-guard.js +79 -0
- package/src/lib/crud-action-helpers.js +34 -0
- package/src/lib/crud-factory.js +83 -0
- package/src/lib/crud-handlers.js +244 -0
- package/src/lib/csrf-protection.js +63 -0
- package/src/lib/database-core.js +258 -0
- package/src/lib/database-migrations.js +96 -0
- package/src/lib/date-utils.js +159 -0
- package/src/lib/db-backup.js +97 -0
- package/src/lib/db-monitor.js +127 -0
- package/src/lib/domain-loader.js +82 -0
- package/src/lib/email-sender.js +100 -0
- package/src/lib/error-boundary.js +134 -0
- package/src/lib/error-handler.js +84 -0
- package/src/lib/error-recovery.js +190 -0
- package/src/lib/error-resilience.js +130 -0
- package/src/lib/errors.js +69 -0
- package/src/lib/events-engine.js +182 -0
- package/src/lib/field-iterator.js +50 -0
- package/src/lib/field-registry.js +68 -0
- package/src/lib/field-types.js +154 -0
- package/src/lib/generic-crud-handler.js +32 -0
- package/src/lib/health-monitor.js +134 -0
- package/src/lib/hook-engine.js +169 -0
- package/src/lib/hot-reload/cache-invalidator.js +115 -0
- package/src/lib/hot-reload/checkpoint.js +95 -0
- package/src/lib/hot-reload/debug-exposure.js +67 -0
- package/src/lib/hot-reload/directory-watcher.js +96 -0
- package/src/lib/hot-reload/index.js +50 -0
- package/src/lib/hot-reload/mutex.js +75 -0
- package/src/lib/hot-reload/promise-container.js +66 -0
- package/src/lib/hot-reload/route-wrapper.js +46 -0
- package/src/lib/hot-reload/safe-error.js +51 -0
- package/src/lib/hot-reload/supervisor.js +161 -0
- package/src/lib/hot-reload/timeout-wrapper.js +52 -0
- package/src/lib/http-methods-factory.js +25 -0
- package/src/lib/index-optimizer.js +96 -0
- package/src/lib/index.js +35 -0
- package/src/lib/list-data-transform.js +39 -0
- package/src/lib/log-aggregator.js +116 -0
- package/src/lib/logger.js +55 -0
- package/src/lib/metrics-collector.js +102 -0
- package/src/lib/minifier.js +19 -0
- package/src/lib/monitoring-init.js +67 -0
- package/src/lib/next-compat.js +80 -0
- package/src/lib/next-polyfills.js +135 -0
- package/src/lib/perf-monitor.js +91 -0
- package/src/lib/progress-components.js +181 -0
- package/src/lib/query-cache.js +126 -0
- package/src/lib/query-engine-write.js +221 -0
- package/src/lib/query-engine.js +399 -0
- package/src/lib/query-perf.js +117 -0
- package/src/lib/query-string-adapter.js +75 -0
- package/src/lib/realtime-server.js +67 -0
- package/src/lib/render-cache.js +61 -0
- package/src/lib/request-tracker.js +43 -0
- package/src/lib/resource-hints.js +29 -0
- package/src/lib/resource-monitor.js +117 -0
- package/src/lib/response-formatter.js +80 -0
- package/src/lib/route-helpers.js +33 -0
- package/src/lib/route-resolver.js +142 -0
- package/src/lib/safe-json.js +8 -0
- package/src/lib/server-bootstrap.js +71 -0
- package/src/lib/stage-pipeline.js +153 -0
- package/src/lib/state-protocol.js +171 -0
- package/src/lib/state-transport-client.js +169 -0
- package/src/lib/state-transport-reconnect.js +121 -0
- package/src/lib/state-transport-server.js +181 -0
- package/src/lib/static-server.js +97 -0
- package/src/lib/status-helpers.js +98 -0
- package/src/lib/universal-handler.js +7 -0
- package/src/lib/utils.js +93 -0
- package/src/lib/validate.js +197 -0
- package/src/lib/validation/business-validators.js +61 -0
- package/src/lib/validation/csrf.js +51 -0
- package/src/lib/validation/file-validators.js +34 -0
- package/src/lib/validation/format-validators.js +106 -0
- package/src/lib/validation/index.js +19 -0
- package/src/lib/validation/rate-limit.js +31 -0
- package/src/lib/validation/security-validators.js +78 -0
- package/src/lib/validation-middleware.js +133 -0
- package/src/lib/validators.js +105 -0
- package/src/lib/with-audit-logging.js +63 -0
- package/src/lib/with-error-handler.js +31 -0
- package/src/lib/workflow-engine.js +250 -0
- package/src/server/server.js +305 -0
- package/src/services/collaborator-role.service.js +205 -0
- package/src/services/email-sender.js +105 -0
- package/src/services/notification-engine.js +110 -0
- package/src/services/permission.service.js +181 -0
- package/src/ui/advanced-search-renderer.js +42 -0
- package/src/ui/advanced-widgets.js +47 -0
- package/src/ui/auth-pages.js +114 -0
- package/src/ui/auth-styles.js +53 -0
- package/src/ui/client.js +99 -0
- package/src/ui/collaboration-dialogs.js +31 -0
- package/src/ui/common-handlers.js +156 -0
- package/src/ui/component-engine.js +103 -0
- package/src/ui/dashboard-renderer.js +150 -0
- package/src/ui/dialog-engine.js +147 -0
- package/src/ui/dialog-factory.js +38 -0
- package/src/ui/engagement-cards.js +76 -0
- package/src/ui/engagement-dialogs.js +100 -0
- package/src/ui/engagement-grid-renderer.js +109 -0
- package/src/ui/entity-renderer.js +176 -0
- package/src/ui/event-delegation.js +108 -0
- package/src/ui/fetch-json.js +24 -0
- package/src/ui/file-dialogs.js +81 -0
- package/src/ui/flexup-report-renderer.js +173 -0
- package/src/ui/format-helpers.js +102 -0
- package/src/ui/global-tags.js +144 -0
- package/src/ui/highlight-threading-renderer.js +176 -0
- package/src/ui/idle-logout.js +156 -0
- package/src/ui/job-management-renderer.js +61 -0
- package/src/ui/layout.js +219 -0
- package/src/ui/letter-dialogs.js +37 -0
- package/src/ui/ml-console-renderer.js +108 -0
- package/src/ui/monitoring-dashboard-client.js +136 -0
- package/src/ui/monitoring-dashboard.js +134 -0
- package/src/ui/notifications-renderer.js +51 -0
- package/src/ui/page-handler-admin.js +121 -0
- package/src/ui/page-handler-helpers.js +111 -0
- package/src/ui/page-handler-reviews.js +165 -0
- package/src/ui/page-handler-rfi.js +42 -0
- package/src/ui/page-handler.js +210 -0
- package/src/ui/password-reset-page.js +146 -0
- package/src/ui/perf-helpers.js +96 -0
- package/src/ui/perf-renderer.js +71 -0
- package/src/ui/permissions-ui.js +163 -0
- package/src/ui/picker-dialogs.js +100 -0
- package/src/ui/render-helpers.js +124 -0
- package/src/ui/renderer.js +35 -0
- package/src/ui/review-comparison-renderer.js +58 -0
- package/src/ui/review-detail-panels.js +71 -0
- package/src/ui/review-detail-renderer.js +170 -0
- package/src/ui/review-detail-script.js +95 -0
- package/src/ui/review-mwr-renderer.js +113 -0
- package/src/ui/review-renderer.js +202 -0
- package/src/ui/review-widgets.js +88 -0
- package/src/ui/review-zone-nav.js +12 -0
- package/src/ui/rfi-detail-renderer.js +191 -0
- package/src/ui/rfi-renderer.js +194 -0
- package/src/ui/rfi-report-renderer.js +56 -0
- package/src/ui/rippleui.css +1 -0
- package/src/ui/settings-renderer-advanced.js +195 -0
- package/src/ui/settings-renderer-advanced2.js +158 -0
- package/src/ui/settings-renderer-teams.js +112 -0
- package/src/ui/settings-renderer.js +166 -0
- package/src/ui/spacing-system.js +155 -0
- package/src/ui/standalone-login.js +109 -0
- package/src/ui/styles.css +2530 -0
- package/src/ui/styles2.css +1602 -0
- package/src/ui/test-page.js +23 -0
- package/src/ui/validation-rules.js +73 -0
- package/src/ui/validation-ui.js +147 -0
- package/src/ui/virtual-scroll.js +107 -0
- package/src/ui/webjsx.js +61 -0
- package/src/ui/widgets.js +152 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export function validateURL(url) {
|
|
2
|
+
if (!url || typeof url !== 'string') return { valid: false, reason: 'URL required' };
|
|
3
|
+
|
|
4
|
+
try {
|
|
5
|
+
const parsed = new URL(url);
|
|
6
|
+
const ALLOWED_PROTOCOLS = ['http:', 'https:'];
|
|
7
|
+
if (!ALLOWED_PROTOCOLS.includes(parsed.protocol)) {
|
|
8
|
+
return { valid: false, reason: `Protocol ${parsed.protocol} not allowed` };
|
|
9
|
+
}
|
|
10
|
+
if (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1') {
|
|
11
|
+
return { valid: false, reason: 'localhost URLs not allowed' };
|
|
12
|
+
}
|
|
13
|
+
return { valid: true, url: url };
|
|
14
|
+
} catch (err) {
|
|
15
|
+
return { valid: false, reason: 'Invalid URL format' };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function validatePhoneNumber(phone) {
|
|
20
|
+
if (!phone || typeof phone !== 'string') return { valid: false, reason: 'Phone number required' };
|
|
21
|
+
const cleaned = phone.replace(/[^0-9+]/g, '');
|
|
22
|
+
if (cleaned.length < 10 || cleaned.length > 15) {
|
|
23
|
+
return { valid: false, reason: 'Invalid phone number length' };
|
|
24
|
+
}
|
|
25
|
+
return { valid: true, phone: cleaned };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function validatePostalCode(postal, country = 'US') {
|
|
29
|
+
if (!postal || typeof postal !== 'string') return { valid: false, reason: 'Postal code required' };
|
|
30
|
+
const patterns = {
|
|
31
|
+
US: /^\d{5}(-\d{4})?$/,
|
|
32
|
+
CA: /^[A-Z]\d[A-Z]\s?\d[A-Z]\d$/i,
|
|
33
|
+
UK: /^[A-Z]{1,2}\d{1,2}[A-Z]?\s?\d[A-Z]{2}$/i
|
|
34
|
+
};
|
|
35
|
+
const pattern = patterns[country] || patterns.US;
|
|
36
|
+
if (!pattern.test(postal)) {
|
|
37
|
+
return { valid: false, reason: `Invalid ${country} postal code format` };
|
|
38
|
+
}
|
|
39
|
+
return { valid: true, postal };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function validateCreditCard(number) {
|
|
43
|
+
if (!number || typeof number !== 'string') return { valid: false, reason: 'Card number required' };
|
|
44
|
+
const cleaned = number.replace(/\D/g, '');
|
|
45
|
+
if (cleaned.length < 13 || cleaned.length > 19) {
|
|
46
|
+
return { valid: false, reason: 'Invalid card number length' };
|
|
47
|
+
}
|
|
48
|
+
let sum = 0;
|
|
49
|
+
let isEven = false;
|
|
50
|
+
for (let i = cleaned.length - 1; i >= 0; i--) {
|
|
51
|
+
let digit = parseInt(cleaned[i]);
|
|
52
|
+
if (isEven) {
|
|
53
|
+
digit *= 2;
|
|
54
|
+
if (digit > 9) digit -= 9;
|
|
55
|
+
}
|
|
56
|
+
sum += digit;
|
|
57
|
+
isEven = !isEven;
|
|
58
|
+
}
|
|
59
|
+
if (sum % 10 !== 0) {
|
|
60
|
+
return { valid: false, reason: 'Invalid card number (failed Luhn check)' };
|
|
61
|
+
}
|
|
62
|
+
return { valid: true, card: cleaned };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function validatePassword(password, options = {}) {
|
|
66
|
+
const { minLength = 8, requireUppercase = true, requireLowercase = true, requireNumber = true, requireSpecial = true } = options;
|
|
67
|
+
const errors = [];
|
|
68
|
+
|
|
69
|
+
if (!password || typeof password !== 'string') {
|
|
70
|
+
return { valid: false, errors: ['Password required'] };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (password.length < minLength) {
|
|
74
|
+
errors.push(`Password must be at least ${minLength} characters`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (requireUppercase && !/[A-Z]/.test(password)) {
|
|
78
|
+
errors.push('Password must contain uppercase letter');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (requireLowercase && !/[a-z]/.test(password)) {
|
|
82
|
+
errors.push('Password must contain lowercase letter');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (requireNumber && !/\d/.test(password)) {
|
|
86
|
+
errors.push('Password must contain number');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (requireSpecial && !/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
|
|
90
|
+
errors.push('Password must contain special character');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { valid: errors.length === 0, errors };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function validateIPAddress(ip) {
|
|
97
|
+
if (!ip || typeof ip !== 'string') return { valid: false, reason: 'IP address required' };
|
|
98
|
+
const ipv4Pattern = /^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/;
|
|
99
|
+
const ipv6Pattern = /^([0-9a-f]{1,4}:){7}[0-9a-f]{1,4}$/i;
|
|
100
|
+
|
|
101
|
+
if (!ipv4Pattern.test(ip) && !ipv6Pattern.test(ip)) {
|
|
102
|
+
return { valid: false, reason: 'Invalid IP address format' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { valid: true, ip };
|
|
106
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export * from '@/lib/validation/security-validators';
|
|
2
|
+
export * from '@/lib/validation/format-validators';
|
|
3
|
+
export * from '@/lib/validation/file-validators';
|
|
4
|
+
export * from '@/lib/validation/business-validators';
|
|
5
|
+
export * from '@/lib/validation/rate-limit';
|
|
6
|
+
export * from '@/lib/validation/csrf';
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
isValidEmail,
|
|
10
|
+
validateField,
|
|
11
|
+
validateEntity,
|
|
12
|
+
validateUpdate,
|
|
13
|
+
hasErrors,
|
|
14
|
+
validateStatusTransition,
|
|
15
|
+
validateDateRange,
|
|
16
|
+
validateDeadline,
|
|
17
|
+
sanitizeData,
|
|
18
|
+
sanitizeHtml
|
|
19
|
+
} from '@/lib/validate';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function rateLimitCheck(identifier, maxRequests = 100, windowMs = 60000) {
|
|
2
|
+
if (!global.rateLimitStore) global.rateLimitStore = {};
|
|
3
|
+
|
|
4
|
+
const now = Date.now();
|
|
5
|
+
const key = `rate:${identifier}`;
|
|
6
|
+
const record = global.rateLimitStore[key] || { count: 0, resetAt: now + windowMs };
|
|
7
|
+
|
|
8
|
+
if (now > record.resetAt) {
|
|
9
|
+
record.count = 0;
|
|
10
|
+
record.resetAt = now + windowMs;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
record.count++;
|
|
14
|
+
global.rateLimitStore[key] = record;
|
|
15
|
+
|
|
16
|
+
if (record.count > maxRequests) {
|
|
17
|
+
return { allowed: false, reason: 'Rate limit exceeded', resetAt: record.resetAt };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { allowed: true, remaining: maxRequests - record.count, resetAt: record.resetAt };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
setInterval(() => {
|
|
24
|
+
if (!global.rateLimitStore) return;
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
for (const key of Object.keys(global.rateLimitStore)) {
|
|
27
|
+
if (global.rateLimitStore[key].resetAt < now) {
|
|
28
|
+
delete global.rateLimitStore[key];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}, 60000);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { sanitizeHtml } from '@/lib/validate';
|
|
2
|
+
|
|
3
|
+
const XSS_PATTERNS = [
|
|
4
|
+
/<script[^>]*>.*?<\/script>/gi,
|
|
5
|
+
/javascript:/gi,
|
|
6
|
+
/on\w+\s*=/gi,
|
|
7
|
+
/<iframe[^>]*>/gi,
|
|
8
|
+
/<object[^>]*>/gi,
|
|
9
|
+
/<embed[^>]*>/gi,
|
|
10
|
+
/eval\(/gi,
|
|
11
|
+
/expression\(/gi
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const SQL_INJECTION_PATTERNS = [
|
|
15
|
+
/('|(\-\-)|(;)|(\|\|)|(\/\*)|(\*\/)|xp_)/gi,
|
|
16
|
+
/(union|select|insert|update|delete|drop|create|alter|exec|execute)\s/gi,
|
|
17
|
+
/0x[0-9a-f]+/gi,
|
|
18
|
+
/char\(/gi,
|
|
19
|
+
/concat\(/gi
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const PATH_TRAVERSAL_PATTERNS = [
|
|
23
|
+
/\.\.[\\\/]/g,
|
|
24
|
+
/\.\.%/g,
|
|
25
|
+
/%2e%2e/gi,
|
|
26
|
+
/\.\.\./g
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export function detectXSS(input) {
|
|
30
|
+
if (typeof input !== 'string') return { safe: true };
|
|
31
|
+
for (const pattern of XSS_PATTERNS) {
|
|
32
|
+
if (pattern.test(input)) {
|
|
33
|
+
return { safe: false, reason: 'Potential XSS attack detected', pattern: pattern.toString() };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { safe: true };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function detectSQLInjection(input) {
|
|
40
|
+
if (typeof input !== 'string') return { safe: true };
|
|
41
|
+
for (const pattern of SQL_INJECTION_PATTERNS) {
|
|
42
|
+
if (pattern.test(input)) {
|
|
43
|
+
return { safe: false, reason: 'Potential SQL injection detected', pattern: pattern.toString() };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { safe: true };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function detectPathTraversal(input) {
|
|
50
|
+
if (typeof input !== 'string') return { safe: true };
|
|
51
|
+
for (const pattern of PATH_TRAVERSAL_PATTERNS) {
|
|
52
|
+
if (pattern.test(input)) {
|
|
53
|
+
return { safe: false, reason: 'Path traversal attempt detected', pattern: pattern.toString() };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { safe: true };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function sanitizeDeep(data) {
|
|
60
|
+
if (typeof data === 'string') {
|
|
61
|
+
const xssCheck = detectXSS(data);
|
|
62
|
+
if (!xssCheck.safe) return '';
|
|
63
|
+
const sqlCheck = detectSQLInjection(data);
|
|
64
|
+
if (!sqlCheck.safe) return '';
|
|
65
|
+
return sanitizeHtml(data);
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(data)) {
|
|
68
|
+
return data.map(sanitizeDeep);
|
|
69
|
+
}
|
|
70
|
+
if (data && typeof data === 'object') {
|
|
71
|
+
const sanitized = {};
|
|
72
|
+
for (const [key, value] of Object.entries(data)) {
|
|
73
|
+
sanitized[key] = sanitizeDeep(value);
|
|
74
|
+
}
|
|
75
|
+
return sanitized;
|
|
76
|
+
}
|
|
77
|
+
return data;
|
|
78
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { NextResponse } from '@/lib/next-polyfills';
|
|
2
|
+
import {
|
|
3
|
+
detectXSS,
|
|
4
|
+
detectSQLInjection,
|
|
5
|
+
sanitizeDeep,
|
|
6
|
+
validateBusinessRules,
|
|
7
|
+
rateLimitCheck,
|
|
8
|
+
validateCSRFToken
|
|
9
|
+
} from '@/lib/validation';
|
|
10
|
+
import { validateEntity, validateUpdate, hasErrors } from '@/lib/validate';
|
|
11
|
+
|
|
12
|
+
export async function validateRequest(request, options = {}) {
|
|
13
|
+
const { requireCSRF = true, rateLimit = { max: 100, window: 60000 }, entityName = null } = options;
|
|
14
|
+
|
|
15
|
+
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
|
|
16
|
+
const sessionId = request.headers.get('cookie')?.match(/session=([^;]+)/)?.[1] || ip;
|
|
17
|
+
|
|
18
|
+
if (rateLimit) {
|
|
19
|
+
const rateLimitResult = rateLimitCheck(ip, rateLimit.max, rateLimit.window);
|
|
20
|
+
if (!rateLimitResult.allowed) {
|
|
21
|
+
return NextResponse.json(
|
|
22
|
+
{ success: false, error: 'Rate limit exceeded', resetAt: rateLimitResult.resetAt },
|
|
23
|
+
{ status: 429, headers: { 'Retry-After': Math.ceil((rateLimitResult.resetAt - Date.now()) / 1000).toString() } }
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (requireCSRF && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(request.method)) {
|
|
29
|
+
const csrfToken = request.headers.get('x-csrf-token');
|
|
30
|
+
const csrfCheck = validateCSRFToken(csrfToken, sessionId);
|
|
31
|
+
if (!csrfCheck.valid) {
|
|
32
|
+
return NextResponse.json(
|
|
33
|
+
{ success: false, error: csrfCheck.reason },
|
|
34
|
+
{ status: 403 }
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function validateRequestBody(request, entityName, isUpdate = false, recordId = null) {
|
|
43
|
+
let body;
|
|
44
|
+
try {
|
|
45
|
+
body = await request.json();
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return {
|
|
48
|
+
valid: false,
|
|
49
|
+
response: NextResponse.json(
|
|
50
|
+
{ success: false, error: 'Invalid JSON in request body' },
|
|
51
|
+
{ status: 400 }
|
|
52
|
+
)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const [key, value] of Object.entries(body)) {
|
|
57
|
+
if (typeof value === 'string') {
|
|
58
|
+
const xssCheck = detectXSS(value);
|
|
59
|
+
if (!xssCheck.safe) {
|
|
60
|
+
return {
|
|
61
|
+
valid: false,
|
|
62
|
+
response: NextResponse.json(
|
|
63
|
+
{ success: false, error: `XSS attempt detected in field '${key}'`, reason: xssCheck.reason },
|
|
64
|
+
{ status: 400 }
|
|
65
|
+
)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const sqlCheck = detectSQLInjection(value);
|
|
70
|
+
if (!sqlCheck.safe) {
|
|
71
|
+
return {
|
|
72
|
+
valid: false,
|
|
73
|
+
response: NextResponse.json(
|
|
74
|
+
{ success: false, error: `SQL injection attempt detected in field '${key}'`, reason: sqlCheck.reason },
|
|
75
|
+
{ status: 400 }
|
|
76
|
+
)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const sanitizedBody = sanitizeDeep(body);
|
|
83
|
+
|
|
84
|
+
const validationErrors = isUpdate
|
|
85
|
+
? await validateUpdate(entityName, recordId, sanitizedBody)
|
|
86
|
+
: await validateEntity(entityName, sanitizedBody);
|
|
87
|
+
|
|
88
|
+
if (hasErrors(validationErrors)) {
|
|
89
|
+
return {
|
|
90
|
+
valid: false,
|
|
91
|
+
response: NextResponse.json(
|
|
92
|
+
{ success: false, error: 'Validation failed', errors: validationErrors },
|
|
93
|
+
{ status: 400 }
|
|
94
|
+
)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const businessErrors = validateBusinessRules(entityName, sanitizedBody, isUpdate ? { id: recordId } : null);
|
|
99
|
+
if (Object.keys(businessErrors).length > 0) {
|
|
100
|
+
return {
|
|
101
|
+
valid: false,
|
|
102
|
+
response: NextResponse.json(
|
|
103
|
+
{ success: false, error: 'Business rule validation failed', errors: businessErrors },
|
|
104
|
+
{ status: 400 }
|
|
105
|
+
)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { valid: true, body: sanitizedBody };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function withValidation(handler, options = {}) {
|
|
113
|
+
return async (request, context) => {
|
|
114
|
+
const validationError = await validateRequest(request, options);
|
|
115
|
+
if (validationError) return validationError;
|
|
116
|
+
|
|
117
|
+
return handler(request, context);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function withBodyValidation(handler, entityName, isUpdate = false) {
|
|
122
|
+
return async (request, context) => {
|
|
123
|
+
const validationError = await validateRequest(request);
|
|
124
|
+
if (validationError) return validationError;
|
|
125
|
+
|
|
126
|
+
const recordId = isUpdate ? context?.params?.id : null;
|
|
127
|
+
const bodyValidation = await validateRequestBody(request, entityName, isUpdate, recordId);
|
|
128
|
+
|
|
129
|
+
if (!bodyValidation.valid) return bodyValidation.response;
|
|
130
|
+
|
|
131
|
+
return handler(request, { ...context, validatedBody: bodyValidation.body });
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Utilities - Reusable validation functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validate email address
|
|
7
|
+
* @param {string} email
|
|
8
|
+
* @returns {{valid: boolean, email?: string, domain?: string, reason?: string}}
|
|
9
|
+
*/
|
|
10
|
+
export function isValidEmail(email) {
|
|
11
|
+
if (!email || typeof email !== 'string') {
|
|
12
|
+
return { valid: false, reason: 'Email is required' };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const trimmed = email.trim().toLowerCase();
|
|
16
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
17
|
+
|
|
18
|
+
if (!EMAIL_REGEX.test(trimmed)) {
|
|
19
|
+
return { valid: false, reason: 'Invalid email format' };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const domain = trimmed.split('@')[1];
|
|
23
|
+
if (!domain || domain.length < 3 || !domain.includes('.')) {
|
|
24
|
+
return { valid: false, reason: 'Invalid email domain' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Disposable email domains
|
|
28
|
+
const DISPOSABLE_DOMAINS = new Set([
|
|
29
|
+
'mailinator.com', 'guerrillamail.com', 'tempmail.com',
|
|
30
|
+
'throwaway.email', 'yopmail.com', '10minutemail.com',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
if (DISPOSABLE_DOMAINS.has(domain)) {
|
|
34
|
+
return { valid: false, reason: 'Disposable email addresses are not allowed' };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { valid: true, email: trimmed, domain };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Validate URL
|
|
42
|
+
* @param {string} url
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
export function isValidUrl(url) {
|
|
46
|
+
try {
|
|
47
|
+
new URL(url);
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate date is within range
|
|
56
|
+
* @param {number} timestamp - Unix timestamp (seconds)
|
|
57
|
+
* @param {number} minYearsAgo
|
|
58
|
+
* @param {number} maxYearsAhead
|
|
59
|
+
* @returns {boolean}
|
|
60
|
+
*/
|
|
61
|
+
export function isWithinYears(timestamp, minYearsAgo = 10, maxYearsAhead = 5) {
|
|
62
|
+
const now = Date.now() / 1000;
|
|
63
|
+
const secondsInYear = 365.25 * 24 * 60 * 60;
|
|
64
|
+
return (
|
|
65
|
+
timestamp > now - (minYearsAgo * secondsInYear) &&
|
|
66
|
+
timestamp < now + (maxYearsAhead * secondsInYear)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if date1 is before date2
|
|
72
|
+
* @param {number} date1 - Unix timestamp
|
|
73
|
+
* @param {number} date2 - Unix timestamp
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
export function isBeforeDate(date1, date2) {
|
|
77
|
+
return date1 < date2;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Slugify string
|
|
82
|
+
* @param {string} str
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
export function slugify(str) {
|
|
86
|
+
return str
|
|
87
|
+
.toLowerCase()
|
|
88
|
+
.trim()
|
|
89
|
+
.replace(/[^\w\s-]/g, '')
|
|
90
|
+
.replace(/[\s_-]+/g, '-')
|
|
91
|
+
.replace(/^-+|-+$/g, '');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Validate required fields object
|
|
96
|
+
* @param {object} data
|
|
97
|
+
* @param {Array<string>} requiredFields
|
|
98
|
+
* @returns {Array<string>} Missing field names
|
|
99
|
+
*/
|
|
100
|
+
export function getMissingFields(data, requiredFields) {
|
|
101
|
+
return requiredFields.filter(field => {
|
|
102
|
+
const val = data[field];
|
|
103
|
+
return val === undefined || val === null || val === '';
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { logCreate, logUpdate, logDelete, logAuthzFailure, logPerformance, logError } from '@/lib/audit-logger-enhanced';
|
|
2
|
+
|
|
3
|
+
const extractEntityInfo = (request, params = {}) => {
|
|
4
|
+
const url = new URL(request.url);
|
|
5
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
6
|
+
const entityType = parts[parts.length - 2] || 'unknown';
|
|
7
|
+
const entityId = params.id || params.engagementId || params.reviewId || params.highlightId || params.permissionId || parts[parts.length - 1];
|
|
8
|
+
return { entityType, entityId };
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const withAuditLogging = (handler, options = {}) => {
|
|
12
|
+
return async (request, context = {}) => {
|
|
13
|
+
const startTime = Date.now();
|
|
14
|
+
const method = request.method;
|
|
15
|
+
const { entityType: defaultEntityType, entityId: defaultEntityId } = extractEntityInfo(request, context.params || {});
|
|
16
|
+
const entityType = options.entityType || defaultEntityType;
|
|
17
|
+
let response;
|
|
18
|
+
let userId = null;
|
|
19
|
+
let entityId = defaultEntityId;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
response = await handler(request, context);
|
|
23
|
+
const endTime = Date.now();
|
|
24
|
+
const durationMs = endTime - startTime;
|
|
25
|
+
|
|
26
|
+
if (response.userId) userId = response.userId;
|
|
27
|
+
if (response.user && response.user.id) userId = response.user.id;
|
|
28
|
+
|
|
29
|
+
const body = response instanceof Response ? await response.clone().json().catch(() => ({})) : response;
|
|
30
|
+
if (body.user && body.user.id) userId = body.user.id;
|
|
31
|
+
|
|
32
|
+
if (method === 'POST' && response.status >= 200 && response.status < 300) {
|
|
33
|
+
const createdId = body.id || body.data?.id || entityId;
|
|
34
|
+
logCreate(entityType, createdId, userId, body.data || body);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (method === 'PATCH' && response.status >= 200 && response.status < 300) {
|
|
38
|
+
logUpdate(entityType, entityId, userId, null, body.data || body);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (method === 'DELETE' && response.status >= 200 && response.status < 300) {
|
|
42
|
+
logDelete(entityType, entityId, userId, null);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (durationMs > 500) {
|
|
46
|
+
logPerformance(`${method} ${entityType}`, entityType, durationMs, userId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (response.status === 403) {
|
|
50
|
+
logAuthzFailure(userId, entityType, entityId, 'unknown', { method, path: request.url });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return response;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
logError(error, { entityType, entityId, userId, method, path: request.url });
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const auditCreate = (entityType, entityId, userId, afterState) => logCreate(entityType, entityId, userId, afterState);
|
|
62
|
+
export const auditUpdate = (entityType, entityId, userId, beforeState, afterState) => logUpdate(entityType, entityId, userId, beforeState, afterState);
|
|
63
|
+
export const auditDelete = (entityType, entityId, userId, beforeState) => logDelete(entityType, entityId, userId, beforeState);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { AppError, normalizeError, createErrorLogger } from '@/lib/error-handler';
|
|
2
|
+
import { apiError } from '@/lib/response-formatter';
|
|
3
|
+
import { HTTP } from '@/config/constants';
|
|
4
|
+
|
|
5
|
+
export const withErrorHandler = (handler, operation = 'Operation') => {
|
|
6
|
+
const logger = createErrorLogger(operation);
|
|
7
|
+
|
|
8
|
+
return async (...args) => {
|
|
9
|
+
try {
|
|
10
|
+
return await handler(...args);
|
|
11
|
+
} catch (e) {
|
|
12
|
+
const error = normalizeError(e);
|
|
13
|
+
logger.error(error.code, error.context);
|
|
14
|
+
return apiError(error);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const withAsyncErrorHandler = (handler, context = '') => {
|
|
20
|
+
const logger = createErrorLogger(context);
|
|
21
|
+
|
|
22
|
+
return async (...args) => {
|
|
23
|
+
try {
|
|
24
|
+
return await handler(...args);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
const error = e instanceof AppError ? e : new AppError(e.message, 'INTERNAL_ERROR', HTTP.INTERNAL_ERROR, { originalMessage: e.message });
|
|
27
|
+
logger.error(error.code || 'ERROR', error.context || {});
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
};
|