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,209 @@
1
+ /**
2
+ * Google Drive Adapter - File storage and manipulation
3
+ * Upload, download, copy, export to PDF
4
+ */
5
+
6
+ import { google } from 'googleapis';
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { getJWTClient, getOAuth2Client } from './google-auth.js';
10
+ import { buildConfig } from '../config/env.js';
11
+
12
+ const DRIVE_SCOPES = ['https://www.googleapis.com/auth/drive'];
13
+
14
+ /**
15
+ * Get authenticated Drive API client
16
+ * @param {object} [user] - User with oauth_token, or null for service account
17
+ * @returns {object}
18
+ */
19
+ export function getDriveClient(user = null) {
20
+ let client;
21
+
22
+ if (user?.oauth_token) {
23
+ // User-delegated access
24
+ client = getOAuth2Client();
25
+ client.setCredentials({ access_token: user.oauth_token });
26
+ } else {
27
+ // Service account (app-wide)
28
+ client = getJWTClient();
29
+ }
30
+
31
+ if (!client) throw new Error('Google Drive not configured');
32
+
33
+ return google.drive({ version: 'v3', auth: client });
34
+ }
35
+
36
+ /**
37
+ * Upload a file to Google Drive
38
+ * @param {string} filePath - Local file path
39
+ * @param {string} name - File name in Drive
40
+ * @param {object} [options] - { folderId, mimeType, parents }
41
+ * @param {object} [user] - Optional user context
42
+ * @returns {object} File metadata
43
+ */
44
+ export async function uploadFile(filePath, name, options = {}, user = null) {
45
+ const drive = getDriveClient(user);
46
+ const absPath = path.resolve(filePath);
47
+ const fileSize = fs.statSync(absPath).size;
48
+
49
+ const res = await drive.files.create({
50
+ requestBody: {
51
+ name,
52
+ parents: options.parents || [options.folderId || getDefaultFolder()],
53
+ mimeType: options.mimeType || 'application/octet-stream',
54
+ },
55
+ media: {
56
+ mimeType: options.mimeType || 'application/octet-stream',
57
+ body: fs.createReadStream(absPath),
58
+ },
59
+ });
60
+
61
+ return res.data;
62
+ }
63
+
64
+ /**
65
+ * Download file from Drive
66
+ * @param {string} fileId
67
+ * @param {string} [destPath] - If provided, write to file; else return buffer
68
+ * @param {object} [user] - Optional user context
69
+ * @returns {Buffer|string}
70
+ */
71
+ export async function downloadFile(fileId, destPath = null, user = null) {
72
+ const drive = getDriveClient(user);
73
+ const res = await drive.files.get(
74
+ { fileId, alt: 'media' },
75
+ { responseType: destPath ? 'stream' : 'arraybuffer' }
76
+ );
77
+
78
+ if (destPath) {
79
+ const writeStream = fs.createWriteStream(destPath);
80
+ res.data
81
+ .on('end', () => {})
82
+ .on('error', err => { throw err; })
83
+ .pipe(writeStream);
84
+ return destPath;
85
+ }
86
+
87
+ return Buffer.from(res.data);
88
+ }
89
+
90
+ /**
91
+ * Create a copy of a file in Drive
92
+ * @param {string} fileId
93
+ * @param {string} [newName]
94
+ * @param {string} [folderId]
95
+ * @param {object} [user]
96
+ * @returns {object}
97
+ */
98
+ export async function copyFile(fileId, newName = null, folderId = null, user = null) {
99
+ const drive = getDriveClient(user);
100
+ const res = await drive.files.copy({
101
+ requestBody: {
102
+ name: newName,
103
+ parents: folderId ? [folderId] : undefined,
104
+ },
105
+ fileId,
106
+ });
107
+ return res.data;
108
+ }
109
+
110
+ /**
111
+ * Export Google Doc to PDF
112
+ * @param {string} fileId
113
+ * @param {object} [user]
114
+ * @returns {Buffer}
115
+ */
116
+ export async function exportToPdf(fileId, user = null) {
117
+ const drive = getDriveClient(user);
118
+ const res = await drive.files.export(
119
+ { fileId, mimeType: 'application/pdf' },
120
+ { responseType: 'arraybuffer' }
121
+ );
122
+ return Buffer.from(res.data);
123
+ }
124
+
125
+ /**
126
+ * List files in folder
127
+ * @param {string} [folderId]
128
+ * @param {object} [query]
129
+ * @param {object} [user]
130
+ * @returns {Array}
131
+ */
132
+ export async function listFiles(folderId = null, query = {}, user = null) {
133
+ const drive = getDriveClient(user);
134
+ const qParts = [];
135
+
136
+ if (folderId) {
137
+ qParts.push(`'${folderId}' in parents`);
138
+ }
139
+ if (query.trashed === false) {
140
+ qParts.push("trashed = false");
141
+ }
142
+ if (query.mimeType) {
143
+ qParts.push(`mimeType = '${query.mimeType}'`);
144
+ }
145
+ if (query.name) {
146
+ qParts.push(`name = '${query.name}'`);
147
+ }
148
+
149
+ const res = await drive.files.list({
150
+ q: qParts.length ? qParts.join(' AND ') : undefined,
151
+ fields: 'files(id, name, mimeType, size, modifiedTime)',
152
+ pageSize: 100,
153
+ });
154
+
155
+ return res.data.files || [];
156
+ }
157
+
158
+ /**
159
+ * Delete file
160
+ * @param {string} fileId
161
+ * @param {object} [user]
162
+ */
163
+ export async function deleteFile(fileId, user = null) {
164
+ const drive = getDriveClient(user);
165
+ await drive.files.delete({ fileId });
166
+ }
167
+
168
+ /**
169
+ * Get default root folder from config
170
+ * @returns {string}
171
+ */
172
+ function getDefaultFolder() {
173
+ const cfg = buildConfig();
174
+ return cfg.drive.rootFolderId || 'root';
175
+ }
176
+
177
+ /**
178
+ * Ensure folder exists, create if not
179
+ * @param {string} name
180
+ * @param {string} [parentId]
181
+ * @param {object} [user]
182
+ * @returns {string} folderId
183
+ */
184
+ export async function ensureFolder(name, parentId = null, user = null) {
185
+ const drive = getDriveClient(user);
186
+ const queryParts = [`name = '${name}'`, "mimeType = 'application/vnd.google-apps.folder'"];
187
+ if (parentId) queryParts.push(`'${parentId}' in parents`);
188
+
189
+ const res = await drive.files.list({
190
+ q: queryParts.join(' AND '),
191
+ fields: 'files(id)',
192
+ pageSize: 1,
193
+ });
194
+
195
+ if (res.data.files.length > 0) {
196
+ return res.data.files[0].id;
197
+ }
198
+
199
+ // Create folder
200
+ const createRes = await drive.files.create({
201
+ requestBody: {
202
+ name,
203
+ mimeType: 'application/vnd.google-apps.folder',
204
+ parents: parentId ? [parentId] : undefined,
205
+ },
206
+ });
207
+
208
+ return createRes.data.id;
209
+ }
@@ -0,0 +1,33 @@
1
+ import { createCrudHandlers } from '@/lib/crud-factory';
2
+ import { setCurrentRequest } from '@/engine.server';
3
+
4
+ function wrapHandler(method) {
5
+ return async (request, context) => {
6
+ setCurrentRequest(request);
7
+ const params = await context.params;
8
+ const pathArray = params.path || [];
9
+ const entityName = params.entity;
10
+ const parentEntity = params.parentEntity || null;
11
+ const parentId = params.parentId || null;
12
+
13
+ const enhancedContext = {
14
+ ...context,
15
+ params: {
16
+ entity: entityName,
17
+ id: pathArray[0] || null,
18
+ childKey: pathArray[1] || null,
19
+ parentEntity,
20
+ parentId,
21
+ },
22
+ };
23
+
24
+ const handlers = createCrudHandlers(entityName);
25
+ return handlers(request, enhancedContext);
26
+ };
27
+ }
28
+
29
+ export const GET = wrapHandler('GET');
30
+ export const POST = wrapHandler('POST');
31
+ export const PUT = wrapHandler('PUT');
32
+ export const PATCH = wrapHandler('PATCH');
33
+ export const DELETE = wrapHandler('DELETE');
@@ -0,0 +1,77 @@
1
+ import { NextResponse } from '@/lib/next-polyfills';
2
+ import { getUser, setCurrentRequest } from '@/engine.server';
3
+ import {
4
+ getPermissionAuditTrail, getPermissionAuditStats, getPermissionAuditBreakdown,
5
+ getPermissionAuditByDateRange, searchPermissionAudit, getPermissionDiff,
6
+ } from '@/lib/audit-logger';
7
+
8
+ export async function GET(request) {
9
+ setCurrentRequest(request);
10
+ try {
11
+ const user = await getUser();
12
+ if (!user || (user.role !== 'partner' && user.role !== 'manager')) {
13
+ return NextResponse.json({ error: 'Permission denied' }, { status: 403 });
14
+ }
15
+
16
+ const { searchParams } = new URL(request.url);
17
+ const view = searchParams.get('view') || 'summary';
18
+ const days = parseInt(searchParams.get('days') || '30', 10);
19
+ const entityType = searchParams.get('entity_type');
20
+ const entityId = searchParams.get('entity_id');
21
+ const userId = searchParams.get('user_id');
22
+ const searchTerm = searchParams.get('search');
23
+ const limit = parseInt(searchParams.get('limit') || '100', 10);
24
+
25
+ const nowTs = Math.floor(Date.now() / 1000);
26
+ const startTs = nowTs - (days * 86400);
27
+
28
+ let data;
29
+ switch (view) {
30
+ case 'summary': {
31
+ const stats = getPermissionAuditStats();
32
+ const actionBreakdown = getPermissionAuditBreakdown('action');
33
+ const reasonBreakdown = getPermissionAuditBreakdown('reason_code');
34
+ const changes = getPermissionAuditByDateRange(startTs, nowTs, limit);
35
+ data = {
36
+ period_days: days,
37
+ total_changes: changes.length,
38
+ stats: { ...stats, unique_users: stats.unique_users, entity_types: stats.entity_types },
39
+ by_action: actionBreakdown.reduce((a, i) => { a[i.action] = i.count; return a; }, {}),
40
+ by_reason: reasonBreakdown.reduce((a, i) => { a[i.reason_code] = i.count; return a; }, {}),
41
+ };
42
+ break;
43
+ }
44
+ case 'recent':
45
+ data = { period: `Last ${days} days`, changes: getPermissionAuditByDateRange(startTs, nowTs, limit) };
46
+ break;
47
+ case 'user':
48
+ if (!userId) return NextResponse.json({ error: 'user_id required' }, { status: 400 });
49
+ data = { user_id: userId, changes: getPermissionAuditTrail({ userId, limit }) };
50
+ break;
51
+ case 'entity':
52
+ if (!entityType || !entityId) return NextResponse.json({ error: 'entity_type and entity_id required' }, { status: 400 });
53
+ data = {
54
+ entity_type: entityType, entity_id: entityId,
55
+ timeline: getPermissionAuditTrail({ entityType, entityId, limit }).map(c => ({
56
+ ...c, timestamp_iso: new Date(c.timestamp * 1000).toISOString(),
57
+ diff: c.old_permissions && c.new_permissions ? getPermissionDiff(c.old_permissions, c.new_permissions) : null,
58
+ })),
59
+ };
60
+ break;
61
+ case 'search':
62
+ if (!searchTerm) return NextResponse.json({ error: 'search parameter required' }, { status: 400 });
63
+ data = { search_term: searchTerm, results: searchPermissionAudit(searchTerm, limit) };
64
+ break;
65
+ case 'roles':
66
+ data = { changes: getPermissionAuditTrail({ limit }).filter(c => c.action === 'role_change') };
67
+ break;
68
+ default:
69
+ return NextResponse.json({ error: `Unknown view: ${view}` }, { status: 400 });
70
+ }
71
+
72
+ return NextResponse.json({ success: true, view, data });
73
+ } catch (error) {
74
+ console.error('[AuditDashboardAPI] GET error:', error);
75
+ return NextResponse.json({ error: 'Failed to retrieve audit dashboard data', details: error.message }, { status: 500 });
76
+ }
77
+ }
@@ -0,0 +1,46 @@
1
+ import { NextResponse } from '@/lib/next-polyfills';
2
+ import { requireAuth } from '@/lib/auth-middleware';
3
+ import { searchLogs, getLogStats, rotateLogsOlderThan } from '@/lib/audit-logger-enhanced';
4
+ import { withErrorHandler } from '@/lib/with-error-handler';
5
+
6
+ export const GET = withErrorHandler(async (request) => {
7
+ const user = await requireAuth();
8
+ if (user.role !== 'admin' && user.role !== 'partner') {
9
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
10
+ }
11
+
12
+ const url = new URL(request.url);
13
+ const filters = {
14
+ level: url.searchParams.get('level'),
15
+ operation: url.searchParams.get('operation'),
16
+ entityType: url.searchParams.get('entityType'),
17
+ entityId: url.searchParams.get('entityId'),
18
+ userId: url.searchParams.get('userId'),
19
+ action: url.searchParams.get('action'),
20
+ searchText: url.searchParams.get('q'),
21
+ fromDate: url.searchParams.get('from') ? parseInt(url.searchParams.get('from')) : Math.floor(Date.now() / 1000) - (7 * 24 * 60 * 60),
22
+ toDate: url.searchParams.get('to') ? parseInt(url.searchParams.get('to')) : Math.floor(Date.now() / 1000),
23
+ };
24
+
25
+ const page = parseInt(url.searchParams.get('page') || '1');
26
+ const pageSize = parseInt(url.searchParams.get('pageSize') || '100');
27
+
28
+ const result = searchLogs(filters, page, pageSize);
29
+ return NextResponse.json(result);
30
+ });
31
+
32
+ export const POST = withErrorHandler(async (request) => {
33
+ const user = await requireAuth();
34
+ if (user.role !== 'admin') {
35
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
36
+ }
37
+
38
+ const body = await request.json();
39
+ if (body.action === 'rotate') {
40
+ const daysOld = body.daysOld || 90;
41
+ const result = rotateLogsOlderThan(daysOld);
42
+ return NextResponse.json({ success: true, archived: result.archived, archiveId: result.archiveId });
43
+ }
44
+
45
+ return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
46
+ });
@@ -0,0 +1,37 @@
1
+ import { NextResponse, cookies } from '@/lib/next-polyfills';
2
+ import { lucia } from '@/engine.server';
3
+ import { getPermissionAuditById, getPermissionDiff } from '@/lib/audit-logger';
4
+
5
+ async function getUser() {
6
+ const sessionId = (await cookies()).get(lucia.sessionCookieName)?.value ?? null;
7
+ if (!sessionId) return null;
8
+ const { user } = await lucia.validateSession(sessionId);
9
+ return user;
10
+ }
11
+
12
+ export async function GET(request, context) {
13
+ try {
14
+ const user = await getUser();
15
+ if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
16
+
17
+ const params = await context.params;
18
+ const auditId = params.id;
19
+ if (!auditId) return NextResponse.json({ error: 'Audit ID is required' }, { status: 400 });
20
+
21
+ const audit = getPermissionAuditById(auditId);
22
+ if (!audit) return NextResponse.json({ error: 'Audit record not found' }, { status: 404 });
23
+
24
+ if (user.role === 'clerk' && audit.user_id !== user.id) return NextResponse.json({ error: 'Permission denied' }, { status: 403 });
25
+ if (user.role === 'client_admin' || user.role === 'client_user') return NextResponse.json({ error: 'Permission denied' }, { status: 403 });
26
+
27
+ const diff = audit.old_permissions && audit.new_permissions ? getPermissionDiff(audit.old_permissions, audit.new_permissions) : null;
28
+
29
+ return NextResponse.json({
30
+ success: true,
31
+ data: { ...audit, timestamp_iso: new Date(audit.timestamp * 1000).toISOString(), diff },
32
+ });
33
+ } catch (error) {
34
+ console.error('[AuditDetailAPI] GET error:', error);
35
+ return NextResponse.json({ error: 'Failed to retrieve audit record', details: error.message }, { status: 500 });
36
+ }
37
+ }
@@ -0,0 +1,93 @@
1
+ import { NextResponse } from '@/lib/next-polyfills';
2
+ import { lucia } from '@/engine.server';
3
+ import { cookies } from '@/lib/next-polyfills';
4
+ import {
5
+ logPermissionChange, getPermissionAuditTrail, searchPermissionAudit,
6
+ getPermissionAuditByDateRange, exportPermissionAuditCSV,
7
+ } from '@/lib/audit-logger';
8
+
9
+ async function getUser() {
10
+ const sessionId = (await cookies()).get(lucia.sessionCookieName)?.value ?? null;
11
+ if (!sessionId) return null;
12
+ const { user } = await lucia.validateSession(sessionId);
13
+ return user;
14
+ }
15
+
16
+ function checkAuditPermission(user) {
17
+ if (!user) return { allowed: false, error: 'Unauthorized' };
18
+ if (user.role === 'partner' || user.role === 'manager') return { allowed: true, scope: 'all' };
19
+ if (user.role === 'clerk') return { allowed: true, scope: 'own' };
20
+ return { allowed: false, error: 'Permission denied' };
21
+ }
22
+
23
+ export async function GET(request) {
24
+ try {
25
+ const user = await getUser();
26
+ const permCheck = checkAuditPermission(user);
27
+ if (!permCheck.allowed) return NextResponse.json({ error: permCheck.error }, { status: 403 });
28
+
29
+ const { searchParams } = new URL(request.url);
30
+ const search = searchParams.get('search');
31
+ const startDate = searchParams.get('start_date');
32
+ const endDate = searchParams.get('end_date');
33
+ const action = searchParams.get('action');
34
+ const limit = parseInt(searchParams.get('limit') || '100', 10);
35
+ const offset = parseInt(searchParams.get('offset') || '0', 10);
36
+ const format = searchParams.get('format');
37
+
38
+ let results;
39
+ if (search) {
40
+ results = searchPermissionAudit(search, limit);
41
+ } else if (startDate && endDate) {
42
+ results = getPermissionAuditByDateRange(parseInt(startDate, 10), parseInt(endDate, 10), limit);
43
+ } else {
44
+ const filters = {};
45
+ const entityType = searchParams.get('entity_type');
46
+ const entityId = searchParams.get('entity_id');
47
+ const userId = searchParams.get('user_id');
48
+ const affectedUserId = searchParams.get('affected_user_id');
49
+ if (entityType) filters.entityType = entityType;
50
+ if (entityId) filters.entityId = entityId;
51
+ if (permCheck.scope === 'own') filters.userId = user.id;
52
+ else if (userId) filters.userId = userId;
53
+ if (affectedUserId) filters.affectedUserId = affectedUserId;
54
+ filters.limit = limit;
55
+ filters.offset = offset;
56
+ results = getPermissionAuditTrail(filters);
57
+ }
58
+
59
+ if (format === 'csv') {
60
+ const csv = await exportPermissionAuditCSV({});
61
+ return new NextResponse(csv, { headers: { 'Content-Type': 'text/csv', 'Content-Disposition': 'attachment; filename="permission_audit.csv"' } });
62
+ }
63
+
64
+ return NextResponse.json({ success: true, data: results, count: results.length, scope: permCheck.scope });
65
+ } catch (error) {
66
+ console.error('[AuditAPI] GET error:', error);
67
+ return NextResponse.json({ error: 'Failed to retrieve audit trail', details: error.message }, { status: 500 });
68
+ }
69
+ }
70
+
71
+ export async function POST(request) {
72
+ try {
73
+ const user = await getUser();
74
+ if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
75
+
76
+ const body = await request.json();
77
+ const { entityType, entityId, action, oldPermissions, newPermissions, reason, reasonCode, affectedUserId, metadata } = body;
78
+ if (!entityType || !entityId || !action) return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
79
+
80
+ const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || null;
81
+ const sessionId = (await cookies()).get(lucia.sessionCookieName)?.value ?? null;
82
+
83
+ const auditId = logPermissionChange({
84
+ userId: user.id, entityType, entityId, action, oldPermissions, newPermissions,
85
+ reason, reasonCode: reasonCode || 'other', affectedUserId, ipAddress, sessionId, metadata,
86
+ });
87
+
88
+ return NextResponse.json({ success: true, auditId });
89
+ } catch (error) {
90
+ console.error('[AuditAPI] POST error:', error);
91
+ return NextResponse.json({ error: 'Failed to log permission change', details: error.message }, { status: 500 });
92
+ }
93
+ }
@@ -0,0 +1,29 @@
1
+ import { NextResponse } from '@/lib/next-polyfills';
2
+ import { getUser, setCurrentRequest } from '@/engine.server';
3
+ import { getPermissionAuditStats, getPermissionAuditBreakdown } from '@/lib/audit-logger';
4
+
5
+ export async function GET(request) {
6
+ setCurrentRequest(request);
7
+ try {
8
+ const user = await getUser();
9
+ if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
10
+ if (user.role !== 'partner' && user.role !== 'manager') return NextResponse.json({ error: 'Permission denied' }, { status: 403 });
11
+
12
+ const stats = getPermissionAuditStats();
13
+ const actionBreakdown = getPermissionAuditBreakdown('action');
14
+ const reasonCodeBreakdown = getPermissionAuditBreakdown('reason_code');
15
+
16
+ return NextResponse.json({
17
+ success: true,
18
+ stats: {
19
+ ...stats,
20
+ earliest_change: stats.earliest_change ? new Date(stats.earliest_change * 1000).toISOString() : null,
21
+ latest_change: stats.latest_change ? new Date(stats.latest_change * 1000).toISOString() : null,
22
+ },
23
+ breakdown: { by_action: actionBreakdown, by_reason_code: reasonCodeBreakdown },
24
+ });
25
+ } catch (error) {
26
+ console.error('[AuditStatsAPI] GET error:', error);
27
+ return NextResponse.json({ error: 'Failed to retrieve audit statistics', details: error.message }, { status: 500 });
28
+ }
29
+ }
@@ -0,0 +1,79 @@
1
+ import { requireAuth, requirePermission } from '@/lib/auth-middleware';
2
+ import { getAuditHistory, getActionStats, getUserStats } from '@/lib/audit-logger';
3
+ import { getSpec } from '@/config/spec-helpers';
4
+ import { paginated, ok } from '@/lib/response-formatter';
5
+ import { withErrorHandler } from '@/lib/with-error-handler';
6
+ import { AppError } from '@/lib/error-handler';
7
+ import { HTTP } from '@/config/constants';
8
+ import { parse as parseQuery } from '@/lib/query-string-adapter';
9
+
10
+ export const GET = withErrorHandler(async (request) => {
11
+ const user = await requireAuth();
12
+ const url = new URL(request.url);
13
+
14
+ const entityType = url.searchParams.get('entityType');
15
+ const entityId = url.searchParams.get('entityId');
16
+ const userId = url.searchParams.get('userId');
17
+ const action = url.searchParams.get('action');
18
+ const fromDate = url.searchParams.get('fromDate') ? parseInt(url.searchParams.get('fromDate')) : null;
19
+ const toDate = url.searchParams.get('toDate') ? parseInt(url.searchParams.get('toDate')) : null;
20
+ const page = url.searchParams.get('page') ? parseInt(url.searchParams.get('page')) : 1;
21
+ const pageSize = url.searchParams.get('pageSize') ? parseInt(url.searchParams.get('pageSize')) : 50;
22
+ const stats = url.searchParams.get('stats');
23
+
24
+ if (page < 1) {
25
+ throw new AppError('page must be >= 1', 'BAD_REQUEST', HTTP.BAD_REQUEST);
26
+ }
27
+ if (pageSize < 1 || pageSize > 200) {
28
+ throw new AppError('pageSize must be between 1 and 200', 'BAD_REQUEST', HTTP.BAD_REQUEST);
29
+ }
30
+
31
+ if (stats === 'true') {
32
+ if (!fromDate || !toDate) {
33
+ throw new AppError('fromDate and toDate required for stats', 'BAD_REQUEST', HTTP.BAD_REQUEST);
34
+ }
35
+ const actionStats = getActionStats(fromDate, toDate);
36
+ const userStats = getUserStats(fromDate, toDate);
37
+ return ok({
38
+ actionStats,
39
+ userStats
40
+ });
41
+ }
42
+
43
+ const filters = {};
44
+ if (entityType) filters.entityType = entityType;
45
+ if (entityId) filters.entityId = entityId;
46
+ if (userId) filters.userId = userId;
47
+ if (action) filters.action = action;
48
+ if (fromDate) filters.fromDate = fromDate;
49
+ if (toDate) filters.toDate = toDate;
50
+
51
+ const { items, pagination } = getAuditHistory(filters, page, pageSize);
52
+
53
+ const enriched = items.map(log => ({
54
+ ...log,
55
+ timestamp: log.createdAt,
56
+ changes: log.beforeState && log.afterState ? findChanges(log.beforeState, log.afterState) : []
57
+ }));
58
+
59
+ return paginated(enriched, pagination);
60
+ }, 'GET /api/audit');
61
+
62
+ function findChanges(before, after) {
63
+ const changes = [];
64
+ const allKeys = new Set([...Object.keys(before || {}), ...Object.keys(after || {})]);
65
+
66
+ for (const key of allKeys) {
67
+ const beforeVal = before?.[key];
68
+ const afterVal = after?.[key];
69
+ if (JSON.stringify(beforeVal) !== JSON.stringify(afterVal)) {
70
+ changes.push({
71
+ field: key,
72
+ from: beforeVal,
73
+ to: afterVal
74
+ });
75
+ }
76
+ }
77
+
78
+ return changes;
79
+ }
@@ -0,0 +1,18 @@
1
+ import { NextResponse } from '@/lib/next-polyfills';
2
+ import { requireAuth } from '@/lib/auth-middleware';
3
+ import { getLogStats } from '@/lib/audit-logger-enhanced';
4
+ import { withErrorHandler } from '@/lib/with-error-handler';
5
+
6
+ export const GET = withErrorHandler(async (request) => {
7
+ const user = await requireAuth();
8
+ if (user.role !== 'admin' && user.role !== 'partner') {
9
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
10
+ }
11
+
12
+ const url = new URL(request.url);
13
+ const fromDate = url.searchParams.get('from') ? parseInt(url.searchParams.get('from')) : Math.floor(Date.now() / 1000) - (24 * 60 * 60);
14
+ const toDate = url.searchParams.get('to') ? parseInt(url.searchParams.get('to')) : Math.floor(Date.now() / 1000);
15
+
16
+ const stats = getLogStats(fromDate, toDate);
17
+ return NextResponse.json({ success: true, stats, period: { from: fromDate, to: toDate } });
18
+ });