@intranefr/superbackend 1.4.3
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/.commiat +4 -0
- package/.env.example +47 -0
- package/README.md +110 -0
- package/index.js +94 -0
- package/package.json +67 -0
- package/public/css/styles.css +139 -0
- package/public/js/animations.js +41 -0
- package/sdk/error-tracking/browser/package.json +16 -0
- package/sdk/error-tracking/browser/src/core.js +270 -0
- package/sdk/error-tracking/browser/src/embed.js +18 -0
- package/sdk/error-tracking/browser/src/index.js +1 -0
- package/server.js +5 -0
- package/src/admin/endpointRegistry.js +300 -0
- package/src/controllers/admin.controller.js +321 -0
- package/src/controllers/adminAssets.controller.js +530 -0
- package/src/controllers/adminAssetsStorage.controller.js +260 -0
- package/src/controllers/adminEjsVirtual.controller.js +354 -0
- package/src/controllers/adminFeatureFlags.controller.js +155 -0
- package/src/controllers/adminHeadless.controller.js +1071 -0
- package/src/controllers/adminI18n.controller.js +604 -0
- package/src/controllers/adminJsonConfigs.controller.js +97 -0
- package/src/controllers/adminLlm.controller.js +273 -0
- package/src/controllers/adminMigration.controller.js +257 -0
- package/src/controllers/adminSeoConfig.controller.js +515 -0
- package/src/controllers/adminStats.controller.js +121 -0
- package/src/controllers/adminUploadNamespaces.controller.js +208 -0
- package/src/controllers/assets.controller.js +248 -0
- package/src/controllers/auth.controller.js +93 -0
- package/src/controllers/billing.controller.js +223 -0
- package/src/controllers/featureFlags.controller.js +35 -0
- package/src/controllers/forms.controller.js +217 -0
- package/src/controllers/globalSettings.controller.js +252 -0
- package/src/controllers/headlessCrud.controller.js +126 -0
- package/src/controllers/i18n.controller.js +12 -0
- package/src/controllers/invite.controller.js +249 -0
- package/src/controllers/jsonConfigs.controller.js +19 -0
- package/src/controllers/metrics.controller.js +149 -0
- package/src/controllers/notificationAdmin.controller.js +264 -0
- package/src/controllers/notifications.controller.js +131 -0
- package/src/controllers/org.controller.js +357 -0
- package/src/controllers/orgAdmin.controller.js +491 -0
- package/src/controllers/stripeAdmin.controller.js +410 -0
- package/src/controllers/user.controller.js +361 -0
- package/src/controllers/userAdmin.controller.js +277 -0
- package/src/controllers/waitingList.controller.js +167 -0
- package/src/controllers/webhook.controller.js +200 -0
- package/src/middleware/auth.js +66 -0
- package/src/middleware/errorCapture.js +170 -0
- package/src/middleware/headlessApiTokenAuth.js +57 -0
- package/src/middleware/org.js +108 -0
- package/src/middleware.js +901 -0
- package/src/models/ActionEvent.js +31 -0
- package/src/models/ActivityLog.js +41 -0
- package/src/models/Asset.js +84 -0
- package/src/models/AuditEvent.js +93 -0
- package/src/models/EmailLog.js +28 -0
- package/src/models/ErrorAggregate.js +72 -0
- package/src/models/FormSubmission.js +41 -0
- package/src/models/GlobalSetting.js +38 -0
- package/src/models/HeadlessApiToken.js +24 -0
- package/src/models/HeadlessModelDefinition.js +41 -0
- package/src/models/I18nEntry.js +77 -0
- package/src/models/I18nLocale.js +33 -0
- package/src/models/Invite.js +70 -0
- package/src/models/JsonConfig.js +46 -0
- package/src/models/Notification.js +60 -0
- package/src/models/Organization.js +57 -0
- package/src/models/OrganizationMember.js +43 -0
- package/src/models/StripeCatalogItem.js +77 -0
- package/src/models/StripeWebhookEvent.js +57 -0
- package/src/models/User.js +89 -0
- package/src/models/VirtualEjsFile.js +60 -0
- package/src/models/VirtualEjsFileVersion.js +43 -0
- package/src/models/VirtualEjsGroupChange.js +32 -0
- package/src/models/WaitingList.js +41 -0
- package/src/models/Webhook.js +63 -0
- package/src/models/Workflow.js +29 -0
- package/src/models/WorkflowExecution.js +12 -0
- package/src/routes/admin.routes.js +26 -0
- package/src/routes/adminAssets.routes.js +28 -0
- package/src/routes/adminAssetsStorage.routes.js +13 -0
- package/src/routes/adminAudit.routes.js +196 -0
- package/src/routes/adminEjsVirtual.routes.js +17 -0
- package/src/routes/adminErrors.routes.js +164 -0
- package/src/routes/adminFeatureFlags.routes.js +12 -0
- package/src/routes/adminHeadless.routes.js +38 -0
- package/src/routes/adminI18n.routes.js +22 -0
- package/src/routes/adminJsonConfigs.routes.js +15 -0
- package/src/routes/adminLlm.routes.js +12 -0
- package/src/routes/adminMigration.routes.js +81 -0
- package/src/routes/adminSeoConfig.routes.js +20 -0
- package/src/routes/adminUploadNamespaces.routes.js +13 -0
- package/src/routes/assets.routes.js +21 -0
- package/src/routes/auth.routes.js +12 -0
- package/src/routes/billing.routes.js +11 -0
- package/src/routes/errorTracking.routes.js +31 -0
- package/src/routes/featureFlags.routes.js +9 -0
- package/src/routes/forms.routes.js +9 -0
- package/src/routes/formsAdmin.routes.js +13 -0
- package/src/routes/globalSettings.routes.js +18 -0
- package/src/routes/headless.routes.js +15 -0
- package/src/routes/i18n.routes.js +8 -0
- package/src/routes/invite.routes.js +9 -0
- package/src/routes/jsonConfigs.routes.js +8 -0
- package/src/routes/log.routes.js +111 -0
- package/src/routes/metrics.routes.js +9 -0
- package/src/routes/notificationAdmin.routes.js +15 -0
- package/src/routes/notifications.routes.js +12 -0
- package/src/routes/org.routes.js +31 -0
- package/src/routes/orgAdmin.routes.js +20 -0
- package/src/routes/publicAssets.routes.js +7 -0
- package/src/routes/stripeAdmin.routes.js +20 -0
- package/src/routes/user.routes.js +22 -0
- package/src/routes/userAdmin.routes.js +15 -0
- package/src/routes/waitingList.routes.js +13 -0
- package/src/routes/waitingListAdmin.routes.js +9 -0
- package/src/routes/webhook.routes.js +32 -0
- package/src/routes/workflowWebhook.routes.js +54 -0
- package/src/routes/workflows.routes.js +110 -0
- package/src/services/assets.service.js +110 -0
- package/src/services/audit.service.js +62 -0
- package/src/services/auditLogger.js +165 -0
- package/src/services/ejsVirtual.service.js +614 -0
- package/src/services/email.service.js +351 -0
- package/src/services/errorLogger.js +221 -0
- package/src/services/featureFlags.service.js +202 -0
- package/src/services/forms.service.js +214 -0
- package/src/services/globalSettings.service.js +49 -0
- package/src/services/headlessApiTokens.service.js +158 -0
- package/src/services/headlessCrypto.service.js +31 -0
- package/src/services/headlessModels.service.js +356 -0
- package/src/services/i18n.service.js +314 -0
- package/src/services/i18nInferredKeys.service.js +337 -0
- package/src/services/jsonConfigs.service.js +392 -0
- package/src/services/llm.service.js +749 -0
- package/src/services/migration.service.js +581 -0
- package/src/services/migrationAssets/fsLocal.js +58 -0
- package/src/services/migrationAssets/index.js +134 -0
- package/src/services/migrationAssets/s3.js +75 -0
- package/src/services/migrationAssets/sftp.js +92 -0
- package/src/services/notification.service.js +212 -0
- package/src/services/objectStorage.service.js +514 -0
- package/src/services/seoConfig.service.js +402 -0
- package/src/services/storage.js +150 -0
- package/src/services/stripe.service.js +185 -0
- package/src/services/stripeHelper.service.js +264 -0
- package/src/services/uploadNamespaces.service.js +326 -0
- package/src/services/webhook.service.js +157 -0
- package/src/services/workflow.service.js +271 -0
- package/src/utils/asyncHandler.js +5 -0
- package/src/utils/encryption.js +80 -0
- package/src/utils/jwt.js +40 -0
- package/src/utils/orgRoles.js +156 -0
- package/src/utils/validation.js +26 -0
- package/src/utils/webhookRetry.js +93 -0
- package/views/admin-assets.ejs +444 -0
- package/views/admin-audit.ejs +283 -0
- package/views/admin-coolify-deploy.ejs +207 -0
- package/views/admin-dashboard-home.ejs +291 -0
- package/views/admin-dashboard.ejs +397 -0
- package/views/admin-ejs-virtual.ejs +280 -0
- package/views/admin-errors.ejs +368 -0
- package/views/admin-feature-flags.ejs +390 -0
- package/views/admin-forms.ejs +526 -0
- package/views/admin-global-settings.ejs +436 -0
- package/views/admin-headless.ejs +2020 -0
- package/views/admin-i18n-locales.ejs +221 -0
- package/views/admin-i18n.ejs +728 -0
- package/views/admin-json-configs.ejs +410 -0
- package/views/admin-llm.ejs +884 -0
- package/views/admin-metrics.ejs +274 -0
- package/views/admin-migration.ejs +814 -0
- package/views/admin-notifications.ejs +430 -0
- package/views/admin-organizations.ejs +984 -0
- package/views/admin-seo-config.ejs +673 -0
- package/views/admin-stripe-pricing.ejs +558 -0
- package/views/admin-test.ejs +342 -0
- package/views/admin-users.ejs +452 -0
- package/views/admin-waiting-list.ejs +547 -0
- package/views/admin-webhooks.ejs +329 -0
- package/views/admin-workflows.ejs +310 -0
- package/views/partials/admin-assets-script.ejs +2022 -0
- package/views/partials/admin-test-sidebar.ejs +14 -0
- package/views/partials/dashboard/nav-items.ejs +66 -0
- package/views/partials/dashboard/palette.ejs +63 -0
- package/views/partials/dashboard/sidebar.ejs +21 -0
- package/views/partials/dashboard/tab-bar.ejs +26 -0
- package/views/partials/footer.ejs +3 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
const Workflow = require('../models/Workflow');
|
|
2
|
+
const WorkflowExecution = require('../models/WorkflowExecution');
|
|
3
|
+
const llmService = require('./llm.service');
|
|
4
|
+
const { NodeVM } = require('vm2');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Workflow Service
|
|
8
|
+
* Handles execution of stacked workflow nodes within SaaSBackend.
|
|
9
|
+
*/
|
|
10
|
+
class WorkflowService {
|
|
11
|
+
constructor(workflowId, initialContext = {}) {
|
|
12
|
+
this.workflowId = workflowId;
|
|
13
|
+
|
|
14
|
+
// Ensure initialContext is a clean object
|
|
15
|
+
const sanitizedContext = (initialContext && typeof initialContext === 'object') ? initialContext : {};
|
|
16
|
+
|
|
17
|
+
this.context = {
|
|
18
|
+
entrypoint: sanitizedContext,
|
|
19
|
+
payload: sanitizedContext, // for backward compatibility
|
|
20
|
+
nodes: {},
|
|
21
|
+
lastNode: {
|
|
22
|
+
method: sanitizedContext.method,
|
|
23
|
+
body: sanitizedContext.body || {},
|
|
24
|
+
query: sanitizedContext.query || {},
|
|
25
|
+
headers: sanitizedContext.headers || {}
|
|
26
|
+
},
|
|
27
|
+
env: process.env
|
|
28
|
+
};
|
|
29
|
+
this.executionLog = [];
|
|
30
|
+
this.status = 'pending';
|
|
31
|
+
this.startTime = Date.now();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async runNodeById(workflowId, nodeId, incomingContext) {
|
|
35
|
+
const workflow = await Workflow.findById(workflowId);
|
|
36
|
+
if (!workflow) throw new Error('Workflow not found');
|
|
37
|
+
const node = workflow.nodes.find(n => n.id === nodeId);
|
|
38
|
+
if (!node) throw new Error('Node not found');
|
|
39
|
+
|
|
40
|
+
// 1. Determine the entrypoint data (priority: incoming context > saved dataset)
|
|
41
|
+
const entrypoint = incomingContext.entrypoint || incomingContext.payload || workflow.testDataset || {};
|
|
42
|
+
|
|
43
|
+
// 2. Rebuild the context from scratch to ensure a clean state
|
|
44
|
+
// If incomingContext has lastNode, we use it, otherwise we default to entrypoint
|
|
45
|
+
this.context = {
|
|
46
|
+
...incomingContext,
|
|
47
|
+
entrypoint: entrypoint,
|
|
48
|
+
payload: entrypoint,
|
|
49
|
+
nodes: incomingContext.nodes || {},
|
|
50
|
+
lastNode: incomingContext.lastNode || {
|
|
51
|
+
method: entrypoint.method || 'POST',
|
|
52
|
+
body: entrypoint.body || {},
|
|
53
|
+
query: entrypoint.query || {},
|
|
54
|
+
headers: entrypoint.headers || {}
|
|
55
|
+
},
|
|
56
|
+
env: process.env
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return await this.executeNode(node);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async run() {
|
|
63
|
+
const workflow = await Workflow.findById(this.workflowId);
|
|
64
|
+
if (!workflow) throw new Error('Workflow not found');
|
|
65
|
+
|
|
66
|
+
// Initialize entrypoint and payload if not already set via constructor
|
|
67
|
+
if (!this.context.entrypoint || Object.keys(this.context.entrypoint).length === 0) {
|
|
68
|
+
const entrypoint = workflow.testDataset || {};
|
|
69
|
+
this.context.entrypoint = entrypoint;
|
|
70
|
+
this.context.payload = entrypoint;
|
|
71
|
+
this.context.lastNode = {
|
|
72
|
+
method: entrypoint.method,
|
|
73
|
+
body: entrypoint.body || {},
|
|
74
|
+
query: entrypoint.query || {},
|
|
75
|
+
headers: entrypoint.headers || {}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.status = 'running';
|
|
80
|
+
try {
|
|
81
|
+
await this.executeNodes(workflow.nodes);
|
|
82
|
+
this.status = 'completed';
|
|
83
|
+
} catch (err) {
|
|
84
|
+
this.status = 'failed';
|
|
85
|
+
this.executionLog.push({ type: 'error', message: err.message, timestamp: new Date() });
|
|
86
|
+
throw err;
|
|
87
|
+
} finally {
|
|
88
|
+
await this.saveExecution();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async executeNodes(nodes) {
|
|
93
|
+
for (const node of nodes) {
|
|
94
|
+
await this.executeNode(node);
|
|
95
|
+
if (node.type === 'exit') break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async executeNode(node) {
|
|
100
|
+
const nodeStartTime = Date.now();
|
|
101
|
+
let result = null;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
switch (node.type) {
|
|
105
|
+
case 'llm':
|
|
106
|
+
result = await this.handleLLM(node);
|
|
107
|
+
break;
|
|
108
|
+
case 'if':
|
|
109
|
+
result = await this.handleIf(node);
|
|
110
|
+
break;
|
|
111
|
+
case 'parallel':
|
|
112
|
+
result = await this.handleParallel(node);
|
|
113
|
+
break;
|
|
114
|
+
case 'http':
|
|
115
|
+
result = await this.handleHttp(node);
|
|
116
|
+
break;
|
|
117
|
+
case 'exit':
|
|
118
|
+
result = this.interpolateObject(node.body || {});
|
|
119
|
+
break;
|
|
120
|
+
default:
|
|
121
|
+
throw new Error(`Unknown node type: ${node.type}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Standardize lastNode as an object
|
|
125
|
+
const standardizedResult = (typeof result !== 'object' || result === null)
|
|
126
|
+
? { result: result }
|
|
127
|
+
: JSON.parse(JSON.stringify(result)); // Deep clone to prevent mutations
|
|
128
|
+
|
|
129
|
+
if (node.name) {
|
|
130
|
+
const reserved = new Set(['entrypoint', 'payload', 'nodes', 'lastNode', 'env', 'JSON', 'console', 'context']);
|
|
131
|
+
if (!reserved.has(node.name)) {
|
|
132
|
+
this.context[node.name] = standardizedResult;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (node.outputVar) {
|
|
136
|
+
this.context.nodes[node.outputVar] = standardizedResult;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.context.lastNode = standardizedResult;
|
|
140
|
+
|
|
141
|
+
this.executionLog.push({
|
|
142
|
+
nodeId: node.id,
|
|
143
|
+
nodeName: node.name,
|
|
144
|
+
type: node.type,
|
|
145
|
+
duration: Date.now() - nodeStartTime,
|
|
146
|
+
status: 'success',
|
|
147
|
+
result: standardizedResult,
|
|
148
|
+
timestamp: new Date()
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return result;
|
|
152
|
+
} catch (err) {
|
|
153
|
+
this.executionLog.push({
|
|
154
|
+
nodeId: node.id,
|
|
155
|
+
type: node.type,
|
|
156
|
+
status: 'error',
|
|
157
|
+
message: err.message,
|
|
158
|
+
timestamp: new Date()
|
|
159
|
+
});
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async handleLLM(node) {
|
|
165
|
+
const prompt = this.interpolate(node.prompt);
|
|
166
|
+
const response = await llmService.callAdhoc({
|
|
167
|
+
providerKey: node.provider || 'openrouter',
|
|
168
|
+
messages: [{ role: 'user', content: prompt }]
|
|
169
|
+
}, {
|
|
170
|
+
model: node.model || 'minimax/minimax-m2.1',
|
|
171
|
+
temperature: node.temperature !== undefined ? parseFloat(node.temperature) : 0.7
|
|
172
|
+
});
|
|
173
|
+
return response.content;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async handleIf(node) {
|
|
177
|
+
const vm = new NodeVM({
|
|
178
|
+
sandbox: {
|
|
179
|
+
...this.context,
|
|
180
|
+
context: this.context // for backward compatibility
|
|
181
|
+
},
|
|
182
|
+
timeout: 1000
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const match = vm.run(`module.exports = (${node.condition})`);
|
|
186
|
+
|
|
187
|
+
// Store branch outcome in context
|
|
188
|
+
this.context[`${node.id}_result`] = match ? 'then' : 'else';
|
|
189
|
+
|
|
190
|
+
if (match) {
|
|
191
|
+
return await this.executeNodes(node.then || []);
|
|
192
|
+
} else {
|
|
193
|
+
return await this.executeNodes(node.else || []);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async handleParallel(node) {
|
|
198
|
+
const results = await Promise.all((node.branches || []).map(branch => this.executeNodes(branch)));
|
|
199
|
+
return results;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async handleHttp(node) {
|
|
203
|
+
const url = this.interpolate(node.url);
|
|
204
|
+
const response = await fetch(url, {
|
|
205
|
+
method: node.method || 'GET',
|
|
206
|
+
headers: node.headers || {},
|
|
207
|
+
body: node.method !== 'GET' ? JSON.stringify(this.interpolateObject(node.body)) : undefined
|
|
208
|
+
});
|
|
209
|
+
return await response.json();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
interpolate(str) {
|
|
213
|
+
if (!str || typeof str !== 'string') return str;
|
|
214
|
+
return str.replace(/\{\{(.*?)\}\}/gs, (match, jsCode) => {
|
|
215
|
+
try {
|
|
216
|
+
const vm = new NodeVM({
|
|
217
|
+
sandbox: this.context,
|
|
218
|
+
timeout: 1000
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const val = vm.run(`module.exports = (${jsCode.trim()})`);
|
|
222
|
+
|
|
223
|
+
if (val === undefined || val === null) return '';
|
|
224
|
+
return typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
// Fallback to path resolution if JS eval fails
|
|
227
|
+
const parts = jsCode.trim().split('.');
|
|
228
|
+
let val = this.context;
|
|
229
|
+
for (const part of parts) {
|
|
230
|
+
if (val === null || val === undefined) return match;
|
|
231
|
+
val = val[part];
|
|
232
|
+
}
|
|
233
|
+
if (val === undefined || val === null) return match;
|
|
234
|
+
return typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
interpolateObject(obj) {
|
|
240
|
+
if (typeof obj === 'string') return this.interpolate(obj);
|
|
241
|
+
if (Array.isArray(obj)) return obj.map(item => this.interpolateObject(item));
|
|
242
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
243
|
+
const result = {};
|
|
244
|
+
for (const key in obj) {
|
|
245
|
+
result[key] = this.interpolateObject(obj[key]);
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
return obj;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async saveExecution() {
|
|
253
|
+
await WorkflowExecution.create({
|
|
254
|
+
workflowId: this.workflowId,
|
|
255
|
+
status: this.status,
|
|
256
|
+
context: this.context,
|
|
257
|
+
log: this.executionLog,
|
|
258
|
+
duration: Date.now() - this.startTime,
|
|
259
|
+
executedAt: new Date()
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = {
|
|
265
|
+
WorkflowService,
|
|
266
|
+
execute: async (workflowId, initialContext) => {
|
|
267
|
+
const service = new WorkflowService(workflowId, initialContext);
|
|
268
|
+
await service.run();
|
|
269
|
+
return service;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
function getEncryptionKey() {
|
|
4
|
+
const raw = process.env.SAASBACKEND_ENCRYPTION_KEY;
|
|
5
|
+
if (!raw) {
|
|
6
|
+
throw new Error('SAASBACKEND_ENCRYPTION_KEY is required for encrypted settings');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let key;
|
|
10
|
+
if (/^[A-Fa-f0-9]{64}$/.test(raw)) {
|
|
11
|
+
key = Buffer.from(raw, 'hex');
|
|
12
|
+
} else {
|
|
13
|
+
try {
|
|
14
|
+
key = Buffer.from(raw, 'base64');
|
|
15
|
+
} catch (e) {
|
|
16
|
+
key = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!key || key.length !== 32) {
|
|
20
|
+
key = Buffer.from(raw, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (key.length !== 32) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
'SAASBACKEND_ENCRYPTION_KEY must be 32 bytes (base64-encoded 32 bytes, hex 64 chars, or 32-char utf8)',
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return key;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function encryptString(plaintext, { keyId = 'v1' } = {}) {
|
|
34
|
+
const key = getEncryptionKey();
|
|
35
|
+
const iv = crypto.randomBytes(12);
|
|
36
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
37
|
+
|
|
38
|
+
const ciphertext = Buffer.concat([
|
|
39
|
+
cipher.update(String(plaintext), 'utf8'),
|
|
40
|
+
cipher.final(),
|
|
41
|
+
]);
|
|
42
|
+
const tag = cipher.getAuthTag();
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
alg: 'aes-256-gcm',
|
|
46
|
+
keyId,
|
|
47
|
+
iv: iv.toString('base64'),
|
|
48
|
+
tag: tag.toString('base64'),
|
|
49
|
+
ciphertext: ciphertext.toString('base64'),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function decryptString(payload) {
|
|
54
|
+
if (!payload || typeof payload !== 'object') {
|
|
55
|
+
throw new Error('Invalid encrypted payload');
|
|
56
|
+
}
|
|
57
|
+
if (payload.alg !== 'aes-256-gcm') {
|
|
58
|
+
throw new Error('Unsupported encryption algorithm');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const key = getEncryptionKey();
|
|
62
|
+
const iv = Buffer.from(payload.iv, 'base64');
|
|
63
|
+
const tag = Buffer.from(payload.tag, 'base64');
|
|
64
|
+
const ciphertext = Buffer.from(payload.ciphertext, 'base64');
|
|
65
|
+
|
|
66
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
67
|
+
decipher.setAuthTag(tag);
|
|
68
|
+
|
|
69
|
+
const plaintext = Buffer.concat([
|
|
70
|
+
decipher.update(ciphertext),
|
|
71
|
+
decipher.final(),
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
return plaintext.toString('utf8');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
encryptString,
|
|
79
|
+
decryptString,
|
|
80
|
+
};
|
package/src/utils/jwt.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const jwt = require('jsonwebtoken');
|
|
2
|
+
|
|
3
|
+
const generateAccessToken = (userId, role = 'user') => {
|
|
4
|
+
return jwt.sign(
|
|
5
|
+
{ userId, role },
|
|
6
|
+
process.env.JWT_ACCESS_SECRET || 'access-secret-change-me',
|
|
7
|
+
{ expiresIn: '30d' }
|
|
8
|
+
);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const generateRefreshToken = (userId) => {
|
|
12
|
+
return jwt.sign(
|
|
13
|
+
{ userId },
|
|
14
|
+
process.env.JWT_REFRESH_SECRET || 'refresh-secret-change-me',
|
|
15
|
+
{ expiresIn: '30d' }
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const verifyAccessToken = (token) => {
|
|
20
|
+
try {
|
|
21
|
+
return jwt.verify(token, process.env.JWT_ACCESS_SECRET || 'access-secret-change-me');
|
|
22
|
+
} catch (error) {
|
|
23
|
+
throw new Error('Invalid or expired token');
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const verifyRefreshToken = (token) => {
|
|
28
|
+
try {
|
|
29
|
+
return jwt.verify(token, process.env.JWT_REFRESH_SECRET || 'refresh-secret-change-me');
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw new Error('Invalid or expired refresh token');
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
generateAccessToken,
|
|
37
|
+
generateRefreshToken,
|
|
38
|
+
verifyAccessToken,
|
|
39
|
+
verifyRefreshToken
|
|
40
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const globalSettingsService = require('../services/globalSettings.service');
|
|
2
|
+
|
|
3
|
+
const DEFAULT_ROLE_HIERARCHY = {
|
|
4
|
+
owner: 4,
|
|
5
|
+
admin: 3,
|
|
6
|
+
member: 2,
|
|
7
|
+
viewer: 1,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const CACHE_TTL_MS = 60_000;
|
|
11
|
+
let cached = null;
|
|
12
|
+
|
|
13
|
+
function parseRoleHierarchy(value) {
|
|
14
|
+
if (!value) return null;
|
|
15
|
+
|
|
16
|
+
let parsed;
|
|
17
|
+
if (typeof value === 'string') {
|
|
18
|
+
const trimmed = value.trim();
|
|
19
|
+
if (!trimmed) return null;
|
|
20
|
+
parsed = JSON.parse(trimmed);
|
|
21
|
+
} else {
|
|
22
|
+
parsed = value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (Array.isArray(parsed)) {
|
|
26
|
+
const out = {};
|
|
27
|
+
for (const entry of parsed) {
|
|
28
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
29
|
+
const key = String(entry.key || '').trim();
|
|
30
|
+
const level = Number(entry.level);
|
|
31
|
+
if (!key || !Number.isFinite(level)) continue;
|
|
32
|
+
out[key] = level;
|
|
33
|
+
}
|
|
34
|
+
return Object.keys(out).length ? out : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (parsed && typeof parsed === 'object') {
|
|
38
|
+
const out = {};
|
|
39
|
+
for (const [keyRaw, levelRaw] of Object.entries(parsed)) {
|
|
40
|
+
const key = String(keyRaw || '').trim();
|
|
41
|
+
const level = Number(levelRaw);
|
|
42
|
+
if (!key || !Number.isFinite(level)) continue;
|
|
43
|
+
out[key] = level;
|
|
44
|
+
}
|
|
45
|
+
return Object.keys(out).length ? out : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeRoleHierarchyOrDefault(input) {
|
|
52
|
+
const roles = input || DEFAULT_ROLE_HIERARCHY;
|
|
53
|
+
|
|
54
|
+
// Ensure owner/admin semantics remain sensible if they exist
|
|
55
|
+
const normalized = { ...roles };
|
|
56
|
+
|
|
57
|
+
// Ensure no negative/zero levels
|
|
58
|
+
for (const [k, v] of Object.entries(normalized)) {
|
|
59
|
+
const level = Number(v);
|
|
60
|
+
if (!Number.isFinite(level) || level <= 0) {
|
|
61
|
+
delete normalized[k];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!Object.keys(normalized).length) {
|
|
66
|
+
return { ...DEFAULT_ROLE_HIERARCHY };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return normalized;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getDefaultRoleFromHierarchy(hierarchy) {
|
|
73
|
+
const entries = Object.entries(hierarchy);
|
|
74
|
+
if (!entries.length) return 'member';
|
|
75
|
+
|
|
76
|
+
entries.sort((a, b) => a[1] - b[1]);
|
|
77
|
+
return entries[0][0] || 'member';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function loadRoleHierarchy() {
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
if (cached && now - cached.timestamp < CACHE_TTL_MS) {
|
|
83
|
+
return cached.value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let raw = process.env.ORG_ROLES_JSON || null;
|
|
87
|
+
|
|
88
|
+
if (!raw) {
|
|
89
|
+
try {
|
|
90
|
+
raw = await globalSettingsService.getSettingValue('ORG_ROLES_JSON', null);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
raw = null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let hierarchy = null;
|
|
97
|
+
try {
|
|
98
|
+
hierarchy = normalizeRoleHierarchyOrDefault(parseRoleHierarchy(raw));
|
|
99
|
+
} catch (e) {
|
|
100
|
+
hierarchy = { ...DEFAULT_ROLE_HIERARCHY };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const allowedRoles = Object.keys(hierarchy);
|
|
104
|
+
const defaultRole = getDefaultRoleFromHierarchy(hierarchy);
|
|
105
|
+
|
|
106
|
+
const value = {
|
|
107
|
+
hierarchy,
|
|
108
|
+
allowedRoles,
|
|
109
|
+
defaultRole,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
cached = { value, timestamp: now };
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function getOrgRoleHierarchy() {
|
|
117
|
+
const { hierarchy } = await loadRoleHierarchy();
|
|
118
|
+
return hierarchy;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function getAllowedOrgRoles() {
|
|
122
|
+
const { allowedRoles } = await loadRoleHierarchy();
|
|
123
|
+
return allowedRoles;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function getDefaultOrgRole() {
|
|
127
|
+
const { defaultRole } = await loadRoleHierarchy();
|
|
128
|
+
return defaultRole;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function isValidOrgRole(role) {
|
|
132
|
+
const r = String(role || '').trim();
|
|
133
|
+
if (!r) return false;
|
|
134
|
+
const { hierarchy } = await loadRoleHierarchy();
|
|
135
|
+
return Boolean(hierarchy[r]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function getOrgRoleLevel(role) {
|
|
139
|
+
const r = String(role || '').trim();
|
|
140
|
+
if (!r) return 0;
|
|
141
|
+
const { hierarchy } = await loadRoleHierarchy();
|
|
142
|
+
return hierarchy[r] || 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function clearOrgRolesCache() {
|
|
146
|
+
cached = null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
getOrgRoleHierarchy,
|
|
151
|
+
getAllowedOrgRoles,
|
|
152
|
+
getDefaultOrgRole,
|
|
153
|
+
isValidOrgRole,
|
|
154
|
+
getOrgRoleLevel,
|
|
155
|
+
clearOrgRolesCache,
|
|
156
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Email validation regex
|
|
2
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
3
|
+
|
|
4
|
+
// Validate email format
|
|
5
|
+
const validateEmail = (email) => {
|
|
6
|
+
if (!email || typeof email !== 'string') return false;
|
|
7
|
+
return emailRegex.test(email.trim());
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Validate password strength
|
|
11
|
+
const validatePassword = (password) => {
|
|
12
|
+
if (!password || typeof password !== 'string') return false;
|
|
13
|
+
return password.length >= 8;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Sanitize input string
|
|
17
|
+
const sanitizeString = (str) => {
|
|
18
|
+
if (!str || typeof str !== 'string') return '';
|
|
19
|
+
return str.trim().replace(/[<>]/g, '');
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
validateEmail,
|
|
24
|
+
validatePassword,
|
|
25
|
+
sanitizeString
|
|
26
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const StripeWebhookEvent = require("../models/StripeWebhookEvent");
|
|
2
|
+
const stripeService = require("../services/stripe.service");
|
|
3
|
+
|
|
4
|
+
const MAX_RETRIES = 3;
|
|
5
|
+
|
|
6
|
+
async function retryFailedWebhooks(options = {}) {
|
|
7
|
+
const { limit = 10, maxRetries = MAX_RETRIES } = options;
|
|
8
|
+
|
|
9
|
+
const failedEvents = await StripeWebhookEvent.find({
|
|
10
|
+
status: "failed",
|
|
11
|
+
retryCount: { $lt: maxRetries }
|
|
12
|
+
})
|
|
13
|
+
.sort({ receivedAt: 1 })
|
|
14
|
+
.limit(limit);
|
|
15
|
+
|
|
16
|
+
const results = {
|
|
17
|
+
total: failedEvents.length,
|
|
18
|
+
succeeded: 0,
|
|
19
|
+
failed: 0,
|
|
20
|
+
errors: []
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
for (const event of failedEvents) {
|
|
24
|
+
try {
|
|
25
|
+
await processWebhookEvent(event);
|
|
26
|
+
|
|
27
|
+
event.status = "processed";
|
|
28
|
+
event.processedAt = new Date();
|
|
29
|
+
await event.save();
|
|
30
|
+
|
|
31
|
+
results.succeeded++;
|
|
32
|
+
console.log(`Retry succeeded for event ${event.stripeEventId}`);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
event.retryCount++;
|
|
35
|
+
event.processingErrors.push({
|
|
36
|
+
message: err.message,
|
|
37
|
+
timestamp: new Date()
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (event.retryCount >= maxRetries) {
|
|
41
|
+
console.error(`Max retries reached for event ${event.stripeEventId}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await event.save();
|
|
45
|
+
results.failed++;
|
|
46
|
+
results.errors.push({
|
|
47
|
+
eventId: event.stripeEventId,
|
|
48
|
+
error: err.message
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return results;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function processWebhookEvent(webhookEvent) {
|
|
57
|
+
const eventData = webhookEvent.data;
|
|
58
|
+
const previousAttributes = webhookEvent.previousAttributes;
|
|
59
|
+
|
|
60
|
+
switch (webhookEvent.eventType) {
|
|
61
|
+
case "checkout.session.completed":
|
|
62
|
+
await stripeService.handleCheckoutSessionCompleted(eventData);
|
|
63
|
+
break;
|
|
64
|
+
|
|
65
|
+
case "customer.subscription.created":
|
|
66
|
+
await stripeService.handleSubscriptionCreated(eventData);
|
|
67
|
+
break;
|
|
68
|
+
|
|
69
|
+
case "customer.subscription.updated":
|
|
70
|
+
await stripeService.handleSubscriptionUpdated(eventData, previousAttributes);
|
|
71
|
+
break;
|
|
72
|
+
|
|
73
|
+
case "customer.subscription.deleted":
|
|
74
|
+
await stripeService.handleSubscriptionDeleted(eventData);
|
|
75
|
+
break;
|
|
76
|
+
|
|
77
|
+
case "invoice.payment_succeeded":
|
|
78
|
+
await stripeService.handleInvoicePaymentSucceeded(eventData);
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case "invoice.payment_failed":
|
|
82
|
+
await stripeService.handleInvoicePaymentFailed(eventData);
|
|
83
|
+
break;
|
|
84
|
+
|
|
85
|
+
default:
|
|
86
|
+
console.log(`Unhandled event type: ${webhookEvent.eventType}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
retryFailedWebhooks,
|
|
92
|
+
processWebhookEvent
|
|
93
|
+
};
|