commit 897597bb54d8ddba5044f980441ce5446fb3e71d Author: cay Date: Fri Mar 27 10:17:15 2026 +0000 Initial commit: Wachplan v1.1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..daf1dd0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.env +*.log +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c3249c --- /dev/null +++ b/README.md @@ -0,0 +1,325 @@ +# PlusFit24 – Mitgliedschafts-Anmeldesystem +## Vollständige Installationsanleitung + +--- + +## 🏗️ Übersicht deiner Server-Infrastruktur + +``` +Internet + │ + ▼ +NGINX (192.168.0.157) ← SSL Zertifikat hier + │ plusfit24.software-joksch.com + │ Proxy → Port 3100 + ▼ +Node.js App Server (192.168.0.163:3100) + │ + ▼ +MariaDB (85.215.63.122:3306) +``` + +--- + +## SCHRITT 1: Datenbank einrichten (MariaDB 85.215.63.122) + +Verbinde dich mit deinem MariaDB Server: + +```bash +mysql -h 85.215.63.122 -u root -p +``` + +Führe das Schema aus: + +```bash +mysql -h 85.215.63.122 -u root -p < database/schema.sql +``` + +Erstelle einen dedizierten DB-User (sicherer als root): + +```sql +CREATE USER 'plusfit24'@'192.168.0.163' IDENTIFIED BY 'DEIN_SICHERES_PASSWORT'; +GRANT ALL PRIVILEGES ON plusfit24.* TO 'plusfit24'@'192.168.0.163'; +FLUSH PRIVILEGES; +``` + +⚠️ **Wichtig:** Ersetze `DEIN_SICHERES_PASSWORT` mit einem echten starken Passwort! + +--- + +## SCHRITT 2: App auf Node.js Server einrichten (192.168.0.163) + +### 2.1 Projekt auf den Server übertragen + +**Option A – via Gitea (empfohlen):** +```bash +# Auf dem Gitea-Server (192.168.0.221): Neues Repository anlegen +# Dann auf dem App-Server: +ssh user@192.168.0.163 +cd /var/www +git clone http://192.168.0.221/dein-user/plusfit24.git +cd plusfit24 +``` + +**Option B – direkt per SCP:** +```bash +# Von deinem lokalen Rechner: +scp -r /pfad/zu/plusfit24 user@192.168.0.163:/var/www/plusfit24 +``` + +### 2.2 Node.js installieren (falls noch nicht vorhanden) + +```bash +# Node.js 20 LTS installieren +curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - +sudo apt-get install -y nodejs + +# Version prüfen +node --version # sollte v20.x.x zeigen +npm --version +``` + +### 2.3 Abhängigkeiten installieren + +```bash +cd /var/www/plusfit24 +npm install +``` + +### 2.4 Umgebungsvariablen konfigurieren + +```bash +cp .env.example .env # oder direkt: +nano .env +``` + +Fülle die `.env` aus: + +```env +DB_HOST=85.215.63.122 +DB_PORT=3306 +DB_USER=plusfit24 +DB_PASSWORD=DEIN_SICHERES_PASSWORT ← gleich wie in Schritt 1 +DB_NAME=plusfit24 + +SESSION_SECRET=ein-sehr-langer-zufaelliger-string-hier-aendern + +ADMIN_USER=admin +ADMIN_PASSWORD=DeinSicheresAdminPasswort123! + +PORT=3100 +``` + +🔐 **Tipp:** Session Secret generieren: +```bash +node -e "console.log(require('crypto').randomBytes(48).toString('hex'))" +``` + +### 2.5 Test-Start (einmalig zum Prüfen) + +```bash +node app.js +# Ausgabe: 🚀 PlusFit24 Server läuft auf Port 3100 +# Ausgabe: ✅ Admin Account erstellt: admin +``` + +Mit Ctrl+C stoppen. + +--- + +## SCHRITT 3: PM2 installieren (Prozess-Manager) + +```bash +# PM2 global installieren +sudo npm install -g pm2 + +# App mit PM2 starten +cd /var/www/plusfit24 +pm2 start ecosystem.config.js + +# Status prüfen +pm2 status + +# Logs ansehen +pm2 logs plusfit24 + +# PM2 beim Systemstart automatisch starten +pm2 startup +# Den angezeigten Befehl kopieren und ausführen, dann: +pm2 save +``` + +**Wichtige PM2 Befehle:** +```bash +pm2 restart plusfit24 # App neu starten +pm2 stop plusfit24 # App stoppen +pm2 logs plusfit24 # Live-Logs anzeigen +pm2 monit # Monitor-Ansicht +``` + +--- + +## SCHRITT 4: NGINX konfigurieren (192.168.0.157) + +### 4.1 NGINX Konfiguration kopieren + +```bash +# Auf dem NGINX Server einloggen +ssh user@192.168.0.157 + +# Konfigurationsdatei erstellen +sudo nano /etc/nginx/sites-available/plusfit24 +``` + +Inhalt aus der Datei `nginx/plusfit24.conf` einfügen. + +⚠️ **SSL Pfade anpassen!** Suche im File nach: +``` +ssl_certificate /etc/ssl/certs/... +ssl_certificate_key /etc/ssl/private/... +``` + +Passe die Pfade auf deine tatsächlichen Zertifikat-Dateien an. + +### 4.2 Site aktivieren + +```bash +# Symlink erstellen +sudo ln -s /etc/nginx/sites-available/plusfit24 /etc/nginx/sites-enabled/ + +# Konfiguration prüfen +sudo nginx -t + +# Bei "syntax is ok": NGINX neu laden +sudo systemctl reload nginx +``` + +### 4.3 Firewall prüfen (falls aktiv) + +```bash +# Auf App-Server: Port 3100 für NGINX-Server freigeben +sudo ufw allow from 192.168.0.157 to any port 3100 +``` + +--- + +## SCHRITT 5: DNS einrichten + +Bei deinem DNS-Anbieter einen A-Record erstellen: + +``` +Typ: A +Name: plusfit24.software-joksch.com +Ziel: [Öffentliche IP deines NGINX-Servers] +TTL: 300 +``` + +--- + +## SCHRITT 6: Testen + +1. **Datenbank-Verbindung:** `node -e "require('./config/database').query('SELECT 1').then(()=>console.log('DB OK')).catch(console.error)"` + +2. **App läuft:** `pm2 status` → Status sollte "online" sein + +3. **Webseite:** https://plusfit24.software-joksch.com aufrufen + +4. **Admin-Bereich:** https://plusfit24.software-joksch.com/admin/login + - Benutzername: `admin` (oder was du in .env gesetzt hast) + - Passwort: aus der `.env` Datei + +--- + +## 📋 Admin-Bereich Anleitung + +### Tarife verwalten +- **Neuer Tarif:** Button "+ Neuer Tarif" → Formular ausfüllen +- **Deaktivieren:** Tarif-Card → "⏸ Deaktivieren" (Tarif bleibt erhalten, erscheint nicht mehr auf der Webseite) +- **Preis ändern:** Alten Tarif deaktivieren → Neuen Tarif mit neuem Preis erstellen +- **Löschen:** Nur möglich wenn keine Mitglieder den Tarif haben + +### Mitglieder ansehen +- Vollständige Liste aller angemeldeten Mitglieder +- Suchfunktion nach Name oder E-Mail +- ⚠️-Symbol zeigt Minderjährige + +### Passwort ändern +- Admin → Einstellungen → Passwort ändern + +--- + +## 🔧 Troubleshooting + +**App startet nicht:** +```bash +pm2 logs plusfit24 --lines 50 +# Häufige Ursachen: DB nicht erreichbar, .env fehlt, Port belegt +``` + +**DB-Verbindungsfehler:** +```bash +# Verbindung testen +mysql -h 85.215.63.122 -u plusfit24 -p plusfit24 +``` + +**NGINX 502 Bad Gateway:** +```bash +# Prüfen ob App läuft +pm2 status +# Prüfen ob Port erreichbar +curl http://192.168.0.163:3100 +``` + +**Nach Code-Updates:** +```bash +cd /var/www/plusfit24 +git pull # falls Gitea genutzt +npm install # falls neue Dependencies +pm2 restart plusfit24 +``` + +--- + +## 📁 Projektstruktur + +``` +plusfit24/ +├── app.js # Haupt-Einstiegspunkt +├── package.json +├── .env # ⚠️ Nicht in Git einchecken! +├── ecosystem.config.js # PM2 Konfiguration +├── config/ +│ └── database.js # DB-Verbindung +├── middleware/ +│ └── auth.js # Admin-Authentifizierung +├── routes/ +│ ├── index.js # Öffentliche Routen (Tarife, Anmeldung) +│ ├── admin.js # Admin-Routen +│ └── api.js # API (E-Mail-Prüfung, Formular-Submit) +├── views/ +│ ├── index.ejs # Tarif-Auswahl +│ ├── signup.ejs # Anmelde-Formular (4 Schritte) +│ ├── success.ejs # Erfolgsseite +│ ├── error.ejs # Fehlerseite +│ └── admin/ +│ ├── login.ejs # Admin Login +│ └── dashboard.ejs # Admin Dashboard +├── public/ +│ ├── css/style.css # Alle Styles +│ └── pdfs/ +│ └── Einverstaendniserklaerung.pdf +├── database/ +│ └── schema.sql # Datenbank-Schema +└── nginx/ + └── plusfit24.conf # NGINX Konfiguration +``` + +--- + +## 🔐 Sicherheitshinweise + +1. `.env` niemals in Git einchecken (ist in `.gitignore`) +2. Starke Passwörter für DB und Admin verwenden +3. DB-User nur von App-Server-IP Zugriff erlauben +4. SESSION_SECRET muss ein langer, zufälliger String sein +5. Admin-URL ist `/admin/login` – nicht öffentlich bewerben diff --git a/app.js b/app.js new file mode 100644 index 0000000..cf2da39 --- /dev/null +++ b/app.js @@ -0,0 +1,73 @@ +require('dotenv').config(); +const express = require('express'); +const session = require('express-session'); +const path = require('path'); +const bcrypt = require('bcryptjs'); +const db = require('./config/database'); + +const app = express(); + +// View Engine +app.set('view engine', 'ejs'); +app.set('views', path.join(__dirname, 'views')); + +// Static Files +app.use(express.static(path.join(__dirname, 'public'))); + +// Body Parser +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// Session +app.use(session({ + secret: process.env.SESSION_SECRET || 'plusfit24-secret', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // auf true setzen wenn HTTPS direkt (nicht via Proxy) + maxAge: 24 * 60 * 60 * 1000 // 24 Stunden + } +})); + +// Routen +const indexRouter = require('./routes/index'); +const adminRouter = require('./routes/admin'); +const apiRouter = require('./routes/api'); + +app.use('/', indexRouter); +app.use('/admin', adminRouter); +app.use('/api', apiRouter); + +// 404 Handler +app.use((req, res) => { + res.status(404).render('error', { message: 'Seite nicht gefunden' }); +}); + +// Fehler Handler +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).render('error', { message: 'Ein Fehler ist aufgetreten' }); +}); + +// Admin Account beim Start erstellen falls keiner existiert +async function initAdmin() { + try { + const [rows] = await db.query('SELECT COUNT(*) as count FROM admins'); + if (rows[0].count === 0) { + const hash = await bcrypt.hash(process.env.ADMIN_PASSWORD || 'Admin1234!', 12); + await db.query( + 'INSERT INTO admins (username, password_hash) VALUES (?, ?)', + [process.env.ADMIN_USER || 'admin', hash] + ); + console.log('✅ Admin Account erstellt:', process.env.ADMIN_USER || 'admin'); + } + } catch (err) { + console.error('❌ Fehler beim Erstellen des Admin Accounts:', err.message); + } +} + +const PORT = process.env.PORT || 3100; +app.listen(PORT, async () => { + console.log(`🚀 PlusFit24 Server läuft auf Port ${PORT}`); + await initAdmin(); +}); diff --git a/config/database.js b/config/database.js new file mode 100644 index 0000000..cf8b0e7 --- /dev/null +++ b/config/database.js @@ -0,0 +1,16 @@ +require('dotenv').config(); +const mysql = require('mysql2/promise'); + +const pool = mysql.createPool({ + host: process.env.DB_HOST, + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0, + charset: 'utf8mb4' +}); + +module.exports = pool; diff --git a/database/schema.sql b/database/schema.sql new file mode 100644 index 0000000..e8be017 --- /dev/null +++ b/database/schema.sql @@ -0,0 +1,85 @@ +-- ============================================ +-- PlusFit24 Datenbank Schema +-- Auf dem MariaDB Server ausführen +-- ============================================ + +CREATE DATABASE IF NOT EXISTS plusfit24 + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; + +USE plusfit24; + +-- Kategorien Tabelle +CREATE TABLE IF NOT EXISTS categories ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO categories (name) VALUES +('Single'), +('Schüler / Studenten / Vereine'), +('Family'), +('Sonstige'); + +-- Tarife Tabelle +CREATE TABLE IF NOT EXISTS tariffs ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + category_id INT, + duration_months INT NOT NULL, + price_monthly DECIMAL(10,2) NOT NULL, + start_package_price DECIMAL(10,2) DEFAULT 35.00, + description TEXT, + active TINYINT(1) DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL +); + +INSERT INTO tariffs (name, category_id, duration_months, price_monthly, start_package_price, description) VALUES +('Single 12 Monate', 1, 12, 29.95, 35.00, 'Einzelmitgliedschaft für 12 Monate'), +('Single 24 Monate', 1, 24, 19.95, 35.00, 'Einzelmitgliedschaft für 24 Monate'), +('Schüler - Studenten - Vereine 12 Monate', 2, 12, 28.95, 35.00, 'Ermäßigter Tarif'), +('Family 24 Monate', 3, 24, 49.95, 35.00, 'Familientarif für 24 Monate'); + +-- Mitgliedschaften Tabelle +CREATE TABLE IF NOT EXISTS memberships ( + id INT AUTO_INCREMENT PRIMARY KEY, + tariff_id INT NOT NULL, + salutation VARCHAR(20) NOT NULL, + title VARCHAR(50), + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + birth_date DATE NOT NULL, + email VARCHAR(255) NOT NULL, + phone VARCHAR(50), + street VARCHAR(200) NOT NULL, + address_addition VARCHAR(200), + zip VARCHAR(10) NOT NULL, + city VARCHAR(100) NOT NULL, + bank_name VARCHAR(200), + account_holder VARCHAR(200), + iban VARCHAR(34), + sepa_accepted TINYINT(1) DEFAULT 0, + agb_accepted TINYINT(1) DEFAULT 0, + datenschutz_accepted TINYINT(1) DEFAULT 0, + data_correct TINYINT(1) DEFAULT 0, + guardian_consent TINYINT(1) DEFAULT 0, + is_minor TINYINT(1) DEFAULT 0, + status VARCHAR(20) DEFAULT 'active', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (tariff_id) REFERENCES tariffs(id) +); + +-- Admin Tabelle +CREATE TABLE IF NOT EXISTS admins ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(100) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_memberships_email ON memberships(email); +CREATE INDEX idx_memberships_status ON memberships(status); +CREATE INDEX idx_tariffs_active ON tariffs(active); diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..514ca85 --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,24 @@ +// PM2 Ecosystem Konfiguration +// Starten mit: pm2 start ecosystem.config.js + +module.exports = { + apps: [ + { + name: 'plusfit24', + script: 'app.js', + cwd: '/var/www/plusfit24', + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '300M', + env: { + NODE_ENV: 'production', + PORT: 3100 + }, + error_file: '/var/log/pm2/plusfit24-error.log', + out_file: '/var/log/pm2/plusfit24-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss', + time: true + } + ] +}; diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..1d98d0b --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,8 @@ +function requireAdmin(req, res, next) { + if (req.session && req.session.adminId) { + return next(); + } + res.redirect('/admin/login'); +} + +module.exports = { requireAdmin }; diff --git a/nginx/plusfit24.conf b/nginx/plusfit24.conf new file mode 100644 index 0000000..4520884 --- /dev/null +++ b/nginx/plusfit24.conf @@ -0,0 +1,28 @@ +# ===================================================== +# NGINX Konfiguration für plusfit24.software-joksch.com +# Datei speichern unter: /etc/nginx/sites-available/plusfit24 +# Symlink: ln -s /etc/nginx/sites-available/plusfit24 /etc/nginx/sites-enabled/ +# ===================================================== + +server { + listen 443 ssl http2; + server_name plusfit24.software-joksch.com; + + # SSL – gleiche Zertifikate wie deine anderen vHosts + ssl_certificate /etc/nginx/ssl/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/software-joksch.com.key; + + client_max_body_size 10M; + + location / { + proxy_pass http://192.168.0.163:3100; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_redirect off; + proxy_read_timeout 3600; + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1b5db89 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "plusfit24-signup", + "version": "1.0.0", + "description": "PlusFit24 Mitgliedschaft Anmeldesystem", + "main": "app.js", + "scripts": { + "start": "node app.js", + "dev": "nodemon app.js" + }, + "dependencies": { + "express": "^4.18.2", + "express-session": "^1.17.3", + "ejs": "^3.1.9", + "mysql2": "^3.6.0", + "bcryptjs": "^2.4.3", + "dotenv": "^16.3.1", + "dns": "^0.2.2" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..ff01d5d --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,819 @@ +/* ========================================= + PlusFit24 – Stylesheet + ========================================= */ + +:root { + --primary: #2d2dcc; + --primary-dark: #1a1a9e; + --accent: #5555ff; + --text: #1a1a2e; + --text-muted: #6b7280; + --border: #e2e4ed; + --bg: #f8f9ff; + --white: #ffffff; + --success: #16a34a; + --error: #dc2626; + --warning: #d97706; + --radius: 12px; + --shadow: 0 2px 16px rgba(45,45,204,0.08); +} + +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: 'Outfit', sans-serif; + color: var(--text); + background: var(--white); + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* ---- HEADER ---- */ +.site-header { + border-bottom: 1px solid var(--border); + padding: 16px 0; + background: var(--white); + position: sticky; + top: 0; + z-index: 100; +} +.header-inner { + max-width: 1100px; + margin: 0 auto; + padding: 0 24px; +} +.logo { + font-size: 1.6rem; + font-weight: 800; + color: var(--text); + letter-spacing: -0.5px; +} +.logo span { color: var(--primary); } + +/* ---- PROGRESS NAV ---- */ +.progress-nav { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 20px 24px; + border-bottom: 1px solid var(--border); + flex-wrap: wrap; +} +.step { + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 1.5px; + color: var(--text-muted); + text-transform: uppercase; + padding: 4px 0; + position: relative; + cursor: default; +} +.step.active { + color: var(--primary); + border-bottom: 2px solid var(--primary); +} +.step.done { color: var(--success); } +.step-dot { + width: 20px; + height: 1px; + background: var(--border); +} + +/* ---- TARIF SECTION ---- */ +.tarif-section { + max-width: 1000px; + margin: 0 auto; + padding: 48px 24px; + flex: 1; +} +.page-title { + font-size: 2.5rem; + font-weight: 800; + text-align: center; + margin-bottom: 40px; + color: var(--text); +} +.page-title.italic { font-style: italic; font-weight: 400; font-size: 1.5rem; } + +.tarif-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 24px; +} +.tarif-card { + border: 1.5px solid var(--border); + border-radius: 20px; + overflow: hidden; + transition: box-shadow 0.2s, transform 0.2s; +} +.tarif-card:hover { + box-shadow: var(--shadow); + transform: translateY(-2px); +} +.tarif-card-inner { padding: 28px 24px; } +.tarif-badge { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.8rem; + color: var(--text-muted); + margin-bottom: 12px; +} +.tarif-icon { font-size: 1rem; } +.tarif-name { + font-size: 1.5rem; + font-weight: 700; + margin-bottom: 16px; + line-height: 1.2; +} +.tarif-feature { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.9rem; + color: var(--text-muted); + margin-bottom: 20px; +} +.tarif-price { + display: flex; + align-items: baseline; + gap: 4px; + margin-bottom: 24px; +} +.price-amount { + font-size: 1.8rem; + font-weight: 800; + color: var(--primary); +} +.price-period { font-size: 0.9rem; color: var(--text-muted); } +.no-tarifs { + grid-column: 1/-1; + text-align: center; + color: var(--text-muted); + padding: 60px 20px; +} + +/* ---- BUTTONS ---- */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 12px 24px; + border-radius: 10px; + font-family: 'Outfit', sans-serif; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + border: 2px solid transparent; + text-decoration: none; + transition: all 0.2s; + white-space: nowrap; +} +.btn-primary { + background: var(--primary); + color: var(--white); + border-color: var(--primary); +} +.btn-primary:hover { background: var(--primary-dark); border-color: var(--primary-dark); } +.btn-outline { + background: transparent; + color: var(--text); + border-color: var(--border); +} +.btn-outline:hover { border-color: var(--primary); color: var(--primary); } +.btn-full { width: 100%; } +.btn-success { background: var(--success); color: white; border-color: var(--success); } +.btn-warning { background: var(--warning); color: white; border-color: var(--warning); } +.btn-danger { background: var(--error); color: white; border-color: var(--error); } +.btn-sm { padding: 6px 12px; font-size: 0.82rem; border-radius: 7px; } +.btn-submit { + width: 100%; + padding: 18px; + background: var(--primary); + color: white; + border: none; + border-radius: 12px; + font-size: 1.1rem; + font-weight: 700; + cursor: pointer; + transition: background 0.2s; +} +.btn-submit:hover { background: var(--primary-dark); } +.btn-submit:disabled { opacity: 0.6; cursor: not-allowed; } + +/* ---- SIGNUP FORM ---- */ +.signup-main { flex: 1; } +.signup-container { + max-width: 680px; + margin: 0 auto; + padding: 32px 24px 60px; +} +.form-step { display: none; } +.form-step.active { display: block; } + +.step-title { + font-size: 2rem; + font-weight: 800; + text-align: center; + margin-bottom: 8px; +} +.step-title.italic { font-style: italic; font-weight: 400; font-size: 1.3rem; } +.step-subtitle { + text-align: center; + color: var(--text-muted); + margin-bottom: 32px; + font-style: italic; +} + +/* Anrede Buttons */ +.salutation-group { + display: flex; + gap: 12px; + margin-bottom: 28px; + flex-wrap: wrap; +} +.sal-btn { + flex: 1; + min-width: 100px; + padding: 12px; + border: 1.5px solid var(--border); + border-radius: 10px; + background: white; + font-family: 'Outfit', sans-serif; + font-weight: 600; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.2s; +} +.sal-btn:hover { border-color: var(--primary); color: var(--primary); } +.sal-btn.active { border-color: var(--primary); background: var(--primary); color: white; } + +/* Form Groups */ +.form-group { margin-bottom: 16px; } +.form-group label { + display: block; + font-size: 0.88rem; + font-weight: 600; + margin-bottom: 6px; + color: var(--text); +} +.req { color: var(--error); } +.input-wrap { + display: flex; + align-items: center; + border: 1.5px solid var(--border); + border-radius: 10px; + padding: 0 14px; + background: white; + transition: border-color 0.2s; +} +.input-wrap:focus-within { border-color: var(--primary); } +.input-icon { font-size: 0.9rem; margin-right: 10px; opacity: 0.5; flex-shrink: 0; } +.input-wrap input, +.input-wrap textarea, +.input-wrap select { + flex: 1; + border: none; + outline: none; + padding: 13px 0; + font-family: 'Outfit', sans-serif; + font-size: 0.95rem; + color: var(--text); + background: transparent; +} +.input-wrap input::placeholder { color: #9ca3af; } +.email-status { font-size: 1rem; margin-left: 8px; } +.email-message { font-size: 0.8rem; margin-top: 4px; padding-left: 4px; } +.email-message.error { color: var(--error); } + +/* Checkboxes */ +.checkbox-label { + display: flex; + align-items: flex-start; + gap: 12px; + cursor: pointer; + margin-bottom: 14px; + line-height: 1.5; + font-size: 0.9rem; +} +.checkbox-label input[type="checkbox"] { display: none; } +.checkbox-custom { + width: 22px; + height: 22px; + min-width: 22px; + border: 2px solid var(--border); + border-radius: 5px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + margin-top: 2px; +} +.checkbox-label input:checked + .checkbox-custom { + background: var(--primary); + border-color: var(--primary); +} +.checkbox-label input:checked + .checkbox-custom::after { + content: '✓'; + color: white; + font-size: 0.8rem; + font-weight: 700; +} + +/* SEPA Mandate */ +.sepa-mandate { + background: #f0f0ff; + border: 1.5px solid var(--primary); + border-radius: 10px; + padding: 16px; + margin-bottom: 24px; + font-size: 0.85rem; + line-height: 1.6; +} +.form-label-section { + display: block; + font-weight: 700; + font-size: 0.9rem; + margin-bottom: 12px; + color: var(--text-muted); +} + +/* Step Buttons */ +.step-buttons { + display: flex; + justify-content: center; + gap: 16px; + margin-top: 32px; +} +.step-buttons .btn { min-width: 140px; } + +/* Alerts */ +.alert { + padding: 12px 16px; + border-radius: 8px; + margin-bottom: 16px; + font-size: 0.9rem; +} +.alert-success { background: #dcfce7; color: var(--success); border: 1px solid #bbf7d0; } +.alert-error { background: #fee2e2; color: var(--error); border: 1px solid #fecaca; } +.hidden { display: none !important; } + +/* Warning Box */ +.minor-warning { margin-top: 16px; } +.warning-box { + background: #fffbeb; + border: 1.5px solid var(--warning); + border-radius: 10px; + padding: 16px; + font-size: 0.9rem; +} +.warning-box strong { display: block; margin-bottom: 6px; } +.minor-info-box { + background: #fffbeb; + border: 1.5px solid var(--warning); + border-radius: 10px; + padding: 16px; + margin-bottom: 16px; + font-size: 0.9rem; +} +.minor-info-box strong { display: block; margin-bottom: 8px; } +.minor-info-box p { margin-bottom: 10px; } + +/* ---- ABSCHLUSS ---- */ +.abschluss-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + margin-bottom: 32px; +} +@media (max-width: 640px) { .abschluss-grid { grid-template-columns: 1fr; } } + +.tarif-summary-card { + border: 1.5px solid var(--border); + border-radius: 16px; + padding: 24px; +} +.summary-badge { + font-size: 0.8rem; + color: var(--text-muted); + margin-bottom: 12px; +} +.tarif-summary-card h3 { font-size: 1.3rem; font-weight: 700; margin-bottom: 16px; } +.summary-feature { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.85rem; + color: var(--text-muted); + margin-bottom: 8px; +} +.summary-price { + display: flex; + align-items: baseline; + gap: 4px; + margin-top: 16px; +} +.price-big { font-size: 2rem; font-weight: 800; color: var(--primary); } + +.final-checks { padding: 4px 0; } +.final-checks h3 { font-size: 1.2rem; font-weight: 700; margin-bottom: 16px; } +.doc-buttons { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 20px; +} +.doc-btn { + padding: 8px 14px; + border: 1.5px solid var(--border); + border-radius: 8px; + font-size: 0.8rem; + font-weight: 600; + text-decoration: none; + color: var(--text); + transition: all 0.2s; + display: inline-block; +} +.doc-btn:hover { border-color: var(--primary); color: var(--primary); } + +.submit-section { text-align: center; padding: 20px 0; } +.submit-notice { font-size: 0.9rem; margin-bottom: 16px; } +.ssl-notice { font-size: 0.8rem; color: var(--text-muted); margin-top: 12px; } + +/* ---- SUCCESS ---- */ +.success-main { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 40px 24px; +} +.success-card { + text-align: center; + max-width: 480px; + padding: 48px 32px; + border: 1.5px solid var(--border); + border-radius: 20px; +} +.success-icon { font-size: 3rem; margin-bottom: 16px; } +.success-card h1 { font-size: 1.8rem; font-weight: 800; margin-bottom: 12px; } +.success-card p { color: var(--text-muted); margin-bottom: 8px; } +.success-sub { font-size: 0.9rem; margin-bottom: 24px !important; } + +/* ---- FOOTER ---- */ +.site-footer { + text-align: center; + padding: 24px; + border-top: 1px solid var(--border); + font-size: 0.85rem; + color: var(--text-muted); + margin-top: auto; +} +.site-footer a { color: var(--primary); text-decoration: none; } + +/* ================================================ + ADMIN STYLES + ================================================ */ +.admin-body { background: var(--bg); } + +/* Admin Login */ +.admin-login-wrap { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; +} +.admin-login-card { + background: white; + border-radius: 20px; + padding: 48px 40px; + width: 100%; + max-width: 420px; + box-shadow: var(--shadow); + border: 1.5px solid var(--border); +} +.admin-logo { margin-bottom: 24px; } +.admin-login-card h1 { font-size: 1.5rem; font-weight: 700; margin-bottom: 24px; } + +/* Admin Layout */ +.admin-layout { + display: flex; + min-height: 100vh; +} + +/* Sidebar */ +.admin-sidebar { + width: 240px; + min-width: 240px; + background: var(--text); + color: white; + display: flex; + flex-direction: column; + padding: 24px 0; + position: sticky; + top: 0; + height: 100vh; + overflow-y: auto; +} +.admin-sidebar .logo { padding: 0 24px 32px; color: white; } +.admin-sidebar .logo span { color: #7c7cff; } +.admin-nav { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; +} +.nav-link { + display: block; + padding: 12px 24px; + color: rgba(255,255,255,0.7); + text-decoration: none; + font-size: 0.9rem; + font-weight: 500; + transition: all 0.2s; +} +.nav-link:hover, .nav-link.active { + background: rgba(255,255,255,0.1); + color: white; +} +.sidebar-footer { + padding: 16px 24px; + border-top: 1px solid rgba(255,255,255,0.15); + font-size: 0.85rem; + color: rgba(255,255,255,0.6); +} +.logout-link { + display: block; + color: #ff6b6b; + text-decoration: none; + margin-top: 8px; + font-weight: 600; +} + +/* Admin Main */ +.admin-main { + flex: 1; + padding: 32px; + overflow-y: auto; +} +.admin-header h1 { + font-size: 2rem; + font-weight: 800; + margin-bottom: 24px; +} + +/* Stats */ +.stats-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 16px; + margin-bottom: 32px; +} +.stat-card { + background: white; + border: 1.5px solid var(--border); + border-radius: 14px; + padding: 20px; + text-align: center; +} +.stat-number { font-size: 2rem; font-weight: 800; color: var(--primary); } +.stat-label { font-size: 0.8rem; color: var(--text-muted); margin-top: 4px; } + +/* Admin Section */ +.admin-section { margin-bottom: 40px; } +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; +} +.section-header h2 { font-size: 1.4rem; font-weight: 700; } + +/* Tariff Admin Cards */ +.tariff-admin-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; +} +.tariff-admin-card { + background: white; + border: 1.5px solid var(--border); + border-radius: 14px; + padding: 20px; +} +.tariff-admin-card.inactive { opacity: 0.65; } +.tariff-admin-header { margin-bottom: 10px; } +.tariff-status-badge { + font-size: 0.78rem; + font-weight: 600; + padding: 3px 10px; + border-radius: 20px; +} +.tariff-status-badge.active { background: #dcfce7; color: var(--success); } +.tariff-status-badge.inactive { background: #fee2e2; color: var(--error); } +.tariff-admin-card h3 { font-size: 1.05rem; font-weight: 700; margin-bottom: 12px; } +.tariff-admin-details { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 0.85rem; + color: var(--text-muted); + margin-bottom: 16px; +} +.tariff-admin-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +/* Table */ +.table-wrap { overflow-x: auto; background: white; border-radius: 14px; border: 1.5px solid var(--border); } +.admin-table { + width: 100%; + border-collapse: collapse; + font-size: 0.88rem; +} +.admin-table th { + padding: 12px 16px; + text-align: left; + background: var(--bg); + font-weight: 700; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-muted); + border-bottom: 1.5px solid var(--border); +} +.admin-table td { + padding: 12px 16px; + border-bottom: 1px solid var(--border); + vertical-align: middle; +} +.admin-table tr:last-child td { border-bottom: none; } +.admin-table tr:hover td { background: var(--bg); } +.iban-cell { font-family: monospace; font-size: 0.8rem; } +.no-data { text-align: center; color: var(--text-muted); padding: 32px !important; } + +/* Search */ +.search-input { + padding: 8px 14px; + border: 1.5px solid var(--border); + border-radius: 8px; + font-family: 'Outfit', sans-serif; + font-size: 0.9rem; + outline: none; + width: 260px; +} +.search-input:focus { border-color: var(--primary); } + +/* Settings */ +.settings-card { + background: white; + border: 1.5px solid var(--border); + border-radius: 14px; + padding: 28px; + max-width: 480px; +} + +/* Form Controls (Admin) */ +.form-control { + width: 100%; + padding: 10px 14px; + border: 1.5px solid var(--border); + border-radius: 8px; + font-family: 'Outfit', sans-serif; + font-size: 0.92rem; + outline: none; + transition: border-color 0.2s; +} +.form-control:focus { border-color: var(--primary); } +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +/* Modal */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 16px; +} +.modal { + background: white; + border-radius: 16px; + width: 100%; + max-width: 520px; + max-height: 90vh; + overflow-y: auto; +} +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px; + border-bottom: 1.5px solid var(--border); +} +.modal-header h3 { font-size: 1.1rem; font-weight: 700; } +.modal-close { + background: none; + border: none; + font-size: 1.1rem; + cursor: pointer; + color: var(--text-muted); + padding: 4px 8px; + border-radius: 6px; +} +.modal-close:hover { background: var(--bg); } +.modal form { padding: 24px; } +.modal .form-group { margin-bottom: 16px; } +.modal .form-group label { display: block; font-size: 0.88rem; font-weight: 600; margin-bottom: 6px; } +.modal-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + padding-top: 16px; + border-top: 1.5px solid var(--border); + margin-top: 8px; +} + +/* Responsive */ +@media (max-width: 768px) { + .admin-layout { flex-direction: column; } + .admin-sidebar { width: 100%; height: auto; position: relative; } + .admin-nav { flex-direction: row; overflow-x: auto; } + .page-title { font-size: 1.8rem; } + .form-row { grid-template-columns: 1fr; } +} + +/* ---- KATEGORIEN ---- */ +.category-list { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 20px; +} +.category-row { + display: flex; + align-items: center; + justify-content: space-between; + background: white; + border: 1.5px solid var(--border); + border-radius: 12px; + padding: 14px 20px; + gap: 16px; +} +.category-info { + display: flex; + align-items: center; + gap: 16px; +} +.category-name { + font-weight: 700; + font-size: 1rem; +} +.category-meta { + font-size: 0.8rem; + color: var(--text-muted); + background: var(--bg); + padding: 3px 10px; + border-radius: 20px; +} +.category-actions { + display: flex; + gap: 8px; + flex-shrink: 0; +} +.category-pill { + font-size: 0.75rem; + font-weight: 600; + background: #ede9fe; + color: #5b21b6; + padding: 3px 10px; + border-radius: 20px; +} +.no-data-card { + padding: 40px; + text-align: center; + color: var(--text-muted); + background: white; + border: 1.5px dashed var(--border); + border-radius: 14px; +} +.info-box { + background: #eff6ff; + border: 1.5px solid #bfdbfe; + border-radius: 10px; + padding: 14px 18px; + font-size: 0.88rem; + color: #1e40af; +} diff --git a/public/pdfs/Einverstaendniserklaerung.pdf b/public/pdfs/Einverstaendniserklaerung.pdf new file mode 100644 index 0000000..0bf5c2e Binary files /dev/null and b/public/pdfs/Einverstaendniserklaerung.pdf differ diff --git a/routes/admin.js b/routes/admin.js new file mode 100644 index 0000000..dfad734 --- /dev/null +++ b/routes/admin.js @@ -0,0 +1,170 @@ +const express = require('express'); +const router = express.Router(); +const bcrypt = require('bcryptjs'); +const db = require('../config/database'); +const { requireAdmin } = require('../middleware/auth'); + +// Login +router.get('/login', (req, res) => { + if (req.session.adminId) return res.redirect('/admin'); + res.render('admin/login', { error: null }); +}); + +router.post('/login', async (req, res) => { + const { username, password } = req.body; + try { + const [admins] = await db.query('SELECT * FROM admins WHERE username = ?', [username]); + if (admins.length === 0) return res.render('admin/login', { error: 'Ungültige Anmeldedaten.' }); + const valid = await bcrypt.compare(password, admins[0].password_hash); + if (!valid) return res.render('admin/login', { error: 'Ungültige Anmeldedaten.' }); + req.session.adminId = admins[0].id; + req.session.adminUser = admins[0].username; + res.redirect('/admin'); + } catch (err) { + res.render('admin/login', { error: 'Serverfehler.' }); + } +}); + +router.get('/logout', (req, res) => { + req.session.destroy(); + res.redirect('/admin/login'); +}); + +// Dashboard +router.get('/', requireAdmin, async (req, res) => { + try { + const [tariffs] = await db.query(` + SELECT t.*, c.name as category_name + FROM tariffs t LEFT JOIN categories c ON t.category_id = c.id + ORDER BY t.active DESC, t.created_at DESC + `); + const [categories] = await db.query('SELECT * FROM categories ORDER BY name ASC'); + const [memberships] = await db.query(` + SELECT m.*, t.name as tariff_name, t.price_monthly + FROM memberships m LEFT JOIN tariffs t ON m.tariff_id = t.id + ORDER BY m.created_at DESC + `); + const [stats] = await db.query(` + SELECT + COUNT(*) as total, + SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active_count, + SUM(CASE WHEN is_minor = 1 THEN 1 ELSE 0 END) as minors, + SUM(CASE WHEN created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 ELSE 0 END) as last_30_days + FROM memberships + `); + res.render('admin/dashboard', { + tariffs, categories, memberships, stats: stats[0], + admin: req.session.adminUser, + success: req.query.success || null, + error: req.query.error || null + }); + } catch (err) { + console.error(err); + res.render('admin/dashboard', { + tariffs: [], categories: [], memberships: [], stats: {}, + admin: req.session.adminUser, + success: null, error: 'Datenbankfehler: ' + err.message + }); + } +}); + +// ===== KATEGORIEN ===== +router.post('/categories', requireAdmin, async (req, res) => { + const { name } = req.body; + if (!name || !name.trim()) return res.redirect('/admin?error=Kategoriename+fehlt'); + try { + await db.query('INSERT INTO categories (name) VALUES (?)', [name.trim()]); + res.redirect('/admin?success=Kategorie+erstellt#kategorien'); + } catch (err) { + res.redirect('/admin?error=Fehler+beim+Erstellen'); + } +}); + +router.post('/categories/:id/update', requireAdmin, async (req, res) => { + const { name } = req.body; + if (!name || !name.trim()) return res.redirect('/admin?error=Kategoriename+fehlt'); + try { + await db.query('UPDATE categories SET name = ? WHERE id = ?', [name.trim(), req.params.id]); + res.redirect('/admin?success=Kategorie+aktualisiert#kategorien'); + } catch (err) { + res.redirect('/admin?error=Fehler+beim+Aktualisieren'); + } +}); + +router.post('/categories/:id/delete', requireAdmin, async (req, res) => { + try { + const [used] = await db.query('SELECT COUNT(*) as c FROM tariffs WHERE category_id = ?', [req.params.id]); + if (used[0].c > 0) { + return res.redirect('/admin?error=Kategorie+wird+von+' + used[0].c + '+Tarifen+verwendet+–+bitte+erst+Tarife+umziehen#kategorien'); + } + await db.query('DELETE FROM categories WHERE id = ?', [req.params.id]); + res.redirect('/admin?success=Kategorie+gelöscht#kategorien'); + } catch (err) { + res.redirect('/admin?error=Fehler+beim+Löschen'); + } +}); + +// ===== TARIFE ===== +router.post('/tariffs', requireAdmin, async (req, res) => { + const { name, category_id, duration_months, price_monthly, start_package_price, description } = req.body; + try { + await db.query( + 'INSERT INTO tariffs (name, category_id, duration_months, price_monthly, start_package_price, description) VALUES (?, ?, ?, ?, ?, ?)', + [name, category_id || null, duration_months, price_monthly, start_package_price || 35.00, description || ''] + ); + res.redirect('/admin?success=Tarif+erstellt'); + } catch (err) { + res.redirect('/admin?error=Fehler+beim+Erstellen+des+Tarifs'); + } +}); + +router.post('/tariffs/:id/toggle', requireAdmin, async (req, res) => { + try { + await db.query('UPDATE tariffs SET active = NOT active WHERE id = ?', [req.params.id]); + res.redirect('/admin?success=Tarif+aktualisiert'); + } catch (err) { + res.redirect('/admin?error=Fehler+beim+Aktualisieren'); + } +}); + +router.post('/tariffs/:id/update', requireAdmin, async (req, res) => { + const { name, category_id, duration_months, price_monthly, start_package_price, description } = req.body; + try { + await db.query( + 'UPDATE tariffs SET name=?, category_id=?, duration_months=?, price_monthly=?, start_package_price=?, description=? WHERE id=?', + [name, category_id || null, duration_months, price_monthly, start_package_price, description, req.params.id] + ); + res.redirect('/admin?success=Tarif+aktualisiert'); + } catch (err) { + res.redirect('/admin?error=Fehler+beim+Aktualisieren'); + } +}); + +router.post('/tariffs/:id/delete', requireAdmin, async (req, res) => { + try { + const [members] = await db.query('SELECT COUNT(*) as c FROM memberships WHERE tariff_id = ?', [req.params.id]); + if (members[0].c > 0) return res.redirect('/admin?error=Tarif+hat+Mitglieder+–+bitte+deaktivieren'); + await db.query('DELETE FROM tariffs WHERE id = ?', [req.params.id]); + res.redirect('/admin?success=Tarif+gelöscht'); + } catch (err) { + res.redirect('/admin?error=Fehler+beim+Löschen'); + } +}); + +// Passwort ändern +router.post('/change-password', requireAdmin, async (req, res) => { + const { current_password, new_password, confirm_password } = req.body; + if (new_password !== confirm_password) return res.redirect('/admin?error=Passwörter+stimmen+nicht+überein'); + try { + const [admins] = await db.query('SELECT * FROM admins WHERE id = ?', [req.session.adminId]); + const valid = await bcrypt.compare(current_password, admins[0].password_hash); + if (!valid) return res.redirect('/admin?error=Aktuelles+Passwort+falsch'); + const hash = await bcrypt.hash(new_password, 12); + await db.query('UPDATE admins SET password_hash = ? WHERE id = ?', [hash, req.session.adminId]); + res.redirect('/admin?success=Passwort+geändert'); + } catch (err) { + res.redirect('/admin?error=Fehler+beim+Ändern+des+Passworts'); + } +}); + +module.exports = router; diff --git a/routes/api.js b/routes/api.js new file mode 100644 index 0000000..753db82 --- /dev/null +++ b/routes/api.js @@ -0,0 +1,105 @@ +const express = require('express'); +const router = express.Router(); +const dns = require('dns').promises; +const db = require('../config/database'); + +// Email Validierung via DNS MX-Record Check +async function verifyEmailDomain(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) return { valid: false, reason: 'Ungültiges E-Mail-Format' }; + + const domain = email.split('@')[1]; + try { + const records = await dns.resolveMx(domain); + if (records && records.length > 0) { + return { valid: true }; + } + return { valid: false, reason: 'Domain hat keine E-Mail-Server (MX-Records fehlen)' }; + } catch (err) { + return { valid: false, reason: 'E-Mail-Domain konnte nicht verifiziert werden' }; + } +} + +// POST /api/verify-email +router.post('/verify-email', async (req, res) => { + const { email } = req.body; + if (!email) return res.json({ valid: false, reason: 'Keine E-Mail angegeben' }); + + const result = await verifyEmailDomain(email); + res.json(result); +}); + +// POST /api/submit-membership +router.post('/submit-membership', async (req, res) => { + try { + const { + tariff_id, salutation, title, first_name, last_name, birth_date, + email, phone, street, address_addition, zip, city, + bank_name, account_holder, iban, + sepa_accepted, agb_accepted, datenschutz_accepted, data_correct, + guardian_consent + } = req.body; + + // E-Mail validieren + const emailCheck = await verifyEmailDomain(email); + if (!emailCheck.valid) { + return res.json({ success: false, error: 'E-Mail-Adresse ist nicht erreichbar: ' + emailCheck.reason }); + } + + // Pflichtfelder prüfen + if (!tariff_id || !first_name || !last_name || !birth_date || !email || !street || !zip || !city) { + return res.json({ success: false, error: 'Bitte alle Pflichtfelder ausfüllen.' }); + } + + if (!agb_accepted || !datenschutz_accepted || !data_correct) { + return res.json({ success: false, error: 'Bitte alle Einverständniserklärungen bestätigen.' }); + } + + // Alter berechnen + const birthDateObj = new Date(birth_date); + const today = new Date(); + let age = today.getFullYear() - birthDateObj.getFullYear(); + const m = today.getMonth() - birthDateObj.getMonth(); + if (m < 0 || (m === 0 && today.getDate() < birthDateObj.getDate())) age--; + + const is_minor = age < 18 ? 1 : 0; + + if (is_minor && !guardian_consent) { + return res.json({ success: false, error: 'Bei Minderjährigen ist die Einverständniserklärung der Erziehungsberechtigten erforderlich.' }); + } + + if (age < 14) { + return res.json({ success: false, error: 'Das Mindestalter für eine Mitgliedschaft beträgt 14 Jahre.' }); + } + + // Tarif prüfen + const [tariffs] = await db.query('SELECT * FROM tariffs WHERE id = ? AND active = 1', [tariff_id]); + if (tariffs.length === 0) { + return res.json({ success: false, error: 'Ungültiger oder inaktiver Tarif.' }); + } + + // In DB speichern + await db.query(` + INSERT INTO memberships + (tariff_id, salutation, title, first_name, last_name, birth_date, email, phone, + street, address_addition, zip, city, bank_name, account_holder, iban, + sepa_accepted, agb_accepted, datenschutz_accepted, data_correct, guardian_consent, is_minor) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, [ + tariff_id, salutation, title || '', first_name, last_name, birth_date, + email, phone || '', street, address_addition || '', zip, city, + bank_name || '', account_holder || '', iban || '', + sepa_accepted ? 1 : 0, agb_accepted ? 1 : 0, + datenschutz_accepted ? 1 : 0, data_correct ? 1 : 0, + guardian_consent ? 1 : 0, is_minor + ]); + + res.json({ success: true }); + + } catch (err) { + console.error('Submit error:', err); + res.json({ success: false, error: 'Serverfehler. Bitte versuche es später erneut.' }); + } +}); + +module.exports = router; diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..42d576e --- /dev/null +++ b/routes/index.js @@ -0,0 +1,40 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../config/database'); + +// Startseite - Tarife anzeigen +router.get('/', async (req, res) => { + try { + const [tariffs] = await db.query( + 'SELECT * FROM tariffs WHERE active = 1 ORDER BY price_monthly ASC' + ); + res.render('index', { tariffs, error: null }); + } catch (err) { + console.error(err); + res.render('index', { tariffs: [], error: 'Tarife konnten nicht geladen werden.' }); + } +}); + +// Anmelde-Formular für gewählten Tarif +router.get('/anmelden/:tariffId', async (req, res) => { + try { + const [tariffs] = await db.query( + 'SELECT * FROM tariffs WHERE id = ? AND active = 1', + [req.params.tariffId] + ); + if (tariffs.length === 0) { + return res.redirect('/'); + } + res.render('signup', { tariff: tariffs[0] }); + } catch (err) { + console.error(err); + res.redirect('/'); + } +}); + +// Erfolgsseite +router.get('/erfolg', (req, res) => { + res.render('success'); +}); + +module.exports = router; diff --git a/views/admin/dashboard.ejs b/views/admin/dashboard.ejs new file mode 100644 index 0000000..9d6b6e8 --- /dev/null +++ b/views/admin/dashboard.ejs @@ -0,0 +1,385 @@ + + + + + + PlusFit24 – Admin Dashboard + + + + +
+ + + +
+
+

Tarife

+
+
+
<%= stats.total || 0 %>
+
Gesamt Mitglieder
+
+
+
<%= stats.active_count || 0 %>
+
Aktive Mitglieder
+
+
+
<%= stats.last_30_days || 0 %>
+
Letzte 30 Tage
+
+
+
<%= stats.minors || 0 %>
+
Minderjährige
+
+
+
+ + <% if (success) { %>
<%= success %>
<% } %> + <% if (error) { %>
<%= error %>
<% } %> + + +
+
+

Tarife verwalten

+ +
+
+ <% tariffs.forEach(tariff => { %> +
+
+ + <%= tariff.active ? '✅ Aktiv' : '❌ Inaktiv' %> + + <% if (tariff.category_name) { %> + <%= tariff.category_name %> + <% } %> +
+

<%= tariff.name %>

+
+ 💰 <%= Number(tariff.price_monthly).toFixed(2) %>€/Monat + 📅 <%= tariff.duration_months %> Monate + 📦 Startpaket: <%= Number(tariff.start_package_price).toFixed(2) %>€ +
+
+ +
+ +
+
+ +
+
+
+ <% }) %> + <% if (tariffs.length === 0) { %> +
Noch keine Tarife angelegt.
+ <% } %> +
+
+ + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + diff --git a/views/admin/login.ejs b/views/admin/login.ejs new file mode 100644 index 0000000..bc39306 --- /dev/null +++ b/views/admin/login.ejs @@ -0,0 +1,40 @@ + + + + + + PlusFit24 – Admin Login + + + + +
+ +
+ + diff --git a/views/error.ejs b/views/error.ejs new file mode 100644 index 0000000..ab00d28 --- /dev/null +++ b/views/error.ejs @@ -0,0 +1,20 @@ + + + + + PlusFit24 – Fehler + + + + + +
+
+
⚠️
+

Fehler

+

<%= message %>

+ Zurück zur Startseite +
+
+ + diff --git a/views/index.ejs b/views/index.ejs new file mode 100644 index 0000000..99f6539 --- /dev/null +++ b/views/index.ejs @@ -0,0 +1,76 @@ + + + + + + PlusFit24 – Tarif Wählen + + + + + + + +
+ + + +
+

Tarif Wählen

+ + <% if (error) { %> +
<%= error %>
+ <% } %> + +
+ <% tariffs.forEach(tariff => { %> +
+
+
+ + <%= tariff.name %> +
+

<%= tariff.name %>

+
+ 📦 + Startpaket <%= Number(tariff.start_package_price).toFixed(2).replace('.', ',') %>€/einmalig +
+
+ <%= Number(tariff.price_monthly).toFixed(2).replace('.', ',') %>€ + /Monat +
+ + Tarif auswählen + +
+
+ <% }) %> + + <% if (tariffs.length === 0) { %> +
+

Aktuell sind keine Tarife verfügbar. Bitte versuche es später erneut.

+
+ <% } %> +
+
+
+ + + + diff --git a/views/signup.ejs b/views/signup.ejs new file mode 100644 index 0000000..5023594 --- /dev/null +++ b/views/signup.ejs @@ -0,0 +1,473 @@ + + + + + + PlusFit24 – Mitgliedschaft abschließen + + + + + + + +
+ + + + +
+ + + + + + diff --git a/views/success.ejs b/views/success.ejs new file mode 100644 index 0000000..fcf8a8e --- /dev/null +++ b/views/success.ejs @@ -0,0 +1,30 @@ + + + + + + PlusFit24 – Anmeldung erfolgreich + + + + + + +
+
+
+

Herzlich Willkommen!

+

Deine Mitgliedschaft wurde erfolgreich abgeschlossen. Wir freuen uns auf dich!

+

Du erhältst in Kürze eine Bestätigung.

+ Zurück zur Startseite +
+
+ + +