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({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.560",
3
+ "version": "1.0.562",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [