Tristan Yu commited on
Commit
3a96436
·
0 Parent(s):

Initial backend deployment with improved stability

Browse files
.gitignore ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+ npm-debug.log*
4
+ yarn-debug.log*
5
+ yarn-error.log*
6
+
7
+ # Environment variables
8
+ .env
9
+ .env.local
10
+ .env.development.local
11
+ .env.test.local
12
+ .env.production.local
13
+
14
+ # Logs
15
+ logs/
16
+ *.log
17
+
18
+ # Runtime data
19
+ pids/
20
+ *.pid
21
+ *.seed
22
+ *.pid.lock
23
+
24
+ # Coverage directory used by tools like istanbul
25
+ coverage/
26
+
27
+ # nyc test coverage
28
+ .nyc_output
29
+
30
+ # Dependency directories
31
+ node_modules/
32
+ jspm_packages/
33
+
34
+ # Optional npm cache directory
35
+ .npm
36
+
37
+ # Optional REPL history
38
+ .node_repl_history
39
+
40
+ # Output of 'npm pack'
41
+ *.tgz
42
+
43
+ # Yarn Integrity file
44
+ .yarn-integrity
45
+
46
+ # dotenv environment variables file
47
+ .env
48
+
49
+ # IDE files
50
+ .vscode/
51
+ .idea/
52
+ *.swp
53
+ *.swo
54
+
55
+ # OS generated files
56
+ .DS_Store
57
+ .DS_Store?
58
+ ._*
59
+ .Spotlight-V100
60
+ .Trashes
61
+ ehthumbs.db
62
+ Thumbs.db
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Node.js 18 Alpine for smaller image size
2
+ FROM node:18-alpine
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Copy package files
8
+ COPY package*.json ./
9
+
10
+ # Install dependencies
11
+ RUN npm install --only=production
12
+
13
+ # Copy server source code
14
+ COPY . ./
15
+
16
+ # Create a non-root user
17
+ RUN addgroup -g 1001 -S nodejs
18
+ RUN adduser -S nodejs -u 1001
19
+
20
+ # Change ownership of the app directory
21
+ RUN chown -R nodejs:nodejs /app
22
+ USER nodejs
23
+
24
+ # Expose port
25
+ EXPOSE 5000
26
+
27
+ # Health check with longer start period and more retries
28
+ HEALTHCHECK --interval=60s --timeout=10s --start-period=120s --retries=5 \
29
+ CMD curl -f http://localhost:5000/api/health || exit 1
30
+
31
+ # Start the application
32
+ CMD ["npm", "start"]
index.js ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const cors = require('cors');
3
+ const mongoose = require('mongoose');
4
+ const dotenv = require('dotenv');
5
+ const rateLimit = require('express-rate-limit');
6
+
7
+ // Import routes
8
+ const { router: authRoutes } = require('./routes/auth');
9
+ const sourceTextRoutes = require('./routes/sourceTexts');
10
+ const submissionRoutes = require('./routes/submissions');
11
+ const searchRoutes = require('./routes/search');
12
+
13
+ dotenv.config();
14
+
15
+ // Global error handlers to prevent crashes
16
+ process.on('uncaughtException', (error) => {
17
+ console.error('Uncaught Exception:', error);
18
+ // Don't exit immediately, try to log and continue
19
+ console.error('Stack trace:', error.stack);
20
+ });
21
+
22
+ process.on('unhandledRejection', (reason, promise) => {
23
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
24
+ // Don't exit immediately, try to log and continue
25
+ console.error('Stack trace:', reason?.stack);
26
+ });
27
+
28
+ // Memory leak prevention
29
+ process.on('warning', (warning) => {
30
+ console.warn('Node.js warning:', warning.name, warning.message);
31
+ });
32
+
33
+ const app = express();
34
+ const PORT = process.env.PORT || 5000;
35
+
36
+ // Trust proxy for rate limiting
37
+ app.set('trust proxy', 1);
38
+
39
+ // Rate limiting
40
+ const limiter = rateLimit({
41
+ windowMs: 15 * 60 * 1000, // 15 minutes
42
+ max: 100 // limit each IP to 100 requests per windowMs
43
+ });
44
+
45
+ // Middleware
46
+ app.use(cors());
47
+ app.use(express.json({ limit: '10mb' }));
48
+ app.use(limiter);
49
+
50
+ // Database connection with better error handling
51
+ mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/transcreation-sandbox', {
52
+ maxPoolSize: 10,
53
+ serverSelectionTimeoutMS: 5000,
54
+ socketTimeoutMS: 45000,
55
+ })
56
+ .then(() => {
57
+ console.log('Connected to MongoDB');
58
+ })
59
+ .catch(err => {
60
+ console.error('MongoDB connection error:', err);
61
+ // Don't exit immediately, try to reconnect
62
+ setTimeout(() => {
63
+ console.log('Attempting to reconnect to MongoDB...');
64
+ mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/transcreation-sandbox');
65
+ }, 5000);
66
+ });
67
+
68
+ // Handle MongoDB connection errors
69
+ mongoose.connection.on('error', (err) => {
70
+ console.error('MongoDB connection error:', err);
71
+ });
72
+
73
+ mongoose.connection.on('disconnected', () => {
74
+ console.log('MongoDB disconnected');
75
+ });
76
+
77
+ // Routes
78
+ app.use('/api/auth', authRoutes);
79
+ app.use('/api/source-texts', sourceTextRoutes);
80
+ app.use('/api/submissions', submissionRoutes);
81
+ app.use('/api/search', searchRoutes);
82
+
83
+ // Health check endpoint
84
+ app.get('/api/health', (req, res) => {
85
+ res.json({ status: 'OK', message: 'Transcreation Sandbox API is running' });
86
+ });
87
+
88
+ // Simple health check for Hugging Face Spaces
89
+ app.get('/health', (req, res) => {
90
+ res.status(200).send('OK');
91
+ });
92
+
93
+ // Error handling middleware
94
+ app.use((err, req, res, next) => {
95
+ console.error(err.stack);
96
+ res.status(500).json({ error: 'Something went wrong!' });
97
+ });
98
+
99
+ app.listen(PORT, () => {
100
+ console.log(`Server running on port ${PORT}`);
101
+
102
+ // Initialize week 1 data after server starts
103
+ const initializeWeek1 = async () => {
104
+ try {
105
+ console.log('Starting week 1 data initialization...');
106
+
107
+ // Check if week 1 tutorial tasks exist
108
+ const existingTutorialTasks = await SourceText.find({
109
+ category: 'tutorial',
110
+ weekNumber: 1
111
+ });
112
+
113
+ if (existingTutorialTasks.length === 0) {
114
+ console.log('Initializing week 1 tutorial tasks...');
115
+ const tutorialTasks = [
116
+ {
117
+ title: 'Tutorial Task 1 - Introduction',
118
+ content: 'The opening paragraph establishes the main theme and provides essential context for the reader to understand the subsequent discussion.',
119
+ category: 'tutorial',
120
+ weekNumber: 1,
121
+ sourceLanguage: 'English',
122
+ sourceCulture: 'Western',
123
+ translationBrief: 'The municipal government of Xiamen plans to launch an international campaign to attract foreign investors and tourists. As part of the campaign, they\'re commissioning an English translation of a Chinese booklet about the city\'s history of foreign trade and cultural exchange, in addition to its tourist attractions. You\'ve been provided with the following excerpt for a test translation. They are aware that certain elements in the source text might not work well when translated into English for promotional purposes. So you\'re given the license to make changes where appropriate, but a list of such changes should be provided for the reference of the commissioner.'
124
+ },
125
+ {
126
+ title: 'Tutorial Task 2 - Development',
127
+ content: 'The second paragraph develops the argument further, providing supporting evidence and examples that reinforce the main points established in the opening section.',
128
+ category: 'tutorial',
129
+ weekNumber: 1,
130
+ sourceLanguage: 'English',
131
+ sourceCulture: 'Western',
132
+ translationBrief: 'The municipal government of Xiamen plans to launch an international campaign to attract foreign investors and tourists. As part of the campaign, they\'re commissioning an English translation of a Chinese booklet about the city\'s history of foreign trade and cultural exchange, in addition to its tourist attractions. You\'ve been provided with the following excerpt for a test translation. They are aware that certain elements in the source text might not work well when translated into English for promotional purposes. So you\'re given the license to make changes where appropriate, but a list of such changes should be provided for the reference of the commissioner.'
133
+ },
134
+ {
135
+ title: 'Tutorial Task 3 - Conclusion',
136
+ content: 'The concluding paragraph brings together all the key elements discussed throughout the text, offering a synthesis of the main ideas and leaving the reader with a clear understanding of the central message.',
137
+ category: 'tutorial',
138
+ weekNumber: 1,
139
+ sourceLanguage: 'English',
140
+ sourceCulture: 'Western',
141
+ translationBrief: 'The municipal government of Xiamen plans to launch an international campaign to attract foreign investors and tourists. As part of the campaign, they\'re commissioning an English translation of a Chinese booklet about the city\'s history of foreign trade and cultural exchange, in addition to its tourist attractions. You\'ve been provided with the following excerpt for a test translation. They are aware that certain elements in the source text might not work well when translated into English for promotional purposes. So you\'re given the license to make changes where appropriate, but a list of such changes should be provided for the reference of the commissioner.'
142
+ }
143
+ ];
144
+ await SourceText.insertMany(tutorialTasks);
145
+ console.log('Week 1 tutorial tasks initialized successfully');
146
+ }
147
+
148
+ // Check if week 1 weekly practice exists
149
+ const existingWeeklyPractice = await SourceText.find({
150
+ category: 'weekly-practice',
151
+ weekNumber: 1
152
+ });
153
+
154
+ if (existingWeeklyPractice.length === 0) {
155
+ console.log('Initializing week 1 weekly practice...');
156
+ const weeklyPractice = [
157
+ {
158
+ title: 'Chinese Pun 1',
159
+ content: '为什么睡前一定要吃夜宵?因为这样才不会做饿梦。',
160
+ category: 'weekly-practice',
161
+ weekNumber: 1,
162
+ sourceLanguage: 'Chinese',
163
+ sourceCulture: 'Chinese'
164
+ },
165
+ {
166
+ title: 'Chinese Pun 2',
167
+ content: '女娲用什么补天?强扭的瓜。',
168
+ category: 'weekly-practice',
169
+ weekNumber: 1,
170
+ sourceLanguage: 'Chinese',
171
+ sourceCulture: 'Chinese'
172
+ },
173
+ {
174
+ title: 'English Pun 1',
175
+ content: 'Why do we drive on a parkway and park on a driveway?',
176
+ category: 'weekly-practice',
177
+ weekNumber: 1,
178
+ sourceLanguage: 'English',
179
+ sourceCulture: 'Western'
180
+ },
181
+ {
182
+ title: 'English Pun 2',
183
+ content: 'I can\'t believe I got fired from the calendar factory. All I did was take a day off.',
184
+ category: 'weekly-practice',
185
+ weekNumber: 1,
186
+ sourceLanguage: 'English',
187
+ sourceCulture: 'Western'
188
+ }
189
+ ];
190
+ await SourceText.insertMany(weeklyPractice);
191
+ console.log('Week 1 weekly practice initialized successfully');
192
+ }
193
+
194
+ console.log('Week 1 data initialization completed successfully');
195
+ } catch (error) {
196
+ console.error('Error initializing week 1 data:', error);
197
+ }
198
+ };
199
+
200
+ // Start initialization after a short delay
201
+ setTimeout(initializeWeek1, 1000);
202
+
203
+ // Keep-alive mechanism
204
+ setInterval(() => {
205
+ console.log('Server heartbeat - still running');
206
+ }, 300000); // Log every 5 minutes
207
+ });
208
+
209
+ // Graceful shutdown
210
+ process.on('SIGTERM', async () => {
211
+ console.log('SIGTERM received, shutting down gracefully');
212
+ try {
213
+ if (mongoose.connection.readyState === 1) {
214
+ await mongoose.connection.close();
215
+ console.log('MongoDB connection closed');
216
+ }
217
+ process.exit(0);
218
+ } catch (error) {
219
+ console.error('Error closing MongoDB connection:', error);
220
+ process.exit(1);
221
+ }
222
+ });
223
+
224
+ process.on('SIGINT', async () => {
225
+ console.log('SIGINT received, shutting down gracefully');
226
+ try {
227
+ if (mongoose.connection.readyState === 1) {
228
+ await mongoose.connection.close();
229
+ console.log('MongoDB connection closed');
230
+ }
231
+ process.exit(0);
232
+ } catch (error) {
233
+ console.error('Error closing MongoDB connection:', error);
234
+ process.exit(1);
235
+ }
236
+ });
237
+
238
+ // Keep the process alive
239
+ process.on('exit', (code) => {
240
+ console.log(`Process exiting with code: ${code}`);
241
+ });
models/SourceText.js ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+
3
+ const culturalElementSchema = new mongoose.Schema({
4
+ element: { type: String, required: true },
5
+ description: { type: String, required: true },
6
+ significance: { type: String, required: true }
7
+ });
8
+
9
+ const sourceTextSchema = new mongoose.Schema({
10
+ title: { type: String, required: true },
11
+ content: { type: String, required: true },
12
+ sourceLanguage: { type: String, required: true },
13
+
14
+ sourceType: {
15
+ type: String,
16
+ enum: ['api', 'manual', 'practice', 'tutorial', 'weekly-practice'],
17
+ default: 'manual'
18
+ },
19
+ category: {
20
+ type: String,
21
+ enum: ['practice', 'tutorial', 'weekly-practice'],
22
+ required: true
23
+ },
24
+ weekNumber: {
25
+ type: Number,
26
+ required: function() { return this.category !== 'practice'; }
27
+ },
28
+ translationBrief: { type: String },
29
+ culturalElements: [culturalElementSchema],
30
+ difficulty: {
31
+ type: String,
32
+ enum: ['beginner', 'intermediate', 'advanced'],
33
+ default: 'intermediate'
34
+ },
35
+ tags: [String],
36
+ targetCultures: [String],
37
+ isActive: { type: Boolean, default: true },
38
+ usageCount: { type: Number, default: 0 },
39
+ averageRating: { type: Number, default: 0 },
40
+ ratingCount: { type: Number, default: 0 }
41
+ }, {
42
+ timestamps: true
43
+ });
44
+
45
+ module.exports = mongoose.model('SourceText', sourceTextSchema);
models/Submission.js ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+
3
+ const feedbackSchema = new mongoose.Schema({
4
+ userId: {
5
+ type: mongoose.Schema.Types.ObjectId,
6
+ ref: 'User',
7
+ required: true
8
+ },
9
+ comment: {
10
+ type: String,
11
+ required: true,
12
+ trim: true
13
+ },
14
+ rating: {
15
+ type: Number,
16
+ min: 1,
17
+ max: 5
18
+ },
19
+ createdAt: {
20
+ type: Date,
21
+ default: Date.now
22
+ }
23
+ });
24
+
25
+ const voteSchema = new mongoose.Schema({
26
+ userId: {
27
+ type: mongoose.Schema.Types.ObjectId,
28
+ ref: 'User',
29
+ required: true
30
+ },
31
+ rank: {
32
+ type: Number,
33
+ enum: [1, 2, 3], // 1 = 1st place, 2 = 2nd place, 3 = 3rd place
34
+ required: true
35
+ },
36
+ createdAt: {
37
+ type: Date,
38
+ default: Date.now
39
+ }
40
+ });
41
+
42
+ const submissionSchema = new mongoose.Schema({
43
+ sourceTextId: {
44
+ type: mongoose.Schema.Types.ObjectId,
45
+ ref: 'SourceText',
46
+ required: true
47
+ },
48
+ userId: {
49
+ type: mongoose.Schema.Types.ObjectId,
50
+ ref: 'User',
51
+ required: true
52
+ },
53
+ username: {
54
+ type: String,
55
+ required: true
56
+ },
57
+ groupNumber: {
58
+ type: Number,
59
+ min: 1,
60
+ max: 8,
61
+ required: function() {
62
+ // Group number is required for tutorial tasks, optional for other submissions
63
+ return this.sourceTextId && this.sourceTextId.category === 'tutorial';
64
+ }
65
+ },
66
+ targetCulture: {
67
+ type: String,
68
+ required: true
69
+ },
70
+ targetLanguage: {
71
+ type: String,
72
+ required: true
73
+ },
74
+ transcreation: {
75
+ type: String,
76
+ required: true
77
+ },
78
+ explanation: {
79
+ type: String,
80
+ required: true
81
+ },
82
+ culturalAdaptations: [{
83
+ type: String
84
+ }],
85
+ isAnonymous: {
86
+ type: Boolean,
87
+ default: true
88
+ },
89
+ status: {
90
+ type: String,
91
+ enum: ['draft', 'submitted', 'reviewed', 'approved', 'rejected'],
92
+ default: 'submitted'
93
+ },
94
+ difficulty: {
95
+ type: String,
96
+ enum: ['beginner', 'intermediate', 'advanced'],
97
+ default: 'intermediate'
98
+ },
99
+ votes: [voteSchema],
100
+ feedback: [{
101
+ userId: {
102
+ type: mongoose.Schema.Types.ObjectId,
103
+ ref: 'User'
104
+ },
105
+ comment: String,
106
+ createdAt: {
107
+ type: Date,
108
+ default: Date.now
109
+ }
110
+ }],
111
+ createdAt: {
112
+ type: Date,
113
+ default: Date.now
114
+ },
115
+ updatedAt: {
116
+ type: Date,
117
+ default: Date.now
118
+ }
119
+ });
120
+
121
+ // Calculate score based on votes (1st place = 3 points, 2nd place = 2 points, 3rd place = 1 point)
122
+ submissionSchema.methods.calculateScore = function() {
123
+ return this.votes.reduce((total, vote) => {
124
+ const points = 4 - vote.rank; // 1st = 3 points, 2nd = 2 points, 3rd = 1 point
125
+ return total + points;
126
+ }, 0);
127
+ };
128
+
129
+ // Get vote count by rank
130
+ submissionSchema.methods.getVoteCountByRank = function() {
131
+ const counts = { 1: 0, 2: 0, 3: 0 };
132
+ this.votes.forEach(vote => {
133
+ counts[vote.rank]++;
134
+ });
135
+ return counts;
136
+ };
137
+
138
+ // Update score before saving
139
+ submissionSchema.pre('save', function(next) {
140
+ this.score = this.calculateScore();
141
+ this.updatedAt = Date.now();
142
+ next();
143
+ });
144
+
145
+ // Index for efficient querying
146
+ submissionSchema.index({
147
+ sourceTextId: 1,
148
+ targetCulture: 1,
149
+ status: 1,
150
+ createdAt: -1
151
+ });
152
+
153
+ module.exports = mongoose.model('Submission', submissionSchema);
models/User.js ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+ const bcrypt = require('bcryptjs');
3
+
4
+ const userSchema = new mongoose.Schema({
5
+ username: {
6
+ type: String,
7
+ required: true,
8
+ unique: true,
9
+ trim: true,
10
+ minlength: 3
11
+ },
12
+ email: {
13
+ type: String,
14
+ required: true,
15
+ unique: true,
16
+ trim: true,
17
+ lowercase: true
18
+ },
19
+ password: {
20
+ type: String,
21
+ required: true,
22
+ minlength: 6
23
+ },
24
+ role: {
25
+ type: String,
26
+ enum: ['student', 'instructor', 'admin'],
27
+ default: 'student'
28
+ },
29
+ targetCultures: [{
30
+ type: String,
31
+ trim: true
32
+ }],
33
+ nativeLanguage: {
34
+ type: String,
35
+ trim: true
36
+ },
37
+ createdAt: {
38
+ type: Date,
39
+ default: Date.now
40
+ },
41
+ lastActive: {
42
+ type: Date,
43
+ default: Date.now
44
+ }
45
+ });
46
+
47
+ // Hash password before saving
48
+ userSchema.pre('save', async function(next) {
49
+ if (!this.isModified('password')) return next();
50
+
51
+ try {
52
+ const salt = await bcrypt.genSalt(10);
53
+ this.password = await bcrypt.hash(this.password, salt);
54
+ next();
55
+ } catch (error) {
56
+ next(error);
57
+ }
58
+ });
59
+
60
+ // Method to compare passwords
61
+ userSchema.methods.comparePassword = async function(candidatePassword) {
62
+ return bcrypt.compare(candidatePassword, this.password);
63
+ };
64
+
65
+ module.exports = mongoose.model('User', userSchema);
monitor.js ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const http = require('http');
2
+
3
+ function checkServerHealth() {
4
+ const options = {
5
+ hostname: 'localhost',
6
+ port: 5000,
7
+ path: '/api/health',
8
+ method: 'GET',
9
+ timeout: 5000
10
+ };
11
+
12
+ const req = http.request(options, (res) => {
13
+ let data = '';
14
+ res.on('data', (chunk) => {
15
+ data += chunk;
16
+ });
17
+ res.on('end', () => {
18
+ try {
19
+ const response = JSON.parse(data);
20
+ if (response.status === 'OK') {
21
+ console.log(`[${new Date().toISOString()}] Server is healthy`);
22
+ } else {
23
+ console.error(`[${new Date().toISOString()}] Server returned unexpected status:`, response);
24
+ }
25
+ } catch (error) {
26
+ console.error(`[${new Date().toISOString()}] Failed to parse health check response:`, error);
27
+ }
28
+ });
29
+ });
30
+
31
+ req.on('error', (error) => {
32
+ console.error(`[${new Date().toISOString()}] Server health check failed:`, error.message);
33
+ });
34
+
35
+ req.on('timeout', () => {
36
+ console.error(`[${new Date().toISOString()}] Server health check timed out`);
37
+ req.destroy();
38
+ });
39
+
40
+ req.end();
41
+ }
42
+
43
+ // Check server health every 30 seconds
44
+ setInterval(checkServerHealth, 30000);
45
+
46
+ // Initial check
47
+ checkServerHealth();
48
+
49
+ console.log('Server monitoring started. Health checks every 30 seconds.');
package-lock.json ADDED
@@ -0,0 +1,2065 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "transcreation-sandbox-server",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "transcreation-sandbox-server",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "axios": "^1.6.2",
12
+ "bcryptjs": "^2.4.3",
13
+ "cheerio": "^1.0.0-rc.12",
14
+ "cors": "^2.8.5",
15
+ "dotenv": "^16.3.1",
16
+ "express": "^4.18.2",
17
+ "express-rate-limit": "^7.1.5",
18
+ "express-validator": "^7.2.1",
19
+ "jsonwebtoken": "^9.0.2",
20
+ "mongoose": "^8.0.3",
21
+ "uuid": "^9.0.1"
22
+ },
23
+ "devDependencies": {
24
+ "nodemon": "^3.0.2"
25
+ }
26
+ },
27
+ "node_modules/@mongodb-js/saslprep": {
28
+ "version": "1.3.0",
29
+ "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz",
30
+ "integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==",
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "sparse-bitfield": "^3.0.3"
34
+ }
35
+ },
36
+ "node_modules/@types/webidl-conversions": {
37
+ "version": "7.0.3",
38
+ "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
39
+ "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
40
+ "license": "MIT"
41
+ },
42
+ "node_modules/@types/whatwg-url": {
43
+ "version": "11.0.5",
44
+ "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
45
+ "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
46
+ "license": "MIT",
47
+ "dependencies": {
48
+ "@types/webidl-conversions": "*"
49
+ }
50
+ },
51
+ "node_modules/accepts": {
52
+ "version": "1.3.8",
53
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
54
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
55
+ "license": "MIT",
56
+ "dependencies": {
57
+ "mime-types": "~2.1.34",
58
+ "negotiator": "0.6.3"
59
+ },
60
+ "engines": {
61
+ "node": ">= 0.6"
62
+ }
63
+ },
64
+ "node_modules/anymatch": {
65
+ "version": "3.1.3",
66
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
67
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
68
+ "dev": true,
69
+ "license": "ISC",
70
+ "dependencies": {
71
+ "normalize-path": "^3.0.0",
72
+ "picomatch": "^2.0.4"
73
+ },
74
+ "engines": {
75
+ "node": ">= 8"
76
+ }
77
+ },
78
+ "node_modules/array-flatten": {
79
+ "version": "1.1.1",
80
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
81
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
82
+ "license": "MIT"
83
+ },
84
+ "node_modules/asynckit": {
85
+ "version": "0.4.0",
86
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
87
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
88
+ "license": "MIT"
89
+ },
90
+ "node_modules/axios": {
91
+ "version": "1.11.0",
92
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
93
+ "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
94
+ "license": "MIT",
95
+ "dependencies": {
96
+ "follow-redirects": "^1.15.6",
97
+ "form-data": "^4.0.4",
98
+ "proxy-from-env": "^1.1.0"
99
+ }
100
+ },
101
+ "node_modules/balanced-match": {
102
+ "version": "1.0.2",
103
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
104
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
105
+ "dev": true,
106
+ "license": "MIT"
107
+ },
108
+ "node_modules/bcryptjs": {
109
+ "version": "2.4.3",
110
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
111
+ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
112
+ "license": "MIT"
113
+ },
114
+ "node_modules/binary-extensions": {
115
+ "version": "2.3.0",
116
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
117
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
118
+ "dev": true,
119
+ "license": "MIT",
120
+ "engines": {
121
+ "node": ">=8"
122
+ },
123
+ "funding": {
124
+ "url": "https://github.com/sponsors/sindresorhus"
125
+ }
126
+ },
127
+ "node_modules/body-parser": {
128
+ "version": "1.20.3",
129
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
130
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
131
+ "license": "MIT",
132
+ "dependencies": {
133
+ "bytes": "3.1.2",
134
+ "content-type": "~1.0.5",
135
+ "debug": "2.6.9",
136
+ "depd": "2.0.0",
137
+ "destroy": "1.2.0",
138
+ "http-errors": "2.0.0",
139
+ "iconv-lite": "0.4.24",
140
+ "on-finished": "2.4.1",
141
+ "qs": "6.13.0",
142
+ "raw-body": "2.5.2",
143
+ "type-is": "~1.6.18",
144
+ "unpipe": "1.0.0"
145
+ },
146
+ "engines": {
147
+ "node": ">= 0.8",
148
+ "npm": "1.2.8000 || >= 1.4.16"
149
+ }
150
+ },
151
+ "node_modules/body-parser/node_modules/iconv-lite": {
152
+ "version": "0.4.24",
153
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
154
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
155
+ "license": "MIT",
156
+ "dependencies": {
157
+ "safer-buffer": ">= 2.1.2 < 3"
158
+ },
159
+ "engines": {
160
+ "node": ">=0.10.0"
161
+ }
162
+ },
163
+ "node_modules/boolbase": {
164
+ "version": "1.0.0",
165
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
166
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
167
+ "license": "ISC"
168
+ },
169
+ "node_modules/brace-expansion": {
170
+ "version": "1.1.12",
171
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
172
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
173
+ "dev": true,
174
+ "license": "MIT",
175
+ "dependencies": {
176
+ "balanced-match": "^1.0.0",
177
+ "concat-map": "0.0.1"
178
+ }
179
+ },
180
+ "node_modules/braces": {
181
+ "version": "3.0.3",
182
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
183
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
184
+ "dev": true,
185
+ "license": "MIT",
186
+ "dependencies": {
187
+ "fill-range": "^7.1.1"
188
+ },
189
+ "engines": {
190
+ "node": ">=8"
191
+ }
192
+ },
193
+ "node_modules/bson": {
194
+ "version": "6.10.4",
195
+ "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz",
196
+ "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==",
197
+ "license": "Apache-2.0",
198
+ "engines": {
199
+ "node": ">=16.20.1"
200
+ }
201
+ },
202
+ "node_modules/buffer-equal-constant-time": {
203
+ "version": "1.0.1",
204
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
205
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
206
+ "license": "BSD-3-Clause"
207
+ },
208
+ "node_modules/bytes": {
209
+ "version": "3.1.2",
210
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
211
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
212
+ "license": "MIT",
213
+ "engines": {
214
+ "node": ">= 0.8"
215
+ }
216
+ },
217
+ "node_modules/call-bind-apply-helpers": {
218
+ "version": "1.0.2",
219
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
220
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
221
+ "license": "MIT",
222
+ "dependencies": {
223
+ "es-errors": "^1.3.0",
224
+ "function-bind": "^1.1.2"
225
+ },
226
+ "engines": {
227
+ "node": ">= 0.4"
228
+ }
229
+ },
230
+ "node_modules/call-bound": {
231
+ "version": "1.0.4",
232
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
233
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
234
+ "license": "MIT",
235
+ "dependencies": {
236
+ "call-bind-apply-helpers": "^1.0.2",
237
+ "get-intrinsic": "^1.3.0"
238
+ },
239
+ "engines": {
240
+ "node": ">= 0.4"
241
+ },
242
+ "funding": {
243
+ "url": "https://github.com/sponsors/ljharb"
244
+ }
245
+ },
246
+ "node_modules/cheerio": {
247
+ "version": "1.1.2",
248
+ "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz",
249
+ "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==",
250
+ "license": "MIT",
251
+ "dependencies": {
252
+ "cheerio-select": "^2.1.0",
253
+ "dom-serializer": "^2.0.0",
254
+ "domhandler": "^5.0.3",
255
+ "domutils": "^3.2.2",
256
+ "encoding-sniffer": "^0.2.1",
257
+ "htmlparser2": "^10.0.0",
258
+ "parse5": "^7.3.0",
259
+ "parse5-htmlparser2-tree-adapter": "^7.1.0",
260
+ "parse5-parser-stream": "^7.1.2",
261
+ "undici": "^7.12.0",
262
+ "whatwg-mimetype": "^4.0.0"
263
+ },
264
+ "engines": {
265
+ "node": ">=20.18.1"
266
+ },
267
+ "funding": {
268
+ "url": "https://github.com/cheeriojs/cheerio?sponsor=1"
269
+ }
270
+ },
271
+ "node_modules/cheerio-select": {
272
+ "version": "2.1.0",
273
+ "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
274
+ "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
275
+ "license": "BSD-2-Clause",
276
+ "dependencies": {
277
+ "boolbase": "^1.0.0",
278
+ "css-select": "^5.1.0",
279
+ "css-what": "^6.1.0",
280
+ "domelementtype": "^2.3.0",
281
+ "domhandler": "^5.0.3",
282
+ "domutils": "^3.0.1"
283
+ },
284
+ "funding": {
285
+ "url": "https://github.com/sponsors/fb55"
286
+ }
287
+ },
288
+ "node_modules/chokidar": {
289
+ "version": "3.6.0",
290
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
291
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
292
+ "dev": true,
293
+ "license": "MIT",
294
+ "dependencies": {
295
+ "anymatch": "~3.1.2",
296
+ "braces": "~3.0.2",
297
+ "glob-parent": "~5.1.2",
298
+ "is-binary-path": "~2.1.0",
299
+ "is-glob": "~4.0.1",
300
+ "normalize-path": "~3.0.0",
301
+ "readdirp": "~3.6.0"
302
+ },
303
+ "engines": {
304
+ "node": ">= 8.10.0"
305
+ },
306
+ "funding": {
307
+ "url": "https://paulmillr.com/funding/"
308
+ },
309
+ "optionalDependencies": {
310
+ "fsevents": "~2.3.2"
311
+ }
312
+ },
313
+ "node_modules/combined-stream": {
314
+ "version": "1.0.8",
315
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
316
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
317
+ "license": "MIT",
318
+ "dependencies": {
319
+ "delayed-stream": "~1.0.0"
320
+ },
321
+ "engines": {
322
+ "node": ">= 0.8"
323
+ }
324
+ },
325
+ "node_modules/concat-map": {
326
+ "version": "0.0.1",
327
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
328
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
329
+ "dev": true,
330
+ "license": "MIT"
331
+ },
332
+ "node_modules/content-disposition": {
333
+ "version": "0.5.4",
334
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
335
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
336
+ "license": "MIT",
337
+ "dependencies": {
338
+ "safe-buffer": "5.2.1"
339
+ },
340
+ "engines": {
341
+ "node": ">= 0.6"
342
+ }
343
+ },
344
+ "node_modules/content-type": {
345
+ "version": "1.0.5",
346
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
347
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
348
+ "license": "MIT",
349
+ "engines": {
350
+ "node": ">= 0.6"
351
+ }
352
+ },
353
+ "node_modules/cookie": {
354
+ "version": "0.7.1",
355
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
356
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
357
+ "license": "MIT",
358
+ "engines": {
359
+ "node": ">= 0.6"
360
+ }
361
+ },
362
+ "node_modules/cookie-signature": {
363
+ "version": "1.0.6",
364
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
365
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
366
+ "license": "MIT"
367
+ },
368
+ "node_modules/cors": {
369
+ "version": "2.8.5",
370
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
371
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
372
+ "license": "MIT",
373
+ "dependencies": {
374
+ "object-assign": "^4",
375
+ "vary": "^1"
376
+ },
377
+ "engines": {
378
+ "node": ">= 0.10"
379
+ }
380
+ },
381
+ "node_modules/css-select": {
382
+ "version": "5.2.2",
383
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
384
+ "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
385
+ "license": "BSD-2-Clause",
386
+ "dependencies": {
387
+ "boolbase": "^1.0.0",
388
+ "css-what": "^6.1.0",
389
+ "domhandler": "^5.0.2",
390
+ "domutils": "^3.0.1",
391
+ "nth-check": "^2.0.1"
392
+ },
393
+ "funding": {
394
+ "url": "https://github.com/sponsors/fb55"
395
+ }
396
+ },
397
+ "node_modules/css-what": {
398
+ "version": "6.2.2",
399
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
400
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
401
+ "license": "BSD-2-Clause",
402
+ "engines": {
403
+ "node": ">= 6"
404
+ },
405
+ "funding": {
406
+ "url": "https://github.com/sponsors/fb55"
407
+ }
408
+ },
409
+ "node_modules/debug": {
410
+ "version": "2.6.9",
411
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
412
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
413
+ "license": "MIT",
414
+ "dependencies": {
415
+ "ms": "2.0.0"
416
+ }
417
+ },
418
+ "node_modules/delayed-stream": {
419
+ "version": "1.0.0",
420
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
421
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
422
+ "license": "MIT",
423
+ "engines": {
424
+ "node": ">=0.4.0"
425
+ }
426
+ },
427
+ "node_modules/depd": {
428
+ "version": "2.0.0",
429
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
430
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
431
+ "license": "MIT",
432
+ "engines": {
433
+ "node": ">= 0.8"
434
+ }
435
+ },
436
+ "node_modules/destroy": {
437
+ "version": "1.2.0",
438
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
439
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
440
+ "license": "MIT",
441
+ "engines": {
442
+ "node": ">= 0.8",
443
+ "npm": "1.2.8000 || >= 1.4.16"
444
+ }
445
+ },
446
+ "node_modules/dom-serializer": {
447
+ "version": "2.0.0",
448
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
449
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
450
+ "license": "MIT",
451
+ "dependencies": {
452
+ "domelementtype": "^2.3.0",
453
+ "domhandler": "^5.0.2",
454
+ "entities": "^4.2.0"
455
+ },
456
+ "funding": {
457
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
458
+ }
459
+ },
460
+ "node_modules/domelementtype": {
461
+ "version": "2.3.0",
462
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
463
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
464
+ "funding": [
465
+ {
466
+ "type": "github",
467
+ "url": "https://github.com/sponsors/fb55"
468
+ }
469
+ ],
470
+ "license": "BSD-2-Clause"
471
+ },
472
+ "node_modules/domhandler": {
473
+ "version": "5.0.3",
474
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
475
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
476
+ "license": "BSD-2-Clause",
477
+ "dependencies": {
478
+ "domelementtype": "^2.3.0"
479
+ },
480
+ "engines": {
481
+ "node": ">= 4"
482
+ },
483
+ "funding": {
484
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
485
+ }
486
+ },
487
+ "node_modules/domutils": {
488
+ "version": "3.2.2",
489
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
490
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
491
+ "license": "BSD-2-Clause",
492
+ "dependencies": {
493
+ "dom-serializer": "^2.0.0",
494
+ "domelementtype": "^2.3.0",
495
+ "domhandler": "^5.0.3"
496
+ },
497
+ "funding": {
498
+ "url": "https://github.com/fb55/domutils?sponsor=1"
499
+ }
500
+ },
501
+ "node_modules/dotenv": {
502
+ "version": "16.6.1",
503
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
504
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
505
+ "license": "BSD-2-Clause",
506
+ "engines": {
507
+ "node": ">=12"
508
+ },
509
+ "funding": {
510
+ "url": "https://dotenvx.com"
511
+ }
512
+ },
513
+ "node_modules/dunder-proto": {
514
+ "version": "1.0.1",
515
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
516
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
517
+ "license": "MIT",
518
+ "dependencies": {
519
+ "call-bind-apply-helpers": "^1.0.1",
520
+ "es-errors": "^1.3.0",
521
+ "gopd": "^1.2.0"
522
+ },
523
+ "engines": {
524
+ "node": ">= 0.4"
525
+ }
526
+ },
527
+ "node_modules/ecdsa-sig-formatter": {
528
+ "version": "1.0.11",
529
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
530
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
531
+ "license": "Apache-2.0",
532
+ "dependencies": {
533
+ "safe-buffer": "^5.0.1"
534
+ }
535
+ },
536
+ "node_modules/ee-first": {
537
+ "version": "1.1.1",
538
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
539
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
540
+ "license": "MIT"
541
+ },
542
+ "node_modules/encodeurl": {
543
+ "version": "2.0.0",
544
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
545
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
546
+ "license": "MIT",
547
+ "engines": {
548
+ "node": ">= 0.8"
549
+ }
550
+ },
551
+ "node_modules/encoding-sniffer": {
552
+ "version": "0.2.1",
553
+ "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
554
+ "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==",
555
+ "license": "MIT",
556
+ "dependencies": {
557
+ "iconv-lite": "^0.6.3",
558
+ "whatwg-encoding": "^3.1.1"
559
+ },
560
+ "funding": {
561
+ "url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
562
+ }
563
+ },
564
+ "node_modules/entities": {
565
+ "version": "4.5.0",
566
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
567
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
568
+ "license": "BSD-2-Clause",
569
+ "engines": {
570
+ "node": ">=0.12"
571
+ },
572
+ "funding": {
573
+ "url": "https://github.com/fb55/entities?sponsor=1"
574
+ }
575
+ },
576
+ "node_modules/es-define-property": {
577
+ "version": "1.0.1",
578
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
579
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
580
+ "license": "MIT",
581
+ "engines": {
582
+ "node": ">= 0.4"
583
+ }
584
+ },
585
+ "node_modules/es-errors": {
586
+ "version": "1.3.0",
587
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
588
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
589
+ "license": "MIT",
590
+ "engines": {
591
+ "node": ">= 0.4"
592
+ }
593
+ },
594
+ "node_modules/es-object-atoms": {
595
+ "version": "1.1.1",
596
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
597
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
598
+ "license": "MIT",
599
+ "dependencies": {
600
+ "es-errors": "^1.3.0"
601
+ },
602
+ "engines": {
603
+ "node": ">= 0.4"
604
+ }
605
+ },
606
+ "node_modules/es-set-tostringtag": {
607
+ "version": "2.1.0",
608
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
609
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
610
+ "license": "MIT",
611
+ "dependencies": {
612
+ "es-errors": "^1.3.0",
613
+ "get-intrinsic": "^1.2.6",
614
+ "has-tostringtag": "^1.0.2",
615
+ "hasown": "^2.0.2"
616
+ },
617
+ "engines": {
618
+ "node": ">= 0.4"
619
+ }
620
+ },
621
+ "node_modules/escape-html": {
622
+ "version": "1.0.3",
623
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
624
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
625
+ "license": "MIT"
626
+ },
627
+ "node_modules/etag": {
628
+ "version": "1.8.1",
629
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
630
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
631
+ "license": "MIT",
632
+ "engines": {
633
+ "node": ">= 0.6"
634
+ }
635
+ },
636
+ "node_modules/express": {
637
+ "version": "4.21.2",
638
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
639
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
640
+ "license": "MIT",
641
+ "dependencies": {
642
+ "accepts": "~1.3.8",
643
+ "array-flatten": "1.1.1",
644
+ "body-parser": "1.20.3",
645
+ "content-disposition": "0.5.4",
646
+ "content-type": "~1.0.4",
647
+ "cookie": "0.7.1",
648
+ "cookie-signature": "1.0.6",
649
+ "debug": "2.6.9",
650
+ "depd": "2.0.0",
651
+ "encodeurl": "~2.0.0",
652
+ "escape-html": "~1.0.3",
653
+ "etag": "~1.8.1",
654
+ "finalhandler": "1.3.1",
655
+ "fresh": "0.5.2",
656
+ "http-errors": "2.0.0",
657
+ "merge-descriptors": "1.0.3",
658
+ "methods": "~1.1.2",
659
+ "on-finished": "2.4.1",
660
+ "parseurl": "~1.3.3",
661
+ "path-to-regexp": "0.1.12",
662
+ "proxy-addr": "~2.0.7",
663
+ "qs": "6.13.0",
664
+ "range-parser": "~1.2.1",
665
+ "safe-buffer": "5.2.1",
666
+ "send": "0.19.0",
667
+ "serve-static": "1.16.2",
668
+ "setprototypeof": "1.2.0",
669
+ "statuses": "2.0.1",
670
+ "type-is": "~1.6.18",
671
+ "utils-merge": "1.0.1",
672
+ "vary": "~1.1.2"
673
+ },
674
+ "engines": {
675
+ "node": ">= 0.10.0"
676
+ },
677
+ "funding": {
678
+ "type": "opencollective",
679
+ "url": "https://opencollective.com/express"
680
+ }
681
+ },
682
+ "node_modules/express-rate-limit": {
683
+ "version": "7.5.1",
684
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
685
+ "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
686
+ "license": "MIT",
687
+ "engines": {
688
+ "node": ">= 16"
689
+ },
690
+ "funding": {
691
+ "url": "https://github.com/sponsors/express-rate-limit"
692
+ },
693
+ "peerDependencies": {
694
+ "express": ">= 4.11"
695
+ }
696
+ },
697
+ "node_modules/express-validator": {
698
+ "version": "7.2.1",
699
+ "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz",
700
+ "integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==",
701
+ "license": "MIT",
702
+ "dependencies": {
703
+ "lodash": "^4.17.21",
704
+ "validator": "~13.12.0"
705
+ },
706
+ "engines": {
707
+ "node": ">= 8.0.0"
708
+ }
709
+ },
710
+ "node_modules/fill-range": {
711
+ "version": "7.1.1",
712
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
713
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
714
+ "dev": true,
715
+ "license": "MIT",
716
+ "dependencies": {
717
+ "to-regex-range": "^5.0.1"
718
+ },
719
+ "engines": {
720
+ "node": ">=8"
721
+ }
722
+ },
723
+ "node_modules/finalhandler": {
724
+ "version": "1.3.1",
725
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
726
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
727
+ "license": "MIT",
728
+ "dependencies": {
729
+ "debug": "2.6.9",
730
+ "encodeurl": "~2.0.0",
731
+ "escape-html": "~1.0.3",
732
+ "on-finished": "2.4.1",
733
+ "parseurl": "~1.3.3",
734
+ "statuses": "2.0.1",
735
+ "unpipe": "~1.0.0"
736
+ },
737
+ "engines": {
738
+ "node": ">= 0.8"
739
+ }
740
+ },
741
+ "node_modules/follow-redirects": {
742
+ "version": "1.15.9",
743
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
744
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
745
+ "funding": [
746
+ {
747
+ "type": "individual",
748
+ "url": "https://github.com/sponsors/RubenVerborgh"
749
+ }
750
+ ],
751
+ "license": "MIT",
752
+ "engines": {
753
+ "node": ">=4.0"
754
+ },
755
+ "peerDependenciesMeta": {
756
+ "debug": {
757
+ "optional": true
758
+ }
759
+ }
760
+ },
761
+ "node_modules/form-data": {
762
+ "version": "4.0.4",
763
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
764
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
765
+ "license": "MIT",
766
+ "dependencies": {
767
+ "asynckit": "^0.4.0",
768
+ "combined-stream": "^1.0.8",
769
+ "es-set-tostringtag": "^2.1.0",
770
+ "hasown": "^2.0.2",
771
+ "mime-types": "^2.1.12"
772
+ },
773
+ "engines": {
774
+ "node": ">= 6"
775
+ }
776
+ },
777
+ "node_modules/forwarded": {
778
+ "version": "0.2.0",
779
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
780
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
781
+ "license": "MIT",
782
+ "engines": {
783
+ "node": ">= 0.6"
784
+ }
785
+ },
786
+ "node_modules/fresh": {
787
+ "version": "0.5.2",
788
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
789
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
790
+ "license": "MIT",
791
+ "engines": {
792
+ "node": ">= 0.6"
793
+ }
794
+ },
795
+ "node_modules/fsevents": {
796
+ "version": "2.3.3",
797
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
798
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
799
+ "dev": true,
800
+ "hasInstallScript": true,
801
+ "license": "MIT",
802
+ "optional": true,
803
+ "os": [
804
+ "darwin"
805
+ ],
806
+ "engines": {
807
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
808
+ }
809
+ },
810
+ "node_modules/function-bind": {
811
+ "version": "1.1.2",
812
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
813
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
814
+ "license": "MIT",
815
+ "funding": {
816
+ "url": "https://github.com/sponsors/ljharb"
817
+ }
818
+ },
819
+ "node_modules/get-intrinsic": {
820
+ "version": "1.3.0",
821
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
822
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
823
+ "license": "MIT",
824
+ "dependencies": {
825
+ "call-bind-apply-helpers": "^1.0.2",
826
+ "es-define-property": "^1.0.1",
827
+ "es-errors": "^1.3.0",
828
+ "es-object-atoms": "^1.1.1",
829
+ "function-bind": "^1.1.2",
830
+ "get-proto": "^1.0.1",
831
+ "gopd": "^1.2.0",
832
+ "has-symbols": "^1.1.0",
833
+ "hasown": "^2.0.2",
834
+ "math-intrinsics": "^1.1.0"
835
+ },
836
+ "engines": {
837
+ "node": ">= 0.4"
838
+ },
839
+ "funding": {
840
+ "url": "https://github.com/sponsors/ljharb"
841
+ }
842
+ },
843
+ "node_modules/get-proto": {
844
+ "version": "1.0.1",
845
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
846
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
847
+ "license": "MIT",
848
+ "dependencies": {
849
+ "dunder-proto": "^1.0.1",
850
+ "es-object-atoms": "^1.0.0"
851
+ },
852
+ "engines": {
853
+ "node": ">= 0.4"
854
+ }
855
+ },
856
+ "node_modules/glob-parent": {
857
+ "version": "5.1.2",
858
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
859
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
860
+ "dev": true,
861
+ "license": "ISC",
862
+ "dependencies": {
863
+ "is-glob": "^4.0.1"
864
+ },
865
+ "engines": {
866
+ "node": ">= 6"
867
+ }
868
+ },
869
+ "node_modules/gopd": {
870
+ "version": "1.2.0",
871
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
872
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
873
+ "license": "MIT",
874
+ "engines": {
875
+ "node": ">= 0.4"
876
+ },
877
+ "funding": {
878
+ "url": "https://github.com/sponsors/ljharb"
879
+ }
880
+ },
881
+ "node_modules/has-flag": {
882
+ "version": "3.0.0",
883
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
884
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
885
+ "dev": true,
886
+ "license": "MIT",
887
+ "engines": {
888
+ "node": ">=4"
889
+ }
890
+ },
891
+ "node_modules/has-symbols": {
892
+ "version": "1.1.0",
893
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
894
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
895
+ "license": "MIT",
896
+ "engines": {
897
+ "node": ">= 0.4"
898
+ },
899
+ "funding": {
900
+ "url": "https://github.com/sponsors/ljharb"
901
+ }
902
+ },
903
+ "node_modules/has-tostringtag": {
904
+ "version": "1.0.2",
905
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
906
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
907
+ "license": "MIT",
908
+ "dependencies": {
909
+ "has-symbols": "^1.0.3"
910
+ },
911
+ "engines": {
912
+ "node": ">= 0.4"
913
+ },
914
+ "funding": {
915
+ "url": "https://github.com/sponsors/ljharb"
916
+ }
917
+ },
918
+ "node_modules/hasown": {
919
+ "version": "2.0.2",
920
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
921
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
922
+ "license": "MIT",
923
+ "dependencies": {
924
+ "function-bind": "^1.1.2"
925
+ },
926
+ "engines": {
927
+ "node": ">= 0.4"
928
+ }
929
+ },
930
+ "node_modules/htmlparser2": {
931
+ "version": "10.0.0",
932
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz",
933
+ "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==",
934
+ "funding": [
935
+ "https://github.com/fb55/htmlparser2?sponsor=1",
936
+ {
937
+ "type": "github",
938
+ "url": "https://github.com/sponsors/fb55"
939
+ }
940
+ ],
941
+ "license": "MIT",
942
+ "dependencies": {
943
+ "domelementtype": "^2.3.0",
944
+ "domhandler": "^5.0.3",
945
+ "domutils": "^3.2.1",
946
+ "entities": "^6.0.0"
947
+ }
948
+ },
949
+ "node_modules/htmlparser2/node_modules/entities": {
950
+ "version": "6.0.1",
951
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
952
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
953
+ "license": "BSD-2-Clause",
954
+ "engines": {
955
+ "node": ">=0.12"
956
+ },
957
+ "funding": {
958
+ "url": "https://github.com/fb55/entities?sponsor=1"
959
+ }
960
+ },
961
+ "node_modules/http-errors": {
962
+ "version": "2.0.0",
963
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
964
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
965
+ "license": "MIT",
966
+ "dependencies": {
967
+ "depd": "2.0.0",
968
+ "inherits": "2.0.4",
969
+ "setprototypeof": "1.2.0",
970
+ "statuses": "2.0.1",
971
+ "toidentifier": "1.0.1"
972
+ },
973
+ "engines": {
974
+ "node": ">= 0.8"
975
+ }
976
+ },
977
+ "node_modules/iconv-lite": {
978
+ "version": "0.6.3",
979
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
980
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
981
+ "license": "MIT",
982
+ "dependencies": {
983
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
984
+ },
985
+ "engines": {
986
+ "node": ">=0.10.0"
987
+ }
988
+ },
989
+ "node_modules/ignore-by-default": {
990
+ "version": "1.0.1",
991
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
992
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
993
+ "dev": true,
994
+ "license": "ISC"
995
+ },
996
+ "node_modules/inherits": {
997
+ "version": "2.0.4",
998
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
999
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
1000
+ "license": "ISC"
1001
+ },
1002
+ "node_modules/ipaddr.js": {
1003
+ "version": "1.9.1",
1004
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
1005
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
1006
+ "license": "MIT",
1007
+ "engines": {
1008
+ "node": ">= 0.10"
1009
+ }
1010
+ },
1011
+ "node_modules/is-binary-path": {
1012
+ "version": "2.1.0",
1013
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
1014
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
1015
+ "dev": true,
1016
+ "license": "MIT",
1017
+ "dependencies": {
1018
+ "binary-extensions": "^2.0.0"
1019
+ },
1020
+ "engines": {
1021
+ "node": ">=8"
1022
+ }
1023
+ },
1024
+ "node_modules/is-extglob": {
1025
+ "version": "2.1.1",
1026
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1027
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
1028
+ "dev": true,
1029
+ "license": "MIT",
1030
+ "engines": {
1031
+ "node": ">=0.10.0"
1032
+ }
1033
+ },
1034
+ "node_modules/is-glob": {
1035
+ "version": "4.0.3",
1036
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
1037
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
1038
+ "dev": true,
1039
+ "license": "MIT",
1040
+ "dependencies": {
1041
+ "is-extglob": "^2.1.1"
1042
+ },
1043
+ "engines": {
1044
+ "node": ">=0.10.0"
1045
+ }
1046
+ },
1047
+ "node_modules/is-number": {
1048
+ "version": "7.0.0",
1049
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
1050
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
1051
+ "dev": true,
1052
+ "license": "MIT",
1053
+ "engines": {
1054
+ "node": ">=0.12.0"
1055
+ }
1056
+ },
1057
+ "node_modules/jsonwebtoken": {
1058
+ "version": "9.0.2",
1059
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
1060
+ "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
1061
+ "license": "MIT",
1062
+ "dependencies": {
1063
+ "jws": "^3.2.2",
1064
+ "lodash.includes": "^4.3.0",
1065
+ "lodash.isboolean": "^3.0.3",
1066
+ "lodash.isinteger": "^4.0.4",
1067
+ "lodash.isnumber": "^3.0.3",
1068
+ "lodash.isplainobject": "^4.0.6",
1069
+ "lodash.isstring": "^4.0.1",
1070
+ "lodash.once": "^4.0.0",
1071
+ "ms": "^2.1.1",
1072
+ "semver": "^7.5.4"
1073
+ },
1074
+ "engines": {
1075
+ "node": ">=12",
1076
+ "npm": ">=6"
1077
+ }
1078
+ },
1079
+ "node_modules/jsonwebtoken/node_modules/ms": {
1080
+ "version": "2.1.3",
1081
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1082
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1083
+ "license": "MIT"
1084
+ },
1085
+ "node_modules/jwa": {
1086
+ "version": "1.4.2",
1087
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
1088
+ "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
1089
+ "license": "MIT",
1090
+ "dependencies": {
1091
+ "buffer-equal-constant-time": "^1.0.1",
1092
+ "ecdsa-sig-formatter": "1.0.11",
1093
+ "safe-buffer": "^5.0.1"
1094
+ }
1095
+ },
1096
+ "node_modules/jws": {
1097
+ "version": "3.2.2",
1098
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
1099
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
1100
+ "license": "MIT",
1101
+ "dependencies": {
1102
+ "jwa": "^1.4.1",
1103
+ "safe-buffer": "^5.0.1"
1104
+ }
1105
+ },
1106
+ "node_modules/kareem": {
1107
+ "version": "2.6.3",
1108
+ "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
1109
+ "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==",
1110
+ "license": "Apache-2.0",
1111
+ "engines": {
1112
+ "node": ">=12.0.0"
1113
+ }
1114
+ },
1115
+ "node_modules/lodash": {
1116
+ "version": "4.17.21",
1117
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
1118
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
1119
+ "license": "MIT"
1120
+ },
1121
+ "node_modules/lodash.includes": {
1122
+ "version": "4.3.0",
1123
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
1124
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
1125
+ "license": "MIT"
1126
+ },
1127
+ "node_modules/lodash.isboolean": {
1128
+ "version": "3.0.3",
1129
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
1130
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
1131
+ "license": "MIT"
1132
+ },
1133
+ "node_modules/lodash.isinteger": {
1134
+ "version": "4.0.4",
1135
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
1136
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
1137
+ "license": "MIT"
1138
+ },
1139
+ "node_modules/lodash.isnumber": {
1140
+ "version": "3.0.3",
1141
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
1142
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
1143
+ "license": "MIT"
1144
+ },
1145
+ "node_modules/lodash.isplainobject": {
1146
+ "version": "4.0.6",
1147
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
1148
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
1149
+ "license": "MIT"
1150
+ },
1151
+ "node_modules/lodash.isstring": {
1152
+ "version": "4.0.1",
1153
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
1154
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
1155
+ "license": "MIT"
1156
+ },
1157
+ "node_modules/lodash.once": {
1158
+ "version": "4.1.1",
1159
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
1160
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
1161
+ "license": "MIT"
1162
+ },
1163
+ "node_modules/math-intrinsics": {
1164
+ "version": "1.1.0",
1165
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
1166
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
1167
+ "license": "MIT",
1168
+ "engines": {
1169
+ "node": ">= 0.4"
1170
+ }
1171
+ },
1172
+ "node_modules/media-typer": {
1173
+ "version": "0.3.0",
1174
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
1175
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
1176
+ "license": "MIT",
1177
+ "engines": {
1178
+ "node": ">= 0.6"
1179
+ }
1180
+ },
1181
+ "node_modules/memory-pager": {
1182
+ "version": "1.5.0",
1183
+ "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
1184
+ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
1185
+ "license": "MIT"
1186
+ },
1187
+ "node_modules/merge-descriptors": {
1188
+ "version": "1.0.3",
1189
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
1190
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
1191
+ "license": "MIT",
1192
+ "funding": {
1193
+ "url": "https://github.com/sponsors/sindresorhus"
1194
+ }
1195
+ },
1196
+ "node_modules/methods": {
1197
+ "version": "1.1.2",
1198
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
1199
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
1200
+ "license": "MIT",
1201
+ "engines": {
1202
+ "node": ">= 0.6"
1203
+ }
1204
+ },
1205
+ "node_modules/mime": {
1206
+ "version": "1.6.0",
1207
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
1208
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
1209
+ "license": "MIT",
1210
+ "bin": {
1211
+ "mime": "cli.js"
1212
+ },
1213
+ "engines": {
1214
+ "node": ">=4"
1215
+ }
1216
+ },
1217
+ "node_modules/mime-db": {
1218
+ "version": "1.52.0",
1219
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1220
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1221
+ "license": "MIT",
1222
+ "engines": {
1223
+ "node": ">= 0.6"
1224
+ }
1225
+ },
1226
+ "node_modules/mime-types": {
1227
+ "version": "2.1.35",
1228
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1229
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1230
+ "license": "MIT",
1231
+ "dependencies": {
1232
+ "mime-db": "1.52.0"
1233
+ },
1234
+ "engines": {
1235
+ "node": ">= 0.6"
1236
+ }
1237
+ },
1238
+ "node_modules/minimatch": {
1239
+ "version": "3.1.2",
1240
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
1241
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
1242
+ "dev": true,
1243
+ "license": "ISC",
1244
+ "dependencies": {
1245
+ "brace-expansion": "^1.1.7"
1246
+ },
1247
+ "engines": {
1248
+ "node": "*"
1249
+ }
1250
+ },
1251
+ "node_modules/mongodb": {
1252
+ "version": "6.17.0",
1253
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.17.0.tgz",
1254
+ "integrity": "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==",
1255
+ "license": "Apache-2.0",
1256
+ "dependencies": {
1257
+ "@mongodb-js/saslprep": "^1.1.9",
1258
+ "bson": "^6.10.4",
1259
+ "mongodb-connection-string-url": "^3.0.0"
1260
+ },
1261
+ "engines": {
1262
+ "node": ">=16.20.1"
1263
+ },
1264
+ "peerDependencies": {
1265
+ "@aws-sdk/credential-providers": "^3.188.0",
1266
+ "@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
1267
+ "gcp-metadata": "^5.2.0",
1268
+ "kerberos": "^2.0.1",
1269
+ "mongodb-client-encryption": ">=6.0.0 <7",
1270
+ "snappy": "^7.2.2",
1271
+ "socks": "^2.7.1"
1272
+ },
1273
+ "peerDependenciesMeta": {
1274
+ "@aws-sdk/credential-providers": {
1275
+ "optional": true
1276
+ },
1277
+ "@mongodb-js/zstd": {
1278
+ "optional": true
1279
+ },
1280
+ "gcp-metadata": {
1281
+ "optional": true
1282
+ },
1283
+ "kerberos": {
1284
+ "optional": true
1285
+ },
1286
+ "mongodb-client-encryption": {
1287
+ "optional": true
1288
+ },
1289
+ "snappy": {
1290
+ "optional": true
1291
+ },
1292
+ "socks": {
1293
+ "optional": true
1294
+ }
1295
+ }
1296
+ },
1297
+ "node_modules/mongodb-connection-string-url": {
1298
+ "version": "3.0.2",
1299
+ "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
1300
+ "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
1301
+ "license": "Apache-2.0",
1302
+ "dependencies": {
1303
+ "@types/whatwg-url": "^11.0.2",
1304
+ "whatwg-url": "^14.1.0 || ^13.0.0"
1305
+ }
1306
+ },
1307
+ "node_modules/mongoose": {
1308
+ "version": "8.16.4",
1309
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.16.4.tgz",
1310
+ "integrity": "sha512-jslgdQ8pY2vcNSKPv3Dbi5ogo/NT8zcvf6kPDyD8Sdsjsa1at3AFAF0F5PT+jySPGSPbvlNaQ49nT9h+Kx2UDA==",
1311
+ "license": "MIT",
1312
+ "dependencies": {
1313
+ "bson": "^6.10.4",
1314
+ "kareem": "2.6.3",
1315
+ "mongodb": "~6.17.0",
1316
+ "mpath": "0.9.0",
1317
+ "mquery": "5.0.0",
1318
+ "ms": "2.1.3",
1319
+ "sift": "17.1.3"
1320
+ },
1321
+ "engines": {
1322
+ "node": ">=16.20.1"
1323
+ },
1324
+ "funding": {
1325
+ "type": "opencollective",
1326
+ "url": "https://opencollective.com/mongoose"
1327
+ }
1328
+ },
1329
+ "node_modules/mongoose/node_modules/ms": {
1330
+ "version": "2.1.3",
1331
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1332
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1333
+ "license": "MIT"
1334
+ },
1335
+ "node_modules/mpath": {
1336
+ "version": "0.9.0",
1337
+ "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
1338
+ "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
1339
+ "license": "MIT",
1340
+ "engines": {
1341
+ "node": ">=4.0.0"
1342
+ }
1343
+ },
1344
+ "node_modules/mquery": {
1345
+ "version": "5.0.0",
1346
+ "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz",
1347
+ "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==",
1348
+ "license": "MIT",
1349
+ "dependencies": {
1350
+ "debug": "4.x"
1351
+ },
1352
+ "engines": {
1353
+ "node": ">=14.0.0"
1354
+ }
1355
+ },
1356
+ "node_modules/mquery/node_modules/debug": {
1357
+ "version": "4.4.1",
1358
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
1359
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
1360
+ "license": "MIT",
1361
+ "dependencies": {
1362
+ "ms": "^2.1.3"
1363
+ },
1364
+ "engines": {
1365
+ "node": ">=6.0"
1366
+ },
1367
+ "peerDependenciesMeta": {
1368
+ "supports-color": {
1369
+ "optional": true
1370
+ }
1371
+ }
1372
+ },
1373
+ "node_modules/mquery/node_modules/ms": {
1374
+ "version": "2.1.3",
1375
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1376
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1377
+ "license": "MIT"
1378
+ },
1379
+ "node_modules/ms": {
1380
+ "version": "2.0.0",
1381
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1382
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
1383
+ "license": "MIT"
1384
+ },
1385
+ "node_modules/negotiator": {
1386
+ "version": "0.6.3",
1387
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
1388
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
1389
+ "license": "MIT",
1390
+ "engines": {
1391
+ "node": ">= 0.6"
1392
+ }
1393
+ },
1394
+ "node_modules/nodemon": {
1395
+ "version": "3.1.10",
1396
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
1397
+ "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
1398
+ "dev": true,
1399
+ "license": "MIT",
1400
+ "dependencies": {
1401
+ "chokidar": "^3.5.2",
1402
+ "debug": "^4",
1403
+ "ignore-by-default": "^1.0.1",
1404
+ "minimatch": "^3.1.2",
1405
+ "pstree.remy": "^1.1.8",
1406
+ "semver": "^7.5.3",
1407
+ "simple-update-notifier": "^2.0.0",
1408
+ "supports-color": "^5.5.0",
1409
+ "touch": "^3.1.0",
1410
+ "undefsafe": "^2.0.5"
1411
+ },
1412
+ "bin": {
1413
+ "nodemon": "bin/nodemon.js"
1414
+ },
1415
+ "engines": {
1416
+ "node": ">=10"
1417
+ },
1418
+ "funding": {
1419
+ "type": "opencollective",
1420
+ "url": "https://opencollective.com/nodemon"
1421
+ }
1422
+ },
1423
+ "node_modules/nodemon/node_modules/debug": {
1424
+ "version": "4.4.1",
1425
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
1426
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
1427
+ "dev": true,
1428
+ "license": "MIT",
1429
+ "dependencies": {
1430
+ "ms": "^2.1.3"
1431
+ },
1432
+ "engines": {
1433
+ "node": ">=6.0"
1434
+ },
1435
+ "peerDependenciesMeta": {
1436
+ "supports-color": {
1437
+ "optional": true
1438
+ }
1439
+ }
1440
+ },
1441
+ "node_modules/nodemon/node_modules/ms": {
1442
+ "version": "2.1.3",
1443
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1444
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1445
+ "dev": true,
1446
+ "license": "MIT"
1447
+ },
1448
+ "node_modules/normalize-path": {
1449
+ "version": "3.0.0",
1450
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1451
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1452
+ "dev": true,
1453
+ "license": "MIT",
1454
+ "engines": {
1455
+ "node": ">=0.10.0"
1456
+ }
1457
+ },
1458
+ "node_modules/nth-check": {
1459
+ "version": "2.1.1",
1460
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
1461
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
1462
+ "license": "BSD-2-Clause",
1463
+ "dependencies": {
1464
+ "boolbase": "^1.0.0"
1465
+ },
1466
+ "funding": {
1467
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
1468
+ }
1469
+ },
1470
+ "node_modules/object-assign": {
1471
+ "version": "4.1.1",
1472
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1473
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1474
+ "license": "MIT",
1475
+ "engines": {
1476
+ "node": ">=0.10.0"
1477
+ }
1478
+ },
1479
+ "node_modules/object-inspect": {
1480
+ "version": "1.13.4",
1481
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
1482
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
1483
+ "license": "MIT",
1484
+ "engines": {
1485
+ "node": ">= 0.4"
1486
+ },
1487
+ "funding": {
1488
+ "url": "https://github.com/sponsors/ljharb"
1489
+ }
1490
+ },
1491
+ "node_modules/on-finished": {
1492
+ "version": "2.4.1",
1493
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1494
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1495
+ "license": "MIT",
1496
+ "dependencies": {
1497
+ "ee-first": "1.1.1"
1498
+ },
1499
+ "engines": {
1500
+ "node": ">= 0.8"
1501
+ }
1502
+ },
1503
+ "node_modules/parse5": {
1504
+ "version": "7.3.0",
1505
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
1506
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
1507
+ "license": "MIT",
1508
+ "dependencies": {
1509
+ "entities": "^6.0.0"
1510
+ },
1511
+ "funding": {
1512
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
1513
+ }
1514
+ },
1515
+ "node_modules/parse5-htmlparser2-tree-adapter": {
1516
+ "version": "7.1.0",
1517
+ "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
1518
+ "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
1519
+ "license": "MIT",
1520
+ "dependencies": {
1521
+ "domhandler": "^5.0.3",
1522
+ "parse5": "^7.0.0"
1523
+ },
1524
+ "funding": {
1525
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
1526
+ }
1527
+ },
1528
+ "node_modules/parse5-parser-stream": {
1529
+ "version": "7.1.2",
1530
+ "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
1531
+ "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
1532
+ "license": "MIT",
1533
+ "dependencies": {
1534
+ "parse5": "^7.0.0"
1535
+ },
1536
+ "funding": {
1537
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
1538
+ }
1539
+ },
1540
+ "node_modules/parse5/node_modules/entities": {
1541
+ "version": "6.0.1",
1542
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
1543
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
1544
+ "license": "BSD-2-Clause",
1545
+ "engines": {
1546
+ "node": ">=0.12"
1547
+ },
1548
+ "funding": {
1549
+ "url": "https://github.com/fb55/entities?sponsor=1"
1550
+ }
1551
+ },
1552
+ "node_modules/parseurl": {
1553
+ "version": "1.3.3",
1554
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1555
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1556
+ "license": "MIT",
1557
+ "engines": {
1558
+ "node": ">= 0.8"
1559
+ }
1560
+ },
1561
+ "node_modules/path-to-regexp": {
1562
+ "version": "0.1.12",
1563
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
1564
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
1565
+ "license": "MIT"
1566
+ },
1567
+ "node_modules/picomatch": {
1568
+ "version": "2.3.1",
1569
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
1570
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
1571
+ "dev": true,
1572
+ "license": "MIT",
1573
+ "engines": {
1574
+ "node": ">=8.6"
1575
+ },
1576
+ "funding": {
1577
+ "url": "https://github.com/sponsors/jonschlinkert"
1578
+ }
1579
+ },
1580
+ "node_modules/proxy-addr": {
1581
+ "version": "2.0.7",
1582
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1583
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1584
+ "license": "MIT",
1585
+ "dependencies": {
1586
+ "forwarded": "0.2.0",
1587
+ "ipaddr.js": "1.9.1"
1588
+ },
1589
+ "engines": {
1590
+ "node": ">= 0.10"
1591
+ }
1592
+ },
1593
+ "node_modules/proxy-from-env": {
1594
+ "version": "1.1.0",
1595
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
1596
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
1597
+ "license": "MIT"
1598
+ },
1599
+ "node_modules/pstree.remy": {
1600
+ "version": "1.1.8",
1601
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
1602
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
1603
+ "dev": true,
1604
+ "license": "MIT"
1605
+ },
1606
+ "node_modules/punycode": {
1607
+ "version": "2.3.1",
1608
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
1609
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
1610
+ "license": "MIT",
1611
+ "engines": {
1612
+ "node": ">=6"
1613
+ }
1614
+ },
1615
+ "node_modules/qs": {
1616
+ "version": "6.13.0",
1617
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
1618
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
1619
+ "license": "BSD-3-Clause",
1620
+ "dependencies": {
1621
+ "side-channel": "^1.0.6"
1622
+ },
1623
+ "engines": {
1624
+ "node": ">=0.6"
1625
+ },
1626
+ "funding": {
1627
+ "url": "https://github.com/sponsors/ljharb"
1628
+ }
1629
+ },
1630
+ "node_modules/range-parser": {
1631
+ "version": "1.2.1",
1632
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1633
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1634
+ "license": "MIT",
1635
+ "engines": {
1636
+ "node": ">= 0.6"
1637
+ }
1638
+ },
1639
+ "node_modules/raw-body": {
1640
+ "version": "2.5.2",
1641
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
1642
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
1643
+ "license": "MIT",
1644
+ "dependencies": {
1645
+ "bytes": "3.1.2",
1646
+ "http-errors": "2.0.0",
1647
+ "iconv-lite": "0.4.24",
1648
+ "unpipe": "1.0.0"
1649
+ },
1650
+ "engines": {
1651
+ "node": ">= 0.8"
1652
+ }
1653
+ },
1654
+ "node_modules/raw-body/node_modules/iconv-lite": {
1655
+ "version": "0.4.24",
1656
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
1657
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
1658
+ "license": "MIT",
1659
+ "dependencies": {
1660
+ "safer-buffer": ">= 2.1.2 < 3"
1661
+ },
1662
+ "engines": {
1663
+ "node": ">=0.10.0"
1664
+ }
1665
+ },
1666
+ "node_modules/readdirp": {
1667
+ "version": "3.6.0",
1668
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1669
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1670
+ "dev": true,
1671
+ "license": "MIT",
1672
+ "dependencies": {
1673
+ "picomatch": "^2.2.1"
1674
+ },
1675
+ "engines": {
1676
+ "node": ">=8.10.0"
1677
+ }
1678
+ },
1679
+ "node_modules/safe-buffer": {
1680
+ "version": "5.2.1",
1681
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1682
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1683
+ "funding": [
1684
+ {
1685
+ "type": "github",
1686
+ "url": "https://github.com/sponsors/feross"
1687
+ },
1688
+ {
1689
+ "type": "patreon",
1690
+ "url": "https://www.patreon.com/feross"
1691
+ },
1692
+ {
1693
+ "type": "consulting",
1694
+ "url": "https://feross.org/support"
1695
+ }
1696
+ ],
1697
+ "license": "MIT"
1698
+ },
1699
+ "node_modules/safer-buffer": {
1700
+ "version": "2.1.2",
1701
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1702
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1703
+ "license": "MIT"
1704
+ },
1705
+ "node_modules/semver": {
1706
+ "version": "7.7.2",
1707
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
1708
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
1709
+ "license": "ISC",
1710
+ "bin": {
1711
+ "semver": "bin/semver.js"
1712
+ },
1713
+ "engines": {
1714
+ "node": ">=10"
1715
+ }
1716
+ },
1717
+ "node_modules/send": {
1718
+ "version": "0.19.0",
1719
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
1720
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
1721
+ "license": "MIT",
1722
+ "dependencies": {
1723
+ "debug": "2.6.9",
1724
+ "depd": "2.0.0",
1725
+ "destroy": "1.2.0",
1726
+ "encodeurl": "~1.0.2",
1727
+ "escape-html": "~1.0.3",
1728
+ "etag": "~1.8.1",
1729
+ "fresh": "0.5.2",
1730
+ "http-errors": "2.0.0",
1731
+ "mime": "1.6.0",
1732
+ "ms": "2.1.3",
1733
+ "on-finished": "2.4.1",
1734
+ "range-parser": "~1.2.1",
1735
+ "statuses": "2.0.1"
1736
+ },
1737
+ "engines": {
1738
+ "node": ">= 0.8.0"
1739
+ }
1740
+ },
1741
+ "node_modules/send/node_modules/encodeurl": {
1742
+ "version": "1.0.2",
1743
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
1744
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
1745
+ "license": "MIT",
1746
+ "engines": {
1747
+ "node": ">= 0.8"
1748
+ }
1749
+ },
1750
+ "node_modules/send/node_modules/ms": {
1751
+ "version": "2.1.3",
1752
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1753
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1754
+ "license": "MIT"
1755
+ },
1756
+ "node_modules/serve-static": {
1757
+ "version": "1.16.2",
1758
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
1759
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
1760
+ "license": "MIT",
1761
+ "dependencies": {
1762
+ "encodeurl": "~2.0.0",
1763
+ "escape-html": "~1.0.3",
1764
+ "parseurl": "~1.3.3",
1765
+ "send": "0.19.0"
1766
+ },
1767
+ "engines": {
1768
+ "node": ">= 0.8.0"
1769
+ }
1770
+ },
1771
+ "node_modules/setprototypeof": {
1772
+ "version": "1.2.0",
1773
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1774
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
1775
+ "license": "ISC"
1776
+ },
1777
+ "node_modules/side-channel": {
1778
+ "version": "1.1.0",
1779
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
1780
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
1781
+ "license": "MIT",
1782
+ "dependencies": {
1783
+ "es-errors": "^1.3.0",
1784
+ "object-inspect": "^1.13.3",
1785
+ "side-channel-list": "^1.0.0",
1786
+ "side-channel-map": "^1.0.1",
1787
+ "side-channel-weakmap": "^1.0.2"
1788
+ },
1789
+ "engines": {
1790
+ "node": ">= 0.4"
1791
+ },
1792
+ "funding": {
1793
+ "url": "https://github.com/sponsors/ljharb"
1794
+ }
1795
+ },
1796
+ "node_modules/side-channel-list": {
1797
+ "version": "1.0.0",
1798
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
1799
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
1800
+ "license": "MIT",
1801
+ "dependencies": {
1802
+ "es-errors": "^1.3.0",
1803
+ "object-inspect": "^1.13.3"
1804
+ },
1805
+ "engines": {
1806
+ "node": ">= 0.4"
1807
+ },
1808
+ "funding": {
1809
+ "url": "https://github.com/sponsors/ljharb"
1810
+ }
1811
+ },
1812
+ "node_modules/side-channel-map": {
1813
+ "version": "1.0.1",
1814
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1815
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1816
+ "license": "MIT",
1817
+ "dependencies": {
1818
+ "call-bound": "^1.0.2",
1819
+ "es-errors": "^1.3.0",
1820
+ "get-intrinsic": "^1.2.5",
1821
+ "object-inspect": "^1.13.3"
1822
+ },
1823
+ "engines": {
1824
+ "node": ">= 0.4"
1825
+ },
1826
+ "funding": {
1827
+ "url": "https://github.com/sponsors/ljharb"
1828
+ }
1829
+ },
1830
+ "node_modules/side-channel-weakmap": {
1831
+ "version": "1.0.2",
1832
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1833
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1834
+ "license": "MIT",
1835
+ "dependencies": {
1836
+ "call-bound": "^1.0.2",
1837
+ "es-errors": "^1.3.0",
1838
+ "get-intrinsic": "^1.2.5",
1839
+ "object-inspect": "^1.13.3",
1840
+ "side-channel-map": "^1.0.1"
1841
+ },
1842
+ "engines": {
1843
+ "node": ">= 0.4"
1844
+ },
1845
+ "funding": {
1846
+ "url": "https://github.com/sponsors/ljharb"
1847
+ }
1848
+ },
1849
+ "node_modules/sift": {
1850
+ "version": "17.1.3",
1851
+ "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
1852
+ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
1853
+ "license": "MIT"
1854
+ },
1855
+ "node_modules/simple-update-notifier": {
1856
+ "version": "2.0.0",
1857
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
1858
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
1859
+ "dev": true,
1860
+ "license": "MIT",
1861
+ "dependencies": {
1862
+ "semver": "^7.5.3"
1863
+ },
1864
+ "engines": {
1865
+ "node": ">=10"
1866
+ }
1867
+ },
1868
+ "node_modules/sparse-bitfield": {
1869
+ "version": "3.0.3",
1870
+ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
1871
+ "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
1872
+ "license": "MIT",
1873
+ "dependencies": {
1874
+ "memory-pager": "^1.0.2"
1875
+ }
1876
+ },
1877
+ "node_modules/statuses": {
1878
+ "version": "2.0.1",
1879
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1880
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
1881
+ "license": "MIT",
1882
+ "engines": {
1883
+ "node": ">= 0.8"
1884
+ }
1885
+ },
1886
+ "node_modules/supports-color": {
1887
+ "version": "5.5.0",
1888
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
1889
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
1890
+ "dev": true,
1891
+ "license": "MIT",
1892
+ "dependencies": {
1893
+ "has-flag": "^3.0.0"
1894
+ },
1895
+ "engines": {
1896
+ "node": ">=4"
1897
+ }
1898
+ },
1899
+ "node_modules/to-regex-range": {
1900
+ "version": "5.0.1",
1901
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1902
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1903
+ "dev": true,
1904
+ "license": "MIT",
1905
+ "dependencies": {
1906
+ "is-number": "^7.0.0"
1907
+ },
1908
+ "engines": {
1909
+ "node": ">=8.0"
1910
+ }
1911
+ },
1912
+ "node_modules/toidentifier": {
1913
+ "version": "1.0.1",
1914
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1915
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1916
+ "license": "MIT",
1917
+ "engines": {
1918
+ "node": ">=0.6"
1919
+ }
1920
+ },
1921
+ "node_modules/touch": {
1922
+ "version": "3.1.1",
1923
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
1924
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
1925
+ "dev": true,
1926
+ "license": "ISC",
1927
+ "bin": {
1928
+ "nodetouch": "bin/nodetouch.js"
1929
+ }
1930
+ },
1931
+ "node_modules/tr46": {
1932
+ "version": "5.1.1",
1933
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
1934
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
1935
+ "license": "MIT",
1936
+ "dependencies": {
1937
+ "punycode": "^2.3.1"
1938
+ },
1939
+ "engines": {
1940
+ "node": ">=18"
1941
+ }
1942
+ },
1943
+ "node_modules/type-is": {
1944
+ "version": "1.6.18",
1945
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1946
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1947
+ "license": "MIT",
1948
+ "dependencies": {
1949
+ "media-typer": "0.3.0",
1950
+ "mime-types": "~2.1.24"
1951
+ },
1952
+ "engines": {
1953
+ "node": ">= 0.6"
1954
+ }
1955
+ },
1956
+ "node_modules/undefsafe": {
1957
+ "version": "2.0.5",
1958
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
1959
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
1960
+ "dev": true,
1961
+ "license": "MIT"
1962
+ },
1963
+ "node_modules/undici": {
1964
+ "version": "7.12.0",
1965
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.12.0.tgz",
1966
+ "integrity": "sha512-GrKEsc3ughskmGA9jevVlIOPMiiAHJ4OFUtaAH+NhfTUSiZ1wMPIQqQvAJUrJspFXJt3EBWgpAeoHEDVT1IBug==",
1967
+ "license": "MIT",
1968
+ "engines": {
1969
+ "node": ">=20.18.1"
1970
+ }
1971
+ },
1972
+ "node_modules/unpipe": {
1973
+ "version": "1.0.0",
1974
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1975
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1976
+ "license": "MIT",
1977
+ "engines": {
1978
+ "node": ">= 0.8"
1979
+ }
1980
+ },
1981
+ "node_modules/utils-merge": {
1982
+ "version": "1.0.1",
1983
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1984
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
1985
+ "license": "MIT",
1986
+ "engines": {
1987
+ "node": ">= 0.4.0"
1988
+ }
1989
+ },
1990
+ "node_modules/uuid": {
1991
+ "version": "9.0.1",
1992
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
1993
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
1994
+ "funding": [
1995
+ "https://github.com/sponsors/broofa",
1996
+ "https://github.com/sponsors/ctavan"
1997
+ ],
1998
+ "license": "MIT",
1999
+ "bin": {
2000
+ "uuid": "dist/bin/uuid"
2001
+ }
2002
+ },
2003
+ "node_modules/validator": {
2004
+ "version": "13.12.0",
2005
+ "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
2006
+ "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
2007
+ "license": "MIT",
2008
+ "engines": {
2009
+ "node": ">= 0.10"
2010
+ }
2011
+ },
2012
+ "node_modules/vary": {
2013
+ "version": "1.1.2",
2014
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
2015
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
2016
+ "license": "MIT",
2017
+ "engines": {
2018
+ "node": ">= 0.8"
2019
+ }
2020
+ },
2021
+ "node_modules/webidl-conversions": {
2022
+ "version": "7.0.0",
2023
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
2024
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
2025
+ "license": "BSD-2-Clause",
2026
+ "engines": {
2027
+ "node": ">=12"
2028
+ }
2029
+ },
2030
+ "node_modules/whatwg-encoding": {
2031
+ "version": "3.1.1",
2032
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
2033
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
2034
+ "license": "MIT",
2035
+ "dependencies": {
2036
+ "iconv-lite": "0.6.3"
2037
+ },
2038
+ "engines": {
2039
+ "node": ">=18"
2040
+ }
2041
+ },
2042
+ "node_modules/whatwg-mimetype": {
2043
+ "version": "4.0.0",
2044
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
2045
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
2046
+ "license": "MIT",
2047
+ "engines": {
2048
+ "node": ">=18"
2049
+ }
2050
+ },
2051
+ "node_modules/whatwg-url": {
2052
+ "version": "14.2.0",
2053
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
2054
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
2055
+ "license": "MIT",
2056
+ "dependencies": {
2057
+ "tr46": "^5.1.0",
2058
+ "webidl-conversions": "^7.0.0"
2059
+ },
2060
+ "engines": {
2061
+ "node": ">=18"
2062
+ }
2063
+ }
2064
+ }
2065
+ }
package.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "transcreation-sandbox-server",
3
+ "version": "1.0.0",
4
+ "description": "Backend server for Transcreation Sandbox",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "node index.js",
8
+ "dev": "nodemon index.js"
9
+ },
10
+ "dependencies": {
11
+ "express": "^4.18.2",
12
+ "cors": "^2.8.5",
13
+ "mongoose": "^8.0.3",
14
+ "dotenv": "^16.3.1",
15
+ "axios": "^1.6.2",
16
+ "cheerio": "^1.0.0-rc.12",
17
+ "uuid": "^9.0.1",
18
+ "bcryptjs": "^2.4.3",
19
+ "jsonwebtoken": "^9.0.2",
20
+ "express-rate-limit": "^7.1.5"
21
+ },
22
+ "devDependencies": {
23
+ "nodemon": "^3.0.2"
24
+ }
25
+ }
routes/auth.js ADDED
@@ -0,0 +1,783 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const router = express.Router();
3
+
4
+ // Pre-defined users
5
+ const PREDEFINED_USERS = {
6
+ 'mche0278@student.monash.edu': { name: 'Michelle Chen', email: 'mche0278@student.monash.edu', role: 'student' },
7
+ 'zfan0011@student.monash.edu': { name: 'Fang Zhou', email: 'zfan0011@student.monash.edu', role: 'student' },
8
+ 'yfuu0071@student.monash.edu': { name: 'Fu Yitong', email: 'yfuu0071@student.monash.edu', role: 'student' },
9
+ 'hhan0022@student.monash.edu': { name: 'Han Heyang', email: 'hhan0022@student.monash.edu', role: 'student' },
10
+ 'ylei0024@student.monash.edu': { name: 'Lei Yang', email: 'ylei0024@student.monash.edu', role: 'student' },
11
+ 'wlii0217@student.monash.edu': { name: 'Li Wenhui', email: 'wlii0217@student.monash.edu', role: 'student' },
12
+ 'zlia0091@student.monash.edu': { name: 'Liao Zhixin', email: 'zlia0091@student.monash.edu', role: 'student' },
13
+ 'hmaa0054@student.monash.edu': { name: 'Ma Huachen', email: 'hmaa0054@student.monash.edu', role: 'student' },
14
+ 'csun0059@student.monash.edu': { name: 'Sun Chongkai', email: 'csun0059@student.monash.edu', role: 'student' },
15
+ 'pwon0030@student.monash.edu': { name: 'Wong Prisca Si-Heng', email: 'pwon0030@student.monash.edu', role: 'student' },
16
+ 'zxia0017@student.monash.edu': { name: 'Xia Zechen', email: 'zxia0017@student.monash.edu', role: 'student' },
17
+ 'lyou0021@student.monash.edu': { name: 'You Lianxiang', email: 'lyou0021@student.monash.edu', role: 'student' },
18
+ 'wzhe0034@student.monash.edu': { name: 'Zheng Weijie', email: 'wzhe0034@student.monash.edu', role: 'student' },
19
+ 'szhe0055@student.monash.edu': { name: 'Zheng Siyuan', email: 'szhe0055@student.monash.edu', role: 'student' },
20
+ 'xzhe0055@student.monash.edu': { name: 'Zheng Xiang', email: 'xzhe0055@student.monash.edu', role: 'student' },
21
+ 'hongchang.yu@monash.edu': { name: 'Tristan', email: 'hongchang.yu@monash.edu', role: 'admin' }
22
+ };
23
+
24
+ // Middleware to verify token (simplified)
25
+ const authenticateToken = (req, res, next) => {
26
+ const authHeader = req.headers['authorization'];
27
+ const token = authHeader && authHeader.split(' ')[1];
28
+
29
+ if (!token) {
30
+ return res.status(401).json({ error: 'Access token required' });
31
+ }
32
+
33
+ // For our simplified system, just check if token exists and has the right format
34
+ if (token.startsWith('user_') || token.startsWith('visitor_')) {
35
+ req.user = { token }; // We'll get user details from localStorage on frontend
36
+ next();
37
+ } else {
38
+ return res.status(403).json({ error: 'Invalid token' });
39
+ }
40
+ };
41
+
42
+ // Middleware to check if user is admin
43
+ const requireAdmin = (req, res, next) => {
44
+ // In our simplified system, we'll check the user's role from the request body or headers
45
+ // The frontend will send the user role in the request
46
+ const userRole = req.headers['user-role'] || req.body.role;
47
+
48
+ if (userRole !== 'admin') {
49
+ return res.status(403).json({ error: 'Admin access required' });
50
+ }
51
+
52
+ next();
53
+ };
54
+
55
+ // Login endpoint (simplified)
56
+ router.post('/login', async (req, res) => {
57
+ try {
58
+ const { email } = req.body;
59
+
60
+ // Check if email is in predefined users
61
+ const user = PREDEFINED_USERS[email];
62
+
63
+ if (user) {
64
+ // For predefined users, create a simple token
65
+ const token = `user_${Date.now()}`;
66
+ res.json({
67
+ success: true,
68
+ token,
69
+ user: {
70
+ name: user.name,
71
+ email: user.email,
72
+ role: user.role
73
+ }
74
+ });
75
+ } else {
76
+ // For visitors, create a visitor account
77
+ const visitorUser = {
78
+ name: 'Visitor',
79
+ email: email,
80
+ role: 'visitor'
81
+ };
82
+ const token = `visitor_${Date.now()}`;
83
+ res.json({
84
+ success: true,
85
+ token,
86
+ user: visitorUser
87
+ });
88
+ }
89
+ } catch (error) {
90
+ console.error('Login error:', error);
91
+ res.status(500).json({ error: 'Login failed' });
92
+ }
93
+ });
94
+
95
+ // Get user profile
96
+ router.get('/profile', authenticateToken, async (req, res) => {
97
+ try {
98
+ // For this simplified system, we'll return a basic profile
99
+ // The actual user data is stored in localStorage on the frontend
100
+ res.json({
101
+ success: true,
102
+ user: {
103
+ name: 'User',
104
+ email: 'user@example.com',
105
+ role: 'student'
106
+ }
107
+ });
108
+ } catch (error) {
109
+ console.error('Profile error:', error);
110
+ res.status(500).json({ error: 'Failed to get profile' });
111
+ }
112
+ });
113
+
114
+ // Admin endpoints
115
+ // Get all users (admin only)
116
+ router.get('/admin/users', authenticateToken, async (req, res) => {
117
+ try {
118
+ // In our simplified system, return predefined users
119
+ const users = Object.values(PREDEFINED_USERS);
120
+ res.json({
121
+ success: true,
122
+ users: users
123
+ });
124
+ } catch (error) {
125
+ console.error('Get users error:', error);
126
+ res.status(500).json({ error: 'Failed to get users' });
127
+ }
128
+ });
129
+
130
+ // Get system statistics (admin only)
131
+ router.get('/admin/stats', authenticateToken, async (req, res) => {
132
+ try {
133
+ // Import models for statistics
134
+ const SourceText = require('../models/SourceText');
135
+ const Submission = require('../models/Submission');
136
+
137
+ const stats = {
138
+ totalUsers: Object.keys(PREDEFINED_USERS).length,
139
+ practiceExamples: await SourceText.countDocuments({ sourceType: 'practice' }),
140
+ totalSubmissions: await Submission.countDocuments(),
141
+ activeSessions: 1 // Placeholder
142
+ };
143
+
144
+ res.json({
145
+ success: true,
146
+ stats: stats
147
+ });
148
+ } catch (error) {
149
+ console.error('Get stats error:', error);
150
+ res.status(500).json({ error: 'Failed to get statistics' });
151
+ }
152
+ });
153
+
154
+ // Get all practice examples (admin only)
155
+ router.get('/admin/practice-examples', authenticateToken, async (req, res) => {
156
+ try {
157
+ const SourceText = require('../models/SourceText');
158
+ const examples = await SourceText.find({ sourceType: 'practice' }).sort({ createdAt: -1 });
159
+
160
+ res.json({
161
+ success: true,
162
+ examples: examples
163
+ });
164
+ } catch (error) {
165
+ console.error('Get practice examples error:', error);
166
+ res.status(500).json({ error: 'Failed to get practice examples' });
167
+ }
168
+ });
169
+
170
+ // Add new practice example (admin only)
171
+ router.post('/admin/practice-examples', authenticateToken, async (req, res) => {
172
+ try {
173
+ const SourceText = require('../models/SourceText');
174
+ const { title, content, sourceLanguage, culturalElements, difficulty } = req.body;
175
+
176
+ const newExample = new SourceText({
177
+ title,
178
+ content,
179
+ sourceLanguage,
180
+ sourceType: 'practice',
181
+ culturalElements: culturalElements || [],
182
+ difficulty: difficulty || 'intermediate'
183
+ });
184
+
185
+ await newExample.save();
186
+
187
+ res.status(201).json({
188
+ success: true,
189
+ message: 'Practice example added successfully',
190
+ example: newExample
191
+ });
192
+ } catch (error) {
193
+ console.error('Add practice example error:', error);
194
+ res.status(500).json({ error: 'Failed to add practice example' });
195
+ }
196
+ });
197
+
198
+ // Update practice example (admin only)
199
+ router.put('/admin/practice-examples/:id', authenticateToken, async (req, res) => {
200
+ try {
201
+ const SourceText = require('../models/SourceText');
202
+ const { title, content, sourceLanguage, culturalElements, difficulty } = req.body;
203
+
204
+ const updatedExample = await SourceText.findByIdAndUpdate(
205
+ req.params.id,
206
+ {
207
+ title,
208
+ content,
209
+ sourceLanguage,
210
+ culturalElements: culturalElements || [],
211
+ difficulty: difficulty || 'intermediate'
212
+ },
213
+ { new: true, runValidators: true }
214
+ );
215
+
216
+ if (!updatedExample) {
217
+ return res.status(404).json({ error: 'Practice example not found' });
218
+ }
219
+
220
+ res.json({
221
+ success: true,
222
+ message: 'Practice example updated successfully',
223
+ example: updatedExample
224
+ });
225
+ } catch (error) {
226
+ console.error('Update practice example error:', error);
227
+ res.status(500).json({ error: 'Failed to update practice example' });
228
+ }
229
+ });
230
+
231
+ // Delete practice example (admin only)
232
+ router.delete('/admin/practice-examples/:id', authenticateToken, async (req, res) => {
233
+ try {
234
+ const SourceText = require('../models/SourceText');
235
+
236
+ const deletedExample = await SourceText.findByIdAndDelete(req.params.id);
237
+
238
+ if (!deletedExample) {
239
+ return res.status(404).json({ error: 'Practice example not found' });
240
+ }
241
+
242
+ res.json({
243
+ success: true,
244
+ message: 'Practice example deleted successfully'
245
+ });
246
+ } catch (error) {
247
+ console.error('Delete practice example error:', error);
248
+ res.status(500).json({ error: 'Failed to delete practice example' });
249
+ }
250
+ });
251
+
252
+ // Add new user (admin only)
253
+ router.post('/admin/users', authenticateToken, async (req, res) => {
254
+ try {
255
+ const { name, email, role } = req.body;
256
+
257
+ // Validate required fields
258
+ if (!name || !email || !role) {
259
+ return res.status(400).json({ error: 'Name, email, and role are required' });
260
+ }
261
+
262
+ // Check if user already exists
263
+ if (PREDEFINED_USERS[email]) {
264
+ return res.status(400).json({ error: 'User with this email already exists' });
265
+ }
266
+
267
+ // Add to predefined users (in a real app, this would be saved to database)
268
+ PREDEFINED_USERS[email] = { name, email, role };
269
+
270
+ res.status(201).json({
271
+ success: true,
272
+ message: 'User added successfully',
273
+ user: { name, email, role }
274
+ });
275
+ } catch (error) {
276
+ console.error('Add user error:', error);
277
+ res.status(500).json({ error: 'Failed to add user' });
278
+ }
279
+ });
280
+
281
+ // Update user (admin only)
282
+ router.put('/admin/users/:email', authenticateToken, async (req, res) => {
283
+ try {
284
+ const { name, role } = req.body;
285
+ const email = req.params.email;
286
+
287
+ // Check if user exists
288
+ if (!PREDEFINED_USERS[email]) {
289
+ return res.status(404).json({ error: 'User not found' });
290
+ }
291
+
292
+ // Update user
293
+ PREDEFINED_USERS[email] = {
294
+ ...PREDEFINED_USERS[email],
295
+ name: name || PREDEFINED_USERS[email].name,
296
+ role: role || PREDEFINED_USERS[email].role
297
+ };
298
+
299
+ res.json({
300
+ success: true,
301
+ message: 'User updated successfully',
302
+ user: PREDEFINED_USERS[email]
303
+ });
304
+ } catch (error) {
305
+ console.error('Update user error:', error);
306
+ res.status(500).json({ error: 'Failed to update user' });
307
+ }
308
+ });
309
+
310
+ // Delete user (admin only)
311
+ router.delete('/admin/users/:email', authenticateToken, async (req, res) => {
312
+ try {
313
+ const email = req.params.email;
314
+
315
+ // Check if user exists
316
+ if (!PREDEFINED_USERS[email]) {
317
+ return res.status(404).json({ error: 'User not found' });
318
+ }
319
+
320
+ // Prevent deleting the admin user
321
+ if (email === 'hongchang.yu@monash.edu') {
322
+ return res.status(400).json({ error: 'Cannot delete the main admin user' });
323
+ }
324
+
325
+ // Delete user
326
+ delete PREDEFINED_USERS[email];
327
+
328
+ res.json({
329
+ success: true,
330
+ message: 'User deleted successfully'
331
+ });
332
+ } catch (error) {
333
+ console.error('Delete user error:', error);
334
+ res.status(500).json({ error: 'Failed to delete user' });
335
+ }
336
+ });
337
+
338
+ // ===== TUTORIAL TASKS MANAGEMENT =====
339
+
340
+ // Get all tutorial tasks (admin only)
341
+ router.get('/admin/tutorial-tasks', authenticateToken, async (req, res) => {
342
+ try {
343
+ const SourceText = require('../models/SourceText');
344
+
345
+ const tutorialTasks = await SourceText.find({ category: 'tutorial' })
346
+ .sort({ weekNumber: 1, createdAt: -1 });
347
+
348
+ res.json({
349
+ success: true,
350
+ tutorialTasks
351
+ });
352
+ } catch (error) {
353
+ console.error('Get tutorial tasks error:', error);
354
+ res.status(500).json({ error: 'Failed to get tutorial tasks' });
355
+ }
356
+ });
357
+
358
+ // Add new tutorial task (admin only)
359
+ router.post('/admin/tutorial-tasks', authenticateToken, async (req, res) => {
360
+ try {
361
+ const SourceText = require('../models/SourceText');
362
+ const { title, content, sourceLanguage, sourceCulture, weekNumber, difficulty, culturalElements } = req.body;
363
+
364
+ // Validate required fields
365
+ if (!title || !content || !sourceLanguage || !weekNumber) {
366
+ return res.status(400).json({ error: 'Title, content, sourceLanguage, and weekNumber are required' });
367
+ }
368
+
369
+ const newTutorialTask = new SourceText({
370
+ title,
371
+ content,
372
+ sourceLanguage,
373
+ category: 'tutorial',
374
+ weekNumber: parseInt(weekNumber),
375
+ difficulty: difficulty || 'intermediate',
376
+ culturalElements: culturalElements || [],
377
+ sourceType: 'tutorial'
378
+ });
379
+
380
+ const savedTask = await newTutorialTask.save();
381
+
382
+ res.status(201).json({
383
+ success: true,
384
+ message: 'Tutorial task added successfully',
385
+ tutorialTask: savedTask
386
+ });
387
+ } catch (error) {
388
+ console.error('Add tutorial task error:', error);
389
+ res.status(500).json({ error: 'Failed to add tutorial task' });
390
+ }
391
+ });
392
+
393
+ // Update tutorial task (admin only)
394
+ router.put('/admin/tutorial-tasks/:id', authenticateToken, requireAdmin, async (req, res) => {
395
+ try {
396
+ const SourceText = require('../models/SourceText');
397
+ const { title, content, sourceLanguage, sourceCulture, weekNumber, difficulty, culturalElements } = req.body;
398
+
399
+ const updatedTask = await SourceText.findByIdAndUpdate(
400
+ req.params.id,
401
+ {
402
+ title,
403
+ content,
404
+ sourceLanguage,
405
+ weekNumber: parseInt(weekNumber),
406
+ difficulty: difficulty || 'intermediate',
407
+ culturalElements: culturalElements || []
408
+ },
409
+ { new: true, runValidators: true }
410
+ );
411
+
412
+ if (!updatedTask) {
413
+ return res.status(404).json({ error: 'Tutorial task not found' });
414
+ }
415
+
416
+ res.json({
417
+ success: true,
418
+ message: 'Tutorial task updated successfully',
419
+ tutorialTask: updatedTask
420
+ });
421
+ } catch (error) {
422
+ console.error('Update tutorial task error:', error);
423
+ res.status(500).json({ error: 'Failed to update tutorial task' });
424
+ }
425
+ });
426
+
427
+ // Delete tutorial task (admin only)
428
+ router.delete('/admin/tutorial-tasks/:id', authenticateToken, async (req, res) => {
429
+ try {
430
+ const SourceText = require('../models/SourceText');
431
+
432
+ const deletedTask = await SourceText.findByIdAndDelete(req.params.id);
433
+
434
+ if (!deletedTask) {
435
+ return res.status(404).json({ error: 'Tutorial task not found' });
436
+ }
437
+
438
+ res.json({
439
+ success: true,
440
+ message: 'Tutorial task deleted successfully'
441
+ });
442
+ } catch (error) {
443
+ console.error('Delete tutorial task error:', error);
444
+ res.status(500).json({ error: 'Failed to delete tutorial task' });
445
+ }
446
+ });
447
+
448
+ // ===== WEEKLY PRACTICE MANAGEMENT =====
449
+
450
+ // Get all weekly practice tasks (admin only)
451
+ router.get('/admin/weekly-practice', authenticateToken, async (req, res) => {
452
+ try {
453
+ const SourceText = require('../models/SourceText');
454
+
455
+ const weeklyPractice = await SourceText.find({ category: 'weekly-practice' })
456
+ .sort({ weekNumber: 1, createdAt: -1 });
457
+
458
+ res.json({
459
+ success: true,
460
+ weeklyPractice
461
+ });
462
+ } catch (error) {
463
+ console.error('Get weekly practice error:', error);
464
+ res.status(500).json({ error: 'Failed to get weekly practice' });
465
+ }
466
+ });
467
+
468
+ // Add new weekly practice task (admin only)
469
+ router.post('/admin/weekly-practice', authenticateToken, async (req, res) => {
470
+ try {
471
+ const SourceText = require('../models/SourceText');
472
+ const { title, content, sourceLanguage, sourceCulture, weekNumber, difficulty, culturalElements } = req.body;
473
+
474
+ // Validate required fields
475
+ if (!title || !content || !sourceLanguage || !weekNumber) {
476
+ return res.status(400).json({ error: 'Title, content, sourceLanguage, and weekNumber are required' });
477
+ }
478
+
479
+ const newWeeklyPractice = new SourceText({
480
+ title,
481
+ content,
482
+ sourceLanguage,
483
+ category: 'weekly-practice',
484
+ weekNumber: parseInt(weekNumber),
485
+ difficulty: difficulty || 'intermediate',
486
+ culturalElements: culturalElements || [],
487
+ sourceType: 'weekly-practice'
488
+ });
489
+
490
+ const savedPractice = await newWeeklyPractice.save();
491
+
492
+ res.status(201).json({
493
+ success: true,
494
+ message: 'Weekly practice added successfully',
495
+ weeklyPractice: savedPractice
496
+ });
497
+ } catch (error) {
498
+ console.error('Add weekly practice error:', error);
499
+ res.status(500).json({ error: 'Failed to add weekly practice' });
500
+ }
501
+ });
502
+
503
+ // Create weekly practice task (admin only)
504
+ router.post('/admin/weekly-practice', authenticateToken, requireAdmin, async (req, res) => {
505
+ try {
506
+ const SourceText = require('../models/SourceText');
507
+ const { content, weekNumber, category } = req.body;
508
+
509
+ // Validate required fields
510
+ if (!content || !weekNumber) {
511
+ return res.status(400).json({ error: 'Content and week number are required' });
512
+ }
513
+
514
+ const newPractice = new SourceText({
515
+ content,
516
+ weekNumber: parseInt(weekNumber),
517
+ category: category || 'weekly-practice',
518
+ title: `Weekly Practice Week ${weekNumber}`,
519
+ sourceLanguage: 'English',
520
+ sourceType: 'weekly-practice'
521
+ });
522
+
523
+ const savedPractice = await newPractice.save();
524
+
525
+ res.status(201).json({
526
+ success: true,
527
+ message: 'Weekly practice created successfully',
528
+ practice: savedPractice
529
+ });
530
+ } catch (error) {
531
+ console.error('Create weekly practice error:', error);
532
+ res.status(500).json({ error: 'Failed to create weekly practice' });
533
+ }
534
+ });
535
+
536
+ // Update weekly practice task (admin only)
537
+ router.put('/admin/weekly-practice/:id', authenticateToken, requireAdmin, async (req, res) => {
538
+ try {
539
+ const SourceText = require('../models/SourceText');
540
+ const { title, content, sourceLanguage, sourceCulture, weekNumber, difficulty, culturalElements } = req.body;
541
+
542
+ const updatedPractice = await SourceText.findByIdAndUpdate(
543
+ req.params.id,
544
+ {
545
+ title,
546
+ content,
547
+ sourceLanguage,
548
+ weekNumber: parseInt(weekNumber),
549
+ difficulty: difficulty || 'intermediate',
550
+ culturalElements: culturalElements || []
551
+ },
552
+ { new: true, runValidators: true }
553
+ );
554
+
555
+ if (!updatedPractice) {
556
+ return res.status(404).json({ error: 'Weekly practice not found' });
557
+ }
558
+
559
+ res.json({
560
+ success: true,
561
+ message: 'Weekly practice updated successfully',
562
+ weeklyPractice: updatedPractice
563
+ });
564
+ } catch (error) {
565
+ console.error('Update weekly practice error:', error);
566
+ res.status(500).json({ error: 'Failed to update weekly practice' });
567
+ }
568
+ });
569
+
570
+ // Delete weekly practice task (admin only)
571
+ router.delete('/admin/weekly-practice/:id', authenticateToken, requireAdmin, async (req, res) => {
572
+ try {
573
+ const SourceText = require('../models/SourceText');
574
+
575
+ const deletedPractice = await SourceText.findByIdAndDelete(req.params.id);
576
+
577
+ if (!deletedPractice) {
578
+ return res.status(404).json({ error: 'Weekly practice not found' });
579
+ }
580
+
581
+ res.json({
582
+ success: true,
583
+ message: 'Weekly practice deleted successfully'
584
+ });
585
+ } catch (error) {
586
+ console.error('Delete weekly practice error:', error);
587
+ res.status(500).json({ error: 'Failed to delete weekly practice' });
588
+ }
589
+ });
590
+
591
+ // Create tutorial task (admin only)
592
+ router.post('/admin/tutorial-tasks', authenticateToken, requireAdmin, async (req, res) => {
593
+ try {
594
+ const SourceText = require('../models/SourceText');
595
+ const { content, weekNumber, category } = req.body;
596
+
597
+ // Validate required fields
598
+ if (!content || !weekNumber) {
599
+ return res.status(400).json({ error: 'Content and week number are required' });
600
+ }
601
+
602
+ const newTask = new SourceText({
603
+ content,
604
+ weekNumber: parseInt(weekNumber),
605
+ category: category || 'tutorial',
606
+ title: `Tutorial Task Week ${weekNumber}`,
607
+ sourceLanguage: 'English',
608
+ sourceType: 'tutorial'
609
+ });
610
+
611
+ const savedTask = await newTask.save();
612
+
613
+ res.status(201).json({
614
+ success: true,
615
+ message: 'Tutorial task created successfully',
616
+ task: savedTask
617
+ });
618
+ } catch (error) {
619
+ console.error('Create tutorial task error:', error);
620
+ res.status(500).json({ error: 'Failed to create tutorial task' });
621
+ }
622
+ });
623
+
624
+ // Update tutorial task (admin only)
625
+ router.put('/admin/tutorial-tasks/:id', authenticateToken, requireAdmin, async (req, res) => {
626
+ try {
627
+ const SourceText = require('../models/SourceText');
628
+ const { content, translationBrief, weekNumber } = req.body;
629
+
630
+ const updatedTask = await SourceText.findByIdAndUpdate(
631
+ req.params.id,
632
+ {
633
+ content,
634
+ translationBrief,
635
+ weekNumber: parseInt(weekNumber)
636
+ },
637
+ { new: true, runValidators: true }
638
+ );
639
+
640
+ if (!updatedTask) {
641
+ return res.status(404).json({ error: 'Tutorial task not found' });
642
+ }
643
+
644
+ res.json({
645
+ success: true,
646
+ message: 'Tutorial task updated successfully',
647
+ task: updatedTask
648
+ });
649
+ } catch (error) {
650
+ console.error('Update tutorial task error:', error);
651
+ res.status(500).json({ error: 'Failed to update tutorial task' });
652
+ }
653
+ });
654
+
655
+ // Delete tutorial task (admin only)
656
+ router.delete('/admin/tutorial-tasks/:id', authenticateToken, requireAdmin, async (req, res) => {
657
+ try {
658
+ const SourceText = require('../models/SourceText');
659
+
660
+ const deletedTask = await SourceText.findByIdAndDelete(req.params.id);
661
+
662
+ if (!deletedTask) {
663
+ return res.status(404).json({ error: 'Tutorial task not found' });
664
+ }
665
+
666
+ res.json({
667
+ success: true,
668
+ message: 'Tutorial task deleted successfully'
669
+ });
670
+ } catch (error) {
671
+ console.error('Delete tutorial task error:', error);
672
+ res.status(500).json({ error: 'Failed to delete tutorial task' });
673
+ }
674
+ });
675
+
676
+ // Add translation brief (admin only)
677
+ router.post('/admin/translation-brief', authenticateToken, requireAdmin, async (req, res) => {
678
+ try {
679
+ const SourceText = require('../models/SourceText');
680
+ const { weekNumber, translationBrief, type } = req.body;
681
+
682
+ // Validate required fields
683
+ if (!weekNumber || !translationBrief || !type) {
684
+ return res.status(400).json({ error: 'Week number, translation brief, and type are required' });
685
+ }
686
+
687
+ // Update all existing tasks of the specified type and week with the translation brief
688
+ const result = await SourceText.updateMany(
689
+ {
690
+ category: type === 'tutorial' ? 'tutorial' : 'weekly-practice',
691
+ weekNumber: parseInt(weekNumber)
692
+ },
693
+ { translationBrief }
694
+ );
695
+
696
+ if (result.modifiedCount === 0) {
697
+ return res.status(404).json({ error: 'No tasks found for the specified week and type' });
698
+ }
699
+
700
+ res.json({
701
+ success: true,
702
+ message: `Translation brief added successfully to ${result.modifiedCount} tasks`,
703
+ modifiedCount: result.modifiedCount
704
+ });
705
+ } catch (error) {
706
+ console.error('Add translation brief error:', error);
707
+ res.status(500).json({ error: 'Failed to add translation brief' });
708
+ }
709
+ });
710
+
711
+ // Update tutorial brief (admin only)
712
+ router.put('/admin/tutorial-brief/:weekNumber', authenticateToken, requireAdmin, async (req, res) => {
713
+ try {
714
+ const SourceText = require('../models/SourceText');
715
+ const { translationBrief } = req.body;
716
+ const weekNumber = parseInt(req.params.weekNumber);
717
+
718
+ // Validate required fields - allow empty string for removal
719
+ if (translationBrief === undefined || translationBrief === null) {
720
+ return res.status(400).json({ error: 'Translation brief is required' });
721
+ }
722
+
723
+ // Update all tutorial tasks for the specified week with the new translation brief
724
+ const result = await SourceText.updateMany(
725
+ {
726
+ category: 'tutorial',
727
+ weekNumber: weekNumber
728
+ },
729
+ { translationBrief }
730
+ );
731
+
732
+ if (result.modifiedCount === 0) {
733
+ return res.status(404).json({ error: 'No tutorial tasks found for the specified week' });
734
+ }
735
+
736
+ res.json({
737
+ success: true,
738
+ message: `Translation brief updated successfully for ${result.modifiedCount} tutorial tasks`,
739
+ modifiedCount: result.modifiedCount
740
+ });
741
+ } catch (error) {
742
+ console.error('Update tutorial brief error:', error);
743
+ res.status(500).json({ error: 'Failed to update translation brief' });
744
+ }
745
+ });
746
+
747
+ // Update weekly practice brief (admin only)
748
+ router.put('/admin/weekly-brief/:weekNumber', authenticateToken, requireAdmin, async (req, res) => {
749
+ try {
750
+ const SourceText = require('../models/SourceText');
751
+ const { translationBrief } = req.body;
752
+ const weekNumber = parseInt(req.params.weekNumber);
753
+
754
+ // Validate required fields - allow empty string for removal
755
+ if (translationBrief === undefined || translationBrief === null) {
756
+ return res.status(400).json({ error: 'Translation brief is required' });
757
+ }
758
+
759
+ // Update all weekly practice tasks for the specified week with the new translation brief
760
+ const result = await SourceText.updateMany(
761
+ {
762
+ category: 'weekly-practice',
763
+ weekNumber: weekNumber
764
+ },
765
+ { translationBrief }
766
+ );
767
+
768
+ if (result.modifiedCount === 0) {
769
+ return res.status(404).json({ error: 'No weekly practice tasks found for the specified week' });
770
+ }
771
+
772
+ res.json({
773
+ success: true,
774
+ message: `Translation brief updated successfully for ${result.modifiedCount} weekly practice tasks`,
775
+ modifiedCount: result.modifiedCount
776
+ });
777
+ } catch (error) {
778
+ console.error('Update weekly practice brief error:', error);
779
+ res.status(500).json({ error: 'Failed to update translation brief' });
780
+ }
781
+ });
782
+
783
+ module.exports = { router, authenticateToken };
routes/search.js ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const { authenticateToken } = require('./auth');
4
+
5
+ // Get practice examples (now weekly practice for week 1)
6
+ router.get('/practice-examples', authenticateToken, async (req, res) => {
7
+ try {
8
+ const SourceText = require('../models/SourceText');
9
+ const examples = await SourceText.find({
10
+ category: 'weekly-practice',
11
+ weekNumber: 1
12
+ }).sort({ createdAt: 1 });
13
+
14
+ res.json(examples);
15
+ } catch (error) {
16
+ console.error('Get practice examples error:', error);
17
+ res.status(500).json({ error: 'Failed to get practice examples' });
18
+ }
19
+ });
20
+
21
+ // Get tutorial tasks by week
22
+ router.get('/tutorial-tasks/:week', authenticateToken, async (req, res) => {
23
+ try {
24
+ const SourceText = require('../models/SourceText');
25
+ const weekNumber = parseInt(req.params.week);
26
+
27
+ const tasks = await SourceText.find({
28
+ category: 'tutorial',
29
+ weekNumber: weekNumber
30
+ }).sort({ title: 1 });
31
+
32
+ res.json(tasks);
33
+ } catch (error) {
34
+ console.error('Get tutorial tasks error:', error);
35
+ res.status(500).json({ error: 'Failed to get tutorial tasks' });
36
+ }
37
+ });
38
+
39
+ // Get weekly practice by week
40
+ router.get('/weekly-practice/:week', authenticateToken, async (req, res) => {
41
+ try {
42
+ const SourceText = require('../models/SourceText');
43
+ const weekNumber = parseInt(req.params.week);
44
+
45
+ const practice = await SourceText.find({
46
+ category: 'weekly-practice',
47
+ weekNumber: weekNumber
48
+ }).sort({ title: 1 });
49
+
50
+ res.json(practice);
51
+ } catch (error) {
52
+ console.error('Get weekly practice error:', error);
53
+ res.status(500).json({ error: 'Failed to get weekly practice' });
54
+ }
55
+ });
56
+
57
+ // Initialize practice examples (convert to weekly practice week 1)
58
+ router.post('/initialize-practice-examples', authenticateToken, async (req, res) => {
59
+ try {
60
+ const SourceText = require('../models/SourceText');
61
+
62
+ // Clear existing practice examples
63
+ await SourceText.deleteMany({ category: 'weekly-practice', weekNumber: 1 });
64
+
65
+ const practiceExamples = [
66
+ {
67
+ content: '为什么睡前一定要吃夜宵?因为这样才不会做饿梦。',
68
+ category: 'weekly-practice',
69
+ weekNumber: 1
70
+ },
71
+ {
72
+ content: '女娲用什么补天?强扭的瓜。',
73
+ category: 'weekly-practice',
74
+ weekNumber: 1
75
+ },
76
+ {
77
+ content: '你知道如何区分真假大象吗?把他们仍进水中,真相会浮出水面的。',
78
+ category: 'weekly-practice',
79
+ weekNumber: 1
80
+ },
81
+ {
82
+ content: 'Why do we drive on a parkway and park on a driveway?',
83
+ category: 'weekly-practice',
84
+ weekNumber: 1
85
+ },
86
+ {
87
+ content: 'I can\'t believe I got fired from the calendar factory. All I did was take a day off.',
88
+ category: 'weekly-practice',
89
+ weekNumber: 1
90
+ },
91
+ {
92
+ content: 'Most people are shocked when they find out how bad I am as an electrician.',
93
+ category: 'weekly-practice',
94
+ weekNumber: 1
95
+ }
96
+ ];
97
+
98
+ await SourceText.insertMany(practiceExamples);
99
+
100
+ res.json({
101
+ success: true,
102
+ message: 'Practice examples initialized successfully',
103
+ count: practiceExamples.length
104
+ });
105
+ } catch (error) {
106
+ console.error('Initialize practice examples error:', error);
107
+ res.status(500).json({ error: 'Failed to initialize practice examples' });
108
+ }
109
+ });
110
+
111
+ // Initialize tutorial tasks for a specific week
112
+ router.post('/initialize-tutorial-tasks/:week', authenticateToken, async (req, res) => {
113
+ try {
114
+ const SourceText = require('../models/SourceText');
115
+ const weekNumber = parseInt(req.params.week);
116
+
117
+ // Clear existing tutorial tasks for this week
118
+ await SourceText.deleteMany({ category: 'tutorial', weekNumber: weekNumber });
119
+
120
+ // Example tutorial tasks (you can customize these)
121
+ const tutorialTasks = [
122
+ {
123
+ title: `Tutorial Task 1 - Week ${weekNumber}`,
124
+ content: 'The first paragraph of the source text introduces the main concept and sets the context for the entire piece. This section establishes the foundation upon which the rest of the text builds.',
125
+ category: 'tutorial',
126
+ weekNumber: weekNumber,
127
+ sourceLanguage: 'English',
128
+ sourceCulture: 'Western',
129
+ translationBrief: weekNumber === 1 ? 'The municipal government of Xiamen plans to launch an international campaign to attract foreign investors and tourists. As part of the campaign, they\'re commissioning an English translation of a Chinese booklet about the city\'s history of foreign trade and cultural exchange, in addition to its tourist attractions. You\'ve been provided with the following excerpt for a test translation. They are aware that certain elements in the source text might not work well when translated into English for promotional purposes. So you\'re given the license to make changes where appropriate, but a list of such changes should be provided for the reference of the commissioner.' : 'Translate this text while maintaining the original tone and style. Pay attention to cultural nuances and ensure the translation is appropriate for the target audience. Focus on conveying the meaning accurately while preserving the author\'s intent.'
130
+ },
131
+ {
132
+ title: `Tutorial Task 2 - Week ${weekNumber}`,
133
+ content: 'The second paragraph develops the argument further, providing supporting evidence and examples that reinforce the main points established in the opening section.',
134
+ category: 'tutorial',
135
+ weekNumber: weekNumber,
136
+ sourceLanguage: 'English',
137
+ sourceCulture: 'Western',
138
+ translationBrief: weekNumber === 1 ? 'The municipal government of Xiamen plans to launch an international campaign to attract foreign investors and tourists. As part of the campaign, they\'re commissioning an English translation of a Chinese booklet about the city\'s history of foreign trade and cultural exchange, in addition to its tourist attractions. You\'ve been provided with the following excerpt for a test translation. They are aware that certain elements in the source text might not work well when translated into English for promotional purposes. So you\'re given the license to make changes where appropriate, but a list of such changes should be provided for the reference of the commissioner.' : 'Translate this text while maintaining the original tone and style. Pay attention to cultural nuances and ensure the translation is appropriate for the target audience. Focus on conveying the meaning accurately while preserving the author\'s intent.'
139
+ },
140
+ {
141
+ title: `Tutorial Task 3 - Week ${weekNumber}`,
142
+ content: 'The concluding paragraph brings together all the key elements discussed throughout the text, offering a synthesis of the main ideas and leaving the reader with a clear understanding of the central message.',
143
+ category: 'tutorial',
144
+ weekNumber: weekNumber,
145
+ sourceLanguage: 'English',
146
+ sourceCulture: 'Western',
147
+ translationBrief: weekNumber === 1 ? 'The municipal government of Xiamen plans to launch an international campaign to attract foreign investors and tourists. As part of the campaign, they\'re commissioning an English translation of a Chinese booklet about the city\'s history of foreign trade and cultural exchange, in addition to its tourist attractions. You\'ve been provided with the following excerpt for a test translation. They are aware that certain elements in the source text might not work well when translated into English for promotional purposes. So you\'re given the license to make changes where appropriate, but a list of such changes should be provided for the reference of the commissioner.' : 'Translate this text while maintaining the original tone and style. Pay attention to cultural nuances and ensure the translation is appropriate for the target audience. Focus on conveying the meaning accurately while preserving the author\'s intent.'
148
+ }
149
+ ];
150
+
151
+ await SourceText.insertMany(tutorialTasks);
152
+
153
+ res.json({
154
+ success: true,
155
+ message: `Tutorial tasks for week ${weekNumber} initialized successfully`,
156
+ count: tutorialTasks.length
157
+ });
158
+ } catch (error) {
159
+ console.error('Initialize tutorial tasks error:', error);
160
+ res.status(500).json({ error: 'Failed to initialize tutorial tasks' });
161
+ }
162
+ });
163
+
164
+ // Initialize weekly practice for a specific week
165
+ router.post('/initialize-weekly-practice/:week', authenticateToken, async (req, res) => {
166
+ try {
167
+ const SourceText = require('../models/SourceText');
168
+ const weekNumber = parseInt(req.params.week);
169
+
170
+ // Clear existing weekly practice for this week
171
+ await SourceText.deleteMany({ category: 'weekly-practice', weekNumber: weekNumber });
172
+
173
+ // Example weekly practice (you can customize these)
174
+ const weeklyPractice = [
175
+ {
176
+ title: `Weekly Practice 1 - Week ${weekNumber}`,
177
+ content: 'This is a sample weekly practice example for week ' + weekNumber + '.',
178
+ category: 'weekly-practice',
179
+ weekNumber: weekNumber,
180
+ sourceLanguage: 'English',
181
+ sourceCulture: 'Western'
182
+ },
183
+ {
184
+ title: `Weekly Practice 2 - Week ${weekNumber}`,
185
+ content: 'Another sample weekly practice example for week ' + weekNumber + '.',
186
+ category: 'weekly-practice',
187
+ weekNumber: weekNumber,
188
+ sourceLanguage: 'English',
189
+ sourceCulture: 'Western'
190
+ }
191
+ ];
192
+
193
+ await SourceText.insertMany(weeklyPractice);
194
+
195
+ res.json({
196
+ success: true,
197
+ message: `Weekly practice for week ${weekNumber} initialized successfully`,
198
+ count: weeklyPractice.length
199
+ });
200
+ } catch (error) {
201
+ console.error('Initialize weekly practice error:', error);
202
+ res.status(500).json({ error: 'Failed to initialize weekly practice' });
203
+ }
204
+ });
205
+
206
+ module.exports = router;
routes/sourceTexts.js ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const jwt = require('jsonwebtoken');
3
+ const SourceText = require('../models/SourceText');
4
+ const router = express.Router();
5
+
6
+ // Middleware to verify JWT token
7
+ const authenticateToken = (req, res, next) => {
8
+ const authHeader = req.headers['authorization'];
9
+ const token = authHeader && authHeader.split(' ')[1];
10
+
11
+ if (!token) {
12
+ return res.status(401).json({ error: 'Access token required' });
13
+ }
14
+
15
+ jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key', (err, user) => {
16
+ if (err) {
17
+ return res.status(403).json({ error: 'Invalid token' });
18
+ }
19
+ req.user = user;
20
+ next();
21
+ });
22
+ };
23
+
24
+ // Get all source texts with filters
25
+ router.get('/', authenticateToken, async (req, res) => {
26
+ try {
27
+ const {
28
+ sourceLanguage,
29
+ sourceCulture,
30
+ targetCulture,
31
+ difficulty,
32
+ sourceType,
33
+ tags,
34
+ page = 1,
35
+ limit = 10
36
+ } = req.query;
37
+
38
+ const filter = { isActive: true };
39
+
40
+ if (sourceLanguage) filter.sourceLanguage = sourceLanguage;
41
+ if (sourceCulture) filter.sourceCulture = sourceCulture;
42
+ if (targetCulture) filter.targetCultures = targetCulture;
43
+ if (difficulty) filter.difficulty = difficulty;
44
+ if (sourceType) filter.sourceType = sourceType;
45
+ if (tags) filter.tags = { $in: tags.split(',') };
46
+
47
+ const skip = (page - 1) * limit;
48
+
49
+ const texts = await SourceText.find(filter)
50
+ .sort({ createdAt: -1 })
51
+ .skip(skip)
52
+ .limit(parseInt(limit))
53
+ .populate('createdBy', 'username');
54
+
55
+ const total = await SourceText.countDocuments(filter);
56
+
57
+ res.json({
58
+ texts,
59
+ pagination: {
60
+ page: parseInt(page),
61
+ limit: parseInt(limit),
62
+ total,
63
+ pages: Math.ceil(total / limit)
64
+ }
65
+ });
66
+ } catch (error) {
67
+ console.error('Get source texts error:', error);
68
+ res.status(500).json({ error: 'Failed to get source texts' });
69
+ }
70
+ });
71
+
72
+ // Get a specific source text by ID
73
+ router.get('/:id', authenticateToken, async (req, res) => {
74
+ try {
75
+ const text = await SourceText.findById(req.params.id)
76
+ .populate('createdBy', 'username');
77
+
78
+ if (!text) {
79
+ return res.status(404).json({ error: 'Source text not found' });
80
+ }
81
+
82
+ // Increment usage count
83
+ text.usageCount += 1;
84
+ await text.save();
85
+
86
+ res.json(text);
87
+ } catch (error) {
88
+ console.error('Get source text error:', error);
89
+ res.status(500).json({ error: 'Failed to get source text' });
90
+ }
91
+ });
92
+
93
+ // Create a new source text
94
+ router.post('/', authenticateToken, async (req, res) => {
95
+ try {
96
+ const {
97
+ title,
98
+ content,
99
+ sourceLanguage,
100
+ sourceCulture,
101
+ sourceUrl,
102
+ sourceType,
103
+ culturalElements,
104
+ difficulty,
105
+ tags,
106
+ context,
107
+ targetCultures
108
+ } = req.body;
109
+
110
+ // Validate required fields
111
+ if (!title || !content || !sourceLanguage || !sourceCulture) {
112
+ return res.status(400).json({
113
+ error: 'Title, content, source language, and source culture are required'
114
+ });
115
+ }
116
+
117
+ const sourceText = new SourceText({
118
+ title,
119
+ content,
120
+ sourceLanguage,
121
+ sourceCulture,
122
+ sourceUrl,
123
+ sourceType,
124
+ culturalElements: culturalElements || [],
125
+ difficulty: difficulty || 'intermediate',
126
+ tags: tags || [],
127
+ context,
128
+ targetCultures: targetCultures || [],
129
+ createdBy: req.user.userId
130
+ });
131
+
132
+ await sourceText.save();
133
+
134
+ res.status(201).json({
135
+ message: 'Source text created successfully',
136
+ sourceText
137
+ });
138
+ } catch (error) {
139
+ console.error('Create source text error:', error);
140
+ res.status(500).json({ error: 'Failed to create source text' });
141
+ }
142
+ });
143
+
144
+ // Update a source text
145
+ router.put('/:id', authenticateToken, async (req, res) => {
146
+ try {
147
+ const sourceText = await SourceText.findById(req.params.id);
148
+
149
+ if (!sourceText) {
150
+ return res.status(404).json({ error: 'Source text not found' });
151
+ }
152
+
153
+ // Only allow creators or admins to update
154
+ if (sourceText.createdBy.toString() !== req.user.userId && req.user.role !== 'admin') {
155
+ return res.status(403).json({ error: 'Not authorized to update this source text' });
156
+ }
157
+
158
+ const updatedText = await SourceText.findByIdAndUpdate(
159
+ req.params.id,
160
+ req.body,
161
+ { new: true, runValidators: true }
162
+ );
163
+
164
+ res.json({
165
+ message: 'Source text updated successfully',
166
+ sourceText: updatedText
167
+ });
168
+ } catch (error) {
169
+ console.error('Update source text error:', error);
170
+ res.status(500).json({ error: 'Failed to update source text' });
171
+ }
172
+ });
173
+
174
+ // Delete a source text (soft delete)
175
+ router.delete('/:id', authenticateToken, async (req, res) => {
176
+ try {
177
+ const sourceText = await SourceText.findById(req.params.id);
178
+
179
+ if (!sourceText) {
180
+ return res.status(404).json({ error: 'Source text not found' });
181
+ }
182
+
183
+ // Only allow creators or admins to delete
184
+ if (sourceText.createdBy.toString() !== req.user.userId && req.user.role !== 'admin') {
185
+ return res.status(403).json({ error: 'Not authorized to delete this source text' });
186
+ }
187
+
188
+ sourceText.isActive = false;
189
+ await sourceText.save();
190
+
191
+ res.json({ message: 'Source text deleted successfully' });
192
+ } catch (error) {
193
+ console.error('Delete source text error:', error);
194
+ res.status(500).json({ error: 'Failed to delete source text' });
195
+ }
196
+ });
197
+
198
+ // Rate a source text
199
+ router.post('/:id/rate', authenticateToken, async (req, res) => {
200
+ try {
201
+ const { rating } = req.body;
202
+
203
+ if (!rating || rating < 1 || rating > 5) {
204
+ return res.status(400).json({ error: 'Rating must be between 1 and 5' });
205
+ }
206
+
207
+ const sourceText = await SourceText.findById(req.params.id);
208
+
209
+ if (!sourceText) {
210
+ return res.status(404).json({ error: 'Source text not found' });
211
+ }
212
+
213
+ // Update average rating
214
+ const newTotal = (sourceText.averageRating * sourceText.ratingCount) + rating;
215
+ sourceText.ratingCount += 1;
216
+ sourceText.averageRating = newTotal / sourceText.ratingCount;
217
+
218
+ await sourceText.save();
219
+
220
+ res.json({
221
+ message: 'Rating submitted successfully',
222
+ averageRating: sourceText.averageRating,
223
+ ratingCount: sourceText.ratingCount
224
+ });
225
+ } catch (error) {
226
+ console.error('Rate source text error:', error);
227
+ res.status(500).json({ error: 'Failed to submit rating' });
228
+ }
229
+ });
230
+
231
+ // Get cultural elements for a source text
232
+ router.get('/:id/cultural-elements', authenticateToken, async (req, res) => {
233
+ try {
234
+ const sourceText = await SourceText.findById(req.params.id);
235
+
236
+ if (!sourceText) {
237
+ return res.status(404).json({ error: 'Source text not found' });
238
+ }
239
+
240
+ res.json({
241
+ culturalElements: sourceText.culturalElements,
242
+ content: sourceText.content
243
+ });
244
+ } catch (error) {
245
+ console.error('Get cultural elements error:', error);
246
+ res.status(500).json({ error: 'Failed to get cultural elements' });
247
+ }
248
+ });
249
+
250
+ // Highlight cultural elements in text
251
+ router.post('/:id/highlight', authenticateToken, async (req, res) => {
252
+ try {
253
+ const { culturalElements } = req.body;
254
+
255
+ const sourceText = await SourceText.findById(req.params.id);
256
+
257
+ if (!sourceText) {
258
+ return res.status(404).json({ error: 'Source text not found' });
259
+ }
260
+
261
+ // Only allow creators or admins to update cultural elements
262
+ if (sourceText.createdBy.toString() !== req.user.userId && req.user.role !== 'admin') {
263
+ return res.status(403).json({ error: 'Not authorized to update this source text' });
264
+ }
265
+
266
+ sourceText.culturalElements = culturalElements;
267
+ await sourceText.save();
268
+
269
+ res.json({
270
+ message: 'Cultural elements updated successfully',
271
+ culturalElements: sourceText.culturalElements
272
+ });
273
+ } catch (error) {
274
+ console.error('Highlight cultural elements error:', error);
275
+ res.status(500).json({ error: 'Failed to update cultural elements' });
276
+ }
277
+ });
278
+
279
+ module.exports = router;
routes/submissions.js ADDED
@@ -0,0 +1,703 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const Submission = require('../models/Submission');
3
+ const SourceText = require('../models/SourceText');
4
+ const User = require('../models/User');
5
+ const router = express.Router();
6
+ const mongoose = require('mongoose'); // Added for mongoose.Types.ObjectId
7
+
8
+ // Middleware to verify token (simplified)
9
+ const authenticateToken = (req, res, next) => {
10
+ const authHeader = req.headers['authorization'];
11
+ const token = authHeader && authHeader.split(' ')[1];
12
+
13
+ if (!token) {
14
+ return res.status(401).json({ error: 'Access token required' });
15
+ }
16
+
17
+ // For our simplified system, just check if token exists and has the right format
18
+ if (token.startsWith('user_') || token.startsWith('visitor_')) {
19
+ // Extract user ID from token and create a consistent ObjectId
20
+ const userIdString = token.split('_')[1] || 'default_user';
21
+
22
+ // Create a consistent ObjectId based on the user token
23
+ // This ensures the same user always gets the same ObjectId across requests
24
+ const crypto = require('crypto');
25
+ const hash = crypto.createHash('md5').update(userIdString).digest('hex');
26
+ const consistentObjectId = new mongoose.Types.ObjectId(hash.substring(0, 24));
27
+
28
+ // Get user role from headers (frontend will send this)
29
+ const userRole = req.headers['user-role'] || 'visitor';
30
+
31
+ req.user = {
32
+ userId: consistentObjectId,
33
+ token,
34
+ role: userRole
35
+ };
36
+ next();
37
+ } else {
38
+ return res.status(403).json({ error: 'Invalid token' });
39
+ }
40
+ };
41
+
42
+ // Get all submissions for voting (anonymized) - MOVED TO TOP
43
+ router.get('/voteable', authenticateToken, async (req, res) => {
44
+ try {
45
+ // Use the consistent user ID from authentication middleware
46
+ const userId = req.user.userId;
47
+
48
+ // Get all submissions that are ready for voting (limit to 500 to prevent server overload)
49
+ const submissions = await Submission.find({
50
+ status: { $in: ['submitted', 'reviewed', 'approved'] }
51
+ })
52
+ .sort({ createdAt: -1 })
53
+ .limit(500)
54
+ .populate('sourceTextId', 'content sourceLanguage category weekNumber title');
55
+
56
+ // Group submissions by source text (example)
57
+ const groupedByExample = {};
58
+
59
+ submissions.forEach(submission => {
60
+ if (!submission.sourceTextId) {
61
+ return;
62
+ }
63
+
64
+ const sourceTextId = submission.sourceTextId._id.toString();
65
+ if (!groupedByExample[sourceTextId]) {
66
+ groupedByExample[sourceTextId] = {
67
+ example: {
68
+ id: sourceTextId,
69
+ content: submission.sourceTextId.content,
70
+ language: submission.sourceTextId.sourceLanguage,
71
+ culture: 'Western',
72
+ category: submission.sourceTextId.category || 'tutorial',
73
+ weekNumber: submission.sourceTextId.weekNumber || 1,
74
+ title: submission.sourceTextId.title || `Example ${sourceTextId.slice(-4)}`
75
+ },
76
+ translations: []
77
+ };
78
+ }
79
+
80
+ // Anonymize the user and handle group submissions
81
+ const anonymizedUser = {
82
+ _id: submission.userId,
83
+ name: submission.groupNumber ? `Group ${submission.groupNumber}` : `Student_${submission.userId.toString().slice(-4)}`
84
+ };
85
+
86
+ // Calculate vote score and counts
87
+ const score = submission.calculateScore();
88
+ const voteCounts = submission.getVoteCountByRank();
89
+
90
+ // Check if current user has voted on this submission
91
+ const userVote = submission.votes.find(v => v.userId.toString() === userId.toString());
92
+ const hasVoted = !!userVote;
93
+ const userRank = userVote ? userVote.rank : null;
94
+
95
+ groupedByExample[sourceTextId].translations.push({
96
+ id: submission._id,
97
+ translation: submission.transcreation,
98
+ score,
99
+ voteCounts,
100
+ totalVotes: submission.votes.length,
101
+ createdAt: submission.createdAt,
102
+ hasVoted,
103
+ userRank,
104
+ groupNumber: submission.groupNumber,
105
+ isGroupSubmission: !!submission.groupNumber
106
+ });
107
+ });
108
+
109
+ // Convert to array format for frontend and sort by week number, then category, then title
110
+ const voteableExamples = Object.values(groupedByExample)
111
+ .sort((a, b) => {
112
+ // First sort by week number
113
+ if (a.example.weekNumber !== b.example.weekNumber) {
114
+ return a.example.weekNumber - b.example.weekNumber;
115
+ }
116
+
117
+ // Then sort by category (tutorial first, then weekly-practice)
118
+ if (a.example.category !== b.example.category) {
119
+ if (a.example.category === 'tutorial') return -1;
120
+ if (b.example.category === 'tutorial') return 1;
121
+ }
122
+
123
+ // Finally sort by title to maintain consistent order
124
+ return a.example.title.localeCompare(b.example.title);
125
+ })
126
+ .map(group => ({
127
+ example: group.example,
128
+ translations: group.translations.sort((a, b) => b.score - a.score)
129
+ }));
130
+
131
+ res.json({
132
+ examples: voteableExamples
133
+ });
134
+ } catch (error) {
135
+ console.error('Get voteable submissions error:', error);
136
+ res.status(500).json({ error: 'Failed to get voteable submissions' });
137
+ }
138
+ });
139
+
140
+ // Submit a new transcreation
141
+ router.post('/', authenticateToken, async (req, res) => {
142
+ try {
143
+ const {
144
+ sourceTextId,
145
+ targetCulture,
146
+ targetLanguage,
147
+ transcreation,
148
+ explanation,
149
+ culturalAdaptations = [],
150
+ isAnonymous = true,
151
+ groupNumber
152
+ } = req.body;
153
+
154
+ // Validate required fields
155
+ if (!sourceTextId || !transcreation) {
156
+ return res.status(400).json({
157
+ error: 'Source text ID and transcreation are required'
158
+ });
159
+ }
160
+
161
+ // Check if source text exists
162
+ const sourceText = await SourceText.findById(sourceTextId);
163
+ if (!sourceText) {
164
+ return res.status(404).json({ error: 'Source text not found' });
165
+ }
166
+
167
+ // Use the consistent user ID from authentication middleware
168
+ const userId = req.user.userId;
169
+
170
+ // For tutorial tasks, group number is required
171
+ if (sourceText.category === 'tutorial') {
172
+ if (!groupNumber || groupNumber < 1 || groupNumber > 8) {
173
+ return res.status(400).json({
174
+ error: 'Group number (1-8) is required for tutorial task submissions'
175
+ });
176
+ }
177
+
178
+ // Check if group already submitted for this source text
179
+ const existingGroupSubmission = await Submission.findOne({
180
+ sourceTextId,
181
+ groupNumber,
182
+ targetCulture
183
+ });
184
+
185
+ if (existingGroupSubmission) {
186
+ return res.status(400).json({
187
+ error: 'Your group has already submitted a transcreation for this tutorial task'
188
+ });
189
+ }
190
+ } else {
191
+ // For non-tutorial tasks, check if user already submitted
192
+ const existingSubmission = await Submission.findOne({
193
+ sourceTextId,
194
+ userId: userId,
195
+ targetCulture
196
+ });
197
+
198
+ if (existingSubmission) {
199
+ return res.status(400).json({
200
+ error: 'You have already submitted a transcreation for this source text and target culture'
201
+ });
202
+ }
203
+ }
204
+
205
+ // Extract username from token - use a more reliable method
206
+ let username = 'Unknown';
207
+ // For now, we'll use a generic approach since the token doesn't contain the username
208
+ // The frontend should send the username in the request body or we should store it differently
209
+ if (req.body.username) {
210
+ username = req.body.username;
211
+ } else if (req.user.name) {
212
+ username = req.user.name;
213
+ } else {
214
+ // Fallback to a generic identifier
215
+ username = `User_${Date.now().toString().slice(-4)}`;
216
+ }
217
+
218
+ const submission = new Submission({
219
+ sourceTextId,
220
+ userId: userId,
221
+ username: username,
222
+ groupNumber: sourceText.category === 'tutorial' ? groupNumber : undefined,
223
+ targetCulture: targetCulture || 'Western',
224
+ targetLanguage: targetLanguage || 'English',
225
+ transcreation,
226
+ explanation: explanation || 'Translation submission',
227
+ culturalAdaptations,
228
+ isAnonymous: isAnonymous !== undefined ? isAnonymous : true,
229
+ status: 'submitted',
230
+ difficulty: sourceText.difficulty || 'intermediate'
231
+ });
232
+
233
+
234
+
235
+ await submission.save();
236
+
237
+ res.status(201).json({
238
+ message: 'Transcreation submitted successfully',
239
+ submission
240
+ });
241
+ } catch (error) {
242
+ console.error('Submit transcreation error:', error);
243
+ res.status(500).json({ error: 'Failed to submit transcreation' });
244
+ }
245
+ });
246
+
247
+ // Get all submissions for a source text (anonymized)
248
+ router.get('/source-text/:sourceTextId', authenticateToken, async (req, res) => {
249
+ try {
250
+ const { targetCulture, page = 1, limit = 10 } = req.query;
251
+ const skip = (page - 1) * limit;
252
+
253
+ const filter = {
254
+ sourceTextId: req.params.sourceTextId,
255
+ status: { $in: ['submitted', 'reviewed', 'approved'] }
256
+ };
257
+
258
+ if (targetCulture) {
259
+ filter.targetCulture = targetCulture;
260
+ }
261
+
262
+ const submissions = await Submission.find(filter)
263
+ .sort({ score: -1, createdAt: -1 })
264
+ .skip(skip)
265
+ .limit(parseInt(limit))
266
+ .populate('userId', 'username')
267
+ .populate('sourceTextId', 'title content');
268
+
269
+ // Anonymize submissions
270
+ const anonymizedSubmissions = submissions.map(submission => {
271
+ const submissionObj = submission.toObject();
272
+
273
+ if (submission.isAnonymous) {
274
+ submissionObj.userId = {
275
+ _id: submission.userId._id,
276
+ username: `Student_${submission.userId._id.toString().slice(-4)}`
277
+ };
278
+ } else {
279
+ // Always use the stored username for non-anonymous submissions
280
+ submissionObj.userId = {
281
+ _id: submission.userId._id || submission.userId,
282
+ username: submission.username || 'Unknown'
283
+ };
284
+ }
285
+
286
+ return submissionObj;
287
+ });
288
+
289
+ const total = await Submission.countDocuments(filter);
290
+
291
+ res.json({
292
+ submissions: anonymizedSubmissions,
293
+ pagination: {
294
+ page: parseInt(page),
295
+ limit: parseInt(limit),
296
+ total,
297
+ pages: Math.ceil(total / limit)
298
+ }
299
+ });
300
+ } catch (error) {
301
+ console.error('Get submissions error:', error);
302
+ res.status(500).json({ error: 'Failed to get submissions' });
303
+ }
304
+ });
305
+
306
+ // Get all submissions for source texts (visible to all users)
307
+ router.get('/my-submissions', authenticateToken, async (req, res) => {
308
+ try {
309
+ // Get all submissions with source text info (limit to 1000 to prevent server overload)
310
+ const submissions = await Submission.find({})
311
+ .sort({ createdAt: -1 })
312
+ .limit(1000)
313
+ .populate('sourceTextId', 'title content sourceLanguage category weekNumber');
314
+
315
+ // Add user info to each submission
316
+ const submissionsWithUserInfo = submissions.map(submission => {
317
+ const submissionObj = submission.toObject();
318
+ submissionObj.isOwner = submission.userId && submission.userId.toString() === req.user.userId.toString();
319
+
320
+ // Handle anonymous submissions and create user info
321
+ let username = 'Unknown';
322
+ if (submission.isAnonymous && !submissionObj.isOwner) {
323
+ username = 'Anonymous';
324
+ } else {
325
+ // For non-anonymous or owner viewing, show a meaningful identifier
326
+ if (submission.groupNumber) {
327
+ username = `Group ${submission.groupNumber}`;
328
+ } else {
329
+ // Use the stored username for weekly practice submissions
330
+ username = submission.username || 'Unknown';
331
+ }
332
+ }
333
+
334
+ submissionObj.userId = {
335
+ _id: submission.userId || 'Unknown',
336
+ username: username
337
+ };
338
+
339
+ // Include isAnonymous field for frontend
340
+ submissionObj.isAnonymous = submission.isAnonymous;
341
+
342
+ // Add vote counts
343
+ submissionObj.voteCounts = submission.getVoteCountByRank();
344
+ return submissionObj;
345
+ });
346
+
347
+ res.json({
348
+ submissions: submissionsWithUserInfo
349
+ });
350
+ } catch (error) {
351
+ console.error('Get submissions error:', error);
352
+ res.status(500).json({ error: 'Failed to get submissions' });
353
+ }
354
+ });
355
+
356
+ // Update a submission (only by owner)
357
+ router.put('/:submissionId', authenticateToken, async (req, res) => {
358
+ try {
359
+ const { submissionId } = req.params;
360
+ const { transcreation } = req.body;
361
+
362
+ if (!transcreation) {
363
+ return res.status(400).json({ error: 'Transcreation is required' });
364
+ }
365
+
366
+ const submission = await Submission.findById(submissionId);
367
+ if (!submission) {
368
+ return res.status(404).json({ error: 'Submission not found' });
369
+ }
370
+
371
+ // Check if user owns this submission
372
+ if (submission.userId.toString() !== req.user.userId.toString()) {
373
+ return res.status(403).json({ error: 'You can only edit your own submissions' });
374
+ }
375
+
376
+ submission.transcreation = transcreation;
377
+ await submission.save();
378
+
379
+ res.json({ message: 'Submission updated successfully', submission });
380
+ } catch (error) {
381
+ console.error('Update submission error:', error);
382
+ res.status(500).json({ error: 'Failed to update submission' });
383
+ }
384
+ });
385
+
386
+ // Delete a submission (only by admin or owner)
387
+ router.delete('/:submissionId', authenticateToken, async (req, res) => {
388
+ try {
389
+ const { submissionId } = req.params;
390
+ const user = req.user;
391
+
392
+ const submission = await Submission.findById(submissionId);
393
+ if (!submission) {
394
+ return res.status(404).json({ error: 'Submission not found' });
395
+ }
396
+
397
+ // Check if user is admin or owns this submission
398
+ const isAdmin = user.role === 'admin';
399
+ const isOwner = submission.userId.toString() === user.userId.toString();
400
+
401
+ if (!isAdmin && !isOwner) {
402
+ return res.status(403).json({ error: 'You can only delete your own submissions or must be an admin' });
403
+ }
404
+
405
+ await Submission.findByIdAndDelete(submissionId);
406
+
407
+ res.json({ message: 'Submission deleted successfully' });
408
+ } catch (error) {
409
+ console.error('Delete submission error:', error);
410
+ res.status(500).json({ error: 'Failed to delete submission' });
411
+ }
412
+ });
413
+
414
+ // Get a specific submission
415
+ router.get('/:id', authenticateToken, async (req, res) => {
416
+ try {
417
+ const submission = await Submission.findById(req.params.id)
418
+ .populate('userId', 'username')
419
+ .populate('sourceTextId', 'title content sourceLanguage culturalElements');
420
+
421
+ if (!submission) {
422
+ return res.status(404).json({ error: 'Submission not found' });
423
+ }
424
+
425
+ // Check if user can view this submission
426
+ if (submission.userId._id.toString() !== req.user.userId && req.user.role === 'student') {
427
+ return res.status(403).json({ error: 'Not authorized to view this submission' });
428
+ }
429
+
430
+ // Anonymize if needed
431
+ if (submission.isAnonymous && submission.userId._id.toString() !== req.user.userId) {
432
+ submission.userId = {
433
+ _id: submission.userId._id,
434
+ username: `Student_${submission.userId._id.toString().slice(-4)}`
435
+ };
436
+ }
437
+
438
+ res.json(submission);
439
+ } catch (error) {
440
+ console.error('Get submission error:', error);
441
+ res.status(500).json({ error: 'Failed to get submission' });
442
+ }
443
+ });
444
+
445
+ // Vote on a submission (top 3 voting with constraints)
446
+ router.post('/:id/vote', authenticateToken, async (req, res) => {
447
+ try {
448
+ const { rank, cancel } = req.body; // rank: 1, 2, or 3 for 1st, 2nd, or 3rd place, cancel: true to cancel vote
449
+
450
+ // Use the consistent user ID from authentication middleware
451
+ const userId = req.user.userId;
452
+
453
+ // Handle vote cancellation
454
+ if (cancel) {
455
+ const submission = await Submission.findById(req.params.id);
456
+
457
+ if (!submission) {
458
+ return res.status(404).json({ error: 'Submission not found' });
459
+ }
460
+
461
+ // Remove the user's vote from this submission
462
+ submission.votes = submission.votes.filter(v => v.userId.toString() !== userId.toString());
463
+
464
+ await submission.save();
465
+
466
+ // Calculate new score and vote counts
467
+ const score = submission.calculateScore();
468
+ const voteCounts = submission.getVoteCountByRank();
469
+
470
+ res.json({
471
+ message: 'Vote cancelled successfully',
472
+ score,
473
+ voteCounts,
474
+ totalVotes: submission.votes.length,
475
+ userRank: null
476
+ });
477
+ return;
478
+ }
479
+
480
+ // Handle vote submission
481
+ if (!rank || ![1, 2, 3].includes(rank)) {
482
+ return res.status(400).json({ error: 'Rank must be 1 (1st place), 2 (2nd place), or 3 (3rd place)' });
483
+ }
484
+
485
+ const submission = await Submission.findById(req.params.id);
486
+
487
+ if (!submission) {
488
+ return res.status(404).json({ error: 'Submission not found' });
489
+ }
490
+
491
+ // Check if user already voted on this submission
492
+ const existingVote = submission.votes.find(v => v.userId.toString() === userId.toString());
493
+
494
+ if (existingVote) {
495
+ // Update existing vote
496
+ existingVote.rank = rank;
497
+ existingVote.createdAt = Date.now();
498
+ } else {
499
+ // For now, allow voting without strict constraints
500
+ // In a real implementation, you'd want to enforce the 3-vote limit per example
501
+ submission.votes.push({
502
+ userId: userId,
503
+ rank,
504
+ createdAt: Date.now()
505
+ });
506
+ }
507
+
508
+ await submission.save();
509
+
510
+ // Calculate new score and vote counts
511
+ const score = submission.calculateScore();
512
+ const voteCounts = submission.getVoteCountByRank();
513
+
514
+ res.json({
515
+ message: 'Vote submitted successfully',
516
+ score,
517
+ voteCounts,
518
+ totalVotes: submission.votes.length,
519
+ userRank: rank
520
+ });
521
+ } catch (error) {
522
+ console.error('Vote submission error:', error);
523
+ res.status(500).json({ error: 'Failed to submit vote' });
524
+ }
525
+ });
526
+
527
+ // Add feedback to a submission
528
+ router.post('/:id/feedback', authenticateToken, async (req, res) => {
529
+ try {
530
+ const { comment, rating } = req.body;
531
+
532
+ if (!comment) {
533
+ return res.status(400).json({ error: 'Comment is required' });
534
+ }
535
+
536
+ if (rating && (rating < 1 || rating > 5)) {
537
+ return res.status(400).json({ error: 'Rating must be between 1 and 5' });
538
+ }
539
+
540
+ const submission = await Submission.findById(req.params.id);
541
+
542
+ if (!submission) {
543
+ return res.status(404).json({ error: 'Submission not found' });
544
+ }
545
+
546
+ // Check if user already provided feedback
547
+ const existingFeedback = submission.feedback.find(f => f.userId.toString() === req.user.userId);
548
+
549
+ if (existingFeedback) {
550
+ return res.status(400).json({ error: 'You have already provided feedback for this submission' });
551
+ }
552
+
553
+ submission.feedback.push({
554
+ userId: req.user.userId,
555
+ comment,
556
+ rating,
557
+ createdAt: Date.now()
558
+ });
559
+
560
+ await submission.save();
561
+
562
+ res.json({
563
+ message: 'Feedback submitted successfully',
564
+ feedbackCount: submission.feedback.length
565
+ });
566
+ } catch (error) {
567
+ console.error('Add feedback error:', error);
568
+ res.status(500).json({ error: 'Failed to add feedback' });
569
+ }
570
+ });
571
+
572
+ // Get feedback for a submission
573
+ router.get('/:id/feedback', authenticateToken, async (req, res) => {
574
+ try {
575
+ const submission = await Submission.findById(req.params.id)
576
+ .populate('feedback.userId', 'username');
577
+
578
+ if (!submission) {
579
+ return res.status(404).json({ error: 'Submission not found' });
580
+ }
581
+
582
+ // Anonymize feedback if submission is anonymous
583
+ const anonymizedFeedback = submission.feedback.map(feedback => {
584
+ const feedbackObj = feedback.toObject();
585
+
586
+ if (submission.isAnonymous && feedback.userId._id.toString() !== req.user.userId) {
587
+ feedbackObj.userId = {
588
+ _id: feedback.userId._id,
589
+ username: `Student_${feedback.userId._id.toString().slice(-4)}`
590
+ };
591
+ }
592
+
593
+ return feedbackObj;
594
+ });
595
+
596
+ res.json({
597
+ feedback: anonymizedFeedback,
598
+ totalFeedback: submission.feedback.length
599
+ });
600
+ } catch (error) {
601
+ console.error('Get feedback error:', error);
602
+ res.status(500).json({ error: 'Failed to get feedback' });
603
+ }
604
+ });
605
+
606
+ // Update submission
607
+ router.put('/:id', authenticateToken, async (req, res) => {
608
+ try {
609
+ const submission = await Submission.findById(req.params.id);
610
+
611
+ if (!submission) {
612
+ return res.status(404).json({ error: 'Submission not found' });
613
+ }
614
+
615
+ // Only allow submission owner to update
616
+ if (submission.userId.toString() !== req.user.userId) {
617
+ return res.status(403).json({ error: 'Not authorized to update this submission' });
618
+ }
619
+
620
+ // Only allow updates if submission is still in draft or submitted status
621
+ if (!['draft', 'submitted'].includes(submission.status)) {
622
+ return res.status(400).json({ error: 'Cannot update submission in current status' });
623
+ }
624
+
625
+ const updatedSubmission = await Submission.findByIdAndUpdate(
626
+ req.params.id,
627
+ req.body,
628
+ { new: true, runValidators: true }
629
+ );
630
+
631
+ res.json({
632
+ message: 'Submission updated successfully',
633
+ submission: updatedSubmission
634
+ });
635
+ } catch (error) {
636
+ console.error('Update submission error:', error);
637
+ res.status(500).json({ error: 'Failed to update submission' });
638
+ }
639
+ });
640
+
641
+ // Delete submission
642
+ router.delete('/:id', authenticateToken, async (req, res) => {
643
+ try {
644
+ const submission = await Submission.findById(req.params.id);
645
+
646
+ if (!submission) {
647
+ return res.status(404).json({ error: 'Submission not found' });
648
+ }
649
+
650
+ // Only allow submission owner to delete
651
+ if (submission.userId.toString() !== req.user.userId) {
652
+ return res.status(403).json({ error: 'Not authorized to delete this submission' });
653
+ }
654
+
655
+ await Submission.findByIdAndDelete(req.params.id);
656
+
657
+ res.json({ message: 'Submission deleted successfully' });
658
+ } catch (error) {
659
+ console.error('Delete submission error:', error);
660
+ res.status(500).json({ error: 'Failed to delete submission' });
661
+ }
662
+ });
663
+
664
+ // Get submission statistics
665
+ router.get('/stats/source-text/:sourceTextId', authenticateToken, async (req, res) => {
666
+ try {
667
+ const { targetCulture } = req.query;
668
+
669
+ const filter = { sourceTextId: req.params.sourceTextId };
670
+ if (targetCulture) {
671
+ filter.targetCulture = targetCulture;
672
+ }
673
+
674
+ const stats = await Submission.aggregate([
675
+ { $match: filter },
676
+ {
677
+ $group: {
678
+ _id: null,
679
+ totalSubmissions: { $sum: 1 },
680
+ averageScore: { $avg: '$score' },
681
+ totalVotes: { $sum: { $size: '$votes' } },
682
+ totalFeedback: { $sum: { $size: '$feedback' } },
683
+ targetCultures: { $addToSet: '$targetCulture' }
684
+ }
685
+ }
686
+ ]);
687
+
688
+ res.json({
689
+ stats: stats[0] || {
690
+ totalSubmissions: 0,
691
+ averageScore: 0,
692
+ totalVotes: 0,
693
+ totalFeedback: 0,
694
+ targetCultures: []
695
+ }
696
+ });
697
+ } catch (error) {
698
+ console.error('Get submission stats error:', error);
699
+ res.status(500).json({ error: 'Failed to get submission statistics' });
700
+ }
701
+ });
702
+
703
+ module.exports = router;
stress-test.js ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+ const Submission = require('./models/Submission');
3
+ const SourceText = require('./models/SourceText');
4
+
5
+ // Connect to MongoDB
6
+ mongoose.connect('mongodb://localhost:27017/transcreation-sandbox', {
7
+ useNewUrlParser: true,
8
+ useUnifiedTopology: true,
9
+ });
10
+
11
+ const sampleTranscreations = [
12
+ "This is a sample transcreation for stress testing",
13
+ "Another creative translation approach",
14
+ "Cultural adaptation with local flavor",
15
+ "Modern interpretation of the source text",
16
+ "Traditional approach with contemporary twist",
17
+ "Innovative translation strategy",
18
+ "Balanced cultural and linguistic adaptation",
19
+ "Dynamic transcreation with cultural sensitivity",
20
+ "Comprehensive translation solution",
21
+ "Strategic cultural adaptation",
22
+ "Linguistic excellence with cultural awareness",
23
+ "Creative translation methodology",
24
+ "Cultural bridge through translation",
25
+ "Adaptive translation approach",
26
+ "Comprehensive cultural translation"
27
+ ];
28
+
29
+ const sampleExplanations = [
30
+ "This translation adapts the cultural context while maintaining the original meaning.",
31
+ "The transcreation considers local cultural nuances and preferences.",
32
+ "Cultural elements have been adapted to resonate with the target audience.",
33
+ "This version maintains the essence while adapting to cultural differences.",
34
+ "The translation strategy focuses on cultural relevance and accessibility.",
35
+ "Adaptive approach that bridges cultural gaps effectively.",
36
+ "Cultural sensitivity guides this translation approach.",
37
+ "This transcreation balances authenticity with cultural adaptation.",
38
+ "Strategic cultural adaptation for maximum impact.",
39
+ "Comprehensive approach to cultural translation challenges.",
40
+ "Linguistic excellence combined with cultural awareness.",
41
+ "Creative methodology for cultural translation.",
42
+ "Cultural bridge building through thoughtful translation.",
43
+ "Adaptive approach to cultural translation challenges.",
44
+ "Comprehensive cultural translation strategy."
45
+ ];
46
+
47
+ const sampleUsers = [
48
+ { name: "Alice Johnson", role: "student" },
49
+ { name: "Bob Smith", role: "student" },
50
+ { name: "Carol Davis", role: "student" },
51
+ { name: "David Wilson", role: "student" },
52
+ { name: "Eva Brown", role: "student" },
53
+ { name: "Frank Miller", role: "student" },
54
+ { name: "Grace Taylor", role: "student" },
55
+ { name: "Henry Anderson", role: "student" },
56
+ { name: "Ivy Martinez", role: "student" },
57
+ { name: "Jack Thompson", role: "student" },
58
+ { name: "Kate Garcia", role: "student" },
59
+ { name: "Leo Rodriguez", role: "student" },
60
+ { name: "Maya Lee", role: "student" },
61
+ { name: "Noah White", role: "student" },
62
+ { name: "Olivia Clark", role: "student" }
63
+ ];
64
+
65
+ async function createStressTestData() {
66
+ try {
67
+ console.log('Starting stress test data creation...');
68
+
69
+ // Get all source texts
70
+ const sourceTexts = await SourceText.find({});
71
+ console.log(`Found ${sourceTexts.length} source texts`);
72
+
73
+ let totalSubmissions = 0;
74
+
75
+ for (const sourceText of sourceTexts) {
76
+ console.log(`Adding 15 submissions for source text: ${sourceText.title}`);
77
+
78
+ for (let i = 0; i < 15; i++) {
79
+ const user = sampleUsers[i];
80
+ const transcreation = sampleTranscreations[i];
81
+ const explanation = sampleExplanations[i];
82
+
83
+ // Create a consistent ObjectId for the user
84
+ const crypto = require('crypto');
85
+ const hash = crypto.createHash('md5').update(user.name).digest('hex');
86
+ const userId = new mongoose.Types.ObjectId(hash.substring(0, 24));
87
+
88
+ const submissionData = {
89
+ sourceTextId: sourceText._id,
90
+ userId: userId,
91
+ username: user.name,
92
+ groupNumber: sourceText.category === 'tutorial' ? (i % 8) + 1 : undefined,
93
+ targetCulture: 'Western',
94
+ targetLanguage: 'English',
95
+ transcreation: transcreation,
96
+ explanation: explanation,
97
+ culturalAdaptations: ['Cultural adaptation 1', 'Cultural adaptation 2'],
98
+ isAnonymous: Math.random() > 0.5, // Randomly make some anonymous
99
+ status: 'submitted',
100
+ difficulty: ['beginner', 'intermediate', 'advanced'][Math.floor(Math.random() * 3)],
101
+ votes: [],
102
+ feedback: [],
103
+ score: Math.floor(Math.random() * 100),
104
+ createdAt: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000) // Random date within last 30 days
105
+ };
106
+
107
+ const submission = new Submission(submissionData);
108
+ await submission.save();
109
+ totalSubmissions++;
110
+ }
111
+ }
112
+
113
+ console.log(`✅ Successfully created ${totalSubmissions} stress test submissions`);
114
+ console.log(`📊 Total submissions in database: ${await Submission.countDocuments()}`);
115
+
116
+ } catch (error) {
117
+ console.error('❌ Error creating stress test data:', error);
118
+ } finally {
119
+ mongoose.connection.close();
120
+ console.log('Database connection closed');
121
+ }
122
+ }
123
+
124
+ // Run the stress test
125
+ createStressTestData();