@intranefr/superbackend 1.7.7 → 1.7.9

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 (119) hide show
  1. package/.beads/.br_history/issues.20260314_212352_900045509.jsonl +0 -0
  2. package/.beads/.br_history/issues.20260314_212352_900045509.jsonl.meta.json +1 -0
  3. package/.beads/.br_history/issues.20260314_212353_087140743.jsonl +1 -0
  4. package/.beads/.br_history/issues.20260314_212353_087140743.jsonl.meta.json +1 -0
  5. package/.beads/.br_history/issues.20260314_212353_285881504.jsonl +2 -0
  6. package/.beads/.br_history/issues.20260314_212353_285881504.jsonl.meta.json +1 -0
  7. package/.beads/.br_history/issues.20260314_212353_473915419.jsonl +3 -0
  8. package/.beads/.br_history/issues.20260314_212353_473915419.jsonl.meta.json +1 -0
  9. package/.beads/.br_history/issues.20260314_212353_659476307.jsonl +4 -0
  10. package/.beads/.br_history/issues.20260314_212353_659476307.jsonl.meta.json +1 -0
  11. package/.beads/.br_history/issues.20260314_212353_869998925.jsonl +5 -0
  12. package/.beads/.br_history/issues.20260314_212353_869998925.jsonl.meta.json +1 -0
  13. package/.beads/.br_history/issues.20260314_212354_054785029.jsonl +6 -0
  14. package/.beads/.br_history/issues.20260314_212354_054785029.jsonl.meta.json +1 -0
  15. package/.beads/.br_history/issues.20260314_213336_175893691.jsonl +7 -0
  16. package/.beads/.br_history/issues.20260314_213336_175893691.jsonl.meta.json +1 -0
  17. package/.beads/.br_history/issues.20260314_213336_338509797.jsonl +7 -0
  18. package/.beads/.br_history/issues.20260314_213336_338509797.jsonl.meta.json +1 -0
  19. package/.beads/.br_history/issues.20260314_213336_515443192.jsonl +7 -0
  20. package/.beads/.br_history/issues.20260314_213336_515443192.jsonl.meta.json +1 -0
  21. package/.beads/.br_history/issues.20260314_213336_676417592.jsonl +7 -0
  22. package/.beads/.br_history/issues.20260314_213336_676417592.jsonl.meta.json +1 -0
  23. package/.beads/.br_history/issues.20260314_213336_839182422.jsonl +7 -0
  24. package/.beads/.br_history/issues.20260314_213336_839182422.jsonl.meta.json +1 -0
  25. package/.beads/.br_history/issues.20260314_213337_004349113.jsonl +7 -0
  26. package/.beads/.br_history/issues.20260314_213337_004349113.jsonl.meta.json +1 -0
  27. package/.beads/.br_history/issues.20260314_213337_179824080.jsonl +7 -0
  28. package/.beads/.br_history/issues.20260314_213337_179824080.jsonl.meta.json +1 -0
  29. package/.beads/.br_history/issues.20260314_213701_705075332.jsonl +7 -0
  30. package/.beads/.br_history/issues.20260314_213701_705075332.jsonl.meta.json +1 -0
  31. package/.beads/.br_history/issues.20260314_213706_783128702.jsonl +8 -0
  32. package/.beads/.br_history/issues.20260314_213706_783128702.jsonl.meta.json +1 -0
  33. package/.beads/config.yaml +4 -0
  34. package/.beads/issues.jsonl +8 -0
  35. package/.beads/metadata.json +4 -0
  36. package/.env.example +8 -0
  37. package/autochangelog/.env.example +36 -0
  38. package/autochangelog/README.md +412 -0
  39. package/autochangelog/config/database.js +27 -0
  40. package/autochangelog/package.json +47 -0
  41. package/autochangelog/public/landing.html +581 -0
  42. package/autochangelog/server.js +104 -0
  43. package/autochangelog/src/app.js +181 -0
  44. package/autochangelog/src/config/database.js +26 -0
  45. package/autochangelog/src/controllers/auth.js +488 -0
  46. package/autochangelog/src/controllers/changelog.js +682 -0
  47. package/autochangelog/src/controllers/project.js +580 -0
  48. package/autochangelog/src/controllers/repository.js +780 -0
  49. package/autochangelog/src/middleware/auth.js +386 -0
  50. package/autochangelog/src/models/Changelog.js +443 -0
  51. package/autochangelog/src/models/Project.js +226 -0
  52. package/autochangelog/src/models/Repository.js +366 -0
  53. package/autochangelog/src/models/User.js +223 -0
  54. package/autochangelog/src/routes/auth.routes.js +32 -0
  55. package/autochangelog/src/routes/changelog.routes.js +42 -0
  56. package/autochangelog/src/routes/github-auth.routes.js +102 -0
  57. package/autochangelog/src/routes/project.routes.js +50 -0
  58. package/autochangelog/src/routes/repository.routes.js +54 -0
  59. package/autochangelog/src/services/changelog.js +722 -0
  60. package/autochangelog/src/services/github.js +243 -0
  61. package/autochangelog/utils/logger.js +77 -0
  62. package/autochangelog/views/404.ejs +18 -0
  63. package/autochangelog/views/dashboard.ejs +596 -0
  64. package/autochangelog/views/index.ejs +231 -0
  65. package/autochangelog/views/layouts/main.ejs +44 -0
  66. package/autochangelog/views/login.ejs +104 -0
  67. package/autochangelog/views/partials/footer.ejs +20 -0
  68. package/autochangelog/views/partials/navbar.ejs +51 -0
  69. package/autochangelog/views/register.ejs +109 -0
  70. package/autochangelog-cli/README.md +266 -0
  71. package/autochangelog-cli/bin/autochangelog +120 -0
  72. package/autochangelog-cli/package.json +46 -0
  73. package/autochangelog-cli/src/cli/commands/auth.js +291 -0
  74. package/autochangelog-cli/src/cli/commands/changelog.js +619 -0
  75. package/autochangelog-cli/src/cli/commands/project.js +427 -0
  76. package/autochangelog-cli/src/cli/commands/repo.js +557 -0
  77. package/autochangelog-cli/src/cli/commands/stats.js +706 -0
  78. package/autochangelog-cli/src/cli/utils/config.js +277 -0
  79. package/autochangelog-cli/src/cli/utils/errors.js +307 -0
  80. package/autochangelog-cli/src/cli/utils/logger.js +75 -0
  81. package/autochangelog-cli/src/cli/utils/output.js +357 -0
  82. package/package.json +8 -3
  83. package/plugins/supercli/README.md +108 -0
  84. package/plugins/supercli/plugin.json +123 -0
  85. package/server.js +1 -1
  86. package/src/cli/api.js +380 -0
  87. package/src/cli/direct/agent-utils.js +61 -0
  88. package/src/cli/direct/cli-utils.js +112 -0
  89. package/src/cli/direct/data-seeding.js +307 -0
  90. package/src/cli/direct/db-admin.js +84 -0
  91. package/src/cli/direct/db-advanced.js +372 -0
  92. package/src/cli/direct/db-utils.js +558 -0
  93. package/src/cli/direct/help.js +195 -0
  94. package/src/cli/direct/migration.js +107 -0
  95. package/src/cli/direct/rbac-advanced.js +132 -0
  96. package/src/cli/direct/resources-additional.js +400 -0
  97. package/src/cli/direct/resources-cms-advanced.js +173 -0
  98. package/src/cli/direct/resources-cms.js +247 -0
  99. package/src/cli/direct/resources-core.js +253 -0
  100. package/src/cli/direct/resources-execution.js +367 -0
  101. package/src/cli/direct/resources-health.js +152 -0
  102. package/src/cli/direct/resources-integrations.js +182 -0
  103. package/src/cli/direct/resources-logs.js +204 -0
  104. package/src/cli/direct/resources-org-rbac.js +187 -0
  105. package/src/cli/direct/resources-system.js +236 -0
  106. package/src/cli/direct.js +556 -0
  107. package/src/controllers/admin.controller.js +4 -0
  108. package/src/controllers/auth.controller.js +148 -1
  109. package/src/controllers/waitingList.controller.js +130 -1
  110. package/src/models/RbacRole.js +1 -1
  111. package/src/models/User.js +39 -5
  112. package/src/routes/auth.routes.js +6 -0
  113. package/src/routes/waitingList.routes.js +12 -2
  114. package/src/routes/waitingListAdmin.routes.js +3 -0
  115. package/src/services/email.service.js +1 -0
  116. package/src/services/github.service.js +255 -0
  117. package/src/services/rateLimiter.service.js +29 -1
  118. package/src/services/waitingListJson.service.js +32 -3
  119. package/views/admin-waiting-list.ejs +386 -3
@@ -1,6 +1,7 @@
1
1
  const User = require('../models/User');
2
2
  const { generateAccessToken, generateRefreshToken, verifyRefreshToken } = require('../utils/jwt');
3
3
  const asyncHandler = require('../utils/asyncHandler');
4
+ const githubService = require('../services/github.service');
4
5
 
5
6
  // Register new user
6
7
  const register = asyncHandler(async (req, res) => {
@@ -85,9 +86,155 @@ const me = asyncHandler(async (req, res) => {
85
86
  res.json({ user: req.user.toJSON() });
86
87
  });
87
88
 
89
+ // Initiate GitHub OAuth flow
90
+ const githubLogin = asyncHandler(async (req, res) => {
91
+ const state = githubService.generateState();
92
+
93
+ // Store state in session for CSRF protection (if sessions are enabled)
94
+ if (req.session) {
95
+ req.session.githubOAuthState = state;
96
+ }
97
+
98
+ const authUrl = githubService.getAuthURL(state);
99
+
100
+ // Return URL for frontend to redirect, or redirect directly
101
+ if (req.query.json === 'true' || req.headers.accept?.includes('application/json')) {
102
+ res.json({
103
+ success: true,
104
+ authUrl: authUrl,
105
+ state: state
106
+ });
107
+ } else {
108
+ res.redirect(authUrl);
109
+ }
110
+ });
111
+
112
+ // Handle GitHub OAuth callback
113
+ const githubCallback = asyncHandler(async (req, res) => {
114
+ const { code, state } = req.query;
115
+
116
+ if (!code) {
117
+ return res.status(400).json({ error: 'Authorization code missing' });
118
+ }
119
+
120
+ // Verify state parameter for CSRF protection
121
+ if (req.session?.githubOAuthState) {
122
+ if (!state || state !== req.session.githubOAuthState) {
123
+ return res.status(400).json({ error: 'Invalid state parameter' });
124
+ }
125
+ // Clear used state
126
+ delete req.session.githubOAuthState;
127
+ }
128
+
129
+ // Exchange code for access token
130
+ const tokenResponse = await githubService.getAccessToken(code, state);
131
+
132
+ if (!tokenResponse.accessToken) {
133
+ return res.status(400).json({ error: 'Failed to get access token' });
134
+ }
135
+
136
+ // Get user info from GitHub
137
+ const githubUser = await githubService.getFullUserInfo(tokenResponse.accessToken);
138
+
139
+ // Check if user exists with this GitHub ID
140
+ let user = await User.findOne({ githubId: githubUser.id });
141
+
142
+ if (!user) {
143
+ // Check if user exists with this email
144
+ const emailToCheck = githubUser.email || `${githubUser.login}@users.noreply.github.com`;
145
+ user = await User.findOne({ email: emailToCheck.toLowerCase() });
146
+
147
+ if (user) {
148
+ // Link GitHub account to existing user
149
+ user.githubId = githubUser.id;
150
+ user.githubUsername = githubUser.login;
151
+ user.githubAccessToken = tokenResponse.accessToken;
152
+ if (tokenResponse.refreshToken) {
153
+ user.githubRefreshToken = tokenResponse.refreshToken;
154
+ }
155
+ user.githubEmail = githubUser.email;
156
+ user.avatar = githubUser.avatarUrl;
157
+ if (githubUser.name) user.name = githubUser.name;
158
+ user.emailVerified = githubUser.emailVerified || user.emailVerified;
159
+ await user.save();
160
+ } else {
161
+ // Create new user
162
+ user = new User({
163
+ email: emailToCheck.toLowerCase(),
164
+ name: githubUser.name || githubUser.login,
165
+ githubId: githubUser.id,
166
+ githubUsername: githubUser.login,
167
+ githubAccessToken: tokenResponse.accessToken,
168
+ githubRefreshToken: tokenResponse.refreshToken,
169
+ githubEmail: githubUser.email,
170
+ avatar: githubUser.avatarUrl,
171
+ emailVerified: githubUser.emailVerified,
172
+ role: 'user'
173
+ });
174
+ await user.save();
175
+ }
176
+ } else {
177
+ // Update existing user's tokens and info
178
+ user.githubAccessToken = tokenResponse.accessToken;
179
+ if (tokenResponse.refreshToken) {
180
+ user.githubRefreshToken = tokenResponse.refreshToken;
181
+ }
182
+ user.avatar = githubUser.avatarUrl;
183
+ if (githubUser.name) user.name = githubUser.name;
184
+ user.githubUsername = githubUser.login;
185
+ await user.save();
186
+ }
187
+
188
+ // Generate JWT tokens
189
+ const token = generateAccessToken(user._id, user.role);
190
+ const refreshToken = generateRefreshToken(user._id);
191
+
192
+ // Update last login
193
+ user.lastLogin = new Date();
194
+ await user.save();
195
+
196
+ // Redirect to frontend with token in URL hash or return JSON
197
+ const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
198
+
199
+ if (req.query.json === 'true' || req.headers.accept?.includes('application/json')) {
200
+ res.json({
201
+ success: true,
202
+ token,
203
+ refreshToken,
204
+ user: user.toJSON()
205
+ });
206
+ } else {
207
+ res.redirect(`${frontendUrl}/dashboard#token=${token}`);
208
+ }
209
+ });
210
+
211
+ // Refresh GitHub access token
212
+ const githubRefreshToken = asyncHandler(async (req, res) => {
213
+ const { refreshToken } = req.body;
214
+
215
+ if (!refreshToken) {
216
+ return res.status(400).json({ error: 'Refresh token required' });
217
+ }
218
+
219
+ try {
220
+ const tokenResponse = await githubService.refreshAccessToken(refreshToken);
221
+
222
+ res.json({
223
+ success: true,
224
+ accessToken: tokenResponse.accessToken,
225
+ refreshToken: tokenResponse.refreshToken
226
+ });
227
+ } catch (error) {
228
+ res.status(400).json({ error: error.message });
229
+ }
230
+ });
231
+
88
232
  module.exports = {
89
233
  register,
90
234
  login,
91
235
  refresh,
92
- me
236
+ me,
237
+ githubLogin,
238
+ githubCallback,
239
+ githubRefreshToken
93
240
  };
@@ -129,12 +129,14 @@ exports.adminList = async (req, res) => {
129
129
  const parsedOffset = Math.max(0, parseInt(offset, 10) || 0);
130
130
 
131
131
  // Use JSON Configs service for admin data with filtering and pagination
132
+ // Always bypass cache for admin list to ensure fresh data
132
133
  const result = await waitingListService.getWaitingListEntriesAdmin({
133
134
  status,
134
135
  type,
135
136
  email,
136
137
  limit: parsedLimit,
137
- offset: parsedOffset
138
+ offset: parsedOffset,
139
+ bypassCache: true
138
140
  });
139
141
 
140
142
  return res.json(result);
@@ -143,3 +145,130 @@ exports.adminList = async (req, res) => {
143
145
  return res.status(500).json({ error: 'Failed to list entries' });
144
146
  }
145
147
  };
148
+
149
+ // Get available types (for admin UI filters)
150
+ exports.getTypes = async (req, res) => {
151
+ try {
152
+ const result = await waitingListService.getAvailableTypes();
153
+ res.json(result);
154
+ } catch (error) {
155
+ console.error('Waiting list get types error:', error);
156
+ return res.status(500).json({ error: 'Failed to get types' });
157
+ }
158
+ };
159
+
160
+ // Export waiting list entries as CSV (server-side, respects filters)
161
+ exports.exportCsv = async (req, res) => {
162
+ try {
163
+ const {
164
+ status,
165
+ type,
166
+ email,
167
+ } = req.query;
168
+
169
+ // Get ALL filtered entries (no pagination)
170
+ const result = await waitingListService.getWaitingListEntriesAdmin({
171
+ status,
172
+ type,
173
+ email,
174
+ limit: 100000, // Large limit to get all entries
175
+ offset: 0
176
+ });
177
+
178
+ const entries = result.entries || [];
179
+
180
+ // Set CSV headers
181
+ res.setHeader('Content-Type', 'text/csv');
182
+ res.setHeader('Content-Disposition', `attachment; filename="waiting-list-${new Date().toISOString().split('T')[0]}.csv"`);
183
+
184
+ // CSV header row
185
+ const csvRows = [
186
+ ['Email', 'Type', 'Status', 'Referral Source', 'Created At', 'Updated At']
187
+ ];
188
+
189
+ // Data rows
190
+ entries.forEach(entry => {
191
+ csvRows.push([
192
+ entry.email || '',
193
+ entry.type || '',
194
+ entry.status || '',
195
+ entry.referralSource || '',
196
+ entry.createdAt ? new Date(entry.createdAt).toISOString() : '',
197
+ entry.updatedAt ? new Date(entry.updatedAt).toISOString() : ''
198
+ ]);
199
+ });
200
+
201
+ // Convert to CSV string with proper escaping
202
+ const csvContent = csvRows.map(row =>
203
+ row.map(cell => {
204
+ const str = String(cell || '');
205
+ // Escape quotes and wrap in quotes if contains comma, quote, or newline
206
+ if (str.includes(',') || str.includes('"') || str.includes('\n')) {
207
+ return `"${str.replace(/"/g, '""')}"`;
208
+ }
209
+ return str;
210
+ }).join(',')
211
+ ).join('\n');
212
+
213
+ res.send(csvContent);
214
+ } catch (error) {
215
+ console.error('Waiting list export CSV error:', error);
216
+ return res.status(500).json({ error: 'Failed to export CSV' });
217
+ }
218
+ };
219
+
220
+ // Bulk remove waiting list entries
221
+ exports.bulkRemove = async (req, res) => {
222
+ try {
223
+ const { entryIds } = req.body;
224
+
225
+ if (!Array.isArray(entryIds) || entryIds.length === 0) {
226
+ return res.status(400).json({ error: 'entryIds must be a non-empty array' });
227
+ }
228
+
229
+ const { removeWaitingListEntry, getWaitingListEntries, clearWaitingListCache } = require('../services/waitingListJson.service');
230
+
231
+ // Get all entries to verify IDs exist (bypass cache)
232
+ const { entries } = await getWaitingListEntries({ bypassCache: true });
233
+ const validIds = new Set(entries.map(e => e.id));
234
+
235
+ const removed = [];
236
+ const notFound = [];
237
+
238
+ // Remove each entry
239
+ for (const entryId of entryIds) {
240
+ if (!validIds.has(entryId)) {
241
+ notFound.push(entryId);
242
+ continue;
243
+ }
244
+
245
+ try {
246
+ await removeWaitingListEntry(entryId);
247
+ removed.push(entryId);
248
+ } catch (error) {
249
+ console.error(`Failed to remove entry ${entryId}:`, error.message);
250
+ notFound.push(entryId);
251
+ }
252
+ }
253
+
254
+ // Force clear cache after all removals
255
+ await clearWaitingListCache();
256
+
257
+ console.log(`[BulkRemove] Removed ${removed.length} entries, ${notFound.length} not found`);
258
+
259
+ res.json({
260
+ message: `Successfully removed ${removed.length} entr${removed.length === 1 ? 'y' : 'ies'}`,
261
+ removed: {
262
+ count: removed.length,
263
+ ids: removed
264
+ },
265
+ notFound: {
266
+ count: notFound.length,
267
+ ids: notFound
268
+ }
269
+ });
270
+ } catch (error) {
271
+ console.error('Waiting list bulk remove error:', error);
272
+ return res.status(500).json({ error: 'Failed to bulk remove entries' });
273
+ }
274
+ };
@@ -2,7 +2,7 @@ const mongoose = require('mongoose');
2
2
 
3
3
  const rbacRoleSchema = new mongoose.Schema(
4
4
  {
5
- key: { type: String, required: true, trim: true, lowercase: true, index: true },
5
+ key: { type: String, required: true, trim: true, lowercase: true },
6
6
  name: { type: String, required: true, trim: true },
7
7
  description: { type: String, default: '', trim: true },
8
8
  status: { type: String, enum: ['active', 'disabled'], default: 'active', index: true },
@@ -7,24 +7,53 @@ const userSchema = new mongoose.Schema({
7
7
  required: true,
8
8
  unique: true,
9
9
  lowercase: true,
10
- trim: true,
11
- index: true
10
+ trim: true
12
11
  },
13
12
  clerkUserId: {
14
13
  type: String,
15
- index: true,
16
14
  sparse: true
17
15
  },
18
16
  passwordHash: {
19
17
  type: String,
20
18
  required: function () {
21
- return !this.clerkUserId;
19
+ // Password is optional for OAuth users (GitHub, Clerk)
20
+ return !this.clerkUserId && !this.githubId;
22
21
  }
23
22
  },
24
23
  name: {
25
24
  type: String,
26
25
  trim: true
27
26
  },
27
+
28
+ // GitHub OAuth Integration
29
+ githubId: {
30
+ type: String,
31
+ sparse: true
32
+ },
33
+ githubUsername: {
34
+ type: String,
35
+ sparse: true
36
+ },
37
+ githubAccessToken: {
38
+ type: String,
39
+ select: false // Don't return by default
40
+ },
41
+ githubRefreshToken: {
42
+ type: String,
43
+ select: false // Don't return by default
44
+ },
45
+ githubEmail: {
46
+ type: String,
47
+ sparse: true
48
+ },
49
+ avatar: {
50
+ type: String
51
+ },
52
+ emailVerified: {
53
+ type: Boolean,
54
+ default: false
55
+ },
56
+
28
57
  subscriptionStatus: {
29
58
  type: String,
30
59
  enum: ['none', 'active', 'cancelled', 'past_due', 'incomplete', 'incomplete_expired', 'trialing', 'unpaid'],
@@ -63,7 +92,10 @@ const userSchema = new mongoose.Schema({
63
92
  timestamps: true
64
93
  });
65
94
 
66
- // userSchema.index({ email: 1 }); // Removed duplicate index
95
+ // Indexes
96
+ userSchema.index({ email: 1 });
97
+ userSchema.index({ githubId: 1 });
98
+ userSchema.index({ clerkUserId: 1 });
67
99
 
68
100
  // Hash password before saving
69
101
  userSchema.pre('save', async function(next) {
@@ -82,6 +114,8 @@ userSchema.methods.comparePassword = async function(candidatePassword) {
82
114
  userSchema.methods.toJSON = function() {
83
115
  const obj = this.toObject();
84
116
  delete obj.passwordHash;
117
+ delete obj.githubAccessToken;
118
+ delete obj.githubRefreshToken;
85
119
  delete obj.__v;
86
120
  return obj;
87
121
  };
@@ -4,9 +4,15 @@ const authController = require('../controllers/auth.controller');
4
4
  const { authenticate } = require('../middleware/auth');
5
5
  const { auditMiddleware } = require('../services/auditLogger');
6
6
 
7
+ // Email/Password Auth
7
8
  router.post('/register', auditMiddleware('public.auth.register', { entityType: 'User' }), authController.register);
8
9
  router.post('/login', auditMiddleware('public.auth.login', { entityType: 'User' }), authController.login);
9
10
  router.post('/refresh-token', auditMiddleware('public.auth.refresh', { entityType: 'User' }), authController.refresh);
10
11
  router.get('/me', authenticate, authController.me);
11
12
 
13
+ // GitHub OAuth Auth
14
+ router.get('/github', auditMiddleware('public.auth.github.init', { entityType: 'User' }), authController.githubLogin);
15
+ router.get('/github/callback', auditMiddleware('public.auth.github.callback', { entityType: 'User' }), authController.githubCallback);
16
+ router.post('/github/refresh-token', auditMiddleware('public.auth.github.refresh', { entityType: 'User' }), authController.githubRefreshToken);
17
+
12
18
  module.exports = router;
@@ -3,11 +3,21 @@ const router = express.Router();
3
3
  const waitingListController = require('../controllers/waitingList.controller');
4
4
  const asyncHandler = require('../utils/asyncHandler');
5
5
  const { auditMiddleware } = require('../services/auditLogger');
6
+ const rateLimiter = require('../services/rateLimiter.service');
6
7
 
7
8
  // POST /api/waiting-list/subscribe - Subscribe to waiting list
8
- router.post('/subscribe', auditMiddleware('public.waiting_list.subscribe', { entityType: 'WaitingList' }), asyncHandler(waitingListController.subscribe));
9
+ // Rate limited by IP to prevent spam/abuse (1 request per minute)
10
+ router.post('/subscribe',
11
+ rateLimiter.limit('waitingListSubscribeLimiter'),
12
+ auditMiddleware('public.waiting_list.subscribe', { entityType: 'WaitingList' }),
13
+ asyncHandler(waitingListController.subscribe)
14
+ );
9
15
 
10
16
  // GET /api/waiting-list/stats - Get waiting list statistics (public)
11
- router.get('/stats', asyncHandler(waitingListController.getStats));
17
+ // Light rate limiting to prevent abuse (60 requests per minute)
18
+ router.get('/stats',
19
+ rateLimiter.limit('waitingListStatsLimiter'),
20
+ asyncHandler(waitingListController.getStats)
21
+ );
12
22
 
13
23
  module.exports = router;
@@ -5,5 +5,8 @@ const waitingListController = require('../controllers/waitingList.controller');
5
5
  const asyncHandler = require('../utils/asyncHandler');
6
6
 
7
7
  router.get('/', adminSessionAuth, asyncHandler(waitingListController.adminList));
8
+ router.get('/types', adminSessionAuth, asyncHandler(waitingListController.getTypes));
9
+ router.get('/export-csv', adminSessionAuth, asyncHandler(waitingListController.exportCsv));
10
+ router.post('/bulk-remove', adminSessionAuth, asyncHandler(waitingListController.bulkRemove));
8
11
 
9
12
  module.exports = router;
@@ -1,6 +1,7 @@
1
1
  // Email service wrapper using Resend
2
2
  // Note: Resend package needs to be installed: npm install resend
3
3
 
4
+ const mongoose = require("mongoose");
4
5
  const GlobalSetting = require("../models/GlobalSetting");
5
6
  const EmailLog = require("../models/EmailLog");
6
7