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.
- package/.claude/settings.local.json +280 -0
- package/CLAUDE.md +46 -0
- package/CONNECT4_PRODUCTION_DEPLOY.md +155 -0
- package/CURRENT_SESSION.md +171 -0
- package/CURRENT_SESSION_DRAW.md +516 -0
- package/MARCH_MADNESS_SURVIVOR.md +254 -0
- package/PANDA.md +166 -0
- package/Procfile +4 -0
- package/README.md +476 -0
- package/controllers/livescoresController.js +376 -0
- package/controllers/pickemController.js +554 -0
- package/controllers/survivorAdminController.js +887 -0
- package/controllers/survivorController.js +623 -0
- package/cron/oracleMonitor.js +77 -0
- package/cron/pickemOracleMonitor.js +73 -0
- package/data/jackpot-history.json +952 -0
- package/data/ncaaTeams.js +406 -0
- package/documentation/API_SECURITY_GUIDE.md +327 -0
- package/documentation/ARCADE_API.md +593 -0
- package/documentation/ARCADE_IMPLEMENTATION_SUMMARY.md +399 -0
- package/documentation/ARCADE_QUICKSTART.md +242 -0
- package/documentation/AUTOMATIC_MODE_ORACLE.md +321 -0
- package/documentation/BUG_FIX_COHORT_DATE_DISPLAY.md +171 -0
- package/documentation/CLAIM_MIGRATION_INSTRUCTIONS.md +52 -0
- package/documentation/CLAIM_STATUS_FIX.md +67 -0
- package/documentation/CLI_TOOL_GUIDE.md +372 -0
- package/documentation/COHORT_RETENTION_ANALYSIS.md +295 -0
- package/documentation/COHORT_RETENTION_IMPLEMENTATION_COMPLETE.md +461 -0
- package/documentation/COHORT_RETENTION_SUMMARY.md +204 -0
- package/documentation/COMPLETE_PROJECT_SUMMARY.md +490 -0
- package/documentation/DATABASE_QUERIES.md +269 -0
- package/documentation/DATABASE_RETENTION_POLICY.md +390 -0
- package/documentation/DATABASE_SETUP_GUIDE.md +361 -0
- package/documentation/DATABASE_SETUP_SUMMARY.md +247 -0
- package/documentation/DEMO_API_CURL_COMMANDS.md +656 -0
- package/documentation/DEPLOYMENT_SUMMARY.txt +100 -0
- package/documentation/DUPLICATE_NOTIFICATIONS_FIXED.md +201 -0
- package/documentation/EXCHANGE_RATES_INTEGRATION.md +371 -0
- package/documentation/FINAL_API_PROTECTION_TABLE.md +175 -0
- package/documentation/GAME_START_NOTIFICATIONS_DEPLOYMENT.md +256 -0
- package/documentation/GAME_START_NOTIFICATIONS_INTEGRATION.md +275 -0
- package/documentation/HEROKU_DEPLOYMENT.md +134 -0
- package/documentation/HEROKU_SCHEDULER_SETUP.md +271 -0
- package/documentation/JACKPOT_API.md +521 -0
- package/documentation/JACKPOT_DEPLOYMENT_GUIDE.md +362 -0
- package/documentation/JWT_IMPLEMENTATION_SUMMARY.md +373 -0
- package/documentation/JWT_QUICK_SETUP.md +268 -0
- package/documentation/JWT_TESTING_GUIDE.md +404 -0
- package/documentation/KEEPER_RECOVERY_GUIDE.md +381 -0
- package/documentation/KEEPER_SETUP.md +206 -0
- package/documentation/KEEPER_STATE_MACHINE.md +423 -0
- package/documentation/LATEST_PRODUCTION_SETUP.md +387 -0
- package/documentation/LOCAL_VOTING_TEST.md +279 -0
- package/documentation/ORACLE_FIXES_SUMMARY.md +188 -0
- package/documentation/ORACLE_POSTGRESQL_UPDATE.md +202 -0
- package/documentation/PAYMENT_DEPLOYMENT.md +209 -0
- package/documentation/PNL_TRACKING_SETUP.md +189 -0
- package/documentation/PREVENTING_LOCKUP_ERRORS.md +472 -0
- package/documentation/PRODUCTION_READY_SUMMARY.md +227 -0
- package/documentation/PUBLIC_VS_PRIVATE_ENDPOINTS.md +278 -0
- package/documentation/QUICK_AUTH_SETUP.md +99 -0
- package/documentation/QUICK_DEPLOY.md +224 -0
- package/documentation/QUICK_FIX.md +114 -0
- package/documentation/QUICK_START.md +152 -0
- package/documentation/REFEREE_MODE_GUIDE.md +392 -0
- package/documentation/RETENTION_CORE_ACTION_UPDATE.md +313 -0
- package/documentation/RETENTION_UPDATE_SUMMARY.md +108 -0
- package/documentation/RUN_MIGRATION_NOW.md +39 -0
- package/documentation/SCRIPTS_UPDATE_SUMMARY.md +251 -0
- package/documentation/SETUP_GUIDE.md +184 -0
- package/documentation/STATE_MACHINE_IMPLEMENTATION.md +250 -0
- package/documentation/TELEGRAM_NOTIFICATIONS_DIAGNOSIS.md +361 -0
- package/documentation/UNIFIED_ARCHITECTURE.md +231 -0
- package/documentation/VOTING_DEPLOYMENT_SUMMARY.md +392 -0
- package/documentation/WEBSOCKET_ARCHITECTURE.md +881 -0
- package/documentation/WHAT_WE_BUILT_TODAY.md +369 -0
- package/documentation/latest/LATEST_PRODUCTION_SETUP.md +865 -0
- package/ecosystem.config.js +65 -0
- package/env.template +125 -0
- package/middleware/apiKeyAuth.js +136 -0
- package/middleware/authenticate.js +214 -0
- package/middleware/developerUserAuth.js +76 -0
- package/middleware/socketAuth.js +69 -0
- package/package.json +49 -0
- package/postman/Dubs-API-v1-With-Voting.postman_collection.json +555 -0
- package/postman/Dubs-API-v1.postman_collection.json +205 -0
- package/postman/Dubs_Developer_API.postman_collection.json +662 -0
- package/postman/QUICKSTART.md +118 -0
- package/postman/QUICK_REFERENCE.md +246 -0
- package/postman/README.md +71 -0
- package/postman/VOTING_API_GUIDE.md +426 -0
- package/refactor/Animations.md +148 -0
- package/refactor/Chat.md +252 -0
- package/routes/actionsRoutes.js +699 -0
- package/routes/adminRoutes.js +370 -0
- package/routes/analyticsRoutes.js +1262 -0
- package/routes/arcadeRoutes.js +557 -0
- package/routes/authRoutes.js +2310 -0
- package/routes/avatarRoutes.js +85 -0
- package/routes/botRoutes.js +211 -0
- package/routes/chatRoutes.js +377 -0
- package/routes/cryptoPriceRoutes.js +105 -0
- package/routes/developerRoutes.js +4201 -0
- package/routes/deviceRoutes.js +214 -0
- package/routes/dmRoutes.js +167 -0
- package/routes/esportsRoutes.js +806 -0
- package/routes/exchangeRateRoutes.js +233 -0
- package/routes/gamesRoutes.js +3028 -0
- package/routes/jackpotRoutes.js +754 -0
- package/routes/keeperMonitoringRoutes.js +156 -0
- package/routes/keeperWebhookRoutes.js +466 -0
- package/routes/livescoresRoutes.js +31 -0
- package/routes/pickemAdminRoutes.js +199 -0
- package/routes/pickemRoutes.js +231 -0
- package/routes/playerStatsRoutes.js +147 -0
- package/routes/portfolioRoutes.js +217 -0
- package/routes/promoRoutes.js +418 -0
- package/routes/referralEarningsRoutes.js +392 -0
- package/routes/socialRoutes.js +459 -0
- package/routes/sportsRoutes.js +1271 -0
- package/routes/survivorAdminRoutes.js +345 -0
- package/routes/survivorRoutes.js +756 -0
- package/routes/uploadRoutes.js +256 -0
- package/routes/userProfileRoutes.js +244 -0
- package/routes/whatsNewRoutes.js +331 -0
- package/scripts/.claude/settings.local.json +15 -0
- package/scripts/README.md +170 -0
- package/scripts/RESTART_EVERYTHING.sh +104 -0
- package/scripts/add-claim-columns.sql +48 -0
- package/scripts/add-crypto-prices-cache.sql +27 -0
- package/scripts/add-exchange-rates-cache.sql +40 -0
- package/scripts/add-game-invite-column.sql +23 -0
- package/scripts/add-game-invite-notification.sql +33 -0
- package/scripts/add-game-invite-telegram-pref.sql +16 -0
- package/scripts/add-game-joined-notification.sql +16 -0
- package/scripts/add-game-joined-pref.js +40 -0
- package/scripts/add-game-joined-preference.sql +6 -0
- package/scripts/add-game-start-notifications.sql +41 -0
- package/scripts/add-notification-flags-to-games.sql +55 -0
- package/scripts/add-pending-game-dismissals.sql +19 -0
- package/scripts/add-preferred-currency.sql +34 -0
- package/scripts/add-winner-columns.js +61 -0
- package/scripts/add_mention_system.sql +53 -0
- package/scripts/add_payment_system.sql +96 -0
- package/scripts/add_sports_event_id_column.sql +22 -0
- package/scripts/analyze-cohort-data-heroku.js +276 -0
- package/scripts/analyze-cohort-data.js +295 -0
- package/scripts/analyze-prod-cohorts.sh +10 -0
- package/scripts/backfill-matchup-images.js +245 -0
- package/scripts/backfill-missing-signatures.js +175 -0
- package/scripts/backfill-referral-earnings.js +202 -0
- package/scripts/check-chat-schema.js +130 -0
- package/scripts/check-db.sh +14 -0
- package/scripts/check_oracle_in_game.js +54 -0
- package/scripts/cleanup-database.js +193 -0
- package/scripts/clear-notification-cache.js +85 -0
- package/scripts/convert-mnemonic.js +50 -0
- package/scripts/create-users-table.sql +44 -0
- package/scripts/debug-cohort-counts.js +248 -0
- package/scripts/debug-winner-calc.js +84 -0
- package/scripts/deploy-payment-system.sh +118 -0
- package/scripts/deploy-to-heroku.sh +63 -0
- package/scripts/diagnose-locked-round.js +143 -0
- package/scripts/dubs-cli.js +720 -0
- package/scripts/dump-account.js +65 -0
- package/scripts/find-vrf-offset.js +48 -0
- package/scripts/fix-chat-notifications-constraint.sql +122 -0
- package/scripts/fix-claim-columns.js +124 -0
- package/scripts/fix-constraint-now.js +44 -0
- package/scripts/fix-lock-timestamps.js +96 -0
- package/scripts/fix-locked-round.sh +126 -0
- package/scripts/fix-missing-badges.sql +91 -0
- package/scripts/fix-payment-notifications.sql +41 -0
- package/scripts/force-new-round.js +55 -0
- package/scripts/force-resolve-and-claim.js +278 -0
- package/scripts/important/README.md +115 -0
- package/scripts/important/authority-force-lock.js +197 -0
- package/scripts/important/authority-resolve-game.js +267 -0
- package/scripts/important/check-game-status.js +373 -0
- package/scripts/important/list-pending-games-by-version.js +270 -0
- package/scripts/important/reconcile-v1-v2-payouts.js +270 -0
- package/scripts/initialize-jackpot.js +111 -0
- package/scripts/jackpot/.claude/settings.local.json +10 -0
- package/scripts/jackpot/force-reset.js +84 -0
- package/scripts/jackpot/initialize-mainnet.js +100 -0
- package/scripts/jackpot/keeper.js +742 -0
- package/scripts/jackpot/status.js +107 -0
- package/scripts/jackpot/update-round-duration.js +143 -0
- package/scripts/keeper-bot.js +112 -0
- package/scripts/list-pending-games.js +131 -0
- package/scripts/migrate-chat-v2.js +127 -0
- package/scripts/migrate-chat-winners.js +84 -0
- package/scripts/migrate-chat.sh +17 -0
- package/scripts/migrate-game-invite.js +83 -0
- package/scripts/migrate-heroku-game-notifications.sh +159 -0
- package/scripts/migrations/001_analytics_tables.sql +422 -0
- package/scripts/migrations/002_add_matchup_image_url.sql +14 -0
- package/scripts/migrations/003_referral_earnings.sql +208 -0
- package/scripts/migrations/004_add_whats_new_notification_type.sql +62 -0
- package/scripts/migrations/005_add_connect4_your_turn_notification.sql +61 -0
- package/scripts/migrations/005_push_notifications.sql +55 -0
- package/scripts/migrations/006_add_draw_team_players.sql +28 -0
- package/scripts/migrations/006_add_game_cancelled_notification.sql +62 -0
- package/scripts/migrations/007_add_gif_url.sql +8 -0
- package/scripts/migrations/008_add_connect4_columns.sql +139 -0
- package/scripts/migrations/008_add_pool_tracking.sql +22 -0
- package/scripts/migrations/009_create_survivor_pool_tables.sql +174 -0
- package/scripts/migrations/010_add_survivor_pool_outcome.sql +28 -0
- package/scripts/migrations/011_create_developer_tables.sql +67 -0
- package/scripts/migrations/011_fix_keeper_tables.sql +85 -0
- package/scripts/migrations/012_create_developer_webhooks.sql +31 -0
- package/scripts/migrations/013_add_network_mode.sql +18 -0
- package/scripts/migrations/014_create_developer_app_users.sql +19 -0
- package/scripts/migrations/015_add_ui_config.sql +4 -0
- package/scripts/migrations/016_add_resolution_secret.sql +4 -0
- package/scripts/migrations/017_add_external_game_id.sql +3 -0
- package/scripts/migrations/018_create_pickem_tables.sql +115 -0
- package/scripts/migrations/019_expo_push_tokens.sql +19 -0
- package/scripts/migrations/create_whats_new_tables.sql +88 -0
- package/scripts/migrations/drop_live_games_tables.sql +34 -0
- package/scripts/open-jackpot-round.js +85 -0
- package/scripts/purge-all-data.sh +329 -0
- package/scripts/purge-all-data.sql +142 -0
- package/scripts/purge-heroku-data.sh +149 -0
- package/scripts/purge-heroku-data.sql +62 -0
- package/scripts/rebuild-heroku-database.sh +113 -0
- package/scripts/recover-funds.js +357 -0
- package/scripts/regenerate-epl-images.js +278 -0
- package/scripts/resize-s3-matchup-images.js +374 -0
- package/scripts/resolve-direct.js +88 -0
- package/scripts/resolve-mock-game.js +124 -0
- package/scripts/resolve-pickem-game.js +55 -0
- package/scripts/resolve-round-manual.js +83 -0
- package/scripts/resolve-stuck-game.js +382 -0
- package/scripts/resolve-stuck-round.js +42 -0
- package/scripts/run-connect4-migration.sh +16 -0
- package/scripts/run-mention-migration.sh +32 -0
- package/scripts/run-payment-migration.sh +51 -0
- package/scripts/run-preferred-currency-migration.sh +31 -0
- package/scripts/run-referral-earnings-migration.sh +32 -0
- package/scripts/run-survivor-outcome-migration.sh +16 -0
- package/scripts/seed-test-users.js +346 -0
- package/scripts/setup-auth-tables.js +78 -0
- package/scripts/setup-complete-database.sql +992 -0
- package/scripts/setup-database-fresh.sh +359 -0
- package/scripts/setup-heroku-keeper.sh +48 -0
- package/scripts/setup-keeper-database.js +83 -0
- package/scripts/setup-keeper-state-db.sql +110 -0
- package/scripts/setup-oracle.sh +39 -0
- package/scripts/setup-pnl-tracking.js +111 -0
- package/scripts/start-devnet.sh +14 -0
- package/scripts/test-arcade-devnet.sh +160 -0
- package/scripts/test-arcade-match.sh +109 -0
- package/scripts/test-automatic-mode.sh +239 -0
- package/scripts/test-connect4-cancel-claim.js +370 -0
- package/scripts/test-connect4-e2e.js +369 -0
- package/scripts/test-connect4-resolve.js +369 -0
- package/scripts/test-game-state-endpoint.js +136 -0
- package/scripts/test-invite-notification.js +86 -0
- package/scripts/test-jackpot-api.sh +71 -0
- package/scripts/test-poll-confirmation.js +267 -0
- package/scripts/test-resolve-game.js +271 -0
- package/scripts/test-resolve-signature.js +223 -0
- package/scripts/test-signature-preservation.js +124 -0
- package/scripts/test-state-machine.js +291 -0
- package/scripts/test-webhook-receiver.js +60 -0
- package/scripts/update-notification-constraint.js +52 -0
- package/scripts/verify-account-layout.js +145 -0
- package/scripts/verify-winner-algorithm.js +278 -0
- package/server.js +5259 -0
- package/services/arcadeMatchService.js +763 -0
- package/services/automaticGameOracle.js +1596 -0
- package/services/chatService.js +1612 -0
- package/services/connect4GameService.js +1049 -0
- package/services/connect4NotificationService.js +374 -0
- package/services/cryptoPriceService.js +223 -0
- package/services/customGameResolver.js +260 -0
- package/services/db.js +79 -0
- package/services/directMessageService.js +389 -0
- package/services/discordNotifications.js +160 -0
- package/services/exchangeRateService.js +289 -0
- package/services/expoPushService.js +314 -0
- package/services/gamesCacheService.js +539 -0
- package/services/jackpotHistory.js +331 -0
- package/services/jackpotService.js +856 -0
- package/services/keeperStateService.js +355 -0
- package/services/matchupImageService.js +591 -0
- package/services/notificationCacheService.js +407 -0
- package/services/pickemOracle.js +440 -0
- package/services/playerStatsService.js +389 -0
- package/services/portfolioService.js +555 -0
- package/services/promoService.js +757 -0
- package/services/promoTreasuryService.js +239 -0
- package/services/pushNotifications.js +353 -0
- package/services/redisService.js +422 -0
- package/services/referralEarningsService.js +728 -0
- package/services/s3Service.js +396 -0
- package/services/socialService.js +1202 -0
- package/services/survivorOracle.js +469 -0
- package/services/survivorSimulator.js +475 -0
- package/services/telegramNotifications.js +461 -0
- package/services/userProfileStatsService.js +1185 -0
- package/services/whatsNewService.js +388 -0
- 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
|
+
|