bulltrackers-module 1.0.620 → 1.0.621

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,19 +22,38 @@ const crypto = require('crypto');
22
22
  * Fetches data from a user-centric sub-collection.
23
23
  * - If `documentName` is provided, returns a SINGLE object.
24
24
  * - If `documentName` is missing, returns an ARRAY of all objects in the collection.
25
+ *
26
+ * @param {Object} firestore - Firestore instance
27
+ * @param {string} userId - User ID
28
+ * @param {string} collectionName - Collection name
29
+ * @param {string} dataType - Data type identifier
30
+ * @param {string} userType - User type (e.g., 'SignedInUsers', 'PopularInvestors')
31
+ * @param {string|null} documentName - Document name (null for all documents)
32
+ * @param {string[]} fields - Optional array of field names to select (Firestore projection)
33
+ * @returns {Promise<Object|Array|null>} Single document, array of documents, or null
25
34
  */
26
- const latestUserCentricSnapshot = async (firestore, userId, collectionName, dataType, userType, documentName) => {
35
+ const latestUserCentricSnapshot = async (firestore, userId, collectionName, dataType, userType, documentName, fields = []) => {
27
36
  try {
28
37
  const baseRef = firestore.collection(userType).doc(userId).collection(collectionName);
29
38
 
30
39
  if (documentName) {
31
40
  // SCENARIO 1: Single Document
32
- const docSnapshot = await baseRef.doc(documentName).get();
41
+ let docRef = baseRef.doc(documentName);
42
+ // Apply field selection if fields are specified (Firestore projection)
43
+ if (fields.length > 0) {
44
+ docRef = docRef.select(...fields);
45
+ }
46
+ const docSnapshot = await docRef.get();
33
47
  if (!docSnapshot.exists) return null;
34
48
  return { id: docSnapshot.id, ...docSnapshot.data(), dataType };
35
49
  } else {
36
50
  // SCENARIO 2: All Documents (e.g. Watchlists)
37
- const querySnapshot = await baseRef.get();
51
+ let query = baseRef;
52
+ // Apply field selection if fields are specified (Firestore projection)
53
+ // Note: Firestore doesn't support select() on collection queries, only on document references
54
+ // For collection queries, we'll fetch all fields but could filter in memory if needed
55
+ // For now, we'll only apply projection to single document queries
56
+ const querySnapshot = await query.get();
38
57
  if (querySnapshot.empty) return [];
39
58
  return querySnapshot.docs.map(doc => ({ id: doc.id, ...doc.data(), dataType }));
40
59
  }
@@ -5,7 +5,19 @@
5
5
 
6
6
  const express = require('express');
7
7
  const cors = require('cors');
8
+ const rateLimit = require('express-rate-limit');
8
9
  const createRouter = require('./routes/index.js');
10
+ const errorHandler = require('./middleware/error_handler.js');
11
+
12
+ /**
13
+ * Allowed origins for CORS
14
+ * Production: https://bulltrackers.web.app/
15
+ * Development: http://172.26.96.1:8000/
16
+ */
17
+ const allowedOrigins = [
18
+ 'https://bulltrackers.web.app',
19
+ 'http://172.26.96.1:8000'
20
+ ];
9
21
 
10
22
  /**
11
23
  * Main pipe: pipe.api.createApiV2App
@@ -15,8 +27,45 @@ function createApiV2App(config, dependencies) {
15
27
  const app = express();
16
28
  const { logger } = dependencies;
17
29
 
18
- // Middleware
19
- app.use(cors({ origin: true }));
30
+ // CORS Configuration - Restrict to specific origins
31
+ app.use(cors({
32
+ origin: function (origin, callback) {
33
+ // Allow requests with no origin (mobile apps, Postman, etc.)
34
+ if (!origin) {
35
+ return callback(null, true);
36
+ }
37
+ if (allowedOrigins.indexOf(origin) !== -1) {
38
+ callback(null, true);
39
+ } else {
40
+ callback(new Error('Not allowed by CORS'));
41
+ }
42
+ },
43
+ credentials: true
44
+ }));
45
+
46
+ // Rate Limiting
47
+ // General API limiter for most routes
48
+ const apiLimiter = rateLimit({
49
+ windowMs: 15 * 60 * 1000, // 15 minutes
50
+ max: 100, // limit each IP to 100 requests per windowMs
51
+ message: 'Too many requests from this IP, please try again later.',
52
+ standardHeaders: true,
53
+ legacyHeaders: false
54
+ });
55
+
56
+ // Stricter limiter for sensitive routes (auth/verification)
57
+ const authLimiter = rateLimit({
58
+ windowMs: 60 * 60 * 1000, // 1 hour
59
+ max: 10, // limit each IP to 10 requests per hour
60
+ message: 'Too many authentication requests from this IP, please try again later.',
61
+ standardHeaders: true,
62
+ legacyHeaders: false
63
+ });
64
+
65
+ // Apply rate limiters
66
+ app.use('/verification', authLimiter);
67
+ app.use('/', apiLimiter);
68
+
20
69
  app.use(express.json());
21
70
 
22
71
  // Health Check
@@ -28,6 +77,9 @@ function createApiV2App(config, dependencies) {
28
77
  const router = createRouter(dependencies);
29
78
  app.use('/', router);
30
79
 
80
+ // Error handling middleware (must be after all routes)
81
+ app.use(errorHandler);
82
+
31
83
  logger.log('INFO', '[API v2] Routes mounted successfully');
32
84
 
33
85
  return app;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Centralized error handling middleware
3
+ * Prevents leaking sensitive information (stack traces, internal errors) to clients
4
+ */
5
+
6
+ module.exports = (err, req, res, next) => {
7
+ // Log full error internally for debugging
8
+ console.error('[Error Handler]', {
9
+ error: err.message,
10
+ stack: err.stack,
11
+ path: req.path,
12
+ method: req.method,
13
+ timestamp: new Date().toISOString()
14
+ });
15
+
16
+ // Don't expose internal errors in production
17
+ const isProduction = process.env.NODE_ENV === 'production';
18
+ const message = isProduction
19
+ ? (err.status === 400 ? err.message : 'Internal Server Error')
20
+ : err.message;
21
+
22
+ // Determine status code
23
+ const status = err.status || err.statusCode || 500;
24
+
25
+ // Send error response
26
+ res.status(status).json({
27
+ success: false,
28
+ error: message,
29
+ ...(isProduction ? {} : { details: err.errors || undefined })
30
+ });
31
+ };
@@ -17,9 +17,15 @@
17
17
  */
18
18
  const { isDeveloper, lookupCidByEmail } = require('../helpers/data-fetchers/firestore.js');
19
19
 
20
- // List of public routes that don't require userCid
21
- // Also includes routes that use Firebase Auth token authentication (like /verification/lookup)
22
- // And routes that return static/public data (like /alerts/types)
20
+ // List of routes that don't require userCid (CID-agnostic routes)
21
+ // These routes either:
22
+ // - Return public/static data (like /popular-investors/master-list)
23
+ // - Use Firebase Auth token authentication instead of userCid (like /verification/lookup)
24
+ // - Have optional userCid for personalization but work without it (like /popular-investors/profile)
25
+ //
26
+ // NOTE: Some routes in this list (like /verification/lookup) still require Firebase Auth,
27
+ // but they don't use userCid for identity resolution. The name "PUBLIC_ROUTES" is a bit
28
+ // misleading - consider renaming to "CID_AGNOSTIC_ROUTES" for clarity.
23
29
  const PUBLIC_ROUTES = [
24
30
  '/watchlists/public',
25
31
  '/popular-investors/trending',
@@ -1,6 +1,7 @@
1
1
  const express = require('express');
2
2
  const { resolveUserIdentity } = require('../middleware/identity_middleware.js');
3
3
  const { verifyFirebaseToken } = require('../middleware/firebase_auth_middleware.js');
4
+ const { handleSyncRequest } = require('./sync.js');
4
5
 
5
6
  const notificationRoutes = require('./notifications.js');
6
7
  const alertsRoutes = require('./alerts.js'); // <--- NEW
@@ -40,8 +41,6 @@ module.exports = (dependencies) => {
40
41
  // Legacy route: /user/:userId/sync -> forwards to sync/request logic
41
42
  // This maintains backward compatibility with frontend calls
42
43
  router.post('/user/:userId/sync', async (req, res) => {
43
- // Import sync handler (need to require it here to get the exported function)
44
- const { handleSyncRequest } = require('./sync.js');
45
44
  // Set targetId from URL param for the handler
46
45
  req.body = req.body || {};
47
46
  req.body.targetId = req.params.userId;
@@ -14,13 +14,15 @@ const router = express.Router();
14
14
 
15
15
 
16
16
  // GET /popular-investors/master-list
17
- router.get('/master-list', async (req, res) => {
17
+ router.get('/master-list', async (req, res, next) => {
18
18
  try {
19
19
  const { db } = req.dependencies;
20
20
  const data = await fetchPopularInvestorMasterList(db);
21
+ // Cache in browser/CDN for 1 hour (public data that doesn't change frequently)
22
+ res.set('Cache-Control', 'public, max-age=3600');
21
23
  res.json({ success: true, count: Object.keys(data).length, data });
22
24
  } catch (error) {
23
- res.status(500).json({ error: error.message });
25
+ next(error);
24
26
  }
25
27
  });
26
28
 
@@ -33,7 +35,7 @@ router.get('/rankings', async (req, res) => {
33
35
  const rankings = await getComputationResults(db, 'PopularInvestorRankings', date);
34
36
  res.json({ success: true, data: rankings });
35
37
  } catch (error) {
36
- res.status(500).json({ error: error.message });
38
+ next(error);
37
39
  }
38
40
  });
39
41
 
@@ -73,24 +75,28 @@ router.post('/:piId/track-view', async (req, res) => {
73
75
  });
74
76
 
75
77
  // GET /popular-investors/trending
76
- router.get('/trending', async (req, res) => {
78
+ router.get('/trending', async (req, res, next) => {
77
79
  try {
78
80
  const { db } = req.dependencies;
79
81
  const data = await fetchTrendingPopularInvestors(db);
82
+ // Cache in browser/CDN for 1 hour (public data that doesn't change frequently)
83
+ res.set('Cache-Control', 'public, max-age=3600');
80
84
  res.json({ success: true, count: data.length, data });
81
85
  } catch (error) {
82
- res.status(500).json({ error: error.message });
86
+ next(error);
83
87
  }
84
88
  });
85
89
 
86
90
  // GET /popular-investors/categories
87
- router.get('/categories', async (req, res) => {
91
+ router.get('/categories', async (req, res, next) => {
88
92
  try {
89
93
  const { db } = req.dependencies;
90
94
  const data = await fetchPopularInvestorCategories(db);
95
+ // Cache in browser/CDN for 1 hour (public data that doesn't change frequently)
96
+ res.set('Cache-Control', 'public, max-age=3600');
91
97
  res.json({ success: true, data });
92
98
  } catch (error) {
93
- res.status(500).json({ error: error.message });
99
+ next(error);
94
100
  }
95
101
  });
96
102
 
@@ -104,7 +110,7 @@ router.get('/search', async (req, res) => {
104
110
  const results = await searchPopularInvestors(db, query);
105
111
  res.json({ success: true, count: results.length, data: results });
106
112
  } catch (error) {
107
- res.status(500).json({ error: error.message });
113
+ next(error);
108
114
  }
109
115
  });
110
116
 
@@ -160,7 +166,7 @@ router.get('/:piId/profile', async (req, res) => {
160
166
  profileType: 'public' // Indicates this is a public PI profile
161
167
  });
162
168
  } catch (error) {
163
- res.status(404).json({ error: error.message });
169
+ next(error);
164
170
  }
165
171
  });
166
172
 
@@ -201,7 +207,7 @@ router.get('/:piId/analytics', async (req, res) => {
201
207
  data: analyticsDoc.data()
202
208
  });
203
209
  } catch (error) {
204
- res.status(500).json({ error: error.message });
210
+ next(error);
205
211
  }
206
212
  });
207
213
 
@@ -1,21 +1,37 @@
1
1
  const express = require('express');
2
- const { manageReviews } = require('../helpers/data-fetchers/firestore.js');
2
+ const { z } = require('zod');
3
+ const { manageReviews, fetchAllReviewsForPI, checkReviewEligibility } = require('../helpers/data-fetchers/firestore.js');
3
4
 
4
5
  const router = express.Router();
5
6
 
7
+ // Input validation schemas
8
+ const reviewSubmitSchema = z.object({
9
+ piId: z.string().min(1, 'PI ID is required'),
10
+ rating: z.number().int().min(1).max(5, 'Rating must be between 1 and 5'),
11
+ comment: z.string().max(1000, 'Comment must be 1000 characters or less').optional(),
12
+ username: z.string().optional(),
13
+ isAnonymous: z.boolean().optional()
14
+ });
15
+
6
16
  // POST /reviews/submit
7
- router.post('/submit', async (req, res) => {
17
+ router.post('/submit', async (req, res, next) => {
8
18
  try {
9
- const { db } = req.dependencies;
10
- const { piId, rating, comment, username, isAnonymous } = req.body;
19
+ // Validate inputs immediately
20
+ const validatedData = reviewSubmitSchema.parse(req.body);
11
21
 
12
- const result = await manageReviews(db, req.targetUserId, 'submit', {
13
- piId, rating, comment, username, isAnonymous
14
- });
22
+ const { db } = req.dependencies;
23
+ const result = await manageReviews(db, req.targetUserId, 'submit', validatedData);
15
24
 
16
25
  res.json(result);
17
26
  } catch (error) {
18
- res.status(403).json({ error: error.message });
27
+ if (error instanceof z.ZodError) {
28
+ return res.status(400).json({
29
+ success: false,
30
+ error: 'Invalid input',
31
+ details: error.errors
32
+ });
33
+ }
34
+ next(error); // Pass to error handler middleware
19
35
  }
20
36
  });
21
37
 
@@ -40,32 +56,44 @@ router.get('/me', async (req, res) => {
40
56
  const data = snap.docs.map(d => d.data());
41
57
  res.json({ success: true, data });
42
58
  }
43
- } catch (e) { res.status(500).json({ error: e.message }); }
59
+ } catch (e) {
60
+ next(e); // Pass to error handler middleware
61
+ }
44
62
  });
45
63
 
46
64
  // GET /reviews/:piId
47
- router.get('/:piId', async (req, res) => {
65
+ router.get('/:piId', async (req, res, next) => {
48
66
  try {
49
67
  const { db } = req.dependencies;
50
68
  const { piId } = req.params;
51
- const { fetchAllReviewsForPI } = require('../helpers/data-fetchers/firestore.js');
69
+
70
+ // Validate piId
71
+ if (!piId || piId.trim().length === 0) {
72
+ return res.status(400).json({ error: 'PI ID is required' });
73
+ }
74
+
52
75
  const result = await fetchAllReviewsForPI(db, piId);
53
76
  res.json({ success: true, ...result });
54
77
  } catch (error) {
55
- res.status(500).json({ error: error.message });
78
+ next(error);
56
79
  }
57
80
  });
58
81
 
59
82
  // GET /reviews/:piId/eligibility
60
- router.get('/:piId/eligibility', async (req, res) => {
83
+ router.get('/:piId/eligibility', async (req, res, next) => {
61
84
  try {
62
85
  const { db } = req.dependencies;
63
86
  const { piId } = req.params;
64
- const { checkReviewEligibility } = require('../helpers/data-fetchers/firestore.js');
87
+
88
+ // Validate piId
89
+ if (!piId || piId.trim().length === 0) {
90
+ return res.status(400).json({ error: 'PI ID is required' });
91
+ }
92
+
65
93
  const result = await checkReviewEligibility(db, req.targetUserId, piId);
66
94
  res.json(result);
67
95
  } catch (error) {
68
- res.status(500).json({ error: error.message });
96
+ next(error);
69
97
  }
70
98
  });
71
99
 
@@ -164,13 +164,13 @@ const handleSyncRequest = async (req, res) => {
164
164
  router.post('/request', handleSyncRequest);
165
165
 
166
166
  // GET /sync/status/:targetId
167
- router.get('/status/:targetId', async (req, res) => {
167
+ router.get('/status/:targetId', async (req, res, next) => {
168
168
  try {
169
169
  const { db } = req.dependencies;
170
170
  const status = await getSyncStatus(db, req.params.targetId);
171
171
  res.json({ success: true, data: status });
172
172
  } catch (error) {
173
- res.status(500).json({ error: error.message });
173
+ next(error);
174
174
  }
175
175
  });
176
176
 
@@ -1,14 +1,29 @@
1
1
  const express = require('express');
2
+ const { z } = require('zod');
2
3
  const { initiateVerification, finalizeVerification, fetchUserVerificationData, lookupCidByEmail, createEmailLookup } = require('../helpers/data-fetchers/firestore.js');
3
4
  const { requireFirebaseAuth } = require('../middleware/firebase_auth_middleware.js');
4
5
 
5
6
  const router = express.Router();
6
7
 
8
+ // Input validation schemas
9
+ const verificationInitSchema = z.object({
10
+ username: z.string().min(1, 'Username is required').max(100, 'Username too long')
11
+ });
12
+
13
+ const verificationFinalizeSchema = z.object({
14
+ username: z.string().min(1, 'Username is required').max(100, 'Username too long')
15
+ });
16
+
17
+ const updateLookupSchema = z.object({
18
+ cid: z.string().min(1, 'CID is required'),
19
+ emails: z.array(z.string().email('Invalid email format')).min(1, 'At least one email is required')
20
+ });
21
+
7
22
  // GET /verification/lookup
8
23
  // Server-side email-to-CID lookup (prevents exposing other users' emails to client)
9
24
  // Requires Firebase Auth token in Authorization header: "Bearer <token>"
10
25
  // Email is extracted from the verified token (secure - cannot be spoofed)
11
- router.get('/lookup', requireFirebaseAuth, async (req, res) => {
26
+ router.get('/lookup', requireFirebaseAuth, async (req, res, next) => {
12
27
  try {
13
28
  const { db } = req.dependencies;
14
29
 
@@ -34,7 +49,7 @@ router.get('/lookup', requireFirebaseAuth, async (req, res) => {
34
49
  }
35
50
  } catch (error) {
36
51
  console.error('[Verification Lookup] Error:', error);
37
- res.status(500).json({ success: false, error: error.message });
52
+ next(error);
38
53
  }
39
54
  });
40
55
 
@@ -45,37 +60,53 @@ router.get('/status', async (req, res) => {
45
60
  const data = await fetchUserVerificationData(db, req.targetUserId);
46
61
  res.json({ success: true, data });
47
62
  } catch (error) {
63
+ // Status endpoint returns 200 even on error (user might not be verified)
48
64
  res.status(200).json({ success: false, message: "Not Verified", error: error.message });
49
65
  }
50
66
  });
51
67
 
52
68
  // POST /verification/init
53
69
  // Generates OTP for a specific username
54
- router.post('/init', async (req, res) => {
70
+ router.post('/init', async (req, res, next) => {
55
71
  try {
72
+ // Validate inputs immediately
73
+ const validatedData = verificationInitSchema.parse(req.body);
74
+
56
75
  const { db } = req.dependencies;
57
- const { username } = req.body; // Logic requires username to key the request
58
- const result = await initiateVerification(db, username);
76
+ const result = await initiateVerification(db, validatedData.username);
59
77
  res.json(result);
60
78
  } catch (error) {
61
- res.status(500).json({ error: error.message });
79
+ if (error instanceof z.ZodError) {
80
+ return res.status(400).json({
81
+ success: false,
82
+ error: 'Invalid input',
83
+ details: error.errors
84
+ });
85
+ }
86
+ next(error);
62
87
  }
63
88
  });
64
89
 
65
90
  // POST /verification/finalize
66
91
  // Checks Bio -> Verifies User -> Triggers Sync
67
- router.post('/finalize', async (req, res) => {
92
+ router.post('/finalize', async (req, res, next) => {
68
93
  try {
94
+ // Validate inputs immediately
95
+ const validatedData = verificationFinalizeSchema.parse(req.body);
96
+
69
97
  const { db, pubsub } = req.dependencies;
70
- const { username } = req.body;
71
-
72
- if (!username) return res.status(400).json({ error: "Username required" });
73
-
74
98
  // Pass req.targetUserId if available, but the logic primarily uses username/realCID
75
- const result = await finalizeVerification(db, pubsub, req.targetUserId, username);
99
+ const result = await finalizeVerification(db, pubsub, req.targetUserId, validatedData.username);
76
100
  res.json(result);
77
101
  } catch (error) {
78
- res.status(400).json({ success: false, error: error.message });
102
+ if (error instanceof z.ZodError) {
103
+ return res.status(400).json({
104
+ success: false,
105
+ error: 'Invalid input',
106
+ details: error.errors
107
+ });
108
+ }
109
+ next(error);
79
110
  }
80
111
  });
81
112
 
@@ -83,23 +114,18 @@ router.post('/finalize', async (req, res) => {
83
114
  // Updates email-to-CID lookup documents when emails are added to verification
84
115
  // Called by frontend after updating verification document with new emails
85
116
  // This ensures O(1) lookups work immediately without waiting for migration
86
- router.post('/update-lookup', requireFirebaseAuth, async (req, res) => {
117
+ router.post('/update-lookup', requireFirebaseAuth, async (req, res, next) => {
87
118
  try {
88
- const { db } = req.dependencies;
89
- const { cid, emails } = req.body;
119
+ // Validate inputs immediately
120
+ const validatedData = updateLookupSchema.parse(req.body);
90
121
 
91
- if (!cid || !Array.isArray(emails) || emails.length === 0) {
92
- return res.status(400).json({
93
- success: false,
94
- error: 'CID and emails array are required'
95
- });
96
- }
122
+ const { db } = req.dependencies;
97
123
 
98
124
  // Verify the authenticated user owns this CID
99
125
  const userEmail = req.firebaseUser?.email;
100
126
  if (userEmail) {
101
127
  const userData = await lookupCidByEmail(db, userEmail);
102
- if (!userData || String(userData.cid) !== String(cid)) {
128
+ if (!userData || String(userData.cid) !== String(validatedData.cid)) {
103
129
  return res.status(403).json({
104
130
  success: false,
105
131
  error: 'Unauthorized: You can only update lookup for your own CID'
@@ -109,10 +135,10 @@ router.post('/update-lookup', requireFirebaseAuth, async (req, res) => {
109
135
 
110
136
  // Create lookup documents for all emails
111
137
  const results = [];
112
- for (const email of emails) {
138
+ for (const email of validatedData.emails) {
113
139
  if (email) {
114
140
  try {
115
- await createEmailLookup(db, email, cid);
141
+ await createEmailLookup(db, email, validatedData.cid);
116
142
  results.push({ email, success: true });
117
143
  } catch (error) {
118
144
  console.error(`[update-lookup] Failed to create lookup for ${email}:`, error);
@@ -127,8 +153,15 @@ router.post('/update-lookup', requireFirebaseAuth, async (req, res) => {
127
153
  results
128
154
  });
129
155
  } catch (error) {
156
+ if (error instanceof z.ZodError) {
157
+ return res.status(400).json({
158
+ success: false,
159
+ error: 'Invalid input',
160
+ details: error.errors
161
+ });
162
+ }
130
163
  console.error('[Verification Update Lookup] Error:', error);
131
- res.status(500).json({ success: false, error: error.message });
164
+ next(error);
132
165
  }
133
166
  });
134
167
 
@@ -21,17 +21,17 @@ router.get('/all', async (req, res) => {
21
21
  const watchlists = await latestUserCentricSnapshot(db, req.targetUserId, 'watchlists', 'watchlist', 'SignedInUsers', null);
22
22
  res.json({ success: true, count: watchlists.length, data: watchlists });
23
23
  } catch (error) {
24
- res.status(500).json({ error: error.message });
24
+ next(error);
25
25
  }
26
26
  });
27
27
 
28
28
  // POST /watchlists/auto-generate (Rec 11)
29
- router.post('/auto-generate', async (req, res) => {
29
+ router.post('/auto-generate', async (req, res, next) => {
30
30
  try {
31
31
  const result = await autoGenerateWatchlist(req.dependencies.db, req.targetUserId);
32
32
  res.json(result);
33
33
  } catch (error) {
34
- res.status(500).json({ error: error.message });
34
+ next(error);
35
35
  }
36
36
  });
37
37
 
@@ -40,11 +40,11 @@ router.get('/public', async (req, res) => {
40
40
  try {
41
41
  const data = await fetchPublicWatchlists(req.dependencies.db, req.query.limit, req.query.offset);
42
42
  res.json({ success: true, data });
43
- } catch (e) { res.status(500).json({ error: e.message }); }
43
+ } catch (e) { next(e); }
44
44
  });
45
45
 
46
46
  // GET /watchlists/:id (Rec 12)
47
- router.get('/:id', async (req, res) => {
47
+ router.get('/:id', async (req, res, next) => {
48
48
  try {
49
49
  const { db } = req.dependencies;
50
50
  const { id } = req.params;
@@ -52,7 +52,7 @@ router.get('/:id', async (req, res) => {
52
52
  if (!doc) return res.status(404).json({ error: "Watchlist not found" });
53
53
  res.json({ success: true, data: doc });
54
54
  } catch (error) {
55
- res.status(500).json({ error: error.message });
55
+ next(error);
56
56
  }
57
57
  });
58
58
 
@@ -77,42 +77,42 @@ router.get('/:id/rankings-check', async (req, res) => {
77
77
  }
78
78
  res.json({ success: true, data: results });
79
79
  } catch (error) {
80
- res.status(500).json({ error: error.message });
80
+ next(error);
81
81
  }
82
82
  });
83
83
 
84
84
  // POST /watchlists/manage (Unified)
85
- router.post('/manage', async (req, res) => {
85
+ router.post('/manage', async (req, res, next) => {
86
86
  try {
87
87
  const { db } = req.dependencies;
88
88
  const { instruction, payload } = req.body;
89
89
  const result = await manageUserWatchlist(db, req.targetUserId, instruction, payload);
90
90
  res.json(result);
91
91
  } catch (error) {
92
- res.status(500).json({ error: error.message });
92
+ next(error);
93
93
  }
94
94
  });
95
95
 
96
96
  // Public & Copy Routes (Existing)
97
- router.post('/:id/publish', async (req, res) => {
97
+ router.post('/:id/publish', async (req, res, next) => {
98
98
  try {
99
99
  const result = await publishWatchlistVersion(req.dependencies.db, req.targetUserId, req.params.id);
100
100
  res.json(result);
101
- } catch (e) { res.status(500).json({ error: e.message }); }
101
+ } catch (e) { next(e); }
102
102
  });
103
103
 
104
104
  router.post('/:id/copy', async (req, res) => {
105
105
  try {
106
106
  const result = await copyWatchlist(req.dependencies.db, req.targetUserId, req.params.id, req.body.version);
107
107
  res.json(result);
108
- } catch (e) { res.status(500).json({ error: e.message }); }
108
+ } catch (e) { next(e); }
109
109
  });
110
110
 
111
- router.get('/:id/versions', async (req, res) => {
111
+ router.get('/:id/versions', async (req, res, next) => {
112
112
  try {
113
113
  const data = await fetchWatchlistVersions(req.dependencies.db, req.params.id);
114
114
  res.json({ success: true, data });
115
- } catch (e) { res.status(500).json({ error: e.message }); }
115
+ } catch (e) { next(e); }
116
116
  });
117
117
 
118
118
  // GET /watchlists/:id/trigger-counts - Get alert trigger counts for watchlist PIs
@@ -124,14 +124,14 @@ router.get('/:id/trigger-counts', async (req, res) => {
124
124
  res.json({ success: true, ...result });
125
125
  } catch (error) {
126
126
  if (error.message === "Watchlist not found") {
127
- return res.status(404).json({ error: error.message });
127
+ error.status = 404;
128
128
  }
129
129
  res.status(500).json({ error: error.message });
130
130
  }
131
131
  });
132
132
 
133
133
  // POST /watchlists/:id/subscribe-all - Subscribe to all PIs in a watchlist
134
- router.post('/:id/subscribe-all', async (req, res) => {
134
+ router.post('/:id/subscribe-all', async (req, res, next) => {
135
135
  try {
136
136
  const { db } = req.dependencies;
137
137
  const { id } = req.params;
@@ -140,7 +140,7 @@ router.post('/:id/subscribe-all', async (req, res) => {
140
140
  res.json(result);
141
141
  } catch (error) {
142
142
  if (error.message === "Watchlist not found") {
143
- return res.status(404).json({ error: error.message });
143
+ error.status = 404;
144
144
  }
145
145
  res.status(500).json({ error: error.message });
146
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.620",
3
+ "version": "1.0.621",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -33,18 +33,20 @@
33
33
  ],
34
34
  "dependencies": {
35
35
  "@google-cloud/firestore": "^7.11.3",
36
+ "@google-cloud/monitoring": "latest",
36
37
  "@google-cloud/pubsub": "latest",
37
38
  "aiden-shared-calculations-unified": "^1.0.110",
38
39
  "cors": "^2.8.5",
39
40
  "dotenv": "latest",
40
41
  "express": "^4.19.2",
42
+ "express-rate-limit": "^8.2.1",
41
43
  "google-auth-library": "^10.5.0",
42
44
  "graphviz": "latest",
43
45
  "node-graphviz": "^0.1.1",
44
46
  "p-limit": "^3.1.0",
45
47
  "require-all": "^3.0.0",
46
48
  "sharedsetup": "latest",
47
- "@google-cloud/monitoring": "latest"
49
+ "zod": "^4.3.5"
48
50
  },
49
51
  "devDependencies": {
50
52
  "bulltracker-deployer": "file:../bulltracker-deployer"