bulltrackers-module 1.0.560 → 1.0.562
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.
|
@@ -628,6 +628,267 @@ const createAdminRouter = (config, dependencies, unifiedCalculations) => {
|
|
|
628
628
|
}
|
|
629
629
|
});
|
|
630
630
|
|
|
631
|
+
// --- 10. DEVELOPER USER MANAGEMENT (Force Sync & Lookup) ---
|
|
632
|
+
// Only accessible to developer accounts
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* POST /admin/users/:cid/sync
|
|
636
|
+
* Force sync a signed-in user (developer only)
|
|
637
|
+
* Allows developers to trigger sync for any user to fix stuck accounts
|
|
638
|
+
*/
|
|
639
|
+
router.post('/users/:cid/sync', async (req, res) => {
|
|
640
|
+
const { cid } = req.params;
|
|
641
|
+
const { userCid: developerCid } = req.query; // Developer's CID
|
|
642
|
+
|
|
643
|
+
if (!developerCid) {
|
|
644
|
+
return res.status(400).json({
|
|
645
|
+
success: false,
|
|
646
|
+
error: "Missing userCid",
|
|
647
|
+
message: "Please provide userCid query parameter (your developer CID)"
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// SECURITY: Only allow developer accounts
|
|
652
|
+
const { isDeveloperAccount } = require('../user-api/helpers/dev/dev_helpers');
|
|
653
|
+
if (!isDeveloperAccount(Number(developerCid))) {
|
|
654
|
+
logger.log('WARN', `[AdminAPI] Unauthorized sync attempt by non-developer ${developerCid}`);
|
|
655
|
+
return res.status(403).json({
|
|
656
|
+
success: false,
|
|
657
|
+
error: "Forbidden",
|
|
658
|
+
message: "This endpoint is only available for developer accounts"
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const targetCidNum = Number(cid);
|
|
663
|
+
if (isNaN(targetCidNum) || targetCidNum <= 0) {
|
|
664
|
+
return res.status(400).json({
|
|
665
|
+
success: false,
|
|
666
|
+
error: "Invalid CID",
|
|
667
|
+
message: "Please provide a valid user CID"
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
try {
|
|
672
|
+
// Import sync helper
|
|
673
|
+
const { requestUserSync } = require('../user-api/helpers/sync/user_sync_helpers');
|
|
674
|
+
|
|
675
|
+
// Create a mock request object with the target CID
|
|
676
|
+
const mockReq = {
|
|
677
|
+
params: { userCid: String(targetCidNum) },
|
|
678
|
+
query: { userCid: String(developerCid), sourcePage: 'admin' },
|
|
679
|
+
headers: {},
|
|
680
|
+
body: {}
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// Call the sync function
|
|
684
|
+
await requestUserSync(mockReq, res, dependencies, config);
|
|
685
|
+
|
|
686
|
+
logger.log('INFO', `[AdminAPI] Developer ${developerCid} force-synced user ${targetCidNum}`);
|
|
687
|
+
|
|
688
|
+
} catch (error) {
|
|
689
|
+
logger.log('ERROR', `[AdminAPI] Error force-syncing user ${targetCidNum}`, error);
|
|
690
|
+
return res.status(500).json({
|
|
691
|
+
success: false,
|
|
692
|
+
error: "Internal server error",
|
|
693
|
+
message: error.message
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* GET /admin/users/:cid/lookup
|
|
700
|
+
* Look up user information by CID (developer only)
|
|
701
|
+
* Helps developers find users to sync
|
|
702
|
+
*/
|
|
703
|
+
router.get('/users/:cid/lookup', async (req, res) => {
|
|
704
|
+
const { cid } = req.params;
|
|
705
|
+
const { userCid: developerCid } = req.query; // Developer's CID
|
|
706
|
+
|
|
707
|
+
if (!developerCid) {
|
|
708
|
+
return res.status(400).json({
|
|
709
|
+
success: false,
|
|
710
|
+
error: "Missing userCid",
|
|
711
|
+
message: "Please provide userCid query parameter (your developer CID)"
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// SECURITY: Only allow developer accounts
|
|
716
|
+
const { isDeveloperAccount } = require('../user-api/helpers/dev/dev_helpers');
|
|
717
|
+
if (!isDeveloperAccount(Number(developerCid))) {
|
|
718
|
+
logger.log('WARN', `[AdminAPI] Unauthorized lookup attempt by non-developer ${developerCid}`);
|
|
719
|
+
return res.status(403).json({
|
|
720
|
+
success: false,
|
|
721
|
+
error: "Forbidden",
|
|
722
|
+
message: "This endpoint is only available for developer accounts"
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const targetCidNum = Number(cid);
|
|
727
|
+
if (isNaN(targetCidNum) || targetCidNum <= 0) {
|
|
728
|
+
return res.status(400).json({
|
|
729
|
+
success: false,
|
|
730
|
+
error: "Invalid CID",
|
|
731
|
+
message: "Please provide a valid user CID"
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
try {
|
|
736
|
+
const signedInUsersCollection = config.signedInUsersCollection || 'signed_in_users';
|
|
737
|
+
const { checkIfUserIsPI } = require('../user-api/helpers/core/user_status_helpers');
|
|
738
|
+
|
|
739
|
+
// Check if user is a PI
|
|
740
|
+
const rankEntry = await checkIfUserIsPI(db, targetCidNum, config, logger);
|
|
741
|
+
const isPI = !!rankEntry;
|
|
742
|
+
|
|
743
|
+
// Get signed-in user data
|
|
744
|
+
const userDoc = await db.collection(signedInUsersCollection).doc(String(targetCidNum)).get();
|
|
745
|
+
const userData = userDoc.exists ? userDoc.data() : null;
|
|
746
|
+
|
|
747
|
+
// Get verification data
|
|
748
|
+
const verificationsCollection = config.verificationsCollection || 'verifications';
|
|
749
|
+
let verificationData = null;
|
|
750
|
+
if (userData && userData.username) {
|
|
751
|
+
const verificationDoc = await db.collection(verificationsCollection)
|
|
752
|
+
.doc(userData.username.toLowerCase())
|
|
753
|
+
.get();
|
|
754
|
+
if (verificationDoc.exists) {
|
|
755
|
+
verificationData = verificationDoc.data();
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Get latest sync request status
|
|
760
|
+
const syncRequestsRef = db.collection('user_sync_requests')
|
|
761
|
+
.doc(String(targetCidNum))
|
|
762
|
+
.collection('requests')
|
|
763
|
+
.orderBy('createdAt', 'desc')
|
|
764
|
+
.limit(1);
|
|
765
|
+
|
|
766
|
+
const latestSyncSnapshot = await syncRequestsRef.get();
|
|
767
|
+
let latestSync = null;
|
|
768
|
+
if (!latestSyncSnapshot.empty) {
|
|
769
|
+
latestSync = {
|
|
770
|
+
requestId: latestSyncSnapshot.docs[0].id,
|
|
771
|
+
...latestSyncSnapshot.docs[0].data()
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Get data status directly (don't use the endpoint wrapper)
|
|
776
|
+
let dataStatus = null;
|
|
777
|
+
try {
|
|
778
|
+
const { signedInUsersCollection, signedInHistoryCollection } = config;
|
|
779
|
+
const { findLatestPortfolioDate } = require('../user-api/helpers/core/data_lookup_helpers');
|
|
780
|
+
const CANARY_BLOCK_ID = '19M';
|
|
781
|
+
const today = new Date().toISOString().split('T')[0];
|
|
782
|
+
|
|
783
|
+
// Check portfolio
|
|
784
|
+
const portfolioDate = await findLatestPortfolioDate(db, signedInUsersCollection, targetCidNum, 30);
|
|
785
|
+
const portfolioExists = !!portfolioDate;
|
|
786
|
+
const isPortfolioFallback = portfolioDate && portfolioDate !== today;
|
|
787
|
+
|
|
788
|
+
// Check history
|
|
789
|
+
let historyExists = false;
|
|
790
|
+
let historyDate = null;
|
|
791
|
+
let isHistoryFallback = false;
|
|
792
|
+
|
|
793
|
+
const historyCollection = signedInHistoryCollection || 'signed_in_user_history';
|
|
794
|
+
const todayHistoryRef = db.collection(historyCollection)
|
|
795
|
+
.doc(CANARY_BLOCK_ID)
|
|
796
|
+
.collection('snapshots')
|
|
797
|
+
.doc(today)
|
|
798
|
+
.collection('parts');
|
|
799
|
+
|
|
800
|
+
const todayHistorySnapshot = await todayHistoryRef.get();
|
|
801
|
+
|
|
802
|
+
if (!todayHistorySnapshot.empty) {
|
|
803
|
+
historyExists = true;
|
|
804
|
+
historyDate = today;
|
|
805
|
+
} else {
|
|
806
|
+
// Check fallback dates
|
|
807
|
+
for (let i = 1; i <= 30; i++) {
|
|
808
|
+
const checkDate = new Date(today);
|
|
809
|
+
checkDate.setDate(checkDate.getDate() - i);
|
|
810
|
+
const dateStr = checkDate.toISOString().split('T')[0];
|
|
811
|
+
|
|
812
|
+
const historyRef = db.collection(historyCollection)
|
|
813
|
+
.doc(CANARY_BLOCK_ID)
|
|
814
|
+
.collection('snapshots')
|
|
815
|
+
.doc(dateStr)
|
|
816
|
+
.collection('parts');
|
|
817
|
+
|
|
818
|
+
const snapshot = await historyRef.get();
|
|
819
|
+
if (!snapshot.empty) {
|
|
820
|
+
// Check if this user's data exists in any part
|
|
821
|
+
for (const partDoc of snapshot.docs) {
|
|
822
|
+
const partData = partDoc.data();
|
|
823
|
+
if (partData.users && partData.users[String(targetCidNum)]) {
|
|
824
|
+
historyExists = true;
|
|
825
|
+
historyDate = dateStr;
|
|
826
|
+
isHistoryFallback = true;
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (historyExists) break;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
dataStatus = {
|
|
836
|
+
portfolioAvailable: portfolioExists,
|
|
837
|
+
historyAvailable: historyExists,
|
|
838
|
+
date: portfolioDate || today,
|
|
839
|
+
portfolioDate: portfolioDate,
|
|
840
|
+
historyDate: historyDate,
|
|
841
|
+
isPortfolioFallback: isPortfolioFallback,
|
|
842
|
+
isHistoryFallback: isHistoryFallback,
|
|
843
|
+
requestedDate: today,
|
|
844
|
+
userCid: String(targetCidNum)
|
|
845
|
+
};
|
|
846
|
+
} catch (e) {
|
|
847
|
+
logger.log('WARN', `[AdminAPI] Error getting data status: ${e.message}`);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const result = {
|
|
851
|
+
success: true,
|
|
852
|
+
cid: targetCidNum,
|
|
853
|
+
isPI: isPI,
|
|
854
|
+
userData: userData ? {
|
|
855
|
+
username: userData.username,
|
|
856
|
+
fullName: userData.fullName,
|
|
857
|
+
avatar: userData.avatar,
|
|
858
|
+
verifiedAt: userData.verifiedAt,
|
|
859
|
+
lastLogin: userData.lastLogin,
|
|
860
|
+
isOptOut: userData.isOptOut
|
|
861
|
+
} : null,
|
|
862
|
+
piData: rankEntry ? {
|
|
863
|
+
username: rankEntry.UserName || rankEntry.username,
|
|
864
|
+
aum: rankEntry.AUMValue,
|
|
865
|
+
copiers: rankEntry.Copiers,
|
|
866
|
+
riskScore: rankEntry.RiskScore,
|
|
867
|
+
gain: rankEntry.Gain
|
|
868
|
+
} : null,
|
|
869
|
+
verification: verificationData ? {
|
|
870
|
+
status: verificationData.status,
|
|
871
|
+
verifiedAt: verificationData.verifiedAt,
|
|
872
|
+
isOptOut: verificationData.isOptOut
|
|
873
|
+
} : null,
|
|
874
|
+
latestSync: latestSync,
|
|
875
|
+
dataStatus: dataStatus
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
logger.log('INFO', `[AdminAPI] Developer ${developerCid} looked up user ${targetCidNum}`);
|
|
879
|
+
|
|
880
|
+
return res.status(200).json(result);
|
|
881
|
+
|
|
882
|
+
} catch (error) {
|
|
883
|
+
logger.log('ERROR', `[AdminAPI] Error looking up user ${targetCidNum}`, error);
|
|
884
|
+
return res.status(500).json({
|
|
885
|
+
success: false,
|
|
886
|
+
error: "Internal server error",
|
|
887
|
+
message: error.message
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
|
|
631
892
|
return router;
|
|
632
893
|
};
|
|
633
894
|
|
|
@@ -170,14 +170,48 @@ async function finalizeVerification(req, res, dependencies, config) {
|
|
|
170
170
|
// Send unified request to task engine that handles both portfolio/history AND social data
|
|
171
171
|
const pubsubUtils = new PubSubUtils(dependencies);
|
|
172
172
|
|
|
173
|
+
// Generate a requestId for tracking and to ensure finalizeOnDemandRequest runs
|
|
174
|
+
// This is critical - without requestId, the root data indexer and computations won't be triggered
|
|
175
|
+
const requestId = `signup-${realCID}-${Date.now()}`;
|
|
176
|
+
const { signedInUsersCollection } = config;
|
|
177
|
+
|
|
178
|
+
// Create request tracking document (similar to user sync requests)
|
|
179
|
+
try {
|
|
180
|
+
const requestRef = db.collection('user_sync_requests')
|
|
181
|
+
.doc(String(realCID))
|
|
182
|
+
.collection('requests')
|
|
183
|
+
.doc(requestId);
|
|
184
|
+
|
|
185
|
+
await requestRef.set({
|
|
186
|
+
targetUserCid: realCID,
|
|
187
|
+
username: profileData.username,
|
|
188
|
+
status: 'pending',
|
|
189
|
+
source: 'user_signup',
|
|
190
|
+
requestedAt: FieldValue.serverTimestamp(),
|
|
191
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
192
|
+
metadata: {
|
|
193
|
+
isNewUser: true,
|
|
194
|
+
verificationSource: 'otp_verification'
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
logger.log('INFO', `[Verification] Created request tracking document for ${username} (${realCID}): ${requestId}`);
|
|
199
|
+
} catch (reqError) {
|
|
200
|
+
logger.log('WARN', `[Verification] Failed to create request tracking document: ${reqError.message}`);
|
|
201
|
+
// Continue anyway - the requestId is still set
|
|
202
|
+
}
|
|
203
|
+
|
|
173
204
|
// Create a unified on-demand request that includes both portfolio and social
|
|
174
205
|
// The task engine will process both, update root data, then trigger computations
|
|
175
206
|
const unifiedTask = {
|
|
176
207
|
type: 'ON_DEMAND_USER_UPDATE', // Matches Task Engine handler
|
|
208
|
+
cid: realCID, // Top-level CID for handler
|
|
209
|
+
username: profileData.username, // Top-level username for handler
|
|
210
|
+
requestId: requestId, // CRITICAL: Required for finalizeOnDemandRequest to run
|
|
211
|
+
source: 'user_signup', // Mark as signup to ensure computations are triggered
|
|
177
212
|
data: {
|
|
178
213
|
cid: realCID,
|
|
179
214
|
username: profileData.username,
|
|
180
|
-
source: 'user_signup', // Mark as signup to ensure computations are triggered
|
|
181
215
|
includeSocial: true, // Flag to include social data fetch
|
|
182
216
|
since: new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString() // Last 7 days for social
|
|
183
217
|
},
|
|
@@ -185,22 +219,26 @@ async function finalizeVerification(req, res, dependencies, config) {
|
|
|
185
219
|
onDemand: true,
|
|
186
220
|
targetCid: realCID, // Optimization: only process this user
|
|
187
221
|
isNewUser: true, // Flag to add to daily update queue
|
|
188
|
-
requestedAt: new Date().toISOString()
|
|
222
|
+
requestedAt: new Date().toISOString(),
|
|
223
|
+
userType: 'SIGNED_IN_USER' // Explicitly set userType for computation selection
|
|
189
224
|
}
|
|
190
225
|
};
|
|
191
226
|
|
|
192
227
|
// Only trigger if user is public (has portfolio data)
|
|
193
228
|
if (!isOptOut) {
|
|
194
229
|
await pubsubUtils.publish(taskEngineTopic, unifiedTask);
|
|
195
|
-
logger.log('INFO', `[Verification] Triggered unified data fetch (portfolio + social) for ${username} (${realCID}) via on-demand topic`);
|
|
230
|
+
logger.log('INFO', `[Verification] Triggered unified data fetch (portfolio + social) for ${username} (${realCID}) via on-demand topic with requestId: ${requestId}`);
|
|
196
231
|
} else {
|
|
197
232
|
// For private users, still fetch social data but no portfolio
|
|
198
233
|
const socialOnlyTask = {
|
|
199
234
|
type: 'ON_DEMAND_USER_UPDATE',
|
|
235
|
+
cid: realCID,
|
|
236
|
+
username: profileData.username,
|
|
237
|
+
requestId: requestId, // CRITICAL: Required for finalizeOnDemandRequest to run
|
|
238
|
+
source: 'user_signup',
|
|
200
239
|
data: {
|
|
201
240
|
cid: realCID,
|
|
202
241
|
username: profileData.username,
|
|
203
|
-
source: 'user_signup',
|
|
204
242
|
includeSocial: true,
|
|
205
243
|
portfolioOnly: false, // Skip portfolio for private users
|
|
206
244
|
since: new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString()
|
|
@@ -209,11 +247,12 @@ async function finalizeVerification(req, res, dependencies, config) {
|
|
|
209
247
|
onDemand: true,
|
|
210
248
|
targetCid: realCID,
|
|
211
249
|
isNewUser: true,
|
|
212
|
-
requestedAt: new Date().toISOString()
|
|
250
|
+
requestedAt: new Date().toISOString(),
|
|
251
|
+
userType: 'SIGNED_IN_USER' // Explicitly set userType for computation selection
|
|
213
252
|
}
|
|
214
253
|
};
|
|
215
254
|
await pubsubUtils.publish(taskEngineTopic, socialOnlyTask);
|
|
216
|
-
logger.log('INFO', `[Verification] Triggered social-only fetch for private user ${username} (${realCID}) via on-demand topic`);
|
|
255
|
+
logger.log('INFO', `[Verification] Triggered social-only fetch for private user ${username} (${realCID}) via on-demand topic with requestId: ${requestId}`);
|
|
217
256
|
}
|
|
218
257
|
|
|
219
258
|
return res.status(200).json({
|