@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.
Files changed (106) hide show
  1. package/cookies.txt +6 -0
  2. package/cookies1.txt +6 -0
  3. package/cookies2.txt +6 -0
  4. package/cookies3.txt +6 -0
  5. package/cookies4.txt +5 -0
  6. package/cookies_old.txt +5 -0
  7. package/cookies_old_test.txt +6 -0
  8. package/cookies_super.txt +5 -0
  9. package/cookies_super_test.txt +6 -0
  10. package/cookies_test.txt +6 -0
  11. package/index.js +7 -0
  12. package/package.json +3 -1
  13. package/plugins/core-waiting-list-migration/README.md +118 -0
  14. package/plugins/core-waiting-list-migration/index.js +438 -0
  15. package/plugins/global-settings-presets/index.js +20 -0
  16. package/plugins/hello-cli/index.js +17 -0
  17. package/plugins/ui-components-seeder/components/suiAlert.js +212 -0
  18. package/plugins/ui-components-seeder/components/suiToast.js +186 -0
  19. package/plugins/ui-components-seeder/index.js +31 -0
  20. package/public/js/admin-ui-components-preview.js +281 -0
  21. package/public/js/admin-ui-components.js +408 -0
  22. package/public/js/llm-provider-model-picker.js +193 -0
  23. package/public/test-iframe-fix.html +63 -0
  24. package/public/test-iframe.html +14 -0
  25. package/src/admin/endpointRegistry.js +68 -0
  26. package/src/controllers/admin.controller.js +25 -5
  27. package/src/controllers/adminDataCleanup.controller.js +45 -0
  28. package/src/controllers/adminLlm.controller.js +0 -8
  29. package/src/controllers/adminLogin.controller.js +269 -0
  30. package/src/controllers/adminPlugins.controller.js +55 -0
  31. package/src/controllers/adminRegistry.controller.js +106 -0
  32. package/src/controllers/adminStats.controller.js +4 -4
  33. package/src/controllers/registry.controller.js +32 -0
  34. package/src/controllers/waitingList.controller.js +52 -74
  35. package/src/middleware/auth.js +71 -1
  36. package/src/middleware/rbac.js +62 -0
  37. package/src/middleware.js +454 -153
  38. package/src/models/GlobalSetting.js +11 -1
  39. package/src/models/UiComponent.js +2 -0
  40. package/src/models/User.js +1 -1
  41. package/src/routes/admin.routes.js +3 -3
  42. package/src/routes/adminAgents.routes.js +2 -2
  43. package/src/routes/adminAssets.routes.js +11 -11
  44. package/src/routes/adminBlog.routes.js +2 -2
  45. package/src/routes/adminBlogAi.routes.js +2 -2
  46. package/src/routes/adminBlogAutomation.routes.js +2 -2
  47. package/src/routes/adminCache.routes.js +2 -2
  48. package/src/routes/adminConsoleManager.routes.js +2 -2
  49. package/src/routes/adminCrons.routes.js +2 -2
  50. package/src/routes/adminDataCleanup.routes.js +26 -0
  51. package/src/routes/adminDbBrowser.routes.js +2 -2
  52. package/src/routes/adminEjsVirtual.routes.js +2 -2
  53. package/src/routes/adminFeatureFlags.routes.js +6 -6
  54. package/src/routes/adminHeadless.routes.js +2 -2
  55. package/src/routes/adminHealthChecks.routes.js +2 -2
  56. package/src/routes/adminI18n.routes.js +2 -2
  57. package/src/routes/adminJsonConfigs.routes.js +8 -8
  58. package/src/routes/adminLlm.routes.js +8 -8
  59. package/src/routes/adminLogin.routes.js +23 -0
  60. package/src/routes/adminMarkdowns.routes.js +3 -9
  61. package/src/routes/adminMigration.routes.js +12 -12
  62. package/src/routes/adminPages.routes.js +2 -2
  63. package/src/routes/adminPlugins.routes.js +15 -0
  64. package/src/routes/adminProxy.routes.js +2 -2
  65. package/src/routes/adminRateLimits.routes.js +8 -8
  66. package/src/routes/adminRbac.routes.js +2 -2
  67. package/src/routes/adminRegistry.routes.js +24 -0
  68. package/src/routes/adminScripts.routes.js +2 -2
  69. package/src/routes/adminSeoConfig.routes.js +10 -10
  70. package/src/routes/adminTelegram.routes.js +2 -2
  71. package/src/routes/adminTerminals.routes.js +2 -2
  72. package/src/routes/adminUiComponents.routes.js +2 -2
  73. package/src/routes/adminUploadNamespaces.routes.js +7 -7
  74. package/src/routes/blogInternal.routes.js +2 -2
  75. package/src/routes/experiments.routes.js +2 -2
  76. package/src/routes/formsAdmin.routes.js +6 -6
  77. package/src/routes/globalSettings.routes.js +8 -8
  78. package/src/routes/internalExperiments.routes.js +2 -2
  79. package/src/routes/notificationAdmin.routes.js +7 -7
  80. package/src/routes/orgAdmin.routes.js +16 -16
  81. package/src/routes/pages.routes.js +3 -3
  82. package/src/routes/registry.routes.js +11 -0
  83. package/src/routes/stripeAdmin.routes.js +12 -12
  84. package/src/routes/userAdmin.routes.js +7 -7
  85. package/src/routes/waitingListAdmin.routes.js +2 -2
  86. package/src/routes/workflows.routes.js +3 -3
  87. package/src/services/dataCleanup.service.js +286 -0
  88. package/src/services/jsonConfigs.service.js +262 -0
  89. package/src/services/plugins.service.js +348 -0
  90. package/src/services/registry.service.js +452 -0
  91. package/src/services/uiComponents.service.js +180 -0
  92. package/src/services/waitingListJson.service.js +401 -0
  93. package/src/utils/rbac/rightsRegistry.js +118 -0
  94. package/test-access.js +63 -0
  95. package/test-iframe-fix.html +63 -0
  96. package/test-iframe.html +14 -0
  97. package/views/admin-403.ejs +92 -0
  98. package/views/admin-dashboard-home.ejs +52 -2
  99. package/views/admin-dashboard.ejs +143 -2
  100. package/views/admin-data-cleanup.ejs +357 -0
  101. package/views/admin-login.ejs +286 -0
  102. package/views/admin-plugins-system.ejs +223 -0
  103. package/views/admin-ui-components.ejs +82 -402
  104. package/views/admin-users.ejs +207 -11
  105. package/views/partials/dashboard/nav-items.ejs +2 -0
  106. 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 WaitingList = require('../models/WaitingList');
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
- totalWaiting,
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
- WaitingList.countDocuments(),
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: totalWaiting,
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 WaitingList = require('../models/WaitingList');
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
- const existingEntry = await WaitingList.findOne({ email: sanitizedEmail.toLowerCase() });
36
- if (existingEntry) {
37
- return res.status(409).json({
38
- error: 'This email is already on our waiting list',
39
- field: 'email'
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
- await waitingListEntry.save();
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
- // Return success response without sensitive data
53
- const response = waitingListEntry.toJSON();
54
- delete response.email; // Don't return email in response for privacy
55
-
56
- res.status(201).json({
57
- message: 'Successfully joined the waiting list!',
58
- data: response
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 MongoDB errors
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
- const totalSubscribers = await WaitingList.countDocuments({ status: 'active' });
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
- const entries = await WaitingList.find(query)
147
- .sort({ createdAt: -1 })
148
- .limit(parsedLimit)
149
- .skip(parsedOffset)
150
- .select('email type status referralSource createdAt updatedAt')
151
- .lean();
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' });