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,2310 @@
1
+ /**
2
+ * 🔐 Authentication API Routes
3
+ *
4
+ * Endpoints for wallet-based authentication and user registration
5
+ */
6
+
7
+ const express = require('express');
8
+ const router = express.Router();
9
+ const nacl = require('tweetnacl');
10
+ const bs58 = require('bs58').default;
11
+ const { PublicKey } = require('@solana/web3.js');
12
+ const { pool } = require('../services/db'); // Shared database pool
13
+ const {
14
+ authenticate,
15
+ generateToken,
16
+ createSession,
17
+ deleteSession,
18
+ deleteAllSessions,
19
+ JWT_EXPIRES_IN
20
+ } = require('../middleware/authenticate');
21
+ const { forwardChatNotification } = require('../services/telegramNotifications');
22
+ const { getVapidPublicKey } = require('../services/pushNotifications');
23
+ const promoService = require('../services/promoService');
24
+
25
+ // Load environment variables
26
+ require('dotenv').config();
27
+
28
+ // Initialize tables
29
+ async function initializeTables() {
30
+ if (!process.env.DATABASE_URL) {
31
+ console.log('⚠️ Auth tables skipped: DATABASE_URL not set (database features disabled)');
32
+ return;
33
+ }
34
+
35
+ try {
36
+ await pool.query(`
37
+ CREATE TABLE IF NOT EXISTS users (
38
+ id SERIAL PRIMARY KEY,
39
+ wallet_address VARCHAR(44) UNIQUE NOT NULL,
40
+ email VARCHAR(255),
41
+ username VARCHAR(50) NOT NULL,
42
+ avatar TEXT,
43
+ referral_code VARCHAR(50), -- The referral code they ENTERED (not unique - multiple users can use same code)
44
+ my_referral_code VARCHAR(50) UNIQUE, -- Their OWN referral code to share (must be unique)
45
+ signature TEXT,
46
+ onboarding_complete BOOLEAN DEFAULT false,
47
+ created_at TIMESTAMP DEFAULT NOW(),
48
+ updated_at TIMESTAMP DEFAULT NOW()
49
+ );
50
+
51
+ CREATE TABLE IF NOT EXISTS auth_nonces (
52
+ wallet_address VARCHAR(44) PRIMARY KEY,
53
+ nonce VARCHAR(64) NOT NULL,
54
+ expires_at TIMESTAMP NOT NULL,
55
+ used BOOLEAN DEFAULT false,
56
+ created_at TIMESTAMP DEFAULT NOW()
57
+ );
58
+
59
+ CREATE TABLE IF NOT EXISTS user_sessions (
60
+ id SERIAL PRIMARY KEY,
61
+ wallet_address VARCHAR(44) NOT NULL,
62
+ user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
63
+ token_hash VARCHAR(64) NOT NULL,
64
+ expires_at TIMESTAMP NOT NULL,
65
+ created_at TIMESTAMP DEFAULT NOW(),
66
+ last_activity TIMESTAMP DEFAULT NOW(),
67
+ UNIQUE(wallet_address, token_hash)
68
+ );
69
+
70
+ CREATE INDEX IF NOT EXISTS idx_users_wallet ON users(wallet_address);
71
+ CREATE INDEX IF NOT EXISTS idx_users_username_lower ON users(LOWER(username));
72
+ CREATE INDEX IF NOT EXISTS idx_users_referral_code ON users(referral_code) WHERE referral_code IS NOT NULL;
73
+ CREATE INDEX IF NOT EXISTS idx_users_my_referral_code ON users(my_referral_code) WHERE my_referral_code IS NOT NULL;
74
+ CREATE INDEX IF NOT EXISTS idx_nonces_expires ON auth_nonces(expires_at);
75
+ CREATE INDEX IF NOT EXISTS idx_sessions_wallet ON user_sessions(wallet_address);
76
+ CREATE INDEX IF NOT EXISTS idx_sessions_expires ON user_sessions(expires_at);
77
+ CREATE INDEX IF NOT EXISTS idx_sessions_token_hash ON user_sessions(token_hash);
78
+
79
+ -- User badges/rewards table
80
+ CREATE TABLE IF NOT EXISTS user_badges (
81
+ id SERIAL PRIMARY KEY,
82
+ user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
83
+ badge_type VARCHAR(50) NOT NULL,
84
+ badge_name VARCHAR(100) NOT NULL,
85
+ badge_description TEXT,
86
+ badge_icon VARCHAR(255),
87
+ earned_at TIMESTAMP DEFAULT NOW(),
88
+ referral_count INTEGER,
89
+ UNIQUE(user_id, badge_type)
90
+ );
91
+
92
+ CREATE INDEX IF NOT EXISTS idx_user_badges_user_id ON user_badges(user_id);
93
+ CREATE INDEX IF NOT EXISTS idx_user_badges_type ON user_badges(badge_type);
94
+
95
+ -- Pending game dismissals (tracks which pending game deeplinks user has dismissed)
96
+ CREATE TABLE IF NOT EXISTS pending_game_dismissals (
97
+ id SERIAL PRIMARY KEY,
98
+ wallet_address VARCHAR(44) NOT NULL,
99
+ game_id VARCHAR(255) NOT NULL,
100
+ dismissed_at TIMESTAMP DEFAULT NOW(),
101
+ UNIQUE(wallet_address, game_id)
102
+ );
103
+ CREATE INDEX IF NOT EXISTS idx_pending_dismissals_wallet ON pending_game_dismissals(wallet_address);
104
+ `);
105
+
106
+ // Migration: Add my_referral_code column if it doesn't exist (for existing databases)
107
+ try {
108
+ await pool.query(`
109
+ DO $$
110
+ BEGIN
111
+ IF NOT EXISTS (
112
+ SELECT 1 FROM information_schema.columns
113
+ WHERE table_name = 'users' AND column_name = 'my_referral_code'
114
+ ) THEN
115
+ ALTER TABLE users ADD COLUMN my_referral_code VARCHAR(50) UNIQUE;
116
+ CREATE INDEX IF NOT EXISTS idx_users_my_referral_code ON users(my_referral_code) WHERE my_referral_code IS NOT NULL;
117
+ RAISE NOTICE 'Added my_referral_code column to users table';
118
+ END IF;
119
+ END $$;
120
+ `);
121
+ } catch (migrationError) {
122
+ console.log('⚠️ Migration note (may be expected):', migrationError.message);
123
+ }
124
+
125
+ // Migration: Add Telegram connection fields if they don't exist
126
+ try {
127
+ await pool.query(`
128
+ DO $$
129
+ BEGIN
130
+ -- Add telegram_user_id column
131
+ IF NOT EXISTS (
132
+ SELECT 1 FROM information_schema.columns
133
+ WHERE table_name = 'users' AND column_name = 'telegram_user_id'
134
+ ) THEN
135
+ ALTER TABLE users ADD COLUMN telegram_user_id BIGINT UNIQUE;
136
+ CREATE INDEX IF NOT EXISTS idx_users_telegram_user_id ON users(telegram_user_id) WHERE telegram_user_id IS NOT NULL;
137
+ RAISE NOTICE 'Added telegram_user_id column to users table';
138
+ END IF;
139
+
140
+ -- Add telegram_username column
141
+ IF NOT EXISTS (
142
+ SELECT 1 FROM information_schema.columns
143
+ WHERE table_name = 'users' AND column_name = 'telegram_username'
144
+ ) THEN
145
+ ALTER TABLE users ADD COLUMN telegram_username VARCHAR(255);
146
+ RAISE NOTICE 'Added telegram_username column to users table';
147
+ END IF;
148
+
149
+ -- Add telegram_first_name column
150
+ IF NOT EXISTS (
151
+ SELECT 1 FROM information_schema.columns
152
+ WHERE table_name = 'users' AND column_name = 'telegram_first_name'
153
+ ) THEN
154
+ ALTER TABLE users ADD COLUMN telegram_first_name VARCHAR(255);
155
+ RAISE NOTICE 'Added telegram_first_name column to users table';
156
+ END IF;
157
+
158
+ -- Add telegram_last_name column
159
+ IF NOT EXISTS (
160
+ SELECT 1 FROM information_schema.columns
161
+ WHERE table_name = 'users' AND column_name = 'telegram_last_name'
162
+ ) THEN
163
+ ALTER TABLE users ADD COLUMN telegram_last_name VARCHAR(255);
164
+ RAISE NOTICE 'Added telegram_last_name column to users table';
165
+ END IF;
166
+
167
+ -- Add telegram_photo_url column
168
+ IF NOT EXISTS (
169
+ SELECT 1 FROM information_schema.columns
170
+ WHERE table_name = 'users' AND column_name = 'telegram_photo_url'
171
+ ) THEN
172
+ ALTER TABLE users ADD COLUMN telegram_photo_url TEXT;
173
+ RAISE NOTICE 'Added telegram_photo_url column to users table';
174
+ END IF;
175
+
176
+ -- Add telegram_connected_at column
177
+ IF NOT EXISTS (
178
+ SELECT 1 FROM information_schema.columns
179
+ WHERE table_name = 'users' AND column_name = 'telegram_connected_at'
180
+ ) THEN
181
+ ALTER TABLE users ADD COLUMN telegram_connected_at TIMESTAMP;
182
+ RAISE NOTICE 'Added telegram_connected_at column to users table';
183
+ END IF;
184
+ END $$;
185
+ `);
186
+ } catch (migrationError) {
187
+ console.log('⚠️ Telegram migration note (may be expected):', migrationError.message);
188
+ }
189
+
190
+ // Migration: Create telegram notification preferences table
191
+ try {
192
+ await pool.query(`
193
+ CREATE TABLE IF NOT EXISTS telegram_notification_preferences (
194
+ user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
195
+ notify_reply BOOLEAN DEFAULT true,
196
+ notify_reaction BOOLEAN DEFAULT true,
197
+ notify_friend_request BOOLEAN DEFAULT true,
198
+ notify_friend_request_accepted BOOLEAN DEFAULT true,
199
+ notify_friend_request_declined BOOLEAN DEFAULT true,
200
+ notify_referral BOOLEAN DEFAULT true,
201
+ notify_mention BOOLEAN DEFAULT true,
202
+ notify_friend_message BOOLEAN DEFAULT true,
203
+ notify_game_joined BOOLEAN DEFAULT true,
204
+ notify_game_invite BOOLEAN DEFAULT true,
205
+ created_at TIMESTAMP DEFAULT NOW(),
206
+ updated_at TIMESTAMP DEFAULT NOW()
207
+ );
208
+ `);
209
+ console.log('✅ Telegram notification preferences table initialized');
210
+ } catch (prefError) {
211
+ console.log('⚠️ Telegram preferences table note:', prefError.message);
212
+ }
213
+
214
+ // Migration: Add notify_game_invite column if it doesn't exist
215
+ try {
216
+ await pool.query(`
217
+ DO $$
218
+ BEGIN
219
+ IF NOT EXISTS (
220
+ SELECT 1 FROM information_schema.columns
221
+ WHERE table_name = 'telegram_notification_preferences' AND column_name = 'notify_game_invite'
222
+ ) THEN
223
+ ALTER TABLE telegram_notification_preferences
224
+ ADD COLUMN notify_game_invite BOOLEAN DEFAULT true;
225
+ RAISE NOTICE 'Added notify_game_invite column';
226
+ END IF;
227
+ END $$;
228
+ `);
229
+ } catch (columnError) {
230
+ console.log('⚠️ notify_game_invite column migration:', columnError.message);
231
+ }
232
+
233
+ console.log('✅ Auth tables initialized (with JWT sessions and Telegram support)');
234
+ } catch (error) {
235
+ console.error('❌ Failed to initialize auth tables:', error.message);
236
+ console.error(' Full error:', error);
237
+ }
238
+ }
239
+
240
+ initializeTables();
241
+
242
+ module.exports = () => {
243
+
244
+ /**
245
+ * GET /auth/nonce/:walletAddress
246
+ * Get a nonce for signing (prevents replay attacks)
247
+ */
248
+ router.get('/nonce/:walletAddress', async (req, res) => {
249
+ try {
250
+ const { walletAddress } = req.params;
251
+ console.log('[Auth] Generating nonce for:', walletAddress);
252
+
253
+ // Generate cryptographically secure nonce
254
+ const nonce = require('crypto').randomBytes(32).toString('hex');
255
+
256
+ // Store nonce in database (use NOW() + interval for timezone safety)
257
+ await pool.query(
258
+ `INSERT INTO auth_nonces (wallet_address, nonce, expires_at, used)
259
+ VALUES ($1, $2, NOW() + INTERVAL '5 minutes', false)
260
+ ON CONFLICT (wallet_address)
261
+ DO UPDATE SET nonce = $2, expires_at = NOW() + INTERVAL '5 minutes', used = false`,
262
+ [walletAddress, nonce]
263
+ );
264
+
265
+ const message = `Welcome to Dubs 🚀 Please sign this message to verify wallet ownership. This won't cost any SOL. \n\nNonce: ${nonce}`;
266
+ console.log('[Auth] Nonce generated successfully');
267
+
268
+ res.json({
269
+ success: true,
270
+ nonce,
271
+ message
272
+ });
273
+ } catch (error) {
274
+ console.error('[Auth] Error generating nonce:', error);
275
+ console.error('[Auth] Full error:', error);
276
+ res.status(500).json({
277
+ success: false,
278
+ error: error.message
279
+ });
280
+ }
281
+ });
282
+
283
+ /**
284
+ * GET /auth/check-username/:username
285
+ * Check if a username is available (case-insensitive)
286
+ * Public endpoint - no authentication required
287
+ */
288
+ router.get('/check-username/:username', async (req, res) => {
289
+ try {
290
+ const { username } = req.params;
291
+
292
+ // Validate username format
293
+ if (!username || username.length < 3) {
294
+ return res.json({
295
+ success: true,
296
+ available: false,
297
+ error: 'Username must be at least 3 characters'
298
+ });
299
+ }
300
+
301
+ if (username.length > 20) {
302
+ return res.json({
303
+ success: true,
304
+ available: false,
305
+ error: 'Username must be 20 characters or less'
306
+ });
307
+ }
308
+
309
+ if (!/^[a-zA-Z0-9_]+$/.test(username)) {
310
+ return res.json({
311
+ success: true,
312
+ available: false,
313
+ error: 'Username can only contain letters, numbers, and underscores'
314
+ });
315
+ }
316
+
317
+ // Check if username already exists (case-insensitive)
318
+ const result = await pool.query(
319
+ 'SELECT id FROM users WHERE LOWER(username) = LOWER($1)',
320
+ [username]
321
+ );
322
+
323
+ const available = result.rows.length === 0;
324
+
325
+ console.log(`[Auth] Username check: "${username}" is ${available ? 'available' : 'taken'}`);
326
+
327
+ res.json({
328
+ success: true,
329
+ available,
330
+ username
331
+ });
332
+ } catch (error) {
333
+ console.error('[Auth] Error checking username:', error);
334
+ res.status(500).json({
335
+ success: false,
336
+ error: error.message
337
+ });
338
+ }
339
+ });
340
+
341
+ /**
342
+ * GET /auth/user/me
343
+ * Get authenticated user's FULL profile (requires authentication)
344
+ * Returns all fields including private data and referrer info
345
+ * IMPORTANT: Must be defined BEFORE /user/:walletAddress to avoid route collision
346
+ */
347
+ router.get('/user/me', authenticate, async (req, res) => {
348
+ try {
349
+ const result = await pool.query(
350
+ 'SELECT * FROM users WHERE wallet_address = $1',
351
+ [req.user.walletAddress]
352
+ );
353
+
354
+ if (result.rows.length === 0) {
355
+ return res.status(404).json({
356
+ success: false,
357
+ error: 'User not found'
358
+ });
359
+ }
360
+
361
+ const user = result.rows[0];
362
+
363
+ // If user was referred by someone, fetch the referrer's info including their highest badge
364
+ let referrer = null;
365
+ if (user.referral_code) {
366
+ try {
367
+ const referrerResult = await pool.query(
368
+ 'SELECT id, wallet_address, username, avatar FROM users WHERE my_referral_code = $1',
369
+ [user.referral_code]
370
+ );
371
+
372
+ if (referrerResult.rows.length > 0) {
373
+ const referrerUser = referrerResult.rows[0];
374
+
375
+ // Get referrer's highest badge (Captain > Ambassador > Recruiter)
376
+ const badgeResult = await pool.query(
377
+ `SELECT badge_type, badge_name, badge_icon
378
+ FROM user_badges
379
+ WHERE user_id = $1
380
+ ORDER BY
381
+ CASE badge_type
382
+ WHEN 'captain' THEN 1
383
+ WHEN 'ambassador' THEN 2
384
+ WHEN 'recruiter' THEN 3
385
+ ELSE 4
386
+ END
387
+ LIMIT 1`,
388
+ [referrerUser.id]
389
+ );
390
+
391
+ referrer = {
392
+ wallet_address: referrerUser.wallet_address,
393
+ username: referrerUser.username,
394
+ avatar: referrerUser.avatar,
395
+ badge: badgeResult.rows.length > 0 ? {
396
+ type: badgeResult.rows[0].badge_type,
397
+ name: badgeResult.rows[0].badge_name,
398
+ icon: badgeResult.rows[0].badge_icon
399
+ } : null
400
+ };
401
+
402
+ console.log('[Auth] Found referrer for user:', referrer.username, 'with badge:', referrer.badge?.name || 'none');
403
+ }
404
+ } catch (referrerError) {
405
+ console.error('[Auth] Error fetching referrer:', referrerError.message);
406
+ }
407
+ }
408
+
409
+ // Return ALL fields (it's the user's own data) plus referrer info
410
+ res.json({
411
+ success: true,
412
+ user: user,
413
+ referrer: referrer
414
+ });
415
+ } catch (error) {
416
+ console.error('Error fetching user profile:', error);
417
+ res.status(500).json({
418
+ success: false,
419
+ error: error.message
420
+ });
421
+ }
422
+ });
423
+
424
+ /**
425
+ * GET /auth/user/:walletAddress
426
+ * Get PUBLIC user profile by wallet address
427
+ * Returns only public-safe fields (no email, referral code, etc.)
428
+ * Returns user: null if not found (instead of 404 to reduce console noise)
429
+ */
430
+ router.get('/user/:walletAddress', async (req, res) => {
431
+ try {
432
+ const { walletAddress } = req.params;
433
+
434
+ const result = await pool.query(
435
+ 'SELECT wallet_address, username, avatar, onboarding_complete, created_at FROM users WHERE wallet_address = $1',
436
+ [walletAddress]
437
+ );
438
+
439
+ if (result.rows.length === 0) {
440
+ // Return success with null user (not a 404 error - this is a valid lookup)
441
+ return res.json({
442
+ success: true,
443
+ user: null
444
+ });
445
+ }
446
+
447
+ // Return only public-safe fields
448
+ const user = result.rows[0];
449
+ res.json({
450
+ success: true,
451
+ user: {
452
+ wallet_address: user.wallet_address,
453
+ username: user.username,
454
+ avatar: user.avatar,
455
+ onboarding_complete: user.onboarding_complete,
456
+ created_at: user.created_at,
457
+ // Note: email, referral_code, signature are NOT included
458
+ }
459
+ });
460
+ } catch (error) {
461
+ console.error('Error fetching user:', error);
462
+ res.status(500).json({
463
+ success: false,
464
+ error: error.message
465
+ });
466
+ }
467
+ });
468
+
469
+ /**
470
+ * GET /auth/user-by-username/:username
471
+ * Get PUBLIC user profile by username
472
+ * Returns only public-safe fields (no email, referral code, etc.)
473
+ * Returns user: null if not found (instead of 404 to reduce console noise)
474
+ */
475
+ router.get('/user-by-username/:username', async (req, res) => {
476
+ try {
477
+ const { username } = req.params;
478
+
479
+ const result = await pool.query(
480
+ 'SELECT wallet_address, username, avatar, onboarding_complete, created_at FROM users WHERE LOWER(username) = LOWER($1)',
481
+ [username]
482
+ );
483
+
484
+ if (result.rows.length === 0) {
485
+ // Return success with null user (not a 404 error - this is a valid lookup)
486
+ return res.json({
487
+ success: true,
488
+ user: null
489
+ });
490
+ }
491
+
492
+ // Return only public-safe fields
493
+ const user = result.rows[0];
494
+ res.json({
495
+ success: true,
496
+ user: {
497
+ wallet_address: user.wallet_address,
498
+ username: user.username,
499
+ avatar: user.avatar,
500
+ onboarding_complete: user.onboarding_complete,
501
+ created_at: user.created_at,
502
+ // Note: email, referral_code, signature are NOT included
503
+ }
504
+ });
505
+ } catch (error) {
506
+ console.error('Error fetching user by username:', error);
507
+ res.status(500).json({
508
+ success: false,
509
+ error: error.message
510
+ });
511
+ }
512
+ });
513
+
514
+ /**
515
+ * GET /auth/referrer/:referralCode
516
+ * Get PUBLIC user profile by their referral code (my_referral_code)
517
+ * Used to show who invited a new user when they visit with ?ref= param
518
+ * Returns only public-safe fields (no email, wallet address, etc.)
519
+ */
520
+ router.get('/referrer/:referralCode', async (req, res) => {
521
+ try {
522
+ const { referralCode } = req.params;
523
+
524
+ if (!referralCode || referralCode.length < 4) {
525
+ return res.json({
526
+ success: true,
527
+ user: null
528
+ });
529
+ }
530
+
531
+ console.log('[Auth] Looking up referrer by code:', referralCode);
532
+
533
+ const result = await pool.query(
534
+ 'SELECT username, avatar, created_at FROM users WHERE my_referral_code = $1',
535
+ [referralCode.toUpperCase()]
536
+ );
537
+
538
+ if (result.rows.length === 0) {
539
+ return res.json({
540
+ success: true,
541
+ user: null
542
+ });
543
+ }
544
+
545
+ // Return only public-safe fields (no wallet address for privacy)
546
+ const user = result.rows[0];
547
+ res.json({
548
+ success: true,
549
+ user: {
550
+ username: user.username,
551
+ avatar: user.avatar,
552
+ created_at: user.created_at,
553
+ }
554
+ });
555
+ } catch (error) {
556
+ console.error('[Auth] Error fetching referrer:', error);
557
+ res.status(500).json({
558
+ success: false,
559
+ error: error.message
560
+ });
561
+ }
562
+ });
563
+
564
+ /**
565
+ * POST /auth/verify-signature
566
+ * Verify that a signature matches the wallet AND nonce is valid
567
+ */
568
+ router.post('/verify-signature', async (req, res) => {
569
+ try {
570
+ const { walletAddress, message, signature, nonce } = req.body;
571
+ console.log('[Auth] Verifying signature for:', walletAddress);
572
+ console.log('[Auth] Nonce:', nonce);
573
+
574
+ // 1. Check nonce exists and hasn't been used
575
+ const nonceResult = await pool.query(
576
+ 'SELECT * FROM auth_nonces WHERE wallet_address = $1 AND nonce = $2 AND used = false AND expires_at > NOW()',
577
+ [walletAddress, nonce]
578
+ );
579
+
580
+ console.log('[Auth] Nonce check result:', nonceResult.rows.length, 'rows');
581
+
582
+ if (nonceResult.rows.length === 0) {
583
+ console.log('[Auth] Nonce validation failed - checking why...');
584
+
585
+ // Debug: check if nonce exists at all
586
+ const debugNonce = await pool.query(
587
+ 'SELECT * FROM auth_nonces WHERE wallet_address = $1',
588
+ [walletAddress]
589
+ );
590
+ console.log('[Auth] All nonces for wallet:', debugNonce.rows);
591
+
592
+ return res.status(400).json({
593
+ success: false,
594
+ valid: false,
595
+ error: 'Invalid or expired nonce'
596
+ });
597
+ }
598
+
599
+ // 2. Verify signature
600
+ console.log('[Auth] Verifying cryptographic signature...');
601
+ const signatureUint8 = bs58.decode(signature);
602
+ const messageUint8 = new TextEncoder().encode(message);
603
+ const publicKeyUint8 = new PublicKey(walletAddress).toBytes();
604
+
605
+ const valid = nacl.sign.detached.verify(
606
+ messageUint8,
607
+ signatureUint8,
608
+ publicKeyUint8
609
+ );
610
+
611
+ console.log('[Auth] Signature valid:', valid);
612
+
613
+ if (!valid) {
614
+ return res.status(400).json({
615
+ success: false,
616
+ valid: false,
617
+ error: 'Invalid signature'
618
+ });
619
+ }
620
+
621
+ // 3. Mark nonce as used
622
+ await pool.query(
623
+ 'UPDATE auth_nonces SET used = true WHERE wallet_address = $1 AND nonce = $2',
624
+ [walletAddress, nonce]
625
+ );
626
+
627
+ console.log('[Auth] Signature verified successfully, nonce marked as used');
628
+
629
+ res.json({
630
+ success: true,
631
+ valid: true
632
+ });
633
+ } catch (error) {
634
+ console.error('[Auth] Error verifying signature:', error.message);
635
+ console.error('[Auth] Full error:', error);
636
+ res.status(400).json({
637
+ success: false,
638
+ valid: false,
639
+ error: error.message
640
+ });
641
+ }
642
+ });
643
+
644
+ /**
645
+ * POST /auth/register
646
+ * Register a new user (nonce should already be verified/used from verify-signature)
647
+ * Issues JWT token on successful registration
648
+ */
649
+ router.post('/register', async (req, res) => {
650
+ try {
651
+ const { walletAddress, signature, nonce, email, username, avatar, referralCode, promoCode } = req.body;
652
+
653
+ // Check if user already exists
654
+ const existing = await pool.query(
655
+ 'SELECT * FROM users WHERE wallet_address = $1',
656
+ [walletAddress]
657
+ );
658
+
659
+ if (existing.rows.length > 0) {
660
+ return res.status(400).json({
661
+ success: false,
662
+ error: 'User already registered'
663
+ });
664
+ }
665
+
666
+ // Check if email is already registered
667
+ if (email) {
668
+ const emailCheck = await pool.query(
669
+ 'SELECT id FROM users WHERE LOWER(email) = LOWER($1)',
670
+ [email]
671
+ );
672
+
673
+ if (emailCheck.rows.length > 0) {
674
+ return res.status(400).json({
675
+ success: false,
676
+ error: 'This email is already registered with another account',
677
+ field: 'email'
678
+ });
679
+ }
680
+ }
681
+
682
+ // Check if username is already taken (case-insensitive)
683
+ if (username) {
684
+ const usernameCheck = await pool.query(
685
+ 'SELECT id FROM users WHERE LOWER(username) = LOWER($1)',
686
+ [username]
687
+ );
688
+
689
+ if (usernameCheck.rows.length > 0) {
690
+ return res.status(400).json({
691
+ success: false,
692
+ error: 'This username is already taken. Please choose a different one.',
693
+ field: 'username'
694
+ });
695
+ }
696
+ }
697
+
698
+ // Check if referral code exists (if provided)
699
+ if (referralCode && referralCode.trim()) {
700
+ const referralCheck = await pool.query(
701
+ 'SELECT id FROM users WHERE my_referral_code = $1',
702
+ [referralCode.trim()]
703
+ );
704
+
705
+ if (referralCheck.rows.length === 0) {
706
+ return res.status(400).json({
707
+ success: false,
708
+ error: 'Invalid referral code. Please check the code and try again.',
709
+ field: 'referralCode'
710
+ });
711
+ }
712
+ }
713
+
714
+ // 🎁 Validate and reserve promo code if provided
715
+ // Promo codes are ONLY for new users (already checked above that user doesn't exist)
716
+ let promoReservation = null;
717
+ if (promoCode && promoCode.trim()) {
718
+ console.log('[Auth] 🎁 User registering with promo code:', promoCode);
719
+
720
+ // Reserve the promo code for this user
721
+ promoReservation = await promoService.reserveCode(promoCode.trim(), walletAddress);
722
+
723
+ if (!promoReservation.success && !promoReservation.valid) {
724
+ console.log('[Auth] ❌ Promo code reservation failed:', promoReservation.error);
725
+ return res.status(400).json({
726
+ success: false,
727
+ error: promoReservation.error || 'Invalid or unavailable promo code',
728
+ field: 'promoCode',
729
+ code: promoReservation.code || 'PROMO_ERROR' // Pass through error code (e.g., IP_ALREADY_USED)
730
+ });
731
+ }
732
+
733
+ console.log('[Auth] ✅ Promo code reserved:', promoReservation.code, promoReservation.amountSOL, 'SOL');
734
+ }
735
+
736
+ // Insert new user
737
+ const result = await pool.query(
738
+ `INSERT INTO users
739
+ (wallet_address, email, username, avatar, referral_code, signature, created_at, onboarding_complete)
740
+ VALUES ($1, $2, $3, $4, $5, $6, NOW(), false)
741
+ RETURNING *`,
742
+ [walletAddress, email, username, avatar, referralCode || null, signature]
743
+ );
744
+
745
+ let user = result.rows[0];
746
+
747
+ // 🎁 Generate the user's own referral code automatically
748
+ try {
749
+ const generateMyReferralCode = () => {
750
+ const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Exclude confusing chars (0, O, I, 1)
751
+ let code = '';
752
+ for (let i = 0; i < 8; i++) {
753
+ code += chars.charAt(Math.floor(Math.random() * chars.length));
754
+ }
755
+ return code;
756
+ };
757
+
758
+ let myNewReferralCode;
759
+ let attempts = 0;
760
+ const maxAttempts = 10;
761
+
762
+ while (attempts < maxAttempts) {
763
+ myNewReferralCode = generateMyReferralCode();
764
+
765
+ // Check if code already exists
766
+ const existingCodeResult = await pool.query(
767
+ 'SELECT id FROM users WHERE my_referral_code = $1',
768
+ [myNewReferralCode]
769
+ );
770
+
771
+ if (existingCodeResult.rows.length === 0) {
772
+ // Code is unique, use it
773
+ break;
774
+ }
775
+
776
+ attempts++;
777
+ myNewReferralCode = null;
778
+ }
779
+
780
+ if (myNewReferralCode) {
781
+ const updateResult = await pool.query(
782
+ 'UPDATE users SET my_referral_code = $1 WHERE id = $2 RETURNING *',
783
+ [myNewReferralCode, user.id]
784
+ );
785
+ user = updateResult.rows[0];
786
+ console.log('[Auth] Generated referral code for new user:', myNewReferralCode);
787
+ } else {
788
+ console.error('[Auth] Failed to generate unique referral code after', maxAttempts, 'attempts');
789
+ }
790
+ } catch (refCodeError) {
791
+ console.error('[Auth] Error generating referral code:', refCodeError.message);
792
+ // Non-fatal error - user can generate code later
793
+ }
794
+
795
+ // Generate JWT token
796
+ const token = generateToken(user.wallet_address, user.id);
797
+
798
+ // Calculate expiration date
799
+ const expiresAt = new Date();
800
+ const daysMatch = JWT_EXPIRES_IN.match(/(\d+)d/);
801
+ if (daysMatch) {
802
+ expiresAt.setDate(expiresAt.getDate() + parseInt(daysMatch[1]));
803
+ } else {
804
+ expiresAt.setDate(expiresAt.getDate() + 7); // Default 7 days
805
+ }
806
+
807
+ // Store session in database
808
+ await createSession(user.wallet_address, user.id, token, expiresAt);
809
+
810
+ console.log('[Auth] User registered and session created:', user.wallet_address);
811
+
812
+ // 🎉 If user registered with a referral code, notify the referrer via existing notification system
813
+ if (referralCode) {
814
+ try {
815
+ // Find the user who owns this referral code
816
+ const referrerResult = await pool.query(
817
+ 'SELECT id, wallet_address, username FROM users WHERE my_referral_code = $1',
818
+ [referralCode]
819
+ );
820
+
821
+ if (referrerResult.rows.length > 0) {
822
+ const referrer = referrerResult.rows[0];
823
+ console.log('[Auth] Notifying referrer:', referrer.wallet_address);
824
+
825
+ // Create notification in database using the existing chat_notifications table
826
+ const notifResult = await pool.query(
827
+ `INSERT INTO chat_notifications (user_id, sender_user_id, notification_type, created_at)
828
+ VALUES ($1, $2, 'referral', NOW())
829
+ RETURNING id`,
830
+ [referrer.id, user.id]
831
+ );
832
+
833
+ const notificationId = notifResult.rows[0].id;
834
+
835
+ // Forward to Telegram if connected
836
+ forwardChatNotification(pool, referrer.id, 'referral', user.username).catch(err =>
837
+ console.error('[Auth] Error forwarding referral notification to Telegram:', err.message)
838
+ );
839
+
840
+ // Send real-time notification via the existing WebSocket system
841
+ if (global.chatNamespace && global.onlineUsers) {
842
+ const targetSocketId = global.onlineUsers.get(referrer.id);
843
+
844
+ if (targetSocketId) {
845
+ global.chatNamespace.to(targetSocketId).emit('notification', {
846
+ id: notificationId,
847
+ type: 'referral',
848
+ read: false,
849
+ message: user.username, // Store the new user's username as the message
850
+ senderUsername: user.username,
851
+ senderWallet: user.wallet_address,
852
+ createdAt: new Date(),
853
+ });
854
+ console.log('[Auth] ✅ Referral notification sent to:', referrer.wallet_address);
855
+ } else {
856
+ console.log('[Auth] Referrer is offline, notification saved to database');
857
+ }
858
+ }
859
+
860
+ // 🏆 Check and award badges to the referrer
861
+ try {
862
+ // Count referrals for this referrer
863
+ const referralCountResult = await pool.query(
864
+ 'SELECT COUNT(*) as count FROM users WHERE referral_code = $1',
865
+ [referralCode]
866
+ );
867
+ const referralCount = parseInt(referralCountResult.rows[0].count);
868
+ console.log('[Auth] Referrer now has', referralCount, 'referrals');
869
+
870
+ // Badge thresholds
871
+ const badgeChecks = [
872
+ { type: 'recruiter', name: 'Recruiter', threshold: 1, icon: '/badges/badge_0-removebg-preview.png', description: 'Referred your first user' },
873
+ { type: 'ambassador', name: 'Ambassador', threshold: 5, icon: '/badges/badge_1-removebg-preview.png', description: 'Referred 5 users' },
874
+ { type: 'captain', name: 'Captain', threshold: 10, icon: '/badges/badge_2-removebg-preview.png', description: 'Referred 10 or more users' }
875
+ ];
876
+
877
+ for (const badge of badgeChecks) {
878
+ if (referralCount >= badge.threshold) {
879
+ await pool.query(
880
+ `INSERT INTO user_badges (user_id, badge_type, badge_name, badge_description, badge_icon, referral_count)
881
+ VALUES ($1, $2, $3, $4, $5, $6)
882
+ ON CONFLICT (user_id, badge_type) DO NOTHING`,
883
+ [referrer.id, badge.type, badge.name, badge.description, badge.icon, referralCount]
884
+ );
885
+ }
886
+ }
887
+ } catch (badgeError) {
888
+ console.error('[Auth] Failed to check badges:', badgeError.message);
889
+ }
890
+
891
+ // 🤝 Automatically make referrer and new user friends
892
+ try {
893
+ await pool.query(
894
+ `INSERT INTO user_relationships (user_id, target_user_id, relationship_type)
895
+ VALUES ($1, $2, 'friend'), ($2, $1, 'friend')
896
+ ON CONFLICT (user_id, target_user_id)
897
+ DO UPDATE SET relationship_type = 'friend'`,
898
+ [referrer.id, user.id]
899
+ );
900
+ console.log('[Auth] ✅ Automatic friendship created between', user.username, 'and', referrer.username);
901
+
902
+ // 📡 Emit WebSocket event to referrer so their friends list updates in real-time
903
+ // Using room-based emission (user-${userId}) which is more reliable than socket ID lookup
904
+ if (global.chatNamespace) {
905
+ const roomName = `user-${referrer.id}`;
906
+ console.log('[Auth] 📡 Emitting friend_added to room:', roomName);
907
+ global.chatNamespace.to(roomName).emit('friend_added', {
908
+ friendId: user.id,
909
+ friendUsername: user.username,
910
+ friendWallet: user.wallet_address,
911
+ friendAvatar: user.avatar,
912
+ source: 'referral'
913
+ });
914
+ console.log('[Auth] 📡 Sent friend_added event to referrer room:', roomName, 'for user:', referrer.username);
915
+ } else {
916
+ console.log('[Auth] ⚠️ chatNamespace not available, cannot send friend_added event');
917
+ }
918
+ } catch (friendError) {
919
+ console.error('[Auth] Failed to create automatic friendship:', friendError.message);
920
+ }
921
+ }
922
+ } catch (notificationError) {
923
+ // Don't fail the registration if notification fails
924
+ console.error('[Auth] Failed to send referral notification:', notificationError.message);
925
+ }
926
+ }
927
+
928
+ // Build response
929
+ const response = {
930
+ success: true,
931
+ user: user,
932
+ token: token, // Return JWT token for Authorization header
933
+ authenticated: true
934
+ };
935
+
936
+ // Include promo reservation info if a promo code was reserved
937
+ if (promoReservation && promoReservation.success) {
938
+ // 🎁 Confirm the reservation so it won't expire (user successfully completed registration)
939
+ console.log('[Auth] 🎁 Confirming reservation for wallet:', walletAddress);
940
+ const confirmResult = await promoService.confirmReservation(walletAddress);
941
+ console.log('[Auth] 🎁 Confirm result:', confirmResult);
942
+
943
+ response.promoReservation = {
944
+ code: promoReservation.code,
945
+ amountSOL: promoReservation.amountSOL,
946
+ amountLamports: promoReservation.amountLamports
947
+ // Note: expiresAt removed since reservation is now permanent until used
948
+ };
949
+ console.log('[Auth] 🎁 User registered with promo code:', promoReservation.code, 'for wallet:', walletAddress);
950
+ }
951
+
952
+ res.json(response);
953
+ } catch (error) {
954
+ console.error('[Auth] Error registering user:', error.message);
955
+ console.error('[Auth] Full error:', error);
956
+ res.status(500).json({
957
+ success: false,
958
+ error: error.message
959
+ });
960
+ }
961
+ });
962
+
963
+ /**
964
+ * PUT /auth/user/:walletAddress
965
+ * Update user profile (requires authentication)
966
+ */
967
+ router.put('/user/:walletAddress', authenticate, async (req, res) => {
968
+ try {
969
+ const { walletAddress } = req.params;
970
+ const { email, username, avatar, preferred_currency } = req.body;
971
+
972
+ // Security: Verify user can only update their own profile
973
+ if (req.user.walletAddress !== walletAddress) {
974
+ return res.status(403).json({
975
+ success: false,
976
+ error: 'Unauthorized: Cannot update another user\'s profile'
977
+ });
978
+ }
979
+
980
+ // Validate preferred_currency if provided
981
+ const supportedCurrencies = ['USD', 'EUR', 'CAD', 'GBP', 'JPY', 'AUD', 'CHF', 'CNY', 'SEK', 'NZD'];
982
+ if (preferred_currency && !supportedCurrencies.includes(preferred_currency)) {
983
+ return res.status(400).json({
984
+ success: false,
985
+ error: `Invalid currency. Supported currencies: ${supportedCurrencies.join(', ')}`
986
+ });
987
+ }
988
+
989
+ const result = await pool.query(
990
+ `UPDATE users
991
+ SET email = COALESCE($2, email),
992
+ username = COALESCE($3, username),
993
+ avatar = COALESCE($4, avatar),
994
+ preferred_currency = COALESCE($5, preferred_currency),
995
+ updated_at = NOW()
996
+ WHERE wallet_address = $1
997
+ RETURNING *`,
998
+ [walletAddress, email, username, avatar, preferred_currency]
999
+ );
1000
+
1001
+ if (result.rows.length === 0) {
1002
+ return res.status(404).json({
1003
+ success: false,
1004
+ error: 'User not found'
1005
+ });
1006
+ }
1007
+
1008
+ res.json({
1009
+ success: true,
1010
+ user: result.rows[0]
1011
+ });
1012
+ } catch (error) {
1013
+ console.error('Error updating user:', error);
1014
+ res.status(500).json({
1015
+ success: false,
1016
+ error: error.message
1017
+ });
1018
+ }
1019
+ });
1020
+
1021
+ /**
1022
+ * POST /auth/user/:walletAddress/onboarding-complete
1023
+ * Mark onboarding as complete (requires authentication)
1024
+ */
1025
+ router.post('/user/:walletAddress/onboarding-complete', authenticate, async (req, res) => {
1026
+ try {
1027
+ const { walletAddress } = req.params;
1028
+
1029
+ // Verify user can only complete their own onboarding
1030
+ if (req.user.walletAddress !== walletAddress) {
1031
+ return res.status(403).json({
1032
+ success: false,
1033
+ error: 'Unauthorized'
1034
+ });
1035
+ }
1036
+
1037
+ console.log('[Auth] Completing onboarding for:', walletAddress);
1038
+
1039
+ await pool.query(
1040
+ 'UPDATE users SET onboarding_complete = true, updated_at = NOW() WHERE wallet_address = $1',
1041
+ [walletAddress]
1042
+ );
1043
+
1044
+ console.log('[Auth] Onboarding completed successfully');
1045
+
1046
+ res.json({
1047
+ success: true
1048
+ });
1049
+ } catch (error) {
1050
+ console.error('[Auth] Error completing onboarding:', error.message);
1051
+ console.error('[Auth] Full error:', error);
1052
+ res.status(500).json({
1053
+ success: false,
1054
+ error: error.message
1055
+ });
1056
+ }
1057
+ });
1058
+
1059
+ /**
1060
+ * POST /auth/user/me/generate-referral-code
1061
+ * Generate a unique referral code for the authenticated user
1062
+ * Requires authentication - users can only generate their own referral code
1063
+ */
1064
+ router.post('/user/me/generate-referral-code', authenticate, async (req, res) => {
1065
+ try {
1066
+ const walletAddress = req.user.walletAddress;
1067
+
1068
+ console.log('[Auth] Generating referral code for:', walletAddress);
1069
+
1070
+ // Check if user already has their own referral code
1071
+ const userResult = await pool.query(
1072
+ 'SELECT my_referral_code FROM users WHERE wallet_address = $1',
1073
+ [walletAddress]
1074
+ );
1075
+
1076
+ if (userResult.rows.length === 0) {
1077
+ return res.status(404).json({
1078
+ success: false,
1079
+ error: 'User not found'
1080
+ });
1081
+ }
1082
+
1083
+ const existingCode = userResult.rows[0].my_referral_code;
1084
+
1085
+ // If user already has their own referral code, return it instead of generating a new one
1086
+ if (existingCode) {
1087
+ console.log('[Auth] User already has their own referral code:', existingCode);
1088
+ return res.json({
1089
+ success: true,
1090
+ referralCode: existingCode,
1091
+ message: 'Referral code already exists'
1092
+ });
1093
+ }
1094
+
1095
+ // Generate a unique referral code
1096
+ // Format: 8 characters, alphanumeric uppercase (e.g., "ABC123XY")
1097
+ const generateReferralCode = () => {
1098
+ const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Exclude confusing chars (0, O, I, 1)
1099
+ let code = '';
1100
+ for (let i = 0; i < 8; i++) {
1101
+ code += chars.charAt(Math.floor(Math.random() * chars.length));
1102
+ }
1103
+ return code;
1104
+ };
1105
+
1106
+ // Ensure uniqueness - try up to 10 times
1107
+ let referralCode;
1108
+ let attempts = 0;
1109
+ const maxAttempts = 10;
1110
+
1111
+ while (attempts < maxAttempts) {
1112
+ referralCode = generateReferralCode();
1113
+
1114
+ // Check if code already exists
1115
+ const existingCodeResult = await pool.query(
1116
+ 'SELECT id FROM users WHERE my_referral_code = $1',
1117
+ [referralCode]
1118
+ );
1119
+
1120
+ if (existingCodeResult.rows.length === 0) {
1121
+ // Code is unique, break out of loop
1122
+ break;
1123
+ }
1124
+
1125
+ attempts++;
1126
+ console.log(`[Auth] Referral code collision detected, attempt ${attempts}/${maxAttempts}`);
1127
+ }
1128
+
1129
+ if (attempts >= maxAttempts) {
1130
+ throw new Error('Failed to generate unique referral code after multiple attempts');
1131
+ }
1132
+
1133
+ // Update user with new referral code (their own code, not the one they entered)
1134
+ const updateResult = await pool.query(
1135
+ 'UPDATE users SET my_referral_code = $1, updated_at = NOW() WHERE wallet_address = $2 RETURNING *',
1136
+ [referralCode, walletAddress]
1137
+ );
1138
+
1139
+ console.log('[Auth] Referral code generated successfully:', referralCode);
1140
+
1141
+ res.json({
1142
+ success: true,
1143
+ referralCode: referralCode,
1144
+ user: updateResult.rows[0]
1145
+ });
1146
+ } catch (error) {
1147
+ console.error('[Auth] Error generating referral code:', error.message);
1148
+ console.error('[Auth] Full error:', error);
1149
+ res.status(500).json({
1150
+ success: false,
1151
+ error: error.message
1152
+ });
1153
+ }
1154
+ });
1155
+
1156
+ /**
1157
+ * GET /auth/user/me/referred-users
1158
+ * Get list of users who joined using the authenticated user's referral code
1159
+ * Requires authentication
1160
+ */
1161
+ router.get('/user/me/referred-users', authenticate, async (req, res) => {
1162
+ try {
1163
+ const walletAddress = req.user.walletAddress;
1164
+
1165
+ console.log('[Auth] Fetching referred users for:', walletAddress);
1166
+
1167
+ // First, get the user's own referral code
1168
+ const userResult = await pool.query(
1169
+ 'SELECT my_referral_code FROM users WHERE wallet_address = $1',
1170
+ [walletAddress]
1171
+ );
1172
+
1173
+ if (userResult.rows.length === 0) {
1174
+ return res.status(404).json({
1175
+ success: false,
1176
+ error: 'User not found'
1177
+ });
1178
+ }
1179
+
1180
+ const myReferralCode = userResult.rows[0].my_referral_code;
1181
+
1182
+ if (!myReferralCode) {
1183
+ // User hasn't generated a referral code yet, so no one could have used it
1184
+ return res.json({
1185
+ success: true,
1186
+ referredUsers: [],
1187
+ totalReferrals: 0
1188
+ });
1189
+ }
1190
+
1191
+ // Get all users who joined using this referral code
1192
+ const referredUsersResult = await pool.query(
1193
+ `SELECT
1194
+ wallet_address,
1195
+ username,
1196
+ avatar,
1197
+ created_at
1198
+ FROM users
1199
+ WHERE referral_code = $1
1200
+ ORDER BY created_at DESC`,
1201
+ [myReferralCode]
1202
+ );
1203
+
1204
+ console.log('[Auth] Found', referredUsersResult.rows.length, 'referred users');
1205
+
1206
+ res.json({
1207
+ success: true,
1208
+ referredUsers: referredUsersResult.rows.map(user => ({
1209
+ walletAddress: user.wallet_address,
1210
+ username: user.username,
1211
+ avatar: user.avatar,
1212
+ joinedAt: user.created_at
1213
+ })),
1214
+ totalReferrals: referredUsersResult.rows.length
1215
+ });
1216
+ } catch (error) {
1217
+ console.error('[Auth] Error fetching referred users:', error.message);
1218
+ console.error('[Auth] Full error:', error);
1219
+ res.status(500).json({
1220
+ success: false,
1221
+ error: error.message
1222
+ });
1223
+ }
1224
+ });
1225
+
1226
+ /**
1227
+ * GET /auth/user/me/badges
1228
+ * Get authenticated user's earned badges
1229
+ * Requires authentication
1230
+ */
1231
+ router.get('/user/me/badges', authenticate, async (req, res) => {
1232
+ try {
1233
+ const walletAddress = req.user.walletAddress;
1234
+
1235
+ console.log('[Auth] Fetching badges for:', walletAddress);
1236
+
1237
+ // Get user ID
1238
+ const userResult = await pool.query(
1239
+ 'SELECT id FROM users WHERE wallet_address = $1',
1240
+ [walletAddress]
1241
+ );
1242
+
1243
+ if (userResult.rows.length === 0) {
1244
+ return res.status(404).json({
1245
+ success: false,
1246
+ error: 'User not found'
1247
+ });
1248
+ }
1249
+
1250
+ const userId = userResult.rows[0].id;
1251
+
1252
+ // Get user's badges
1253
+ const badgesResult = await pool.query(
1254
+ `SELECT
1255
+ badge_type,
1256
+ badge_name,
1257
+ badge_description,
1258
+ badge_icon,
1259
+ earned_at,
1260
+ referral_count
1261
+ FROM user_badges
1262
+ WHERE user_id = $1
1263
+ ORDER BY earned_at DESC`,
1264
+ [userId]
1265
+ );
1266
+
1267
+ console.log('[Auth] Found', badgesResult.rows.length, 'badges');
1268
+
1269
+ res.json({
1270
+ success: true,
1271
+ badges: badgesResult.rows
1272
+ });
1273
+ } catch (error) {
1274
+ console.error('[Auth] Error fetching badges:', error.message);
1275
+ console.error('[Auth] Full error:', error);
1276
+ res.status(500).json({
1277
+ success: false,
1278
+ error: error.message
1279
+ });
1280
+ }
1281
+ });
1282
+
1283
+ /**
1284
+ * POST /auth/user/me/check-badges
1285
+ * Check and award badges based on referral count
1286
+ * Requires authentication
1287
+ */
1288
+ router.post('/user/me/check-badges', authenticate, async (req, res) => {
1289
+ try {
1290
+ const walletAddress = req.user.walletAddress;
1291
+
1292
+ console.log('[Auth] Checking badges for:', walletAddress);
1293
+
1294
+ // Get user ID and referral code
1295
+ const userResult = await pool.query(
1296
+ 'SELECT id, my_referral_code FROM users WHERE wallet_address = $1',
1297
+ [walletAddress]
1298
+ );
1299
+
1300
+ if (userResult.rows.length === 0) {
1301
+ return res.status(404).json({
1302
+ success: false,
1303
+ error: 'User not found'
1304
+ });
1305
+ }
1306
+
1307
+ const userId = userResult.rows[0].id;
1308
+ const myReferralCode = userResult.rows[0].my_referral_code;
1309
+
1310
+ if (!myReferralCode) {
1311
+ return res.json({
1312
+ success: true,
1313
+ message: 'No referral code generated yet',
1314
+ badgesAwarded: []
1315
+ });
1316
+ }
1317
+
1318
+ // Count referrals
1319
+ const referralCountResult = await pool.query(
1320
+ 'SELECT COUNT(*) as count FROM users WHERE referral_code = $1',
1321
+ [myReferralCode]
1322
+ );
1323
+
1324
+ const referralCount = parseInt(referralCountResult.rows[0].count);
1325
+ console.log('[Auth] User has', referralCount, 'referrals');
1326
+
1327
+ // Define badge thresholds
1328
+ const badges = [
1329
+ {
1330
+ type: 'recruiter',
1331
+ name: 'Recruiter',
1332
+ description: 'Referred your first user',
1333
+ icon: '/badges/badge_0-removebg-preview.png',
1334
+ threshold: 1
1335
+ },
1336
+ {
1337
+ type: 'ambassador',
1338
+ name: 'Ambassador',
1339
+ description: 'Referred 5 users',
1340
+ icon: '/badges/badge_1-removebg-preview.png',
1341
+ threshold: 5
1342
+ },
1343
+ {
1344
+ type: 'captain',
1345
+ name: 'Captain',
1346
+ description: 'Referred 10 or more users',
1347
+ icon: '/badges/badge_2-removebg-preview.png',
1348
+ threshold: 10
1349
+ }
1350
+ ];
1351
+
1352
+ const badgesAwarded = [];
1353
+
1354
+ // Check each badge threshold
1355
+ for (const badge of badges) {
1356
+ if (referralCount >= badge.threshold) {
1357
+ // Try to award badge (will be ignored if already exists due to UNIQUE constraint)
1358
+ try {
1359
+ const insertResult = await pool.query(
1360
+ `INSERT INTO user_badges (user_id, badge_type, badge_name, badge_description, badge_icon, referral_count)
1361
+ VALUES ($1, $2, $3, $4, $5, $6)
1362
+ ON CONFLICT (user_id, badge_type) DO NOTHING
1363
+ RETURNING *`,
1364
+ [userId, badge.type, badge.name, badge.description, badge.icon, referralCount]
1365
+ );
1366
+
1367
+ if (insertResult.rows.length > 0) {
1368
+ badgesAwarded.push({
1369
+ ...badge,
1370
+ earnedAt: insertResult.rows[0].earned_at
1371
+ });
1372
+ console.log('[Auth] ✅ Awarded badge:', badge.name);
1373
+ }
1374
+ } catch (badgeError) {
1375
+ console.error('[Auth] Error awarding badge:', badge.name, badgeError.message);
1376
+ }
1377
+ }
1378
+ }
1379
+
1380
+ res.json({
1381
+ success: true,
1382
+ referralCount,
1383
+ badgesAwarded,
1384
+ message: badgesAwarded.length > 0
1385
+ ? `Congratulations! You earned ${badgesAwarded.length} badge(s)!`
1386
+ : 'Keep referring to earn more badges!'
1387
+ });
1388
+ } catch (error) {
1389
+ console.error('[Auth] Error checking badges:', error.message);
1390
+ console.error('[Auth] Full error:', error);
1391
+ res.status(500).json({
1392
+ success: false,
1393
+ error: error.message
1394
+ });
1395
+ }
1396
+ });
1397
+
1398
+ /**
1399
+ * POST /auth/login
1400
+ * Login existing user after signature verification
1401
+ * Issues JWT token for authenticated session
1402
+ */
1403
+ router.post('/login', async (req, res) => {
1404
+ try {
1405
+ const { walletAddress, signature, nonce, message } = req.body;
1406
+ console.log('[Auth] Login attempt for:', walletAddress);
1407
+
1408
+ // 1. Check nonce exists and hasn't been used
1409
+ const nonceResult = await pool.query(
1410
+ 'SELECT * FROM auth_nonces WHERE wallet_address = $1 AND nonce = $2 AND used = false AND expires_at > NOW()',
1411
+ [walletAddress, nonce]
1412
+ );
1413
+
1414
+ if (nonceResult.rows.length === 0) {
1415
+ return res.status(400).json({
1416
+ success: false,
1417
+ error: 'Invalid or expired nonce'
1418
+ });
1419
+ }
1420
+
1421
+ // 2. Verify signature
1422
+ const signatureUint8 = bs58.decode(signature);
1423
+ const messageUint8 = new TextEncoder().encode(message);
1424
+ const publicKeyUint8 = new PublicKey(walletAddress).toBytes();
1425
+
1426
+ const valid = nacl.sign.detached.verify(
1427
+ messageUint8,
1428
+ signatureUint8,
1429
+ publicKeyUint8
1430
+ );
1431
+
1432
+ if (!valid) {
1433
+ return res.status(400).json({
1434
+ success: false,
1435
+ error: 'Invalid signature'
1436
+ });
1437
+ }
1438
+
1439
+ // 3. Mark nonce as used
1440
+ await pool.query(
1441
+ 'UPDATE auth_nonces SET used = true WHERE wallet_address = $1 AND nonce = $2',
1442
+ [walletAddress, nonce]
1443
+ );
1444
+
1445
+ // 4. Get user from database
1446
+ const userResult = await pool.query(
1447
+ 'SELECT * FROM users WHERE wallet_address = $1',
1448
+ [walletAddress]
1449
+ );
1450
+
1451
+ if (userResult.rows.length === 0) {
1452
+ return res.status(404).json({
1453
+ success: false,
1454
+ error: 'User not found. Please register first.'
1455
+ });
1456
+ }
1457
+
1458
+ const user = userResult.rows[0];
1459
+
1460
+ // 5. Generate JWT token
1461
+ const token = generateToken(user.wallet_address, user.id);
1462
+
1463
+ // Calculate expiration date
1464
+ const expiresAt = new Date();
1465
+ const daysMatch = JWT_EXPIRES_IN.match(/(\d+)d/);
1466
+ if (daysMatch) {
1467
+ expiresAt.setDate(expiresAt.getDate() + parseInt(daysMatch[1]));
1468
+ } else {
1469
+ expiresAt.setDate(expiresAt.getDate() + 7);
1470
+ }
1471
+
1472
+ // 6. Store session in database
1473
+ await createSession(user.wallet_address, user.id, token, expiresAt);
1474
+
1475
+ console.log('[Auth] User logged in successfully:', user.wallet_address);
1476
+
1477
+ res.json({
1478
+ success: true,
1479
+ user: user,
1480
+ token: token, // Return JWT token for Authorization header
1481
+ authenticated: true
1482
+ });
1483
+ } catch (error) {
1484
+ console.error('[Auth] Error logging in user:', error.message);
1485
+ res.status(500).json({
1486
+ success: false,
1487
+ error: error.message
1488
+ });
1489
+ }
1490
+ });
1491
+
1492
+ /**
1493
+ * GET /auth/validate-session
1494
+ * Check if current session is valid (requires authentication)
1495
+ */
1496
+ router.get('/validate-session', authenticate, async (req, res) => {
1497
+ try {
1498
+ // If middleware passed, session is valid
1499
+ // Get fresh user data
1500
+ const result = await pool.query(
1501
+ 'SELECT * FROM users WHERE wallet_address = $1',
1502
+ [req.user.walletAddress]
1503
+ );
1504
+
1505
+ if (result.rows.length === 0) {
1506
+ return res.status(404).json({
1507
+ success: false,
1508
+ valid: false,
1509
+ error: 'User not found'
1510
+ });
1511
+ }
1512
+
1513
+ res.json({
1514
+ success: true,
1515
+ valid: true,
1516
+ user: result.rows[0]
1517
+ });
1518
+ } catch (error) {
1519
+ console.error('[Auth] Error validating session:', error);
1520
+ res.status(500).json({
1521
+ success: false,
1522
+ valid: false,
1523
+ error: error.message
1524
+ });
1525
+ }
1526
+ });
1527
+
1528
+ /**
1529
+ * POST /auth/logout
1530
+ * Logout user and invalidate session
1531
+ */
1532
+ router.post('/logout', authenticate, async (req, res) => {
1533
+ try {
1534
+ // Get token from Authorization header
1535
+ const authHeader = req.headers.authorization;
1536
+ const token = authHeader ? authHeader.substring(7) : null;
1537
+
1538
+ if (token) {
1539
+ // Delete session from database
1540
+ await deleteSession(req.user.walletAddress, token);
1541
+ }
1542
+
1543
+ console.log('[Auth] User logged out:', req.user.walletAddress);
1544
+
1545
+ res.json({
1546
+ success: true,
1547
+ message: 'Logged out successfully'
1548
+ });
1549
+ } catch (error) {
1550
+ console.error('[Auth] Error logging out:', error);
1551
+ res.status(500).json({
1552
+ success: false,
1553
+ error: error.message
1554
+ });
1555
+ }
1556
+ });
1557
+
1558
+ /**
1559
+ * POST /auth/logout-all
1560
+ * Logout from all devices (requires authentication)
1561
+ */
1562
+ router.post('/logout-all', authenticate, async (req, res) => {
1563
+ try {
1564
+ // Delete all sessions for this wallet
1565
+ await deleteAllSessions(req.user.walletAddress);
1566
+
1567
+ console.log('[Auth] User logged out from all devices:', req.user.walletAddress);
1568
+
1569
+ res.json({
1570
+ success: true,
1571
+ message: 'Logged out from all devices'
1572
+ });
1573
+ } catch (error) {
1574
+ console.error('[Auth] Error logging out from all devices:', error);
1575
+ res.status(500).json({
1576
+ success: false,
1577
+ error: error.message
1578
+ });
1579
+ }
1580
+ });
1581
+
1582
+ // ===== TELEGRAM CONNECTION ENDPOINTS =====
1583
+
1584
+ /**
1585
+ * POST /auth/user/me/link-telegram
1586
+ * Link Telegram account to authenticated user
1587
+ */
1588
+ router.post('/user/me/link-telegram', authenticate, async (req, res) => {
1589
+ try {
1590
+ const { telegramUser } = req.body;
1591
+ const walletAddress = req.user.walletAddress;
1592
+
1593
+ console.log('[Auth] Linking Telegram for user:', walletAddress, 'Telegram ID:', telegramUser.id);
1594
+
1595
+ // Validate Telegram data
1596
+ if (!telegramUser || !telegramUser.id) {
1597
+ return res.status(400).json({
1598
+ success: false,
1599
+ error: 'Invalid Telegram user data'
1600
+ });
1601
+ }
1602
+
1603
+ // Verify Telegram auth hash
1604
+ const crypto = require('crypto');
1605
+ const botToken = process.env.TELEGRAM_BOT_TOKEN;
1606
+
1607
+ if (botToken && telegramUser.hash) {
1608
+ // Create data check string
1609
+ const dataCheckArr = Object.keys(telegramUser)
1610
+ .filter(key => key !== 'hash')
1611
+ .sort()
1612
+ .map(key => `${key}=${telegramUser[key]}`);
1613
+ const dataCheckString = dataCheckArr.join('\n');
1614
+
1615
+ // Create secret key
1616
+ const secretKey = crypto.createHash('sha256').update(botToken).digest();
1617
+
1618
+ // Calculate hash
1619
+ const calculatedHash = crypto
1620
+ .createHmac('sha256', secretKey)
1621
+ .update(dataCheckString)
1622
+ .digest('hex');
1623
+
1624
+ // Verify hash matches
1625
+ if (calculatedHash !== telegramUser.hash) {
1626
+ console.error('[Auth] Telegram auth hash verification failed');
1627
+ return res.status(401).json({
1628
+ success: false,
1629
+ error: 'Invalid Telegram authentication'
1630
+ });
1631
+ }
1632
+
1633
+ // Check auth date (must be within 24 hours)
1634
+ const authDate = parseInt(telegramUser.auth_date);
1635
+ const now = Math.floor(Date.now() / 1000);
1636
+ if (now - authDate > 86400) {
1637
+ return res.status(401).json({
1638
+ success: false,
1639
+ error: 'Telegram authentication expired'
1640
+ });
1641
+ }
1642
+ }
1643
+
1644
+ // Check if this Telegram account is already linked to another user
1645
+ const existingTelegramUser = await pool.query(
1646
+ 'SELECT wallet_address FROM users WHERE telegram_user_id = $1 AND wallet_address != $2',
1647
+ [telegramUser.id, walletAddress]
1648
+ );
1649
+
1650
+ if (existingTelegramUser.rows.length > 0) {
1651
+ return res.status(400).json({
1652
+ success: false,
1653
+ error: 'This Telegram account is already linked to another user'
1654
+ });
1655
+ }
1656
+
1657
+ // Update user with Telegram info
1658
+ const result = await pool.query(
1659
+ `UPDATE users
1660
+ SET telegram_user_id = $1,
1661
+ telegram_username = $2,
1662
+ telegram_first_name = $3,
1663
+ telegram_last_name = $4,
1664
+ telegram_photo_url = $5,
1665
+ telegram_connected_at = NOW(),
1666
+ updated_at = NOW()
1667
+ WHERE wallet_address = $6
1668
+ RETURNING *`,
1669
+ [
1670
+ telegramUser.id,
1671
+ telegramUser.username || null,
1672
+ telegramUser.first_name || null,
1673
+ telegramUser.last_name || null,
1674
+ telegramUser.photo_url || null,
1675
+ walletAddress
1676
+ ]
1677
+ );
1678
+
1679
+ if (result.rows.length === 0) {
1680
+ return res.status(404).json({
1681
+ success: false,
1682
+ error: 'User not found'
1683
+ });
1684
+ }
1685
+
1686
+ const updatedUser = result.rows[0];
1687
+
1688
+ console.log('[Auth] Telegram linked successfully for user:', walletAddress);
1689
+
1690
+ res.json({
1691
+ success: true,
1692
+ message: 'Telegram account linked successfully',
1693
+ telegram: {
1694
+ telegramUserId: updatedUser.telegram_user_id,
1695
+ telegramUsername: updatedUser.telegram_username,
1696
+ telegramFirstName: updatedUser.telegram_first_name,
1697
+ telegramLastName: updatedUser.telegram_last_name,
1698
+ telegramPhotoUrl: updatedUser.telegram_photo_url,
1699
+ connectedAt: updatedUser.telegram_connected_at
1700
+ }
1701
+ });
1702
+ } catch (error) {
1703
+ console.error('[Auth] Error linking Telegram:', error);
1704
+ res.status(500).json({
1705
+ success: false,
1706
+ error: error.message
1707
+ });
1708
+ }
1709
+ });
1710
+
1711
+ /**
1712
+ * POST /auth/user/me/unlink-telegram
1713
+ * Unlink Telegram account from authenticated user
1714
+ */
1715
+ router.post('/user/me/unlink-telegram', authenticate, async (req, res) => {
1716
+ try {
1717
+ const walletAddress = req.user.walletAddress;
1718
+
1719
+ console.log('[Auth] Unlinking Telegram for user:', walletAddress);
1720
+
1721
+ // Update user to remove Telegram info
1722
+ const result = await pool.query(
1723
+ `UPDATE users
1724
+ SET telegram_user_id = NULL,
1725
+ telegram_username = NULL,
1726
+ telegram_first_name = NULL,
1727
+ telegram_last_name = NULL,
1728
+ telegram_photo_url = NULL,
1729
+ telegram_connected_at = NULL,
1730
+ updated_at = NOW()
1731
+ WHERE wallet_address = $1
1732
+ RETURNING *`,
1733
+ [walletAddress]
1734
+ );
1735
+
1736
+ if (result.rows.length === 0) {
1737
+ return res.status(404).json({
1738
+ success: false,
1739
+ error: 'User not found'
1740
+ });
1741
+ }
1742
+
1743
+ console.log('[Auth] Telegram unlinked successfully for user:', walletAddress);
1744
+
1745
+ res.json({
1746
+ success: true,
1747
+ message: 'Telegram account unlinked successfully'
1748
+ });
1749
+ } catch (error) {
1750
+ console.error('[Auth] Error unlinking Telegram:', error);
1751
+ res.status(500).json({
1752
+ success: false,
1753
+ error: error.message
1754
+ });
1755
+ }
1756
+ });
1757
+
1758
+ /**
1759
+ * GET /auth/user/me/telegram
1760
+ * Get Telegram connection status for authenticated user
1761
+ */
1762
+ router.get('/user/me/telegram', authenticate, async (req, res) => {
1763
+ try {
1764
+ const walletAddress = req.user.walletAddress;
1765
+
1766
+ const result = await pool.query(
1767
+ `SELECT telegram_user_id, telegram_username, telegram_first_name,
1768
+ telegram_last_name, telegram_photo_url, telegram_connected_at
1769
+ FROM users
1770
+ WHERE wallet_address = $1`,
1771
+ [walletAddress]
1772
+ );
1773
+
1774
+ if (result.rows.length === 0) {
1775
+ return res.status(404).json({
1776
+ success: false,
1777
+ error: 'User not found'
1778
+ });
1779
+ }
1780
+
1781
+ const user = result.rows[0];
1782
+ const isConnected = user.telegram_user_id !== null;
1783
+
1784
+ res.json({
1785
+ connected: isConnected,
1786
+ telegram: isConnected ? {
1787
+ telegramUserId: user.telegram_user_id,
1788
+ telegramUsername: user.telegram_username,
1789
+ telegramFirstName: user.telegram_first_name,
1790
+ telegramLastName: user.telegram_last_name,
1791
+ telegramPhotoUrl: user.telegram_photo_url,
1792
+ connectedAt: user.telegram_connected_at
1793
+ } : undefined
1794
+ });
1795
+ } catch (error) {
1796
+ console.error('[Auth] Error getting Telegram connection:', error);
1797
+ res.status(500).json({
1798
+ success: false,
1799
+ error: error.message
1800
+ });
1801
+ }
1802
+ });
1803
+
1804
+ /**
1805
+ * GET /auth/user/me/telegram-notification-preferences
1806
+ * Get Telegram notification preferences for authenticated user
1807
+ */
1808
+ router.get('/user/me/telegram-notification-preferences', authenticate, async (req, res) => {
1809
+ try {
1810
+ const walletAddress = req.user.walletAddress;
1811
+
1812
+ // Get user ID from wallet address
1813
+ const userResult = await pool.query('SELECT id FROM users WHERE wallet_address = $1', [walletAddress]);
1814
+ if (userResult.rows.length === 0) {
1815
+ return res.status(404).json({ success: false, error: 'User not found' });
1816
+ }
1817
+ const userId = userResult.rows[0].id;
1818
+
1819
+ const result = await pool.query(
1820
+ `SELECT notify_reply, notify_reaction, notify_friend_request,
1821
+ notify_friend_request_accepted, notify_friend_request_declined,
1822
+ notify_referral, notify_mention, notify_friend_message,
1823
+ notify_game_joined, notify_game_invite
1824
+ FROM telegram_notification_preferences
1825
+ WHERE user_id = $1`,
1826
+ [userId]
1827
+ );
1828
+
1829
+ if (result.rows.length === 0) {
1830
+ // Return defaults if no preferences set
1831
+ return res.json({
1832
+ success: true,
1833
+ preferences: {
1834
+ reply: true,
1835
+ reaction: true,
1836
+ friend_request: true,
1837
+ friend_request_accepted: true,
1838
+ friend_request_declined: true,
1839
+ referral: true,
1840
+ mention: true,
1841
+ friend_message: true,
1842
+ game_joined: true,
1843
+ game_invite: true
1844
+ }
1845
+ });
1846
+ }
1847
+
1848
+ const prefs = result.rows[0];
1849
+ res.json({
1850
+ success: true,
1851
+ preferences: {
1852
+ reply: prefs.notify_reply,
1853
+ reaction: prefs.notify_reaction,
1854
+ friend_request: prefs.notify_friend_request,
1855
+ friend_request_accepted: prefs.notify_friend_request_accepted,
1856
+ friend_request_declined: prefs.notify_friend_request_declined,
1857
+ referral: prefs.notify_referral,
1858
+ mention: prefs.notify_mention,
1859
+ friend_message: prefs.notify_friend_message,
1860
+ game_joined: prefs.notify_game_joined,
1861
+ game_invite: prefs.notify_game_invite
1862
+ }
1863
+ });
1864
+ } catch (error) {
1865
+ console.error('[Auth] Error getting Telegram notification preferences:', error);
1866
+ res.status(500).json({
1867
+ success: false,
1868
+ error: error.message
1869
+ });
1870
+ }
1871
+ });
1872
+
1873
+ /**
1874
+ * PUT /auth/user/me/telegram-notification-preferences
1875
+ * Update Telegram notification preferences for authenticated user
1876
+ */
1877
+ router.put('/user/me/telegram-notification-preferences', authenticate, async (req, res) => {
1878
+ try {
1879
+ const walletAddress = req.user.walletAddress;
1880
+ const { preferences } = req.body;
1881
+
1882
+ if (!preferences) {
1883
+ return res.status(400).json({
1884
+ success: false,
1885
+ error: 'Preferences object required'
1886
+ });
1887
+ }
1888
+
1889
+ // Get user ID from wallet address
1890
+ const userResult = await pool.query('SELECT id FROM users WHERE wallet_address = $1', [walletAddress]);
1891
+ if (userResult.rows.length === 0) {
1892
+ return res.status(404).json({ success: false, error: 'User not found' });
1893
+ }
1894
+ const userId = userResult.rows[0].id;
1895
+
1896
+ await pool.query(
1897
+ `INSERT INTO telegram_notification_preferences
1898
+ (user_id, notify_reply, notify_reaction, notify_friend_request,
1899
+ notify_friend_request_accepted, notify_friend_request_declined,
1900
+ notify_referral, notify_mention, notify_friend_message,
1901
+ notify_game_joined, notify_game_invite, updated_at)
1902
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW())
1903
+ ON CONFLICT (user_id)
1904
+ DO UPDATE SET
1905
+ notify_reply = $2,
1906
+ notify_reaction = $3,
1907
+ notify_friend_request = $4,
1908
+ notify_friend_request_accepted = $5,
1909
+ notify_friend_request_declined = $6,
1910
+ notify_referral = $7,
1911
+ notify_mention = $8,
1912
+ notify_friend_message = $9,
1913
+ notify_game_joined = $10,
1914
+ notify_game_invite = $11,
1915
+ updated_at = NOW()`,
1916
+ [
1917
+ userId,
1918
+ preferences.reply !== undefined ? preferences.reply : true,
1919
+ preferences.reaction !== undefined ? preferences.reaction : true,
1920
+ preferences.friend_request !== undefined ? preferences.friend_request : true,
1921
+ preferences.friend_request_accepted !== undefined ? preferences.friend_request_accepted : true,
1922
+ preferences.friend_request_declined !== undefined ? preferences.friend_request_declined : true,
1923
+ preferences.referral !== undefined ? preferences.referral : true,
1924
+ preferences.mention !== undefined ? preferences.mention : true,
1925
+ preferences.friend_message !== undefined ? preferences.friend_message : true,
1926
+ preferences.game_joined !== undefined ? preferences.game_joined : true,
1927
+ preferences.game_invite !== undefined ? preferences.game_invite : true
1928
+ ]
1929
+ );
1930
+
1931
+ res.json({
1932
+ success: true,
1933
+ message: 'Telegram notification preferences updated'
1934
+ });
1935
+ } catch (error) {
1936
+ console.error('[Auth] Error updating Telegram notification preferences:', error);
1937
+ res.status(500).json({
1938
+ success: false,
1939
+ error: error.message
1940
+ });
1941
+ }
1942
+ });
1943
+
1944
+ /**
1945
+ * POST /auth/pending-game/dismiss
1946
+ * Dismiss a pending game deeplink so it never shows again
1947
+ */
1948
+ router.post('/pending-game/dismiss', async (req, res) => {
1949
+ try {
1950
+ const { walletAddress, gameId } = req.body;
1951
+
1952
+ if (!walletAddress || !gameId) {
1953
+ return res.status(400).json({
1954
+ success: false,
1955
+ error: 'walletAddress and gameId are required'
1956
+ });
1957
+ }
1958
+
1959
+ await pool.query(
1960
+ `INSERT INTO pending_game_dismissals (wallet_address, game_id, dismissed_at)
1961
+ VALUES ($1, $2, NOW())
1962
+ ON CONFLICT (wallet_address, game_id) DO NOTHING`,
1963
+ [walletAddress, gameId]
1964
+ );
1965
+
1966
+ console.log(`[Auth] User ${walletAddress} dismissed pending game ${gameId}`);
1967
+
1968
+ res.json({ success: true });
1969
+ } catch (error) {
1970
+ console.error('[Auth] Error dismissing pending game:', error);
1971
+ res.status(500).json({ success: false, error: error.message });
1972
+ }
1973
+ });
1974
+
1975
+ /**
1976
+ * GET /auth/pending-game/dismissed/:walletAddress/:gameId
1977
+ * Check if a pending game was dismissed
1978
+ */
1979
+ router.get('/pending-game/dismissed/:walletAddress/:gameId', async (req, res) => {
1980
+ try {
1981
+ const { walletAddress, gameId } = req.params;
1982
+
1983
+ const result = await pool.query(
1984
+ `SELECT id FROM pending_game_dismissals
1985
+ WHERE wallet_address = $1 AND game_id = $2`,
1986
+ [walletAddress, gameId]
1987
+ );
1988
+
1989
+ res.json({
1990
+ success: true,
1991
+ dismissed: result.rows.length > 0
1992
+ });
1993
+ } catch (error) {
1994
+ console.error('[Auth] Error checking pending game dismissal:', error);
1995
+ res.status(500).json({ success: false, error: error.message });
1996
+ }
1997
+ });
1998
+
1999
+ // ============================================
2000
+ // PUSH NOTIFICATION ROUTES (PWA/Seeker Mode)
2001
+ // ============================================
2002
+
2003
+ /**
2004
+ * GET /auth/vapid-public-key
2005
+ * Get VAPID public key for push subscription (no auth required)
2006
+ */
2007
+ router.get('/vapid-public-key', (req, res) => {
2008
+ const publicKey = getVapidPublicKey();
2009
+ if (!publicKey) {
2010
+ return res.status(503).json({
2011
+ success: false,
2012
+ error: 'Push notifications not configured'
2013
+ });
2014
+ }
2015
+ res.json({
2016
+ success: true,
2017
+ publicKey
2018
+ });
2019
+ });
2020
+
2021
+ /**
2022
+ * POST /auth/user/me/push-subscription
2023
+ * Register a push subscription for authenticated user
2024
+ */
2025
+ router.post('/user/me/push-subscription', authenticate, async (req, res) => {
2026
+ try {
2027
+ const walletAddress = req.user.walletAddress;
2028
+ const { subscription, deviceType } = req.body;
2029
+
2030
+ if (!subscription || !subscription.endpoint || !subscription.keys) {
2031
+ return res.status(400).json({
2032
+ success: false,
2033
+ error: 'Valid subscription object required (endpoint, keys.p256dh, keys.auth)'
2034
+ });
2035
+ }
2036
+
2037
+ // Get user ID from wallet address
2038
+ const userResult = await pool.query('SELECT id FROM users WHERE wallet_address = $1', [walletAddress]);
2039
+ if (userResult.rows.length === 0) {
2040
+ return res.status(404).json({ success: false, error: 'User not found' });
2041
+ }
2042
+ const userId = userResult.rows[0].id;
2043
+
2044
+ // Upsert push subscription
2045
+ await pool.query(
2046
+ `INSERT INTO push_subscriptions
2047
+ (user_id, endpoint, p256dh, auth, device_type, updated_at)
2048
+ VALUES ($1, $2, $3, $4, $5, NOW())
2049
+ ON CONFLICT (user_id, endpoint)
2050
+ DO UPDATE SET
2051
+ p256dh = $3,
2052
+ auth = $4,
2053
+ device_type = $5,
2054
+ updated_at = NOW()`,
2055
+ [
2056
+ userId,
2057
+ subscription.endpoint,
2058
+ subscription.keys.p256dh,
2059
+ subscription.keys.auth,
2060
+ deviceType || 'android_pwa'
2061
+ ]
2062
+ );
2063
+
2064
+ // Create default notification preferences if they don't exist
2065
+ await pool.query(
2066
+ `INSERT INTO push_notification_preferences (user_id)
2067
+ VALUES ($1)
2068
+ ON CONFLICT (user_id) DO NOTHING`,
2069
+ [userId]
2070
+ );
2071
+
2072
+ console.log(`[Auth] Push subscription registered for user ${walletAddress} (${deviceType || 'android_pwa'})`);
2073
+
2074
+ res.json({
2075
+ success: true,
2076
+ message: 'Push subscription registered'
2077
+ });
2078
+ } catch (error) {
2079
+ console.error('[Auth] Error registering push subscription:', error);
2080
+ res.status(500).json({
2081
+ success: false,
2082
+ error: error.message
2083
+ });
2084
+ }
2085
+ });
2086
+
2087
+ /**
2088
+ * DELETE /auth/user/me/push-subscription
2089
+ * Remove a push subscription for authenticated user
2090
+ */
2091
+ router.delete('/user/me/push-subscription', authenticate, async (req, res) => {
2092
+ try {
2093
+ const walletAddress = req.user.walletAddress;
2094
+ const { endpoint } = req.body;
2095
+
2096
+ if (!endpoint) {
2097
+ return res.status(400).json({
2098
+ success: false,
2099
+ error: 'Endpoint required'
2100
+ });
2101
+ }
2102
+
2103
+ // Get user ID from wallet address
2104
+ const userResult = await pool.query('SELECT id FROM users WHERE wallet_address = $1', [walletAddress]);
2105
+ if (userResult.rows.length === 0) {
2106
+ return res.status(404).json({ success: false, error: 'User not found' });
2107
+ }
2108
+ const userId = userResult.rows[0].id;
2109
+
2110
+ const result = await pool.query(
2111
+ 'DELETE FROM push_subscriptions WHERE user_id = $1 AND endpoint = $2',
2112
+ [userId, endpoint]
2113
+ );
2114
+
2115
+ console.log(`[Auth] Push subscription removed for user ${walletAddress}`);
2116
+
2117
+ res.json({
2118
+ success: true,
2119
+ message: 'Push subscription removed',
2120
+ deleted: result.rowCount > 0
2121
+ });
2122
+ } catch (error) {
2123
+ console.error('[Auth] Error removing push subscription:', error);
2124
+ res.status(500).json({
2125
+ success: false,
2126
+ error: error.message
2127
+ });
2128
+ }
2129
+ });
2130
+
2131
+ /**
2132
+ * GET /auth/user/me/push-subscription
2133
+ * Get push subscription status for authenticated user
2134
+ */
2135
+ router.get('/user/me/push-subscription', authenticate, async (req, res) => {
2136
+ try {
2137
+ const walletAddress = req.user.walletAddress;
2138
+
2139
+ // Get user ID from wallet address
2140
+ const userResult = await pool.query('SELECT id FROM users WHERE wallet_address = $1', [walletAddress]);
2141
+ if (userResult.rows.length === 0) {
2142
+ return res.status(404).json({ success: false, error: 'User not found' });
2143
+ }
2144
+ const userId = userResult.rows[0].id;
2145
+
2146
+ const result = await pool.query(
2147
+ 'SELECT device_type, created_at FROM push_subscriptions WHERE user_id = $1',
2148
+ [userId]
2149
+ );
2150
+
2151
+ res.json({
2152
+ success: true,
2153
+ hasSubscription: result.rows.length > 0,
2154
+ subscriptions: result.rows.map(row => ({
2155
+ deviceType: row.device_type,
2156
+ createdAt: row.created_at
2157
+ }))
2158
+ });
2159
+ } catch (error) {
2160
+ console.error('[Auth] Error getting push subscription status:', error);
2161
+ res.status(500).json({
2162
+ success: false,
2163
+ error: error.message
2164
+ });
2165
+ }
2166
+ });
2167
+
2168
+ /**
2169
+ * GET /auth/user/me/push-notification-preferences
2170
+ * Get push notification preferences for authenticated user
2171
+ */
2172
+ router.get('/user/me/push-notification-preferences', authenticate, async (req, res) => {
2173
+ try {
2174
+ const walletAddress = req.user.walletAddress;
2175
+
2176
+ // Get user ID from wallet address
2177
+ const userResult = await pool.query('SELECT id FROM users WHERE wallet_address = $1', [walletAddress]);
2178
+ if (userResult.rows.length === 0) {
2179
+ return res.status(404).json({ success: false, error: 'User not found' });
2180
+ }
2181
+ const userId = userResult.rows[0].id;
2182
+
2183
+ const result = await pool.query(
2184
+ `SELECT notify_reply, notify_reaction, notify_friend_request,
2185
+ notify_friend_request_accepted, notify_friend_request_declined,
2186
+ notify_referral, notify_mention, notify_friend_message,
2187
+ notify_game_joined, notify_game_invite
2188
+ FROM push_notification_preferences
2189
+ WHERE user_id = $1`,
2190
+ [userId]
2191
+ );
2192
+
2193
+ if (result.rows.length === 0) {
2194
+ // Return defaults if no preferences set
2195
+ return res.json({
2196
+ success: true,
2197
+ preferences: {
2198
+ reply: true,
2199
+ reaction: true,
2200
+ friend_request: true,
2201
+ friend_request_accepted: true,
2202
+ friend_request_declined: true,
2203
+ referral: true,
2204
+ mention: true,
2205
+ friend_message: true,
2206
+ game_joined: true,
2207
+ game_invite: true
2208
+ }
2209
+ });
2210
+ }
2211
+
2212
+ const prefs = result.rows[0];
2213
+ res.json({
2214
+ success: true,
2215
+ preferences: {
2216
+ reply: prefs.notify_reply,
2217
+ reaction: prefs.notify_reaction,
2218
+ friend_request: prefs.notify_friend_request,
2219
+ friend_request_accepted: prefs.notify_friend_request_accepted,
2220
+ friend_request_declined: prefs.notify_friend_request_declined,
2221
+ referral: prefs.notify_referral,
2222
+ mention: prefs.notify_mention,
2223
+ friend_message: prefs.notify_friend_message,
2224
+ game_joined: prefs.notify_game_joined,
2225
+ game_invite: prefs.notify_game_invite
2226
+ }
2227
+ });
2228
+ } catch (error) {
2229
+ console.error('[Auth] Error getting push notification preferences:', error);
2230
+ res.status(500).json({
2231
+ success: false,
2232
+ error: error.message
2233
+ });
2234
+ }
2235
+ });
2236
+
2237
+ /**
2238
+ * PUT /auth/user/me/push-notification-preferences
2239
+ * Update push notification preferences for authenticated user
2240
+ */
2241
+ router.put('/user/me/push-notification-preferences', authenticate, async (req, res) => {
2242
+ try {
2243
+ const walletAddress = req.user.walletAddress;
2244
+ const { preferences } = req.body;
2245
+
2246
+ if (!preferences) {
2247
+ return res.status(400).json({
2248
+ success: false,
2249
+ error: 'Preferences object required'
2250
+ });
2251
+ }
2252
+
2253
+ // Get user ID from wallet address
2254
+ const userResult = await pool.query('SELECT id FROM users WHERE wallet_address = $1', [walletAddress]);
2255
+ if (userResult.rows.length === 0) {
2256
+ return res.status(404).json({ success: false, error: 'User not found' });
2257
+ }
2258
+ const userId = userResult.rows[0].id;
2259
+
2260
+ await pool.query(
2261
+ `INSERT INTO push_notification_preferences
2262
+ (user_id, notify_reply, notify_reaction, notify_friend_request,
2263
+ notify_friend_request_accepted, notify_friend_request_declined,
2264
+ notify_referral, notify_mention, notify_friend_message,
2265
+ notify_game_joined, notify_game_invite, updated_at)
2266
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW())
2267
+ ON CONFLICT (user_id)
2268
+ DO UPDATE SET
2269
+ notify_reply = $2,
2270
+ notify_reaction = $3,
2271
+ notify_friend_request = $4,
2272
+ notify_friend_request_accepted = $5,
2273
+ notify_friend_request_declined = $6,
2274
+ notify_referral = $7,
2275
+ notify_mention = $8,
2276
+ notify_friend_message = $9,
2277
+ notify_game_joined = $10,
2278
+ notify_game_invite = $11,
2279
+ updated_at = NOW()`,
2280
+ [
2281
+ userId,
2282
+ preferences.reply !== undefined ? preferences.reply : true,
2283
+ preferences.reaction !== undefined ? preferences.reaction : true,
2284
+ preferences.friend_request !== undefined ? preferences.friend_request : true,
2285
+ preferences.friend_request_accepted !== undefined ? preferences.friend_request_accepted : true,
2286
+ preferences.friend_request_declined !== undefined ? preferences.friend_request_declined : true,
2287
+ preferences.referral !== undefined ? preferences.referral : true,
2288
+ preferences.mention !== undefined ? preferences.mention : true,
2289
+ preferences.friend_message !== undefined ? preferences.friend_message : true,
2290
+ preferences.game_joined !== undefined ? preferences.game_joined : true,
2291
+ preferences.game_invite !== undefined ? preferences.game_invite : true
2292
+ ]
2293
+ );
2294
+
2295
+ res.json({
2296
+ success: true,
2297
+ message: 'Push notification preferences updated'
2298
+ });
2299
+ } catch (error) {
2300
+ console.error('[Auth] Error updating push notification preferences:', error);
2301
+ res.status(500).json({
2302
+ success: false,
2303
+ error: error.message
2304
+ });
2305
+ }
2306
+ });
2307
+
2308
+ return router;
2309
+ };
2310
+