@intranefr/superbackend 1.5.3 → 1.6.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/cookies.txt +6 -0
- package/cookies1.txt +6 -0
- package/cookies2.txt +6 -0
- package/cookies3.txt +6 -0
- package/cookies4.txt +5 -0
- package/cookies_old.txt +5 -0
- package/cookies_old_test.txt +6 -0
- package/cookies_super.txt +5 -0
- package/cookies_super_test.txt +6 -0
- package/cookies_test.txt +6 -0
- package/index.js +7 -0
- package/package.json +3 -1
- package/plugins/core-waiting-list-migration/README.md +118 -0
- package/plugins/core-waiting-list-migration/index.js +438 -0
- package/plugins/global-settings-presets/index.js +20 -0
- package/plugins/hello-cli/index.js +17 -0
- package/plugins/ui-components-seeder/components/suiAlert.js +212 -0
- package/plugins/ui-components-seeder/components/suiToast.js +186 -0
- package/plugins/ui-components-seeder/index.js +31 -0
- package/public/js/admin-ui-components-preview.js +281 -0
- package/public/js/admin-ui-components.js +408 -0
- package/public/js/llm-provider-model-picker.js +193 -0
- package/public/test-iframe-fix.html +63 -0
- package/public/test-iframe.html +14 -0
- package/src/admin/endpointRegistry.js +68 -0
- package/src/controllers/admin.controller.js +25 -5
- package/src/controllers/adminDataCleanup.controller.js +45 -0
- package/src/controllers/adminLlm.controller.js +0 -8
- package/src/controllers/adminLogin.controller.js +269 -0
- package/src/controllers/adminPlugins.controller.js +55 -0
- package/src/controllers/adminRegistry.controller.js +106 -0
- package/src/controllers/adminStats.controller.js +4 -4
- package/src/controllers/registry.controller.js +32 -0
- package/src/controllers/waitingList.controller.js +52 -74
- package/src/middleware/auth.js +71 -1
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +454 -153
- package/src/models/GlobalSetting.js +11 -1
- package/src/models/UiComponent.js +2 -0
- package/src/models/User.js +1 -1
- package/src/routes/admin.routes.js +3 -3
- package/src/routes/adminAgents.routes.js +2 -2
- package/src/routes/adminAssets.routes.js +11 -11
- package/src/routes/adminBlog.routes.js +2 -2
- package/src/routes/adminBlogAi.routes.js +2 -2
- package/src/routes/adminBlogAutomation.routes.js +2 -2
- package/src/routes/adminCache.routes.js +2 -2
- package/src/routes/adminConsoleManager.routes.js +2 -2
- package/src/routes/adminCrons.routes.js +2 -2
- package/src/routes/adminDataCleanup.routes.js +26 -0
- package/src/routes/adminDbBrowser.routes.js +2 -2
- package/src/routes/adminEjsVirtual.routes.js +2 -2
- package/src/routes/adminFeatureFlags.routes.js +6 -6
- package/src/routes/adminHeadless.routes.js +2 -2
- package/src/routes/adminHealthChecks.routes.js +2 -2
- package/src/routes/adminI18n.routes.js +2 -2
- package/src/routes/adminJsonConfigs.routes.js +8 -8
- package/src/routes/adminLlm.routes.js +8 -8
- package/src/routes/adminLogin.routes.js +23 -0
- package/src/routes/adminMarkdowns.routes.js +3 -9
- package/src/routes/adminMigration.routes.js +12 -12
- package/src/routes/adminPages.routes.js +2 -2
- package/src/routes/adminPlugins.routes.js +15 -0
- package/src/routes/adminProxy.routes.js +2 -2
- package/src/routes/adminRateLimits.routes.js +8 -8
- package/src/routes/adminRbac.routes.js +2 -2
- package/src/routes/adminRegistry.routes.js +24 -0
- package/src/routes/adminScripts.routes.js +2 -2
- package/src/routes/adminSeoConfig.routes.js +10 -10
- package/src/routes/adminTelegram.routes.js +2 -2
- package/src/routes/adminTerminals.routes.js +2 -2
- package/src/routes/adminUiComponents.routes.js +2 -2
- package/src/routes/adminUploadNamespaces.routes.js +7 -7
- package/src/routes/blogInternal.routes.js +2 -2
- package/src/routes/experiments.routes.js +2 -2
- package/src/routes/formsAdmin.routes.js +6 -6
- package/src/routes/globalSettings.routes.js +8 -8
- package/src/routes/internalExperiments.routes.js +2 -2
- package/src/routes/notificationAdmin.routes.js +7 -7
- package/src/routes/orgAdmin.routes.js +16 -16
- package/src/routes/pages.routes.js +3 -3
- package/src/routes/registry.routes.js +11 -0
- package/src/routes/stripeAdmin.routes.js +12 -12
- package/src/routes/userAdmin.routes.js +7 -7
- package/src/routes/waitingListAdmin.routes.js +2 -2
- package/src/routes/workflows.routes.js +3 -3
- package/src/services/dataCleanup.service.js +286 -0
- package/src/services/jsonConfigs.service.js +262 -0
- package/src/services/plugins.service.js +348 -0
- package/src/services/registry.service.js +452 -0
- package/src/services/uiComponents.service.js +180 -0
- package/src/services/waitingListJson.service.js +401 -0
- package/src/utils/rbac/rightsRegistry.js +118 -0
- package/test-access.js +63 -0
- package/test-iframe-fix.html +63 -0
- package/test-iframe.html +14 -0
- package/views/admin-403.ejs +92 -0
- package/views/admin-dashboard-home.ejs +52 -2
- package/views/admin-dashboard.ejs +143 -2
- package/views/admin-data-cleanup.ejs +357 -0
- package/views/admin-login.ejs +286 -0
- package/views/admin-plugins-system.ejs +223 -0
- package/views/admin-ui-components.ejs +82 -402
- package/views/admin-users.ejs +207 -11
- package/views/partials/dashboard/nav-items.ejs +2 -0
- package/views/partials/llm-provider-model-picker.ejs +0 -161
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const {
|
|
3
|
+
getJsonConfigValueBySlug,
|
|
4
|
+
updateJsonConfigValueBySlug,
|
|
5
|
+
clearJsonConfigCacheByPattern,
|
|
6
|
+
isJsonConfigCached,
|
|
7
|
+
getJsonConfigCacheInfo
|
|
8
|
+
} = require('./jsonConfigs.service');
|
|
9
|
+
|
|
10
|
+
const WAITING_LIST_ENTRIES_KEY = 'waiting-list-entries';
|
|
11
|
+
const WAITING_LIST_STATS_KEY = 'waiting-list-stats';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Waiting List JSON Service
|
|
15
|
+
* Manages waiting list data using JSON Configs system with enhanced caching
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
function generateId() {
|
|
19
|
+
return crypto.randomBytes(16).toString('hex');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeEmail(email) {
|
|
23
|
+
return String(email || '').trim().toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function validateEntry(entry) {
|
|
27
|
+
if (!entry || typeof entry !== 'object') {
|
|
28
|
+
const err = new Error('Entry must be an object');
|
|
29
|
+
err.code = 'VALIDATION';
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!entry.email || typeof entry.email !== 'string') {
|
|
34
|
+
const err = new Error('Email is required');
|
|
35
|
+
err.code = 'VALIDATION';
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const normalizedEmail = normalizeEmail(entry.email);
|
|
40
|
+
if (!normalizedEmail.includes('@')) {
|
|
41
|
+
const err = new Error('Invalid email format');
|
|
42
|
+
err.code = 'VALIDATION';
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!entry.type || typeof entry.type !== 'string') {
|
|
47
|
+
const err = new Error('Type is required');
|
|
48
|
+
err.code = 'VALIDATION';
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
id: entry.id || generateId(),
|
|
54
|
+
email: normalizedEmail,
|
|
55
|
+
type: String(entry.type).trim(),
|
|
56
|
+
status: entry.status || 'active',
|
|
57
|
+
referralSource: entry.referralSource || 'website',
|
|
58
|
+
createdAt: entry.createdAt || new Date().toISOString(),
|
|
59
|
+
updatedAt: entry.updatedAt || new Date().toISOString()
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get waiting list entries with caching
|
|
65
|
+
*/
|
|
66
|
+
async function getWaitingListEntries(options = {}) {
|
|
67
|
+
try {
|
|
68
|
+
const data = await getJsonConfigValueBySlug(WAITING_LIST_ENTRIES_KEY, {
|
|
69
|
+
bypassCache: options.bypassCache
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
entries: Array.isArray(data.entries) ? data.entries : [],
|
|
74
|
+
lastUpdated: data.lastUpdated || null
|
|
75
|
+
};
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error.code === 'NOT_FOUND') {
|
|
78
|
+
return { entries: [], lastUpdated: null };
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Add new waiting list entry with automatic cache invalidation
|
|
86
|
+
*/
|
|
87
|
+
async function addWaitingListEntry(entryData) {
|
|
88
|
+
const validatedEntry = validateEntry(entryData);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const result = await updateJsonConfigValueBySlug(WAITING_LIST_ENTRIES_KEY, (currentData) => {
|
|
92
|
+
const data = currentData || { entries: [] };
|
|
93
|
+
const entries = Array.isArray(data.entries) ? data.entries : [];
|
|
94
|
+
|
|
95
|
+
// Check for duplicate email
|
|
96
|
+
const existingEntry = entries.find(e =>
|
|
97
|
+
normalizeEmail(e.email) === normalizeEmail(validatedEntry.email)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (existingEntry) {
|
|
101
|
+
const err = new Error('This email is already on our waiting list');
|
|
102
|
+
err.code = 'DUPLICATE_EMAIL';
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Add new entry
|
|
107
|
+
entries.push(validatedEntry);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
...data,
|
|
111
|
+
entries,
|
|
112
|
+
lastUpdated: new Date().toISOString()
|
|
113
|
+
};
|
|
114
|
+
}, { invalidateCache: true });
|
|
115
|
+
|
|
116
|
+
// Clear stats cache since data changed
|
|
117
|
+
clearWaitingListCache();
|
|
118
|
+
|
|
119
|
+
return validatedEntry;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
// If the config doesn't exist, initialize it first
|
|
122
|
+
if (error.code === 'NOT_FOUND') {
|
|
123
|
+
await initializeWaitingListData();
|
|
124
|
+
|
|
125
|
+
// Retry the operation after initialization (only once)
|
|
126
|
+
try {
|
|
127
|
+
return await addWaitingListEntry(entryData);
|
|
128
|
+
} catch (retryError) {
|
|
129
|
+
// If we still get NOT_FOUND, something went wrong with initialization
|
|
130
|
+
if (retryError.code === 'NOT_FOUND') {
|
|
131
|
+
const err = new Error('Failed to initialize waiting list data structure');
|
|
132
|
+
err.code = 'INITIALIZATION_FAILED';
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
throw retryError;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Update waiting list entry
|
|
144
|
+
*/
|
|
145
|
+
async function updateWaitingListEntry(entryId, updates) {
|
|
146
|
+
if (!entryId) {
|
|
147
|
+
const err = new Error('Entry ID is required');
|
|
148
|
+
err.code = 'VALIDATION';
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const result = await updateJsonConfigValueBySlug(WAITING_LIST_ENTRIES_KEY, (currentData) => {
|
|
153
|
+
const data = currentData || { entries: [] };
|
|
154
|
+
const entries = Array.isArray(data.entries) ? data.entries : [];
|
|
155
|
+
|
|
156
|
+
const entryIndex = entries.findIndex(e => e.id === entryId);
|
|
157
|
+
if (entryIndex === -1) {
|
|
158
|
+
const err = new Error('Waiting list entry not found');
|
|
159
|
+
err.code = 'NOT_FOUND';
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Update entry with validation
|
|
164
|
+
const updatedEntry = validateEntry({
|
|
165
|
+
...entries[entryIndex],
|
|
166
|
+
...updates,
|
|
167
|
+
id: entryId, // Preserve original ID
|
|
168
|
+
updatedAt: new Date().toISOString()
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
entries[entryIndex] = updatedEntry;
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
...data,
|
|
175
|
+
entries,
|
|
176
|
+
lastUpdated: new Date().toISOString()
|
|
177
|
+
};
|
|
178
|
+
}, { invalidateCache: true });
|
|
179
|
+
|
|
180
|
+
// Clear stats cache since data changed
|
|
181
|
+
clearWaitingListCache();
|
|
182
|
+
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Remove waiting list entry
|
|
188
|
+
*/
|
|
189
|
+
async function removeWaitingListEntry(entryId) {
|
|
190
|
+
if (!entryId) {
|
|
191
|
+
const err = new Error('Entry ID is required');
|
|
192
|
+
err.code = 'VALIDATION';
|
|
193
|
+
throw err;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const result = await updateJsonConfigValueBySlug(WAITING_LIST_ENTRIES_KEY, (currentData) => {
|
|
197
|
+
const data = currentData || { entries: [] };
|
|
198
|
+
const entries = Array.isArray(data.entries) ? data.entries : [];
|
|
199
|
+
|
|
200
|
+
const entryIndex = entries.findIndex(e => e.id === entryId);
|
|
201
|
+
if (entryIndex === -1) {
|
|
202
|
+
const err = new Error('Waiting list entry not found');
|
|
203
|
+
err.code = 'NOT_FOUND';
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Remove entry
|
|
208
|
+
entries.splice(entryIndex, 1);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
...data,
|
|
212
|
+
entries,
|
|
213
|
+
lastUpdated: new Date().toISOString()
|
|
214
|
+
};
|
|
215
|
+
}, { invalidateCache: true });
|
|
216
|
+
|
|
217
|
+
// Clear stats cache since data changed
|
|
218
|
+
clearWaitingListCache();
|
|
219
|
+
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get waiting list statistics with 5-minute TTL
|
|
225
|
+
*/
|
|
226
|
+
async function getWaitingListStats(options = {}) {
|
|
227
|
+
const ttlSeconds = options.ttlSeconds || 300; // 5 minutes default
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
// Try to get cached stats first
|
|
231
|
+
if (!options.bypassCache && isJsonConfigCached(WAITING_LIST_STATS_KEY)) {
|
|
232
|
+
const cacheInfo = getJsonConfigCacheInfo(WAITING_LIST_STATS_KEY);
|
|
233
|
+
if (cacheInfo.exists && cacheInfo.ttlRemaining > 0) {
|
|
234
|
+
return await getJsonConfigValueBySlug(WAITING_LIST_STATS_KEY);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Generate fresh stats
|
|
239
|
+
const { entries } = await getWaitingListEntries();
|
|
240
|
+
|
|
241
|
+
const activeEntries = entries.filter(e => e.status === 'active');
|
|
242
|
+
const totalSubscribers = activeEntries.length;
|
|
243
|
+
|
|
244
|
+
// Type aggregation
|
|
245
|
+
const typeAgg = activeEntries.reduce((acc, entry) => {
|
|
246
|
+
const type = String(entry.type || 'unknown').trim();
|
|
247
|
+
acc[type] = (acc[type] || 0) + 1;
|
|
248
|
+
return acc;
|
|
249
|
+
}, {});
|
|
250
|
+
|
|
251
|
+
// Backward compatibility fields
|
|
252
|
+
const buyerCount = (typeAgg.buyer || 0) + (typeAgg.both || 0);
|
|
253
|
+
const sellerCount = (typeAgg.seller || 0) + (typeAgg.both || 0);
|
|
254
|
+
|
|
255
|
+
// Mock growth data (same as original)
|
|
256
|
+
const growthThisWeek = Math.floor(totalSubscribers * 0.05);
|
|
257
|
+
|
|
258
|
+
const stats = {
|
|
259
|
+
totalSubscribers,
|
|
260
|
+
buyerCount,
|
|
261
|
+
sellerCount,
|
|
262
|
+
typeCounts: typeAgg,
|
|
263
|
+
growthThisWeek,
|
|
264
|
+
lastUpdated: new Date().toISOString()
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// Cache the stats
|
|
268
|
+
await updateJsonConfigValueBySlug(WAITING_LIST_STATS_KEY, () => stats, {
|
|
269
|
+
invalidateCache: false
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
return stats;
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (error.code === 'NOT_FOUND') {
|
|
275
|
+
// Return default stats if no data exists
|
|
276
|
+
return {
|
|
277
|
+
totalSubscribers: 0,
|
|
278
|
+
buyerCount: 0,
|
|
279
|
+
sellerCount: 0,
|
|
280
|
+
typeCounts: {},
|
|
281
|
+
growthThisWeek: 0,
|
|
282
|
+
lastUpdated: new Date().toISOString()
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get paginated waiting list entries for admin
|
|
291
|
+
*/
|
|
292
|
+
async function getWaitingListEntriesAdmin(filters = {}) {
|
|
293
|
+
const { status, type, email, limit = 50, offset = 0 } = filters;
|
|
294
|
+
|
|
295
|
+
const { entries } = await getWaitingListEntries();
|
|
296
|
+
|
|
297
|
+
// Apply filters
|
|
298
|
+
let filteredEntries = entries;
|
|
299
|
+
|
|
300
|
+
if (status) {
|
|
301
|
+
filteredEntries = filteredEntries.filter(e => e.status === status);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (type) {
|
|
305
|
+
filteredEntries = filteredEntries.filter(e => e.type === type);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (email) {
|
|
309
|
+
const searchEmail = normalizeEmail(email);
|
|
310
|
+
filteredEntries = filteredEntries.filter(e =>
|
|
311
|
+
normalizeEmail(e.email) === searchEmail
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Sort by creation date (newest first)
|
|
316
|
+
filteredEntries.sort((a, b) =>
|
|
317
|
+
new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime()
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// Apply pagination
|
|
321
|
+
const parsedLimit = Math.min(500, Math.max(1, parseInt(limit, 10) || 50));
|
|
322
|
+
const parsedOffset = Math.max(0, parseInt(offset, 10) || 0);
|
|
323
|
+
|
|
324
|
+
const paginatedEntries = filteredEntries.slice(parsedOffset, parsedOffset + parsedLimit);
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
entries: paginatedEntries,
|
|
328
|
+
pagination: {
|
|
329
|
+
total: filteredEntries.length,
|
|
330
|
+
limit: parsedLimit,
|
|
331
|
+
offset: parsedOffset
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Clear all waiting list related caches
|
|
338
|
+
*/
|
|
339
|
+
function clearWaitingListCache() {
|
|
340
|
+
return clearJsonConfigCacheByPattern('waiting-list-*');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Get waiting list cache information
|
|
345
|
+
*/
|
|
346
|
+
function getWaitingListCacheInfo() {
|
|
347
|
+
return {
|
|
348
|
+
entries: getJsonConfigCacheInfo(WAITING_LIST_ENTRIES_KEY),
|
|
349
|
+
stats: getJsonConfigCacheInfo(WAITING_LIST_STATS_KEY)
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Initialize waiting list data structure if it doesn't exist
|
|
355
|
+
*/
|
|
356
|
+
async function initializeWaitingListData() {
|
|
357
|
+
try {
|
|
358
|
+
await getJsonConfigValueBySlug(WAITING_LIST_ENTRIES_KEY, { bypassCache: true });
|
|
359
|
+
} catch (error) {
|
|
360
|
+
if (error.code === 'NOT_FOUND') {
|
|
361
|
+
// Create initial data structure
|
|
362
|
+
const { createJsonConfig } = require('./jsonConfigs.service');
|
|
363
|
+
await createJsonConfig({
|
|
364
|
+
title: 'Waiting List Entries',
|
|
365
|
+
alias: WAITING_LIST_ENTRIES_KEY,
|
|
366
|
+
jsonRaw: JSON.stringify({
|
|
367
|
+
entries: [],
|
|
368
|
+
lastUpdated: new Date().toISOString()
|
|
369
|
+
}),
|
|
370
|
+
publicEnabled: false,
|
|
371
|
+
cacheTtlSeconds: 300
|
|
372
|
+
});
|
|
373
|
+
} else {
|
|
374
|
+
throw error;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
module.exports = {
|
|
380
|
+
// Core operations
|
|
381
|
+
getWaitingListEntries,
|
|
382
|
+
addWaitingListEntry,
|
|
383
|
+
updateWaitingListEntry,
|
|
384
|
+
removeWaitingListEntry,
|
|
385
|
+
getWaitingListStats,
|
|
386
|
+
getWaitingListEntriesAdmin,
|
|
387
|
+
|
|
388
|
+
// Cache management
|
|
389
|
+
clearWaitingListCache,
|
|
390
|
+
getWaitingListCacheInfo,
|
|
391
|
+
|
|
392
|
+
// Utilities
|
|
393
|
+
initializeWaitingListData,
|
|
394
|
+
validateEntry,
|
|
395
|
+
normalizeEmail,
|
|
396
|
+
generateId,
|
|
397
|
+
|
|
398
|
+
// Constants
|
|
399
|
+
WAITING_LIST_ENTRIES_KEY,
|
|
400
|
+
WAITING_LIST_STATS_KEY
|
|
401
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const DEFAULT_RIGHTS = [
|
|
2
|
+
// Existing RBAC rights
|
|
2
3
|
'rbac:roles:read',
|
|
3
4
|
'rbac:roles:write',
|
|
4
5
|
'rbac:groups:read',
|
|
@@ -6,6 +7,8 @@ const DEFAULT_RIGHTS = [
|
|
|
6
7
|
'rbac:grants:read',
|
|
7
8
|
'rbac:grants:write',
|
|
8
9
|
'rbac:test',
|
|
10
|
+
|
|
11
|
+
// Existing experiment and file manager rights
|
|
9
12
|
'experiments:*',
|
|
10
13
|
'experiments:read',
|
|
11
14
|
'experiments:events:write',
|
|
@@ -21,6 +24,121 @@ const DEFAULT_RIGHTS = [
|
|
|
21
24
|
'file_manager:files:share',
|
|
22
25
|
'backoffice:*',
|
|
23
26
|
'backoffice:dashboard:access',
|
|
27
|
+
|
|
28
|
+
// Admin panel login rights
|
|
29
|
+
'admin_panel__login',
|
|
30
|
+
'admin_panel__dashboard',
|
|
31
|
+
|
|
32
|
+
// Dashboard section
|
|
33
|
+
'admin_panel__overview:read',
|
|
34
|
+
|
|
35
|
+
// User Management section
|
|
36
|
+
'admin_panel__users:read',
|
|
37
|
+
'admin_panel__users:write',
|
|
38
|
+
'admin_panel__organizations:read',
|
|
39
|
+
'admin_panel__organizations:write',
|
|
40
|
+
'admin_panel__rbac:read',
|
|
41
|
+
'admin_panel__rbac:write',
|
|
42
|
+
'admin_panel__notifications:read',
|
|
43
|
+
'admin_panel__notifications:write',
|
|
44
|
+
'admin_panel__waiting-list:read',
|
|
45
|
+
'admin_panel__waiting-list:write',
|
|
46
|
+
|
|
47
|
+
// Content & Config section
|
|
48
|
+
'admin_panel__i18n:read',
|
|
49
|
+
'admin_panel__i18n:write',
|
|
50
|
+
'admin_panel__i18n-locales:read',
|
|
51
|
+
'admin_panel__i18n-locales:write',
|
|
52
|
+
'admin_panel__json-configs:read',
|
|
53
|
+
'admin_panel__json-configs:write',
|
|
54
|
+
'admin_panel__markdowns:read',
|
|
55
|
+
'admin_panel__markdowns:write',
|
|
56
|
+
'admin_panel__seo-config:read',
|
|
57
|
+
'admin_panel__seo-config:write',
|
|
58
|
+
'admin_panel__assets:read',
|
|
59
|
+
'admin_panel__assets:write',
|
|
60
|
+
'admin_panel__file-manager:read',
|
|
61
|
+
'admin_panel__file-manager:write',
|
|
62
|
+
'admin_panel__ui-components:read',
|
|
63
|
+
'admin_panel__ui-components:write',
|
|
64
|
+
'admin_panel__headless:read',
|
|
65
|
+
'admin_panel__headless:write',
|
|
66
|
+
'admin_panel__pages:read',
|
|
67
|
+
'admin_panel__pages:write',
|
|
68
|
+
'admin_panel__blog:read',
|
|
69
|
+
'admin_panel__blog:write',
|
|
70
|
+
|
|
71
|
+
// System & DevOps section
|
|
72
|
+
'admin_panel__global-settings:read',
|
|
73
|
+
'admin_panel__global-settings:write',
|
|
74
|
+
'admin_panel__plugins-system:read',
|
|
75
|
+
'admin_panel__plugins-system:write',
|
|
76
|
+
'admin_panel__feature-flags:read',
|
|
77
|
+
'admin_panel__feature-flags:write',
|
|
78
|
+
'admin_panel__ejs-virtual:read',
|
|
79
|
+
'admin_panel__ejs-virtual:write',
|
|
80
|
+
'admin_panel__rate-limiter:read',
|
|
81
|
+
'admin_panel__rate-limiter:write',
|
|
82
|
+
'admin_panel__proxy:read',
|
|
83
|
+
'admin_panel__proxy:write',
|
|
84
|
+
'admin_panel__cache:read',
|
|
85
|
+
'admin_panel__cache:write',
|
|
86
|
+
'admin_panel__db-browser:read',
|
|
87
|
+
'admin_panel__db-browser:write',
|
|
88
|
+
'admin_panel__data-cleanup:read',
|
|
89
|
+
'admin_panel__data-cleanup:write',
|
|
90
|
+
'admin_panel__migration:read',
|
|
91
|
+
'admin_panel__migration:write',
|
|
92
|
+
'admin_panel__webhooks:read',
|
|
93
|
+
'admin_panel__webhooks:write',
|
|
94
|
+
'admin_panel__coolify-deploy:read',
|
|
95
|
+
'admin_panel__coolify-deploy:write',
|
|
96
|
+
|
|
97
|
+
// Monitoring & AI section
|
|
98
|
+
'admin_panel__audit:read',
|
|
99
|
+
'admin_panel__audit:write',
|
|
100
|
+
'admin_panel__errors:read',
|
|
101
|
+
'admin_panel__errors:write',
|
|
102
|
+
'admin_panel__experiments:read',
|
|
103
|
+
'admin_panel__experiments:write',
|
|
104
|
+
'admin_panel__console-manager:read',
|
|
105
|
+
'admin_panel__console-manager:write',
|
|
106
|
+
'admin_panel__health-checks:read',
|
|
107
|
+
'admin_panel__health-checks:write',
|
|
108
|
+
'admin_panel__metrics:read',
|
|
109
|
+
'admin_panel__metrics:write',
|
|
110
|
+
'admin_panel__llm:read',
|
|
111
|
+
'admin_panel__llm:write',
|
|
112
|
+
|
|
113
|
+
// Billing & Forms section
|
|
114
|
+
'admin_panel__stripe-pricing:read',
|
|
115
|
+
'admin_panel__stripe-pricing:write',
|
|
116
|
+
'admin_panel__forms:read',
|
|
117
|
+
'admin_panel__forms:write',
|
|
118
|
+
|
|
119
|
+
// Automation section
|
|
120
|
+
'admin_panel__agents:read',
|
|
121
|
+
'admin_panel__agents:write',
|
|
122
|
+
'admin_panel__telegram:read',
|
|
123
|
+
'admin_panel__telegram:write',
|
|
124
|
+
'admin_panel__workflows:read',
|
|
125
|
+
'admin_panel__workflows:write',
|
|
126
|
+
'admin_panel__scripts:read',
|
|
127
|
+
'admin_panel__scripts:write',
|
|
128
|
+
'admin_panel__crons:read',
|
|
129
|
+
'admin_panel__crons:write',
|
|
130
|
+
'admin_panel__terminals:read',
|
|
131
|
+
'admin_panel__terminals:write',
|
|
132
|
+
|
|
133
|
+
// Section wildcards for easier role management
|
|
134
|
+
'admin_panel__user-management:*',
|
|
135
|
+
'admin_panel__content-config:*',
|
|
136
|
+
'admin_panel__system-devops:*',
|
|
137
|
+
'admin_panel__monitoring-ai:*',
|
|
138
|
+
'admin_panel__billing-forms:*',
|
|
139
|
+
'admin_panel__automation:*',
|
|
140
|
+
|
|
141
|
+
// Superuser wildcard
|
|
24
142
|
'*',
|
|
25
143
|
];
|
|
26
144
|
|
package/test-access.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require('dotenv').config();
|
|
2
|
+
const mongoose = require('mongoose');
|
|
3
|
+
|
|
4
|
+
async function testAccessControl() {
|
|
5
|
+
try {
|
|
6
|
+
await mongoose.connect(process.env.MONGODB_URI, { authSource: 'admin' });
|
|
7
|
+
console.log('✅ Connected to MongoDB');
|
|
8
|
+
|
|
9
|
+
// Check if roles and grants exist
|
|
10
|
+
const RbacRole = require('./src/models/RbacRole');
|
|
11
|
+
const RbacGrant = require('./src/models/RbacGrant');
|
|
12
|
+
const User = require('./src/models/User');
|
|
13
|
+
|
|
14
|
+
// Find the limited-admin user
|
|
15
|
+
const user = await User.findOne({ email: 'limitedadmin@example.com' });
|
|
16
|
+
|
|
17
|
+
if (!user) {
|
|
18
|
+
console.log('❌ User not found');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(`✅ Found user: ${user.email} (${user.role})`);
|
|
23
|
+
|
|
24
|
+
// Find the limited-admin role
|
|
25
|
+
const limitedAdminRole = await RbacRole.findOne({ key: 'limited-admin' });
|
|
26
|
+
if (!limitedAdminRole) {
|
|
27
|
+
console.log('❌ limited-admin role not found');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(`✅ Found role: ${limitedAdminRole.name}`);
|
|
32
|
+
|
|
33
|
+
// Check grants for the role
|
|
34
|
+
const grants = await RbacGrant.find({
|
|
35
|
+
subjectType: 'role',
|
|
36
|
+
subjectId: limitedAdminRole._id
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
console.log(`\n📋 Role grants (${grants.length}):`);
|
|
40
|
+
grants.forEach(grant => {
|
|
41
|
+
console.log(` - ${grant.right}`);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Check specific permissions
|
|
45
|
+
const auditGrant = grants.find(g => g.right === 'admin_panel__audit:read');
|
|
46
|
+
const usersGrant = grants.find(g => g.right === 'admin_panel__users:read');
|
|
47
|
+
const errorsGrant = grants.find(g => g.right === 'admin_panel__errors:read');
|
|
48
|
+
|
|
49
|
+
console.log('\n🔍 Permission Summary:');
|
|
50
|
+
console.log(`Audit access: ${auditGrant ? '✅ ALLOWED' : '❌ DENIED'}`);
|
|
51
|
+
console.log(`Users access: ${usersGrant ? '✅ ALLOWED' : '❌ DENIED'}`);
|
|
52
|
+
console.log(`Errors access: ${errorsGrant ? '✅ ALLOWED' : '❌ DENIED'}`);
|
|
53
|
+
|
|
54
|
+
console.log('\n🎉 Access control test completed!');
|
|
55
|
+
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('❌ Test failed:', error);
|
|
58
|
+
} finally {
|
|
59
|
+
await mongoose.disconnect();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
testAccessControl();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Iframe Fix Test</title>
|
|
5
|
+
<style>
|
|
6
|
+
body { font-family: Arial, sans-serif; padding: 20px; }
|
|
7
|
+
.test-container { margin: 20px 0; }
|
|
8
|
+
iframe { width: 100%; height: 400px; border: 2px solid #ccc; }
|
|
9
|
+
.success { color: green; font-weight: bold; }
|
|
10
|
+
.error { color: red; font-weight: bold; }
|
|
11
|
+
</style>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<h1>Iframe Authentication Fix Test</h1>
|
|
15
|
+
|
|
16
|
+
<div class="test-container">
|
|
17
|
+
<h2>Test 1: Iframe with Token (Should Work)</h2>
|
|
18
|
+
<iframe src="/admin/stats/dashboard-home?iframe_token=authenticated"></iframe>
|
|
19
|
+
<p id="test1-result">Loading...</p>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="test-container">
|
|
23
|
+
<h2>Test 2: Iframe without Token (Should Redirect to Login)</h2>
|
|
24
|
+
<iframe src="/admin/stats/dashboard-home"></iframe>
|
|
25
|
+
<p id="test2-result">Loading...</p>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<script>
|
|
29
|
+
// Test if iframe loads correctly
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
const iframes = document.querySelectorAll('iframe');
|
|
32
|
+
|
|
33
|
+
// Test 1 - should show Command Center
|
|
34
|
+
iframes[0].contentDocument && iframes[0].contentDocument.body) {
|
|
35
|
+
const content = iframes[0].contentDocument.body.innerText;
|
|
36
|
+
if (content.includes('Command Center')) {
|
|
37
|
+
document.getElementById('test1-result').innerHTML = '<span class="success">✅ SUCCESS: Iframe with token loads correctly</span>';
|
|
38
|
+
} else if (content.includes('login') || content.includes('Login')) {
|
|
39
|
+
document.getElementById('test1-result').innerHTML = '<span class="error">❌ FAILED: Iframe with token redirected to login</span>';
|
|
40
|
+
} else {
|
|
41
|
+
document.getElementById('test1-result').innerHTML = '<span class="error">❌ UNKNOWN: Could not determine iframe content</span>';
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
document.getElementById('test1-result').innerHTML = '<span class="error">❌ FAILED: Could not access iframe content (cross-origin)</span>';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Test 2 - should redirect to login
|
|
48
|
+
if (iframes[1].contentDocument && iframes[1].contentDocument.body) {
|
|
49
|
+
const content = iframes[1].contentDocument.body.innerText;
|
|
50
|
+
if (content.includes('login') || content.includes('Login')) {
|
|
51
|
+
document.getElementById('test2-result').innerHTML = '<span class="success">✅ SUCCESS: Iframe without token correctly redirects to login</span>';
|
|
52
|
+
} else if (content.includes('Command Center')) {
|
|
53
|
+
document.getElementById('test2-result').innerHTML = '<span class="error">❌ FAILED: Iframe without token loaded content (security issue)</span>';
|
|
54
|
+
} else {
|
|
55
|
+
document.getElementById('test2-result').innerHTML = '<span class="error">❌ UNKNOWN: Could not determine iframe content</span>';
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
document.getElementById('test2-result').innerHTML = '<span class="error">❌ FAILED: Could not access iframe content (cross-origin)</span>';
|
|
59
|
+
}
|
|
60
|
+
}, 3000);
|
|
61
|
+
</script>
|
|
62
|
+
</body>
|
|
63
|
+
</html>
|
package/test-iframe.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Iframe Test</title>
|
|
5
|
+
</head>
|
|
6
|
+
<body>
|
|
7
|
+
<h1>Iframe Test</h1>
|
|
8
|
+
<p>Testing iframe loading of admin dashboard content...</p>
|
|
9
|
+
|
|
10
|
+
<iframe src="/admin/stats/dashboard-home" width="100%" height="500" style="border: 1px solid #ccc;"></iframe>
|
|
11
|
+
|
|
12
|
+
<p>If you see the admin dashboard content above, iframes work. If you see a login page, there's a cookie/session issue.</p>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|