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