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.
Files changed (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +398 -0
  3. package/package.json +73 -0
  4. package/src/adapters/google-auth.js +148 -0
  5. package/src/adapters/google-drive.js +209 -0
  6. package/src/app/api/[entity]/[[...path]]/route.js +33 -0
  7. package/src/app/api/audit/dashboard/route.js +77 -0
  8. package/src/app/api/audit/logs/route.js +46 -0
  9. package/src/app/api/audit/permissions/[id]/route.js +37 -0
  10. package/src/app/api/audit/permissions/route.js +93 -0
  11. package/src/app/api/audit/permissions/stats/route.js +29 -0
  12. package/src/app/api/audit/route.js +79 -0
  13. package/src/app/api/audit/stats/route.js +18 -0
  14. package/src/app/api/auth/google/callback/route.js +94 -0
  15. package/src/app/api/auth/google/route.js +58 -0
  16. package/src/app/api/auth/login/route.js +121 -0
  17. package/src/app/api/auth/logout/route.js +52 -0
  18. package/src/app/api/auth/me/route.js +16 -0
  19. package/src/app/api/auth/mwr-bridge/route.js +77 -0
  20. package/src/app/api/auth/password-reset/route.js +65 -0
  21. package/src/app/api/cron/trigger/route.js +58 -0
  22. package/src/app/api/csrf-token/route.js +8 -0
  23. package/src/app/api/debug/config/route.js +22 -0
  24. package/src/app/api/debug/hooks/route.js +16 -0
  25. package/src/app/api/debug/plugins/route.js +20 -0
  26. package/src/app/api/debug/sqlite/route.js +21 -0
  27. package/src/app/api/debug/sync/route.js +29 -0
  28. package/src/app/api/debug/workflow/route.js +20 -0
  29. package/src/app/api/domains/[domain]/route.js +26 -0
  30. package/src/app/api/domains/route.js +21 -0
  31. package/src/app/api/email/allocate/batch/route.js +106 -0
  32. package/src/app/api/email/allocate/route.js +141 -0
  33. package/src/app/api/email/receive/route.js +158 -0
  34. package/src/app/api/email/route.js +3 -0
  35. package/src/app/api/email/send/route.js +77 -0
  36. package/src/app/api/email/unallocated/route.js +47 -0
  37. package/src/app/api/files/[id]/route.js +38 -0
  38. package/src/app/api/health/route.js +95 -0
  39. package/src/app/api/metrics/route.js +73 -0
  40. package/src/app/api/monitoring/dashboard/route.js +18 -0
  41. package/src/cli.js +243 -0
  42. package/src/config/config-loader.js +112 -0
  43. package/src/config/constants.js +127 -0
  44. package/src/config/env.js +164 -0
  45. package/src/config/spec-helpers.js +232 -0
  46. package/src/engine.server.js +212 -0
  47. package/src/index.js +368 -0
  48. package/src/lib/accessibility.js +162 -0
  49. package/src/lib/action-factory.js +34 -0
  50. package/src/lib/action-utils.js +21 -0
  51. package/src/lib/alert-manager.js +189 -0
  52. package/src/lib/api-error-wrapper.js +125 -0
  53. package/src/lib/api-helpers.js +53 -0
  54. package/src/lib/api.js +82 -0
  55. package/src/lib/audit-logger-enhanced.js +117 -0
  56. package/src/lib/audit-logger.js +193 -0
  57. package/src/lib/auth-middleware.js +102 -0
  58. package/src/lib/auth-route-helpers.js +83 -0
  59. package/src/lib/business-rules-engine.js +86 -0
  60. package/src/lib/compression.js +44 -0
  61. package/src/lib/config-field-helpers.js +91 -0
  62. package/src/lib/config-generator-engine.js +445 -0
  63. package/src/lib/config-helpers.js +120 -0
  64. package/src/lib/connection-guard.js +79 -0
  65. package/src/lib/crud-action-helpers.js +34 -0
  66. package/src/lib/crud-factory.js +83 -0
  67. package/src/lib/crud-handlers.js +244 -0
  68. package/src/lib/csrf-protection.js +63 -0
  69. package/src/lib/database-core.js +258 -0
  70. package/src/lib/database-migrations.js +96 -0
  71. package/src/lib/date-utils.js +159 -0
  72. package/src/lib/db-backup.js +97 -0
  73. package/src/lib/db-monitor.js +127 -0
  74. package/src/lib/domain-loader.js +82 -0
  75. package/src/lib/email-sender.js +100 -0
  76. package/src/lib/error-boundary.js +134 -0
  77. package/src/lib/error-handler.js +84 -0
  78. package/src/lib/error-recovery.js +190 -0
  79. package/src/lib/error-resilience.js +130 -0
  80. package/src/lib/errors.js +69 -0
  81. package/src/lib/events-engine.js +182 -0
  82. package/src/lib/field-iterator.js +50 -0
  83. package/src/lib/field-registry.js +68 -0
  84. package/src/lib/field-types.js +154 -0
  85. package/src/lib/generic-crud-handler.js +32 -0
  86. package/src/lib/health-monitor.js +134 -0
  87. package/src/lib/hook-engine.js +169 -0
  88. package/src/lib/hot-reload/cache-invalidator.js +115 -0
  89. package/src/lib/hot-reload/checkpoint.js +95 -0
  90. package/src/lib/hot-reload/debug-exposure.js +67 -0
  91. package/src/lib/hot-reload/directory-watcher.js +96 -0
  92. package/src/lib/hot-reload/index.js +50 -0
  93. package/src/lib/hot-reload/mutex.js +75 -0
  94. package/src/lib/hot-reload/promise-container.js +66 -0
  95. package/src/lib/hot-reload/route-wrapper.js +46 -0
  96. package/src/lib/hot-reload/safe-error.js +51 -0
  97. package/src/lib/hot-reload/supervisor.js +161 -0
  98. package/src/lib/hot-reload/timeout-wrapper.js +52 -0
  99. package/src/lib/http-methods-factory.js +25 -0
  100. package/src/lib/index-optimizer.js +96 -0
  101. package/src/lib/index.js +35 -0
  102. package/src/lib/list-data-transform.js +39 -0
  103. package/src/lib/log-aggregator.js +116 -0
  104. package/src/lib/logger.js +55 -0
  105. package/src/lib/metrics-collector.js +102 -0
  106. package/src/lib/minifier.js +19 -0
  107. package/src/lib/monitoring-init.js +67 -0
  108. package/src/lib/next-compat.js +80 -0
  109. package/src/lib/next-polyfills.js +135 -0
  110. package/src/lib/perf-monitor.js +91 -0
  111. package/src/lib/progress-components.js +181 -0
  112. package/src/lib/query-cache.js +126 -0
  113. package/src/lib/query-engine-write.js +221 -0
  114. package/src/lib/query-engine.js +399 -0
  115. package/src/lib/query-perf.js +117 -0
  116. package/src/lib/query-string-adapter.js +75 -0
  117. package/src/lib/realtime-server.js +67 -0
  118. package/src/lib/render-cache.js +61 -0
  119. package/src/lib/request-tracker.js +43 -0
  120. package/src/lib/resource-hints.js +29 -0
  121. package/src/lib/resource-monitor.js +117 -0
  122. package/src/lib/response-formatter.js +80 -0
  123. package/src/lib/route-helpers.js +33 -0
  124. package/src/lib/route-resolver.js +142 -0
  125. package/src/lib/safe-json.js +8 -0
  126. package/src/lib/server-bootstrap.js +71 -0
  127. package/src/lib/stage-pipeline.js +153 -0
  128. package/src/lib/state-protocol.js +171 -0
  129. package/src/lib/state-transport-client.js +169 -0
  130. package/src/lib/state-transport-reconnect.js +121 -0
  131. package/src/lib/state-transport-server.js +181 -0
  132. package/src/lib/static-server.js +97 -0
  133. package/src/lib/status-helpers.js +98 -0
  134. package/src/lib/universal-handler.js +7 -0
  135. package/src/lib/utils.js +93 -0
  136. package/src/lib/validate.js +197 -0
  137. package/src/lib/validation/business-validators.js +61 -0
  138. package/src/lib/validation/csrf.js +51 -0
  139. package/src/lib/validation/file-validators.js +34 -0
  140. package/src/lib/validation/format-validators.js +106 -0
  141. package/src/lib/validation/index.js +19 -0
  142. package/src/lib/validation/rate-limit.js +31 -0
  143. package/src/lib/validation/security-validators.js +78 -0
  144. package/src/lib/validation-middleware.js +133 -0
  145. package/src/lib/validators.js +105 -0
  146. package/src/lib/with-audit-logging.js +63 -0
  147. package/src/lib/with-error-handler.js +31 -0
  148. package/src/lib/workflow-engine.js +250 -0
  149. package/src/server/server.js +305 -0
  150. package/src/services/collaborator-role.service.js +205 -0
  151. package/src/services/email-sender.js +105 -0
  152. package/src/services/notification-engine.js +110 -0
  153. package/src/services/permission.service.js +181 -0
  154. package/src/ui/advanced-search-renderer.js +42 -0
  155. package/src/ui/advanced-widgets.js +47 -0
  156. package/src/ui/auth-pages.js +114 -0
  157. package/src/ui/auth-styles.js +53 -0
  158. package/src/ui/client.js +99 -0
  159. package/src/ui/collaboration-dialogs.js +31 -0
  160. package/src/ui/common-handlers.js +156 -0
  161. package/src/ui/component-engine.js +103 -0
  162. package/src/ui/dashboard-renderer.js +150 -0
  163. package/src/ui/dialog-engine.js +147 -0
  164. package/src/ui/dialog-factory.js +38 -0
  165. package/src/ui/engagement-cards.js +76 -0
  166. package/src/ui/engagement-dialogs.js +100 -0
  167. package/src/ui/engagement-grid-renderer.js +109 -0
  168. package/src/ui/entity-renderer.js +176 -0
  169. package/src/ui/event-delegation.js +108 -0
  170. package/src/ui/fetch-json.js +24 -0
  171. package/src/ui/file-dialogs.js +81 -0
  172. package/src/ui/flexup-report-renderer.js +173 -0
  173. package/src/ui/format-helpers.js +102 -0
  174. package/src/ui/global-tags.js +144 -0
  175. package/src/ui/highlight-threading-renderer.js +176 -0
  176. package/src/ui/idle-logout.js +156 -0
  177. package/src/ui/job-management-renderer.js +61 -0
  178. package/src/ui/layout.js +219 -0
  179. package/src/ui/letter-dialogs.js +37 -0
  180. package/src/ui/ml-console-renderer.js +108 -0
  181. package/src/ui/monitoring-dashboard-client.js +136 -0
  182. package/src/ui/monitoring-dashboard.js +134 -0
  183. package/src/ui/notifications-renderer.js +51 -0
  184. package/src/ui/page-handler-admin.js +121 -0
  185. package/src/ui/page-handler-helpers.js +111 -0
  186. package/src/ui/page-handler-reviews.js +165 -0
  187. package/src/ui/page-handler-rfi.js +42 -0
  188. package/src/ui/page-handler.js +210 -0
  189. package/src/ui/password-reset-page.js +146 -0
  190. package/src/ui/perf-helpers.js +96 -0
  191. package/src/ui/perf-renderer.js +71 -0
  192. package/src/ui/permissions-ui.js +163 -0
  193. package/src/ui/picker-dialogs.js +100 -0
  194. package/src/ui/render-helpers.js +124 -0
  195. package/src/ui/renderer.js +35 -0
  196. package/src/ui/review-comparison-renderer.js +58 -0
  197. package/src/ui/review-detail-panels.js +71 -0
  198. package/src/ui/review-detail-renderer.js +170 -0
  199. package/src/ui/review-detail-script.js +95 -0
  200. package/src/ui/review-mwr-renderer.js +113 -0
  201. package/src/ui/review-renderer.js +202 -0
  202. package/src/ui/review-widgets.js +88 -0
  203. package/src/ui/review-zone-nav.js +12 -0
  204. package/src/ui/rfi-detail-renderer.js +191 -0
  205. package/src/ui/rfi-renderer.js +194 -0
  206. package/src/ui/rfi-report-renderer.js +56 -0
  207. package/src/ui/rippleui.css +1 -0
  208. package/src/ui/settings-renderer-advanced.js +195 -0
  209. package/src/ui/settings-renderer-advanced2.js +158 -0
  210. package/src/ui/settings-renderer-teams.js +112 -0
  211. package/src/ui/settings-renderer.js +166 -0
  212. package/src/ui/spacing-system.js +155 -0
  213. package/src/ui/standalone-login.js +109 -0
  214. package/src/ui/styles.css +2530 -0
  215. package/src/ui/styles2.css +1602 -0
  216. package/src/ui/test-page.js +23 -0
  217. package/src/ui/validation-rules.js +73 -0
  218. package/src/ui/validation-ui.js +147 -0
  219. package/src/ui/virtual-scroll.js +107 -0
  220. package/src/ui/webjsx.js +61 -0
  221. 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
+ };