dubs-server 1.0.0

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.
Files changed (304) hide show
  1. package/.claude/settings.local.json +280 -0
  2. package/CLAUDE.md +46 -0
  3. package/CONNECT4_PRODUCTION_DEPLOY.md +155 -0
  4. package/CURRENT_SESSION.md +171 -0
  5. package/CURRENT_SESSION_DRAW.md +516 -0
  6. package/MARCH_MADNESS_SURVIVOR.md +254 -0
  7. package/PANDA.md +166 -0
  8. package/Procfile +4 -0
  9. package/README.md +476 -0
  10. package/controllers/livescoresController.js +376 -0
  11. package/controllers/pickemController.js +554 -0
  12. package/controllers/survivorAdminController.js +887 -0
  13. package/controllers/survivorController.js +623 -0
  14. package/cron/oracleMonitor.js +77 -0
  15. package/cron/pickemOracleMonitor.js +73 -0
  16. package/data/jackpot-history.json +952 -0
  17. package/data/ncaaTeams.js +406 -0
  18. package/documentation/API_SECURITY_GUIDE.md +327 -0
  19. package/documentation/ARCADE_API.md +593 -0
  20. package/documentation/ARCADE_IMPLEMENTATION_SUMMARY.md +399 -0
  21. package/documentation/ARCADE_QUICKSTART.md +242 -0
  22. package/documentation/AUTOMATIC_MODE_ORACLE.md +321 -0
  23. package/documentation/BUG_FIX_COHORT_DATE_DISPLAY.md +171 -0
  24. package/documentation/CLAIM_MIGRATION_INSTRUCTIONS.md +52 -0
  25. package/documentation/CLAIM_STATUS_FIX.md +67 -0
  26. package/documentation/CLI_TOOL_GUIDE.md +372 -0
  27. package/documentation/COHORT_RETENTION_ANALYSIS.md +295 -0
  28. package/documentation/COHORT_RETENTION_IMPLEMENTATION_COMPLETE.md +461 -0
  29. package/documentation/COHORT_RETENTION_SUMMARY.md +204 -0
  30. package/documentation/COMPLETE_PROJECT_SUMMARY.md +490 -0
  31. package/documentation/DATABASE_QUERIES.md +269 -0
  32. package/documentation/DATABASE_RETENTION_POLICY.md +390 -0
  33. package/documentation/DATABASE_SETUP_GUIDE.md +361 -0
  34. package/documentation/DATABASE_SETUP_SUMMARY.md +247 -0
  35. package/documentation/DEMO_API_CURL_COMMANDS.md +656 -0
  36. package/documentation/DEPLOYMENT_SUMMARY.txt +100 -0
  37. package/documentation/DUPLICATE_NOTIFICATIONS_FIXED.md +201 -0
  38. package/documentation/EXCHANGE_RATES_INTEGRATION.md +371 -0
  39. package/documentation/FINAL_API_PROTECTION_TABLE.md +175 -0
  40. package/documentation/GAME_START_NOTIFICATIONS_DEPLOYMENT.md +256 -0
  41. package/documentation/GAME_START_NOTIFICATIONS_INTEGRATION.md +275 -0
  42. package/documentation/HEROKU_DEPLOYMENT.md +134 -0
  43. package/documentation/HEROKU_SCHEDULER_SETUP.md +271 -0
  44. package/documentation/JACKPOT_API.md +521 -0
  45. package/documentation/JACKPOT_DEPLOYMENT_GUIDE.md +362 -0
  46. package/documentation/JWT_IMPLEMENTATION_SUMMARY.md +373 -0
  47. package/documentation/JWT_QUICK_SETUP.md +268 -0
  48. package/documentation/JWT_TESTING_GUIDE.md +404 -0
  49. package/documentation/KEEPER_RECOVERY_GUIDE.md +381 -0
  50. package/documentation/KEEPER_SETUP.md +206 -0
  51. package/documentation/KEEPER_STATE_MACHINE.md +423 -0
  52. package/documentation/LATEST_PRODUCTION_SETUP.md +387 -0
  53. package/documentation/LOCAL_VOTING_TEST.md +279 -0
  54. package/documentation/ORACLE_FIXES_SUMMARY.md +188 -0
  55. package/documentation/ORACLE_POSTGRESQL_UPDATE.md +202 -0
  56. package/documentation/PAYMENT_DEPLOYMENT.md +209 -0
  57. package/documentation/PNL_TRACKING_SETUP.md +189 -0
  58. package/documentation/PREVENTING_LOCKUP_ERRORS.md +472 -0
  59. package/documentation/PRODUCTION_READY_SUMMARY.md +227 -0
  60. package/documentation/PUBLIC_VS_PRIVATE_ENDPOINTS.md +278 -0
  61. package/documentation/QUICK_AUTH_SETUP.md +99 -0
  62. package/documentation/QUICK_DEPLOY.md +224 -0
  63. package/documentation/QUICK_FIX.md +114 -0
  64. package/documentation/QUICK_START.md +152 -0
  65. package/documentation/REFEREE_MODE_GUIDE.md +392 -0
  66. package/documentation/RETENTION_CORE_ACTION_UPDATE.md +313 -0
  67. package/documentation/RETENTION_UPDATE_SUMMARY.md +108 -0
  68. package/documentation/RUN_MIGRATION_NOW.md +39 -0
  69. package/documentation/SCRIPTS_UPDATE_SUMMARY.md +251 -0
  70. package/documentation/SETUP_GUIDE.md +184 -0
  71. package/documentation/STATE_MACHINE_IMPLEMENTATION.md +250 -0
  72. package/documentation/TELEGRAM_NOTIFICATIONS_DIAGNOSIS.md +361 -0
  73. package/documentation/UNIFIED_ARCHITECTURE.md +231 -0
  74. package/documentation/VOTING_DEPLOYMENT_SUMMARY.md +392 -0
  75. package/documentation/WEBSOCKET_ARCHITECTURE.md +881 -0
  76. package/documentation/WHAT_WE_BUILT_TODAY.md +369 -0
  77. package/documentation/latest/LATEST_PRODUCTION_SETUP.md +865 -0
  78. package/ecosystem.config.js +65 -0
  79. package/env.template +125 -0
  80. package/middleware/apiKeyAuth.js +136 -0
  81. package/middleware/authenticate.js +214 -0
  82. package/middleware/developerUserAuth.js +76 -0
  83. package/middleware/socketAuth.js +69 -0
  84. package/package.json +49 -0
  85. package/postman/Dubs-API-v1-With-Voting.postman_collection.json +555 -0
  86. package/postman/Dubs-API-v1.postman_collection.json +205 -0
  87. package/postman/Dubs_Developer_API.postman_collection.json +662 -0
  88. package/postman/QUICKSTART.md +118 -0
  89. package/postman/QUICK_REFERENCE.md +246 -0
  90. package/postman/README.md +71 -0
  91. package/postman/VOTING_API_GUIDE.md +426 -0
  92. package/refactor/Animations.md +148 -0
  93. package/refactor/Chat.md +252 -0
  94. package/routes/actionsRoutes.js +699 -0
  95. package/routes/adminRoutes.js +370 -0
  96. package/routes/analyticsRoutes.js +1262 -0
  97. package/routes/arcadeRoutes.js +557 -0
  98. package/routes/authRoutes.js +2310 -0
  99. package/routes/avatarRoutes.js +85 -0
  100. package/routes/botRoutes.js +211 -0
  101. package/routes/chatRoutes.js +377 -0
  102. package/routes/cryptoPriceRoutes.js +105 -0
  103. package/routes/developerRoutes.js +4201 -0
  104. package/routes/deviceRoutes.js +214 -0
  105. package/routes/dmRoutes.js +167 -0
  106. package/routes/esportsRoutes.js +806 -0
  107. package/routes/exchangeRateRoutes.js +233 -0
  108. package/routes/gamesRoutes.js +3028 -0
  109. package/routes/jackpotRoutes.js +754 -0
  110. package/routes/keeperMonitoringRoutes.js +156 -0
  111. package/routes/keeperWebhookRoutes.js +466 -0
  112. package/routes/livescoresRoutes.js +31 -0
  113. package/routes/pickemAdminRoutes.js +199 -0
  114. package/routes/pickemRoutes.js +231 -0
  115. package/routes/playerStatsRoutes.js +147 -0
  116. package/routes/portfolioRoutes.js +217 -0
  117. package/routes/promoRoutes.js +418 -0
  118. package/routes/referralEarningsRoutes.js +392 -0
  119. package/routes/socialRoutes.js +459 -0
  120. package/routes/sportsRoutes.js +1271 -0
  121. package/routes/survivorAdminRoutes.js +345 -0
  122. package/routes/survivorRoutes.js +756 -0
  123. package/routes/uploadRoutes.js +256 -0
  124. package/routes/userProfileRoutes.js +244 -0
  125. package/routes/whatsNewRoutes.js +331 -0
  126. package/scripts/.claude/settings.local.json +15 -0
  127. package/scripts/README.md +170 -0
  128. package/scripts/RESTART_EVERYTHING.sh +104 -0
  129. package/scripts/add-claim-columns.sql +48 -0
  130. package/scripts/add-crypto-prices-cache.sql +27 -0
  131. package/scripts/add-exchange-rates-cache.sql +40 -0
  132. package/scripts/add-game-invite-column.sql +23 -0
  133. package/scripts/add-game-invite-notification.sql +33 -0
  134. package/scripts/add-game-invite-telegram-pref.sql +16 -0
  135. package/scripts/add-game-joined-notification.sql +16 -0
  136. package/scripts/add-game-joined-pref.js +40 -0
  137. package/scripts/add-game-joined-preference.sql +6 -0
  138. package/scripts/add-game-start-notifications.sql +41 -0
  139. package/scripts/add-notification-flags-to-games.sql +55 -0
  140. package/scripts/add-pending-game-dismissals.sql +19 -0
  141. package/scripts/add-preferred-currency.sql +34 -0
  142. package/scripts/add-winner-columns.js +61 -0
  143. package/scripts/add_mention_system.sql +53 -0
  144. package/scripts/add_payment_system.sql +96 -0
  145. package/scripts/add_sports_event_id_column.sql +22 -0
  146. package/scripts/analyze-cohort-data-heroku.js +276 -0
  147. package/scripts/analyze-cohort-data.js +295 -0
  148. package/scripts/analyze-prod-cohorts.sh +10 -0
  149. package/scripts/backfill-matchup-images.js +245 -0
  150. package/scripts/backfill-missing-signatures.js +175 -0
  151. package/scripts/backfill-referral-earnings.js +202 -0
  152. package/scripts/check-chat-schema.js +130 -0
  153. package/scripts/check-db.sh +14 -0
  154. package/scripts/check_oracle_in_game.js +54 -0
  155. package/scripts/cleanup-database.js +193 -0
  156. package/scripts/clear-notification-cache.js +85 -0
  157. package/scripts/convert-mnemonic.js +50 -0
  158. package/scripts/create-users-table.sql +44 -0
  159. package/scripts/debug-cohort-counts.js +248 -0
  160. package/scripts/debug-winner-calc.js +84 -0
  161. package/scripts/deploy-payment-system.sh +118 -0
  162. package/scripts/deploy-to-heroku.sh +63 -0
  163. package/scripts/diagnose-locked-round.js +143 -0
  164. package/scripts/dubs-cli.js +720 -0
  165. package/scripts/dump-account.js +65 -0
  166. package/scripts/find-vrf-offset.js +48 -0
  167. package/scripts/fix-chat-notifications-constraint.sql +122 -0
  168. package/scripts/fix-claim-columns.js +124 -0
  169. package/scripts/fix-constraint-now.js +44 -0
  170. package/scripts/fix-lock-timestamps.js +96 -0
  171. package/scripts/fix-locked-round.sh +126 -0
  172. package/scripts/fix-missing-badges.sql +91 -0
  173. package/scripts/fix-payment-notifications.sql +41 -0
  174. package/scripts/force-new-round.js +55 -0
  175. package/scripts/force-resolve-and-claim.js +278 -0
  176. package/scripts/important/README.md +115 -0
  177. package/scripts/important/authority-force-lock.js +197 -0
  178. package/scripts/important/authority-resolve-game.js +267 -0
  179. package/scripts/important/check-game-status.js +373 -0
  180. package/scripts/important/list-pending-games-by-version.js +270 -0
  181. package/scripts/important/reconcile-v1-v2-payouts.js +270 -0
  182. package/scripts/initialize-jackpot.js +111 -0
  183. package/scripts/jackpot/.claude/settings.local.json +10 -0
  184. package/scripts/jackpot/force-reset.js +84 -0
  185. package/scripts/jackpot/initialize-mainnet.js +100 -0
  186. package/scripts/jackpot/keeper.js +742 -0
  187. package/scripts/jackpot/status.js +107 -0
  188. package/scripts/jackpot/update-round-duration.js +143 -0
  189. package/scripts/keeper-bot.js +112 -0
  190. package/scripts/list-pending-games.js +131 -0
  191. package/scripts/migrate-chat-v2.js +127 -0
  192. package/scripts/migrate-chat-winners.js +84 -0
  193. package/scripts/migrate-chat.sh +17 -0
  194. package/scripts/migrate-game-invite.js +83 -0
  195. package/scripts/migrate-heroku-game-notifications.sh +159 -0
  196. package/scripts/migrations/001_analytics_tables.sql +422 -0
  197. package/scripts/migrations/002_add_matchup_image_url.sql +14 -0
  198. package/scripts/migrations/003_referral_earnings.sql +208 -0
  199. package/scripts/migrations/004_add_whats_new_notification_type.sql +62 -0
  200. package/scripts/migrations/005_add_connect4_your_turn_notification.sql +61 -0
  201. package/scripts/migrations/005_push_notifications.sql +55 -0
  202. package/scripts/migrations/006_add_draw_team_players.sql +28 -0
  203. package/scripts/migrations/006_add_game_cancelled_notification.sql +62 -0
  204. package/scripts/migrations/007_add_gif_url.sql +8 -0
  205. package/scripts/migrations/008_add_connect4_columns.sql +139 -0
  206. package/scripts/migrations/008_add_pool_tracking.sql +22 -0
  207. package/scripts/migrations/009_create_survivor_pool_tables.sql +174 -0
  208. package/scripts/migrations/010_add_survivor_pool_outcome.sql +28 -0
  209. package/scripts/migrations/011_create_developer_tables.sql +67 -0
  210. package/scripts/migrations/011_fix_keeper_tables.sql +85 -0
  211. package/scripts/migrations/012_create_developer_webhooks.sql +31 -0
  212. package/scripts/migrations/013_add_network_mode.sql +18 -0
  213. package/scripts/migrations/014_create_developer_app_users.sql +19 -0
  214. package/scripts/migrations/015_add_ui_config.sql +4 -0
  215. package/scripts/migrations/016_add_resolution_secret.sql +4 -0
  216. package/scripts/migrations/017_add_external_game_id.sql +3 -0
  217. package/scripts/migrations/018_create_pickem_tables.sql +115 -0
  218. package/scripts/migrations/019_expo_push_tokens.sql +19 -0
  219. package/scripts/migrations/create_whats_new_tables.sql +88 -0
  220. package/scripts/migrations/drop_live_games_tables.sql +34 -0
  221. package/scripts/open-jackpot-round.js +85 -0
  222. package/scripts/purge-all-data.sh +329 -0
  223. package/scripts/purge-all-data.sql +142 -0
  224. package/scripts/purge-heroku-data.sh +149 -0
  225. package/scripts/purge-heroku-data.sql +62 -0
  226. package/scripts/rebuild-heroku-database.sh +113 -0
  227. package/scripts/recover-funds.js +357 -0
  228. package/scripts/regenerate-epl-images.js +278 -0
  229. package/scripts/resize-s3-matchup-images.js +374 -0
  230. package/scripts/resolve-direct.js +88 -0
  231. package/scripts/resolve-mock-game.js +124 -0
  232. package/scripts/resolve-pickem-game.js +55 -0
  233. package/scripts/resolve-round-manual.js +83 -0
  234. package/scripts/resolve-stuck-game.js +382 -0
  235. package/scripts/resolve-stuck-round.js +42 -0
  236. package/scripts/run-connect4-migration.sh +16 -0
  237. package/scripts/run-mention-migration.sh +32 -0
  238. package/scripts/run-payment-migration.sh +51 -0
  239. package/scripts/run-preferred-currency-migration.sh +31 -0
  240. package/scripts/run-referral-earnings-migration.sh +32 -0
  241. package/scripts/run-survivor-outcome-migration.sh +16 -0
  242. package/scripts/seed-test-users.js +346 -0
  243. package/scripts/setup-auth-tables.js +78 -0
  244. package/scripts/setup-complete-database.sql +992 -0
  245. package/scripts/setup-database-fresh.sh +359 -0
  246. package/scripts/setup-heroku-keeper.sh +48 -0
  247. package/scripts/setup-keeper-database.js +83 -0
  248. package/scripts/setup-keeper-state-db.sql +110 -0
  249. package/scripts/setup-oracle.sh +39 -0
  250. package/scripts/setup-pnl-tracking.js +111 -0
  251. package/scripts/start-devnet.sh +14 -0
  252. package/scripts/test-arcade-devnet.sh +160 -0
  253. package/scripts/test-arcade-match.sh +109 -0
  254. package/scripts/test-automatic-mode.sh +239 -0
  255. package/scripts/test-connect4-cancel-claim.js +370 -0
  256. package/scripts/test-connect4-e2e.js +369 -0
  257. package/scripts/test-connect4-resolve.js +369 -0
  258. package/scripts/test-game-state-endpoint.js +136 -0
  259. package/scripts/test-invite-notification.js +86 -0
  260. package/scripts/test-jackpot-api.sh +71 -0
  261. package/scripts/test-poll-confirmation.js +267 -0
  262. package/scripts/test-resolve-game.js +271 -0
  263. package/scripts/test-resolve-signature.js +223 -0
  264. package/scripts/test-signature-preservation.js +124 -0
  265. package/scripts/test-state-machine.js +291 -0
  266. package/scripts/test-webhook-receiver.js +60 -0
  267. package/scripts/update-notification-constraint.js +52 -0
  268. package/scripts/verify-account-layout.js +145 -0
  269. package/scripts/verify-winner-algorithm.js +278 -0
  270. package/server.js +5259 -0
  271. package/services/arcadeMatchService.js +763 -0
  272. package/services/automaticGameOracle.js +1596 -0
  273. package/services/chatService.js +1612 -0
  274. package/services/connect4GameService.js +1049 -0
  275. package/services/connect4NotificationService.js +374 -0
  276. package/services/cryptoPriceService.js +223 -0
  277. package/services/customGameResolver.js +260 -0
  278. package/services/db.js +79 -0
  279. package/services/directMessageService.js +389 -0
  280. package/services/discordNotifications.js +160 -0
  281. package/services/exchangeRateService.js +289 -0
  282. package/services/expoPushService.js +314 -0
  283. package/services/gamesCacheService.js +539 -0
  284. package/services/jackpotHistory.js +331 -0
  285. package/services/jackpotService.js +856 -0
  286. package/services/keeperStateService.js +355 -0
  287. package/services/matchupImageService.js +591 -0
  288. package/services/notificationCacheService.js +407 -0
  289. package/services/pickemOracle.js +440 -0
  290. package/services/playerStatsService.js +389 -0
  291. package/services/portfolioService.js +555 -0
  292. package/services/promoService.js +757 -0
  293. package/services/promoTreasuryService.js +239 -0
  294. package/services/pushNotifications.js +353 -0
  295. package/services/redisService.js +422 -0
  296. package/services/referralEarningsService.js +728 -0
  297. package/services/s3Service.js +396 -0
  298. package/services/socialService.js +1202 -0
  299. package/services/survivorOracle.js +469 -0
  300. package/services/survivorSimulator.js +475 -0
  301. package/services/telegramNotifications.js +461 -0
  302. package/services/userProfileStatsService.js +1185 -0
  303. package/services/whatsNewService.js +388 -0
  304. package/utils/urlHelper.js +95 -0
@@ -0,0 +1,256 @@
1
+ /**
2
+ * 📸 Upload API Routes
3
+ *
4
+ * Endpoints for file uploads (avatars, OG images, etc.)
5
+ */
6
+
7
+ const express = require('express');
8
+ const router = express.Router();
9
+ const multer = require('multer');
10
+ const { authenticate } = require('../middleware/authenticate');
11
+ const { pool } = require('../services/db'); // Shared database pool
12
+ const nacl = require('tweetnacl');
13
+ const bs58 = require('bs58').default;
14
+
15
+ // Configure multer for memory storage (we'll upload to S3)
16
+ const upload = multer({
17
+ storage: multer.memoryStorage(),
18
+ limits: {
19
+ fileSize: 5 * 1024 * 1024, // 5MB max for OG images
20
+ },
21
+ fileFilter: (req, file, cb) => {
22
+ const allowedTypes = ['image/png', 'image/jpeg', 'image/webp'];
23
+ if (allowedTypes.includes(file.mimetype)) {
24
+ cb(null, true);
25
+ } else {
26
+ cb(new Error('Invalid file type. Only PNG, JPEG, and WebP are allowed.'));
27
+ }
28
+ },
29
+ });
30
+
31
+ module.exports = (s3Service) => {
32
+
33
+ /**
34
+ * POST /upload/avatar/presigned-url
35
+ * Get a presigned URL for avatar upload (requires authentication)
36
+ */
37
+ router.post('/avatar/presigned-url', authenticate, async (req, res) => {
38
+ try {
39
+ const { walletAddress, fileExtension } = req.body;
40
+
41
+ if (!walletAddress) {
42
+ return res.status(400).json({
43
+ success: false,
44
+ error: 'Wallet address is required'
45
+ });
46
+ }
47
+
48
+ // Security: Verify user can only upload their own avatar
49
+ if (req.user.walletAddress !== walletAddress) {
50
+ return res.status(403).json({
51
+ success: false,
52
+ error: 'Unauthorized: Cannot upload avatar for another user'
53
+ });
54
+ }
55
+
56
+ if (!fileExtension) {
57
+ return res.status(400).json({
58
+ success: false,
59
+ error: 'File extension is required'
60
+ });
61
+ }
62
+
63
+ // Validate file type
64
+ if (!s3Service.isValidFileType(fileExtension)) {
65
+ return res.status(400).json({
66
+ success: false,
67
+ error: 'Invalid file type. Allowed: jpg, jpeg, png, gif, webp'
68
+ });
69
+ }
70
+
71
+ console.log('[Upload] Generating presigned URL for:', walletAddress, fileExtension);
72
+
73
+ const { uploadUrl, publicUrl, key } = await s3Service.getUploadUrl(walletAddress, fileExtension);
74
+
75
+ console.log('[Upload] Presigned URL generated successfully');
76
+
77
+ res.json({
78
+ success: true,
79
+ uploadUrl,
80
+ publicUrl,
81
+ key
82
+ });
83
+ } catch (error) {
84
+ console.error('[Upload] Error generating presigned URL:', error.message);
85
+ console.error('[Upload] Full error:', error);
86
+ res.status(500).json({
87
+ success: false,
88
+ error: error.message
89
+ });
90
+ }
91
+ });
92
+
93
+ /**
94
+ * POST /upload/avatar/presigned-url-registration
95
+ * Get a presigned URL for avatar upload during registration (signature-based auth)
96
+ * This allows users to upload avatars before they have a JWT session
97
+ */
98
+ router.post('/avatar/presigned-url-registration', async (req, res) => {
99
+ try {
100
+ const { walletAddress, fileExtension, signature, nonce } = req.body;
101
+
102
+ if (!walletAddress || !fileExtension || !signature || !nonce) {
103
+ return res.status(400).json({
104
+ success: false,
105
+ error: 'Missing required fields: walletAddress, fileExtension, signature, nonce'
106
+ });
107
+ }
108
+
109
+ // Verify the nonce exists and hasn't expired
110
+ // Note: Allow used nonces since verify-signature marks it as used before avatar upload
111
+ const nonceCheck = await pool.query(
112
+ 'SELECT * FROM auth_nonces WHERE nonce = $1 AND wallet_address = $2 AND expires_at > NOW()',
113
+ [nonce, walletAddress]
114
+ );
115
+
116
+ if (nonceCheck.rows.length === 0) {
117
+ return res.status(401).json({
118
+ success: false,
119
+ error: 'Invalid or expired nonce'
120
+ });
121
+ }
122
+
123
+ // Verify signature using the correct message format
124
+ const message = `Sign this message to verify wallet ownership for Dubs Jackpot.\n\nNonce: ${nonce}`;
125
+ const messageBytes = new TextEncoder().encode(message);
126
+ const signatureBytes = bs58.decode(signature);
127
+ const publicKeyBytes = bs58.decode(walletAddress);
128
+
129
+ const isValid = nacl.sign.detached.verify(messageBytes, signatureBytes, publicKeyBytes);
130
+
131
+ if (!isValid) {
132
+ return res.status(401).json({
133
+ success: false,
134
+ error: 'Invalid signature'
135
+ });
136
+ }
137
+
138
+ console.log('[Upload] Signature verified for registration avatar upload')
139
+
140
+ // Validate file type
141
+ if (!s3Service.isValidFileType(fileExtension)) {
142
+ return res.status(400).json({
143
+ success: false,
144
+ error: 'Invalid file type. Allowed: jpg, jpeg, png, gif, webp'
145
+ });
146
+ }
147
+
148
+ console.log('[Upload] Generating presigned URL for registration:', walletAddress, fileExtension);
149
+
150
+ const { uploadUrl, publicUrl, key } = await s3Service.getUploadUrl(walletAddress, fileExtension);
151
+
152
+ console.log('[Upload] Presigned URL generated successfully for registration');
153
+
154
+ res.json({
155
+ success: true,
156
+ uploadUrl,
157
+ publicUrl,
158
+ key
159
+ });
160
+ } catch (error) {
161
+ console.error('[Upload] Error generating presigned URL for registration:', error.message);
162
+ console.error('[Upload] Full error:', error);
163
+ res.status(500).json({
164
+ success: false,
165
+ error: error.message
166
+ });
167
+ }
168
+ });
169
+
170
+ /**
171
+ * POST /upload/profile-og-image
172
+ * Upload a profile OG image (the beautiful ShareablePNLCard)
173
+ * No auth required - anyone viewing a profile triggers this upload
174
+ * Query param ?variant=twitter for Twitter-optimized 1200x628 version
175
+ */
176
+ router.post('/profile-og-image', upload.single('image'), async (req, res) => {
177
+ try {
178
+ const { username, variant } = req.body;
179
+ const imageVariant = variant === 'twitter' ? 'twitter' : 'default';
180
+
181
+ if (!username) {
182
+ return res.status(400).json({
183
+ success: false,
184
+ error: 'Username is required',
185
+ });
186
+ }
187
+
188
+ if (!req.file) {
189
+ return res.status(400).json({
190
+ success: false,
191
+ error: 'Image file is required',
192
+ });
193
+ }
194
+
195
+ console.log(`[Upload] Uploading profile OG image (${imageVariant}) for: ${username} (${req.file.size} bytes)`);
196
+
197
+ const { publicUrl, key } = await s3Service.uploadProfileOGImage(username, req.file.buffer, imageVariant);
198
+
199
+ console.log(`[Upload] Profile OG image (${imageVariant}) uploaded successfully: ${publicUrl}`);
200
+
201
+ res.json({
202
+ success: true,
203
+ imageUrl: publicUrl,
204
+ key,
205
+ variant: imageVariant,
206
+ });
207
+ } catch (error) {
208
+ console.error('[Upload] Error uploading profile OG image:', error.message);
209
+ res.status(500).json({
210
+ success: false,
211
+ error: error.message,
212
+ });
213
+ }
214
+ });
215
+
216
+ /**
217
+ * GET /upload/profile-og-image/:username
218
+ * Check if a profile OG image exists and get its URL
219
+ * Query param ?variant=twitter for Twitter-optimized version
220
+ */
221
+ router.get('/profile-og-image/:username', async (req, res) => {
222
+ try {
223
+ const { username } = req.params;
224
+ const { variant } = req.query;
225
+ const imageVariant = variant === 'twitter' ? 'twitter' : 'default';
226
+
227
+ const exists = await s3Service.profileOGImageExists(username, imageVariant);
228
+
229
+ if (!exists) {
230
+ return res.status(404).json({
231
+ success: false,
232
+ error: 'Profile OG image not found',
233
+ variant: imageVariant,
234
+ });
235
+ }
236
+
237
+ const imageUrl = s3Service.getProfileOGImageUrl(username, imageVariant);
238
+
239
+ res.json({
240
+ success: true,
241
+ imageUrl,
242
+ exists: true,
243
+ variant: imageVariant,
244
+ });
245
+ } catch (error) {
246
+ console.error('[Upload] Error checking profile OG image:', error.message);
247
+ res.status(500).json({
248
+ success: false,
249
+ error: error.message,
250
+ });
251
+ }
252
+ });
253
+
254
+ return router;
255
+ };
256
+
@@ -0,0 +1,244 @@
1
+ /**
2
+ * 📊 User Profile Stats API Routes
3
+ *
4
+ * High-performance endpoints for user profile stats, leaderboards,
5
+ * and PNL tracking across all game types.
6
+ *
7
+ * Most endpoints are public for viewing profiles, but some sensitive
8
+ * endpoints (games history, batch) require authentication.
9
+ */
10
+
11
+ const express = require('express');
12
+ const router = express.Router();
13
+ const { authenticate } = require('../middleware/authenticate');
14
+
15
+ module.exports = (userProfileStatsService) => {
16
+
17
+ /**
18
+ * GET /api/profile/:walletAddress
19
+ * Get comprehensive user profile stats
20
+ *
21
+ * Response includes:
22
+ * - User info (username, avatar, member since)
23
+ * - Combined summary (total PNL, win rate, games played)
24
+ * - Jackpot stats (solpot-style games)
25
+ * - Sports betting stats
26
+ * - Recent games history
27
+ */
28
+ router.get('/:walletAddress', async (req, res) => {
29
+ try {
30
+ const { walletAddress } = req.params;
31
+
32
+ // Validate wallet address format
33
+ if (!walletAddress || walletAddress.length < 32) {
34
+ return res.status(400).json({
35
+ success: false,
36
+ error: 'Invalid wallet address'
37
+ });
38
+ }
39
+
40
+ const stats = await userProfileStatsService.getUserProfileStats(walletAddress);
41
+
42
+ // Check if user has any activity
43
+ const hasActivity = stats.jackpot || stats.sports ||
44
+ (stats.summary.totalGamesPlayed > 0);
45
+
46
+ res.json({
47
+ success: true,
48
+ found: hasActivity,
49
+ profile: stats
50
+ });
51
+ } catch (error) {
52
+ console.error('[Profile] Error fetching stats:', error);
53
+ res.status(500).json({
54
+ success: false,
55
+ error: 'Failed to fetch profile stats'
56
+ });
57
+ }
58
+ });
59
+
60
+ /**
61
+ * GET /api/profile/:walletAddress/games
62
+ * Get user's recent games history with pagination (SECURED - game history is sensitive)
63
+ */
64
+ router.get('/:walletAddress/games', authenticate, async (req, res) => {
65
+ try {
66
+ const { walletAddress } = req.params;
67
+ const limit = Math.min(parseInt(req.query.limit) || 20, 100);
68
+ const offset = parseInt(req.query.offset) || 0;
69
+
70
+ const stats = await userProfileStatsService.getUserProfileStats(walletAddress);
71
+ const games = stats.recentGames || [];
72
+
73
+ res.json({
74
+ success: true,
75
+ games: games.slice(offset, offset + limit),
76
+ total: games.length,
77
+ limit,
78
+ offset
79
+ });
80
+ } catch (error) {
81
+ console.error('[Profile] Error fetching games:', error);
82
+ res.status(500).json({
83
+ success: false,
84
+ error: 'Failed to fetch games history'
85
+ });
86
+ }
87
+ });
88
+
89
+ /**
90
+ * GET /api/profile/leaderboard/:type
91
+ * Get leaderboard by different criteria
92
+ *
93
+ * Types:
94
+ * - pnl: Sort by net profit/loss (default)
95
+ * - wagered: Sort by total amount wagered
96
+ * - wins: Sort by total wins
97
+ * - winrate: Sort by win percentage
98
+ */
99
+ router.get('/leaderboard/:type?', async (req, res) => {
100
+ try {
101
+ const sortBy = req.params.type || 'pnl';
102
+ const limit = Math.min(parseInt(req.query.limit) || 20, 100);
103
+
104
+ const validSortTypes = ['pnl', 'wagered', 'wins', 'winrate'];
105
+ if (!validSortTypes.includes(sortBy)) {
106
+ return res.status(400).json({
107
+ success: false,
108
+ error: `Invalid sort type. Must be one of: ${validSortTypes.join(', ')}`
109
+ });
110
+ }
111
+
112
+ const leaderboard = await userProfileStatsService.getLeaderboard(limit, sortBy);
113
+
114
+ res.json({
115
+ success: true,
116
+ sortBy,
117
+ leaderboard,
118
+ count: leaderboard.length
119
+ });
120
+ } catch (error) {
121
+ console.error('[Profile] Error fetching leaderboard:', error);
122
+ res.status(500).json({
123
+ success: false,
124
+ error: 'Failed to fetch leaderboard'
125
+ });
126
+ }
127
+ });
128
+
129
+ /**
130
+ * GET /api/profile/:walletAddress/summary
131
+ * Get lightweight summary stats (for avatar hover tooltips)
132
+ */
133
+ router.get('/:walletAddress/summary', async (req, res) => {
134
+ try {
135
+ const { walletAddress } = req.params;
136
+
137
+ const stats = await userProfileStatsService.getUserProfileStats(walletAddress);
138
+
139
+ res.json({
140
+ success: true,
141
+ summary: {
142
+ walletAddress: stats.walletAddress,
143
+ username: stats.username,
144
+ avatar: stats.avatar,
145
+ netPNL: stats.summary.netPNL,
146
+ winRate: stats.summary.winRate,
147
+ totalGamesPlayed: stats.summary.totalGamesPlayed,
148
+ isProfitable: stats.summary.isProfitable,
149
+ }
150
+ });
151
+ } catch (error) {
152
+ console.error('[Profile] Error fetching summary:', error);
153
+ res.status(500).json({
154
+ success: false,
155
+ error: 'Failed to fetch profile summary'
156
+ });
157
+ }
158
+ });
159
+
160
+ /**
161
+ * GET /api/profile/:walletAddress/friends
162
+ * Get user's friends list (public)
163
+ */
164
+ router.get('/:walletAddress/friends', async (req, res) => {
165
+ try {
166
+ const { walletAddress } = req.params;
167
+ const limit = Math.min(parseInt(req.query.limit) || 20, 50);
168
+
169
+ const friends = await userProfileStatsService.getUserFriends(walletAddress, limit);
170
+
171
+ res.json({
172
+ success: true,
173
+ friends,
174
+ count: friends.length
175
+ });
176
+ } catch (error) {
177
+ console.error('[Profile] Error fetching friends:', error);
178
+ res.status(500).json({
179
+ success: false,
180
+ error: 'Failed to fetch friends'
181
+ });
182
+ }
183
+ });
184
+
185
+ /**
186
+ * POST /api/profile/batch
187
+ * Get stats for multiple users at once (SECURED - prevents bulk scraping)
188
+ */
189
+ router.post('/batch', authenticate, async (req, res) => {
190
+ try {
191
+ const { walletAddresses } = req.body;
192
+
193
+ if (!walletAddresses || !Array.isArray(walletAddresses)) {
194
+ return res.status(400).json({
195
+ success: false,
196
+ error: 'walletAddresses array is required'
197
+ });
198
+ }
199
+
200
+ // Limit batch size
201
+ const limitedAddresses = walletAddresses.slice(0, 50);
202
+
203
+ // Fetch stats in parallel
204
+ const statsPromises = limitedAddresses.map(wallet =>
205
+ userProfileStatsService.getUserProfileStats(wallet)
206
+ .then(stats => ({
207
+ walletAddress: wallet,
208
+ username: stats.username,
209
+ avatar: stats.avatar,
210
+ netPNL: stats.summary.netPNL,
211
+ winRate: stats.summary.winRate,
212
+ totalGamesPlayed: stats.summary.totalGamesPlayed,
213
+ isProfitable: stats.summary.isProfitable,
214
+ }))
215
+ .catch(() => ({
216
+ walletAddress: wallet,
217
+ username: null,
218
+ avatar: null,
219
+ netPNL: 0,
220
+ winRate: 0,
221
+ totalGamesPlayed: 0,
222
+ isProfitable: false,
223
+ }))
224
+ );
225
+
226
+ const profiles = await Promise.all(statsPromises);
227
+
228
+ res.json({
229
+ success: true,
230
+ profiles,
231
+ count: profiles.length
232
+ });
233
+ } catch (error) {
234
+ console.error('[Profile] Error fetching batch:', error);
235
+ res.status(500).json({
236
+ success: false,
237
+ error: 'Failed to fetch batch profiles'
238
+ });
239
+ }
240
+ });
241
+
242
+ return router;
243
+ };
244
+