thatcher 1.0.7 → 1.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -16
- package/package.json +12 -18
- package/src/adapters/google-auth.js +4 -43
- package/src/adapters/google-drive.js +12 -67
- package/src/app/api/audit/dashboard/route.js +14 -11
- package/src/app/api/audit/logs/route.js +2 -2
- package/src/app/api/audit/permissions/[id]/route.js +6 -3
- package/src/app/api/audit/permissions/route.js +10 -7
- package/src/app/api/audit/permissions/stats/route.js +8 -5
- package/src/app/api/audit/route.js +4 -4
- package/src/app/api/audit/stats/route.js +1 -1
- package/src/app/api/auth/google/callback/route.js +5 -2
- package/src/app/api/auth/google/route.js +4 -1
- package/src/app/api/auth/login/route.js +5 -2
- package/src/app/api/auth/mwr-bridge/route.js +11 -21
- package/src/app/api/auth/password-reset/route.js +15 -14
- package/src/app/api/cron/trigger/route.js +4 -1
- package/src/app/api/debug/[[...path]]/route.js +6 -5
- package/src/app/api/debug/sqlite/route.js +7 -12
- package/src/app/api/debug/sync/route.js +5 -5
- package/src/app/api/domains/route.js +4 -1
- package/src/app/api/email/allocate/batch/route.js +19 -29
- package/src/app/api/email/allocate/route.js +16 -17
- package/src/app/api/email/receive/route.js +31 -42
- package/src/app/api/email/send/route.js +28 -16
- package/src/app/api/email/unallocated/route.js +10 -13
- package/src/app/api/files/[id]/route.js +4 -1
- package/src/app/api/formula/[[...path]]/route.js +0 -4
- package/src/app/api/health/route.js +10 -9
- package/src/app/api/metrics/route.js +1 -1
- package/src/cli.js +4 -4
- package/src/config/config-loader.js +1 -21
- package/src/config/constants.js +0 -35
- package/src/config/env.js +0 -30
- package/src/config/spec-helpers.js +6 -95
- package/src/engine.js +23 -0
- package/src/engine.server.js +8 -78
- package/src/index.js +42 -67
- package/src/lib/alert-manager.js +7 -3
- package/src/lib/api-helpers.js +4 -3
- package/src/lib/api.js +10 -10
- package/src/lib/audit-logger-enhanced.js +77 -83
- package/src/lib/auth-middleware.js +1 -32
- package/src/lib/auth-route-helpers.js +5 -0
- package/src/lib/busybase-adapter.js +0 -5
- package/src/lib/busybase-audit-reads.js +142 -0
- package/src/lib/busybase-audit.js +41 -0
- package/src/lib/busybase-lucia-adapter.js +72 -0
- package/src/lib/busybase-store.js +338 -0
- package/src/lib/config-field-helpers.js +0 -17
- package/src/lib/config-generator-engine.js +19 -36
- package/src/lib/config-helpers.js +0 -21
- package/src/lib/crud-action-helpers.js +2 -2
- package/src/lib/crud-factory.js +1 -16
- package/src/lib/crud-handlers.js +17 -52
- package/src/lib/csrf-protection.js +5 -2
- package/src/lib/date-utils.js +3 -127
- package/src/lib/debug-registry.js +0 -5
- package/src/lib/domain-loader.js +4 -1
- package/src/lib/email-sender.js +25 -23
- package/src/lib/error-handler.js +5 -41
- package/src/lib/error-recovery.js +10 -7
- package/src/lib/error-resilience.js +7 -5
- package/src/lib/events-engine.js +9 -6
- package/src/lib/export-sink.js +0 -5
- package/src/lib/field-iterator.js +7 -76
- package/src/lib/field-registry.js +0 -22
- package/src/lib/generic-crud-handler.js +0 -16
- package/src/lib/hook-engine.js +5 -53
- package/src/lib/hot-reload/checkpoint.js +5 -2
- package/src/lib/hot-reload/promise-container.js +7 -3
- package/src/lib/hot-reload/route-wrapper.js +4 -1
- package/src/lib/hot-reload/supervisor.js +6 -3
- package/src/lib/hyperformula-service.js +0 -5
- package/src/lib/id-helpers.js +10 -0
- package/src/lib/index.js +4 -7
- package/src/lib/log-aggregator.js +0 -1
- package/src/lib/logger.js +0 -15
- package/src/lib/metrics-collector.js +2 -46
- package/src/lib/next-compat.js +15 -1
- package/src/lib/observability-bootstrap.js +0 -5
- package/src/lib/perf-monitor.js +4 -1
- package/src/lib/perf-profiler.js +0 -5
- package/src/lib/query-string-adapter.js +5 -47
- package/src/lib/realtime-server.js +4 -26
- package/src/lib/request-tracing.js +0 -5
- package/src/lib/resource-monitor.js +4 -1
- package/src/lib/response-formatter.js +0 -38
- package/src/lib/route-helpers.js +3 -1
- package/src/lib/safe-json.js +5 -1
- package/src/lib/server-bootstrap.js +10 -9
- package/src/lib/stage-pipeline.js +2 -2
- package/src/lib/state-transport-client.js +4 -1
- package/src/lib/static-server.js +2 -2
- package/src/lib/status-helpers.js +2 -51
- package/src/lib/tracing.js +0 -5
- package/src/lib/utils.js +6 -67
- package/src/lib/validate.js +5 -88
- package/src/lib/validators.js +0 -38
- package/src/lib/with-audit-logging.js +1 -1
- package/src/lib/workflow-engine.js +10 -77
- package/src/lib/xstate-workflow-engine.js +3 -9
- package/src/plugins/index.js +5 -3
- package/src/server/server.js +26 -27
- package/src/services/collaborator-role.service.js +21 -77
- package/src/services/email-sender.js +7 -34
- package/src/services/notification-engine.js +12 -33
- package/src/services/permission.service.js +10 -73
- package/src/ui/advanced-search-renderer.js +3 -2
- package/src/ui/advanced-widgets.js +1 -1
- package/src/ui/auth-pages.js +10 -9
- package/src/ui/auth-styles.js +1 -1
- package/src/ui/client.js +1 -1
- package/src/ui/component-engine.js +1 -1
- package/src/ui/dashboard-renderer.js +5 -5
- package/src/ui/dialog-engine.js +6 -9
- package/src/ui/dialog-factory.js +2 -2
- package/src/ui/engagement-cards.js +7 -7
- package/src/ui/engagement-dialogs.js +2 -3
- package/src/ui/engagement-grid-renderer.js +20 -19
- package/src/ui/entity-renderer.js +56 -42
- package/src/ui/file-dialogs.js +25 -9
- package/src/ui/flexup-report-renderer.js +21 -18
- package/src/ui/format-helpers.js +27 -28
- package/src/ui/highlight-threading-renderer.js +9 -9
- package/src/ui/job-management-renderer.js +18 -16
- package/src/ui/layout.js +51 -25
- package/src/ui/ml-console-renderer.js +15 -16
- package/src/ui/monitoring-dashboard-client.js +9 -8
- package/src/ui/notifications-renderer.js +11 -10
- package/src/ui/page-handler-admin.js +32 -23
- package/src/ui/page-handler-helpers.js +27 -23
- package/src/ui/page-handler-reviews.js +71 -35
- package/src/ui/page-handler-rfi.js +28 -14
- package/src/ui/page-handler.js +65 -48
- package/src/ui/picker-dialogs.js +26 -19
- package/src/ui/render-helpers.js +84 -46
- package/src/ui/review-comparison-renderer.js +8 -7
- package/src/ui/review-detail-panels.js +8 -9
- package/src/ui/review-detail-renderer.js +3 -3
- package/src/ui/review-detail-script.js +13 -12
- package/src/ui/review-mwr-renderer.js +6 -5
- package/src/ui/review-renderer.js +7 -6
- package/src/ui/review-widgets.js +3 -3
- package/src/ui/rfi-detail-renderer.js +37 -20
- package/src/ui/rfi-report-renderer.js +14 -9
- package/src/ui/settings-renderer-advanced.js +21 -18
- package/src/ui/settings-renderer-advanced2.js +20 -19
- package/src/ui/settings-renderer-teams.js +4 -4
- package/src/ui/settings-renderer.js +4 -4
- package/src/ui/spacing-system.js +5 -2
- package/src/ui/standalone-login.js +2 -2
- package/src/ui/styles.css +2 -1
- package/src/ui/styles2.css +75 -5
- package/src/ui/test-page.js +4 -4
- package/src/ui/widgets.js +20 -15
- package/src/lib/audit-logger.js +0 -193
- package/src/lib/database-core.js +0 -255
- package/src/lib/database-migrations.js +0 -350
- package/src/lib/db-backup.js +0 -97
- package/src/lib/error-boundary.js +0 -134
- package/src/lib/errors.js +0 -69
- package/src/lib/health-monitor.js +0 -134
- package/src/lib/index-optimizer.js +0 -96
- package/src/lib/query-engine-write.js +0 -221
- package/src/lib/query-engine.js +0 -446
- package/src/ui/collaboration-dialogs.js +0 -31
- package/src/ui/global-tags.js +0 -144
- package/src/ui/idle-logout.js +0 -156
- package/src/ui/letter-dialogs.js +0 -37
- package/src/ui/password-reset-page.js +0 -146
- package/src/ui/perf-renderer.js +0 -71
- package/src/ui/rfi-renderer.js +0 -194
- package/src/ui/validation-ui.js +0 -147
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Thatcher is a complete extraction of the moonlanding application framework. Buil
|
|
|
12
12
|
- **Plugin System** — Extend entities with hooks, validators, and custom fields
|
|
13
13
|
- **External Integrations** — Google Drive/Gmail, Email (SMTP), PDF generation
|
|
14
14
|
- **Hot Reload** — Config changes reflect instantly without restart
|
|
15
|
-
- **Production-Ready** —
|
|
15
|
+
- **Production-Ready** — busybase (LanceDB) document store, vector search, metrics, audit logging
|
|
16
16
|
|
|
17
17
|
## Quick Start
|
|
18
18
|
|
|
@@ -84,7 +84,7 @@ thresholds:
|
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
That's it. Start the server and you have:
|
|
87
|
-
-
|
|
87
|
+
- busybase `item` table (schemaless; created on first insert)
|
|
88
88
|
- REST endpoints: `GET/POST/PUT/DELETE /api/item`
|
|
89
89
|
- Role-based permissions enforced
|
|
90
90
|
- State transitions via `POST /api/item/:id/transition`
|
|
@@ -143,7 +143,7 @@ thatcher/
|
|
|
143
143
|
│ │ ├── env.js # Environment config
|
|
144
144
|
│ │ └── constants.js # HTTP codes, statuses
|
|
145
145
|
│ ├── lib/
|
|
146
|
-
│ │ ├──
|
|
146
|
+
│ │ ├── busybase-store.js # busybase data layer (async CRUD)
|
|
147
147
|
│ │ ├── query-engine.js # Read operations (GET, search)
|
|
148
148
|
│ │ ├── query-engine-write.js # Write operations (CRUD)
|
|
149
149
|
│ │ ├── config-generator-engine.js # Spec builder
|
|
@@ -359,7 +359,7 @@ All APIs accessible via thatcher instance:
|
|
|
359
359
|
| Variable | Purpose | Default |
|
|
360
360
|
|----------|---------|---------|
|
|
361
361
|
| `PORT` | Server port | 3000 |
|
|
362
|
-
| `
|
|
362
|
+
| `BUSYBASE_DIR` | busybase data directory | `busybase_data` |
|
|
363
363
|
| `NODE_ENV` | `development` \| `production` | `development` |
|
|
364
364
|
| `DEBUG` | Enable debug logging | `false` |
|
|
365
365
|
| `GOOGLE_CLIENT_ID` | OAuth client ID | — |
|
|
@@ -370,18 +370,6 @@ All APIs accessible via thatcher instance:
|
|
|
370
370
|
| `EMAIL_USER` | SMTP username | — |
|
|
371
371
|
| `EMAIL_PASSWORD` | SMTP password | — |
|
|
372
372
|
|
|
373
|
-
## Migrating from Moonlanding
|
|
374
|
-
|
|
375
|
-
Thatcher is a drop-in replacement for moonlanding's core:
|
|
376
|
-
|
|
377
|
-
1. Copy `master-config.yml` → `thatcher.config.yml`
|
|
378
|
-
2. Move any `.plugin.js` files to new project's `plugins/`
|
|
379
|
-
3. Change import paths: `@/lib/...` → `thatcher/...`
|
|
380
|
-
4. Replace `import ... from '@/engine'` with `thatcher.*` methods
|
|
381
|
-
5. Update server initialization: `new Thatcher({ config: '...' })`
|
|
382
|
-
|
|
383
|
-
All business logic remains in config and plugins — no code changes required.
|
|
384
|
-
|
|
385
373
|
## Publishing
|
|
386
374
|
|
|
387
375
|
Thatcher is published to npm as `thatcher`. Every push to `main` triggers:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thatcher",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "A config-driven application framework for building data-intensive web apps without code.
|
|
3
|
+
"version": "1.0.34",
|
|
4
|
+
"description": "A config-driven application framework for building data-intensive web apps without code.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
"./config/*.js": "./src/config/*.js",
|
|
16
16
|
"./services/*": "./src/services/*.js",
|
|
17
17
|
"./services/*.js": "./src/services/*.js",
|
|
18
|
-
"./
|
|
19
|
-
"./validation/*": "./src/validation/*.js",
|
|
18
|
+
"./validation/*": "./src/lib/validation/*.js",
|
|
20
19
|
"./adapters/*": "./src/adapters/*.js",
|
|
21
20
|
"./plugins/*": "./src/plugins/*.js",
|
|
22
21
|
"./package.json": "./package.json"
|
|
@@ -28,7 +27,7 @@
|
|
|
28
27
|
"scripts": {
|
|
29
28
|
"dev": "bun run src/cli.js dev",
|
|
30
29
|
"start": "bun run src/cli.js start",
|
|
31
|
-
"test": "
|
|
30
|
+
"test": "node test.js",
|
|
32
31
|
"lint": "eslint src/",
|
|
33
32
|
"typecheck": "tsc --noEmit"
|
|
34
33
|
},
|
|
@@ -41,32 +40,27 @@
|
|
|
41
40
|
"workflow",
|
|
42
41
|
"sdk",
|
|
43
42
|
"bun",
|
|
44
|
-
"
|
|
43
|
+
"busybase"
|
|
45
44
|
],
|
|
46
|
-
"author": "
|
|
45
|
+
"author": "lanmower",
|
|
47
46
|
"license": "MIT",
|
|
48
47
|
"engines": {
|
|
49
48
|
"node": ">=18.0.0",
|
|
50
49
|
"bun": ">=1.0.0"
|
|
51
50
|
},
|
|
52
51
|
"dependencies": {
|
|
53
|
-
"@casl/ability": "^6.8.0",
|
|
54
|
-
"@lucia-auth/adapter-sqlite": "^3.0.2",
|
|
55
52
|
"arctic": "^1.9.2",
|
|
56
|
-
"bcrypt": "^
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
53
|
+
"bcrypt": "^6.0.0",
|
|
54
|
+
"busybase": "^1.0.2",
|
|
55
|
+
"googleapis": "^173.0.0",
|
|
56
|
+
"hyperformula": "^3.2.0",
|
|
60
57
|
"js-yaml": "^4.1.1",
|
|
61
58
|
"lucia": "^3.2.2",
|
|
62
|
-
"
|
|
63
|
-
"nodemailer": "^6.9.7",
|
|
64
|
-
"puppeteer": "^21.6.1",
|
|
59
|
+
"nodemailer": "^9.0.1",
|
|
65
60
|
"webjsx": "^0.0.73",
|
|
66
|
-
"
|
|
61
|
+
"xstate": "^5.0.0"
|
|
67
62
|
},
|
|
68
63
|
"devDependencies": {
|
|
69
|
-
"@types/better-sqlite3": "^7.6.11",
|
|
70
64
|
"@types/node": "^20.16.5",
|
|
71
65
|
"eslint": "^8.56.0",
|
|
72
66
|
"typescript": "^5.3.3"
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
* Google Auth Adapter - Factory for Google OAuth2 clients
|
|
3
|
-
* Provides JWT and OAuth2 clients for Google APIs
|
|
4
|
-
*/
|
|
5
|
-
|
|
1
|
+
import { createLogger } from '../lib/logger.js';
|
|
6
2
|
import { google } from 'googleapis';
|
|
3
|
+
|
|
4
|
+
const log = createLogger('[GoogleAuth]');
|
|
7
5
|
import { buildConfig } from '../config/env.js';
|
|
8
6
|
import fs from 'fs';
|
|
9
7
|
import path from 'path';
|
|
@@ -11,10 +9,6 @@ import path from 'path';
|
|
|
11
9
|
let _oauth2Client = null;
|
|
12
10
|
let _jwtClient = null;
|
|
13
11
|
|
|
14
|
-
/**
|
|
15
|
-
* Initialize Google auth clients
|
|
16
|
-
* @param {object} [config] - Optional config override
|
|
17
|
-
*/
|
|
18
12
|
export function initGoogleAuth(config = null) {
|
|
19
13
|
const cfg = config || buildConfig();
|
|
20
14
|
const scopes = [
|
|
@@ -45,36 +39,23 @@ export function initGoogleAuth(config = null) {
|
|
|
45
39
|
],
|
|
46
40
|
});
|
|
47
41
|
} catch (err) {
|
|
48
|
-
|
|
42
|
+
log.warn('failed to load service account:', { message: err.message });
|
|
49
43
|
}
|
|
50
44
|
}
|
|
51
45
|
|
|
52
46
|
return { oauth2: _oauth2Client, jwt: _jwtClient };
|
|
53
47
|
}
|
|
54
48
|
|
|
55
|
-
/**
|
|
56
|
-
* Get OAuth2 client
|
|
57
|
-
* @returns {OAuth2Client|null}
|
|
58
|
-
*/
|
|
59
49
|
export function getOAuth2Client() {
|
|
60
50
|
if (!_oauth2Client) initGoogleAuth();
|
|
61
51
|
return _oauth2Client;
|
|
62
52
|
}
|
|
63
53
|
|
|
64
|
-
/**
|
|
65
|
-
* Get JWT client (service account)
|
|
66
|
-
* @returns {JWT|null}
|
|
67
|
-
*/
|
|
68
54
|
export function getJWTClient() {
|
|
69
55
|
if (!_jwtClient) initGoogleAuth();
|
|
70
56
|
return _jwtClient;
|
|
71
57
|
}
|
|
72
58
|
|
|
73
|
-
/**
|
|
74
|
-
* Generate OAuth2 authorization URL
|
|
75
|
-
* @param {object} options - state, access_type, prompt, etc.
|
|
76
|
-
* @returns {string}
|
|
77
|
-
*/
|
|
78
59
|
export function generateAuthUrl(options = {}) {
|
|
79
60
|
const client = getOAuth2Client();
|
|
80
61
|
if (!client) throw new Error('Google OAuth not configured');
|
|
@@ -92,11 +73,6 @@ export function generateAuthUrl(options = {}) {
|
|
|
92
73
|
});
|
|
93
74
|
}
|
|
94
75
|
|
|
95
|
-
/**
|
|
96
|
-
* Exchange code for tokens
|
|
97
|
-
* @param {string} code
|
|
98
|
-
* @returns {Promise<object>} tokens
|
|
99
|
-
*/
|
|
100
76
|
export async function exchangeCode(code) {
|
|
101
77
|
const client = getOAuth2Client();
|
|
102
78
|
if (!client) throw new Error('Google OAuth not configured');
|
|
@@ -105,22 +81,12 @@ export async function exchangeCode(code) {
|
|
|
105
81
|
return tokens;
|
|
106
82
|
}
|
|
107
83
|
|
|
108
|
-
/**
|
|
109
|
-
* Get user info from Google
|
|
110
|
-
* @param {string} accessToken
|
|
111
|
-
* @returns {Promise<object>}
|
|
112
|
-
*/
|
|
113
84
|
export async function getUserInfo(accessToken) {
|
|
114
85
|
const oauth2 = google.oauth2({ version: 'v2', auth: `Bearer ${accessToken}` });
|
|
115
86
|
const res = await oauth2.userinfo.get();
|
|
116
87
|
return res.data;
|
|
117
88
|
}
|
|
118
89
|
|
|
119
|
-
/**
|
|
120
|
-
* Verify ID token
|
|
121
|
-
* @param {string} idToken
|
|
122
|
-
* @returns {Promise<object>}
|
|
123
|
-
*/
|
|
124
90
|
export async function verifyIdToken(idToken) {
|
|
125
91
|
const client = getOAuth2Client();
|
|
126
92
|
if (!client) throw new Error('Google OAuth not configured');
|
|
@@ -132,11 +98,6 @@ export async function verifyIdToken(idToken) {
|
|
|
132
98
|
return ticket.getPayload();
|
|
133
99
|
}
|
|
134
100
|
|
|
135
|
-
/**
|
|
136
|
-
* Create OAuth2 client with refresh token
|
|
137
|
-
* @param {string} refreshToken
|
|
138
|
-
* @returns {OAuth2Client}
|
|
139
|
-
*/
|
|
140
101
|
export function createClientWithRefresh(refreshToken) {
|
|
141
102
|
const client = new google.auth.OAuth2(
|
|
142
103
|
buildConfig().auth.google.clientId,
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Google Drive Adapter - File storage and manipulation
|
|
3
|
-
* Upload, download, copy, export to PDF
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import { google } from 'googleapis';
|
|
7
2
|
import fs from 'fs';
|
|
8
3
|
import path from 'path';
|
|
@@ -11,11 +6,6 @@ import { buildConfig } from '../config/env.js';
|
|
|
11
6
|
|
|
12
7
|
const DRIVE_SCOPES = ['https://www.googleapis.com/auth/drive'];
|
|
13
8
|
|
|
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
9
|
export function getDriveClient(user = null) {
|
|
20
10
|
let client;
|
|
21
11
|
|
|
@@ -33,14 +23,6 @@ export function getDriveClient(user = null) {
|
|
|
33
23
|
return google.drive({ version: 'v3', auth: client });
|
|
34
24
|
}
|
|
35
25
|
|
|
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
26
|
export async function uploadFile(filePath, name, options = {}, user = null) {
|
|
45
27
|
const drive = getDriveClient(user);
|
|
46
28
|
const absPath = path.resolve(filePath);
|
|
@@ -61,13 +43,6 @@ export async function uploadFile(filePath, name, options = {}, user = null) {
|
|
|
61
43
|
return res.data;
|
|
62
44
|
}
|
|
63
45
|
|
|
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
46
|
export async function downloadFile(fileId, destPath = null, user = null) {
|
|
72
47
|
const drive = getDriveClient(user);
|
|
73
48
|
const res = await drive.files.get(
|
|
@@ -87,14 +62,6 @@ export async function downloadFile(fileId, destPath = null, user = null) {
|
|
|
87
62
|
return Buffer.from(res.data);
|
|
88
63
|
}
|
|
89
64
|
|
|
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
65
|
export async function copyFile(fileId, newName = null, folderId = null, user = null) {
|
|
99
66
|
const drive = getDriveClient(user);
|
|
100
67
|
const res = await drive.files.copy({
|
|
@@ -107,12 +74,6 @@ export async function copyFile(fileId, newName = null, folderId = null, user = n
|
|
|
107
74
|
return res.data;
|
|
108
75
|
}
|
|
109
76
|
|
|
110
|
-
/**
|
|
111
|
-
* Export Google Doc to PDF
|
|
112
|
-
* @param {string} fileId
|
|
113
|
-
* @param {object} [user]
|
|
114
|
-
* @returns {Buffer}
|
|
115
|
-
*/
|
|
116
77
|
export async function exportToPdf(fileId, user = null) {
|
|
117
78
|
const drive = getDriveClient(user);
|
|
118
79
|
const res = await drive.files.export(
|
|
@@ -122,28 +83,28 @@ export async function exportToPdf(fileId, user = null) {
|
|
|
122
83
|
return Buffer.from(res.data);
|
|
123
84
|
}
|
|
124
85
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
86
|
+
// Escape a value for inclusion in a single-quoted Google Drive query string.
|
|
87
|
+
// Drive Query Language escapes \ and ' with a backslash; without this an
|
|
88
|
+
// attacker-controlled name/id/mimeType could inject query operators.
|
|
89
|
+
function escapeGQLString(str) {
|
|
90
|
+
return String(str).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
91
|
+
}
|
|
92
|
+
|
|
132
93
|
export async function listFiles(folderId = null, query = {}, user = null) {
|
|
133
94
|
const drive = getDriveClient(user);
|
|
134
95
|
const qParts = [];
|
|
135
96
|
|
|
136
97
|
if (folderId) {
|
|
137
|
-
qParts.push(`'${folderId}' in parents`);
|
|
98
|
+
qParts.push(`'${escapeGQLString(folderId)}' in parents`);
|
|
138
99
|
}
|
|
139
100
|
if (query.trashed === false) {
|
|
140
101
|
qParts.push("trashed = false");
|
|
141
102
|
}
|
|
142
103
|
if (query.mimeType) {
|
|
143
|
-
qParts.push(`mimeType = '${query.mimeType}'`);
|
|
104
|
+
qParts.push(`mimeType = '${escapeGQLString(query.mimeType)}'`);
|
|
144
105
|
}
|
|
145
106
|
if (query.name) {
|
|
146
|
-
qParts.push(`name = '${query.name}'`);
|
|
107
|
+
qParts.push(`name = '${escapeGQLString(query.name)}'`);
|
|
147
108
|
}
|
|
148
109
|
|
|
149
110
|
const res = await drive.files.list({
|
|
@@ -155,36 +116,20 @@ export async function listFiles(folderId = null, query = {}, user = null) {
|
|
|
155
116
|
return res.data.files || [];
|
|
156
117
|
}
|
|
157
118
|
|
|
158
|
-
/**
|
|
159
|
-
* Delete file
|
|
160
|
-
* @param {string} fileId
|
|
161
|
-
* @param {object} [user]
|
|
162
|
-
*/
|
|
163
119
|
export async function deleteFile(fileId, user = null) {
|
|
164
120
|
const drive = getDriveClient(user);
|
|
165
121
|
await drive.files.delete({ fileId });
|
|
166
122
|
}
|
|
167
123
|
|
|
168
|
-
/**
|
|
169
|
-
* Get default root folder from config
|
|
170
|
-
* @returns {string}
|
|
171
|
-
*/
|
|
172
124
|
function getDefaultFolder() {
|
|
173
125
|
const cfg = buildConfig();
|
|
174
126
|
return cfg.drive.rootFolderId || 'root';
|
|
175
127
|
}
|
|
176
128
|
|
|
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
129
|
export async function ensureFolder(name, parentId = null, user = null) {
|
|
185
130
|
const drive = getDriveClient(user);
|
|
186
|
-
const queryParts = [`name = '${name}'`, "mimeType = 'application/vnd.google-apps.folder'"];
|
|
187
|
-
if (parentId) queryParts.push(`'${parentId}' in parents`);
|
|
131
|
+
const queryParts = [`name = '${escapeGQLString(name)}'`, "mimeType = 'application/vnd.google-apps.folder'"];
|
|
132
|
+
if (parentId) queryParts.push(`'${escapeGQLString(parentId)}' in parents`);
|
|
188
133
|
|
|
189
134
|
const res = await drive.files.list({
|
|
190
135
|
q: queryParts.join(' AND '),
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { NextResponse } from '@/lib/next-polyfills';
|
|
2
|
+
import { createLogger } from '@/lib/logger.js';
|
|
3
|
+
|
|
4
|
+
const log = createLogger('[AuditDashboardAPI]');
|
|
2
5
|
import { getUser, setCurrentRequest } from '@/engine.server';
|
|
3
6
|
import {
|
|
4
7
|
getPermissionAuditTrail, getPermissionAuditStats, getPermissionAuditBreakdown,
|
|
5
8
|
getPermissionAuditByDateRange, searchPermissionAudit, getPermissionDiff,
|
|
6
|
-
} from '@/lib/audit-
|
|
9
|
+
} from '@/lib/busybase-audit-reads';
|
|
7
10
|
|
|
8
11
|
export async function GET(request) {
|
|
9
12
|
setCurrentRequest(request);
|
|
@@ -28,10 +31,10 @@ export async function GET(request) {
|
|
|
28
31
|
let data;
|
|
29
32
|
switch (view) {
|
|
30
33
|
case 'summary': {
|
|
31
|
-
const stats = getPermissionAuditStats();
|
|
32
|
-
const actionBreakdown = getPermissionAuditBreakdown('action');
|
|
33
|
-
const reasonBreakdown = getPermissionAuditBreakdown('reason_code');
|
|
34
|
-
const changes = getPermissionAuditByDateRange(startTs, nowTs, limit);
|
|
34
|
+
const stats = await getPermissionAuditStats();
|
|
35
|
+
const actionBreakdown = await getPermissionAuditBreakdown('action');
|
|
36
|
+
const reasonBreakdown = await getPermissionAuditBreakdown('reason_code');
|
|
37
|
+
const changes = await getPermissionAuditByDateRange(startTs, nowTs, limit);
|
|
35
38
|
data = {
|
|
36
39
|
period_days: days,
|
|
37
40
|
total_changes: changes.length,
|
|
@@ -42,17 +45,17 @@ export async function GET(request) {
|
|
|
42
45
|
break;
|
|
43
46
|
}
|
|
44
47
|
case 'recent':
|
|
45
|
-
data = { period: `Last ${days} days`, changes: getPermissionAuditByDateRange(startTs, nowTs, limit) };
|
|
48
|
+
data = { period: `Last ${days} days`, changes: await getPermissionAuditByDateRange(startTs, nowTs, limit) };
|
|
46
49
|
break;
|
|
47
50
|
case 'user':
|
|
48
51
|
if (!userId) return NextResponse.json({ error: 'user_id required' }, { status: 400 });
|
|
49
|
-
data = { user_id: userId, changes: getPermissionAuditTrail({ userId, limit }) };
|
|
52
|
+
data = { user_id: userId, changes: await getPermissionAuditTrail({ userId, limit }) };
|
|
50
53
|
break;
|
|
51
54
|
case 'entity':
|
|
52
55
|
if (!entityType || !entityId) return NextResponse.json({ error: 'entity_type and entity_id required' }, { status: 400 });
|
|
53
56
|
data = {
|
|
54
57
|
entity_type: entityType, entity_id: entityId,
|
|
55
|
-
timeline: getPermissionAuditTrail({ entityType, entityId, limit }).map(c => ({
|
|
58
|
+
timeline: (await getPermissionAuditTrail({ entityType, entityId, limit })).map(c => ({
|
|
56
59
|
...c, timestamp_iso: new Date(c.timestamp * 1000).toISOString(),
|
|
57
60
|
diff: c.old_permissions && c.new_permissions ? getPermissionDiff(c.old_permissions, c.new_permissions) : null,
|
|
58
61
|
})),
|
|
@@ -60,10 +63,10 @@ export async function GET(request) {
|
|
|
60
63
|
break;
|
|
61
64
|
case 'search':
|
|
62
65
|
if (!searchTerm) return NextResponse.json({ error: 'search parameter required' }, { status: 400 });
|
|
63
|
-
data = { search_term: searchTerm, results: searchPermissionAudit(searchTerm, limit) };
|
|
66
|
+
data = { search_term: searchTerm, results: await searchPermissionAudit(searchTerm, limit) };
|
|
64
67
|
break;
|
|
65
68
|
case 'roles':
|
|
66
|
-
data = { changes: getPermissionAuditTrail({ limit }).filter(c => c.action === 'role_change') };
|
|
69
|
+
data = { changes: (await getPermissionAuditTrail({ limit })).filter(c => c.action === 'role_change') };
|
|
67
70
|
break;
|
|
68
71
|
default:
|
|
69
72
|
return NextResponse.json({ error: `Unknown view: ${view}` }, { status: 400 });
|
|
@@ -71,7 +74,7 @@ export async function GET(request) {
|
|
|
71
74
|
|
|
72
75
|
return NextResponse.json({ success: true, view, data });
|
|
73
76
|
} catch (error) {
|
|
74
|
-
|
|
77
|
+
log.error('GET error:', { message: error.message });
|
|
75
78
|
return NextResponse.json({ error: 'Failed to retrieve audit dashboard data', details: error.message }, { status: 500 });
|
|
76
79
|
}
|
|
77
80
|
}
|
|
@@ -25,7 +25,7 @@ export const GET = withErrorHandler(async (request) => {
|
|
|
25
25
|
const page = parseInt(url.searchParams.get('page') || '1');
|
|
26
26
|
const pageSize = parseInt(url.searchParams.get('pageSize') || '100');
|
|
27
27
|
|
|
28
|
-
const result = searchLogs(filters, page, pageSize);
|
|
28
|
+
const result = await searchLogs(filters, page, pageSize);
|
|
29
29
|
return NextResponse.json(result);
|
|
30
30
|
});
|
|
31
31
|
|
|
@@ -38,7 +38,7 @@ export const POST = withErrorHandler(async (request) => {
|
|
|
38
38
|
const body = await request.json();
|
|
39
39
|
if (body.action === 'rotate') {
|
|
40
40
|
const daysOld = body.daysOld || 90;
|
|
41
|
-
const result = rotateLogsOlderThan(daysOld);
|
|
41
|
+
const result = await rotateLogsOlderThan(daysOld);
|
|
42
42
|
return NextResponse.json({ success: true, archived: result.archived, archiveId: result.archiveId });
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { NextResponse, cookies } from '@/lib/next-polyfills';
|
|
2
|
+
import { createLogger } from '@/lib/logger.js';
|
|
3
|
+
|
|
4
|
+
const log = createLogger('[AuditDetailAPI]');
|
|
2
5
|
import { lucia } from '@/engine.server';
|
|
3
|
-
import { getPermissionAuditById, getPermissionDiff } from '@/lib/audit-
|
|
6
|
+
import { getPermissionAuditById, getPermissionDiff } from '@/lib/busybase-audit-reads';
|
|
4
7
|
|
|
5
8
|
async function getUser() {
|
|
6
9
|
const sessionId = (await cookies()).get(lucia.sessionCookieName)?.value ?? null;
|
|
@@ -18,7 +21,7 @@ export async function GET(request, context) {
|
|
|
18
21
|
const auditId = params.id;
|
|
19
22
|
if (!auditId) return NextResponse.json({ error: 'Audit ID is required' }, { status: 400 });
|
|
20
23
|
|
|
21
|
-
const audit = getPermissionAuditById(auditId);
|
|
24
|
+
const audit = await getPermissionAuditById(auditId);
|
|
22
25
|
if (!audit) return NextResponse.json({ error: 'Audit record not found' }, { status: 404 });
|
|
23
26
|
|
|
24
27
|
if (user.role === 'clerk' && audit.user_id !== user.id) return NextResponse.json({ error: 'Permission denied' }, { status: 403 });
|
|
@@ -31,7 +34,7 @@ export async function GET(request, context) {
|
|
|
31
34
|
data: { ...audit, timestamp_iso: new Date(audit.timestamp * 1000).toISOString(), diff },
|
|
32
35
|
});
|
|
33
36
|
} catch (error) {
|
|
34
|
-
|
|
37
|
+
log.error('GET error:', { message: error.message });
|
|
35
38
|
return NextResponse.json({ error: 'Failed to retrieve audit record', details: error.message }, { status: 500 });
|
|
36
39
|
}
|
|
37
40
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { NextResponse } from '@/lib/next-polyfills';
|
|
2
|
+
import { createLogger } from '@/lib/logger.js';
|
|
3
|
+
|
|
4
|
+
const log = createLogger('[AuditAPI]');
|
|
2
5
|
import { lucia } from '@/engine.server';
|
|
3
6
|
import { cookies } from '@/lib/next-polyfills';
|
|
4
7
|
import {
|
|
5
8
|
logPermissionChange, getPermissionAuditTrail, searchPermissionAudit,
|
|
6
9
|
getPermissionAuditByDateRange, exportPermissionAuditCSV,
|
|
7
|
-
} from '@/lib/audit-
|
|
10
|
+
} from '@/lib/busybase-audit-reads';
|
|
8
11
|
|
|
9
12
|
async function getUser() {
|
|
10
13
|
const sessionId = (await cookies()).get(lucia.sessionCookieName)?.value ?? null;
|
|
@@ -37,9 +40,9 @@ export async function GET(request) {
|
|
|
37
40
|
|
|
38
41
|
let results;
|
|
39
42
|
if (search) {
|
|
40
|
-
results = searchPermissionAudit(search, limit);
|
|
43
|
+
results = await searchPermissionAudit(search, limit);
|
|
41
44
|
} else if (startDate && endDate) {
|
|
42
|
-
results = getPermissionAuditByDateRange(parseInt(startDate, 10), parseInt(endDate, 10), limit);
|
|
45
|
+
results = await getPermissionAuditByDateRange(parseInt(startDate, 10), parseInt(endDate, 10), limit);
|
|
43
46
|
} else {
|
|
44
47
|
const filters = {};
|
|
45
48
|
const entityType = searchParams.get('entity_type');
|
|
@@ -53,7 +56,7 @@ export async function GET(request) {
|
|
|
53
56
|
if (affectedUserId) filters.affectedUserId = affectedUserId;
|
|
54
57
|
filters.limit = limit;
|
|
55
58
|
filters.offset = offset;
|
|
56
|
-
results = getPermissionAuditTrail(filters);
|
|
59
|
+
results = await getPermissionAuditTrail(filters);
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
if (format === 'csv') {
|
|
@@ -63,7 +66,7 @@ export async function GET(request) {
|
|
|
63
66
|
|
|
64
67
|
return NextResponse.json({ success: true, data: results, count: results.length, scope: permCheck.scope });
|
|
65
68
|
} catch (error) {
|
|
66
|
-
|
|
69
|
+
log.error('GET error:', { message: error.message });
|
|
67
70
|
return NextResponse.json({ error: 'Failed to retrieve audit trail', details: error.message }, { status: 500 });
|
|
68
71
|
}
|
|
69
72
|
}
|
|
@@ -80,14 +83,14 @@ export async function POST(request) {
|
|
|
80
83
|
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || null;
|
|
81
84
|
const sessionId = (await cookies()).get(lucia.sessionCookieName)?.value ?? null;
|
|
82
85
|
|
|
83
|
-
const auditId = logPermissionChange({
|
|
86
|
+
const auditId = await logPermissionChange({
|
|
84
87
|
userId: user.id, entityType, entityId, action, oldPermissions, newPermissions,
|
|
85
88
|
reason, reasonCode: reasonCode || 'other', affectedUserId, ipAddress, sessionId, metadata,
|
|
86
89
|
});
|
|
87
90
|
|
|
88
91
|
return NextResponse.json({ success: true, auditId });
|
|
89
92
|
} catch (error) {
|
|
90
|
-
|
|
93
|
+
log.error('POST error:', { message: error.message });
|
|
91
94
|
return NextResponse.json({ error: 'Failed to log permission change', details: error.message }, { status: 500 });
|
|
92
95
|
}
|
|
93
96
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { NextResponse } from '@/lib/next-polyfills';
|
|
2
|
+
import { createLogger } from '@/lib/logger.js';
|
|
3
|
+
|
|
4
|
+
const log = createLogger('[AuditStatsAPI]');
|
|
2
5
|
import { getUser, setCurrentRequest } from '@/engine.server';
|
|
3
|
-
import { getPermissionAuditStats, getPermissionAuditBreakdown } from '@/lib/audit-
|
|
6
|
+
import { getPermissionAuditStats, getPermissionAuditBreakdown } from '@/lib/busybase-audit-reads';
|
|
4
7
|
|
|
5
8
|
export async function GET(request) {
|
|
6
9
|
setCurrentRequest(request);
|
|
@@ -9,9 +12,9 @@ export async function GET(request) {
|
|
|
9
12
|
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
10
13
|
if (user.role !== 'partner' && user.role !== 'manager') return NextResponse.json({ error: 'Permission denied' }, { status: 403 });
|
|
11
14
|
|
|
12
|
-
const stats = getPermissionAuditStats();
|
|
13
|
-
const actionBreakdown = getPermissionAuditBreakdown('action');
|
|
14
|
-
const reasonCodeBreakdown = getPermissionAuditBreakdown('reason_code');
|
|
15
|
+
const stats = await getPermissionAuditStats();
|
|
16
|
+
const actionBreakdown = await getPermissionAuditBreakdown('action');
|
|
17
|
+
const reasonCodeBreakdown = await getPermissionAuditBreakdown('reason_code');
|
|
15
18
|
|
|
16
19
|
return NextResponse.json({
|
|
17
20
|
success: true,
|
|
@@ -23,7 +26,7 @@ export async function GET(request) {
|
|
|
23
26
|
breakdown: { by_action: actionBreakdown, by_reason_code: reasonCodeBreakdown },
|
|
24
27
|
});
|
|
25
28
|
} catch (error) {
|
|
26
|
-
|
|
29
|
+
log.error('GET error:', { message: error.message });
|
|
27
30
|
return NextResponse.json({ error: 'Failed to retrieve audit statistics', details: error.message }, { status: 500 });
|
|
28
31
|
}
|
|
29
32
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { requireAuth, requirePermission } from '@/lib/auth-middleware';
|
|
2
|
-
import { getAuditHistory, getActionStats, getUserStats } from '@/lib/audit-
|
|
2
|
+
import { getAuditHistory, getActionStats, getUserStats } from '@/lib/busybase-audit-reads';
|
|
3
3
|
import { getSpec } from '@/config/spec-helpers';
|
|
4
4
|
import { paginated, ok } from '@/lib/response-formatter';
|
|
5
5
|
import { withErrorHandler } from '@/lib/with-error-handler';
|
|
@@ -32,8 +32,8 @@ export const GET = withErrorHandler(async (request) => {
|
|
|
32
32
|
if (!fromDate || !toDate) {
|
|
33
33
|
throw new AppError('fromDate and toDate required for stats', 'BAD_REQUEST', HTTP.BAD_REQUEST);
|
|
34
34
|
}
|
|
35
|
-
const actionStats = getActionStats(fromDate, toDate);
|
|
36
|
-
const userStats = getUserStats(fromDate, toDate);
|
|
35
|
+
const actionStats = await getActionStats(fromDate, toDate);
|
|
36
|
+
const userStats = await getUserStats(fromDate, toDate);
|
|
37
37
|
return ok({
|
|
38
38
|
actionStats,
|
|
39
39
|
userStats
|
|
@@ -48,7 +48,7 @@ export const GET = withErrorHandler(async (request) => {
|
|
|
48
48
|
if (fromDate) filters.fromDate = fromDate;
|
|
49
49
|
if (toDate) filters.toDate = toDate;
|
|
50
50
|
|
|
51
|
-
const { items, pagination } = getAuditHistory(filters, page, pageSize);
|
|
51
|
+
const { items, pagination } = await getAuditHistory(filters, page, pageSize);
|
|
52
52
|
|
|
53
53
|
const enriched = items.map(log => ({
|
|
54
54
|
...log,
|
|
@@ -13,6 +13,6 @@ export const GET = withErrorHandler(async (request) => {
|
|
|
13
13
|
const fromDate = url.searchParams.get('from') ? parseInt(url.searchParams.get('from')) : Math.floor(Date.now() / 1000) - (24 * 60 * 60);
|
|
14
14
|
const toDate = url.searchParams.get('to') ? parseInt(url.searchParams.get('to')) : Math.floor(Date.now() / 1000);
|
|
15
15
|
|
|
16
|
-
const stats = getLogStats(fromDate, toDate);
|
|
16
|
+
const stats = await getLogStats(fromDate, toDate);
|
|
17
17
|
return NextResponse.json({ success: true, stats, period: { from: fromDate, to: toDate } });
|
|
18
18
|
});
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { createLogger } from '@/lib/logger.js';
|
|
1
2
|
import { google, createSession } from '@/engine.server';
|
|
3
|
+
|
|
4
|
+
const log = createLogger('[OAuth]');
|
|
2
5
|
import { getBy, create } from '@/engine';
|
|
3
6
|
import { Google } from 'arctic';
|
|
4
7
|
import { GOOGLE_APIS } from '@/config/constants';
|
|
@@ -40,7 +43,7 @@ export async function GET(request) {
|
|
|
40
43
|
|
|
41
44
|
const stateValidation = validateOAuthState(code, stateKey, state, codeVerifier);
|
|
42
45
|
if (!stateValidation.valid) {
|
|
43
|
-
|
|
46
|
+
log.warn('state validation failed:', { error: stateValidation.error });
|
|
44
47
|
return buildOAuthErrorResponse(stateValidation.error, request);
|
|
45
48
|
}
|
|
46
49
|
|
|
@@ -88,7 +91,7 @@ export async function GET(request) {
|
|
|
88
91
|
|
|
89
92
|
return response;
|
|
90
93
|
} catch (error) {
|
|
91
|
-
|
|
94
|
+
log.error('Google OAuth error:', { message: error.message });
|
|
92
95
|
return buildOAuthErrorResponse('oauth_failed', request);
|
|
93
96
|
}
|
|
94
97
|
}
|