thatcher 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +398 -0
- package/package.json +73 -0
- package/src/adapters/google-auth.js +148 -0
- package/src/adapters/google-drive.js +209 -0
- package/src/app/api/[entity]/[[...path]]/route.js +33 -0
- package/src/app/api/audit/dashboard/route.js +77 -0
- package/src/app/api/audit/logs/route.js +46 -0
- package/src/app/api/audit/permissions/[id]/route.js +37 -0
- package/src/app/api/audit/permissions/route.js +93 -0
- package/src/app/api/audit/permissions/stats/route.js +29 -0
- package/src/app/api/audit/route.js +79 -0
- package/src/app/api/audit/stats/route.js +18 -0
- package/src/app/api/auth/google/callback/route.js +94 -0
- package/src/app/api/auth/google/route.js +58 -0
- package/src/app/api/auth/login/route.js +121 -0
- package/src/app/api/auth/logout/route.js +52 -0
- package/src/app/api/auth/me/route.js +16 -0
- package/src/app/api/auth/mwr-bridge/route.js +77 -0
- package/src/app/api/auth/password-reset/route.js +65 -0
- package/src/app/api/cron/trigger/route.js +58 -0
- package/src/app/api/csrf-token/route.js +8 -0
- package/src/app/api/debug/config/route.js +22 -0
- package/src/app/api/debug/hooks/route.js +16 -0
- package/src/app/api/debug/plugins/route.js +20 -0
- package/src/app/api/debug/sqlite/route.js +21 -0
- package/src/app/api/debug/sync/route.js +29 -0
- package/src/app/api/debug/workflow/route.js +20 -0
- package/src/app/api/domains/[domain]/route.js +26 -0
- package/src/app/api/domains/route.js +21 -0
- package/src/app/api/email/allocate/batch/route.js +106 -0
- package/src/app/api/email/allocate/route.js +141 -0
- package/src/app/api/email/receive/route.js +158 -0
- package/src/app/api/email/route.js +3 -0
- package/src/app/api/email/send/route.js +77 -0
- package/src/app/api/email/unallocated/route.js +47 -0
- package/src/app/api/files/[id]/route.js +38 -0
- package/src/app/api/health/route.js +95 -0
- package/src/app/api/metrics/route.js +73 -0
- package/src/app/api/monitoring/dashboard/route.js +18 -0
- package/src/cli.js +243 -0
- package/src/config/config-loader.js +112 -0
- package/src/config/constants.js +127 -0
- package/src/config/env.js +164 -0
- package/src/config/spec-helpers.js +232 -0
- package/src/engine.server.js +212 -0
- package/src/index.js +368 -0
- package/src/lib/accessibility.js +162 -0
- package/src/lib/action-factory.js +34 -0
- package/src/lib/action-utils.js +21 -0
- package/src/lib/alert-manager.js +189 -0
- package/src/lib/api-error-wrapper.js +125 -0
- package/src/lib/api-helpers.js +53 -0
- package/src/lib/api.js +82 -0
- package/src/lib/audit-logger-enhanced.js +117 -0
- package/src/lib/audit-logger.js +193 -0
- package/src/lib/auth-middleware.js +102 -0
- package/src/lib/auth-route-helpers.js +83 -0
- package/src/lib/business-rules-engine.js +86 -0
- package/src/lib/compression.js +44 -0
- package/src/lib/config-field-helpers.js +91 -0
- package/src/lib/config-generator-engine.js +445 -0
- package/src/lib/config-helpers.js +120 -0
- package/src/lib/connection-guard.js +79 -0
- package/src/lib/crud-action-helpers.js +34 -0
- package/src/lib/crud-factory.js +83 -0
- package/src/lib/crud-handlers.js +244 -0
- package/src/lib/csrf-protection.js +63 -0
- package/src/lib/database-core.js +258 -0
- package/src/lib/database-migrations.js +96 -0
- package/src/lib/date-utils.js +159 -0
- package/src/lib/db-backup.js +97 -0
- package/src/lib/db-monitor.js +127 -0
- package/src/lib/domain-loader.js +82 -0
- package/src/lib/email-sender.js +100 -0
- package/src/lib/error-boundary.js +134 -0
- package/src/lib/error-handler.js +84 -0
- package/src/lib/error-recovery.js +190 -0
- package/src/lib/error-resilience.js +130 -0
- package/src/lib/errors.js +69 -0
- package/src/lib/events-engine.js +182 -0
- package/src/lib/field-iterator.js +50 -0
- package/src/lib/field-registry.js +68 -0
- package/src/lib/field-types.js +154 -0
- package/src/lib/generic-crud-handler.js +32 -0
- package/src/lib/health-monitor.js +134 -0
- package/src/lib/hook-engine.js +169 -0
- package/src/lib/hot-reload/cache-invalidator.js +115 -0
- package/src/lib/hot-reload/checkpoint.js +95 -0
- package/src/lib/hot-reload/debug-exposure.js +67 -0
- package/src/lib/hot-reload/directory-watcher.js +96 -0
- package/src/lib/hot-reload/index.js +50 -0
- package/src/lib/hot-reload/mutex.js +75 -0
- package/src/lib/hot-reload/promise-container.js +66 -0
- package/src/lib/hot-reload/route-wrapper.js +46 -0
- package/src/lib/hot-reload/safe-error.js +51 -0
- package/src/lib/hot-reload/supervisor.js +161 -0
- package/src/lib/hot-reload/timeout-wrapper.js +52 -0
- package/src/lib/http-methods-factory.js +25 -0
- package/src/lib/index-optimizer.js +96 -0
- package/src/lib/index.js +35 -0
- package/src/lib/list-data-transform.js +39 -0
- package/src/lib/log-aggregator.js +116 -0
- package/src/lib/logger.js +55 -0
- package/src/lib/metrics-collector.js +102 -0
- package/src/lib/minifier.js +19 -0
- package/src/lib/monitoring-init.js +67 -0
- package/src/lib/next-compat.js +80 -0
- package/src/lib/next-polyfills.js +135 -0
- package/src/lib/perf-monitor.js +91 -0
- package/src/lib/progress-components.js +181 -0
- package/src/lib/query-cache.js +126 -0
- package/src/lib/query-engine-write.js +221 -0
- package/src/lib/query-engine.js +399 -0
- package/src/lib/query-perf.js +117 -0
- package/src/lib/query-string-adapter.js +75 -0
- package/src/lib/realtime-server.js +67 -0
- package/src/lib/render-cache.js +61 -0
- package/src/lib/request-tracker.js +43 -0
- package/src/lib/resource-hints.js +29 -0
- package/src/lib/resource-monitor.js +117 -0
- package/src/lib/response-formatter.js +80 -0
- package/src/lib/route-helpers.js +33 -0
- package/src/lib/route-resolver.js +142 -0
- package/src/lib/safe-json.js +8 -0
- package/src/lib/server-bootstrap.js +71 -0
- package/src/lib/stage-pipeline.js +153 -0
- package/src/lib/state-protocol.js +171 -0
- package/src/lib/state-transport-client.js +169 -0
- package/src/lib/state-transport-reconnect.js +121 -0
- package/src/lib/state-transport-server.js +181 -0
- package/src/lib/static-server.js +97 -0
- package/src/lib/status-helpers.js +98 -0
- package/src/lib/universal-handler.js +7 -0
- package/src/lib/utils.js +93 -0
- package/src/lib/validate.js +197 -0
- package/src/lib/validation/business-validators.js +61 -0
- package/src/lib/validation/csrf.js +51 -0
- package/src/lib/validation/file-validators.js +34 -0
- package/src/lib/validation/format-validators.js +106 -0
- package/src/lib/validation/index.js +19 -0
- package/src/lib/validation/rate-limit.js +31 -0
- package/src/lib/validation/security-validators.js +78 -0
- package/src/lib/validation-middleware.js +133 -0
- package/src/lib/validators.js +105 -0
- package/src/lib/with-audit-logging.js +63 -0
- package/src/lib/with-error-handler.js +31 -0
- package/src/lib/workflow-engine.js +250 -0
- package/src/server/server.js +305 -0
- package/src/services/collaborator-role.service.js +205 -0
- package/src/services/email-sender.js +105 -0
- package/src/services/notification-engine.js +110 -0
- package/src/services/permission.service.js +181 -0
- package/src/ui/advanced-search-renderer.js +42 -0
- package/src/ui/advanced-widgets.js +47 -0
- package/src/ui/auth-pages.js +114 -0
- package/src/ui/auth-styles.js +53 -0
- package/src/ui/client.js +99 -0
- package/src/ui/collaboration-dialogs.js +31 -0
- package/src/ui/common-handlers.js +156 -0
- package/src/ui/component-engine.js +103 -0
- package/src/ui/dashboard-renderer.js +150 -0
- package/src/ui/dialog-engine.js +147 -0
- package/src/ui/dialog-factory.js +38 -0
- package/src/ui/engagement-cards.js +76 -0
- package/src/ui/engagement-dialogs.js +100 -0
- package/src/ui/engagement-grid-renderer.js +109 -0
- package/src/ui/entity-renderer.js +176 -0
- package/src/ui/event-delegation.js +108 -0
- package/src/ui/fetch-json.js +24 -0
- package/src/ui/file-dialogs.js +81 -0
- package/src/ui/flexup-report-renderer.js +173 -0
- package/src/ui/format-helpers.js +102 -0
- package/src/ui/global-tags.js +144 -0
- package/src/ui/highlight-threading-renderer.js +176 -0
- package/src/ui/idle-logout.js +156 -0
- package/src/ui/job-management-renderer.js +61 -0
- package/src/ui/layout.js +219 -0
- package/src/ui/letter-dialogs.js +37 -0
- package/src/ui/ml-console-renderer.js +108 -0
- package/src/ui/monitoring-dashboard-client.js +136 -0
- package/src/ui/monitoring-dashboard.js +134 -0
- package/src/ui/notifications-renderer.js +51 -0
- package/src/ui/page-handler-admin.js +121 -0
- package/src/ui/page-handler-helpers.js +111 -0
- package/src/ui/page-handler-reviews.js +165 -0
- package/src/ui/page-handler-rfi.js +42 -0
- package/src/ui/page-handler.js +210 -0
- package/src/ui/password-reset-page.js +146 -0
- package/src/ui/perf-helpers.js +96 -0
- package/src/ui/perf-renderer.js +71 -0
- package/src/ui/permissions-ui.js +163 -0
- package/src/ui/picker-dialogs.js +100 -0
- package/src/ui/render-helpers.js +124 -0
- package/src/ui/renderer.js +35 -0
- package/src/ui/review-comparison-renderer.js +58 -0
- package/src/ui/review-detail-panels.js +71 -0
- package/src/ui/review-detail-renderer.js +170 -0
- package/src/ui/review-detail-script.js +95 -0
- package/src/ui/review-mwr-renderer.js +113 -0
- package/src/ui/review-renderer.js +202 -0
- package/src/ui/review-widgets.js +88 -0
- package/src/ui/review-zone-nav.js +12 -0
- package/src/ui/rfi-detail-renderer.js +191 -0
- package/src/ui/rfi-renderer.js +194 -0
- package/src/ui/rfi-report-renderer.js +56 -0
- package/src/ui/rippleui.css +1 -0
- package/src/ui/settings-renderer-advanced.js +195 -0
- package/src/ui/settings-renderer-advanced2.js +158 -0
- package/src/ui/settings-renderer-teams.js +112 -0
- package/src/ui/settings-renderer.js +166 -0
- package/src/ui/spacing-system.js +155 -0
- package/src/ui/standalone-login.js +109 -0
- package/src/ui/styles.css +2530 -0
- package/src/ui/styles2.css +1602 -0
- package/src/ui/test-page.js +23 -0
- package/src/ui/validation-rules.js +73 -0
- package/src/ui/validation-ui.js +147 -0
- package/src/ui/virtual-scroll.js +107 -0
- package/src/ui/webjsx.js +61 -0
- package/src/ui/widgets.js +152 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment configuration
|
|
3
|
+
* Provides typed access to environment variables with defaults
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get environment variable with optional default
|
|
8
|
+
*/
|
|
9
|
+
function getEnv(key, defaultValue = null) {
|
|
10
|
+
const val = process.env[key];
|
|
11
|
+
if (val === undefined || val === null) return defaultValue;
|
|
12
|
+
return val;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get boolean from env
|
|
17
|
+
*/
|
|
18
|
+
function getEnvBool(key, defaultValue = false) {
|
|
19
|
+
const val = getEnv(key);
|
|
20
|
+
if (val === null) return defaultValue;
|
|
21
|
+
return val === 'true' || val === '1';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get number from env
|
|
26
|
+
*/
|
|
27
|
+
function getEnvInt(key, defaultValue = 0) {
|
|
28
|
+
const val = getEnv(key);
|
|
29
|
+
if (val === null) return defaultValue;
|
|
30
|
+
const num = parseInt(val, 10);
|
|
31
|
+
return isNaN(num) ? defaultValue : num;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Configuration object builder
|
|
36
|
+
*/
|
|
37
|
+
export function buildConfig(overrides = {}) {
|
|
38
|
+
const config = {
|
|
39
|
+
db: {
|
|
40
|
+
path: overrides.databasePath || getEnv('DATABASE_PATH', './data/app.db'),
|
|
41
|
+
...overrides.db,
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
auth: {
|
|
45
|
+
google: {
|
|
46
|
+
clientId: getEnv('GOOGLE_CLIENT_ID'),
|
|
47
|
+
clientSecret: getEnv('GOOGLE_CLIENT_SECRET'),
|
|
48
|
+
redirectUri: getEnv('GOOGLE_REDIRECT_URI') || 'http://localhost:3000/api/auth/google/callback',
|
|
49
|
+
serviceAccountPath: getEnv('GOOGLE_SERVICE_ACCOUNT_PATH', './config/service-account.json'),
|
|
50
|
+
...overrides.auth?.google,
|
|
51
|
+
},
|
|
52
|
+
session: {
|
|
53
|
+
secure: getEnv('NODE_ENV') === 'production',
|
|
54
|
+
expires: false,
|
|
55
|
+
cookieName: getEnv('SESSION_COOKIE_NAME', 'thatcher_session'),
|
|
56
|
+
maxAge: getEnvInt('SESSION_MAX_AGE', 60 * 60 * 24 * 30), // 30 days
|
|
57
|
+
...overrides.auth?.session,
|
|
58
|
+
},
|
|
59
|
+
...overrides.auth,
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
drive: {
|
|
63
|
+
serviceAccountPath: getEnv('GOOGLE_SERVICE_ACCOUNT_PATH', './config/service-account.json'),
|
|
64
|
+
rootFolderId: getEnv('GOOGLE_DRIVE_FOLDER_ID'),
|
|
65
|
+
cache: {
|
|
66
|
+
enabled: true,
|
|
67
|
+
ttl: 3600,
|
|
68
|
+
bucket: getEnv('CACHE_BUCKET', 'thatcher_cache'),
|
|
69
|
+
...overrides.drive?.cache,
|
|
70
|
+
},
|
|
71
|
+
...overrides.drive,
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
email: {
|
|
75
|
+
provider: getEnv('EMAIL_PROVIDER', 'nodemailer'),
|
|
76
|
+
from: getEnv('EMAIL_FROM', 'noreply@example.com'),
|
|
77
|
+
smtp: {
|
|
78
|
+
host: getEnv('EMAIL_HOST', 'smtp.gmail.com'),
|
|
79
|
+
port: getEnvInt('EMAIL_PORT', 587),
|
|
80
|
+
user: getEnv('EMAIL_USER'),
|
|
81
|
+
password: getEnv('EMAIL_PASSWORD'),
|
|
82
|
+
...overrides.email?.smtp,
|
|
83
|
+
},
|
|
84
|
+
limits: {
|
|
85
|
+
daily: getEnvInt('EMAIL_DAILY_LIMIT', 500),
|
|
86
|
+
ratePerMinute: getEnvInt('EMAIL_RATE_PER_MINUTE', 10),
|
|
87
|
+
...overrides.email?.limits,
|
|
88
|
+
},
|
|
89
|
+
...overrides.email,
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
app: {
|
|
93
|
+
url: getEnv('APP_URL', 'http://localhost:3000'),
|
|
94
|
+
env: getEnv('NODE_ENV', 'development'),
|
|
95
|
+
debug: getEnvBool('DEBUG', false),
|
|
96
|
+
name: getEnv('APP_NAME', 'Thatcher App'),
|
|
97
|
+
...overrides.app,
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
server: {
|
|
101
|
+
port: getEnvInt('PORT', 3000),
|
|
102
|
+
host: getEnv('HOST', '0.0.0.0'),
|
|
103
|
+
...overrides.server,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return config;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if Google OAuth is configured
|
|
112
|
+
*/
|
|
113
|
+
export function hasGoogleAuth(config) {
|
|
114
|
+
const cfg = config?.auth?.google || buildConfig().auth.google;
|
|
115
|
+
return !!(cfg.clientId && cfg.clientSecret);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if Drive integration is configured
|
|
120
|
+
*/
|
|
121
|
+
export function hasDriveConfig(config) {
|
|
122
|
+
const cfg = config?.drive || buildConfig().drive;
|
|
123
|
+
return !!cfg.rootFolderId;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if email sending is configured
|
|
128
|
+
*/
|
|
129
|
+
export function hasEmailConfig(config) {
|
|
130
|
+
const cfg = config?.email || buildConfig().email;
|
|
131
|
+
return !!(cfg.smtp.user && cfg.smtp.password);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Validate environment configuration
|
|
136
|
+
* Returns array of warnings (non-fatal)
|
|
137
|
+
*/
|
|
138
|
+
export function validateEnv(config = buildConfig()) {
|
|
139
|
+
const warnings = [];
|
|
140
|
+
|
|
141
|
+
if (!getEnv('DATABASE_PATH')) {
|
|
142
|
+
warnings.push('DATABASE_PATH not set, using default ./data/app.db');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (config.app.env === 'production') {
|
|
146
|
+
if (!config.email.smtp.user) warnings.push('EMAIL_USER not set - email sending disabled');
|
|
147
|
+
if (!config.email.smtp.password) warnings.push('EMAIL_PASSWORD not set - email sending disabled');
|
|
148
|
+
if (!config.app.url) warnings.push('APP_URL not set - links in emails will use localhost');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!hasGoogleAuth(config)) {
|
|
152
|
+
warnings.push('Google OAuth not configured - auth will be email/password only');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return warnings;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export default {
|
|
159
|
+
buildConfig,
|
|
160
|
+
hasGoogleAuth,
|
|
161
|
+
hasDriveConfig,
|
|
162
|
+
hasEmailConfig,
|
|
163
|
+
validateEnv,
|
|
164
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec Helpers - Utility functions for entity specifications
|
|
3
|
+
* Provides easy access to entity specs, navigation, and derived properties
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get entity spec by name
|
|
8
|
+
* @param {string} name - Entity name
|
|
9
|
+
* @param {object} configEngine - Config engine instance
|
|
10
|
+
* @returns {object|null} Entity specification or null if not found
|
|
11
|
+
*/
|
|
12
|
+
export function getSpec(name, configEngine) {
|
|
13
|
+
try {
|
|
14
|
+
return configEngine.generateEntitySpec(name);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error.message.includes('not found') || error.message.includes('Unknown entity')) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get all navigable entities (top-level, non-embedded)
|
|
25
|
+
* @param {object} configEngine
|
|
26
|
+
* @returns {Array} Sorted array of entity specs
|
|
27
|
+
*/
|
|
28
|
+
export function getNavItems(configEngine) {
|
|
29
|
+
try {
|
|
30
|
+
const allEntities = configEngine.getAllEntities();
|
|
31
|
+
return allEntities
|
|
32
|
+
.map(e => configEngine.generateEntitySpec(e))
|
|
33
|
+
.filter(s => !s.embedded && !s.parent && !s.system_entity)
|
|
34
|
+
.sort((a, b) => (a.order || 999) - (b.order || 999))
|
|
35
|
+
.map(s => ({
|
|
36
|
+
name: s.name,
|
|
37
|
+
label: s.labelPlural || s.label,
|
|
38
|
+
icon: s.icon,
|
|
39
|
+
href: `/${s.name}`,
|
|
40
|
+
}));
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('[spec-helpers] getNavItems error:', error.message);
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get child entity definitions for a parent spec
|
|
49
|
+
* @param {object} spec - Parent entity spec
|
|
50
|
+
* @returns {Array} Child entity configs
|
|
51
|
+
*/
|
|
52
|
+
export function getChildEntities(spec) {
|
|
53
|
+
if (!spec.children) return [];
|
|
54
|
+
return Object.entries(spec.children).map(([key, child]) => ({
|
|
55
|
+
key,
|
|
56
|
+
entity: child.entity,
|
|
57
|
+
label: child.label,
|
|
58
|
+
fk: child.fk,
|
|
59
|
+
filter: child.filter,
|
|
60
|
+
component: child.component,
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get parent entity name
|
|
66
|
+
* @param {object} spec
|
|
67
|
+
* @returns {string|null}
|
|
68
|
+
*/
|
|
69
|
+
export function getParentEntity(spec) {
|
|
70
|
+
return spec.parent || null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get default sort configuration
|
|
75
|
+
* @param {object} spec
|
|
76
|
+
* @returns {{field: string, dir: string}}
|
|
77
|
+
*/
|
|
78
|
+
export function getDefaultSort(spec) {
|
|
79
|
+
return spec.list?.defaultSort || { field: 'created_at', dir: 'desc' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get available filters for entity
|
|
84
|
+
* @param {object} spec
|
|
85
|
+
* @returns {Array}
|
|
86
|
+
*/
|
|
87
|
+
export function getAvailableFilters(spec) {
|
|
88
|
+
return spec.list?.filters || [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get page size for entity
|
|
93
|
+
* @param {object} spec
|
|
94
|
+
* @returns {number}
|
|
95
|
+
*/
|
|
96
|
+
export function getPageSize(spec, systemPagination = { default_page_size: 20 }) {
|
|
97
|
+
return spec.list?.pageSize || systemPagination.default_page_size;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get entity label (singular or plural)
|
|
102
|
+
* @param {object} spec
|
|
103
|
+
* @param {boolean} plural
|
|
104
|
+
* @returns {string}
|
|
105
|
+
*/
|
|
106
|
+
export function getEntityLabel(spec, plural = false) {
|
|
107
|
+
return plural ? (spec.labelPlural || spec.label) : spec.label;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get default initial state for entity (empty record with defaults)
|
|
112
|
+
* @param {object} spec
|
|
113
|
+
* @returns {object}
|
|
114
|
+
*/
|
|
115
|
+
export function getInitialState(spec) {
|
|
116
|
+
const state = {};
|
|
117
|
+
for (const [key, field] of Object.entries(spec.fields)) {
|
|
118
|
+
if (field.type === 'id') continue;
|
|
119
|
+
if (field.default !== undefined) {
|
|
120
|
+
state[key] = field.default;
|
|
121
|
+
} else if (field.type === 'bool') {
|
|
122
|
+
state[key] = false;
|
|
123
|
+
} else if (field.type === 'int' || field.type === 'decimal') {
|
|
124
|
+
state[key] = 0;
|
|
125
|
+
} else if (field.type === 'json') {
|
|
126
|
+
state[key] = [];
|
|
127
|
+
} else if (field.type === 'date' || field.type === 'timestamp') {
|
|
128
|
+
state[key] = null;
|
|
129
|
+
} else {
|
|
130
|
+
state[key] = '';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return state;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check if entity is embedded (used in parent contexts)
|
|
138
|
+
* @param {object} spec
|
|
139
|
+
* @returns {boolean}
|
|
140
|
+
*/
|
|
141
|
+
export function isEmbeddedEntity(spec) {
|
|
142
|
+
return spec.embedded === true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if entity is a top-level parent
|
|
147
|
+
* @param {object} spec
|
|
148
|
+
* @returns {boolean}
|
|
149
|
+
*/
|
|
150
|
+
export function isParentEntity(spec) {
|
|
151
|
+
return !spec.embedded && !spec.parent;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if entity has child relationships
|
|
156
|
+
* @param {object} spec
|
|
157
|
+
* @returns {boolean}
|
|
158
|
+
*/
|
|
159
|
+
export function hasChildRelationships(spec) {
|
|
160
|
+
return !!spec.children && Object.keys(spec.children).length > 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get entity options (enums, picklists)
|
|
165
|
+
* @param {object} spec
|
|
166
|
+
* @param {string} optionKey
|
|
167
|
+
* @returns {Array}
|
|
168
|
+
*/
|
|
169
|
+
export function getOptions(spec, optionKey) {
|
|
170
|
+
return spec.options?.[optionKey] || [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get display label for an option value
|
|
175
|
+
* @param {object} spec
|
|
176
|
+
* @param {string} optionKey
|
|
177
|
+
* @param {string} value
|
|
178
|
+
* @returns {string}
|
|
179
|
+
*/
|
|
180
|
+
export function getOptionLabel(spec, optionKey, value) {
|
|
181
|
+
const option = getOptions(spec, optionKey).find(o => o.value === value);
|
|
182
|
+
return option?.label || String(value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get color for an option value (for UI badges)
|
|
187
|
+
* @param {object} spec
|
|
188
|
+
* @param {string} optionKey
|
|
189
|
+
* @param {string} value
|
|
190
|
+
* @returns {string}
|
|
191
|
+
*/
|
|
192
|
+
export function getOptionColor(spec, optionKey, value) {
|
|
193
|
+
const option = getOptions(spec, optionKey).find(o => o.value === value);
|
|
194
|
+
return option?.color || 'gray';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Build navigation structure for menus
|
|
199
|
+
* @param {object} configEngine
|
|
200
|
+
* @param {object} user - Optional user for permission filtering
|
|
201
|
+
* @returns {Array} Navigation items
|
|
202
|
+
*/
|
|
203
|
+
export function buildNavigation(configEngine, user = null) {
|
|
204
|
+
try {
|
|
205
|
+
const allEntities = configEngine.getAllEntities();
|
|
206
|
+
const items = allEntities
|
|
207
|
+
.map(e => configEngine.generateEntitySpec(e))
|
|
208
|
+
.filter(s => !s.embedded && !s.parent && !s.system_entity)
|
|
209
|
+
.sort((a, b) => (a.order || 999) - (b.order || 999))
|
|
210
|
+
.map(s => ({
|
|
211
|
+
name: s.name,
|
|
212
|
+
label: s.labelPlural || s.label,
|
|
213
|
+
icon: s.icon,
|
|
214
|
+
href: `/${s.name}`,
|
|
215
|
+
badge: s.badge,
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
// Filter by user permissions if provided
|
|
219
|
+
if (user && configEngine.getRolePermissions) {
|
|
220
|
+
return items.filter(item => {
|
|
221
|
+
const spec = configEngine.generateEntitySpec(item.name);
|
|
222
|
+
const perms = configEngine.getRolePermissions(user.role, item.name);
|
|
223
|
+
return perms.includes('list') || perms.includes('view') || perms.includes('all');
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return items;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error('[spec-helpers] buildNavigation error:', error.message);
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Engine - Lucia session management and Google OAuth
|
|
3
|
+
* Simplified extraction from moonlanding/src/engine.server.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Lucia } from 'lucia';
|
|
7
|
+
import { BetterSqlite3Adapter } from '@lucia-auth/adapter-sqlite';
|
|
8
|
+
import { Google } from 'arctic';
|
|
9
|
+
import bcrypt from 'bcrypt';
|
|
10
|
+
import { getDatabase } from './database-core.js';
|
|
11
|
+
import { buildConfig, hasGoogleAuth } from '../config/env.js';
|
|
12
|
+
|
|
13
|
+
let _lucia = null;
|
|
14
|
+
let _google = null;
|
|
15
|
+
let _adapter = null;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize auth engine with database and config
|
|
19
|
+
* @param {object} [config] - Optional config override
|
|
20
|
+
*/
|
|
21
|
+
export function initAuth(config = null) {
|
|
22
|
+
const cfg = config || buildConfig();
|
|
23
|
+
const db = getDatabase(cfg.db.path);
|
|
24
|
+
|
|
25
|
+
_adapter = new BetterSqlite3Adapter(db, { user: 'users', session: 'sessions' });
|
|
26
|
+
|
|
27
|
+
_lucia = new Lucia(_adapter, {
|
|
28
|
+
sessionCookie: {
|
|
29
|
+
expires: cfg.auth.session.expires,
|
|
30
|
+
attributes: {
|
|
31
|
+
secure: cfg.auth.session.secure,
|
|
32
|
+
httpOnly: true,
|
|
33
|
+
sameSite: 'lax',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
getUserAttributes: (row) => ({
|
|
37
|
+
id: row.id,
|
|
38
|
+
email: row.email,
|
|
39
|
+
name: row.name,
|
|
40
|
+
avatar: row.avatar,
|
|
41
|
+
type: row.type,
|
|
42
|
+
role: row.role,
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (hasGoogleAuth(cfg)) {
|
|
47
|
+
_google = new Google(
|
|
48
|
+
cfg.auth.google.clientId,
|
|
49
|
+
cfg.auth.google.clientSecret,
|
|
50
|
+
cfg.auth.google.redirectUri
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { lucia: _lucia, google: _google };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get Lucia instance
|
|
59
|
+
* @returns {Lucia}
|
|
60
|
+
*/
|
|
61
|
+
export function getLucia() {
|
|
62
|
+
if (!_lucia) initAuth();
|
|
63
|
+
return _lucia;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get Google OAuth client
|
|
68
|
+
* @returns {Google|null}
|
|
69
|
+
*/
|
|
70
|
+
export function getGoogle() {
|
|
71
|
+
if (!_google) initAuth();
|
|
72
|
+
return _google;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parse cookies from header
|
|
77
|
+
* @param {string} cookieHeader
|
|
78
|
+
* @returns {object}
|
|
79
|
+
*/
|
|
80
|
+
function parseCookies(cookieHeader) {
|
|
81
|
+
const cookies = {};
|
|
82
|
+
if (!cookieHeader) return cookies;
|
|
83
|
+
cookieHeader.split(';').forEach(cookie => {
|
|
84
|
+
const [name, value] = cookie.trim().split('=');
|
|
85
|
+
if (name && value) cookies[name] = decodeURIComponent(value);
|
|
86
|
+
});
|
|
87
|
+
return cookies;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let _currentRequest = null;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Set current request context (for auth)
|
|
94
|
+
* @param {object} req
|
|
95
|
+
*/
|
|
96
|
+
export function setCurrentRequest(req) {
|
|
97
|
+
_currentRequest = req;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get current user from session
|
|
102
|
+
* @returns {object|null}
|
|
103
|
+
*/
|
|
104
|
+
export async function getUser() {
|
|
105
|
+
try {
|
|
106
|
+
const request = _currentRequest;
|
|
107
|
+
if (!request) return null;
|
|
108
|
+
|
|
109
|
+
const lucia = getLucia();
|
|
110
|
+
const cookies = parseCookies(request.headers?.cookie || '');
|
|
111
|
+
const sessionId = cookies[lucia.sessionCookieName];
|
|
112
|
+
if (!sessionId) return null;
|
|
113
|
+
|
|
114
|
+
const { user, session } = await lucia.validateSession(sessionId);
|
|
115
|
+
if (!user || !session) return null;
|
|
116
|
+
|
|
117
|
+
return user;
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Require user (throw if not authenticated)
|
|
125
|
+
* @returns {Promise<object>}
|
|
126
|
+
*/
|
|
127
|
+
export async function requireUser() {
|
|
128
|
+
const user = await getUser();
|
|
129
|
+
if (!user) throw new Error('Unauthorized');
|
|
130
|
+
return user;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Create a new session
|
|
135
|
+
* @param {string} userId
|
|
136
|
+
* @returns {{session: object, sessionCookie: object}}
|
|
137
|
+
*/
|
|
138
|
+
export async function createSession(userId) {
|
|
139
|
+
const lucia = getLucia();
|
|
140
|
+
// Note: cookies() requires Next.js polyfill in moonlanding; for SDK we return session data
|
|
141
|
+
const session = await lucia.createSession(userId, {});
|
|
142
|
+
const sessionCookie = lucia.createSessionCookie(session.id);
|
|
143
|
+
return { session, sessionCookie };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Invalidate current session
|
|
148
|
+
*/
|
|
149
|
+
export async function invalidateSession() {
|
|
150
|
+
const lucia = getLucia();
|
|
151
|
+
// Simplified - caller handles cookie clearing
|
|
152
|
+
// In full app this uses @lucia-auth/adapter-sqlite's cookie management
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Hash password with bcrypt
|
|
157
|
+
* @param {string} password
|
|
158
|
+
* @returns {Promise<string>}
|
|
159
|
+
*/
|
|
160
|
+
export async function hashPassword(password) {
|
|
161
|
+
return bcrypt.hash(password, 12);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Verify password
|
|
166
|
+
* @param {string} password
|
|
167
|
+
* @param {string} hash
|
|
168
|
+
* @returns {Promise<boolean>}
|
|
169
|
+
*/
|
|
170
|
+
export async function verifyPassword(password, hash) {
|
|
171
|
+
return bcrypt.compare(password, hash);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get user by email
|
|
176
|
+
* @param {string} email
|
|
177
|
+
* @returns {object|null}
|
|
178
|
+
*/
|
|
179
|
+
export function getUserByEmail(email) {
|
|
180
|
+
const db = getDatabase();
|
|
181
|
+
return db.prepare('SELECT * FROM users WHERE email = ?').get(email);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Create a new user
|
|
186
|
+
* @param {object} userData
|
|
187
|
+
* @returns {object}
|
|
188
|
+
*/
|
|
189
|
+
export async function createUser(userData) {
|
|
190
|
+
const hashedPassword = await hashPassword(userData.password);
|
|
191
|
+
const { create } = await import('./query-engine-write.js');
|
|
192
|
+
return create('user', {
|
|
193
|
+
...userData,
|
|
194
|
+
password: hashedPassword,
|
|
195
|
+
}, { id: 'system' });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Authenticate user with credentials
|
|
200
|
+
* @param {string} email
|
|
201
|
+
* @param {string} password
|
|
202
|
+
* @returns {object|null} User if valid, null otherwise
|
|
203
|
+
*/
|
|
204
|
+
export async function authenticate(email, password) {
|
|
205
|
+
const user = getUserByEmail(email);
|
|
206
|
+
if (!user) return null;
|
|
207
|
+
|
|
208
|
+
const valid = await verifyPassword(password, user.password);
|
|
209
|
+
if (!valid) return null;
|
|
210
|
+
|
|
211
|
+
return user;
|
|
212
|
+
}
|