Refactor ALL

This commit is contained in:
2025-09-04 14:31:17 +02:00
parent f6cd5c734f
commit eb1d713942
12 changed files with 2933 additions and 3039 deletions

81
public/js/adminlogin.js Normal file
View File

@@ -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 = '<span class="loading"></span>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();
});

577
public/js/dashboard.js Normal file
View File

@@ -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 = `
<div style="grid-column: 1 / -1; text-align: center; padding: 3rem; color: #8892b0;">
<h3>Noch keine Zeiten aufgezeichnet</h3>
<p>Deine ersten Läufe werden hier angezeigt, sobald du sie abgeschlossen hast!</p>
</div>
`;
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 `
<div class="run-item">
<div>
<div class="run-time">${formatTime(run.recorded_time)}</div>
</div>
<div class="run-details">
<div>${new Date(run.created_at).toLocaleDateString('de-DE')}</div>
<div>${new Date(run.created_at).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}</div>
<span class="run-rank-badge ${rankClass}">${rankBadge}</span>
</div>
</div>
`;
}).join('');
return `
<div class="user-time-card" onclick="toggleTimeCard(this)" data-location="${locationName}">
<div class="card-header">
<div class="time-location-name">${locationName}</div>
<div class="expand-indicator">▼</div>
</div>
<div class="card-main-content">
<div class="time-value-large">${formatTime(bestTime.recorded_time)}</div>
<div class="time-date-info">
<span>${new Date(bestTime.created_at).toLocaleDateString('de-DE')}</span>
<span class="time-rank">${locationTimes.length} Läufe</span>
</div>
</div>
<div class="expanded-content">
<div class="all-runs-title">Alle Läufe an diesem Standort:</div>
${allRunsHtml}
</div>
</div>
`;
}).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 = `<div class="message ${type}">${message}</div>`;
}

560
public/js/generator.js Normal file
View File

@@ -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 = `
<h3>🗄️ Token-Informationen (für Stufe 3+)</h3>
<div class="form-group">
<label for="description">Token Beschreibung</label>
<textarea id="description" placeholder="z.B. API-Zugang für Standort München"></textarea>
</div>
<div class="form-group">
<label for="standorte">Standorte</label>
<input type="text" id="standorte" placeholder="z.B. München, Berlin">
</div>
<!-- Neue Standortsuche-Sektion -->
<div class="form-group">
<label for="locationSearch">Standort suchen & auf Karte anzeigen</label>
<div class="location-search-container">
<input type="text" id="locationSearch" placeholder="z.B. München, Marienplatz">
<button onclick="searchLocation(this)" style="padding: 15px 20px; background: #4caf50; color: white; border: none; border-radius: 12px; cursor: pointer; font-weight: 500; transition: all 0.3s ease;">🔍 Suchen</button>
</div>
</div>
<!-- Koordinaten-Anzeige -->
<div id="coordinates" class="coordinates-display" style="display: none;">
<div style="background: #f0f8ff; border: 2px solid #4caf50; border-radius: 8px; padding: 15px; margin-top: 15px;">
<h4 style="margin: 0 0 10px 0; color: #2e7d32;">📍 Gefundene Koordinaten:</h4>
<div style="display: flex; gap: 20px; flex-wrap: wrap;">
<div>
<strong>Breitengrad (LAT):</strong>
<span id="latitude" style="font-family: monospace; color: #1565c0;"></span>
</div>
<div>
<strong>Längengrad (LON):</strong>
<span id="longitude" style="font-family: monospace; color: #1565c0;"></span>
</div>
</div>
<div style="margin-top: 15px; text-align: center;">
<div style="font-size: 0.85em; color: #666; margin-bottom: 10px;">
💡 Der Standort wird automatisch beim Generieren der Lizenz gespeichert
</div>
<button id="saveLocationBtn" onclick="saveLocationToDatabase()" style="padding: 12px 24px; background: #2196f3; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; transition: all 0.3s ease;">
💾 Standort manuell speichern
</button>
</div>
</div>
</div>
<!-- Karten-Container -->
<div id="mapContainer" class="map-container" style="display: none; margin-top: 20px;">
<h4 style="margin: 0 0 15px 0; color: #333;">🗺️ Standort auf der Karte:</h4>
<div id="mapFrame" style="width: 100%; height: 300px; border: 2px solid #ddd; border-radius: 12px; overflow: hidden;"></div>
</div>
<div class="info-text" style="margin-top: 10px; font-size: 0.8em;">
📝 Standorte werden in der lokalen PostgreSQL-Datenbank gespeichert
</div>
`;
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 = '<span class="loading"></span>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 = '<span class="loading"></span>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 = `
<div id="interactiveMap" style="width: 100%; height: 100%; position: relative;">
<div id="map" style="width: 100%; height: 100%; border-radius: 10px;"></div>
<div style="position: absolute; top: 10px; right: 10px; background: white; padding: 8px; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); font-size: 12px; color: #666;">
📍 Klicken Sie auf die Karte, um den Pin zu verschieben
</div>
</div>
`;
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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> 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 = `<iframe src="${mapUrl}" width="100%" height="100%" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" title="Standort auf der Karte"></iframe>`;
}
}
// 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 = '<span class="loading"></span>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();
}
});
}
});

190
public/js/reset-password.js Normal file
View File

@@ -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 = `
<div class="message ${type}">
${message}
</div>
`;
}
// 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!');
}
}
});