@intranefr/superbackend 1.5.2 → 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 +9 -0
- package/manage.js +745 -0
- package/package.json +6 -2
- 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 +36 -10
- package/src/controllers/adminAgents.controller.js +37 -0
- package/src/controllers/adminDataCleanup.controller.js +45 -0
- package/src/controllers/adminLlm.controller.js +19 -8
- package/src/controllers/adminLogin.controller.js +269 -0
- package/src/controllers/adminMarkdowns.controller.js +157 -0
- package/src/controllers/adminPlugins.controller.js +55 -0
- package/src/controllers/adminRegistry.controller.js +106 -0
- package/src/controllers/adminScripts.controller.js +138 -0
- package/src/controllers/adminStats.controller.js +4 -4
- package/src/controllers/adminTelegram.controller.js +72 -0
- package/src/controllers/markdowns.controller.js +42 -0
- package/src/controllers/registry.controller.js +32 -0
- package/src/controllers/waitingList.controller.js +52 -74
- package/src/helpers/mongooseHelper.js +6 -6
- package/src/helpers/scriptBase.js +2 -2
- package/src/middleware/auth.js +71 -1
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +584 -176
- package/src/models/Agent.js +105 -0
- package/src/models/AgentMessage.js +82 -0
- package/src/models/GlobalSetting.js +11 -1
- package/src/models/Markdown.js +75 -0
- package/src/models/ScriptRun.js +8 -0
- package/src/models/TelegramBot.js +42 -0
- 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 +13 -0
- 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 -7
- package/src/routes/adminLogin.routes.js +23 -0
- package/src/routes/adminMarkdowns.routes.js +10 -0
- 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 +6 -3
- package/src/routes/adminSeoConfig.routes.js +10 -10
- package/src/routes/adminTelegram.routes.js +14 -0
- 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/markdowns.routes.js +16 -0
- 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/agent.service.js +546 -0
- package/src/services/agentHistory.service.js +345 -0
- package/src/services/agentTools.service.js +578 -0
- package/src/services/dataCleanup.service.js +286 -0
- package/src/services/jsonConfigs.service.js +284 -10
- package/src/services/llm.service.js +219 -6
- package/src/services/markdowns.service.js +522 -0
- package/src/services/plugins.service.js +348 -0
- package/src/services/registry.service.js +452 -0
- package/src/services/scriptsRunner.service.js +328 -37
- package/src/services/telegram.service.js +130 -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-agents.ejs +273 -0
- package/views/admin-coolify-deploy.ejs +8 -8
- package/views/admin-dashboard-home.ejs +52 -2
- package/views/admin-dashboard.ejs +179 -7
- package/views/admin-data-cleanup.ejs +357 -0
- package/views/admin-experiments.ejs +1 -1
- package/views/admin-login.ejs +286 -0
- package/views/admin-markdowns.ejs +905 -0
- package/views/admin-plugins-system.ejs +223 -0
- package/views/admin-scripts.ejs +221 -4
- package/views/admin-telegram.ejs +269 -0
- package/views/admin-ui-components.ejs +82 -402
- package/views/admin-users.ejs +207 -11
- package/views/partials/dashboard/nav-items.ejs +5 -0
- package/views/partials/llm-provider-model-picker.ejs +0 -161
- package/analysis-only.skill +0 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
const UiComponent = require('../models/UiComponent');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UI Components Service
|
|
5
|
+
* Provides service layer for UI Components operations
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class UiComponentsService {
|
|
9
|
+
/**
|
|
10
|
+
* Upsert a UI component (create or update)
|
|
11
|
+
* @param {Object} componentData - Component data
|
|
12
|
+
* @param {string} componentData.code - Unique component code
|
|
13
|
+
* @param {string} componentData.name - Component display name
|
|
14
|
+
* @param {string} componentData.html - HTML template
|
|
15
|
+
* @param {string} componentData.css - CSS styles
|
|
16
|
+
* @param {string} componentData.js - JavaScript code
|
|
17
|
+
* @param {string} componentData.usageMarkdown - Usage documentation
|
|
18
|
+
* @param {string} componentData.api - API summary
|
|
19
|
+
* @param {number} componentData.version - Component version
|
|
20
|
+
* @param {boolean} componentData.isActive - Whether component is active
|
|
21
|
+
* @returns {Promise<Object>} The created/updated component
|
|
22
|
+
*/
|
|
23
|
+
async upsertComponent(componentData) {
|
|
24
|
+
try {
|
|
25
|
+
const {
|
|
26
|
+
code,
|
|
27
|
+
name,
|
|
28
|
+
html = '',
|
|
29
|
+
css = '',
|
|
30
|
+
js = '',
|
|
31
|
+
usageMarkdown = '',
|
|
32
|
+
api = null,
|
|
33
|
+
version = 1,
|
|
34
|
+
isActive = true,
|
|
35
|
+
previewExample = null,
|
|
36
|
+
} = componentData;
|
|
37
|
+
|
|
38
|
+
// Validate required fields
|
|
39
|
+
if (!code || typeof code !== 'string') {
|
|
40
|
+
throw new Error('code is required and must be a string');
|
|
41
|
+
}
|
|
42
|
+
if (!name || typeof name !== 'string') {
|
|
43
|
+
throw new Error('name is required and must be a string');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Normalize code to lowercase
|
|
47
|
+
const normalizedCode = String(code).trim().toLowerCase();
|
|
48
|
+
|
|
49
|
+
// Check if component exists
|
|
50
|
+
const existing = await UiComponent.findOne({ code: normalizedCode });
|
|
51
|
+
|
|
52
|
+
if (existing) {
|
|
53
|
+
// Update existing component
|
|
54
|
+
const updateData = {
|
|
55
|
+
name: String(name).trim(),
|
|
56
|
+
html: String(html),
|
|
57
|
+
css: String(css),
|
|
58
|
+
js: String(js),
|
|
59
|
+
usageMarkdown: String(usageMarkdown),
|
|
60
|
+
api,
|
|
61
|
+
version: Number(version) || 1,
|
|
62
|
+
isActive: Boolean(isActive),
|
|
63
|
+
updatedAt: new Date(),
|
|
64
|
+
previewExample,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const updated = await UiComponent.findOneAndUpdate(
|
|
68
|
+
{ code: normalizedCode },
|
|
69
|
+
updateData,
|
|
70
|
+
{ new: true, runValidators: true }
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
console.log(`[uiComponents] Updated component: ${normalizedCode}`);
|
|
74
|
+
return updated.toObject();
|
|
75
|
+
} else {
|
|
76
|
+
// Create new component
|
|
77
|
+
const createData = {
|
|
78
|
+
code: normalizedCode,
|
|
79
|
+
name: String(name).trim(),
|
|
80
|
+
html: String(html),
|
|
81
|
+
css: String(css),
|
|
82
|
+
js: String(js),
|
|
83
|
+
usageMarkdown: String(usageMarkdown),
|
|
84
|
+
api,
|
|
85
|
+
version: Number(version) || 1,
|
|
86
|
+
isActive: Boolean(isActive),
|
|
87
|
+
previewExample,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const created = await UiComponent.create(createData);
|
|
91
|
+
console.log(`[uiComponents] Created component: ${normalizedCode}`);
|
|
92
|
+
return created.toObject();
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(`[uiComponents] Failed to upsert component ${componentData.code}:`, error);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get a component by code
|
|
102
|
+
* @param {string} code - Component code
|
|
103
|
+
* @returns {Promise<Object|null>} Component data or null if not found
|
|
104
|
+
*/
|
|
105
|
+
async getComponent(code) {
|
|
106
|
+
try {
|
|
107
|
+
const component = await UiComponent.findOne({
|
|
108
|
+
code: String(code).trim().toLowerCase()
|
|
109
|
+
}).lean();
|
|
110
|
+
return component;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error(`[uiComponents] Failed to get component ${code}:`, error);
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* List all components
|
|
119
|
+
* @param {Object} options - Query options
|
|
120
|
+
* @param {boolean} options.activeOnly - Only return active components
|
|
121
|
+
* @returns {Promise<Array>} Array of components
|
|
122
|
+
*/
|
|
123
|
+
async listComponents(options = {}) {
|
|
124
|
+
try {
|
|
125
|
+
const { activeOnly = false } = options;
|
|
126
|
+
const query = activeOnly ? { isActive: true } : {};
|
|
127
|
+
|
|
128
|
+
const components = await UiComponent.find(query)
|
|
129
|
+
.sort({ updatedAt: -1 })
|
|
130
|
+
.lean();
|
|
131
|
+
|
|
132
|
+
return components;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('[uiComponents] Failed to list components:', error);
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Delete a component by code
|
|
141
|
+
* @param {string} code - Component code
|
|
142
|
+
* @returns {Promise<boolean>} True if deleted, false if not found
|
|
143
|
+
*/
|
|
144
|
+
async deleteComponent(code) {
|
|
145
|
+
try {
|
|
146
|
+
const result = await UiComponent.deleteOne({
|
|
147
|
+
code: String(code).trim().toLowerCase()
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (result.deletedCount > 0) {
|
|
151
|
+
console.log(`[uiComponents] Deleted component: ${code}`);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return false;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error(`[uiComponents] Failed to delete component ${code}:`, error);
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Check if component exists
|
|
164
|
+
* @param {string} code - Component code
|
|
165
|
+
* @returns {Promise<boolean>} True if component exists
|
|
166
|
+
*/
|
|
167
|
+
async componentExists(code) {
|
|
168
|
+
try {
|
|
169
|
+
const count = await UiComponent.countDocuments({
|
|
170
|
+
code: String(code).trim().toLowerCase()
|
|
171
|
+
});
|
|
172
|
+
return count > 0;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error(`[uiComponents] Failed to check component existence ${code}:`, error);
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = new UiComponentsService();
|
|
@@ -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
|
|