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,20 @@
1
+ import { NextResponse } from '@/lib/next-polyfills';
2
+
3
+ export async function GET() {
4
+ if (process.env.NODE_ENV === 'production') {
5
+ return NextResponse.json({ error: 'Not available in production' }, { status: 403 });
6
+ }
7
+ const { getConfigEngineSync } = await import('@/lib/config-generator-engine.js');
8
+ const engine = getConfigEngineSync();
9
+ const wf = engine.getConfig().workflows || {};
10
+ const summary = Object.fromEntries(
11
+ Object.entries(wf).map(([k, v]) => [k, {
12
+ state_field: v.state_field || 'status',
13
+ stages: v.stages ? Object.keys(v.stages) : [],
14
+ transitions: Array.isArray(v.transitions) ? v.transitions.length : null,
15
+ }])
16
+ );
17
+ return NextResponse.json({ workflows: summary });
18
+ }
19
+
20
+ export const config = { runtime: 'nodejs' };
@@ -0,0 +1,26 @@
1
+ import { getDomainLoader } from '@/lib/domain-loader';
2
+ import { getConfigEngine } from '@/lib/config-generator-engine';
3
+ import { ok } from '@/lib/response-formatter';
4
+ import { withErrorHandler } from '@/lib/with-error-handler';
5
+ import { AppError, NotFoundError } from '@/lib/error-handler';
6
+ import { HTTP } from '@/config/constants';
7
+
8
+ export const GET = withErrorHandler(async (request, context) => {
9
+ const params = await context.params;
10
+ const { domain } = params;
11
+
12
+ if (!domain) {
13
+ throw new AppError('Domain parameter required', 'BAD_REQUEST', HTTP.BAD_REQUEST);
14
+ }
15
+
16
+ await getConfigEngine();
17
+ const domainLoader = getDomainLoader();
18
+ const validDomains = domainLoader.getValidDomains();
19
+
20
+ if (!validDomains.includes(domain.toLowerCase())) {
21
+ throw NotFoundError('domain', domain);
22
+ }
23
+
24
+ const domainInfo = domainLoader.getDomainInfo(domain);
25
+ return ok(domainInfo);
26
+ }, 'Domains:Get');
@@ -0,0 +1,21 @@
1
+ import { getDomainLoader } from '@/lib/domain-loader';
2
+ import { ok } from '@/lib/response-formatter';
3
+ import { withErrorHandler } from '@/lib/with-error-handler';
4
+ import { getConfigEngine } from '@/lib/config-generator-engine';
5
+
6
+ export const GET = withErrorHandler(async (request) => {
7
+ await getConfigEngine();
8
+ const domainLoader = getDomainLoader();
9
+ const validDomains = domainLoader.getValidDomains();
10
+
11
+ const domains = validDomains.map(domainName => {
12
+ try {
13
+ return domainLoader.getDomainInfo(domainName);
14
+ } catch (error) {
15
+ console.error(`[Domains API] Error loading domain ${domainName}:`, error.message);
16
+ return null;
17
+ }
18
+ }).filter(Boolean);
19
+
20
+ return ok({ domains });
21
+ }, 'Domains:List');
@@ -0,0 +1,106 @@
1
+ import { NextResponse } from '@/lib/next-polyfills';
2
+ import { getDatabase, genId, now } from '@/lib/database-core';
3
+ import { autoAllocateEmail } from '@/lib/email-parser';
4
+
5
+ export async function POST(request) {
6
+ try {
7
+ const { requireUser, setCurrentRequest } = await import('@/engine.server');
8
+ setCurrentRequest(request);
9
+ const user = await requireUser();
10
+ if (user.role !== 'admin' && user.role !== 'partner') {
11
+ return NextResponse.json({ error: 'Permission denied' }, { status: 403 });
12
+ }
13
+
14
+ const body = await request.json();
15
+ const { min_confidence = 70, batch_size = 50 } = body;
16
+
17
+ const db = getDatabase();
18
+
19
+ const unallocatedEmails = db.prepare(`
20
+ SELECT * FROM email
21
+ WHERE allocated = 0 AND status = 'pending'
22
+ ORDER BY received_at DESC
23
+ LIMIT ?
24
+ `).all(batch_size);
25
+
26
+ const results = {
27
+ allocated: [],
28
+ skipped: [],
29
+ failed: [],
30
+ total: unallocatedEmails.length,
31
+ };
32
+
33
+ for (const email of unallocatedEmails) {
34
+ try {
35
+ const result = await autoAllocateEmail(email);
36
+
37
+ if (result.success && result.confidence >= min_confidence) {
38
+ const logId = genId();
39
+ const timestamp = now();
40
+
41
+ db.prepare(`
42
+ INSERT INTO activity_log (
43
+ id, entity_type, entity_id, action, message, details, created_at
44
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
45
+ `).run(
46
+ logId,
47
+ 'email',
48
+ email.id,
49
+ 'batch_allocated',
50
+ `Email batch-allocated to ${result.engagement_id ? 'engagement' : 'RFI'}`,
51
+ JSON.stringify({
52
+ engagement_id: result.engagement_id || null,
53
+ rfi_id: result.rfi_id || null,
54
+ confidence: result.confidence,
55
+ method: 'batch_automatic',
56
+ }),
57
+ timestamp
58
+ );
59
+
60
+ results.allocated.push({
61
+ email_id: email.id,
62
+ subject: email.subject,
63
+ engagement_id: result.engagement_id || null,
64
+ rfi_id: result.rfi_id || null,
65
+ confidence: result.confidence,
66
+ });
67
+ } else if (result.success && result.confidence < min_confidence) {
68
+ results.skipped.push({
69
+ email_id: email.id,
70
+ subject: email.subject,
71
+ confidence: result.confidence,
72
+ reason: `confidence ${result.confidence}% < ${min_confidence}%`,
73
+ });
74
+ } else {
75
+ results.failed.push({
76
+ email_id: email.id,
77
+ subject: email.subject,
78
+ reason: result.reason,
79
+ });
80
+ }
81
+ } catch (error) {
82
+ results.failed.push({
83
+ email_id: email.id,
84
+ subject: email.subject,
85
+ reason: error.message,
86
+ });
87
+
88
+ db.prepare(`
89
+ UPDATE email
90
+ SET processing_error = ?,
91
+ updated_at = ?
92
+ WHERE id = ?
93
+ `).run(error.message, now(), email.id);
94
+ }
95
+ }
96
+
97
+ return NextResponse.json(results);
98
+
99
+ } catch (error) {
100
+ console.error('[EMAIL_BATCH_ALLOCATE] Error:', error);
101
+ return NextResponse.json(
102
+ { error: 'Internal server error', message: error.message },
103
+ { status: 500 }
104
+ );
105
+ }
106
+ }
@@ -0,0 +1,141 @@
1
+ import { NextResponse } from '@/lib/next-polyfills';
2
+ import { getDatabase, genId, now } from '@/lib/database-core';
3
+ import {
4
+ allocateEmailToEntity,
5
+ autoAllocateEmail,
6
+ validateAllocation,
7
+ findEntityByAlternateId,
8
+ } from '@/lib/email-parser';
9
+
10
+ export async function POST(request) {
11
+ try {
12
+ const { requireUser, setCurrentRequest } = await import('@/engine.server');
13
+ setCurrentRequest(request);
14
+ await requireUser();
15
+
16
+ const body = await request.json();
17
+ const { email_id, engagement_id, rfi_id, auto = false } = body;
18
+
19
+ if (!email_id) {
20
+ return NextResponse.json(
21
+ { error: 'email_id is required' },
22
+ { status: 400 }
23
+ );
24
+ }
25
+
26
+ const db = getDatabase();
27
+ const email = db.prepare('SELECT * FROM email WHERE id = ?').get(email_id);
28
+
29
+ if (!email) {
30
+ return NextResponse.json(
31
+ { error: 'Email not found' },
32
+ { status: 404 }
33
+ );
34
+ }
35
+
36
+ if (email.allocated) {
37
+ return NextResponse.json(
38
+ { error: 'Email already allocated' },
39
+ { status: 400 }
40
+ );
41
+ }
42
+
43
+ let result;
44
+
45
+ if (auto) {
46
+ result = await autoAllocateEmail(email);
47
+
48
+ if (!result.success) {
49
+ return NextResponse.json(
50
+ {
51
+ success: false,
52
+ reason: result.reason,
53
+ confidence: result.confidence,
54
+ message: `Auto-allocation failed: ${result.reason}`,
55
+ },
56
+ { status: 400 }
57
+ );
58
+ }
59
+ } else {
60
+ if (!engagement_id && !rfi_id) {
61
+ return NextResponse.json(
62
+ { error: 'Either engagement_id or rfi_id must be provided for manual allocation' },
63
+ { status: 400 }
64
+ );
65
+ }
66
+
67
+ let resolvedEngagementId = engagement_id;
68
+ let resolvedRfiId = rfi_id;
69
+
70
+ if (engagement_id) {
71
+ resolvedEngagementId = findEntityByAlternateId('engagement', engagement_id);
72
+ if (!resolvedEngagementId) {
73
+ return NextResponse.json(
74
+ { error: 'Engagement not found' },
75
+ { status: 404 }
76
+ );
77
+ }
78
+ }
79
+
80
+ if (rfi_id) {
81
+ resolvedRfiId = findEntityByAlternateId('rfi', rfi_id);
82
+ if (!resolvedRfiId) {
83
+ return NextResponse.json(
84
+ { error: 'RFI not found' },
85
+ { status: 404 }
86
+ );
87
+ }
88
+ }
89
+
90
+ const updatedEmail = allocateEmailToEntity(
91
+ email_id,
92
+ resolvedEngagementId,
93
+ resolvedRfiId
94
+ );
95
+
96
+ result = {
97
+ success: true,
98
+ email: updatedEmail,
99
+ engagement_id: resolvedEngagementId,
100
+ rfi_id: resolvedRfiId,
101
+ };
102
+ }
103
+
104
+ const logId = genId();
105
+ const timestamp = now();
106
+
107
+ db.prepare(`
108
+ INSERT INTO activity_log (
109
+ id, entity_type, entity_id, action, message, details, created_at
110
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
111
+ `).run(
112
+ logId,
113
+ 'email',
114
+ email_id,
115
+ 'allocated',
116
+ `Email allocated to ${result.engagement_id ? 'engagement' : 'RFI'}`,
117
+ JSON.stringify({
118
+ engagement_id: result.engagement_id || null,
119
+ rfi_id: result.rfi_id || null,
120
+ method: auto ? 'automatic' : 'manual',
121
+ confidence: result.confidence || 100,
122
+ }),
123
+ timestamp
124
+ );
125
+
126
+ return NextResponse.json({
127
+ success: true,
128
+ email: result.email,
129
+ engagement_id: result.engagement_id || null,
130
+ rfi_id: result.rfi_id || null,
131
+ confidence: result.confidence || 100,
132
+ });
133
+
134
+ } catch (error) {
135
+ console.error('[EMAIL_ALLOCATE] Error:', error);
136
+ return NextResponse.json(
137
+ { error: 'Internal server error', message: error.message },
138
+ { status: 500 }
139
+ );
140
+ }
141
+ }
@@ -0,0 +1,158 @@
1
+ import { NextResponse } from '@/lib/next-polyfills';
2
+ import { getDatabase, genId, now } from '@/lib/database-core';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+
6
+ const TEMP_EMAIL_ATTACHMENTS_DIR = path.resolve(process.cwd(), 'data', 'temp_email_attachments');
7
+
8
+ if (!fs.existsSync(TEMP_EMAIL_ATTACHMENTS_DIR)) {
9
+ fs.mkdirSync(TEMP_EMAIL_ATTACHMENTS_DIR, { recursive: true });
10
+ }
11
+
12
+ export async function POST(request) {
13
+ try {
14
+ const authHeader = request.headers.get('authorization');
15
+ const token = authHeader?.replace('Bearer ', '');
16
+ if (!token || token !== process.env.EMAIL_WEBHOOK_SECRET) {
17
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
18
+ }
19
+
20
+ const body = await request.json();
21
+
22
+ const {
23
+ from,
24
+ sender_email,
25
+ subject,
26
+ body: emailBody,
27
+ html_body,
28
+ text_body,
29
+ attachments = [],
30
+ message_id,
31
+ in_reply_to,
32
+ references,
33
+ received_date,
34
+ } = body;
35
+
36
+ if (!sender_email || !subject) {
37
+ return NextResponse.json(
38
+ { error: 'Missing required fields: sender_email and subject are required' },
39
+ { status: 400 }
40
+ );
41
+ }
42
+
43
+ const attachmentData = [];
44
+
45
+ for (const attachment of attachments) {
46
+ const { filename, content, content_type, size } = attachment;
47
+
48
+ if (filename && content) {
49
+ const attachmentId = genId();
50
+ const ext = path.extname(filename);
51
+ const safeFilename = `${attachmentId}${ext}`;
52
+ const filePath = path.join(TEMP_EMAIL_ATTACHMENTS_DIR, safeFilename);
53
+
54
+ const buffer = Buffer.isBuffer(content)
55
+ ? content
56
+ : Buffer.from(content, 'base64');
57
+
58
+ fs.writeFileSync(filePath, buffer);
59
+
60
+ attachmentData.push({
61
+ id: attachmentId,
62
+ filename,
63
+ path: filePath,
64
+ content_type,
65
+ size: size || buffer.length,
66
+ });
67
+ }
68
+ }
69
+
70
+ const emailRecord = {
71
+ sender_email,
72
+ sender_name: from || sender_email,
73
+ subject,
74
+ body: text_body || emailBody || '',
75
+ html_body: html_body || '',
76
+ message_id: message_id || genId(),
77
+ in_reply_to: in_reply_to || null,
78
+ references: references || null,
79
+ received_at: received_date ? Math.floor(new Date(received_date).getTime() / 1000) : now(),
80
+ allocated: false,
81
+ engagement_id: null,
82
+ rfi_id: null,
83
+ attachments: JSON.stringify(attachmentData),
84
+ status: 'pending',
85
+ processed: false,
86
+ processing_error: null,
87
+ };
88
+
89
+ const db = getDatabase();
90
+ const stmt = db.prepare(`
91
+ INSERT INTO email (
92
+ id, sender_email, sender_name, subject, body, html_body,
93
+ message_id, in_reply_to, references, received_at, allocated,
94
+ engagement_id, rfi_id, attachments, status, processed, processing_error,
95
+ created_at, updated_at
96
+ ) VALUES (
97
+ ?, ?, ?, ?, ?, ?,
98
+ ?, ?, ?, ?, ?,
99
+ ?, ?, ?, ?, ?, ?,
100
+ ?, ?
101
+ )
102
+ `);
103
+
104
+ const emailId = genId();
105
+ const timestamp = now();
106
+
107
+ stmt.run(
108
+ emailId,
109
+ emailRecord.sender_email,
110
+ emailRecord.sender_name,
111
+ emailRecord.subject,
112
+ emailRecord.body,
113
+ emailRecord.html_body,
114
+ emailRecord.message_id,
115
+ emailRecord.in_reply_to,
116
+ emailRecord.references,
117
+ emailRecord.received_at,
118
+ emailRecord.allocated ? 1 : 0,
119
+ emailRecord.engagement_id,
120
+ emailRecord.rfi_id,
121
+ emailRecord.attachments,
122
+ emailRecord.status,
123
+ emailRecord.processed ? 1 : 0,
124
+ emailRecord.processing_error,
125
+ timestamp,
126
+ timestamp
127
+ );
128
+
129
+ return NextResponse.json({
130
+ success: true,
131
+ email_id: emailId,
132
+ attachments_count: attachmentData.length,
133
+ message: 'Email received and stored successfully',
134
+ });
135
+
136
+ } catch (error) {
137
+ console.error('[EMAIL_RECEIVE] Error processing webhook:', error);
138
+
139
+ return NextResponse.json(
140
+ {
141
+ error: 'Internal server error',
142
+ message: error.message,
143
+ },
144
+ { status: 500 }
145
+ );
146
+ }
147
+ }
148
+
149
+ export async function GET(request) {
150
+ return NextResponse.json({
151
+ endpoint: 'Email Receive Webhook',
152
+ method: 'POST',
153
+ description: 'Accepts incoming emails from Gmail webhook',
154
+ required_fields: ['sender_email', 'subject'],
155
+ optional_fields: ['from', 'body', 'html_body', 'text_body', 'attachments', 'message_id', 'in_reply_to', 'references', 'received_date'],
156
+ attachment_format: 'Array of { filename, content (base64), content_type, size }',
157
+ });
158
+ }
@@ -0,0 +1,3 @@
1
+ import { createHttpMethods } from '@/lib/http-methods-factory';
2
+
3
+ export const { GET, POST, PUT, PATCH, DELETE } = createHttpMethods('email');
@@ -0,0 +1,77 @@
1
+ import { NextResponse } from '@/lib/next-polyfills';
2
+ import { getDatabase, now } from '@/lib/database-core';
3
+ import { getConfigEngine } from '@/lib/config-generator-engine';
4
+ import { EMAIL_STATUS } from '@/config/constants';
5
+ import { sendSingleEmail, checkFailureRate } from '@/lib/email-sender';
6
+
7
+ let emailConfig = null;
8
+
9
+ async function getEmailConfig() {
10
+ if (!emailConfig) {
11
+ const engine = await getConfigEngine();
12
+ emailConfig = engine.getConfig()?.thresholds?.email || {};
13
+ }
14
+ return emailConfig;
15
+ }
16
+
17
+ export async function POST(request) {
18
+ const token = request.headers.get('authorization')?.replace('Bearer ', '');
19
+ if (!token || token !== process.env.CRON_SECRET)
20
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
21
+
22
+ const db = getDatabase();
23
+ try {
24
+ const emailCfg = await getEmailConfig();
25
+ const MAX_RETRIES = emailCfg.send_max_retries || 3;
26
+ const BATCH_SIZE = emailCfg.send_batch_size || 10;
27
+ const RATE_LIMIT_DELAY = emailCfg.rate_limit_delay_ms || 6000;
28
+ const MAX_DELAY_MS = emailCfg.retry_max_delay_ms || 30000;
29
+
30
+ const pendingEmails = db.prepare(`SELECT * FROM email WHERE status=? AND (retry_count IS NULL OR retry_count < ?) ORDER BY created_at ASC LIMIT ?`)
31
+ .all(EMAIL_STATUS.PENDING, MAX_RETRIES, BATCH_SIZE);
32
+
33
+ if (!pendingEmails.length)
34
+ return NextResponse.json({ success: true, message: 'No pending emails', processed: 0 });
35
+
36
+ const results = [];
37
+ let successCount = 0, failureCount = 0;
38
+
39
+ for (let i = 0; i < pendingEmails.length; i++) {
40
+ const email = pendingEmails[i];
41
+ db.prepare(`UPDATE email SET status=?, updated_at=? WHERE id=?`).run(EMAIL_STATUS.PROCESSING, now(), email.id);
42
+ const result = await sendSingleEmail(db, email, email.retry_count || 1, MAX_RETRIES, MAX_DELAY_MS);
43
+ results.push(result);
44
+ result.success ? successCount++ : failureCount++;
45
+ if (i < pendingEmails.length - 1)
46
+ await new Promise(r => setTimeout(r, RATE_LIMIT_DELAY));
47
+ }
48
+
49
+ checkFailureRate(db);
50
+ return NextResponse.json({ success: true, processed: pendingEmails.length, results: { success: successCount, failed: failureCount }, details: results });
51
+ } catch (error) {
52
+ console.error('[EMAIL] Queue processing error:', error);
53
+ return NextResponse.json({ error: 'Email queue processing failed', details: error.message }, { status: 500 });
54
+ }
55
+ }
56
+
57
+ export async function GET(request) {
58
+ const token = request.headers.get('authorization')?.replace('Bearer ', '');
59
+ if (!token || token !== process.env.CRON_SECRET)
60
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
61
+
62
+ const db = getDatabase();
63
+ try {
64
+ const stats = db.prepare(`SELECT status, COUNT(*) as count FROM email GROUP BY status`).all();
65
+ const failureStats = db.prepare(`SELECT COUNT(*) as total, SUM(CASE WHEN status=? THEN 1 ELSE 0 END) as failed FROM email WHERE created_at >= ?`)
66
+ .get(EMAIL_STATUS.FAILED, now() - 86400);
67
+ const recentFailures = db.prepare(`SELECT id, recipient_email, subject, processing_error, retry_count, created_at FROM email WHERE status=? ORDER BY created_at DESC LIMIT 10`)
68
+ .all(EMAIL_STATUS.FAILED);
69
+ return NextResponse.json({
70
+ stats: stats.reduce((acc, r) => ({ ...acc, [r.status]: r.count }), {}),
71
+ failureRate: failureStats.total > 0 ? failureStats.failed / failureStats.total : 0,
72
+ recentFailures,
73
+ });
74
+ } catch (error) {
75
+ return NextResponse.json({ error: 'Failed to get email stats', details: error.message }, { status: 500 });
76
+ }
77
+ }
@@ -0,0 +1,47 @@
1
+ import { NextResponse } from '@/lib/next-polyfills';
2
+ import { getDatabase } from '@/lib/database-core';
3
+
4
+ export async function GET(request) {
5
+ try {
6
+ const { requireUser, setCurrentRequest } = await import('@/engine.server');
7
+ setCurrentRequest(request);
8
+ await requireUser();
9
+
10
+ const { searchParams } = new URL(request.url);
11
+ const limit = parseInt(searchParams.get('limit') || '50', 10);
12
+ const offset = parseInt(searchParams.get('offset') || '0', 10);
13
+
14
+ const db = getDatabase();
15
+
16
+ const emails = db.prepare(`
17
+ SELECT *
18
+ FROM email
19
+ WHERE allocated = 0
20
+ ORDER BY received_at DESC
21
+ LIMIT ? OFFSET ?
22
+ `).all(limit, offset);
23
+
24
+ const total = db.prepare('SELECT COUNT(*) as count FROM email WHERE allocated = 0').get();
25
+
26
+ const emailsWithParsedAttachments = emails.map(email => ({
27
+ ...email,
28
+ attachments: email.attachments ? JSON.parse(email.attachments) : [],
29
+ allocated: Boolean(email.allocated),
30
+ processed: Boolean(email.processed),
31
+ }));
32
+
33
+ return NextResponse.json({
34
+ emails: emailsWithParsedAttachments,
35
+ total: total.count,
36
+ limit,
37
+ offset,
38
+ });
39
+
40
+ } catch (error) {
41
+ console.error('[EMAIL_UNALLOCATED] Error:', error);
42
+ return NextResponse.json(
43
+ { error: 'Internal server error', message: error.message },
44
+ { status: 500 }
45
+ );
46
+ }
47
+ }
@@ -0,0 +1,38 @@
1
+ import { NextResponse } from '@/lib/next-polyfills';
2
+ import { requireUser } from '@/engine.server';
3
+ import { permissionService } from '@/services/permission.service';
4
+ import { get } from '@/engine';
5
+ import { getSpec } from '@/config/spec-helpers';
6
+ import { fileService } from '@/services/file.service';
7
+ import { HTTP } from '@/config/constants';
8
+ import { notFound } from '@/lib/response-formatter';
9
+
10
+ export async function GET(request, { params }) {
11
+ try {
12
+ const user = await requireUser();
13
+ const { id } = params;
14
+
15
+ const spec = getSpec('file');
16
+ if (!permissionService.checkAccess(user, spec, 'view')) {
17
+ return NextResponse.json({ error: 'Permission denied' }, { status: HTTP.FORBIDDEN });
18
+ }
19
+
20
+ const fileRecord = get('file', id);
21
+ if (!fileRecord) {
22
+ return notFound('File not found');
23
+ }
24
+
25
+ const content = await fileService.download(fileRecord.drive_file_id);
26
+
27
+ const safeName = (fileRecord.file_name || 'download').replace(/[^\w.\-]/g, '_');
28
+ return new NextResponse(content, {
29
+ headers: {
30
+ 'Content-Type': fileRecord.mime_type || 'application/octet-stream',
31
+ 'Content-Disposition': `attachment; filename="${safeName}"`,
32
+ },
33
+ });
34
+ } catch (error) {
35
+ console.error('File download error:', error);
36
+ return NextResponse.json({ error: error.message }, { status: HTTP.INTERNAL_ERROR });
37
+ }
38
+ }