Initial local commit
This commit is contained in:
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
.env
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.env
|
||||
node_modules/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
.pgpass
|
||||
238
README.md
Normal file
238
README.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# 🔐 Lizenzgenerator mit PostgreSQL Integration
|
||||
|
||||
Ein sicherer Lizenzgenerator mit PostgreSQL-Datenbank, interaktiver Karte und API-Key Authentifizierung.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **🔑 Sichere Lizenzgenerierung** mit HMAC-SHA256
|
||||
- **🗄️ PostgreSQL Integration** für lokale Datenspeicherung
|
||||
- **🗺️ Interaktive Karte** mit Leaflet.js und OpenStreetMap
|
||||
- **🔍 Standortsuche** über Nominatim API
|
||||
- **🔐 API-Key Authentifizierung** für alle API-Endpunkte
|
||||
- **🌐 Web-Interface** mit Login-Schutz
|
||||
- **📱 Responsive Design** für alle Geräte
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
### Voraussetzungen
|
||||
- Node.js (v16 oder höher)
|
||||
- PostgreSQL Datenbank
|
||||
- npm oder yarn
|
||||
|
||||
### Setup
|
||||
```bash
|
||||
# Repository klonen
|
||||
git clone <repository-url>
|
||||
cd ninjaserver
|
||||
|
||||
# Abhängigkeiten installieren
|
||||
npm install
|
||||
|
||||
# Umgebungsvariablen konfigurieren
|
||||
cp .env.example .env
|
||||
# .env-Datei mit Ihren Datenbankdaten bearbeiten
|
||||
|
||||
# Datenbank initialisieren
|
||||
npm run init-db
|
||||
|
||||
# Server starten
|
||||
npm start
|
||||
```
|
||||
|
||||
## 🔧 Konfiguration
|
||||
|
||||
### Umgebungsvariablen (.env)
|
||||
```env
|
||||
# PostgreSQL Datenbankverbindung
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=ninjacross
|
||||
DB_USER=reptil1990
|
||||
DB_PASSWORD=!Delfine1!!!
|
||||
DB_SSL=false
|
||||
|
||||
# Server Konfiguration
|
||||
PORT=3000
|
||||
SESSION_SECRET=your-secret-key-change-this
|
||||
```
|
||||
|
||||
## 🔐 Authentifizierung
|
||||
|
||||
### Web-Interface
|
||||
- **Standardanmeldung**: `admin` / `admin123`
|
||||
- **Benutzer erstellen**: `npm run create-user`
|
||||
|
||||
### API-Key Authentifizierung
|
||||
Alle API-Endpunkte erfordern einen gültigen API-Key im `Authorization` Header:
|
||||
|
||||
```bash
|
||||
Authorization: Bearer YOUR_API_KEY_HERE
|
||||
```
|
||||
|
||||
#### API-Key generieren
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/generate-api-key \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"description": "Mein API Key", "standorte": "München, Berlin"}'
|
||||
```
|
||||
|
||||
## 📡 API-Endpunkte
|
||||
|
||||
### Geschützte Endpunkte (API-Key erforderlich)
|
||||
|
||||
#### Standorte abrufen
|
||||
```bash
|
||||
curl -X GET http://localhost:3000/api/locations \
|
||||
-H "Authorization: Bearer YOUR_API_KEY"
|
||||
```
|
||||
|
||||
#### Neuen Standort erstellen
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/create-location \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_API_KEY" \
|
||||
-d '{"name": "München", "lat": 48.1351, "lon": 11.5820}'
|
||||
```
|
||||
|
||||
#### API-Tokens abrufen
|
||||
```bash
|
||||
curl -X GET http://localhost:3000/api/tokens \
|
||||
-H "Authorization: Bearer YOUR_API_KEY"
|
||||
```
|
||||
|
||||
#### Token validieren
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/validate-token \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_API_KEY" \
|
||||
-d '{"token": "TOKEN_TO_VALIDATE"}'
|
||||
```
|
||||
|
||||
#### Token speichern
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/save-token \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_API_KEY" \
|
||||
-d '{"token": "GENERATED_TOKEN", "description": "Beschreibung", "standorte": "Standorte"}'
|
||||
```
|
||||
|
||||
### Öffentliche Endpunkte (nur Web-Interface)
|
||||
|
||||
#### Login
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username": "admin", "password": "admin123"}'
|
||||
```
|
||||
|
||||
#### Logout
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/logout
|
||||
```
|
||||
|
||||
## 🗄️ Datenbankstruktur
|
||||
|
||||
### `adminusers` Tabelle
|
||||
```sql
|
||||
CREATE TABLE adminusers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### `api_tokens` Tabelle
|
||||
```sql
|
||||
CREATE TABLE api_tokens (
|
||||
id SERIAL PRIMARY KEY,
|
||||
token VARCHAR(255) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
standorte TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT true
|
||||
);
|
||||
```
|
||||
|
||||
### `locations` Tabelle
|
||||
```sql
|
||||
CREATE TABLE locations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) UNIQUE NOT NULL,
|
||||
latitude DECIMAL(10, 8) NOT NULL,
|
||||
longitude DECIMAL(11, 8) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
## 🧪 API testen
|
||||
|
||||
### Test-Skript verwenden
|
||||
```bash
|
||||
# test-api.js bearbeiten und API_KEY setzen
|
||||
node test-api.js
|
||||
```
|
||||
|
||||
### Manueller Test
|
||||
```bash
|
||||
# 1. API-Key generieren
|
||||
curl -X POST http://localhost:3000/api/generate-api-key \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"description": "Test Key"}'
|
||||
|
||||
# 2. API mit generiertem Key testen
|
||||
curl -X GET http://localhost:3000/api/locations \
|
||||
-H "Authorization: Bearer GENERATED_API_KEY"
|
||||
```
|
||||
|
||||
## 📁 Projektstruktur
|
||||
|
||||
```
|
||||
ninjaserver/
|
||||
├── server.js # Hauptserver-Datei
|
||||
├── routes/
|
||||
│ └── api.js # API-Routen mit Bearer Token Auth
|
||||
├── scripts/
|
||||
│ ├── init-db.js # Datenbankinitialisierung
|
||||
│ └── create-user.js # Benutzer-Erstellung
|
||||
├── public/
|
||||
│ ├── index.html # Hauptanwendung
|
||||
│ └── login.html # Login-Seite
|
||||
├── test-api.js # API-Test-Skript
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
### Lokale Entwicklung
|
||||
```bash
|
||||
npm run dev # Mit Nodemon für automatisches Neuladen
|
||||
```
|
||||
|
||||
### Produktion
|
||||
```bash
|
||||
npm start # Produktionsserver
|
||||
```
|
||||
|
||||
## 🔒 Sicherheit
|
||||
|
||||
- **API-Key Authentifizierung** für alle API-Endpunkte
|
||||
- **Session-basierte Authentifizierung** für Web-Interface
|
||||
- **Passwort-Hashing** mit bcrypt
|
||||
- **HTTPS empfohlen** für Produktionsumgebung
|
||||
- **Regelmäßige API-Key Rotation** empfohlen
|
||||
|
||||
## 📝 Lizenz
|
||||
|
||||
Proprietär - Alle Rechte vorbehalten
|
||||
|
||||
## 👨💻 Autor
|
||||
|
||||
Carsten Graf
|
||||
|
||||
---
|
||||
|
||||
**⚠️ Wichtig**: Ändern Sie die Standardpasswörter in der Produktionsumgebung!
|
||||
371
adminlogin.html
Normal file
371
adminlogin.html
Normal file
@@ -0,0 +1,371 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ninja Server - Login</title>
|
||||
<script src="https://unpkg.com/@supabase/supabase-js@2"></script>
|
||||
<style>
|
||||
* {
|
||||
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;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.logo h1 {
|
||||
color: #333;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.logo p {
|
||||
color: #666;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-container.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: #667eea;
|
||||
border: 2px solid #667eea;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toggle-form {
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.toggle-form button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #667eea;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.toggle-form button:hover {
|
||||
color: #764ba2;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 0.75rem;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="logo">
|
||||
<h1>🥷 Ninja Server</h1>
|
||||
<p>Secure Authentication Portal</p>
|
||||
</div>
|
||||
|
||||
<div id="message"></div>
|
||||
<div id="loading" class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Processing...</p>
|
||||
</div>
|
||||
|
||||
<!-- Login Form -->
|
||||
<div id="loginForm" class="form-container active">
|
||||
<h2 style="text-align: center; margin-bottom: 1.5rem; color: #333;">Welcome Back</h2>
|
||||
<form id="loginFormElement">
|
||||
<div class="form-group">
|
||||
<label for="loginEmail">Email</label>
|
||||
<input type="email" id="loginEmail" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="loginPassword">Password</label>
|
||||
<input type="password" id="loginPassword" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Sign In</button>
|
||||
</form>
|
||||
<div class="toggle-form">
|
||||
<p>Don't have an account? <button type="button" onclick="toggleForm()">Sign Up</button></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Register Form -->
|
||||
<div id="registerForm" class="form-container">
|
||||
<h2 style="text-align: center; margin-bottom: 1.5rem; color: #333;">Create Account</h2>
|
||||
<form id="registerFormElement">
|
||||
<div class="form-group">
|
||||
<label for="registerEmail">Email</label>
|
||||
<input type="email" id="registerEmail" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="registerPassword">Password</label>
|
||||
<input type="password" id="registerPassword" required minlength="6">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirmPassword">Confirm Password</label>
|
||||
<input type="password" id="confirmPassword" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Create Account</button>
|
||||
</form>
|
||||
<div class="toggle-form">
|
||||
<p>Already have an account? <button type="button" onclick="toggleForm()">Sign In</button></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 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);
|
||||
|
||||
// Check if user is already logged in
|
||||
async function checkAuth() {
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
if (session) {
|
||||
window.location.href = 'dashboard.html';
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle between login and register forms
|
||||
function toggleForm() {
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
const registerForm = document.getElementById('registerForm');
|
||||
|
||||
if (loginForm.classList.contains('active')) {
|
||||
loginForm.classList.remove('active');
|
||||
registerForm.classList.add('active');
|
||||
} else {
|
||||
registerForm.classList.remove('active');
|
||||
loginForm.classList.add('active');
|
||||
}
|
||||
clearMessage();
|
||||
}
|
||||
|
||||
// Show message
|
||||
function showMessage(message, type = 'success') {
|
||||
const messageDiv = document.getElementById('message');
|
||||
messageDiv.innerHTML = `<div class="message ${type}">${message}</div>`;
|
||||
setTimeout(() => {
|
||||
messageDiv.innerHTML = '';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Clear message
|
||||
function clearMessage() {
|
||||
document.getElementById('message').innerHTML = '';
|
||||
}
|
||||
|
||||
// Show/hide loading
|
||||
function setLoading(show) {
|
||||
const loading = document.getElementById('loading');
|
||||
if (show) {
|
||||
loading.classList.add('active');
|
||||
} else {
|
||||
loading.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle login
|
||||
document.getElementById('loginFormElement').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
clearMessage();
|
||||
|
||||
const email = document.getElementById('loginEmail').value;
|
||||
const password = document.getElementById('loginPassword').value;
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email: email,
|
||||
password: password
|
||||
});
|
||||
|
||||
if (error) {
|
||||
showMessage(error.message, 'error');
|
||||
} else {
|
||||
showMessage('Login successful! Redirecting...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = 'dashboard.html';
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('An unexpected error occurred', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle registration
|
||||
document.getElementById('registerFormElement').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
clearMessage();
|
||||
|
||||
const email = document.getElementById('registerEmail').value;
|
||||
const password = document.getElementById('registerPassword').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
showMessage('Passwords do not match', 'error');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
showMessage('Password must be at least 6 characters', 'error');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email: email,
|
||||
password: password
|
||||
});
|
||||
|
||||
if (error) {
|
||||
showMessage(error.message, 'error');
|
||||
} else {
|
||||
if (data.user && !data.user.email_confirmed_at) {
|
||||
showMessage('Registration successful! Please check your email to confirm your account.', 'success');
|
||||
} else {
|
||||
showMessage('Registration successful! Redirecting...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = 'dashboard.html';
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('An unexpected error occurred', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Check authentication on page load
|
||||
checkAuth();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1
dashboard.html
Normal file
1
dashboard.html
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
20
docker-compose.yml
Normal file
20
docker-compose.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
|
||||
app:
|
||||
build: .
|
||||
environment:
|
||||
DB_HOST: host.docker.internal
|
||||
DB_PORT: 5432
|
||||
DB_NAME: ninjacross
|
||||
DB_USER: reptil1990
|
||||
DB_PASSWORD: \!Delfine1\!\!\!
|
||||
DB_SSL: "false"
|
||||
PORT: 3000
|
||||
LICENSE_SECRET: 542ff224606c61fb3024e22f76ef9ac8
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- .:/app
|
||||
restart: unless-stopped
|
||||
18
dockerfile
Normal file
18
dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
# Use official Node.js LTS image
|
||||
FROM node:18
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files and install dependencies
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install --production
|
||||
|
||||
# Copy the rest of the app
|
||||
COPY . .
|
||||
|
||||
# Expose the port (default 3000)
|
||||
EXPOSE 3000
|
||||
|
||||
# Start the server
|
||||
CMD ["npm", "start"]
|
||||
6605
package-lock.json
generated
Normal file
6605
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "lizenz-generator",
|
||||
"version": "1.0.0",
|
||||
"description": "Lizenzgenerator mit PostgreSQL Integration",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js",
|
||||
"init-db": "node scripts/init-db.js",
|
||||
"create-user": "node scripts/create-user.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hisma/server-puppeteer": "^0.6.5",
|
||||
"bcrypt": "^5.1.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"enhanced-postgres-mcp-server": "^1.0.1",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"pg": "^8.11.3",
|
||||
"socket.io": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
},
|
||||
"keywords": [
|
||||
"license",
|
||||
"generator",
|
||||
"postgresql",
|
||||
"api",
|
||||
"token"
|
||||
],
|
||||
"author": "Carsten Graf",
|
||||
"license": "propriatary"
|
||||
}
|
||||
315
public/adminlogin.html
Normal file
315
public/adminlogin.html
Normal file
@@ -0,0 +1,315 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - Lizenzgenerator</title>
|
||||
<style>
|
||||
* {
|
||||
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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<h1>🔐 Anmeldung</h1>
|
||||
|
||||
<form id="loginForm" onsubmit="handleLogin(event)">
|
||||
<div class="form-group">
|
||||
<label for="username">Benutzername</label>
|
||||
<input type="text" id="username" name="username" placeholder="Ihr Benutzername" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Passwort</label>
|
||||
<input type="password" id="password" name="password" placeholder="Ihr Passwort" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="login-btn" id="loginBtn">
|
||||
<span id="btn-text">Anmelden</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div id="error" class="error"></div>
|
||||
<div id="success" class="success"></div>
|
||||
|
||||
<div class="info-text">
|
||||
Melden Sie sich an, um auf den Lizenzgenerator zuzugreifen.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
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 = '/generator';
|
||||
}, 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();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
319
public/dashboard.html
Normal file
319
public/dashboard.html
Normal file
@@ -0,0 +1,319 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ninja Server - Admin Dashboard</title>
|
||||
<script src="https://unpkg.com/@supabase/supabase-js@2"></script>
|
||||
<style>
|
||||
* {
|
||||
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;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 1rem 2rem;
|
||||
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.logo h1 {
|
||||
color: #333;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #0056b3;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-logout:hover {
|
||||
background: #c82333;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.welcome-card {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
color: #333;
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #667eea;
|
||||
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: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="logo">
|
||||
<h1>🥷 Ninja Server - Admin</h1>
|
||||
</div>
|
||||
<div class="nav-buttons">
|
||||
<div class="user-info">
|
||||
<div class="user-avatar" id="userAvatar">U</div>
|
||||
<span id="userEmail">user@example.com</span>
|
||||
</div>
|
||||
<a href="index.html" class="btn btn-primary">Back to Times</a>
|
||||
<button class="btn btn-logout" onclick="logout()">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div id="loading" class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading dashboard...</p>
|
||||
</div>
|
||||
|
||||
<div id="dashboardContent" style="display: none;">
|
||||
<div class="welcome-card">
|
||||
<h2>Admin Dashboard 🥷</h2>
|
||||
<p>Welcome to the admin panel! Manage your ninja server from here.</p>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<div class="card">
|
||||
<h3>📊 Analytics</h3>
|
||||
<p>Track your performance and monitor key metrics. This section will show detailed analytics once we implement the functionality.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>⚡ Quick Actions</h3>
|
||||
<p>Common tasks and shortcuts will be available here. We'll add buttons for creating new records, managing settings, and more.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3><EFBFBD><EFBFBD> Settings</h3>
|
||||
<p>Configure your account preferences, security settings, and application options. This will be fully functional in the next phase.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>📝 Recent Activity</h3>
|
||||
<p>View your recent actions and system events. This will show a timeline of your activities once we connect it to the database.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 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);
|
||||
|
||||
// Check authentication and load dashboard
|
||||
async function initDashboard() {
|
||||
try {
|
||||
const { data: { session }, error } = await supabase.auth.getSession();
|
||||
|
||||
if (error) {
|
||||
console.error('Error checking authentication:', error);
|
||||
window.location.href = 'login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
// No session, redirect to login
|
||||
window.location.href = 'login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
// User is authenticated, show dashboard
|
||||
displayUserInfo(session.user);
|
||||
showDashboard();
|
||||
|
||||
} catch (error) {
|
||||
console.error('An unexpected error occurred:', error);
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = 'index.html';
|
||||
}
|
||||
} 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.html';
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize dashboard when page loads
|
||||
initDashboard();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1052
public/generator.html
Normal file
1052
public/generator.html
Normal file
File diff suppressed because it is too large
Load Diff
426
public/index.html
Normal file
426
public/index.html
Normal file
@@ -0,0 +1,426 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ninja Server - Times Display</title>
|
||||
<script src="https://unpkg.com/@supabase/supabase-js@2"></script>
|
||||
<style>
|
||||
* {
|
||||
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;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 1rem 2rem;
|
||||
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo h1 {
|
||||
color: #333;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-admin {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-admin:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-dashboard {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-dashboard:hover {
|
||||
background: #218838;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-logout:hover {
|
||||
background: #c82333;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.welcome-card {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.welcome-card h2 {
|
||||
color: #333;
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.welcome-card p {
|
||||
color: #666;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.times-section {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.times-section h3 {
|
||||
color: #333;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.times-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.time-card {
|
||||
background: #f8f9fa;
|
||||
border-radius: 15px;
|
||||
padding: 1.5rem;
|
||||
border-left: 4px solid #667eea;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.time-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.time-card h4 {
|
||||
color: #333;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.time-card p {
|
||||
color: #666;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.time-value {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #667eea;
|
||||
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); }
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
padding: 1rem;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.times-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="logo">
|
||||
<h1>🥷 Ninja Server</h1>
|
||||
</div>
|
||||
<div class="nav-buttons">
|
||||
<div id="userInfo" class="user-info" style="display: none;">
|
||||
<div class="user-avatar" id="userAvatar">U</div>
|
||||
<span id="userEmail">user@example.com</span>
|
||||
</div>
|
||||
<a href="login.html" class="btn btn-admin" id="adminLoginBtn">Admin Login</a>
|
||||
<a href="dashboard.html" class="btn btn-dashboard" id="dashboardBtn" style="display: none;">Dashboard</a>
|
||||
<button class="btn btn-logout" id="logoutBtn" onclick="logout()" style="display: none;">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="welcome-card">
|
||||
<h2>Welcome to Ninja Server! 🥷</h2>
|
||||
<p>View the latest times and performance data from our ninja training sessions.</p>
|
||||
</div>
|
||||
|
||||
<div class="times-section">
|
||||
<h3>⏱️ Latest Times</h3>
|
||||
<div id="loading" class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading times...</p>
|
||||
</div>
|
||||
<div id="timesContent" style="display: none;">
|
||||
<div class="times-grid" id="timesGrid">
|
||||
<!-- Times will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
<div id="noData" class="no-data" style="display: none;">
|
||||
No times available yet. Check back later!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 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);
|
||||
|
||||
// Check authentication status
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
if (session) {
|
||||
// User is logged in, show user info and dashboard button
|
||||
displayUserInfo(session.user);
|
||||
showAuthenticatedUI();
|
||||
} else {
|
||||
// User is not logged in, show admin login button
|
||||
showPublicUI();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking auth:', error);
|
||||
showPublicUI();
|
||||
}
|
||||
}
|
||||
|
||||
// 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 UI for authenticated users
|
||||
function showAuthenticatedUI() {
|
||||
document.getElementById('userInfo').style.display = 'flex';
|
||||
document.getElementById('adminLoginBtn').style.display = 'none';
|
||||
document.getElementById('dashboardBtn').style.display = 'inline-block';
|
||||
document.getElementById('logoutBtn').style.display = 'inline-block';
|
||||
}
|
||||
|
||||
// Show UI for public users
|
||||
function showPublicUI() {
|
||||
document.getElementById('userInfo').style.display = 'none';
|
||||
document.getElementById('adminLoginBtn').style.display = 'inline-block';
|
||||
document.getElementById('dashboardBtn').style.display = 'none';
|
||||
document.getElementById('logoutBtn').style.display = 'none';
|
||||
}
|
||||
|
||||
// Load times from local database
|
||||
async function loadTimes() {
|
||||
try {
|
||||
// Fetch times from local database
|
||||
const response = await fetch('/api/times');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch times');
|
||||
}
|
||||
|
||||
const times = await response.json();
|
||||
|
||||
if (times && times.length > 0) {
|
||||
displayTimes(times);
|
||||
} else {
|
||||
showNoData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading times:', error);
|
||||
showNoData();
|
||||
}
|
||||
}
|
||||
|
||||
// Display times in the grid
|
||||
function displayTimes(times) {
|
||||
const timesGrid = document.getElementById('timesGrid');
|
||||
timesGrid.innerHTML = '';
|
||||
|
||||
times.forEach(time => {
|
||||
const timeCard = document.createElement('div');
|
||||
timeCard.className = 'time-card';
|
||||
|
||||
// Format time from recorded_time object
|
||||
let timeString = 'N/A';
|
||||
if (time.recorded_time) {
|
||||
const { minutes, seconds, milliseconds } = time.recorded_time;
|
||||
timeString = `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds}`;
|
||||
}
|
||||
|
||||
const userName = time.player ?
|
||||
`${time.player.firstname || ''} ${time.player.lastname || ''}`.trim() || 'Unknown Player' :
|
||||
'Unknown Player';
|
||||
const locationName = time.location ? time.location.name : 'Unknown Location';
|
||||
const createdAt = new Date(time.created_at).toLocaleString();
|
||||
|
||||
timeCard.innerHTML = `
|
||||
<h4>${userName}</h4>
|
||||
<p><strong>Time:</strong> <span class="time-value">${timeString}</span></p>
|
||||
<p><strong>Location:</strong> ${locationName}</p>
|
||||
<p><strong>Date:</strong> ${createdAt}</p>
|
||||
`;
|
||||
|
||||
timesGrid.appendChild(timeCard);
|
||||
});
|
||||
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('timesContent').style.display = 'block';
|
||||
}
|
||||
|
||||
// Show no data message
|
||||
function showNoData() {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
document.getElementById('noData').style.display = 'block';
|
||||
}
|
||||
|
||||
// Logout function
|
||||
async function logout() {
|
||||
try {
|
||||
const { error } = await supabase.auth.signOut();
|
||||
if (error) {
|
||||
console.error('Error logging out:', error);
|
||||
} else {
|
||||
// Refresh the page to update UI
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during logout:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for auth state changes
|
||||
supabase.auth.onAuthStateChange((event, session) => {
|
||||
if (event === 'SIGNED_OUT') {
|
||||
showPublicUI();
|
||||
} else if (event === 'SIGNED_IN' && session) {
|
||||
displayUserInfo(session.user);
|
||||
showAuthenticatedUI();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize page
|
||||
async function init() {
|
||||
await checkAuth();
|
||||
await loadTimes();
|
||||
}
|
||||
|
||||
// Start the application
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
371
public/login.html
Normal file
371
public/login.html
Normal file
@@ -0,0 +1,371 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ninja Server - Admin Login</title>
|
||||
<script src="https://unpkg.com/@supabase/supabase-js@2"></script>
|
||||
<style>
|
||||
* {
|
||||
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;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.logo h1 {
|
||||
color: #333;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.logo p {
|
||||
color: #666;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-container.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: #667eea;
|
||||
border: 2px solid #667eea;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toggle-form {
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.toggle-form button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #667eea;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.toggle-form button:hover {
|
||||
color: #764ba2;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 0.75rem;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="logo">
|
||||
<h1>🥷 Ninja Server</h1>
|
||||
<p>Secure Authentication Portal</p>
|
||||
</div>
|
||||
|
||||
<div id="message"></div>
|
||||
<div id="loading" class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Processing...</p>
|
||||
</div>
|
||||
|
||||
<!-- Login Form -->
|
||||
<div id="loginForm" class="form-container active">
|
||||
<h2 style="text-align: center; margin-bottom: 1.5rem; color: #333;">Welcome Back</h2>
|
||||
<form id="loginFormElement">
|
||||
<div class="form-group">
|
||||
<label for="loginEmail">Email</label>
|
||||
<input type="email" id="loginEmail" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="loginPassword">Password</label>
|
||||
<input type="password" id="loginPassword" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Sign In</button>
|
||||
</form>
|
||||
<div class="toggle-form">
|
||||
<p>Don't have an account? <button type="button" onclick="toggleForm()">Sign Up</button></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Register Form -->
|
||||
<div id="registerForm" class="form-container">
|
||||
<h2 style="text-align: center; margin-bottom: 1.5rem; color: #333;">Create Account</h2>
|
||||
<form id="registerFormElement">
|
||||
<div class="form-group">
|
||||
<label for="registerEmail">Email</label>
|
||||
<input type="email" id="registerEmail" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="registerPassword">Password</label>
|
||||
<input type="password" id="registerPassword" required minlength="6">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirmPassword">Confirm Password</label>
|
||||
<input type="password" id="confirmPassword" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Create Account</button>
|
||||
</form>
|
||||
<div class="toggle-form">
|
||||
<p>Already have an account? <button type="button" onclick="toggleForm()">Sign In</button></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 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);
|
||||
|
||||
// Check if user is already logged in
|
||||
async function checkAuth() {
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
if (session) {
|
||||
window.location.href = 'dashboard.html';
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle between login and register forms
|
||||
function toggleForm() {
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
const registerForm = document.getElementById('registerForm');
|
||||
|
||||
if (loginForm.classList.contains('active')) {
|
||||
loginForm.classList.remove('active');
|
||||
registerForm.classList.add('active');
|
||||
} else {
|
||||
registerForm.classList.remove('active');
|
||||
loginForm.classList.add('active');
|
||||
}
|
||||
clearMessage();
|
||||
}
|
||||
|
||||
// Show message
|
||||
function showMessage(message, type = 'success') {
|
||||
const messageDiv = document.getElementById('message');
|
||||
messageDiv.innerHTML = `<div class="message ${type}">${message}</div>`;
|
||||
setTimeout(() => {
|
||||
messageDiv.innerHTML = '';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Clear message
|
||||
function clearMessage() {
|
||||
document.getElementById('message').innerHTML = '';
|
||||
}
|
||||
|
||||
// Show/hide loading
|
||||
function setLoading(show) {
|
||||
const loading = document.getElementById('loading');
|
||||
if (show) {
|
||||
loading.classList.add('active');
|
||||
} else {
|
||||
loading.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle login
|
||||
document.getElementById('loginFormElement').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
clearMessage();
|
||||
|
||||
const email = document.getElementById('loginEmail').value;
|
||||
const password = document.getElementById('loginPassword').value;
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email: email,
|
||||
password: password
|
||||
});
|
||||
|
||||
if (error) {
|
||||
showMessage(error.message, 'error');
|
||||
} else {
|
||||
showMessage('Login successful! Redirecting...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = 'dashboard.html';
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('An unexpected error occurred', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle registration
|
||||
document.getElementById('registerFormElement').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
clearMessage();
|
||||
|
||||
const email = document.getElementById('registerEmail').value;
|
||||
const password = document.getElementById('registerPassword').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
showMessage('Passwords do not match', 'error');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
showMessage('Password must be at least 6 characters', 'error');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email: email,
|
||||
password: password
|
||||
});
|
||||
|
||||
if (error) {
|
||||
showMessage(error.message, 'error');
|
||||
} else {
|
||||
if (data.user && !data.user.email_confirmed_at) {
|
||||
showMessage('Registration successful! Please check your email to confirm your account.', 'success');
|
||||
} else {
|
||||
showMessage('Registration successful! Redirecting...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = 'dashboard.html';
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('An unexpected error occurred', 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Check authentication on page load
|
||||
checkAuth();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1001
routes/api.js
Normal file
1001
routes/api.js
Normal file
File diff suppressed because it is too large
Load Diff
76
routes/public.js
Normal file
76
routes/public.js
Normal file
@@ -0,0 +1,76 @@
|
||||
// routes/public.js
|
||||
const express = require('express');
|
||||
const { Pool } = require('pg');
|
||||
const router = express.Router();
|
||||
|
||||
// PostgreSQL Pool mit .env Konfiguration
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false
|
||||
});
|
||||
|
||||
// Fehlerbehandlung für Pool
|
||||
pool.on('error', (err) => {
|
||||
console.error('PostgreSQL Pool Fehler:', err);
|
||||
});
|
||||
|
||||
// Public endpoint für Standorte (keine Authentifizierung erforderlich)
|
||||
router.get('/locations', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT * FROM "GetLocations"');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.rows
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der getlocations:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Fehler beim Abrufen der Standorte'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Public route to get times for location with parameter
|
||||
router.get('/times', async (req, res) => {
|
||||
const { location } = req.query;
|
||||
|
||||
try {
|
||||
// First, let's check if the view exists and has data
|
||||
const viewCheck = await pool.query('SELECT COUNT(*) as count FROM "GetTimesWithPlayerAndLocation"');
|
||||
|
||||
// Check what location names are available
|
||||
const availableLocations = await pool.query('SELECT DISTINCT location_name FROM "GetTimesWithPlayerAndLocation"');
|
||||
|
||||
// Now search for the specific location
|
||||
const result = await pool.query('SELECT * FROM "GetTimesWithPlayerAndLocation" WHERE location_name = $1', [location]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.rows,
|
||||
debug: {
|
||||
searchedFor: location,
|
||||
totalRecords: viewCheck.rows[0].count,
|
||||
availableLocations: availableLocations.rows.map(r => r.location_name),
|
||||
foundRecords: result.rows.length
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Abrufen der Zeiten:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Fehler beim Abrufen der Zeiten',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
109
scripts/create-user.js
Normal file
109
scripts/create-user.js
Normal file
@@ -0,0 +1,109 @@
|
||||
// scripts/create-user.js
|
||||
const { Pool } = require('pg');
|
||||
const bcrypt = require('bcrypt');
|
||||
const readline = require('readline');
|
||||
require('dotenv').config();
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false
|
||||
});
|
||||
|
||||
// Readline Interface für Benutzereingaben
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
// Hilfsfunktion für Benutzereingaben
|
||||
function askQuestion(question) {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function createUser() {
|
||||
try {
|
||||
console.log('👤 Erstelle neuen Benutzer...\n');
|
||||
|
||||
// Verbindung testen
|
||||
await pool.query('SELECT NOW()');
|
||||
console.log('✅ Datenbankverbindung erfolgreich\n');
|
||||
|
||||
// Benutzereingaben abfragen
|
||||
const username = await askQuestion('Benutzername eingeben: ');
|
||||
if (!username) {
|
||||
console.log('❌ Benutzername darf nicht leer sein!');
|
||||
return;
|
||||
}
|
||||
|
||||
const password = await askQuestion('Passwort eingeben: ');
|
||||
if (!password) {
|
||||
console.log('❌ Passwort darf nicht leer sein!');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmPassword = await askQuestion('Passwort bestätigen: ');
|
||||
if (password !== confirmPassword) {
|
||||
console.log('❌ Passwörter stimmen nicht überein!');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('\n🔄 Erstelle Benutzer...');
|
||||
|
||||
// Prüfen ob Benutzer bereits existiert
|
||||
const existingUser = await pool.query(
|
||||
'SELECT id FROM adminusers WHERE username = $1',
|
||||
[username]
|
||||
);
|
||||
|
||||
if (existingUser.rows.length > 0) {
|
||||
console.log(`ℹ️ Benutzer "${username}" existiert bereits`);
|
||||
|
||||
const update = await askQuestion('Passwort aktualisieren? (j/n): ');
|
||||
if (update.toLowerCase() === 'j' || update.toLowerCase() === 'ja' || update.toLowerCase() === 'y' || update.toLowerCase() === 'yes') {
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
await pool.query(
|
||||
'UPDATE adminusers SET password_hash = $1 WHERE username = $2',
|
||||
[passwordHash, username]
|
||||
);
|
||||
console.log(`✅ Passwort für Benutzer "${username}" aktualisiert`);
|
||||
} else {
|
||||
console.log('❌ Vorgang abgebrochen');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Neuen Benutzer erstellen
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
await pool.query(
|
||||
'INSERT INTO adminusers (username, password_hash) VALUES ($1, $2)',
|
||||
[username, passwordHash]
|
||||
);
|
||||
console.log(`✅ Benutzer "${username}" erfolgreich erstellt`);
|
||||
}
|
||||
|
||||
console.log('\n📝 Anmeldedaten:');
|
||||
console.log(` Benutzername: ${username}`);
|
||||
console.log(` Passwort: ${password}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Erstellen des Benutzers:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
rl.close();
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
// Skript ausführen wenn direkt aufgerufen
|
||||
if (require.main === module) {
|
||||
createUser();
|
||||
}
|
||||
|
||||
module.exports = { createUser };
|
||||
107
scripts/init-db.js
Normal file
107
scripts/init-db.js
Normal file
@@ -0,0 +1,107 @@
|
||||
// scripts/init-db.js
|
||||
const { Pool } = require('pg');
|
||||
const bcrypt = require('bcrypt');
|
||||
require('dotenv').config();
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false
|
||||
});
|
||||
|
||||
async function initDatabase() {
|
||||
try {
|
||||
console.log('🚀 Initialisiere Datenbank...');
|
||||
|
||||
// Verbindung testen
|
||||
await pool.query('SELECT NOW()');
|
||||
console.log('✅ Datenbankverbindung erfolgreich');
|
||||
|
||||
// Adminusers Tabelle erstellen
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS adminusers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login TIMESTAMP
|
||||
)
|
||||
`);
|
||||
console.log('✅ adminusers Tabelle erstellt/überprüft');
|
||||
|
||||
// API Tokens Tabelle erstellen
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS api_tokens (
|
||||
id SERIAL PRIMARY KEY,
|
||||
token VARCHAR(255) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
standorte TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT true
|
||||
)
|
||||
`);
|
||||
console.log('✅ api_tokens Tabelle erstellt/überprüft');
|
||||
|
||||
// Locations Tabelle erstellen
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS locations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) UNIQUE NOT NULL,
|
||||
latitude DECIMAL(10, 8) NOT NULL,
|
||||
longitude DECIMAL(11, 8) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
console.log('✅ locations Tabelle erstellt/überprüft');
|
||||
|
||||
// Standardbenutzer erstellen (falls nicht vorhanden)
|
||||
const existingUser = await pool.query(
|
||||
'SELECT id FROM adminusers WHERE username = $1',
|
||||
['admin']
|
||||
);
|
||||
|
||||
if (existingUser.rows.length === 0) {
|
||||
const passwordHash = await bcrypt.hash('admin123', 10);
|
||||
await pool.query(
|
||||
'INSERT INTO adminusers (username, password_hash) VALUES ($1, $2)',
|
||||
['admin', passwordHash]
|
||||
);
|
||||
console.log('✅ Standardbenutzer "admin" mit Passwort "admin123" erstellt');
|
||||
} else {
|
||||
console.log('ℹ️ Standardbenutzer "admin" existiert bereits');
|
||||
}
|
||||
|
||||
// Index für bessere Performance
|
||||
await pool.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_adminusers_username ON adminusers(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_adminusers_active ON adminusers(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_tokens_token ON api_tokens(token);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_tokens_active ON api_tokens(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_locations_name ON locations(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_locations_coords ON locations(latitude, longitude);
|
||||
`);
|
||||
console.log('✅ Indizes erstellt/überprüft');
|
||||
|
||||
console.log('🎉 Datenbank erfolgreich initialisiert!');
|
||||
console.log('📝 Standardanmeldung: admin / admin123');
|
||||
console.log('⚠️ Ändern Sie das Standardpasswort in der Produktion!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler bei der Datenbankinitialisierung:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
// Skript ausführen wenn direkt aufgerufen
|
||||
if (require.main === module) {
|
||||
initDatabase();
|
||||
}
|
||||
|
||||
module.exports = { initDatabase };
|
||||
87
scripts/setup-players.js
Normal file
87
scripts/setup-players.js
Normal file
@@ -0,0 +1,87 @@
|
||||
// scripts/setup-players.js
|
||||
// Script to set up players table and fix the player name issue
|
||||
const { Pool } = require('pg');
|
||||
require('dotenv').config();
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false
|
||||
});
|
||||
|
||||
async function setupPlayers() {
|
||||
try {
|
||||
console.log('🚀 Setting up players table and views...');
|
||||
|
||||
// Test connection
|
||||
await pool.query('SELECT NOW()');
|
||||
console.log('✅ Database connection successful');
|
||||
|
||||
// Check if players table exists and has the expected structure
|
||||
const tableCheck = await pool.query(`
|
||||
SELECT column_name, data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'players'
|
||||
`);
|
||||
|
||||
if (tableCheck.rows.length === 0) {
|
||||
console.log('❌ Players table not found. Please create it first with the structure:');
|
||||
console.log(' - id (UUID)');
|
||||
console.log(' - firstname (VARCHAR)');
|
||||
console.log(' - lastname (VARCHAR)');
|
||||
console.log(' - birthdate (DATE)');
|
||||
console.log(' - created_at (TIMESTAMP)');
|
||||
console.log(' - rfiduid (VARCHAR)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('✅ Players table structure verified');
|
||||
console.log('📋 Available columns:', tableCheck.rows.map(r => r.column_name).join(', '));
|
||||
|
||||
// Create the updated view using your actual table structure
|
||||
await pool.query(`
|
||||
CREATE OR REPLACE VIEW "GetTimesWithPlayerAndLocation" AS
|
||||
SELECT
|
||||
gt.*,
|
||||
l.name as location_name,
|
||||
l.latitude,
|
||||
l.longitude,
|
||||
COALESCE(CONCAT(p.firstname, ' ', p.lastname), 'Unknown Player') as player_name
|
||||
FROM "gettimes" gt
|
||||
JOIN locations l ON gt.location_id = l.id
|
||||
LEFT JOIN players p ON gt.player_id = p.id
|
||||
`);
|
||||
console.log('✅ Updated view created');
|
||||
|
||||
// Test the view
|
||||
const testResult = await pool.query('SELECT COUNT(*) as count FROM "GetTimesWithPlayerAndLocation"');
|
||||
console.log(`✅ View test successful: ${testResult.rows[0].count} records found`);
|
||||
|
||||
// Show sample data
|
||||
const sampleData = await pool.query('SELECT player_name, location_name, recorded_time FROM "GetTimesWithPlayerAndLocation" LIMIT 3');
|
||||
console.log('📊 Sample data from view:');
|
||||
sampleData.rows.forEach((row, index) => {
|
||||
console.log(` ${index + 1}. ${row.player_name} at ${row.location_name}: ${row.recorded_time}`);
|
||||
});
|
||||
|
||||
console.log('\n🎉 Setup completed successfully!');
|
||||
console.log('📝 Your index.html should now display player names instead of IDs');
|
||||
console.log('🔄 Restart your server to use the new view');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error during setup:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
setupPlayers();
|
||||
}
|
||||
|
||||
module.exports = { setupPlayers };
|
||||
231
server.js
Normal file
231
server.js
Normal file
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* NinjaCross Leaderboard Server
|
||||
*
|
||||
* Hauptserver für das NinjaCross Timer-System mit:
|
||||
* - Express.js Web-Server
|
||||
* - Socket.IO für Real-time Updates
|
||||
* - PostgreSQL Datenbankanbindung
|
||||
* - API-Key Authentifizierung
|
||||
* - Session-basierte Web-Authentifizierung
|
||||
*
|
||||
* @author NinjaCross Team
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// DEPENDENCIES & IMPORTS
|
||||
// ============================================================================
|
||||
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const session = require('express-session');
|
||||
const { createServer } = require('http');
|
||||
const { Server } = require('socket.io');
|
||||
require('dotenv').config();
|
||||
|
||||
// Route Imports
|
||||
const { router: apiRoutes, requireApiKey } = require('./routes/api');
|
||||
const publicRoutes = require('./routes/public');
|
||||
|
||||
// ============================================================================
|
||||
// SERVER CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
const app = express();
|
||||
const server = createServer(app);
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
// Socket.IO Configuration
|
||||
const io = new Server(server, {
|
||||
cors: {
|
||||
origin: "*",
|
||||
methods: ["GET", "POST"]
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// MIDDLEWARE SETUP
|
||||
// ============================================================================
|
||||
|
||||
// Body Parser Middleware
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
|
||||
// Session Configuration
|
||||
app.use(session({
|
||||
secret: process.env.SESSION_SECRET || 'kjhdizr3lhwho8fpjslgf825ß0hsd',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: false, // Set to true when using HTTPS
|
||||
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
||||
httpOnly: true // Security: prevent XSS attacks
|
||||
}
|
||||
}));
|
||||
|
||||
// ============================================================================
|
||||
// AUTHENTICATION MIDDLEWARE
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Web Interface Authentication Middleware
|
||||
* Überprüft ob der Benutzer für das Web-Interface authentifiziert ist
|
||||
*/
|
||||
function requireWebAuth(req, res, next) {
|
||||
if (req.session.userId) {
|
||||
next();
|
||||
} else {
|
||||
res.redirect('/login');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ROUTE SETUP
|
||||
// ============================================================================
|
||||
|
||||
// Public API Routes (no authentication required)
|
||||
// Diese Routen sind für das Frontend-Leaderboard gedacht
|
||||
app.use('/public-api', publicRoutes);
|
||||
|
||||
// Private API Routes (API-Key authentication required)
|
||||
// Diese Routen sind für die Timer-Geräte und Admin-Interface
|
||||
app.use('/api', apiRoutes);
|
||||
|
||||
// ============================================================================
|
||||
// WEB INTERFACE ROUTES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Public Landing Page - NinjaCross Leaderboard
|
||||
* Hauptseite mit dem öffentlichen Leaderboard
|
||||
*/
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||
});
|
||||
|
||||
/**
|
||||
* Admin Generator Page
|
||||
* Geschützte Seite für die Lizenz-Generierung
|
||||
*/
|
||||
app.get('/generator', requireWebAuth, (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public', 'generator.html'));
|
||||
});
|
||||
|
||||
/**
|
||||
* Login Page
|
||||
* Authentifizierungsseite für Admin-Benutzer
|
||||
*/
|
||||
app.get('/login', (req, res) => {
|
||||
// Redirect to main page if already authenticated
|
||||
if (req.session.userId) {
|
||||
return res.redirect('/');
|
||||
}
|
||||
res.sendFile(path.join(__dirname, 'public', 'login.html'));
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// STATIC FILE SERVING
|
||||
// ============================================================================
|
||||
|
||||
// Serve static files for public pages (CSS, JS, images)
|
||||
app.use('/public', express.static('public'));
|
||||
|
||||
// Serve static files for login page
|
||||
app.use('/login', express.static('public'));
|
||||
|
||||
// ============================================================================
|
||||
// WEBSOCKET CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* WebSocket Connection Handler
|
||||
* Verwaltet Real-time Verbindungen für Live-Updates
|
||||
*/
|
||||
io.on('connection', (socket) => {
|
||||
// Client connected - connection is established
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
// Client disconnected - cleanup if needed
|
||||
});
|
||||
});
|
||||
|
||||
// Make Socket.IO instance available to other modules
|
||||
app.set('io', io);
|
||||
|
||||
// ============================================================================
|
||||
// ERROR HANDLING
|
||||
// ============================================================================
|
||||
|
||||
// 404 Handler
|
||||
app.use('*', (req, res) => {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: 'Route not found',
|
||||
path: req.originalUrl
|
||||
});
|
||||
});
|
||||
|
||||
// Global Error Handler
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Server Error:', err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// SERVER STARTUP
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Start the server and initialize all services
|
||||
*/
|
||||
server.listen(port, () => {
|
||||
console.log(`🚀 Server läuft auf http://ninja.reptilfpv.de:${port}`);
|
||||
console.log(`📊 Datenbank: ${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`);
|
||||
console.log(`🔐 API-Key Authentifizierung aktiviert`);
|
||||
console.log(`🔌 WebSocket-Server aktiviert`);
|
||||
console.log(`📁 Static files: /public`);
|
||||
console.log(`🌐 Public API: /public-api`);
|
||||
console.log(`🔑 Private API: /api`);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// GRACEFUL SHUTDOWN
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Handle graceful shutdown on SIGINT (Ctrl+C)
|
||||
*/
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\n🛑 Server wird heruntergefahren...');
|
||||
|
||||
// Close server gracefully
|
||||
server.close(() => {
|
||||
console.log('✅ Server erfolgreich heruntergefahren');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Force exit after 5 seconds if graceful shutdown fails
|
||||
setTimeout(() => {
|
||||
console.log('⚠️ Forced shutdown after timeout');
|
||||
process.exit(1);
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle uncaught exceptions
|
||||
*/
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('Uncaught Exception:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle unhandled promise rejections
|
||||
*/
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user