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.
- package/functions/api-v2/helpers/data-fetchers/firestore.js +22 -3
- package/functions/api-v2/index.js +54 -2
- package/functions/api-v2/middleware/error_handler.js +31 -0
- package/functions/api-v2/middleware/identity_middleware.js +9 -3
- package/functions/api-v2/routes/index.js +1 -2
- package/functions/api-v2/routes/popular_investors.js +16 -10
- package/functions/api-v2/routes/reviews.js +43 -15
- package/functions/api-v2/routes/sync.js +2 -2
- package/functions/api-v2/routes/verification.js +59 -26
- package/functions/api-v2/routes/watchlists.js +17 -17
- package/package.json +4 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
19
|
-
app.use(cors({
|
|
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
|
|
21
|
-
//
|
|
22
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
+
next(error);
|
|
205
211
|
}
|
|
206
212
|
});
|
|
207
213
|
|
|
@@ -1,21 +1,37 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
|
-
const {
|
|
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
|
-
|
|
10
|
-
const
|
|
19
|
+
// Validate inputs immediately
|
|
20
|
+
const validatedData = reviewSubmitSchema.parse(req.body);
|
|
11
21
|
|
|
12
|
-
const
|
|
13
|
-
|
|
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
|
-
|
|
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) {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
const
|
|
119
|
+
// Validate inputs immediately
|
|
120
|
+
const validatedData = updateLookupSchema.parse(req.body);
|
|
90
121
|
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
49
|
+
"zod": "^4.3.5"
|
|
48
50
|
},
|
|
49
51
|
"devDependencies": {
|
|
50
52
|
"bulltracker-deployer": "file:../bulltracker-deployer"
|