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
|
|
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 {
|