Achivements abends um 19 uhr
This commit is contained in:
150
package-lock.json
generated
150
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
336
public/404.html
Normal file
336
public/404.html
Normal file
@@ -0,0 +1,336 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>404 - Seite nicht gefunden | NinjaCross</title>
|
||||
<link rel="icon" type="image/x-icon" href="/pictures/favicon.ico">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Animated background particles */
|
||||
.particles {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.particle {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.particle:nth-child(1) { width: 4px; height: 4px; left: 10%; animation-delay: 0s; }
|
||||
.particle:nth-child(2) { width: 6px; height: 6px; left: 20%; animation-delay: 1s; }
|
||||
.particle:nth-child(3) { width: 3px; height: 3px; left: 30%; animation-delay: 2s; }
|
||||
.particle:nth-child(4) { width: 5px; height: 5px; left: 40%; animation-delay: 3s; }
|
||||
.particle:nth-child(5) { width: 4px; height: 4px; left: 50%; animation-delay: 4s; }
|
||||
.particle:nth-child(6) { width: 7px; height: 7px; left: 60%; animation-delay: 5s; }
|
||||
.particle:nth-child(7) { width: 3px; height: 3px; left: 70%; animation-delay: 0.5s; }
|
||||
.particle:nth-child(8) { width: 5px; height: 5px; left: 80%; animation-delay: 1.5s; }
|
||||
.particle:nth-child(9) { width: 4px; height: 4px; left: 90%; animation-delay: 2.5s; }
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
|
||||
10% { opacity: 1; }
|
||||
90% { opacity: 1; }
|
||||
50% { transform: translateY(-10vh) rotate(180deg); }
|
||||
}
|
||||
|
||||
/* Main container */
|
||||
.container {
|
||||
text-align: center;
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
max-width: 600px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* Ninja emoji with animation */
|
||||
.ninja-emoji {
|
||||
font-size: 8rem;
|
||||
margin-bottom: 1rem;
|
||||
display: inline-block;
|
||||
animation: ninja-bounce 2s ease-in-out infinite;
|
||||
filter: drop-shadow(0 0 20px rgba(0, 255, 255, 0.5));
|
||||
}
|
||||
|
||||
@keyframes ninja-bounce {
|
||||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
||||
25% { transform: translateY(-20px) rotate(-5deg); }
|
||||
50% { transform: translateY(-10px) rotate(0deg); }
|
||||
75% { transform: translateY(-15px) rotate(5deg); }
|
||||
}
|
||||
|
||||
/* 404 number with glow effect */
|
||||
.error-code {
|
||||
font-size: 6rem;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(45deg, #00ffff, #ff00ff, #ffff00, #00ffff);
|
||||
background-size: 400% 400%;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
animation: gradient-shift 3s ease-in-out infinite;
|
||||
margin-bottom: 1rem;
|
||||
text-shadow: 0 0 30px rgba(0, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
@keyframes gradient-shift {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
/* Error message */
|
||||
.error-message {
|
||||
font-size: 1.5rem;
|
||||
color: #ffffff;
|
||||
margin-bottom: 2rem;
|
||||
opacity: 0;
|
||||
animation: fade-in-up 1s ease-out 0.5s forwards;
|
||||
}
|
||||
|
||||
@keyframes fade-in-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Description */
|
||||
.description {
|
||||
font-size: 1.1rem;
|
||||
color: #b0b0b0;
|
||||
margin-bottom: 3rem;
|
||||
line-height: 1.6;
|
||||
opacity: 0;
|
||||
animation: fade-in-up 1s ease-out 1s forwards;
|
||||
}
|
||||
|
||||
/* Action buttons */
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
opacity: 0;
|
||||
animation: fade-in-up 1s ease-out 1.5s forwards;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #00ffff, #0080ff);
|
||||
color: white;
|
||||
box-shadow: 0 4px 15px rgba(0, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: #00ffff;
|
||||
border: 2px solid #00ffff;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #00ffff;
|
||||
color: #1a1a2e;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Glitch effect for 404 */
|
||||
.glitch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.glitch::before,
|
||||
.glitch::after {
|
||||
content: '404';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(45deg, #00ffff, #ff00ff, #ffff00, #00ffff);
|
||||
background-size: 400% 400%;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.glitch::before {
|
||||
animation: glitch-1 0.5s infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.glitch::after {
|
||||
animation: glitch-2 0.5s infinite;
|
||||
z-index: -2;
|
||||
}
|
||||
|
||||
@keyframes glitch-1 {
|
||||
0%, 100% { transform: translate(0); }
|
||||
20% { transform: translate(-2px, 2px); }
|
||||
40% { transform: translate(-2px, -2px); }
|
||||
60% { transform: translate(2px, 2px); }
|
||||
80% { transform: translate(2px, -2px); }
|
||||
}
|
||||
|
||||
@keyframes glitch-2 {
|
||||
0%, 100% { transform: translate(0); }
|
||||
20% { transform: translate(2px, -2px); }
|
||||
40% { transform: translate(2px, 2px); }
|
||||
60% { transform: translate(-2px, -2px); }
|
||||
80% { transform: translate(-2px, 2px); }
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.ninja-emoji { font-size: 6rem; }
|
||||
.error-code { font-size: 4rem; }
|
||||
.error-message { font-size: 1.2rem; }
|
||||
.description { font-size: 1rem; }
|
||||
.actions { flex-direction: column; align-items: center; }
|
||||
.btn { width: 200px; }
|
||||
}
|
||||
|
||||
/* Loading animation for page load */
|
||||
.container {
|
||||
animation: page-load 1s ease-out;
|
||||
}
|
||||
|
||||
@keyframes page-load {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Animated background particles -->
|
||||
<div class="particles">
|
||||
<div class="particle"></div>
|
||||
<div class="particle"></div>
|
||||
<div class="particle"></div>
|
||||
<div class="particle"></div>
|
||||
<div class="particle"></div>
|
||||
<div class="particle"></div>
|
||||
<div class="particle"></div>
|
||||
<div class="particle"></div>
|
||||
<div class="particle"></div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- Animated ninja emoji -->
|
||||
<div class="ninja-emoji">🥷</div>
|
||||
|
||||
<!-- Glitchy 404 number -->
|
||||
<div class="error-code glitch">404</div>
|
||||
|
||||
<!-- Error message -->
|
||||
<h1 class="error-message">Oops! Diese Seite ist im Ninja-Modus verschwunden!</h1>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="description">
|
||||
Die Seite, die du suchst, hat sich wie ein echter Ninja versteckt.<br>
|
||||
Vielleicht ist sie auf einer geheimen Mission oder hat sich in der Dunkelheit versteckt.
|
||||
</p>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="actions">
|
||||
<a href="/" class="btn btn-primary">🏠 Zur Hauptseite</a>
|
||||
<a href="/dashboard.html" class="btn btn-secondary">📊 Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Add some interactive effects
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
const particles = document.querySelectorAll('.particle');
|
||||
const x = e.clientX / window.innerWidth;
|
||||
const y = e.clientY / window.innerHeight;
|
||||
|
||||
particles.forEach((particle, index) => {
|
||||
const speed = (index + 1) * 0.5;
|
||||
const xOffset = (x - 0.5) * speed * 20;
|
||||
const yOffset = (y - 0.5) * speed * 20;
|
||||
|
||||
particle.style.transform = `translate(${xOffset}px, ${yOffset}px)`;
|
||||
});
|
||||
});
|
||||
|
||||
// Add click effect on ninja emoji
|
||||
document.querySelector('.ninja-emoji').addEventListener('click', () => {
|
||||
const ninja = document.querySelector('.ninja-emoji');
|
||||
ninja.style.animation = 'none';
|
||||
ninja.style.transform = 'scale(1.2) rotate(360deg)';
|
||||
|
||||
setTimeout(() => {
|
||||
ninja.style.animation = 'ninja-bounce 2s ease-in-out infinite';
|
||||
ninja.style.transform = '';
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Add some console easter egg
|
||||
console.log(`
|
||||
🥷 NINJA 404 CONSOLE EASTER EGG 🥷
|
||||
|
||||
Du hast die geheime Konsole gefunden!
|
||||
Hier ist ein Ninja-Haiku für dich:
|
||||
|
||||
"Versteckte Seite
|
||||
Wie ein Ninja in der Nacht
|
||||
Kehrt bald zurück"
|
||||
|
||||
- Der NinjaCross Server
|
||||
`);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<!-- Login Form -->
|
||||
<div id="loginForm" class="form-container active">
|
||||
<h2 style="text-align: center; margin-bottom: 1.5rem; color: #e2e8f0; font-weight: 600;">Welcome Back</h2>
|
||||
<!-- Google OAuth Button -->
|
||||
<!-- OAuth Buttons -->
|
||||
<div class="oauth-container">
|
||||
<button type="button" id="googleSignInBtn" class="btn btn-google">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -39,6 +39,13 @@
|
||||
</svg>
|
||||
Continue with Google
|
||||
</button>
|
||||
|
||||
<button type="button" id="discordSignInBtn" class="btn btn-discord">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#5865F2" d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
|
||||
</svg>
|
||||
Continue with Discord
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="divider">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
198
scripts/best_time_achievements.js
Normal file
198
scripts/best_time_achievements.js
Normal file
@@ -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 };
|
||||
@@ -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 };
|
||||
|
||||
595
server.js
595
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(`
|
||||
<h1>Zugriff verweigert</h1>
|
||||
<p>Sie benötigen Level 2 Zugriff für den Lizenzgenerator.</p>
|
||||
<a href="/admin-dashboard">Zurück zum Dashboard</a>
|
||||
`);
|
||||
}
|
||||
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(`
|
||||
<h1>Zugriff verweigert</h1>
|
||||
<p>Sie benötigen Level 2 Zugriff für den Lizenzgenerator.</p>
|
||||
<a href="/admin-dashboard">Zurück zum Dashboard</a>
|
||||
`);
|
||||
}
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user