@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,269 @@
|
|
|
1
|
+
const User = require('../models/User');
|
|
2
|
+
const asyncHandler = require('../utils/asyncHandler');
|
|
3
|
+
const rbacService = require('../services/rbac.service');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Auto-detect authentication type based on identifier format
|
|
7
|
+
* @param {string} identifier - Email or username
|
|
8
|
+
* @returns {string} 'iam' for email format, 'basic' for username format
|
|
9
|
+
*/
|
|
10
|
+
function detectAuthType(identifier) {
|
|
11
|
+
return identifier.includes('@') ? 'iam' : 'basic';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validate basic auth credentials against environment variables
|
|
16
|
+
* @param {string} username
|
|
17
|
+
* @param {string} password
|
|
18
|
+
* @returns {boolean} true if credentials are valid
|
|
19
|
+
*/
|
|
20
|
+
function validateBasicAuth(username, password) {
|
|
21
|
+
const adminUsername = process.env.ADMIN_USERNAME || "admin";
|
|
22
|
+
const adminPassword = process.env.ADMIN_PASSWORD || "admin";
|
|
23
|
+
|
|
24
|
+
return username === adminUsername && password === adminPassword;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Serve the admin login page
|
|
29
|
+
*/
|
|
30
|
+
const getLogin = asyncHandler(async (req, res) => {
|
|
31
|
+
// If already authenticated, redirect to admin dashboard
|
|
32
|
+
if (req.session && req.session.authenticated) {
|
|
33
|
+
return res.redirect(req.adminPath || '/admin');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const templatePath = require('path').join(__dirname, '..', '..', 'views', 'admin-login.ejs');
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
const ejs = require('ejs');
|
|
39
|
+
|
|
40
|
+
fs.readFile(templatePath, 'utf8', (err, template) => {
|
|
41
|
+
if (err) {
|
|
42
|
+
console.error('Error reading login template:', err);
|
|
43
|
+
return res.status(500).send('Error loading login page');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const html = ejs.render(template, {
|
|
48
|
+
baseUrl: req.baseUrl,
|
|
49
|
+
adminPath: req.adminPath || '/admin',
|
|
50
|
+
error: req.query.error || null,
|
|
51
|
+
success: req.query.success || null
|
|
52
|
+
}, {
|
|
53
|
+
filename: templatePath
|
|
54
|
+
});
|
|
55
|
+
res.send(html);
|
|
56
|
+
} catch (renderErr) {
|
|
57
|
+
console.error('Error rendering login template:', renderErr);
|
|
58
|
+
res.status(500).send('Error rendering login page');
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Process login credentials (supports both basic auth and IAM)
|
|
65
|
+
*/
|
|
66
|
+
const postLogin = asyncHandler(async (req, res) => {
|
|
67
|
+
const { identifier, password } = req.body;
|
|
68
|
+
|
|
69
|
+
if (!identifier || !password) {
|
|
70
|
+
return res.redirect(`${req.adminPath || '/admin'}/login?error=Email/username and password are required`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const authType = detectAuthType(identifier);
|
|
74
|
+
let user = null;
|
|
75
|
+
let authData = {};
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
if (authType === 'iam') {
|
|
79
|
+
// IAM authentication - validate against User model
|
|
80
|
+
user = await User.findOne({ email: identifier.toLowerCase() });
|
|
81
|
+
|
|
82
|
+
if (!user) {
|
|
83
|
+
return res.redirect(`${req.adminPath || '/admin'}/login?error=Invalid credentials`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check RBAC permissions for admin panel access with backward compatibility
|
|
87
|
+
const RbacUserRole = require('../models/RbacUserRole');
|
|
88
|
+
const RbacRole = require('../models/RbacRole');
|
|
89
|
+
const RbacGrant = require('../models/RbacGrant');
|
|
90
|
+
const { matches } = require('../utils/rbac/engine');
|
|
91
|
+
|
|
92
|
+
let hasAdminAccess = false;
|
|
93
|
+
|
|
94
|
+
// Phase 1: Try RBAC assignment with pattern matching
|
|
95
|
+
const userRoleAssignment = await RbacUserRole.findOne({ userId: user._id });
|
|
96
|
+
if (userRoleAssignment) {
|
|
97
|
+
const userRole = await RbacRole.findById(userRoleAssignment.roleId);
|
|
98
|
+
if (userRole && userRole.status === 'active') {
|
|
99
|
+
// Get all grants for this role
|
|
100
|
+
const grants = await RbacGrant.find({
|
|
101
|
+
subjectType: 'role',
|
|
102
|
+
subjectId: userRole._id,
|
|
103
|
+
scopeType: 'global',
|
|
104
|
+
effect: 'allow'
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Check if any grant matches admin_panel__login using pattern matching
|
|
108
|
+
hasAdminAccess = grants.some(grant =>
|
|
109
|
+
matches('admin_panel__login', grant.right)
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
console.log(`RBAC check for user ${user.email}: role=${userRole.key}, grants=${grants.length}, hasAccess=${hasAdminAccess}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Phase 2: Fallback to IAM role for backward compatibility
|
|
117
|
+
if (!hasAdminAccess && ['admin', 'superadmin'].includes(user.role)) {
|
|
118
|
+
console.log(`Fallback to IAM role for user ${user.email}: role=${user.role}`);
|
|
119
|
+
hasAdminAccess = true; // Admin and superadmin roles get panel access
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!hasAdminAccess) {
|
|
123
|
+
return res.redirect(`${req.adminPath || '/admin'}/login?error=Insufficient permissions - Admin panel access required`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Validate password
|
|
127
|
+
const isMatch = await user.comparePassword(password);
|
|
128
|
+
if (!isMatch) {
|
|
129
|
+
return res.redirect(`${req.adminPath || '/admin'}/login?error=Invalid credentials`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Get user's RBAC roles for session data
|
|
133
|
+
let rbacRoles = [];
|
|
134
|
+
try {
|
|
135
|
+
// Get role information directly
|
|
136
|
+
if (userRoleAssignment) {
|
|
137
|
+
const userRole = await RbacRole.findById(userRoleAssignment.roleId);
|
|
138
|
+
if (userRole) {
|
|
139
|
+
rbacRoles = [{
|
|
140
|
+
roleId: userRole._id,
|
|
141
|
+
roleKey: userRole.key,
|
|
142
|
+
roleName: userRole.name,
|
|
143
|
+
grants: [] // We could populate this if needed
|
|
144
|
+
}];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error('Error fetching RBAC roles:', error);
|
|
149
|
+
// Continue without RBAC roles if there's an error
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Store IAM user session data with RBAC context
|
|
153
|
+
authData = {
|
|
154
|
+
authType: 'iam',
|
|
155
|
+
userId: user._id,
|
|
156
|
+
email: user.email,
|
|
157
|
+
name: user.name,
|
|
158
|
+
role: user.role, // Keep for backward compatibility
|
|
159
|
+
rbacRoles: rbacRoles, // New RBAC context
|
|
160
|
+
authenticated: true,
|
|
161
|
+
loginTime: new Date().toISOString()
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
} else {
|
|
165
|
+
// Basic auth authentication - validate against environment variables
|
|
166
|
+
const isValid = validateBasicAuth(identifier, password);
|
|
167
|
+
|
|
168
|
+
if (!isValid) {
|
|
169
|
+
return res.redirect(`${req.adminPath || '/admin'}/login?error=Invalid credentials`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Store basic auth session data
|
|
173
|
+
authData = {
|
|
174
|
+
authType: 'basic',
|
|
175
|
+
username: identifier,
|
|
176
|
+
role: 'admin', // Basic auth users have admin privileges
|
|
177
|
+
authenticated: true,
|
|
178
|
+
loginTime: new Date().toISOString()
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Create session
|
|
183
|
+
req.session = req.session || {};
|
|
184
|
+
Object.assign(req.session, authData);
|
|
185
|
+
|
|
186
|
+
// Regenerate session to prevent fixation
|
|
187
|
+
req.session.regenerate((err) => {
|
|
188
|
+
if (err) {
|
|
189
|
+
console.error('Error regenerating session:', err);
|
|
190
|
+
return res.redirect(`${req.adminPath || '/admin'}/login?error=Session error`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Store auth data in new session
|
|
194
|
+
Object.assign(req.session, authData);
|
|
195
|
+
|
|
196
|
+
// Save session and redirect
|
|
197
|
+
req.session.save((saveErr) => {
|
|
198
|
+
if (saveErr) {
|
|
199
|
+
console.error('Error saving session:', saveErr);
|
|
200
|
+
return res.redirect(`${req.adminPath || '/admin'}/login?error=Session save error`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Redirect to admin dashboard or originally requested URL
|
|
204
|
+
const redirectTo = req.session.returnTo || (req.adminPath || '/admin');
|
|
205
|
+
delete req.session.returnTo;
|
|
206
|
+
res.redirect(redirectTo);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error('Login error:', error);
|
|
212
|
+
res.redirect(`${req.adminPath || '/admin'}/login?error=Authentication failed`);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Logout user and clear session
|
|
218
|
+
*/
|
|
219
|
+
const postLogout = asyncHandler(async (req, res) => {
|
|
220
|
+
req.session.destroy((err) => {
|
|
221
|
+
if (err) {
|
|
222
|
+
console.error('Error destroying session:', err);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
res.redirect(`${req.adminPath || '/admin'}/login?success=Logged out successfully`);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Check current authentication status (API endpoint)
|
|
231
|
+
*/
|
|
232
|
+
const getAuthStatus = asyncHandler(async (req, res) => {
|
|
233
|
+
if (!req.session || !req.session.authenticated) {
|
|
234
|
+
return res.json({
|
|
235
|
+
authenticated: false,
|
|
236
|
+
authType: null,
|
|
237
|
+
user: null
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const authData = {
|
|
242
|
+
authenticated: req.session.authenticated,
|
|
243
|
+
authType: req.session.authType,
|
|
244
|
+
loginTime: req.session.loginTime
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
if (req.session.authType === 'iam') {
|
|
248
|
+
authData.user = {
|
|
249
|
+
id: req.session.userId,
|
|
250
|
+
email: req.session.email,
|
|
251
|
+
name: req.session.name,
|
|
252
|
+
role: req.session.role
|
|
253
|
+
};
|
|
254
|
+
} else {
|
|
255
|
+
authData.user = {
|
|
256
|
+
username: req.session.username,
|
|
257
|
+
role: req.session.role
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
res.json(authData);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
module.exports = {
|
|
265
|
+
getLogin,
|
|
266
|
+
postLogin,
|
|
267
|
+
postLogout,
|
|
268
|
+
getAuthStatus
|
|
269
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const pluginsService = require('../services/plugins.service');
|
|
2
|
+
|
|
3
|
+
function handleError(res, error) {
|
|
4
|
+
const code = error?.code;
|
|
5
|
+
const message = error?.message || 'Operation failed';
|
|
6
|
+
|
|
7
|
+
if (code === 'NOT_FOUND') return res.status(404).json({ error: message });
|
|
8
|
+
if (code === 'VALIDATION') return res.status(400).json({ error: message });
|
|
9
|
+
return res.status(500).json({ error: message });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function runtimeContext(req) {
|
|
13
|
+
const superbackend = globalThis.superbackend || globalThis.saasbackend || {};
|
|
14
|
+
return {
|
|
15
|
+
services: superbackend.services || {},
|
|
16
|
+
helpers: superbackend.helpers || {},
|
|
17
|
+
request: req,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
exports.list = async (req, res) => {
|
|
22
|
+
try {
|
|
23
|
+
const items = await pluginsService.listPlugins();
|
|
24
|
+
return res.json({ items });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return handleError(res, error);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
exports.enable = async (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
const result = await pluginsService.enablePlugin(req.params.id, { context: runtimeContext(req) });
|
|
33
|
+
return res.json(result);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return handleError(res, error);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
exports.disable = async (req, res) => {
|
|
40
|
+
try {
|
|
41
|
+
const result = await pluginsService.disablePlugin(req.params.id);
|
|
42
|
+
return res.json(result);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return handleError(res, error);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
exports.install = async (req, res) => {
|
|
49
|
+
try {
|
|
50
|
+
const result = await pluginsService.installPlugin(req.params.id, { context: runtimeContext(req) });
|
|
51
|
+
return res.json(result);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return handleError(res, error);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const registryService = require('../services/registry.service');
|
|
2
|
+
|
|
3
|
+
function handleError(res, error) {
|
|
4
|
+
const code = error?.code;
|
|
5
|
+
const message = error?.message || 'Operation failed';
|
|
6
|
+
|
|
7
|
+
if (code === 'VALIDATION') return res.status(400).json({ error: message });
|
|
8
|
+
if (code === 'NOT_FOUND') return res.status(404).json({ error: message });
|
|
9
|
+
if (code === 'CONFLICT') return res.status(409).json({ error: message });
|
|
10
|
+
return res.status(500).json({ error: message });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
exports.listRegistries = async (req, res) => {
|
|
14
|
+
try {
|
|
15
|
+
const items = await registryService.listRegistries();
|
|
16
|
+
return res.json({ items });
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return handleError(res, error);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
exports.createRegistry = async (req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const item = await registryService.createRegistry(req.body || {});
|
|
25
|
+
return res.status(201).json({ item });
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return handleError(res, error);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
exports.getRegistry = async (req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
const item = await registryService.getRegistry(req.params.id);
|
|
34
|
+
if (!item) return res.status(404).json({ error: 'registry not found' });
|
|
35
|
+
return res.json({ item });
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return handleError(res, error);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
exports.updateRegistry = async (req, res) => {
|
|
42
|
+
try {
|
|
43
|
+
const item = await registryService.updateRegistry(req.params.id, req.body || {});
|
|
44
|
+
return res.json({ item });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return handleError(res, error);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
exports.deleteRegistry = async (req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
const result = await registryService.deleteRegistry(req.params.id);
|
|
53
|
+
return res.json(result);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return handleError(res, error);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
exports.createToken = async (req, res) => {
|
|
60
|
+
try {
|
|
61
|
+
const result = await registryService.createToken(req.params.id, req.body || {});
|
|
62
|
+
return res.status(201).json(result);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return handleError(res, error);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
exports.deleteToken = async (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const result = await registryService.deleteToken(req.params.id, req.params.tokenId);
|
|
71
|
+
return res.json(result);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return handleError(res, error);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
exports.listItems = async (req, res) => {
|
|
78
|
+
try {
|
|
79
|
+
const result = await registryService.listItemsForRegistry(
|
|
80
|
+
req.params.id,
|
|
81
|
+
req.query,
|
|
82
|
+
req.headers.authorization,
|
|
83
|
+
);
|
|
84
|
+
return res.json(result);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return handleError(res, error);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
exports.upsertItem = async (req, res) => {
|
|
91
|
+
try {
|
|
92
|
+
const item = await registryService.upsertItem(req.params.id, req.body || {});
|
|
93
|
+
return res.json({ item });
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return handleError(res, error);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
exports.deleteItem = async (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
const result = await registryService.deleteItem(req.params.id, req.params.itemId);
|
|
102
|
+
return res.json(result);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return handleError(res, error);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
@@ -4,7 +4,7 @@ const AuditEvent = require('../models/AuditEvent');
|
|
|
4
4
|
const ErrorAggregate = require('../models/ErrorAggregate');
|
|
5
5
|
const Asset = require('../models/Asset');
|
|
6
6
|
const FormSubmission = require('../models/FormSubmission');
|
|
7
|
-
const
|
|
7
|
+
const waitingListService = require('../services/waitingListJson.service');
|
|
8
8
|
const EmailLog = require('../models/EmailLog');
|
|
9
9
|
const VirtualEjsFile = require('../models/VirtualEjsFile');
|
|
10
10
|
const JsonConfig = require('../models/JsonConfig');
|
|
@@ -35,7 +35,7 @@ exports.getOverviewStats = async (req, res) => {
|
|
|
35
35
|
totalJsonConfigs,
|
|
36
36
|
// SaaS & Billing
|
|
37
37
|
totalForms,
|
|
38
|
-
|
|
38
|
+
waitingListStats,
|
|
39
39
|
totalPlans,
|
|
40
40
|
totalWorkflows
|
|
41
41
|
] = await Promise.all([
|
|
@@ -51,7 +51,7 @@ exports.getOverviewStats = async (req, res) => {
|
|
|
51
51
|
VirtualEjsFile.countDocuments(),
|
|
52
52
|
JsonConfig.countDocuments(),
|
|
53
53
|
FormSubmission.countDocuments(),
|
|
54
|
-
|
|
54
|
+
waitingListService.getWaitingListStats().catch(() => ({ totalSubscribers: 0 })), // Fallback to 0 if service fails
|
|
55
55
|
StripeCatalogItem.countDocuments({ active: true }),
|
|
56
56
|
Workflow.countDocuments()
|
|
57
57
|
]);
|
|
@@ -106,7 +106,7 @@ exports.getOverviewStats = async (req, res) => {
|
|
|
106
106
|
},
|
|
107
107
|
saas: {
|
|
108
108
|
forms: totalForms,
|
|
109
|
-
waiting:
|
|
109
|
+
waiting: waitingListStats.totalSubscribers || 0,
|
|
110
110
|
plans: totalPlans,
|
|
111
111
|
workflows: totalWorkflows
|
|
112
112
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const registryService = require('../services/registry.service');
|
|
2
|
+
|
|
3
|
+
function handleError(res, error) {
|
|
4
|
+
const code = error?.code;
|
|
5
|
+
const message = error?.message || 'Operation failed';
|
|
6
|
+
|
|
7
|
+
if (code === 'VALIDATION') return res.status(400).json({ error: { code: 'INVALID_REQUEST', message } });
|
|
8
|
+
if (code === 'NOT_FOUND') return res.status(404).json({ error: { code: 'NOT_FOUND', message } });
|
|
9
|
+
return res.status(500).json({ error: { code: 'INTERNAL_ERROR', message } });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
exports.auth = async (req, res) => {
|
|
13
|
+
try {
|
|
14
|
+
const payload = await registryService.getAuthStatus(req.params.id, req.headers.authorization);
|
|
15
|
+
return res.json(payload);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return handleError(res, error);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
exports.list = async (req, res) => {
|
|
22
|
+
try {
|
|
23
|
+
const payload = await registryService.listItemsForRegistry(
|
|
24
|
+
req.params.id,
|
|
25
|
+
req.query,
|
|
26
|
+
req.headers.authorization,
|
|
27
|
+
);
|
|
28
|
+
return res.json(payload);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return handleError(res, error);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const waitingListService = require('../services/waitingListJson.service');
|
|
2
2
|
const { validateEmail, sanitizeString } = require('../utils/validation');
|
|
3
3
|
|
|
4
4
|
// Subscribe to waiting list
|
|
@@ -31,36 +31,51 @@ exports.subscribe = async (req, res) => {
|
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
// Check if email already exists
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
// Check if email already exists and create new entry using JSON Configs service
|
|
35
|
+
try {
|
|
36
|
+
const waitingListEntry = await waitingListService.addWaitingListEntry({
|
|
37
|
+
email: sanitizedEmail.toLowerCase(),
|
|
38
|
+
type: sanitizedType.trim(),
|
|
39
|
+
referralSource: sanitizeString(referralSource) || 'website'
|
|
40
40
|
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Create new waiting list entry
|
|
44
|
-
const waitingListEntry = new WaitingList({
|
|
45
|
-
email: sanitizedEmail.toLowerCase(),
|
|
46
|
-
type: sanitizedType.trim(),
|
|
47
|
-
referralSource: sanitizeString(referralSource) || 'website'
|
|
48
|
-
});
|
|
49
41
|
|
|
50
|
-
|
|
42
|
+
// Return success response without sensitive data
|
|
43
|
+
const response = { ...waitingListEntry };
|
|
44
|
+
delete response.email; // Don't return email in response for privacy
|
|
51
45
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
46
|
+
res.status(201).json({
|
|
47
|
+
message: 'Successfully joined the waiting list!',
|
|
48
|
+
data: response
|
|
49
|
+
});
|
|
50
|
+
} catch (serviceError) {
|
|
51
|
+
// Handle validation and duplicate errors from service
|
|
52
|
+
if (serviceError.code === 'VALIDATION') {
|
|
53
|
+
return res.status(400).json({
|
|
54
|
+
error: serviceError.message,
|
|
55
|
+
field: 'general'
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (serviceError.code === 'DUPLICATE_EMAIL' || serviceError.code === 'DUPLICATE' || serviceError.message.includes('already exists')) {
|
|
60
|
+
return res.status(409).json({
|
|
61
|
+
error: 'This email is already on our waiting list',
|
|
62
|
+
field: 'email'
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (serviceError.code === 'INITIALIZATION_FAILED') {
|
|
67
|
+
return res.status(500).json({
|
|
68
|
+
error: 'Service temporarily unavailable - please try again',
|
|
69
|
+
field: 'general'
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
throw serviceError; // Re-throw for general error handling
|
|
74
|
+
}
|
|
60
75
|
} catch (error) {
|
|
61
76
|
console.error('Waiting list subscription error:', error);
|
|
62
77
|
|
|
63
|
-
// Handle specific
|
|
78
|
+
// Handle specific errors
|
|
64
79
|
if (error.code === 11000) {
|
|
65
80
|
return res.status(409).json({
|
|
66
81
|
error: 'This email is already on our waiting list',
|
|
@@ -86,35 +101,10 @@ exports.subscribe = async (req, res) => {
|
|
|
86
101
|
// Get waiting list stats (public)
|
|
87
102
|
exports.getStats = async (req, res) => {
|
|
88
103
|
try {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const typeAgg = await WaitingList.aggregate([
|
|
92
|
-
{ $match: { status: 'active' } },
|
|
93
|
-
{ $group: { _id: '$type', count: { $sum: 1 } } },
|
|
94
|
-
{ $sort: { count: -1, _id: 1 } },
|
|
95
|
-
]);
|
|
96
|
-
|
|
97
|
-
const typeCounts = (typeAgg || []).reduce((acc, row) => {
|
|
98
|
-
if (!row?._id) return acc;
|
|
99
|
-
acc[String(row._id)] = row.count || 0;
|
|
100
|
-
return acc;
|
|
101
|
-
}, {});
|
|
102
|
-
|
|
103
|
-
// Backward compatibility fields (legacy UI/tests)
|
|
104
|
-
const buyerCount = (typeCounts.buyer || 0) + (typeCounts.both || 0);
|
|
105
|
-
const sellerCount = (typeCounts.seller || 0) + (typeCounts.both || 0);
|
|
106
|
-
|
|
107
|
-
// Add some mock growth data for demonstration
|
|
108
|
-
const growthThisWeek = Math.floor(totalSubscribers * 0.05); // 5% growth
|
|
104
|
+
// Use JSON Configs service for cached statistics
|
|
105
|
+
const stats = await waitingListService.getWaitingListStats();
|
|
109
106
|
|
|
110
|
-
res.json(
|
|
111
|
-
totalSubscribers,
|
|
112
|
-
buyerCount,
|
|
113
|
-
sellerCount,
|
|
114
|
-
typeCounts,
|
|
115
|
-
growthThisWeek,
|
|
116
|
-
lastUpdated: new Date().toISOString()
|
|
117
|
-
});
|
|
107
|
+
res.json(stats);
|
|
118
108
|
} catch (error) {
|
|
119
109
|
console.error('Waiting list stats error:', error);
|
|
120
110
|
res.status(500).json({
|
|
@@ -135,31 +125,19 @@ exports.adminList = async (req, res) => {
|
|
|
135
125
|
offset = 0,
|
|
136
126
|
} = req.query;
|
|
137
127
|
|
|
138
|
-
const query = {};
|
|
139
|
-
if (status) query.status = String(status);
|
|
140
|
-
if (type) query.type = String(type);
|
|
141
|
-
if (email) query.email = String(email).trim().toLowerCase();
|
|
142
|
-
|
|
143
128
|
const parsedLimit = Math.min(500, Math.max(1, parseInt(limit, 10) || 50));
|
|
144
129
|
const parsedOffset = Math.max(0, parseInt(offset, 10) || 0);
|
|
145
130
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const total = await WaitingList.countDocuments(query);
|
|
154
|
-
|
|
155
|
-
return res.json({
|
|
156
|
-
entries,
|
|
157
|
-
pagination: {
|
|
158
|
-
total,
|
|
159
|
-
limit: parsedLimit,
|
|
160
|
-
offset: parsedOffset,
|
|
161
|
-
},
|
|
131
|
+
// Use JSON Configs service for admin data with filtering and pagination
|
|
132
|
+
const result = await waitingListService.getWaitingListEntriesAdmin({
|
|
133
|
+
status,
|
|
134
|
+
type,
|
|
135
|
+
email,
|
|
136
|
+
limit: parsedLimit,
|
|
137
|
+
offset: parsedOffset
|
|
162
138
|
});
|
|
139
|
+
|
|
140
|
+
return res.json(result);
|
|
163
141
|
} catch (error) {
|
|
164
142
|
console.error('Waiting list admin list error:', error);
|
|
165
143
|
return res.status(500).json({ error: 'Failed to list entries' });
|