From 5bed125cf6f90289fabda15cc91445e20438c3b8 Mon Sep 17 00:00:00 2001 From: Carsten Graf Date: Sun, 7 Sep 2025 16:54:09 +0200 Subject: [PATCH] Achivements abends um 19 uhr --- package-lock.json | 150 ++++++++ package.json | 3 + public/404.html | 336 +++++++++++++++++ public/css/login.css | 35 ++ public/js/login.js | 29 ++ public/login.html | 9 +- routes/api.js | 44 +-- scripts/best_time_achievements.js | 198 ++++++++++ scripts/setup_cron.js | 84 +++-- server.js | 595 +++++++++++++++--------------- 10 files changed, 1136 insertions(+), 347 deletions(-) create mode 100644 public/404.html create mode 100644 scripts/best_time_achievements.js diff --git a/package-lock.json b/package-lock.json index 6400254..906ce69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,14 @@ "dependencies": { "@hisma/server-puppeteer": "^0.6.5", "bcrypt": "^5.1.1", + "discord-oauth2": "^2.12.1", "dotenv": "^16.3.1", "enhanced-postgres-mcp-server": "^1.0.1", "express": "^4.18.2", "express-session": "^1.17.3", "node-cron": "^4.2.1", + "passport": "^0.7.0", + "passport-discord": "^0.1.4", "pg": "^8.11.3", "socket.io": "^4.8.1", "swagger-jsdoc": "^6.2.8", @@ -832,6 +835,15 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/basic-ftp": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", @@ -1243,6 +1255,12 @@ "integrity": "sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA==", "license": "BSD-3-Clause" }, + "node_modules/discord-oauth2": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/discord-oauth2/-/discord-oauth2-2.12.1.tgz", + "integrity": "sha512-/Um39bRxVjcGHUu1YaTLangZvZveXjsX4BNsa1Iyd6OQG0jL972IBQGKD0mYqQswxC3bT+hqWSouabfI2RdaZA==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2648,6 +2666,12 @@ "set-blocking": "^2.0.0" } }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2819,6 +2843,61 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-discord": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/passport-discord/-/passport-discord-0.1.4.tgz", + "integrity": "sha512-VJWPYqSOmh7SaCLw/C+k1ZqCzJnn2frrmQRx1YrcPJ3MQ+Oa31XclbbmqFICSvl8xv3Fqd6YWQ4H4p1MpIN9rA==", + "license": "ISC", + "dependencies": { + "passport-oauth2": "^1.5.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -2842,6 +2921,11 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -4010,6 +4094,12 @@ "node": ">= 0.8" } }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -4909,6 +4999,11 @@ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" + }, "basic-ftp": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", @@ -5188,6 +5283,11 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1475386.tgz", "integrity": "sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA==" }, + "discord-oauth2": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/discord-oauth2/-/discord-oauth2-2.12.1.tgz", + "integrity": "sha512-/Um39bRxVjcGHUu1YaTLangZvZveXjsX4BNsa1Iyd6OQG0jL972IBQGKD0mYqQswxC3bT+hqWSouabfI2RdaZA==" + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -6134,6 +6234,11 @@ "set-blocking": "^2.0.0" } }, + "oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==" + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6248,6 +6353,41 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + } + }, + "passport-discord": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/passport-discord/-/passport-discord-0.1.4.tgz", + "integrity": "sha512-VJWPYqSOmh7SaCLw/C+k1ZqCzJnn2frrmQRx1YrcPJ3MQ+Oa31XclbbmqFICSvl8xv3Fqd6YWQ4H4p1MpIN9rA==", + "requires": { + "passport-oauth2": "^1.5.0" + } + }, + "passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "requires": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -6263,6 +6403,11 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -7062,6 +7207,11 @@ "random-bytes": "~1.0.0" } }, + "uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" + }, "undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", diff --git a/package.json b/package.json index b03a2c2..e67b8f9 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,14 @@ "dependencies": { "@hisma/server-puppeteer": "^0.6.5", "bcrypt": "^5.1.1", + "discord-oauth2": "^2.12.1", "dotenv": "^16.3.1", "enhanced-postgres-mcp-server": "^1.0.1", "express": "^4.18.2", "express-session": "^1.17.3", "node-cron": "^4.2.1", + "passport": "^0.7.0", + "passport-discord": "^0.1.4", "pg": "^8.11.3", "socket.io": "^4.8.1", "swagger-jsdoc": "^6.2.8", diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..8de5af0 --- /dev/null +++ b/public/404.html @@ -0,0 +1,336 @@ + + + + + + 404 - Seite nicht gefunden | NinjaCross + + + + + +
+
+
+
+
+
+
+
+
+
+
+ +
+ +
🥷
+ + +
404
+ + +

Oops! Diese Seite ist im Ninja-Modus verschwunden!

+ + +

+ Die Seite, die du suchst, hat sich wie ein echter Ninja versteckt.
+ Vielleicht ist sie auf einer geheimen Mission oder hat sich in der Dunkelheit versteckt. +

+ + + +
+ + + + diff --git a/public/css/login.css b/public/css/login.css index ce72ef2..69cb817 100644 --- a/public/css/login.css +++ b/public/css/login.css @@ -354,6 +354,41 @@ body { flex-shrink: 0; } +.btn-discord { + width: 100%; + background: #5865F2; + color: white; + border: 1px solid #4752C4; + padding: 12px 16px; + border-radius: 8px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + font-size: 0.95rem; + box-shadow: 0 1px 3px rgba(88, 101, 242, 0.3); + margin-top: 10px; +} + +.btn-discord:hover { + background: #4752C4; + border-color: #3C45A5; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(88, 101, 242, 0.4); +} + +.btn-discord:active { + transform: translateY(0); + box-shadow: 0 1px 3px rgba(88, 101, 242, 0.3); +} + +.btn-discord svg { + flex-shrink: 0; +} + /* Divider */ .divider { position: relative; diff --git a/public/js/login.js b/public/js/login.js index 44e1487..2c28356 100644 --- a/public/js/login.js +++ b/public/js/login.js @@ -39,6 +39,32 @@ async function signInWithGoogle() { } } +// Discord OAuth Sign In +async function signInWithDiscord() { + try { + setLoading(true); + clearMessage(); + + const { data, error } = await supabase.auth.signInWithOAuth({ + provider: 'discord', + options: { + redirectTo: `${window.location.origin}/auth/callback` + } + }); + + if (error) { + console.error('Discord OAuth error:', error); + showMessage('Fehler bei der Discord-Anmeldung: ' + error.message, 'error'); + } + // Note: OAuth redirects the page, so we don't need to handle success here + } catch (error) { + console.error('Discord OAuth error:', error); + showMessage('Fehler bei der Discord-Anmeldung: ' + error.message, 'error'); + } finally { + setLoading(false); + } +} + // Toggle between login and register forms function toggleForm() { const loginForm = document.getElementById('loginForm'); @@ -94,6 +120,9 @@ function setupEventListeners() { // Handle Google OAuth document.getElementById('googleSignInBtn').addEventListener('click', signInWithGoogle); + // Handle Discord OAuth + document.getElementById('discordSignInBtn').addEventListener('click', signInWithDiscord); + // Cookie settings button const cookieSettingsBtn = document.getElementById('cookie-settings-footer'); if (cookieSettingsBtn) { diff --git a/public/login.html b/public/login.html index b34bfef..421c3d9 100644 --- a/public/login.html +++ b/public/login.html @@ -28,7 +28,7 @@

Welcome Back

- +
+ +
diff --git a/routes/api.js b/routes/api.js index 7eee924..491c249 100644 --- a/routes/api.js +++ b/routes/api.js @@ -890,7 +890,7 @@ router.post('/v1/private/create-time', requireApiKey, async (req, res) => { // Achievement-Überprüfung nach Zeit-Eingabe try { - await pool.query('SELECT check_all_achievements($1)', [player_id]); + await pool.query('SELECT check_immediate_achievements($1)', [player_id]); console.log(`✅ Achievement-Check für Spieler ${player_id} ausgeführt`); } catch (achievementError) { console.error('Fehler bei Achievement-Check:', achievementError); @@ -1027,25 +1027,6 @@ router.post('/v1/private/users/find', requireApiKey, async (req, res) => { // RFID LINKING & USER MANAGEMENT ENDPOINTS (No API Key required for dashboard) // ============================================================================ -// Get all players for RFID linking (no auth required for dashboard) -router.get('/v1/public/players', async (req, res) => { - try { - const result = await pool.query( - `SELECT id, firstname, lastname, birthdate, rfiduid, created_at - FROM players - ORDER BY created_at DESC` - ); - - res.json(result.rows); - - } catch (error) { - console.error('Fehler beim Abrufen der Spieler:', error); - res.status(500).json({ - success: false, - message: 'Fehler beim Abrufen der Spieler' - }); - } -}); // Create new player with optional Supabase user linking (no auth required for dashboard) router.post('/v1/public/players', async (req, res) => { @@ -2180,7 +2161,7 @@ router.post('/v1/admin/runs', requireAdminAuth, async (req, res) => { // Achievement-Überprüfung nach Zeit-Eingabe try { - await pool.query('SELECT check_all_achievements($1)', [player_id]); + await pool.query('SELECT check_immediate_achievements($1)', [player_id]); console.log(`✅ Achievement-Check für Spieler ${player_id} ausgeführt`); } catch (achievementError) { console.error('Fehler bei Achievement-Check:', achievementError); @@ -2786,6 +2767,27 @@ router.post('/achievements/daily-check', async (req, res) => { } }); +// Run best-time achievement check (for manual testing or cron jobs) +router.post('/achievements/best-time-check', async (req, res) => { + try { + // This endpoint runs the best-time achievement check + const { runBestTimeAchievements } = require('../scripts/best_time_achievements'); + + await runBestTimeAchievements(); + + res.json({ + success: true, + message: 'Best-Time Achievement-Überprüfung abgeschlossen' + }); + } catch (error) { + console.error('Error running best-time achievement check:', error); + res.status(500).json({ + success: false, + message: 'Fehler bei der Best-Time Achievement-Überprüfung' + }); + } +}); + // Get achievement leaderboard router.get('/achievements/leaderboard', async (req, res) => { try { diff --git a/scripts/best_time_achievements.js b/scripts/best_time_achievements.js new file mode 100644 index 0000000..8bcef1e --- /dev/null +++ b/scripts/best_time_achievements.js @@ -0,0 +1,198 @@ +const { Pool } = require('pg'); +require('dotenv').config(); + +const pool = new Pool({ + host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || 5432, + database: process.env.DB_NAME || 'ninjacross', + user: process.env.DB_USER || '', + password: process.env.DB_PASSWORD || '', + ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false +}); + +async function runBestTimeAchievements() { + const client = await pool.connect(); + + try { + console.log('🏆 Starting best-time achievement check at 19:00...'); + + const currentHour = new Date().getHours(); + const currentDay = new Date().getDay(); // 0 = Sunday + const currentDate = new Date(); + const isLastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate() === currentDate.getDate(); + + console.log(`Current time: ${currentHour}:00`); + console.log(`Is Sunday: ${currentDay === 0}`); + console.log(`Is last day of month: ${isLastDayOfMonth}`); + + // Get all players who have played + const playersResult = await client.query(` + SELECT DISTINCT p.id, p.firstname, p.lastname + FROM players p + INNER JOIN times t ON p.id = t.player_id + `); + + console.log(`Found ${playersResult.rows.length} players with times`); + + let dailyAwards = 0; + let weeklyAwards = 0; + let monthlyAwards = 0; + + // Check best-time achievements for each player + for (const player of playersResult.rows) { + console.log(`Checking best-time achievements for ${player.firstname} ${player.lastname}...`); + + // Run best-time achievement check function + await client.query('SELECT check_best_time_achievements_timed($1)', [player.id]); + + // Check if new daily achievement was earned today + const dailyResult = await client.query(` + SELECT COUNT(*) as count + FROM player_achievements pa + INNER JOIN achievements a ON pa.achievement_id = a.id + WHERE pa.player_id = $1 + AND a.category = 'best_time' + AND a.condition_type = 'daily_best' + AND pa.is_completed = true + AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE + `, [player.id]); + + if (parseInt(dailyResult.rows[0].count) > 0) { + dailyAwards++; + console.log(` 🥇 Daily best achievement earned!`); + } + + // Check if new weekly achievement was earned (only on Sunday) + if (currentDay === 0) { + const weeklyResult = await client.query(` + SELECT COUNT(*) as count + FROM player_achievements pa + INNER JOIN achievements a ON pa.achievement_id = a.id + WHERE pa.player_id = $1 + AND a.category = 'best_time' + AND a.condition_type = 'weekly_best' + AND pa.is_completed = true + AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE + `, [player.id]); + + if (parseInt(weeklyResult.rows[0].count) > 0) { + weeklyAwards++; + console.log(` 🏆 Weekly best achievement earned!`); + } + } + + // Check if new monthly achievement was earned (only on last day of month) + if (isLastDayOfMonth) { + const monthlyResult = await client.query(` + SELECT COUNT(*) as count + FROM player_achievements pa + INNER JOIN achievements a ON pa.achievement_id = a.id + WHERE pa.player_id = $1 + AND a.category = 'best_time' + AND a.condition_type = 'monthly_best' + AND pa.is_completed = true + AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE + `, [player.id]); + + if (parseInt(monthlyResult.rows[0].count) > 0) { + monthlyAwards++; + console.log(` 👑 Monthly best achievement earned!`); + } + } + } + + console.log(`\n🎉 Best-time achievement check completed!`); + console.log(`Daily awards: ${dailyAwards}`); + console.log(`Weekly awards: ${weeklyAwards}`); + console.log(`Monthly awards: ${monthlyAwards}`); + + // Get current best times for today + const bestTimesResult = await client.query(` + SELECT + 'daily' as period, + p.firstname || ' ' || p.lastname as player_name, + MIN(t.recorded_time) as best_time + FROM times t + INNER JOIN players p ON t.player_id = p.id + WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE + GROUP BY p.id, p.firstname, p.lastname + ORDER BY MIN(t.recorded_time) ASC + LIMIT 1 + `); + + if (bestTimesResult.rows.length > 0) { + const dailyBest = bestTimesResult.rows[0]; + console.log(`\n🥇 Today's best time: ${dailyBest.player_name} - ${dailyBest.best_time}`); + } + + // Get current best times for this week (if Sunday) + if (currentDay === 0) { + const weekStart = new Date(); + weekStart.setDate(weekStart.getDate() - weekStart.getDay()); + + const weeklyBestResult = await client.query(` + SELECT + 'weekly' as period, + p.firstname || ' ' || p.lastname as player_name, + MIN(t.recorded_time) as best_time + FROM times t + INNER JOIN players p ON t.player_id = p.id + WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1 + AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= CURRENT_DATE + GROUP BY p.id, p.firstname, p.lastname + ORDER BY MIN(t.recorded_time) ASC + LIMIT 1 + `, [weekStart.toISOString().split('T')[0]]); + + if (weeklyBestResult.rows.length > 0) { + const weeklyBest = weeklyBestResult.rows[0]; + console.log(`🏆 This week's best time: ${weeklyBest.player_name} - ${weeklyBest.best_time}`); + } + } + + // Get current best times for this month (if last day of month) + if (isLastDayOfMonth) { + const monthStart = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1); + + const monthlyBestResult = await client.query(` + SELECT + 'monthly' as period, + p.firstname || ' ' || p.lastname as player_name, + MIN(t.recorded_time) as best_time + FROM times t + INNER JOIN players p ON t.player_id = p.id + WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1 + AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= CURRENT_DATE + GROUP BY p.id, p.firstname, p.lastname + ORDER BY MIN(t.recorded_time) ASC + LIMIT 1 + `, [monthStart.toISOString().split('T')[0]]); + + if (monthlyBestResult.rows.length > 0) { + const monthlyBest = monthlyBestResult.rows[0]; + console.log(`👑 This month's best time: ${monthlyBest.player_name} - ${monthlyBest.best_time}`); + } + } + + } catch (error) { + console.error('❌ Error running best-time achievements:', error); + throw error; + } finally { + client.release(); + } +} + +// Run if called directly +if (require.main === module) { + runBestTimeAchievements() + .then(() => { + console.log('✅ Best-time achievements script completed successfully'); + process.exit(0); + }) + .catch((error) => { + console.error('❌ Best-time achievements script failed:', error); + process.exit(1); + }); +} + +module.exports = { runBestTimeAchievements }; diff --git a/scripts/setup_cron.js b/scripts/setup_cron.js index 400eb80..3f23e78 100644 --- a/scripts/setup_cron.js +++ b/scripts/setup_cron.js @@ -1,24 +1,48 @@ const { exec } = require('child_process'); const path = require('path'); -// Cron job setup for daily achievements -const cronJob = { - // Run daily at 23:59 (end of day) - schedule: '59 23 * * *', - command: `cd ${__dirname} && node daily_achievements.js >> /var/log/ninjaserver_achievements.log 2>&1`, - description: 'Daily achievement check for Ninja Cross Parkour' -}; +// Cron job setup for achievements +const cronJobs = [ + { + name: 'daily_achievements', + // Run daily at 19:00 for best-time achievements + schedule: '0 19 * * *', + command: `cd ${__dirname} && node best_time_achievements.js >> /var/log/ninjaserver_achievements.log 2>&1`, + description: 'Daily best-time achievement check at 19:00' + }, + { + name: 'weekly_achievements', + // Run every Sunday at 19:00 for weekly best-time achievements + schedule: '0 19 * * 0', + command: `cd ${__dirname} && node best_time_achievements.js >> /var/log/ninjaserver_achievements.log 2>&1`, + description: 'Weekly best-time achievement check on Sunday at 19:00' + }, + { + name: 'monthly_achievements', + // Run on last day of month at 19:00 for monthly best-time achievements + schedule: '0 19 28-31 * * [ $(date -d tomorrow +\\%d) -eq 1 ]', + command: `cd ${__dirname} && node best_time_achievements.js >> /var/log/ninjaserver_achievements.log 2>&1`, + description: 'Monthly best-time achievement check on last day of month at 19:00' + } +]; -function setupCronJob() { - console.log('🕐 Setting up daily achievement cron job...'); +function setupCronJobs() { + console.log('🕐 Setting up best-time achievement cron jobs...'); - // Create cron job entry - const cronEntry = `${cronJob.schedule} ${cronJob.command}`; + let cronEntries = []; - // Add to crontab - exec(`(crontab -l 2>/dev/null; echo "${cronEntry}") | crontab -`, (error, stdout, stderr) => { + // Create cron job entries + cronJobs.forEach(job => { + const cronEntry = `${job.schedule} ${job.command}`; + cronEntries.push(cronEntry); + console.log(`📅 ${job.name}: ${job.schedule} - ${job.description}`); + }); + + // Add all cron jobs to crontab + const allCronEntries = cronEntries.join('\n'); + exec(`(crontab -l 2>/dev/null; echo "${allCronEntries}") | crontab -`, (error, stdout, stderr) => { if (error) { - console.error('❌ Error setting up cron job:', error); + console.error('❌ Error setting up cron jobs:', error); return; } @@ -26,9 +50,7 @@ function setupCronJob() { console.error('⚠️ Cron job warning:', stderr); } - console.log('✅ Cron job setup successfully!'); - console.log(`📅 Schedule: ${cronJob.schedule}`); - console.log(`🔧 Command: ${cronJob.command}`); + console.log('✅ All cron jobs setup successfully!'); console.log('📝 Logs will be written to: /var/log/ninjaserver_achievements.log'); // Show current crontab @@ -41,16 +63,16 @@ function setupCronJob() { }); } -function removeCronJob() { - console.log('🗑️ Removing daily achievement cron job...'); +function removeCronJobs() { + console.log('🗑️ Removing best-time achievement cron jobs...'); - exec('crontab -l | grep -v "daily_achievements.js" | crontab -', (error, stdout, stderr) => { + exec('crontab -l | grep -v "best_time_achievements.js" | crontab -', (error, stdout, stderr) => { if (error) { - console.error('❌ Error removing cron job:', error); + console.error('❌ Error removing cron jobs:', error); return; } - console.log('✅ Cron job removed successfully!'); + console.log('✅ All cron jobs removed successfully!'); }); } @@ -60,27 +82,27 @@ if (require.main === module) { switch (command) { case 'setup': - setupCronJob(); + setupCronJobs(); break; case 'remove': - removeCronJob(); + removeCronJobs(); break; case 'status': - exec('crontab -l | grep daily_achievements', (error, stdout, stderr) => { + exec('crontab -l | grep best_time_achievements', (error, stdout, stderr) => { if (stdout) { - console.log('✅ Cron job is active:'); + console.log('✅ Best-time achievement cron jobs are active:'); console.log(stdout); } else { - console.log('❌ No cron job found'); + console.log('❌ No best-time achievement cron jobs found'); } }); break; default: console.log('Usage: node setup_cron.js [setup|remove|status]'); - console.log(' setup - Add daily achievement cron job'); - console.log(' remove - Remove daily achievement cron job'); - console.log(' status - Check if cron job is active'); + console.log(' setup - Add best-time achievement cron jobs (daily 19:00, Sunday 19:00, last day of month 19:00)'); + console.log(' remove - Remove all best-time achievement cron jobs'); + console.log(' status - Check if best-time achievement cron jobs are active'); } } -module.exports = { setupCronJob, removeCronJob }; +module.exports = { setupCronJobs, removeCronJobs }; diff --git a/server.js b/server.js index c48fac4..090ac81 100644 --- a/server.js +++ b/server.js @@ -1,295 +1,302 @@ -/** - * NinjaCross Leaderboard Server - * - * Hauptserver für das NinjaCross Timer-System mit: - * - Express.js Web-Server - * - Socket.IO für Real-time Updates - * - PostgreSQL Datenbankanbindung - * - API-Key Authentifizierung - * - Session-basierte Web-Authentifizierung - * - * @author NinjaCross Team - * @version 1.0.0 - */ - -// ============================================================================ -// DEPENDENCIES & IMPORTS -// ============================================================================ - -const express = require('express'); -const path = require('path'); -const session = require('express-session'); -const { createServer } = require('http'); -const { Server } = require('socket.io'); -const swaggerUi = require('swagger-ui-express'); -const swaggerSpecs = require('./swagger'); -require('dotenv').config(); - -// Route Imports -const { router: apiRoutes, requireApiKey } = require('./routes/api'); - -// ============================================================================ -// SERVER CONFIGURATION -// ============================================================================ - -const app = express(); -const server = createServer(app); -const port = process.env.PORT || 3000; - -// Socket.IO Configuration -const io = new Server(server, { - cors: { - origin: "*", - methods: ["GET", "POST"] - } -}); - -// ============================================================================ -// MIDDLEWARE SETUP -// ============================================================================ - -// Body Parser Middleware -app.use(express.json({ limit: '10mb' })); -app.use(express.urlencoded({ extended: true, limit: '10mb' })); - -// Session Configuration -app.use(session({ - secret: process.env.SESSION_SECRET || 'kjhdizr3lhwho8fpjslgf825ß0hsd', - resave: false, - saveUninitialized: false, - cookie: { - secure: false, // Set to true when using HTTPS - maxAge: 24 * 60 * 60 * 1000, // 24 hours - httpOnly: true // Security: prevent XSS attacks - } -})); - -// ============================================================================ -// AUTHENTICATION MIDDLEWARE -// ============================================================================ - -/** - * Web Interface Authentication Middleware - * Überprüft ob der Benutzer für das Web-Interface authentifiziert ist - */ -function requireWebAuth(req, res, next) { - if (req.session.userId) { - next(); - } else { - res.redirect('/login'); - } -} - -// ============================================================================ -// ROUTE SETUP -// ============================================================================ - -// Swagger API Documentation -app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs, { - customCss: '.swagger-ui .topbar { display: none }', - customSiteTitle: 'Ninja Cross Parkour API Documentation' -})); - -// Unified API Routes (all under /api/v1/) -// - /api/v1/public/* - Public routes (no authentication) -// - /api/v1/private/* - API-Key protected routes -// - /api/v1/web/* - Session protected routes -// - /api/v1/admin/* - Admin protected routes -app.use('/api', apiRoutes); - -// ============================================================================ -// WEB INTERFACE ROUTES -// ============================================================================ - -/** - * Public Landing Page - NinjaCross Leaderboard - * Hauptseite mit dem öffentlichen Leaderboard - */ -app.get('/', (req, res) => { - res.sendFile(path.join(__dirname, 'public', 'index.html')); -}); - -/** - * Admin Dashboard Page - * Hauptdashboard für Admin-Benutzer - */ -app.get('/admin-dashboard', (req, res) => { - res.sendFile(path.join(__dirname, 'public', 'admin-dashboard.html')); -}); - -/** - * Admin Generator Page - * Geschützte Seite für die Lizenz-Generierung (Level 2 Zugriff erforderlich) - */ -app.get('/generator', requireWebAuth, (req, res) => { - // Prüfe Zugriffslevel für Generator - if (req.session.accessLevel < 2) { - return res.status(403).send(` -

Zugriff verweigert

-

Sie benötigen Level 2 Zugriff für den Lizenzgenerator.

- Zurück zum Dashboard - `); - } - res.sendFile(path.join(__dirname, 'public', 'generator.html')); -}); - -/** - * Login Page - * Authentifizierungsseite für Admin-Benutzer - */ -app.get('/login', (req, res) => { - // Redirect to main page if already authenticated - if (req.session.userId) { - return res.redirect('/'); - } - res.sendFile(path.join(__dirname, 'public', 'login.html')); -}); - -/** - * Admin Dashboard Page - * Dashboard-Seite für eingeloggte Administratoren - * Authentifizierung wird client-side über Supabase gehandhabt - */ -app.get('/dashboard', (req, res) => { - res.sendFile(path.join(__dirname, 'public', 'dashboard.html')); -}); - -/** - * Reset Password Page - * Seite für das Zurücksetzen von Passwörtern über Supabase - * Wird von Supabase E-Mail-Links aufgerufen - */ -app.get('/reset-password.html', (req, res) => { - res.sendFile(path.join(__dirname, 'public', 'reset-password.html')); -}); - -/** - * Admin Login Page - * Lizenzgenerator Login-Seite für Admin-Benutzer - */ -app.get('/adminlogin.html', (req, res) => { - res.sendFile(path.join(__dirname, 'public', 'adminlogin.html')); -}); - -/** - * OAuth Callback Route - * Handles OAuth redirects from Supabase (Google, etc.) - */ -app.get('/auth/callback', (req, res) => { - // Redirect to the main page after OAuth callback - // Supabase handles the OAuth flow and redirects here - res.redirect('/'); -}); - -// ============================================================================ -// STATIC FILE SERVING -// ============================================================================ - -// Serve static files directly from public directory -app.use(express.static('public')); - -// Serve static files for public pages (CSS, JS, images) - legacy route -app.use('/public', express.static('public')); - -// Serve static files for login page -app.use('/login', express.static('public')); - -// ============================================================================ -// WEBSOCKET CONFIGURATION -// ============================================================================ - -/** - * WebSocket Connection Handler - * Verwaltet Real-time Verbindungen für Live-Updates - */ -io.on('connection', (socket) => { - // Client connected - connection is established - - socket.on('disconnect', () => { - // Client disconnected - cleanup if needed - }); -}); - -// Make Socket.IO instance available to other modules -app.set('io', io); - -// ============================================================================ -// ERROR HANDLING -// ============================================================================ - -// 404 Handler -app.use('*', (req, res) => { - res.status(404).json({ - success: false, - message: 'Route not found', - path: req.originalUrl - }); -}); - -// Global Error Handler -app.use((err, req, res, next) => { - console.error('Server Error:', err); - res.status(500).json({ - success: false, - message: 'Internal server error' - }); -}); - -// ============================================================================ -// SERVER STARTUP -// ============================================================================ - -/** - * Start the server and initialize all services - */ -server.listen(port, () => { - console.log(`🚀 Server läuft auf http://ninja.reptilfpv.de:${port}`); - console.log(`📊 Datenbank: ${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`); - console.log(`🔐 API-Key Authentifizierung aktiviert`); - console.log(`🔌 WebSocket-Server aktiviert`); - console.log(`📁 Static files: /public`); - console.log(`🌐 Unified API: /api/v1/`); - console.log(` 📖 Public: /api/v1/public/`); - console.log(` 🔒 Private: /api/v1/private/`); - console.log(` 🔐 Web: /api/v1/web/`); - console.log(` 👑 Admin: /api/v1/admin/`); -}); - -// ============================================================================ -// GRACEFUL SHUTDOWN -// ============================================================================ - -/** - * Handle graceful shutdown on SIGINT (Ctrl+C) - */ -process.on('SIGINT', async () => { - console.log('\n🛑 Server wird heruntergefahren...'); - - // Close server gracefully - server.close(() => { - console.log('✅ Server erfolgreich heruntergefahren'); - process.exit(0); - }); - - // Force exit after 5 seconds if graceful shutdown fails - setTimeout(() => { - console.log('⚠️ Forced shutdown after timeout'); - process.exit(1); - }, 5000); -}); - -/** - * Handle uncaught exceptions - */ -process.on('uncaughtException', (err) => { - console.error('Uncaught Exception:', err); - process.exit(1); -}); - -/** - * Handle unhandled promise rejections - */ -process.on('unhandledRejection', (reason, promise) => { - console.error('Unhandled Rejection at:', promise, 'reason:', reason); - process.exit(1); +/** + * NinjaCross Leaderboard Server + * + * Hauptserver für das NinjaCross Timer-System mit: + * - Express.js Web-Server + * - Socket.IO für Real-time Updates + * - PostgreSQL Datenbankanbindung + * - API-Key Authentifizierung + * - Session-basierte Web-Authentifizierung + * + * @author NinjaCross Team + * @version 1.0.0 + */ + +// ============================================================================ +// DEPENDENCIES & IMPORTS +// ============================================================================ + +const express = require('express'); +const path = require('path'); +const session = require('express-session'); +const { createServer } = require('http'); +const { Server } = require('socket.io'); +const swaggerUi = require('swagger-ui-express'); +const swaggerSpecs = require('./swagger'); +require('dotenv').config(); + +// Route Imports +const { router: apiRoutes, requireApiKey } = require('./routes/api'); + +// ============================================================================ +// SERVER CONFIGURATION +// ============================================================================ + +const app = express(); +const server = createServer(app); +const port = process.env.PORT || 3000; + +// Socket.IO Configuration +const io = new Server(server, { + cors: { + origin: "*", + methods: ["GET", "POST"] + } +}); + +// ============================================================================ +// MIDDLEWARE SETUP +// ============================================================================ + +// Body Parser Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); + +// Session Configuration +app.use(session({ + secret: process.env.SESSION_SECRET || 'kjhdizr3lhwho8fpjslgf825ß0hsd', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true when using HTTPS + maxAge: 24 * 60 * 60 * 1000, // 24 hours + httpOnly: true // Security: prevent XSS attacks + } +})); + + +// ============================================================================ +// AUTHENTICATION MIDDLEWARE +// ============================================================================ + +/** + * Web Interface Authentication Middleware + * Überprüft ob der Benutzer für das Web-Interface authentifiziert ist + */ +function requireWebAuth(req, res, next) { + if (req.session.userId) { + next(); + } else { + res.redirect('/login'); + } +} + +// ============================================================================ +// ROUTE SETUP +// ============================================================================ + +// Swagger API Documentation +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs, { + customCss: '.swagger-ui .topbar { display: none }', + customSiteTitle: 'Ninja Cross Parkour API Documentation' +})); + +// Unified API Routes (all under /api/v1/) +// - /api/v1/public/* - Public routes (no authentication) +// - /api/v1/private/* - API-Key protected routes +// - /api/v1/web/* - Session protected routes +// - /api/v1/admin/* - Admin protected routes +app.use('/api', apiRoutes); + +// ============================================================================ +// WEB INTERFACE ROUTES +// ============================================================================ + +/** + * Public Landing Page - NinjaCross Leaderboard + * Hauptseite mit dem öffentlichen Leaderboard + */ +app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'index.html')); +}); + +/** + * Admin Dashboard Page + * Hauptdashboard für Admin-Benutzer + */ +app.get('/admin-dashboard', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'admin-dashboard.html')); +}); + +/** + * Admin Generator Page + * Geschützte Seite für die Lizenz-Generierung (Level 2 Zugriff erforderlich) + */ +app.get('/generator', requireWebAuth, (req, res) => { + // Prüfe Zugriffslevel für Generator + if (req.session.accessLevel < 2) { + return res.status(403).send(` +

Zugriff verweigert

+

Sie benötigen Level 2 Zugriff für den Lizenzgenerator.

+ Zurück zum Dashboard + `); + } + res.sendFile(path.join(__dirname, 'public', 'generator.html')); +}); + +/** + * Login Page + * Authentifizierungsseite für Admin-Benutzer + */ +app.get('/login', (req, res) => { + // Redirect to main page if already authenticated + if (req.session.userId) { + return res.redirect('/'); + } + res.sendFile(path.join(__dirname, 'public', 'login.html')); +}); + +/** + * Admin Dashboard Page + * Dashboard-Seite für eingeloggte Administratoren + * Authentifizierung wird client-side über Supabase gehandhabt + */ +app.get('/dashboard', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'dashboard.html')); +}); + +/** + * Reset Password Page + * Seite für das Zurücksetzen von Passwörtern über Supabase + * Wird von Supabase E-Mail-Links aufgerufen + */ +app.get('/reset-password.html', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'reset-password.html')); +}); + +/** + * Admin Login Page + * Lizenzgenerator Login-Seite für Admin-Benutzer + */ +app.get('/adminlogin.html', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'adminlogin.html')); +}); + +/** + * OAuth Callback Route + * Handles OAuth redirects from Supabase (Google, etc.) + */ +app.get('/auth/callback', (req, res) => { + // Redirect to the main page after OAuth callback + // Supabase handles the OAuth flow and redirects here + res.redirect('/'); +}); + +// ============================================================================ +// STATIC FILE SERVING +// ============================================================================ + +// Serve static files directly from public directory +app.use(express.static('public')); + +// Serve static files for public pages (CSS, JS, images) - legacy route +app.use('/public', express.static('public')); + +// Serve static files for login page +app.use('/login', express.static('public')); + +// ============================================================================ +// WEBSOCKET CONFIGURATION +// ============================================================================ + +/** + * WebSocket Connection Handler + * Verwaltet Real-time Verbindungen für Live-Updates + */ +io.on('connection', (socket) => { + // Client connected - connection is established + + socket.on('disconnect', () => { + // Client disconnected - cleanup if needed + }); +}); + +// Make Socket.IO instance available to other modules +app.set('io', io); + +// ============================================================================ +// ERROR HANDLING +// ============================================================================ + +// 404 Handler +app.use('*', (req, res) => { + // Check if it's an API request + if (req.originalUrl.startsWith('/api/')) { + res.status(404).json({ + success: false, + message: 'Route not found', + path: req.originalUrl + }); + } else { + // Serve custom 404 page for non-API requests + res.status(404).sendFile(path.join(__dirname, 'public', '404.html')); + } +}); + +// Global Error Handler +app.use((err, req, res, next) => { + console.error('Server Error:', err); + res.status(500).json({ + success: false, + message: 'Internal server error' + }); +}); + +// ============================================================================ +// SERVER STARTUP +// ============================================================================ + +/** + * Start the server and initialize all services + */ +server.listen(port, () => { + console.log(`🚀 Server läuft auf http://ninja.reptilfpv.de:${port}`); + console.log(`📊 Datenbank: ${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`); + console.log(`🔐 API-Key Authentifizierung aktiviert`); + console.log(`🔌 WebSocket-Server aktiviert`); + console.log(`📁 Static files: /public`); + console.log(`🌐 Unified API: /api/v1/`); + console.log(` 📖 Public: /api/v1/public/`); + console.log(` 🔒 Private: /api/v1/private/`); + console.log(` 🔐 Web: /api/v1/web/`); + console.log(` 👑 Admin: /api/v1/admin/`); +}); + +// ============================================================================ +// GRACEFUL SHUTDOWN +// ============================================================================ + +/** + * Handle graceful shutdown on SIGINT (Ctrl+C) + */ +process.on('SIGINT', async () => { + console.log('\n🛑 Server wird heruntergefahren...'); + + // Close server gracefully + server.close(() => { + console.log('✅ Server erfolgreich heruntergefahren'); + process.exit(0); + }); + + // Force exit after 5 seconds if graceful shutdown fails + setTimeout(() => { + console.log('⚠️ Forced shutdown after timeout'); + process.exit(1); + }, 5000); +}); + +/** + * Handle uncaught exceptions + */ +process.on('uncaughtException', (err) => { + console.error('Uncaught Exception:', err); + process.exit(1); +}); + +/** + * Handle unhandled promise rejections + */ +process.on('unhandledRejection', (reason, promise) => { + console.error('Unhandled Rejection at:', promise, 'reason:', reason); + process.exit(1); }); \ No newline at end of file