From eb1d7139429eef1bd1fdaa6d4c9503d8fd6c2123 Mon Sep 17 00:00:00 2001 From: Carsten Graf Date: Thu, 4 Sep 2025 14:31:17 +0200 Subject: [PATCH] Refactor ALL --- public/adminlogin.html | 282 +------ public/css/adminlogin.css | 191 +++++ public/css/dashboard.css | 684 +++++++++++++++++ public/css/generator.css | 386 ++++++++++ public/css/reset-password.css | 235 ++++++ public/dashboard.html | 1306 +-------------------------------- public/generator.html | 1049 +------------------------- public/js/adminlogin.js | 81 ++ public/js/dashboard.js | 577 +++++++++++++++ public/js/generator.js | 560 ++++++++++++++ public/js/reset-password.js | 190 +++++ public/reset-password.html | 431 +---------- 12 files changed, 2933 insertions(+), 3039 deletions(-) create mode 100644 public/css/adminlogin.css create mode 100644 public/css/dashboard.css create mode 100644 public/css/generator.css create mode 100644 public/css/reset-password.css create mode 100644 public/js/adminlogin.js create mode 100644 public/js/dashboard.js create mode 100644 public/js/generator.js create mode 100644 public/js/reset-password.js diff --git a/public/adminlogin.html b/public/adminlogin.html index 6fc8c04..87412a1 100644 --- a/public/adminlogin.html +++ b/public/adminlogin.html @@ -4,199 +4,7 @@ Login - Lizenzgenerator - +
@@ -226,90 +34,6 @@
- + - - - + \ No newline at end of file diff --git a/public/css/adminlogin.css b/public/css/adminlogin.css new file mode 100644 index 0000000..529ef00 --- /dev/null +++ b/public/css/adminlogin.css @@ -0,0 +1,191 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.login-container { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-radius: 20px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + padding: 40px; + max-width: 400px; + width: 100%; + position: relative; + overflow: hidden; +} + +.login-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #667eea, #764ba2, #f093fb, #f5576c); + background-size: 300% 100%; + animation: gradientShift 3s ease infinite; +} + +@keyframes gradientShift { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +h1 { + text-align: center; + color: #333; + margin-bottom: 30px; + font-size: 2em; + font-weight: 300; + letter-spacing: -1px; +} + +.form-group { + margin-bottom: 25px; + position: relative; +} + +label { + display: block; + margin-bottom: 8px; + color: #555; + font-weight: 500; + font-size: 0.95em; +} + +input { + width: 100%; + padding: 15px 20px; + border: 2px solid #e0e0e0; + border-radius: 12px; + font-size: 1em; + transition: all 0.3s ease; + background: #fafafa; + font-family: inherit; +} + +input:focus { + outline: none; + border-color: #667eea; + background: white; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); + transform: translateY(-2px); +} + +input:hover { + border-color: #ccc; +} + +.login-btn { + width: 100%; + padding: 18px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 12px; + font-size: 1.1em; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + margin-top: 10px; + position: relative; + overflow: hidden; +} + +.login-btn:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3); +} + +.login-btn:active { + transform: translateY(0); +} + +.login-btn:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; +} + +.error { + background: #ffebee; + color: #c62828; + padding: 15px; + border-radius: 8px; + margin-top: 15px; + border-left: 4px solid #f44336; + font-size: 0.9em; + opacity: 0; + transform: translateY(-10px); + transition: all 0.3s ease; +} + +.error.show { + opacity: 1; + transform: translateY(0); +} + +.success { + background: #e8f5e8; + color: #2e7d32; + padding: 15px; + border-radius: 8px; + margin-top: 15px; + border-left: 4px solid #4caf50; + font-size: 0.9em; + opacity: 0; + transform: translateY(-10px); + transition: all 0.3s ease; +} + +.success.show { + opacity: 1; + transform: translateY(0); +} + +.loading { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(255,255,255,.3); + border-radius: 50%; + border-top-color: #fff; + animation: spin 1s ease-in-out infinite; + margin-right: 10px; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.info-text { + text-align: center; + color: #666; + font-size: 0.85em; + margin-top: 20px; + line-height: 1.5; +} + +@media (max-width: 480px) { + .login-container { + padding: 30px 20px; + margin: 10px; + } + + h1 { + font-size: 1.6em; + } +} diff --git a/public/css/dashboard.css b/public/css/dashboard.css new file mode 100644 index 0000000..434dc96 --- /dev/null +++ b/public/css/dashboard.css @@ -0,0 +1,684 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', sans-serif; + background: #0a0a0f; + color: #ffffff; + min-height: 100vh; + background-image: + radial-gradient(circle at 20% 80%, #1a1a2e 0%, transparent 50%), + radial-gradient(circle at 80% 20%, #16213e 0%, transparent 50%), + radial-gradient(circle at 40% 40%, #0f3460 0%, transparent 50%); +} + +.main-container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; + min-height: 100vh; +} + +.header-section { + text-align: center; + margin-bottom: 3rem; +} + +.main-title { + font-size: 4rem; + font-weight: 700; + background: linear-gradient(135deg, #00d4ff, #ff6b35, #ffd700); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 0.5rem; + letter-spacing: -0.02em; +} + +.tagline { + font-size: 1.2rem; + color: #8892b0; + font-weight: 300; +} + +.nav-buttons { + position: fixed; + top: 2rem; + right: 2rem; + display: flex; + gap: 1rem; + align-items: center; + z-index: 1000; +} + +.btn { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 0.75rem; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + display: inline-block; +} + +.btn-primary { + background: linear-gradient(135deg, #00d4ff, #0891b2); + color: white; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(0, 212, 255, 0.3); +} + +.btn-logout { + background: linear-gradient(135deg, #dc3545, #c82333); + color: white; +} + +.btn-logout:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(220, 53, 69, 0.3); +} + +.dashboard-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + margin-bottom: 2rem; +} + +.welcome-card { + background: rgba(15, 23, 42, 0.8); + border: 1px solid #1e293b; + border-radius: 1rem; + padding: 2rem; + backdrop-filter: blur(20px); + margin-bottom: 2rem; + text-align: center; +} + +.card { + background: rgba(15, 23, 42, 0.8); + border: 1px solid #1e293b; + border-radius: 1rem; + padding: 2rem; + backdrop-filter: blur(20px); + transition: all 0.2s ease; +} + +.card:hover { + transform: translateY(-2px); + border-color: #00d4ff; + box-shadow: 0 10px 25px rgba(0, 212, 255, 0.1); +} + +.card h3 { + color: #ffffff; + font-size: 1.3rem; + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.card p { + color: #8892b0; + line-height: 1.6; +} + +.loading { + text-align: center; + padding: 2rem; + color: #8892b0; +} + +.spinner { + border: 3px solid #1e293b; + border-top: 3px solid #00d4ff; + border-radius: 50%; + width: 40px; + height: 40px; + animation: spin 1s linear infinite; + margin: 0 auto 1rem; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.user-info { + display: flex; + align-items: center; + gap: 0.5rem; + color: #8892b0; + font-size: 0.9rem; +} + +.user-avatar { + width: 30px; + height: 30px; + border-radius: 50%; + background: linear-gradient(135deg, #00d4ff, #0891b2); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: bold; + font-size: 0.8rem; +} + +/* Modal Styles */ +.modal { + display: none; + position: fixed; + z-index: 2000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(10px); +} + +.modal-content { + background: rgba(15, 23, 42, 0.95); + margin: 5% auto; + padding: 2rem; + border: 1px solid #1e293b; + border-radius: 1rem; + width: 90%; + max-width: 500px; + backdrop-filter: blur(20px); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.modal-title { + color: #ffffff; + font-size: 1.5rem; + font-weight: 600; +} + +.close { + color: #8892b0; + font-size: 2rem; + font-weight: bold; + cursor: pointer; + transition: color 0.2s ease; +} + +.close:hover { + color: #ffffff; +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-label { + display: block; + color: #e2e8f0; + font-weight: 500; + margin-bottom: 0.5rem; +} + +.form-input { + width: 100%; + padding: 0.75rem; + background: #1e293b; + border: 2px solid #334155; + border-radius: 0.5rem; + color: #ffffff; + font-size: 1rem; + transition: border-color 0.2s ease; +} + +.form-input:focus { + outline: none; + border-color: #00d4ff; +} + +.form-input::placeholder { + color: #64748b; +} + +.message { + padding: 1rem; + border-radius: 0.5rem; + margin-bottom: 1rem; + font-weight: 500; +} + +.message.success { + background: rgba(34, 197, 94, 0.1); + color: #22c55e; + border: 1px solid rgba(34, 197, 94, 0.3); +} + +.message.error { + background: rgba(239, 68, 68, 0.1); + color: #ef4444; + border: 1px solid rgba(239, 68, 68, 0.3); +} + +.message.info { + background: rgba(59, 130, 246, 0.1); + color: #3b82f6; + border: 1px solid rgba(59, 130, 246, 0.3); +} + +.player-selection { + display: grid; + gap: 0.5rem; + max-height: 200px; + overflow-y: auto; + background: #0f172a; + border-radius: 0.5rem; + padding: 1rem; +} + +.player-option { + padding: 0.75rem; + background: #1e293b; + border: 2px solid #334155; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + justify-content: space-between; + align-items: center; +} + +.player-option:hover { + border-color: #00d4ff; + background: #334155; +} + +.player-option.selected { + border-color: #00d4ff; + background: rgba(0, 212, 255, 0.1); +} + +.player-info { + color: #e2e8f0; +} + +.player-rfid { + color: #8892b0; + font-size: 0.9rem; + font-family: monospace; +} + +.times-grid { + display: grid; + gap: 1rem; + max-height: 400px; + overflow-y: auto; +} + +.time-card { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.5rem; + padding: 1rem; +} + +.time-location { + color: #00d4ff; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.time-value { + color: #ffffff; + font-size: 1.2rem; + font-weight: bold; + font-family: monospace; +} + +.time-date { + color: #8892b0; + font-size: 0.9rem; + margin-top: 0.5rem; +} + +/* Times Section Styles */ +.times-section { + background: rgba(15, 23, 42, 0.8); + border: 1px solid #1e293b; + border-radius: 1rem; + padding: 2rem; + backdrop-filter: blur(20px); + margin-top: 2rem; +} + +.times-header { + text-align: center; + margin-bottom: 2rem; +} + +.times-header h2 { + color: #ffffff; + font-size: 2rem; + font-weight: 700; + margin-bottom: 0.5rem; +} + +.times-header p { + color: #8892b0; + font-size: 1.1rem; +} + +.times-loading { + text-align: center; + padding: 3rem; + color: #8892b0; +} + +.times-not-linked { + text-align: center; + padding: 3rem 2rem; +} + +.not-linked-content { + max-width: 600px; + margin: 0 auto; +} + +.not-linked-icon { + font-size: 4rem; + margin-bottom: 1rem; +} + +.times-not-linked h3 { + color: #ffffff; + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1rem; +} + +.times-not-linked p { + color: #8892b0; + font-size: 1.1rem; + margin-bottom: 2rem; + line-height: 1.6; +} + +.link-info { + background: rgba(0, 212, 255, 0.1); + border: 1px solid rgba(0, 212, 255, 0.3); + border-radius: 0.75rem; + padding: 1.5rem; + margin-top: 2rem; + text-align: left; +} + +.link-info h4 { + color: #00d4ff; + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 1rem; +} + +.link-info ol { + color: #cbd5e1; + padding-left: 1.5rem; +} + +.link-info li { + margin-bottom: 0.5rem; + line-height: 1.5; +} + +.times-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.stat-card { + background: rgba(0, 212, 255, 0.1); + border: 1px solid rgba(0, 212, 255, 0.3); + border-radius: 0.75rem; + padding: 1.5rem; + text-align: center; + transition: all 0.2s ease; +} + +.stat-card:hover { + transform: translateY(-2px); + border-color: #00d4ff; + box-shadow: 0 10px 25px rgba(0, 212, 255, 0.1); +} + +.stat-number { + color: #00d4ff; + font-size: 2rem; + font-weight: 700; + font-family: monospace; + margin-bottom: 0.5rem; +} + +.stat-label { + color: #8892b0; + font-size: 0.9rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.times-content { + margin-top: 2rem; +} + +.times-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; +} + +.user-time-card { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + padding: 1.5rem; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + cursor: pointer; +} + +.user-time-card:hover { + transform: translateY(-2px); + border-color: #00d4ff; + box-shadow: 0 10px 25px rgba(0, 212, 255, 0.1); +} + +.user-time-card.expanded { + transform: none; + border-color: #00d4ff; + box-shadow: 0 10px 25px rgba(0, 212, 255, 0.2); +} + +.user-time-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, #00d4ff, #0891b2); +} + +.time-location-name { + color: #00d4ff; + font-weight: 600; + font-size: 1.1rem; + margin-bottom: 0.5rem; +} + +.time-value-large { + color: #ffffff; + font-size: 2rem; + font-weight: bold; + font-family: monospace; + margin-bottom: 0.5rem; +} + +.time-date-info { + color: #8892b0; + font-size: 0.9rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.time-rank { + background: rgba(255, 107, 53, 0.1); + color: #ff6b35; + padding: 0.25rem 0.5rem; + border-radius: 0.375rem; + font-size: 0.8rem; + font-weight: 600; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1rem; +} + +.expand-indicator { + color: #8892b0; + font-size: 1.2rem; + transition: transform 0.3s ease; + margin-left: 0.5rem; +} + +.user-time-card.expanded .expand-indicator { + transform: rotate(180deg); +} + +.card-main-content { + margin-bottom: 1rem; +} + +.expanded-content { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease, opacity 0.3s ease; + opacity: 0; +} + +.user-time-card.expanded .expanded-content { + max-height: 1000px; + opacity: 1; +} + +.all-runs-title { + color: #00d4ff; + font-size: 1rem; + font-weight: 600; + margin-bottom: 1rem; + padding-top: 1rem; + border-top: 1px solid #334155; +} + +.run-item { + background: rgba(0, 212, 255, 0.05); + border: 1px solid rgba(0, 212, 255, 0.1); + border-radius: 0.5rem; + padding: 1rem; + margin-bottom: 0.75rem; + display: flex; + justify-content: space-between; + align-items: center; + transition: all 0.2s ease; +} + +.run-item:hover { + background: rgba(0, 212, 255, 0.1); + border-color: rgba(0, 212, 255, 0.3); +} + +.run-item:last-child { + margin-bottom: 0; +} + +.run-time { + color: #ffffff; + font-size: 1.1rem; + font-weight: 600; + font-family: monospace; +} + +.run-details { + text-align: right; + color: #8892b0; + font-size: 0.85rem; +} + +.run-rank-badge { + background: #00d4ff; + color: #0a0a0f; + padding: 0.2rem 0.5rem; + border-radius: 0.3rem; + font-size: 0.75rem; + font-weight: 700; + margin-left: 0.5rem; +} + +.run-rank-badge.best { + background: #22c55e; +} + +.run-rank-badge.second { + background: #f59e0b; +} + +.run-rank-badge.third { + background: #ef4444; +} + +@media (max-width: 768px) { + .header { + padding: 1rem; + flex-direction: column; + gap: 1rem; + } + + .nav-buttons { + flex-wrap: wrap; + justify-content: center; + } + + .container { + padding: 0 1rem; + } + + .dashboard-grid { + grid-template-columns: 1fr; + } + + .modal-content { + margin: 2% auto; + width: 95%; + padding: 1.5rem; + } +} diff --git a/public/css/generator.css b/public/css/generator.css new file mode 100644 index 0000000..9fb8f7f --- /dev/null +++ b/public/css/generator.css @@ -0,0 +1,386 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.container { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border-radius: 20px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + padding: 40px; + max-width: 700px; + width: 100%; + position: relative; + overflow: hidden; +} + +.container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #667eea, #764ba2, #f093fb, #f5576c); + background-size: 300% 100%; + animation: gradientShift 3s ease infinite; +} + +@keyframes gradientShift { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +h1 { + text-align: center; + color: #333; + margin-bottom: 30px; + font-size: 2.2em; + font-weight: 300; + letter-spacing: -1px; +} + +.form-group { + margin-bottom: 25px; + position: relative; +} + +label { + display: block; + margin-bottom: 8px; + color: #555; + font-weight: 500; + font-size: 0.95em; +} + +input, textarea { + width: 100%; + padding: 15px 20px; + border: 2px solid #e0e0e0; + border-radius: 12px; + font-size: 1em; + transition: all 0.3s ease; + background: #fafafa; + font-family: inherit; +} + +textarea { + resize: vertical; + min-height: 80px; +} + +input:focus, textarea:focus { + outline: none; + border-color: #667eea; + background: white; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); + transform: translateY(-2px); +} + +input:hover, textarea:hover { + border-color: #ccc; +} + +.db-config { + background: linear-gradient(135deg, #f8f9ff 0%, #e8f2ff 100%); + border: 2px solid #e3f2fd; + border-radius: 12px; + padding: 20px; + margin-bottom: 25px; + opacity: 0; + transform: translateY(-20px); + transition: all 0.4s ease; + max-height: 0; + overflow: hidden; +} + +.db-config.show { + opacity: 1; + transform: translateY(0); + max-height: 1000px; +} + +.db-config h3 { + color: #1565c0; + margin-bottom: 15px; + font-size: 1.1em; +} + +.tier-notice { + background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%); + border: 2px solid #ffcc02; + border-radius: 8px; + padding: 12px; + margin-top: 10px; + font-size: 0.9em; + color: #f57c00; + text-align: center; +} + +.generate-btn { + width: 100%; + padding: 18px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 12px; + font-size: 1.1em; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + margin-top: 10px; + position: relative; + overflow: hidden; +} + +.generate-btn:hover { + transform: translateY(-2px); + box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3); +} + +.generate-btn:active { + transform: translateY(0); +} + +.result-section { + margin-top: 30px; + opacity: 0; + transform: translateY(20px); + transition: all 0.4s ease; +} + +.result-section.show { + opacity: 1; + transform: translateY(0); +} + +.license-output { + background: linear-gradient(135deg, #f8f9ff 0%, #e8f2ff 100%); + border: 2px solid #e3f2fd; + border-radius: 12px; + padding: 20px; + font-family: 'Courier New', monospace; + font-size: 0.9em; + word-break: break-all; + color: #1565c0; + position: relative; +} + +.license-label { + font-family: 'Segoe UI', sans-serif; + font-size: 0.85em; + color: #666; + margin-bottom: 8px; + font-weight: 500; +} + +.copy-btn { + width: 100%; + padding: 12px; + background: #4caf50; + color: white; + border: none; + border-radius: 8px; + font-size: 0.95em; + font-weight: 500; + cursor: pointer; + margin-top: 15px; + transition: all 0.3s ease; +} + +.copy-btn:hover { + background: #45a049; + transform: translateY(-1px); +} + +.copy-btn.copied { + background: #2196f3; + animation: pulse 0.6s; +} + +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } +} + +.success { + background: #e8f5e8; + color: #2e7d32; + padding: 15px; + border-radius: 8px; + margin-top: 15px; + border-left: 4px solid #4caf50; + font-size: 0.9em; + opacity: 0; + transform: translateY(-10px); + transition: all 0.3s ease; +} + +.success.show { + opacity: 1; + transform: translateY(0); +} + +.error { + background: #ffebee; + color: #c62828; + padding: 15px; + border-radius: 8px; + margin-top: 15px; + border-left: 4px solid #f44336; + font-size: 0.9em; + opacity: 0; + transform: translateY(-10px); + transition: all 0.3s ease; +} + +.error.show { + opacity: 1; + transform: translateY(0); +} + +.info-text { + text-align: center; + color: #666; + font-size: 0.85em; + margin-top: 20px; + line-height: 1.5; +} + +@media (max-width: 480px) { + .container { + padding: 30px 20px; + margin: 10px; + } + + h1 { + font-size: 1.8em; + } +} + +.loading { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(255,255,255,.3); + border-radius: 50%; + border-top-color: #fff; + animation: spin 1s ease-in-out infinite; + margin-right: 10px; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Standortsuche Styles */ +.coordinates-display { + animation: slideDown 0.4s ease; +} + +.map-container { + animation: slideDown 0.4s ease; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +#mapFrame iframe { + border-radius: 10px; +} + +.coordinates-display h4 { + display: flex; + align-items: center; + gap: 8px; +} + +.coordinates-display strong { + color: #2e7d32; +} + +/* Verbesserte Standortsuche Layouts */ +.location-search-container { + display: flex; + gap: 10px; + align-items: stretch; +} + +.location-search-container input { + flex: 1; + min-width: 0; +} + +.location-search-container button { + white-space: nowrap; + min-width: 120px; +} + +/* Responsive Design für Standortsuche */ +@media (max-width: 600px) { + .location-search-container { + flex-direction: column; + gap: 15px; + } + + .location-search-container button { + min-width: auto; + width: 100%; + } + + .coordinates-display .flex-container { + flex-direction: column; + gap: 10px; + } +} + +/* Interaktive Karte Styles */ +#interactiveMap { + position: relative; +} + +#map { + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); +} + +.leaflet-container { + border-radius: 10px; +} + +.leaflet-control-zoom { + border: none; + box-shadow: 0 2px 8px rgba(0,0,0,0.2); +} + +.leaflet-control-zoom a { + background: white; + color: #333; + border: 1px solid #ddd; +} + +.leaflet-control-zoom a:hover { + background: #f8f9fa; +} diff --git a/public/css/reset-password.css b/public/css/reset-password.css new file mode 100644 index 0000000..09ba2b5 --- /dev/null +++ b/public/css/reset-password.css @@ -0,0 +1,235 @@ +/* Reset und Basis-Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: radial-gradient(ellipse at top, #1e293b 0%, #0f172a 50%, #020617 100%); + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + color: #e2e8f0; + line-height: 1.6; +} + +.container { + background: rgba(30, 41, 59, 0.95); + backdrop-filter: blur(20px); + border: 1px solid rgba(51, 65, 85, 0.3); + border-radius: 20px; + padding: 40px; + max-width: 500px; + width: 90%; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); + text-align: center; +} + +.logo { + font-size: 2.5rem; + font-weight: 900; + background: linear-gradient(135deg, #00d4ff, #0891b2); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 10px; + text-transform: uppercase; + letter-spacing: 2px; +} + +.tagline { + color: #94a3b8; + font-size: 0.9rem; + margin-bottom: 30px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.title { + font-size: 1.8rem; + font-weight: 700; + color: #ffffff; + margin-bottom: 20px; +} + +.subtitle { + color: #cbd5e1; + font-size: 1rem; + margin-bottom: 30px; +} + +.form-group { + margin-bottom: 20px; + text-align: left; +} + +.form-label { + display: block; + color: #e2e8f0; + font-weight: 600; + margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 0.5px; + font-size: 0.9rem; +} + +.form-input { + width: 100%; + padding: 15px 20px; + background: #1e293b; + border: 2px solid #334155; + border-radius: 12px; + color: #ffffff; + font-size: 1rem; + transition: all 0.3s ease; +} + +.form-input:focus { + outline: none; + border-color: #00d4ff; + box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1); +} + +.form-input::placeholder { + color: #64748b; +} + +.btn { + width: 100%; + padding: 15px 30px; + border: none; + border-radius: 12px; + font-size: 1rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1px; + cursor: pointer; + transition: all 0.3s ease; + margin-bottom: 20px; +} + +.btn-primary { + background: linear-gradient(135deg, #00d4ff, #0891b2); + color: #ffffff; + box-shadow: 0 4px 15px rgba(0, 212, 255, 0.3); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 212, 255, 0.4); +} + +.btn-primary:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.btn-secondary { + background: transparent; + color: #00d4ff; + border: 2px solid #00d4ff; +} + +.btn-secondary:hover { + background: #00d4ff; + color: #ffffff; +} + +.message { + padding: 15px 20px; + border-radius: 12px; + margin-bottom: 20px; + font-weight: 600; + text-align: center; +} + +.message.success { + background: rgba(34, 197, 94, 0.1); + border: 1px solid #22c55e; + color: #22c55e; +} + +.message.error { + background: rgba(239, 68, 68, 0.1); + border: 1px solid #ef4444; + color: #ef4444; +} + +.message.info { + background: rgba(59, 130, 246, 0.1); + border: 1px solid #3b82f6; + color: #3b82f6; +} + +.loading { + display: none; + text-align: center; + color: #94a3b8; +} + +.spinner { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid #334155; + border-radius: 50%; + border-top-color: #00d4ff; + animation: spin 1s ease-in-out infinite; + margin-right: 10px; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.back-link { + color: #00d4ff; + text-decoration: none; + font-size: 0.9rem; + margin-top: 20px; + display: inline-block; +} + +.back-link:hover { + color: #0891b2; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .container { + margin: 20px; + padding: 30px 20px; + } + + .logo { + font-size: 2rem; + } + + .title { + font-size: 1.5rem; + } + + .form-input, .btn { + padding: 12px 15px; + font-size: 0.9rem; + } +} + +@media (max-width: 480px) { + .container { + margin: 10px; + padding: 20px 15px; + } + + .logo { + font-size: 1.8rem; + } + + .title { + font-size: 1.3rem; + } +} diff --git a/public/dashboard.html b/public/dashboard.html index a9453f5..37e9010 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -7,692 +7,7 @@ - +
@@ -860,623 +175,6 @@
- - - - + diff --git a/public/generator.html b/public/generator.html index 6810286..216097c 100644 --- a/public/generator.html +++ b/public/generator.html @@ -4,394 +4,7 @@ Lizenzgenerator - +
@@ -403,658 +16,40 @@
-
- - -
- -
- - -
- 📝 Bei Lizenzstufe 3+ wird der Schlüssel automatisch als API-Token gespeichert -
-
- -
-

🗄️ Token-Informationen (für Stufe 3+)

+
- - + +
-
- - -
- - -
- -
- - -
-
- - - - - - -
- 📝 Standorte werden in der lokalen PostgreSQL-Datenbank gespeichert -
-
- +
+ + +
+ +
+ +
+ + +
Generierter Lizenzschlüssel:
-
- +
+
- Geben Sie eine gültige MAC-Adresse und Lizenzstufe ein, um einen sicheren Lizenzschlüssel zu generieren. + Der Lizenzgenerator erstellt sichere API-Token für verschiedene Zugriffsstufen.
- + - \ No newline at end of file + diff --git a/public/js/adminlogin.js b/public/js/adminlogin.js new file mode 100644 index 0000000..199234c --- /dev/null +++ b/public/js/adminlogin.js @@ -0,0 +1,81 @@ +function showMessage(elementId, message, isError = false) { + const messageDiv = document.getElementById(elementId); + messageDiv.textContent = message; + messageDiv.classList.add("show"); + setTimeout(() => { + messageDiv.classList.remove("show"); + }, 4000); +} + +function showError(message) { + showMessage("error", message, true); +} + +function showSuccess(message) { + showMessage("success", message, false); +} + +function setLoading(isLoading) { + const btnText = document.getElementById("btn-text"); + const btn = document.getElementById("loginBtn"); + + if (isLoading) { + btnText.innerHTML = 'Anmelde...'; + btn.disabled = true; + } else { + btnText.textContent = 'Anmelden'; + btn.disabled = false; + } +} + +async function handleLogin(event) { + event.preventDefault(); + + const username = document.getElementById('username').value.trim(); + const password = document.getElementById('password').value; + + if (!username || !password) { + showError('Bitte füllen Sie alle Felder aus.'); + return; + } + + setLoading(true); + + try { + const response = await fetch('/api/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username, password }) + }); + + const result = await response.json(); + + if (result.success) { + showSuccess('✅ Anmeldung erfolgreich! Weiterleitung...'); + setTimeout(() => { + window.location.href = '/admin-dashboard'; + }, 1000); + } else { + showError(result.message || 'Anmeldung fehlgeschlagen'); + } + } catch (error) { + console.error('Fehler bei der Anmeldung:', error); + showError('Verbindungsfehler. Bitte versuchen Sie es erneut.'); + } finally { + setLoading(false); + } +} + +// Enter-Taste für Login +document.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + handleLogin(e); + } +}); + +// Fokus auf erstes Eingabefeld +document.addEventListener('DOMContentLoaded', function() { + document.getElementById('username').focus(); +}); diff --git a/public/js/dashboard.js b/public/js/dashboard.js new file mode 100644 index 0000000..aa8d877 --- /dev/null +++ b/public/js/dashboard.js @@ -0,0 +1,577 @@ +// Supabase configuration +const SUPABASE_URL = 'https://lfxlplnypzvjrhftaoog.supabase.co'; +const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxmeGxwbG55cHp2anJoZnRhb29nIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDkyMTQ3NzIsImV4cCI6MjA2NDc5MDc3Mn0.XR4preBqWAQ1rT4PFbpkmRdz57BTwIusBI89fIxDHM8'; + +// Initialize Supabase client +const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY); + +// Global variables +let currentUser = null; + +// Check authentication and load dashboard +async function initDashboard() { + try { + // Get current session + const { data: { session }, error } = await supabase.auth.getSession(); + + if (error) { + console.error('Error checking authentication:', error); + // Temporarily show dashboard for testing + currentUser = { id: 'test-user', email: 'admin@speedrun-arena.com' }; + displayUserInfo({ email: 'admin@speedrun-arena.com' }); + showDashboard(); + // Check times section + checkLinkStatusAndLoadTimes(); + return; + } + + if (!session) { + // No session, redirect to login + window.location.href = '/login'; + return; + } + + // User is authenticated, show dashboard + if (session.user) { + console.log('User data:', session.user); + currentUser = session.user; + displayUserInfo(session.user); + } else { + // Fallback if no user data + currentUser = { id: 'test-user', email: 'admin@speedrun-arena.com' }; + displayUserInfo({ email: 'admin@speedrun-arena.com' }); + } + showDashboard(); + + // Load times section + checkLinkStatusAndLoadTimes(); + + } catch (error) { + console.error('An unexpected error occurred:', error); + // window.location.href = '/login'; + } +} + +// Display user information +function displayUserInfo(user) { + const userEmail = document.getElementById('userEmail'); + const userAvatar = document.getElementById('userAvatar'); + + userEmail.textContent = user.email; + userAvatar.textContent = user.email.charAt(0).toUpperCase(); +} + +// Show dashboard content +function showDashboard() { + document.getElementById('loading').style.display = 'none'; + document.getElementById('dashboardContent').style.display = 'block'; +} + +// Logout function +async function logout() { + try { + const { error } = await supabase.auth.signOut(); + if (error) { + console.error('Error logging out:', error); + } else { + window.location.href = '/'; + } + } catch (error) { + console.error('Error during logout:', error); + } +} + +// Listen for auth state changes +supabase.auth.onAuthStateChange((event, session) => { + if (event === 'SIGNED_OUT' || !session) { + window.location.href = '/login'; + } +}); + +// Initialize dashboard when page loads +initDashboard(); + +// Modal functions +function openModal(modalId) { + document.getElementById(modalId).style.display = 'block'; +} + +function closeModal(modalId) { + document.getElementById(modalId).style.display = 'none'; + // Reset modal state + if (modalId === 'rfidModal') { + stopQRScanner(); + document.getElementById('manualRfidInput').value = ''; + } +} + +// Close modal when clicking outside +window.onclick = function(event) { + if (event.target.classList.contains('modal')) { + closeModal(event.target.id); + } +} + +// QR Scanner variables +let qrStream = null; +let qrScanning = false; + +// Show RFID Settings +async function showRFIDSettings() { + openModal('rfidModal'); + // Reset scanner state + stopQRScanner(); +} + +// Check link status and load times +async function checkLinkStatusAndLoadTimes() { + if (!currentUser) { + showTimesNotLinked(); + return; + } + + try { + // Check if user has a linked player + const response = await fetch(`/api/user-player/${currentUser.id}`); + + if (response.ok) { + const result = await response.json(); + // User is linked, load times + await loadUserTimesSection(result.data); + } else { + // User is not linked + showTimesNotLinked(); + } + } catch (error) { + console.error('Error checking link status:', error); + showTimesNotLinked(); + } +} + +// Start QR Scanner +async function startQRScanner() { + try { + // Request camera access + qrStream = await navigator.mediaDevices.getUserMedia({ + video: { + facingMode: 'environment', // Use back camera if available + width: { ideal: 1280 }, + height: { ideal: 720 } + } + }); + + const video = document.getElementById('qrVideo'); + const canvas = document.getElementById('qrCanvas'); + const context = canvas.getContext('2d'); + + video.srcObject = qrStream; + video.play(); + + // Show camera container and update buttons + document.getElementById('cameraContainer').style.display = 'block'; + document.getElementById('startScanBtn').style.display = 'none'; + document.getElementById('stopScanBtn').style.display = 'inline-block'; + document.getElementById('scanningStatus').style.display = 'block'; + + qrScanning = true; + + // Start scanning loop + video.addEventListener('loadedmetadata', () => { + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + scanQRCode(); + }); + + } catch (error) { + console.error('Error accessing camera:', error); + showMessage('rfidMessage', 'Kamera-Zugriff fehlgeschlagen. Bitte verwende die manuelle Eingabe.', 'error'); + } +} + +// Stop QR Scanner +function stopQRScanner() { + qrScanning = false; + + if (qrStream) { + qrStream.getTracks().forEach(track => track.stop()); + qrStream = null; + } + + // Reset UI + document.getElementById('cameraContainer').style.display = 'none'; + document.getElementById('startScanBtn').style.display = 'inline-block'; + document.getElementById('stopScanBtn').style.display = 'none'; + document.getElementById('scanningStatus').style.display = 'none'; +} + +// Scan QR Code from video stream +function scanQRCode() { + if (!qrScanning) return; + + const video = document.getElementById('qrVideo'); + const canvas = document.getElementById('qrCanvas'); + const context = canvas.getContext('2d'); + + if (video.readyState === video.HAVE_ENOUGH_DATA) { + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + context.drawImage(video, 0, 0, canvas.width, canvas.height); + + const imageData = context.getImageData(0, 0, canvas.width, canvas.height); + const code = jsQR(imageData.data, imageData.width, imageData.height); + + if (code) { + console.log('QR Code detected:', code.data); + handleQRCodeDetected(code.data); + return; + } + } + + // Continue scanning + if (qrScanning) { + requestAnimationFrame(scanQRCode); + } +} + +// Format RFID UID to match database format +function formatRfidUid(rawUid) { + // Remove any existing formatting (spaces, colons, etc.) + let cleanUid = rawUid.replace(/[^a-fA-F0-9]/g, '').toUpperCase(); + + // Handle different UID lengths + if (cleanUid.length === 6) { + // Pad 6-digit UID to 8 digits by adding leading zeros + cleanUid = '00' + cleanUid; + } else if (cleanUid.length === 8) { + // Already correct length + } else if (cleanUid.length < 6) { + // Pad shorter UIDs to 8 digits + cleanUid = cleanUid.padStart(8, '0'); + } else { + throw new Error(`Ungültige RFID UID Länge: ${cleanUid.length} Zeichen (unterstützt: 6-8)`); + } + + // Format as XX:XX:XX:XX + return cleanUid.match(/.{2}/g).join(':'); +} + +// Handle detected QR code +async function handleQRCodeDetected(qrData) { + stopQRScanner(); + + try { + // Extract and format RFID UID from QR code + const rawUid = qrData.trim(); + + if (!rawUid) { + showMessage('rfidMessage', 'QR-Code enthält keine gültige RFID UID', 'error'); + return; + } + + // Format the UID to match database format (XX:XX:XX:XX) + const formattedUid = formatRfidUid(rawUid); + + showMessage('rfidMessage', `QR-Code erkannt: ${rawUid} → ${formattedUid}`, 'info'); + + // Link the user using the formatted RFID UID + await linkUserByRfidUid(formattedUid); + + } catch (error) { + console.error('Error formatting RFID UID:', error); + showMessage('rfidMessage', `Fehler beim Formatieren der RFID UID: ${error.message}`, 'error'); + } +} + +// Manual RFID linking +async function linkManualRfid() { + const rawUid = document.getElementById('manualRfidInput').value.trim(); + + if (!rawUid) { + showMessage('rfidMessage', 'Bitte gib eine RFID UID ein', 'error'); + return; + } + + try { + // Format the UID to match database format + const formattedUid = formatRfidUid(rawUid); + + showMessage('rfidMessage', `Formatiert: ${rawUid} → ${formattedUid}`, 'info'); + + await linkUserByRfidUid(formattedUid); + + } catch (error) { + console.error('Error formatting manual RFID UID:', error); + showMessage('rfidMessage', `Fehler beim Formatieren: ${error.message}`, 'error'); + } +} + +// Link user by RFID UID (core function) +async function linkUserByRfidUid(rfidUid) { + if (!currentUser) { + showMessage('rfidMessage', 'Benutzer nicht authentifiziert', 'error'); + return; + } + + try { + // First, find the player with this RFID UID + const response = await fetch('/api/link-by-rfid', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + rfiduid: rfidUid, + supabase_user_id: currentUser.id + }) + }); + + const result = await response.json(); + + if (response.ok) { + showMessage('rfidMessage', `✅ RFID erfolgreich verknüpft!\nSpieler: ${result.data.firstname} ${result.data.lastname}`, 'success'); + setTimeout(() => { + closeModal('rfidModal'); + // Reload times section after successful linking + checkLinkStatusAndLoadTimes(); + }, 2000); + } else { + showMessage('rfidMessage', result.message || 'Fehler beim Verknüpfen', 'error'); + } + } catch (error) { + console.error('Error linking RFID:', error); + showMessage('rfidMessage', 'Fehler beim Verknüpfen der RFID', 'error'); + } +} + +// Show not linked state +function showTimesNotLinked() { + document.getElementById('timesLoading').style.display = 'none'; + document.getElementById('timesNotLinked').style.display = 'block'; + document.getElementById('timesDisplay').style.display = 'none'; +} + +// Show loading state +function showTimesLoading() { + document.getElementById('timesLoading').style.display = 'block'; + document.getElementById('timesNotLinked').style.display = 'none'; + document.getElementById('timesDisplay').style.display = 'none'; +} + +// Load user times for the section +async function loadUserTimesSection(playerData) { + showTimesLoading(); + + try { + const response = await fetch(`/api/user-times/${currentUser.id}`); + const times = await response.json(); + + // Update stats + updateTimesStats(times, playerData); + + // Display times + displayUserTimes(times); + + // Show the times display + document.getElementById('timesLoading').style.display = 'none'; + document.getElementById('timesNotLinked').style.display = 'none'; + document.getElementById('timesDisplay').style.display = 'block'; + + } catch (error) { + console.error('Error loading user times:', error); + showTimesNotLinked(); + } +} + +// Update stats cards +function updateTimesStats(times, playerData) { + // Total runs + document.getElementById('totalRuns').textContent = times.length; + + // Best time + if (times.length > 0) { + const bestTimeValue = times.reduce((best, current) => { + const currentSeconds = convertTimeToSeconds(current.recorded_time); + const bestSeconds = convertTimeToSeconds(best.recorded_time); + return currentSeconds < bestSeconds ? current : best; + }); + document.getElementById('bestTime').textContent = formatTime(bestTimeValue.recorded_time); + } else { + document.getElementById('bestTime').textContent = '--:--'; + } + + // Unique locations count + const uniqueLocations = [...new Set(times.map(time => time.location_name))]; + document.getElementById('locationsCount').textContent = uniqueLocations.length; + + // Linked player name + document.getElementById('linkedPlayer').textContent = `${playerData.firstname} ${playerData.lastname}`; +} + +// Display user times in grid +function displayUserTimes(times) { + const timesGrid = document.getElementById('userTimesGrid'); + + if (times.length === 0) { + timesGrid.innerHTML = ` +
+

Noch keine Zeiten aufgezeichnet

+

Deine ersten Läufe werden hier angezeigt, sobald du sie abgeschlossen hast!

+
+ `; + return; + } + + // Group times by location + const timesByLocation = times.reduce((acc, time) => { + if (!acc[time.location_name]) { + acc[time.location_name] = []; + } + acc[time.location_name].push(time); + return acc; + }, {}); + + // Generate cards for each location + const cards = Object.entries(timesByLocation).map(([locationName, locationTimes], index) => { + // Sort times by performance (best first) + const sortedTimes = locationTimes.sort((a, b) => { + return convertTimeToSeconds(a.recorded_time) - convertTimeToSeconds(b.recorded_time); + }); + + // Get best time for this location + const bestTime = sortedTimes[0]; + + // Generate all runs for expanded view + const allRunsHtml = sortedTimes.map((run, runIndex) => { + let rankBadge = ''; + let rankClass = ''; + + if (runIndex === 0) { + rankBadge = '🥇 Beste'; + rankClass = 'best'; + } else if (runIndex === 1) { + rankBadge = '🥈 2.'; + rankClass = 'second'; + } else if (runIndex === 2) { + rankBadge = '🥉 3.'; + rankClass = 'third'; + } else { + rankBadge = `${runIndex + 1}.`; + rankClass = ''; + } + + return ` +
+
+
${formatTime(run.recorded_time)}
+
+
+
${new Date(run.created_at).toLocaleDateString('de-DE')}
+
${new Date(run.created_at).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
+ ${rankBadge} +
+
+ `; + }).join(''); + + return ` +
+
+
${locationName}
+
+
+ +
+
${formatTime(bestTime.recorded_time)}
+
+ ${new Date(bestTime.created_at).toLocaleDateString('de-DE')} + ${locationTimes.length} Läufe +
+
+ +
+
Alle Läufe an diesem Standort:
+ ${allRunsHtml} +
+
+ `; + }).join(''); + + timesGrid.innerHTML = cards; +} + +// Toggle time card expansion +function toggleTimeCard(cardElement) { + const isExpanded = cardElement.classList.contains('expanded'); + + // Close all other cards first + document.querySelectorAll('.user-time-card.expanded').forEach(card => { + if (card !== cardElement) { + card.classList.remove('expanded'); + } + }); + + // Toggle current card + if (isExpanded) { + cardElement.classList.remove('expanded'); + } else { + cardElement.classList.add('expanded'); + } +} + +// Helper function to convert time to seconds for comparison +function convertTimeToSeconds(timeValue) { + if (typeof timeValue === 'string') { + // Handle HH:MM:SS format + const parts = timeValue.split(':'); + if (parts.length === 3) { + return parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseFloat(parts[2]); + } + // Handle MM:SS format + if (parts.length === 2) { + return parseInt(parts[0]) * 60 + parseFloat(parts[1]); + } + } + return parseFloat(timeValue) || 0; +} + +// Format time interval to readable format +function formatTime(interval) { + // Postgres interval format: {"hours":0,"minutes":1,"seconds":23.45} + if (typeof interval === 'object') { + const { hours = 0, minutes = 0, seconds = 0 } = interval; + const totalSeconds = hours * 3600 + minutes * 60 + seconds; + return formatSeconds(totalSeconds); + } + + // Fallback for string format + if (typeof interval === 'string') { + // Parse format like "00:01:23.45" + const parts = interval.split(':'); + if (parts.length === 3) { + const hours = parseInt(parts[0]); + const minutes = parseInt(parts[1]); + const seconds = parseFloat(parts[2]); + const totalSeconds = hours * 3600 + minutes * 60 + seconds; + return formatSeconds(totalSeconds); + } + } + + return interval; +} + +function formatSeconds(totalSeconds) { + const minutes = Math.floor(totalSeconds / 60); + const seconds = (totalSeconds % 60).toFixed(2); + + if (minutes > 0) { + return `${minutes}:${seconds.padStart(5, '0')}`; + } else { + return `${seconds}s`; + } +} + +// Show message in modal +function showMessage(containerId, message, type) { + const container = document.getElementById(containerId); + container.innerHTML = `
${message}
`; +} diff --git a/public/js/generator.js b/public/js/generator.js new file mode 100644 index 0000000..4b4960c --- /dev/null +++ b/public/js/generator.js @@ -0,0 +1,560 @@ +// Toggle Token-Felder basierend auf Lizenzstufe +function toggleTokenFields() { + const tierInput = document.getElementById("tier"); + const dbConfig = document.getElementById("dbConfig"); + const tier = parseInt(tierInput.value); + + if (tier >= 3 && !isNaN(tier)) { + dbConfig.innerHTML = ` +

🗄️ Token-Informationen (für Stufe 3+)

+
+ + +
+
+ + +
+ + +
+ +
+ + +
+
+ + + + + + +
+ 📝 Standorte werden in der lokalen PostgreSQL-Datenbank gespeichert +
+ `; + dbConfig.classList.add("show"); + } else { + dbConfig.classList.remove("show"); + setTimeout(() => { + if (!dbConfig.classList.contains("show")) { + dbConfig.innerHTML = ""; + } + }, 400); + } + } + + const secret = "542ff224606c61fb3024e22f76ef9ac8"; + + function isValidMac(mac) { + const pattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$|^[0-9A-Fa-f]{12}$/; + return pattern.test(mac); + } + + function showMessage(elementId, message, isError = false) { + const messageDiv = document.getElementById(elementId); + messageDiv.textContent = message; + messageDiv.classList.add("show"); + setTimeout(() => { + messageDiv.classList.remove("show"); + }, 4000); + } + + function showError(message) { + showMessage("error", message, true); + } + + function showSuccess(message) { + showMessage("success", message, false); + } + + function setLoading(isLoading) { + const btnText = document.getElementById("btn-text"); + const btn = document.querySelector(".generate-btn"); + + if (isLoading) { + btnText.innerHTML = 'Generiere...'; + btn.disabled = true; + btn.style.opacity = '0.7'; + } else { + btnText.textContent = 'Lizenz generieren'; + btn.disabled = false; + btn.style.opacity = '1'; + } + } + + async function saveToDatabase(token, tier) { + const description = document.getElementById("description").value.trim(); + const standorte = document.getElementById("standorte").value.trim(); + + try { + const response = await fetch('/api/web/save-token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + token: token, + description: description || `API-Token Stufe ${tier}`, + standorte: standorte + }) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || 'Fehler beim Speichern in der Datenbank'); + } + + const result = await response.json(); + return result; + } catch (error) { + // Fallback: Zeige dem Benutzer den SQL-Befehl an, den er manuell ausführen kann + const sql = `INSERT INTO api_tokens (token, description, standorte) VALUES ('${token}', '${description || `API-Token Stufe ${tier}`}', '${standorte}');`; + + throw new Error(`Automatisches Speichern fehlgeschlagen. Server nicht erreichbar.\n\nFühren Sie folgenden SQL-Befehl manuell aus:\n${sql}`); + } + } + + async function generateLicense() { + const macInput = document.getElementById("mac").value.trim(); + const tierInput = document.getElementById("tier").value.trim(); + const resultDiv = document.getElementById("result"); + const licenseOutput = document.getElementById("license-output"); + const errorDiv = document.getElementById("error"); + const successDiv = document.getElementById("success"); + + // Reset states + resultDiv.classList.remove("show"); + errorDiv.classList.remove("show"); + successDiv.classList.remove("show"); + setLoading(true); + + // Simulate slight delay for better UX + await new Promise(resolve => setTimeout(resolve, 500)); + + try { + if (!isValidMac(macInput)) { + throw new Error("Ungültige MAC-Adresse. Bitte verwenden Sie das Format 00:1A:2B:3C:4D:5E"); + } + + const mac = macInput.replace(/[:-]/g, "").toUpperCase(); + const tier = parseInt(tierInput); + + if (isNaN(tier) || tier < 1 || tier > 4) { + throw new Error("Lizenzstufe muss eine Zahl zwischen 1 und 4 sein."); + } + + // Standort automatisch speichern, falls vorhanden + let locationSaved = false; + const locationName = document.getElementById('locationSearch')?.value?.trim(); + const latitude = document.getElementById('latitude')?.textContent; + const longitude = document.getElementById('longitude')?.textContent; + + if (locationName && latitude && longitude && tier >= 3) { + try { + await saveLocationToDatabase(); + locationSaved = true; + } catch (locationError) { + console.warn('Standort konnte nicht gespeichert werden:', locationError); + // Fahre trotzdem mit der Lizenzgenerierung fort + } + } + + const data = `${mac}:${tier}`; + const enc = new TextEncoder(); + const key = await crypto.subtle.importKey( + "raw", + enc.encode(secret), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"] + ); + const signature = await crypto.subtle.sign("HMAC", key, enc.encode(data)); + const hex = Array.from(new Uint8Array(signature)) + .map(b => b.toString(16).padStart(2, "0")) + .join("") + .toUpperCase(); + + licenseOutput.textContent = hex; + resultDiv.classList.add("show"); + + // Reset copy button + const copyBtn = document.getElementById("copyButton"); + copyBtn.textContent = "📋 In Zwischenablage kopieren"; + copyBtn.classList.remove("copied"); + + // Bei Stufe 3+ in Datenbank speichern + if (tier >= 3) { + try { + await saveToDatabase(hex, tier); + let successMessage = `✅ Lizenzschlüssel generiert und als API-Token gespeichert!`; + if (locationSaved) { + successMessage += ` Standort wurde ebenfalls gespeichert.`; + } + showSuccess(successMessage); + } catch (dbError) { + showError(`⚠️ Lizenz generiert, aber Datenbank-Fehler: ${dbError.message}`); + } + } else { + let successMessage = `✅ Lizenzschlüssel erfolgreich generiert!`; + if (locationSaved) { + successMessage += ` Standort wurde in der Datenbank gespeichert.`; + } + showSuccess(successMessage); + } + + } catch (error) { + showError(error.message); + } finally { + setLoading(false); + } + } + + async function copyToClipboard() { + const licenseOutput = document.getElementById("license-output"); + const copyBtn = document.getElementById("copyButton"); + + try { + await navigator.clipboard.writeText(licenseOutput.textContent); + copyBtn.textContent = "✅ Kopiert!"; + copyBtn.classList.add("copied"); + + setTimeout(() => { + copyBtn.textContent = "📋 In Zwischenablage kopieren"; + copyBtn.classList.remove("copied"); + }, 2000); + } catch (err) { + // Fallback for older browsers + const textArea = document.createElement("textarea"); + textArea.value = licenseOutput.textContent; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + + copyBtn.textContent = "✅ Kopiert!"; + copyBtn.classList.add("copied"); + + setTimeout(() => { + copyBtn.textContent = "📋 In Zwischenablage kopieren"; + copyBtn.classList.remove("copied"); + }, 2000); + } + } + + // Enter key support + document.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + generateLicense(); + } + }); + + // Input formatting for MAC address + document.getElementById('mac').addEventListener('input', function(e) { + let value = e.target.value.replace(/[^0-9A-Fa-f]/g, ''); + if (value.length > 12) value = value.substr(0, 12); + + // Add colons every 2 characters + value = value.replace(/(.{2})/g, '$1:').replace(/:$/, ''); + e.target.value = value; + }); + + // Input event listener für Lizenzstufe + document.getElementById('tier').addEventListener('input', toggleTokenFields); + + // Standortsuche-Funktionalität + async function searchLocation(buttonElement) { + const locationInput = document.getElementById('locationSearch').value.trim(); + const coordinatesDiv = document.getElementById('coordinates'); + const mapContainer = document.getElementById('mapContainer'); + const latitudeSpan = document.getElementById('latitude'); + const longitudeSpan = document.getElementById('longitude'); + const mapFrame = document.getElementById('mapFrame'); + + if (!locationInput) { + showError('Bitte geben Sie einen Standort ein.'); + return; + } + + let originalText = ''; + let searchBtn = null; + + try { + // Zeige Ladeanimation + searchBtn = buttonElement || document.querySelector('button[onclick*="searchLocation"]'); + if (searchBtn) { + originalText = searchBtn.innerHTML; + searchBtn.innerHTML = 'Suche...'; + searchBtn.disabled = true; + } + + // API-Abfrage an Nominatim (OpenStreetMap) + const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(locationInput)}&limit=1`); + + if (!response.ok) { + throw new Error('Fehler bei der API-Abfrage'); + } + + const data = await response.json(); + + if (data.length === 0) { + throw new Error('Standort nicht gefunden. Bitte versuchen Sie eine andere Beschreibung.'); + } + + const location = data[0]; + const lat = parseFloat(location.lat); + const lon = parseFloat(location.lon); + + // Koordinaten anzeigen + updateCoordinates(lat, lon); + coordinatesDiv.style.display = 'block'; + + // Interaktive Karte erstellen + createInteractiveMap(lat, lon); + mapContainer.style.display = 'block'; + + // Erfolgsmeldung + showSuccess(`✅ Standort "${locationInput}" erfolgreich gefunden! Klicken Sie auf die Karte, um den Pin zu verschieben.`); + + } catch (error) { + showError(`Fehler bei der Standortsuche: ${error.message}`); + coordinatesDiv.style.display = 'none'; + mapContainer.style.display = 'none'; + } finally { + // Button zurücksetzen + if (searchBtn && originalText) { + searchBtn.innerHTML = originalText; + searchBtn.disabled = false; + } + } + } + + // Koordinaten aktualisieren + function updateCoordinates(lat, lon) { + const latitudeSpan = document.getElementById('latitude'); + const longitudeSpan = document.getElementById('longitude'); + + if (latitudeSpan && longitudeSpan) { + latitudeSpan.textContent = lat.toFixed(6); + longitudeSpan.textContent = lon.toFixed(6); + } + } + + // Interaktive Karte erstellen + function createInteractiveMap(initialLat, initialLon) { + const mapFrame = document.getElementById('mapFrame'); + + // Verwende Leaflet.js für interaktive Karte + const mapHtml = ` +
+
+
+ 📍 Klicken Sie auf die Karte, um den Pin zu verschieben +
+
+ `; + + mapFrame.innerHTML = mapHtml; + + // Leaflet.js laden und Karte initialisieren + loadLeafletAndCreateMap(initialLat, initialLon); + } + + // Leaflet.js laden und Karte erstellen + function loadLeafletAndCreateMap(initialLat, initialLon) { + // Prüfe ob Leaflet bereits geladen ist + if (typeof L !== 'undefined') { + createMap(initialLat, initialLon); + return; + } + + // Leaflet CSS laden + const leafletCSS = document.createElement('link'); + leafletCSS.rel = 'stylesheet'; + leafletCSS.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'; + document.head.appendChild(leafletCSS); + + // Leaflet JavaScript laden + const leafletScript = document.createElement('script'); + leafletScript.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'; + leafletScript.onload = () => createMap(initialLat, initialLon); + document.head.appendChild(leafletScript); + } + + // Karte mit Leaflet erstellen + function createMap(initialLat, initialLon) { + try { + const map = L.map('map').setView([initialLat, initialLon], 15); + + // OpenStreetMap Tile Layer + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }).addTo(map); + + // Marker erstellen + const marker = L.marker([initialLat, initialLon], { + draggable: true, + title: 'Standort' + }).addTo(map); + + // Marker-Drag Event + marker.on('dragend', function(event) { + const newLat = event.target.getLatLng().lat; + const newLon = event.target.getLatLng().lng; + updateCoordinates(newLat, newLon); + showSuccess(`📍 Pin auf neue Position verschoben: ${newLat.toFixed(6)}, ${newLon.toFixed(6)}`); + }); + + // Klick-Event auf die Karte + map.on('click', function(event) { + const newLat = event.latlng.lat; + const newLon = event.latlng.lng; + + // Marker auf neue Position setzen + marker.setLatLng([newLat, newLon]); + + // Koordinaten aktualisieren + updateCoordinates(newLat, newLon); + + // Erfolgsmeldung + showSuccess(`📍 Pin auf neue Position gesetzt: ${newLat.toFixed(6)}, ${newLon.toFixed(6)}`); + }); + + // Zoom-Controls hinzufügen + map.zoomControl.setPosition('bottomright'); + + } catch (error) { + console.error('Fehler beim Erstellen der Karte:', error); + // Fallback zu iframe + const mapFrame = document.getElementById('mapFrame'); + const mapUrl = `https://www.openstreetmap.org/export/embed.html?bbox=${initialLon-0.01},${initialLat-0.01},${initialLon+0.01},${initialLat+0.01}&layer=mapnik&marker=${initialLat},${initialLon}`; + mapFrame.innerHTML = ``; + } + } + + // Standort in Datenbank speichern + async function saveLocationToDatabase() { + const locationName = document.getElementById('locationSearch').value.trim(); + const latitude = document.getElementById('latitude').textContent; + const longitude = document.getElementById('longitude').textContent; + const saveBtn = document.getElementById('saveLocationBtn'); + + if (!locationName || !latitude || !longitude) { + showError('Bitte suchen Sie zuerst einen Standort.'); + return; + } + + try { + // Button-Status ändern + const originalText = saveBtn.innerHTML; + saveBtn.innerHTML = 'Speichere...'; + saveBtn.disabled = true; + + // Web-authenticated API für Standortverwaltung aufrufen + const response = await fetch('/api/web/create-location', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: locationName, + lat: parseFloat(latitude), + lon: parseFloat(longitude) + }) + }); + + const result = await response.json(); + + if (result.success) { + showSuccess(`✅ Standort "${locationName}" erfolgreich in der Datenbank gespeichert!`); + saveBtn.innerHTML = '✅ Gespeichert!'; + saveBtn.style.background = '#4caf50'; + + // Button nach 3 Sekunden zurücksetzen + setTimeout(() => { + saveBtn.innerHTML = originalText; + saveBtn.disabled = false; + saveBtn.style.background = '#2196f3'; + }, 3000); + } else { + throw new Error(result.message || 'Unbekannter Fehler beim Speichern'); + } + + } catch (error) { + console.error('Fehler beim Speichern:', error); + showError(`Fehler beim Speichern: ${error.message}`); + + // Button zurücksetzen + saveBtn.innerHTML = '💾 Standort in Datenbank speichern'; + saveBtn.disabled = false; + } + } + + // Zurück zum Dashboard + function goBackToDashboard() { + window.location.href = '/admin-dashboard'; + } + + // Logout-Funktion + async function logout() { + try { + const response = await fetch('/api/logout', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }); + + const result = await response.json(); + + if (result.success) { + window.location.href = '/login'; + } else { + console.error('Fehler beim Abmelden:', result.message); + // Trotzdem zur Login-Seite weiterleiten + window.location.href = '/login'; + } + } catch (error) { + console.error('Fehler beim Abmelden:', error); + // Bei Fehler trotzdem zur Login-Seite weiterleiten + window.location.href = '/login'; + } + } + + // Enter-Taste für Standortsuche + document.addEventListener('DOMContentLoaded', function() { + const locationSearch = document.getElementById('locationSearch'); + if (locationSearch) { + locationSearch.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + searchLocation(); + } + }); + } + }); diff --git a/public/js/reset-password.js b/public/js/reset-password.js new file mode 100644 index 0000000..2c8d541 --- /dev/null +++ b/public/js/reset-password.js @@ -0,0 +1,190 @@ +// Supabase Konfiguration +const supabaseUrl = 'https://lfxlplnypzvjrhftaoog.supabase.co'; +const supabaseKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxmeGxwbG55cHp2anJoZnRhb29nIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDkyMTQ3NzIsImV4cCI6MjA2NDc5MDc3Mn0.XR4preBqWAQ1rT4PFbpkmRdz57BTwIusBI89fIxDHM8'; + +const supabase = window.supabase.createClient(supabaseUrl, supabaseKey); + +// DOM Elemente +const resetForm = document.getElementById('resetForm'); +const newPasswordInput = document.getElementById('newPassword'); +const confirmPasswordInput = document.getElementById('confirmPassword'); +const resetBtn = document.getElementById('resetBtn'); +const loading = document.getElementById('loading'); +const messageContainer = document.getElementById('messageContainer'); + +// Nachricht anzeigen (muss vor anderen Funktionen definiert werden) +function showMessage(type, message) { + messageContainer.innerHTML = ` +
+ ${message} +
+ `; +} + +// URL-Parameter extrahieren +console.log('URL Hash:', window.location.hash); +const urlParams = new URLSearchParams(window.location.hash.substring(1)); +const accessToken = urlParams.get('access_token'); +const refreshToken = urlParams.get('refresh_token'); +const tokenType = urlParams.get('token_type'); + +console.log('Access Token gefunden:', !!accessToken); +console.log('Refresh Token gefunden:', !!refreshToken); + +// Prüfen ob Reset-Token vorhanden ist +if (!accessToken) { + showMessage('error', 'Ungültiger oder fehlender Reset-Link. Bitte fordere einen neuen Reset-Link an.'); + resetForm.style.display = 'none'; +} + +// Session mit Token setzen +async function setSession() { + if (!accessToken || !refreshToken) { + showMessage('error', 'Ungültiger Reset-Link. Tokens fehlen.'); + resetForm.style.display = 'none'; + return false; + } + + try { + console.log('Setze Session mit Tokens...'); + const { data, error } = await supabase.auth.setSession({ + access_token: accessToken, + refresh_token: refreshToken + }); + + if (error) { + console.error('Session Error:', error); + throw error; + } + + console.log('Session erfolgreich gesetzt:', data.user?.email); + showMessage('success', `Session aktiv für: ${data.user?.email}`); + return true; + } catch (error) { + console.error('Fehler beim Setzen der Session:', error); + showMessage('error', `Fehler beim Laden des Reset-Links: ${error.message}`); + resetForm.style.display = 'none'; + return false; + } +} + +// Passwort zurücksetzen +async function resetPassword(newPassword) { + try { + console.log('Starte Passwort-Update...'); + + // Erstmal Session prüfen + const { data: session } = await supabase.auth.getSession(); + console.log('Aktuelle Session:', session); + + if (!session.session) { + throw new Error('Keine aktive Session gefunden'); + } + + const { data, error } = await supabase.auth.updateUser({ + password: newPassword + }); + + if (error) { + console.error('Update User Error:', error); + throw error; + } + + console.log('Passwort erfolgreich aktualisiert:', data); + return { success: true, data }; + } catch (error) { + console.error('Fehler beim Zurücksetzen des Passworts:', error); + return { success: false, error: error.message }; + } +} + +// Formular-Validierung +function validateForm() { + const newPassword = newPasswordInput.value; + const confirmPassword = confirmPasswordInput.value; + + if (newPassword.length < 8) { + showMessage('error', 'Das Passwort muss mindestens 8 Zeichen lang sein.'); + return false; + } + + if (newPassword !== confirmPassword) { + showMessage('error', 'Die Passwörter stimmen nicht überein.'); + return false; + } + + return true; +} + +// Formular-Submit Handler +resetForm.addEventListener('submit', async (e) => { + e.preventDefault(); + + if (!validateForm()) { + return; + } + + // UI-Status ändern + resetBtn.disabled = true; + loading.style.display = 'block'; + resetForm.style.display = 'none'; + + try { + // Warten bis Session gesetzt ist + const sessionSet = await setSession(); + if (!sessionSet) { + throw new Error('Session konnte nicht gesetzt werden'); + } + + const result = await resetPassword(newPasswordInput.value); + + if (result.success) { + showMessage('success', '✅ Passwort erfolgreich zurückgesetzt! Du wirst zur Hauptseite weitergeleitet...'); + + // Nach 3 Sekunden zur Hauptseite weiterleiten + setTimeout(() => { + window.location.href = '/'; + }, 3000); + } else { + showMessage('error', `❌ Fehler beim Zurücksetzen: ${result.error}`); + resetForm.style.display = 'block'; + } + } catch (error) { + console.error('Submit Error:', error); + showMessage('error', `❌ Fehler: ${error.message}`); + resetForm.style.display = 'block'; + } finally { + resetBtn.disabled = false; + loading.style.display = 'none'; + } +}); + +// Session beim Laden der Seite setzen (nur wenn Token vorhanden) +if (accessToken && refreshToken) { + setSession(); +} + +// Passwort-Sicherheitshinweise +newPasswordInput.addEventListener('input', function() { + const password = this.value; + const hasLength = password.length >= 8; + const hasUpper = /[A-Z]/.test(password); + const hasLower = /[a-z]/.test(password); + const hasNumber = /\d/.test(password); + const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(password); + + if (password.length > 0) { + let hints = []; + if (!hasLength) hints.push('Mindestens 8 Zeichen'); + if (!hasUpper) hints.push('Großbuchstaben'); + if (!hasLower) hints.push('Kleinbuchstaben'); + if (!hasNumber) hints.push('Zahlen'); + if (!hasSpecial) hints.push('Sonderzeichen'); + + if (hints.length > 0) { + showMessage('info', `💡 Tipp: Verwende auch ${hints.join(', ')} für ein sicheres Passwort.`); + } else { + showMessage('success', '✅ Starkes Passwort!'); + } + } +}); diff --git a/public/reset-password.html b/public/reset-password.html index 200a150..0699a8c 100644 --- a/public/reset-password.html +++ b/public/reset-password.html @@ -8,243 +8,7 @@ - +
@@ -296,197 +60,6 @@ ← Zurück zur Hauptseite
- + \ No newline at end of file