@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.
- package/.beads/.br_history/issues.20260314_212352_900045509.jsonl +0 -0
- package/.beads/.br_history/issues.20260314_212352_900045509.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_212353_087140743.jsonl +1 -0
- package/.beads/.br_history/issues.20260314_212353_087140743.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_212353_285881504.jsonl +2 -0
- package/.beads/.br_history/issues.20260314_212353_285881504.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_212353_473915419.jsonl +3 -0
- package/.beads/.br_history/issues.20260314_212353_473915419.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_212353_659476307.jsonl +4 -0
- package/.beads/.br_history/issues.20260314_212353_659476307.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_212353_869998925.jsonl +5 -0
- package/.beads/.br_history/issues.20260314_212353_869998925.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_212354_054785029.jsonl +6 -0
- package/.beads/.br_history/issues.20260314_212354_054785029.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213336_175893691.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213336_175893691.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213336_338509797.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213336_338509797.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213336_515443192.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213336_515443192.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213336_676417592.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213336_676417592.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213336_839182422.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213336_839182422.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213337_004349113.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213337_004349113.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213337_179824080.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213337_179824080.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213701_705075332.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213701_705075332.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213706_783128702.jsonl +8 -0
- package/.beads/.br_history/issues.20260314_213706_783128702.jsonl.meta.json +1 -0
- package/.beads/config.yaml +4 -0
- package/.beads/issues.jsonl +8 -0
- package/.beads/metadata.json +4 -0
- package/.env.example +8 -0
- package/autochangelog/.env.example +36 -0
- package/autochangelog/README.md +412 -0
- package/autochangelog/config/database.js +27 -0
- package/autochangelog/package.json +47 -0
- package/autochangelog/public/landing.html +581 -0
- package/autochangelog/server.js +104 -0
- package/autochangelog/src/app.js +181 -0
- package/autochangelog/src/config/database.js +26 -0
- package/autochangelog/src/controllers/auth.js +488 -0
- package/autochangelog/src/controllers/changelog.js +682 -0
- package/autochangelog/src/controllers/project.js +580 -0
- package/autochangelog/src/controllers/repository.js +780 -0
- package/autochangelog/src/middleware/auth.js +386 -0
- package/autochangelog/src/models/Changelog.js +443 -0
- package/autochangelog/src/models/Project.js +226 -0
- package/autochangelog/src/models/Repository.js +366 -0
- package/autochangelog/src/models/User.js +223 -0
- package/autochangelog/src/routes/auth.routes.js +32 -0
- package/autochangelog/src/routes/changelog.routes.js +42 -0
- package/autochangelog/src/routes/github-auth.routes.js +102 -0
- package/autochangelog/src/routes/project.routes.js +50 -0
- package/autochangelog/src/routes/repository.routes.js +54 -0
- package/autochangelog/src/services/changelog.js +722 -0
- package/autochangelog/src/services/github.js +243 -0
- package/autochangelog/utils/logger.js +77 -0
- package/autochangelog/views/404.ejs +18 -0
- package/autochangelog/views/dashboard.ejs +596 -0
- package/autochangelog/views/index.ejs +231 -0
- package/autochangelog/views/layouts/main.ejs +44 -0
- package/autochangelog/views/login.ejs +104 -0
- package/autochangelog/views/partials/footer.ejs +20 -0
- package/autochangelog/views/partials/navbar.ejs +51 -0
- package/autochangelog/views/register.ejs +109 -0
- package/autochangelog-cli/README.md +266 -0
- package/autochangelog-cli/bin/autochangelog +120 -0
- package/autochangelog-cli/package.json +46 -0
- package/autochangelog-cli/src/cli/commands/auth.js +291 -0
- package/autochangelog-cli/src/cli/commands/changelog.js +619 -0
- package/autochangelog-cli/src/cli/commands/project.js +427 -0
- package/autochangelog-cli/src/cli/commands/repo.js +557 -0
- package/autochangelog-cli/src/cli/commands/stats.js +706 -0
- package/autochangelog-cli/src/cli/utils/config.js +277 -0
- package/autochangelog-cli/src/cli/utils/errors.js +307 -0
- package/autochangelog-cli/src/cli/utils/logger.js +75 -0
- package/autochangelog-cli/src/cli/utils/output.js +357 -0
- package/package.json +8 -3
- package/plugins/supercli/README.md +108 -0
- package/plugins/supercli/plugin.json +123 -0
- package/server.js +1 -1
- package/src/cli/api.js +380 -0
- package/src/cli/direct/agent-utils.js +61 -0
- package/src/cli/direct/cli-utils.js +112 -0
- package/src/cli/direct/data-seeding.js +307 -0
- package/src/cli/direct/db-admin.js +84 -0
- package/src/cli/direct/db-advanced.js +372 -0
- package/src/cli/direct/db-utils.js +558 -0
- package/src/cli/direct/help.js +195 -0
- package/src/cli/direct/migration.js +107 -0
- package/src/cli/direct/rbac-advanced.js +132 -0
- package/src/cli/direct/resources-additional.js +400 -0
- package/src/cli/direct/resources-cms-advanced.js +173 -0
- package/src/cli/direct/resources-cms.js +247 -0
- package/src/cli/direct/resources-core.js +253 -0
- package/src/cli/direct/resources-execution.js +367 -0
- package/src/cli/direct/resources-health.js +152 -0
- package/src/cli/direct/resources-integrations.js +182 -0
- package/src/cli/direct/resources-logs.js +204 -0
- package/src/cli/direct/resources-org-rbac.js +187 -0
- package/src/cli/direct/resources-system.js +236 -0
- package/src/cli/direct.js +556 -0
- package/src/controllers/admin.controller.js +4 -0
- package/src/controllers/auth.controller.js +148 -1
- package/src/controllers/waitingList.controller.js +130 -1
- package/src/models/RbacRole.js +1 -1
- package/src/models/User.js +39 -5
- package/src/routes/auth.routes.js +6 -0
- package/src/routes/waitingList.routes.js +12 -2
- package/src/routes/waitingListAdmin.routes.js +3 -0
- package/src/services/email.service.js +1 -0
- package/src/services/github.service.js +255 -0
- package/src/services/rateLimiter.service.js +29 -1
- package/src/services/waitingListJson.service.js +32 -3
- package/views/admin-waiting-list.ejs +386 -3
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
const globalSettingsService = require('./globalSettings.service');
|
|
4
|
+
|
|
5
|
+
class GitHubService {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.clientId = null;
|
|
8
|
+
this.clientSecret = null;
|
|
9
|
+
this.callbackUrl = null;
|
|
10
|
+
this.baseURL = 'https://github.com';
|
|
11
|
+
this.apiBaseURL = 'https://api.github.com';
|
|
12
|
+
|
|
13
|
+
// OAuth scopes requested
|
|
14
|
+
this.scopes = ['read:user', 'user:email'];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get GitHub OAuth credentials from global settings or environment variables
|
|
19
|
+
* Global settings take precedence over env vars
|
|
20
|
+
*/
|
|
21
|
+
async getCredentials() {
|
|
22
|
+
// Try global settings first (they override env vars)
|
|
23
|
+
const clientId = await globalSettingsService.getSettingValue('github.oauth.clientId');
|
|
24
|
+
const clientSecret = await globalSettingsService.getSettingValue('github.oauth.clientSecret');
|
|
25
|
+
const callbackUrl = await globalSettingsService.getSettingValue('github.oauth.callbackUrl');
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
clientId: clientId || process.env.GITHUB_CLIENT_ID,
|
|
29
|
+
clientSecret: clientSecret || process.env.GITHUB_CLIENT_SECRET,
|
|
30
|
+
callbackUrl: callbackUrl || process.env.GITHUB_CALLBACK_URL || 'http://localhost:3000/api/auth/github/callback'
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generate OAuth authorization URL
|
|
36
|
+
* @param {string} state - CSRF protection state parameter
|
|
37
|
+
* @returns {string} GitHub OAuth URL
|
|
38
|
+
*/
|
|
39
|
+
async getAuthURL(state) {
|
|
40
|
+
const credentials = await this.getCredentials();
|
|
41
|
+
|
|
42
|
+
if (!credentials.clientId) {
|
|
43
|
+
throw new Error('GITHUB_CLIENT_ID not configured');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const params = new URLSearchParams({
|
|
47
|
+
client_id: credentials.clientId,
|
|
48
|
+
redirect_uri: credentials.callbackUrl,
|
|
49
|
+
scope: this.scopes.join(' '),
|
|
50
|
+
state: state
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return `${this.baseURL}/login/oauth/authorize?${params.toString()}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Exchange authorization code for access token
|
|
58
|
+
* @param {string} code - Authorization code from GitHub callback
|
|
59
|
+
* @param {string} state - State parameter to verify
|
|
60
|
+
* @returns {Promise<object>} Token response
|
|
61
|
+
*/
|
|
62
|
+
async getAccessToken(code, state) {
|
|
63
|
+
const credentials = await this.getCredentials();
|
|
64
|
+
|
|
65
|
+
if (!credentials.clientId || !credentials.clientSecret) {
|
|
66
|
+
throw new Error('GitHub OAuth credentials not configured');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const response = await axios.post(
|
|
71
|
+
`${this.baseURL}/login/oauth/access_token`,
|
|
72
|
+
{
|
|
73
|
+
client_id: credentials.clientId,
|
|
74
|
+
client_secret: credentials.clientSecret,
|
|
75
|
+
code: code,
|
|
76
|
+
redirect_uri: credentials.callbackUrl,
|
|
77
|
+
state: state
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
headers: {
|
|
81
|
+
'Accept': 'application/json',
|
|
82
|
+
'Content-Type': 'application/json'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (response.data.error) {
|
|
88
|
+
throw new Error(response.data.error_description || response.data.error);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
accessToken: response.data.access_token,
|
|
93
|
+
refreshToken: response.data.refresh_token,
|
|
94
|
+
scope: response.data.scope,
|
|
95
|
+
tokenType: response.data.token_type
|
|
96
|
+
};
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error.response) {
|
|
99
|
+
throw new Error(`GitHub token exchange failed: ${error.response.data.error_description || error.response.statusText}`);
|
|
100
|
+
}
|
|
101
|
+
throw new Error(`GitHub token exchange failed: ${error.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get user profile from GitHub API
|
|
107
|
+
* @param {string} accessToken - GitHub access token
|
|
108
|
+
* @returns {Promise<object>} GitHub user profile
|
|
109
|
+
*/
|
|
110
|
+
async getUserProfile(accessToken) {
|
|
111
|
+
try {
|
|
112
|
+
const response = await axios.get(`${this.apiBaseURL}/user`, {
|
|
113
|
+
headers: {
|
|
114
|
+
'Authorization': `token ${accessToken}`,
|
|
115
|
+
'Accept': 'application/vnd.github.v3+json'
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
id: String(response.data.id),
|
|
121
|
+
login: response.data.login,
|
|
122
|
+
name: response.data.name || response.data.login,
|
|
123
|
+
email: response.data.email,
|
|
124
|
+
avatarUrl: response.data.avatar_url,
|
|
125
|
+
htmlUrl: response.data.html_url,
|
|
126
|
+
company: response.data.company,
|
|
127
|
+
location: response.data.location,
|
|
128
|
+
bio: response.data.bio
|
|
129
|
+
};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (error.response) {
|
|
132
|
+
throw new Error(`GitHub API error: ${error.response.data.message || error.response.statusText}`);
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`Failed to fetch GitHub profile: ${error.message}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get user emails from GitHub API
|
|
140
|
+
* @param {string} accessToken - GitHub access token
|
|
141
|
+
* @returns {Promise<Array>} List of user emails
|
|
142
|
+
*/
|
|
143
|
+
async getUserEmails(accessToken) {
|
|
144
|
+
try {
|
|
145
|
+
const response = await axios.get(`${this.apiBaseURL}/user/emails`, {
|
|
146
|
+
headers: {
|
|
147
|
+
'Authorization': `token ${accessToken}`,
|
|
148
|
+
'Accept': 'application/vnd.github.v3+json'
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Return primary verified email or first verified email
|
|
153
|
+
const emails = response.data || [];
|
|
154
|
+
const primaryEmail = emails.find(e => e.primary && e.verified);
|
|
155
|
+
const verifiedEmail = emails.find(e => e.verified);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
primary: primaryEmail?.email || verifiedEmail?.email || emails[0]?.email,
|
|
159
|
+
all: emails
|
|
160
|
+
};
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (error.response) {
|
|
163
|
+
throw new Error(`GitHub API error: ${error.response.data.message || error.response.statusText}`);
|
|
164
|
+
}
|
|
165
|
+
throw new Error(`Failed to fetch GitHub emails: ${error.message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Refresh access token (if refresh token is available)
|
|
171
|
+
* @param {string} refreshToken - GitHub refresh token
|
|
172
|
+
* @returns {Promise<object>} New token response
|
|
173
|
+
*/
|
|
174
|
+
async refreshAccessToken(refreshToken) {
|
|
175
|
+
const credentials = await this.getCredentials();
|
|
176
|
+
|
|
177
|
+
if (!credentials.clientId || !credentials.clientSecret) {
|
|
178
|
+
throw new Error('GitHub OAuth credentials not configured');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const response = await axios.post(
|
|
183
|
+
`${this.baseURL}/login/oauth/access_token`,
|
|
184
|
+
{
|
|
185
|
+
client_id: credentials.clientId,
|
|
186
|
+
client_secret: credentials.clientSecret,
|
|
187
|
+
grant_type: 'refresh_token',
|
|
188
|
+
refresh_token: refreshToken
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
headers: {
|
|
192
|
+
'Accept': 'application/json',
|
|
193
|
+
'Content-Type': 'application/json'
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
if (response.data.error) {
|
|
199
|
+
throw new Error(response.data.error_description || response.data.error);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
accessToken: response.data.access_token,
|
|
204
|
+
refreshToken: response.data.refresh_token || refreshToken,
|
|
205
|
+
scope: response.data.scope,
|
|
206
|
+
tokenType: response.data.token_type
|
|
207
|
+
};
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (error.response) {
|
|
210
|
+
throw new Error(`GitHub token refresh failed: ${error.response.data.error_description || error.response.statusText}`);
|
|
211
|
+
}
|
|
212
|
+
throw new Error(`GitHub token refresh failed: ${error.message}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Generate a cryptographically secure state parameter
|
|
218
|
+
* @returns {string} State parameter for CSRF protection
|
|
219
|
+
*/
|
|
220
|
+
generateState() {
|
|
221
|
+
return crypto.randomBytes(32).toString('hex');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Verify state parameter matches
|
|
226
|
+
* @param {string} provided - State from callback
|
|
227
|
+
* @param {string} expected - Expected state
|
|
228
|
+
* @returns {boolean} True if valid
|
|
229
|
+
*/
|
|
230
|
+
verifyState(provided, expected) {
|
|
231
|
+
if (!provided || !expected) return false;
|
|
232
|
+
return crypto.timingSafeEqual(
|
|
233
|
+
Buffer.from(provided),
|
|
234
|
+
Buffer.from(expected)
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get full user info including email
|
|
240
|
+
* @param {string} accessToken - GitHub access token
|
|
241
|
+
* @returns {Promise<object>} Complete user profile
|
|
242
|
+
*/
|
|
243
|
+
async getFullUserInfo(accessToken) {
|
|
244
|
+
const profile = await this.getUserProfile(accessToken);
|
|
245
|
+
const emails = await this.getUserEmails(accessToken);
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
...profile,
|
|
249
|
+
email: profile.email || emails.primary,
|
|
250
|
+
emailVerified: !!emails.primary
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = new GitHubService();
|
|
@@ -128,6 +128,31 @@ function registerLimiter(limiterId, { label, integration, inferredMountPath } =
|
|
|
128
128
|
return next;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
function getLimiterDefaultConfig(limiterId) {
|
|
132
|
+
// Default configuration for specific limiters
|
|
133
|
+
const defaults = {
|
|
134
|
+
// Waiting list endpoints - strict rate limiting to prevent spam
|
|
135
|
+
waitingListSubscribeLimiter: {
|
|
136
|
+
enabled: true,
|
|
137
|
+
mode: 'enforce',
|
|
138
|
+
algorithm: 'fixedWindow',
|
|
139
|
+
limit: { max: 1, windowMs: 60000 }, // 1 request per minute
|
|
140
|
+
identity: { type: 'ip' },
|
|
141
|
+
metrics: { enabled: true, bucketMs: 60000, retentionDays: 14 }
|
|
142
|
+
},
|
|
143
|
+
waitingListStatsLimiter: {
|
|
144
|
+
enabled: true,
|
|
145
|
+
mode: 'reportOnly', // Start with monitoring
|
|
146
|
+
algorithm: 'fixedWindow',
|
|
147
|
+
limit: { max: 60, windowMs: 60000 }, // 60 requests per minute
|
|
148
|
+
identity: { type: 'ip' },
|
|
149
|
+
metrics: { enabled: true, bucketMs: 60000, retentionDays: 14 }
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return defaults[limiterId] || null;
|
|
154
|
+
}
|
|
155
|
+
|
|
131
156
|
async function ensureLimiterOverrideExists(limiterId) {
|
|
132
157
|
const id = normalizeLimiterId(limiterId);
|
|
133
158
|
|
|
@@ -156,10 +181,13 @@ async function ensureLimiterOverrideExists(limiterId) {
|
|
|
156
181
|
return;
|
|
157
182
|
}
|
|
158
183
|
|
|
184
|
+
// Get default config for this limiter, or use minimal enabled: false
|
|
185
|
+
const defaultConfig = getLimiterDefaultConfig(id) || { enabled: false };
|
|
186
|
+
|
|
159
187
|
const updated = {
|
|
160
188
|
version: Number(data?.version || 1) || 1,
|
|
161
189
|
defaults: data?.defaults || {},
|
|
162
|
-
limiters: { ...(data?.limiters || {}), [id]:
|
|
190
|
+
limiters: { ...(data?.limiters || {}), [id]: defaultConfig },
|
|
163
191
|
};
|
|
164
192
|
|
|
165
193
|
doc.jsonRaw = JSON.stringify(updated, null, 2);
|
|
@@ -290,9 +290,9 @@ async function getWaitingListStats(options = {}) {
|
|
|
290
290
|
* Get paginated waiting list entries for admin
|
|
291
291
|
*/
|
|
292
292
|
async function getWaitingListEntriesAdmin(filters = {}) {
|
|
293
|
-
const { status, type, email, limit = 50, offset = 0 } = filters;
|
|
293
|
+
const { status, type, email, limit = 50, offset = 0, bypassCache = false } = filters;
|
|
294
294
|
|
|
295
|
-
const { entries } = await getWaitingListEntries();
|
|
295
|
+
const { entries } = await getWaitingListEntries({ bypassCache });
|
|
296
296
|
|
|
297
297
|
// Apply filters
|
|
298
298
|
let filteredEntries = entries;
|
|
@@ -333,6 +333,32 @@ async function getWaitingListEntriesAdmin(filters = {}) {
|
|
|
333
333
|
};
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
+
/**
|
|
337
|
+
* Get available types with counts
|
|
338
|
+
*/
|
|
339
|
+
async function getAvailableTypes() {
|
|
340
|
+
const { entries } = await getWaitingListEntries();
|
|
341
|
+
|
|
342
|
+
// Count entries by type
|
|
343
|
+
const typeCounts = entries.reduce((acc, entry) => {
|
|
344
|
+
const type = String(entry.type || 'unknown').trim();
|
|
345
|
+
if (type) {
|
|
346
|
+
acc[type] = (acc[type] || 0) + 1;
|
|
347
|
+
}
|
|
348
|
+
return acc;
|
|
349
|
+
}, {});
|
|
350
|
+
|
|
351
|
+
// Convert to sorted array
|
|
352
|
+
const types = Object.entries(typeCounts)
|
|
353
|
+
.map(([type, count]) => ({ type, count }))
|
|
354
|
+
.sort((a, b) => b.count - a.count);
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
types,
|
|
358
|
+
total: entries.length
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
336
362
|
/**
|
|
337
363
|
* Clear all waiting list related caches
|
|
338
364
|
*/
|
|
@@ -352,6 +378,8 @@ function getWaitingListCacheInfo() {
|
|
|
352
378
|
|
|
353
379
|
/**
|
|
354
380
|
* Initialize waiting list data structure if it doesn't exist
|
|
381
|
+
* Note: cacheTtlSeconds MUST be 0 for persistence use cases (waiting list, rate limiters)
|
|
382
|
+
* to ensure real-time data consistency. Caching would cause stale reads.
|
|
355
383
|
*/
|
|
356
384
|
async function initializeWaitingListData() {
|
|
357
385
|
try {
|
|
@@ -368,7 +396,7 @@ async function initializeWaitingListData() {
|
|
|
368
396
|
lastUpdated: new Date().toISOString()
|
|
369
397
|
}),
|
|
370
398
|
publicEnabled: false,
|
|
371
|
-
cacheTtlSeconds:
|
|
399
|
+
cacheTtlSeconds: 0 // No caching - required for real-time persistence
|
|
372
400
|
});
|
|
373
401
|
} else {
|
|
374
402
|
throw error;
|
|
@@ -384,6 +412,7 @@ module.exports = {
|
|
|
384
412
|
removeWaitingListEntry,
|
|
385
413
|
getWaitingListStats,
|
|
386
414
|
getWaitingListEntriesAdmin,
|
|
415
|
+
getAvailableTypes,
|
|
387
416
|
|
|
388
417
|
// Cache management
|
|
389
418
|
clearWaitingListCache,
|