bulltrackers-module 1.0.596 → 1.0.598

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.
@@ -510,23 +510,6 @@ const fetchUserVerificationData = async (firestore, userId) => {
510
510
  const docRef = firestore.collection('SignedInUsers').doc(userId).collection('verification').doc('data');
511
511
  const docSnapshot = await docRef.get();
512
512
  if (!docSnapshot.exists) {
513
- // Attempt migration from old location
514
- const oldCollectionRef = firestore.collection('signedInUsers');
515
- const oldSnapshot = await oldCollectionRef.get();
516
- for (const doc of oldSnapshot.docs) {
517
- const data = doc.data();
518
- if (data.etoroCID && String(data.etoroCID) === String(userId)) {
519
- // Found old data, migrate it
520
- await docRef.set({
521
- ...data,
522
- migratedFromId: doc.id // Store old firestore ID
523
- });
524
- // Delete old document
525
- await oldCollectionRef.doc(doc.id).delete();
526
- return { id: doc.id, ...data, migratedFromId: doc.id };
527
- }
528
- }
529
- // No data found
530
513
  throw new Error(`Verification data for User ID ${userId} not found`);
531
514
  }
532
515
  return { id: docSnapshot.id, ...docSnapshot.data() };
@@ -536,6 +519,59 @@ const fetchUserVerificationData = async (firestore, userId) => {
536
519
  }
537
520
  };
538
521
 
522
+ /**
523
+ * Lookup CID by email (server-side only for security)
524
+ * Searches through all SignedInUsers verification documents to find matching email
525
+ * Returns only the CID and basic verification status - no email exposure
526
+ */
527
+ const lookupCidByEmail = async (firestore, userEmail) => {
528
+ if (!userEmail) {
529
+ throw new Error('Email is required for lookup');
530
+ }
531
+
532
+ try {
533
+ // Normalize email for comparison
534
+ const normalizedEmail = String(userEmail).toLowerCase().trim();
535
+
536
+ // Get all SignedInUsers documents
537
+ const signedInUsersSnapshot = await firestore.collection('SignedInUsers').get();
538
+
539
+ // Search through each CID's verification document
540
+ for (const userDoc of signedInUsersSnapshot.docs) {
541
+ const verificationRef = userDoc.ref.collection('verification').doc('data');
542
+ const verificationDoc = await verificationRef.get();
543
+
544
+ if (verificationDoc.exists()) {
545
+ const verificationData = verificationDoc.data();
546
+ const emails = Array.isArray(verificationData.email)
547
+ ? verificationData.email
548
+ : (verificationData.email ? [verificationData.email] : []);
549
+
550
+ // Case-insensitive email comparison
551
+ const normalizedEmails = emails.map(e => String(e).toLowerCase().trim());
552
+ if (normalizedEmails.includes(normalizedEmail)) {
553
+ // Found match - return only CID and verification status (no email)
554
+ return {
555
+ cid: verificationData.etoroCID || Number(userDoc.id),
556
+ accountSetupComplete: verificationData.accountSetupComplete || false,
557
+ etoroUsername: verificationData.etoroUsername,
558
+ displayName: verificationData.displayName,
559
+ photoURL: verificationData.photoURL,
560
+ verifiedAt: verificationData.verifiedAt,
561
+ setupCompletedAt: verificationData.setupCompletedAt,
562
+ };
563
+ }
564
+ }
565
+ }
566
+
567
+ // No match found
568
+ return null;
569
+ } catch (error) {
570
+ console.error(`Error looking up CID by email: ${error}`);
571
+ throw error;
572
+ }
573
+ };
574
+
539
575
 
540
576
  // 7. PI addition requests fetcher
541
577
  // This is used to allow the user to request new PIs to add to the popular investor list to process data each day.
@@ -1517,19 +1553,7 @@ const finalizeVerification = async (db, pubsub, userId, username) => {
1517
1553
  isOptOut
1518
1554
  });
1519
1555
 
1520
- // 2. Create/Update User Doc in SignedInUsers (legacy location - for backward compatibility)
1521
- // Note: We use the realCID as the document ID, matching the generic-api logic
1522
- await db.collection(signedInUsersCollection).doc(String(realCID)).set({
1523
- username: profileData.username,
1524
- cid: realCID,
1525
- fullName: `${profileData.firstName || ''} ${profileData.lastName || ''}`.trim(),
1526
- avatar: profileData.avatars?.find(a => a.type === 'Original')?.url || null,
1527
- isOptOut,
1528
- verifiedAt: FieldValue.serverTimestamp(),
1529
- lastLogin: FieldValue.serverTimestamp()
1530
- }, { merge: true });
1531
-
1532
- // 3. Create/Update verification data in new format location: /SignedInUsers/{cid}/verification/data
1556
+ // 2. Create/Update verification data in new format location: /SignedInUsers/{cid}/verification/data
1533
1557
  const verificationDataRef = db.collection('SignedInUsers').doc(String(realCID)).collection('verification').doc('data');
1534
1558
  const existingVerificationDoc = await verificationDataRef.get();
1535
1559
 
@@ -2273,6 +2297,7 @@ module.exports = {
2273
2297
  pageCollection,
2274
2298
  fetchPopularInvestorMasterList,
2275
2299
  isDeveloper,
2300
+ lookupCidByEmail,
2276
2301
  manageUserWatchlist,
2277
2302
  fetchUserVerificationData,
2278
2303
  requestPopularInvestorAddition,
@@ -0,0 +1,141 @@
1
+ # Firebase Auth Token Verification - How It Works
2
+
3
+ ## Overview
4
+
5
+ This middleware verifies Firebase Authentication ID tokens server-side to securely extract user information (like email) without relying on client-provided data.
6
+
7
+ ## Why This Is Important
8
+
9
+ **Without token verification:**
10
+ - Client sends email as query parameter: `GET /verification/lookup?email=user@example.com`
11
+ - Anyone could call this with any email address
12
+ - No way to verify the email actually belongs to the authenticated user
13
+
14
+ **With token verification:**
15
+ - Client sends Firebase ID token: `Authorization: Bearer <token>`
16
+ - Server verifies the token is valid and not expired
17
+ - Server extracts email from the verified token (cannot be spoofed)
18
+ - Only the authenticated user's email can be used
19
+
20
+ ## How It Works
21
+
22
+ ### 1. Frontend (Client-Side)
23
+
24
+ ```typescript
25
+ // User signs in with Firebase Auth
26
+ const user = await signInWithPopup(auth, googleProvider);
27
+
28
+ // Get the ID token (this is a JWT signed by Firebase)
29
+ const idToken = await user.getIdToken();
30
+
31
+ // Send token in Authorization header
32
+ fetch('/verification/lookup', {
33
+ headers: {
34
+ 'Authorization': `Bearer ${idToken}`
35
+ }
36
+ });
37
+ ```
38
+
39
+ ### 2. Backend (Server-Side)
40
+
41
+ ```javascript
42
+ // Middleware verifies the token
43
+ const decodedToken = await admin.auth().verifyIdToken(token);
44
+
45
+ // Extract email from verified token
46
+ const email = decodedToken.email; // This is guaranteed to be correct
47
+
48
+ // Use email for lookup
49
+ const result = await lookupCidByEmail(db, email);
50
+ ```
51
+
52
+ ## Token Structure
53
+
54
+ Firebase ID tokens are JWTs (JSON Web Tokens) that contain:
55
+ - `uid`: User's Firebase UID
56
+ - `email`: User's email address
57
+ - `email_verified`: Whether email is verified
58
+ - `name`: User's display name
59
+ - `picture`: User's profile picture URL
60
+ - `exp`: Expiration timestamp
61
+ - `iat`: Issued at timestamp
62
+
63
+ The token is **cryptographically signed by Firebase**, so:
64
+ - ✅ Cannot be forged
65
+ - ✅ Cannot be modified
66
+ - ✅ Expires automatically (1 hour default)
67
+ - ✅ Can be verified server-side
68
+
69
+ ## Security Benefits
70
+
71
+ 1. **Prevents Email Spoofing**: Users cannot lookup other users' data by providing a different email
72
+ 2. **Automatic Expiration**: Tokens expire after 1 hour, requiring re-authentication
73
+ 3. **Cryptographic Verification**: Token signature is verified against Firebase's public keys
74
+ 4. **No Client Trust**: Server doesn't trust client-provided data, only verified tokens
75
+
76
+ ## Usage
77
+
78
+ ### In Routes
79
+
80
+ ```javascript
81
+ const { requireFirebaseAuth } = require('../middleware/firebase_auth_middleware.js');
82
+
83
+ // Require authentication
84
+ router.get('/lookup', requireFirebaseAuth, async (req, res) => {
85
+ // req.firebaseUser.email is guaranteed to be from verified token
86
+ const email = req.firebaseUser.email;
87
+ // ... use email securely
88
+ });
89
+ ```
90
+
91
+ ### Optional Auth (for public routes)
92
+
93
+ ```javascript
94
+ const { verifyFirebaseToken } = require('../middleware/firebase_auth_middleware.js');
95
+
96
+ // Optional authentication
97
+ router.get('/public', verifyFirebaseToken, async (req, res) => {
98
+ if (req.firebaseUser) {
99
+ // User is authenticated
100
+ } else {
101
+ // Public access
102
+ }
103
+ });
104
+ ```
105
+
106
+ ## Firebase Admin SDK Setup
107
+
108
+ The middleware automatically initializes Firebase Admin using:
109
+ 1. **Cloud Functions**: Uses the function's service account automatically
110
+ 2. **Local Development**: Uses `GOOGLE_APPLICATION_CREDENTIALS` environment variable
111
+ 3. **Default Credentials**: Falls back to Application Default Credentials (ADC)
112
+
113
+ No manual configuration needed in Cloud Functions - it "just works"!
114
+
115
+ ## Error Handling
116
+
117
+ - **Missing Token**: `req.firebaseUser` is `null`, request continues (routes can check)
118
+ - **Invalid Token**: `req.firebaseUser` is `null`, request continues
119
+ - **Expired Token**: `req.firebaseUser` is `null`, request continues
120
+ - **Required Auth**: `requireFirebaseAuth` returns 401 if token is invalid/missing
121
+
122
+ ## Token Refresh
123
+
124
+ Firebase tokens expire after 1 hour. The frontend automatically refreshes tokens:
125
+ - Firebase SDK handles token refresh automatically
126
+ - `getIdToken()` always returns a valid token (refreshes if needed)
127
+ - No manual refresh logic needed
128
+
129
+ ## Example Flow
130
+
131
+ ```
132
+ 1. User signs in → Firebase Auth creates session
133
+ 2. Frontend calls getIdToken() → Gets current ID token
134
+ 3. Frontend sends token in Authorization header
135
+ 4. Backend middleware verifies token with Firebase Admin
136
+ 5. Backend extracts email from verified token
137
+ 6. Backend uses email for secure lookup
138
+ 7. Backend returns result (no email exposure)
139
+ ```
140
+
141
+ This ensures the email used for lookup is **always** the authenticated user's email, with cryptographic proof.
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @fileoverview Firebase Auth Token Verification Middleware
3
+ * Verifies Firebase ID tokens and extracts user information
4
+ */
5
+
6
+ // Firebase Admin SDK is used server-side to verify tokens
7
+ // Note: This requires firebase-admin to be installed
8
+ let admin = null;
9
+ try {
10
+ admin = require('firebase-admin');
11
+ } catch (e) {
12
+ console.warn('[FirebaseAuth] firebase-admin not available. Token verification will be disabled.');
13
+ }
14
+
15
+ /**
16
+ * Initialize Firebase Admin if not already initialized
17
+ */
18
+ function initializeFirebaseAdmin() {
19
+ if (!admin) {
20
+ return null;
21
+ }
22
+
23
+ try {
24
+ // Check if already initialized
25
+ if (admin.apps.length > 0) {
26
+ return admin.app();
27
+ }
28
+
29
+ // Initialize with default credentials (uses GOOGLE_APPLICATION_CREDENTIALS or default service account)
30
+ // In Cloud Functions, this automatically uses the function's service account
31
+ admin.initializeApp({
32
+ // Credentials are automatically loaded from environment
33
+ projectId: process.env.GCP_PROJECT_ID || 'stocks-12345'
34
+ });
35
+
36
+ return admin.app();
37
+ } catch (error) {
38
+ // If already initialized, that's fine
39
+ if (error.code === 'app/already-initialized') {
40
+ return admin.app();
41
+ }
42
+ console.error('[FirebaseAuth] Error initializing Firebase Admin:', error);
43
+ return null;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Middleware to verify Firebase Auth ID token
49
+ * Extracts email and UID from verified token and attaches to req.firebaseUser
50
+ *
51
+ * Usage:
52
+ * router.use('/protected-route', verifyFirebaseToken);
53
+ * // Then access req.firebaseUser.email and req.firebaseUser.uid
54
+ */
55
+ const verifyFirebaseToken = async (req, res, next) => {
56
+ // Skip if Firebase Admin is not available
57
+ if (!admin) {
58
+ return next();
59
+ }
60
+
61
+ try {
62
+ // Get token from Authorization header
63
+ // Format: "Bearer <token>" or just "<token>"
64
+ const authHeader = req.headers.authorization;
65
+
66
+ if (!authHeader) {
67
+ // No token provided - continue without setting req.firebaseUser
68
+ // Routes can check for req.firebaseUser to determine if auth is required
69
+ return next();
70
+ }
71
+
72
+ // Extract token (handle both "Bearer <token>" and just "<token>" formats)
73
+ const token = authHeader.startsWith('Bearer ')
74
+ ? authHeader.substring(7)
75
+ : authHeader;
76
+
77
+ if (!token || token === 'null' || token === 'undefined') {
78
+ return next();
79
+ }
80
+
81
+ // Initialize Firebase Admin if needed
82
+ const app = initializeFirebaseAdmin();
83
+ if (!app) {
84
+ console.warn('[FirebaseAuth] Cannot verify token - Firebase Admin not initialized');
85
+ return next();
86
+ }
87
+
88
+ // Verify the token
89
+ const decodedToken = await admin.auth().verifyIdToken(token);
90
+
91
+ // Attach user info to request
92
+ req.firebaseUser = {
93
+ uid: decodedToken.uid,
94
+ email: decodedToken.email,
95
+ emailVerified: decodedToken.email_verified || false,
96
+ name: decodedToken.name,
97
+ picture: decodedToken.picture,
98
+ };
99
+
100
+ next();
101
+ } catch (error) {
102
+ // Token verification failed
103
+ // Don't block the request - let individual routes decide if auth is required
104
+ console.warn('[FirebaseAuth] Token verification failed:', error.message);
105
+ req.firebaseUser = null;
106
+ next();
107
+ }
108
+ };
109
+
110
+ /**
111
+ * Middleware that REQUIRES authentication
112
+ * Returns 401 if token is invalid or missing
113
+ */
114
+ const requireFirebaseAuth = async (req, res, next) => {
115
+ // First verify the token
116
+ await verifyFirebaseToken(req, res, () => {
117
+ // Check if user was authenticated
118
+ if (!req.firebaseUser || !req.firebaseUser.email) {
119
+ return res.status(401).json({
120
+ success: false,
121
+ error: 'Authentication required. Please provide a valid Firebase ID token.'
122
+ });
123
+ }
124
+ next();
125
+ });
126
+ };
127
+
128
+ module.exports = {
129
+ verifyFirebaseToken,
130
+ requireFirebaseAuth,
131
+ initializeFirebaseAdmin,
132
+ };
@@ -1,8 +1,43 @@
1
1
  const express = require('express');
2
- const { initiateVerification, finalizeVerification, fetchUserVerificationData } = require('../helpers/data-fetchers/firestore.js');
2
+ const { initiateVerification, finalizeVerification, fetchUserVerificationData, lookupCidByEmail } = require('../helpers/data-fetchers/firestore.js');
3
+ const { requireFirebaseAuth } = require('../middleware/firebase_auth_middleware.js');
3
4
 
4
5
  const router = express.Router();
5
6
 
7
+ // GET /verification/lookup
8
+ // Server-side email-to-CID lookup (prevents exposing other users' emails to client)
9
+ // Requires Firebase Auth token in Authorization header: "Bearer <token>"
10
+ // Email is extracted from the verified token (secure - cannot be spoofed)
11
+ router.get('/lookup', requireFirebaseAuth, async (req, res) => {
12
+ try {
13
+ const { db } = req.dependencies;
14
+
15
+ // Get email from verified Firebase token (secure - cannot be spoofed)
16
+ const userEmail = req.firebaseUser?.email;
17
+
18
+ if (!userEmail) {
19
+ return res.status(400).json({
20
+ success: false,
21
+ error: 'Email not found in authentication token'
22
+ });
23
+ }
24
+
25
+ const result = await lookupCidByEmail(db, userEmail);
26
+
27
+ if (result) {
28
+ res.json({ success: true, data: result });
29
+ } else {
30
+ res.status(200).json({
31
+ success: false,
32
+ message: 'No verification data found for this email'
33
+ });
34
+ }
35
+ } catch (error) {
36
+ console.error('[Verification Lookup] Error:', error);
37
+ res.status(500).json({ success: false, error: error.message });
38
+ }
39
+ });
40
+
6
41
  // GET /verification/status
7
42
  router.get('/status', async (req, res) => {
8
43
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.596",
3
+ "version": "1.0.598",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [