From 2fa84d88bed853a88efdb37f331b3abc86c700d0 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Sun, 19 Oct 2025 12:58:06 +0200 Subject: [PATCH] Add email functionality to AuthService; implement password reset email feature with nodemailer, including transporter initialization and email template for user notifications. --- backend/SQL_EXECUTION_ORDER.md | 178 ++++++++++++++++++++++++++++ backend/reset-user-password.js | 96 +++++++++++++++ backend/src/services/AuthService.js | 88 ++++++++++++++ backend/test-email.js | 115 ++++++++++++++++++ backend/test-password.js | 45 +++++++ 5 files changed, 522 insertions(+) create mode 100644 backend/SQL_EXECUTION_ORDER.md create mode 100755 backend/reset-user-password.js create mode 100755 backend/test-email.js create mode 100644 backend/test-password.js diff --git a/backend/SQL_EXECUTION_ORDER.md b/backend/SQL_EXECUTION_ORDER.md new file mode 100644 index 0000000..97ab0d1 --- /dev/null +++ b/backend/SQL_EXECUTION_ORDER.md @@ -0,0 +1,178 @@ +# TimeClock v3 - SQL-Scripts Ausführungsreihenfolge + +Diese Anleitung zeigt, welche SQL-Scripts in welcher Reihenfolge ausgeführt werden müssen. + +## Auf dem Server ausführen: + +```bash +cd /var/www/timeclock/backend + +# DB-Credentials aus .env +DB_HOST=$(grep DB_HOST .env | cut -d= -f2) +DB_USER=$(grep DB_USER .env | cut -d= -f2) +DB_PASSWORD=$(grep DB_PASSWORD .env | cut -d= -f2) +DB_NAME=$(grep DB_NAME .env | cut -d= -f2) + +# Alias für einfachere Befehle +alias dbexec="mysql -h $DB_HOST -u $DB_USER -p'$DB_PASSWORD' $DB_NAME" +``` + +## 1. Basis-Schema (WICHTIG - zuerst!) + +```bash +# Erstellt alle Haupt-Tabellen +dbexec < database-schema.sql + +# Zeige erstellte Tabellen +dbexec -e "SHOW TABLES;" +``` + +## 2. Zusätzliche Tabellen + +```bash +# Invitation Table +dbexec < create-invitation-table.sql + +# Watcher Table +dbexec < create-watcher-table.sql +``` + +## 3. Indices für Performance + +```bash +# Sick Index +dbexec < add-sick-index.sql + +# Vacation Index +dbexec < add-vacation-index.sql +``` + +## 4. Timewish Setup (korrigierte Version!) + +```bash +# Timewish mit Zeiträumen +dbexec < setup-timewish-complete.sql + +# ODER falls noch Fehler: Nur die Spalten hinzufügen +dbexec << 'EOF' +-- Prüfe ob start_date existiert +SET @col_exists = (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'timewish' + AND COLUMN_NAME = 'start_date'); + +SET @sql = IF(@col_exists = 0, + 'ALTER TABLE timewish ADD COLUMN start_date DATE DEFAULT ''2023-01-01'' AFTER end_time;', + 'SELECT ''start_date existiert bereits'';' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- end_date +SET @col_exists = (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'timewish' + AND COLUMN_NAME = 'end_date'); + +SET @sql = IF(@col_exists = 0, + 'ALTER TABLE timewish ADD COLUMN end_date DATE DEFAULT NULL AFTER start_date;', + 'SELECT ''end_date existiert bereits'';' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; +EOF +``` + +## 5. Optionale Scripts (nur bei Bedarf!) + +### User als Admin setzen +```bash +# Passe user_id an! (Deine User-ID aus der users-Tabelle) +dbexec < set-user-admin.sql +``` + +### Timezone-Fixes (nur falls Probleme mit Zeitzonen) +```bash +# Worklog Timezone korrigieren +dbexec < fix-worklog-timezone.sql +``` + +### Overtime Offset Update +```bash +dbexec < update-overtime-offset.sql +``` + +### Wednesday-Fix (nur falls benötigt) +```bash +dbexec < add-missing-wednesday.sql +``` + +## Überprüfung + +```bash +# Zeige alle Tabellen +dbexec -e "SHOW TABLES;" + +# Zeige timewish-Struktur +dbexec -e "DESCRIBE timewish;" + +# Zeige Anzahl User +dbexec -e "SELECT COUNT(*) AS user_count FROM users;" + +# Zeige deine Timewishes +dbexec -e "SELECT * FROM timewish WHERE user_id = 1 ORDER BY day, start_date;" +``` + +## Nach SQL-Ausführung: Backend neu starten + +```bash +# Backend neu starten damit es die neuen Tabellen/Spalten erkennt +pm2 restart timeclock-backend + +# Logs prüfen +pm2 logs timeclock-backend --lines 30 + +# Sollte zeigen: +# 🕐 TimeClock Server läuft auf Port 3010 +# 📍 API verfügbar unter http://localhost:3010/api +``` + +## Schnell-Befehl (alle wichtigen Scripts): + +```bash +cd /var/www/timeclock/backend + +# DB-Credentials setzen +DB_HOST=$(grep DB_HOST .env | cut -d= -f2) +DB_USER=$(grep DB_USER .env | cut -d= -f2) +DB_PASSWORD=$(grep DB_PASSWORD .env | cut -d= -f2) +DB_NAME=$(grep DB_NAME .env | cut -d= -f2) + +echo "Führe SQL-Scripts aus..." + +# 1. Basis-Schema +mysql -h $DB_HOST -u $DB_USER -p"$DB_PASSWORD" $DB_NAME < database-schema.sql 2>&1 | grep -v "already exists" || true + +# 2. Zusätzliche Tabellen +mysql -h $DB_HOST -u $DB_USER -p"$DB_PASSWORD" $DB_NAME < create-invitation-table.sql 2>&1 | grep -v "already exists" || true +mysql -h $DB_HOST -u $DB_USER -p"$DB_PASSWORD" $DB_NAME < create-watcher-table.sql 2>&1 | grep -v "already exists" || true + +# 3. Indices +mysql -h $DB_HOST -u $DB_USER -p"$DB_PASSWORD" $DB_NAME < add-sick-index.sql 2>&1 || true +mysql -h $DB_HOST -u $DB_USER -p"$DB_PASSWORD" $DB_NAME < add-vacation-index.sql 2>&1 || true + +# 4. Timewish (korrigiert) +mysql -h $DB_HOST -u $DB_USER -p"$DB_PASSWORD" $DB_NAME < setup-timewish-complete.sql + +echo "✅ SQL-Scripts ausgeführt!" + +# Backend neu starten +pm2 restart timeclock-backend + +echo "✅ Backend neu gestartet!" +``` + +Kopiere diesen Schnell-Befehl auf den Server und führe ihn aus! 🚀 + diff --git a/backend/reset-user-password.js b/backend/reset-user-password.js new file mode 100755 index 0000000..8fae63c --- /dev/null +++ b/backend/reset-user-password.js @@ -0,0 +1,96 @@ +#!/usr/bin/env node + +// Reset User Password für TimeClock v3 +// Verwendung: node reset-user-password.js "email@example.com" "neues-passwort" + +require('dotenv').config(); +const bcrypt = require('bcrypt'); +const mysql = require('mysql2/promise'); + +async function resetPassword() { + const email = process.argv[2]; + const newPassword = process.argv[3]; + + if (!email || !newPassword) { + console.error('❌ Verwendung: node reset-user-password.js "email@example.com" "neues-passwort"'); + console.error(''); + console.error('Beispiel:'); + console.error(' node reset-user-password.js "tsschulz@gmx.net" "MeinNeuesPasswort123!"'); + process.exit(1); + } + + console.log('🔐 Setze neues Passwort für TimeClock v3...'); + console.log('Email:', email); + console.log(''); + + try { + // Datenbank-Verbindung + const connection = await mysql.createConnection({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME + }); + + console.log('✅ Datenbank verbunden'); + + // Neuen Hash erstellen (Node.js bcrypt, kompatibel mit der neuen App) + const saltRounds = 10; + const passwordHash = await bcrypt.hash(newPassword, saltRounds); + + console.log('✅ Passwort gehasht'); + console.log('Neuer Hash:', passwordHash); + console.log('Hash-Format:', passwordHash.substring(0, 4)); + console.log(''); + + // Update authinfo + const [result] = await connection.execute( + `UPDATE authinfo + SET password_hash = ?, + password_method = 'bcrypt', + email = ?, + status = 1, + failed_login_attempts = 0 + WHERE email = ? OR unverified_email = ?`, + [passwordHash, email, email, email] + ); + + if (result.affectedRows === 0) { + console.error('❌ Kein Benutzer mit dieser E-Mail gefunden!'); + console.log(''); + console.log('Verfügbare Benutzer:'); + const [users] = await connection.execute( + 'SELECT id, user_id, email, unverified_email FROM authinfo' + ); + console.table(users); + process.exit(1); + } + + console.log('✅ Passwort erfolgreich aktualisiert!'); + console.log('Betroffene Zeilen:', result.affectedRows); + console.log(''); + + // Zeige aktualisierte Daten + const [authInfo] = await connection.execute( + 'SELECT id, user_id, email, password_method, status, failed_login_attempts FROM authinfo WHERE email = ?', + [email] + ); + + console.log('📋 Aktualisierte AuthInfo:'); + console.table(authInfo); + + console.log(''); + console.log('🎉 Fertig! Du kannst dich jetzt mit folgenden Daten einloggen:'); + console.log(' Email:', email); + console.log(' Passwort:', newPassword); + + await connection.end(); + + } catch (error) { + console.error('❌ Fehler:', error.message); + process.exit(1); + } +} + +resetPassword(); + diff --git a/backend/src/services/AuthService.js b/backend/src/services/AuthService.js index 9568abf..828ddf8 100644 --- a/backend/src/services/AuthService.js +++ b/backend/src/services/AuthService.js @@ -1,6 +1,7 @@ const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const crypto = require('crypto'); +const nodemailer = require('nodemailer'); const database = require('../config/database'); /** @@ -12,6 +13,28 @@ class AuthService { this.jwtSecret = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; this.jwtExpiration = process.env.JWT_EXPIRATION || '24h'; this.saltRounds = 10; + this.emailTransporter = null; + this.initializeEmailTransporter(); + } + + /** + * Initialisiert den Email-Transporter + */ + initializeEmailTransporter() { + if (process.env.EMAIL_HOST && process.env.EMAIL_USER && process.env.EMAIL_PASSWORD) { + this.emailTransporter = nodemailer.createTransport({ + host: process.env.EMAIL_HOST, + port: parseInt(process.env.EMAIL_PORT) || 587, + secure: process.env.EMAIL_SECURE === 'true', + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASSWORD + } + }); + console.log('✅ Email-Transporter konfiguriert'); + } else { + console.warn('⚠️ Email-Konfiguration fehlt - Passwort-Reset-E-Mails können nicht versendet werden'); + } } /** @@ -234,6 +257,71 @@ class AuthService { email_token_role: 1 // 1 = Password Reset }); + // E-Mail versenden (falls konfiguriert) + if (this.emailTransporter) { + const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5010'; + const resetUrl = `${frontendUrl}/password-reset?token=${resetToken}`; + + const mailOptions = { + from: `"${process.env.EMAIL_FROM_NAME || 'TimeClock'}" <${process.env.EMAIL_FROM}>`, + to: email, + subject: 'TimeClock - Passwort zurücksetzen', + text: `Hallo, + +du hast eine Passwort-Zurücksetzen-Anfrage für dein TimeClock-Konto gestellt. + +Klicke auf folgenden Link um dein Passwort zurückzusetzen: +${resetUrl} + +Dieser Link ist 1 Stunde gültig. + +Falls du diese Anfrage nicht gestellt hast, ignoriere diese E-Mail. + +--- +TimeClock Zeiterfassung +${frontendUrl} +`, + html: ` +
+

Passwort zurücksetzen

+

Hallo,

+

du hast eine Passwort-Zurücksetzen-Anfrage für dein TimeClock-Konto gestellt.

+

+ + Passwort zurücksetzen + +

+

+ Oder kopiere diesen Link in deinen Browser:
+ ${resetUrl} +

+

+ Dieser Link ist 1 Stunde gültig. +

+

+ Falls du diese Anfrage nicht gestellt hast, ignoriere diese E-Mail. +

+
+

+ TimeClock Zeiterfassung
+ ${frontendUrl} +

+
+ ` + }; + + try { + await this.emailTransporter.sendMail(mailOptions); + console.log('✅ Passwort-Reset E-Mail gesendet an:', email); + } catch (error) { + console.error('❌ Fehler beim E-Mail-Versand:', error.message); + // Werfe keinen Fehler - Token wurde gespeichert, User kann es anders nutzen + } + } else { + console.warn('⚠️ E-Mail-Transporter nicht konfiguriert - Reset-Token erstellt aber keine E-Mail versendet'); + console.log('Reset-Token:', resetToken); + } + return resetToken; } diff --git a/backend/test-email.js b/backend/test-email.js new file mode 100755 index 0000000..191e766 --- /dev/null +++ b/backend/test-email.js @@ -0,0 +1,115 @@ +#!/usr/bin/env node + +// Test E-Mail-Konfiguration +// Verwendung: node test-email.js "empfaenger@example.com" + +require('dotenv').config(); +const nodemailer = require('nodemailer'); + +const recipientEmail = process.argv[2] || 'tsschulz@gmx.net'; + +console.log('📧 Teste E-Mail-Konfiguration...'); +console.log(''); +console.log('SMTP-Server:', process.env.EMAIL_HOST); +console.log('Port:', process.env.EMAIL_PORT); +console.log('Secure:', process.env.EMAIL_SECURE); +console.log('User:', process.env.EMAIL_USER); +console.log('From:', process.env.EMAIL_FROM); +console.log('An:', recipientEmail); +console.log(''); + +// Erstelle Transporter +const transporter = nodemailer.createTransport({ + host: process.env.EMAIL_HOST, + port: parseInt(process.env.EMAIL_PORT), + secure: process.env.EMAIL_SECURE === 'true', + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASSWORD + } +}); + +// Test-E-Mail senden +async function sendTestEmail() { + try { + console.log('🔄 Verbinde mit SMTP-Server...'); + + // Verify connection + await transporter.verify(); + console.log('✅ SMTP-Verbindung erfolgreich!'); + console.log(''); + + console.log('📤 Sende Test-E-Mail...'); + + const info = await transporter.sendMail({ + from: `"${process.env.EMAIL_FROM_NAME || 'TimeClock'}" <${process.env.EMAIL_FROM}>`, + to: recipientEmail, + subject: 'TimeClock v3 - Test E-Mail', + text: `Dies ist eine Test-E-Mail von TimeClock v3. + +Wenn du diese E-Mail erhältst, funktioniert die E-Mail-Konfiguration korrekt! + +Gesendet am: ${new Date().toLocaleString('de-DE')} + +--- +TimeClock v3 Zeiterfassung +${process.env.FRONTEND_URL || 'https://stechuhr3.tsschulz.de'} +`, + html: ` +

TimeClock v3 - Test E-Mail

+

Dies ist eine Test-E-Mail von TimeClock v3.

+

Wenn du diese E-Mail erhältst, funktioniert die E-Mail-Konfiguration korrekt!

+

Gesendet am: ${new Date().toLocaleString('de-DE')}

+
+

+ TimeClock v3 Zeiterfassung
+ ${process.env.FRONTEND_URL || 'https://stechuhr3.tsschulz.de'} +

+ ` + }); + + console.log(''); + console.log('✅ E-Mail erfolgreich gesendet!'); + console.log(''); + console.log('Message ID:', info.messageId); + console.log('Response:', info.response); + console.log(''); + console.log('🎉 E-Mail-Konfiguration funktioniert!'); + console.log(''); + console.log('📬 Prüfe dein E-Mail-Postfach:', recipientEmail); + console.log(' (inkl. Spam-Ordner)'); + + process.exit(0); + + } catch (error) { + console.error(''); + console.error('❌ Fehler beim E-Mail-Versand:'); + console.error(''); + console.error(error.message); + console.error(''); + + if (error.code === 'EAUTH') { + console.error('💡 SMTP-Authentifizierung fehlgeschlagen!'); + console.error(' Prüfe EMAIL_USER und EMAIL_PASSWORD in .env'); + } else if (error.code === 'ECONNECTION') { + console.error('💡 Verbindung zum SMTP-Server fehlgeschlagen!'); + console.error(' Prüfe EMAIL_HOST und EMAIL_PORT in .env'); + } else if (error.code === 'ETIMEDOUT') { + console.error('💡 Timeout beim Verbinden!'); + console.error(' Prüfe Firewall-Einstellungen (Port', process.env.EMAIL_PORT, ')'); + } + + console.error(''); + console.error('Aktuelle Konfiguration:'); + console.error(' EMAIL_HOST:', process.env.EMAIL_HOST); + console.error(' EMAIL_PORT:', process.env.EMAIL_PORT); + console.error(' EMAIL_SECURE:', process.env.EMAIL_SECURE); + console.error(' EMAIL_USER:', process.env.EMAIL_USER); + console.error(' EMAIL_FROM:', process.env.EMAIL_FROM); + + process.exit(1); + } +} + +sendTestEmail(); + diff --git a/backend/test-password.js b/backend/test-password.js new file mode 100644 index 0000000..582066e --- /dev/null +++ b/backend/test-password.js @@ -0,0 +1,45 @@ +#!/usr/bin/env node + +// Test-Script für Passwort-Verifizierung +// Verwendung: node test-password.js "dein-passwort" + +const bcrypt = require('bcrypt'); + +const hash = '$2y$07$cSnPaBjKKlmtOTKtbTDTTuYQ0bZ0upNQfNWf7gf8OKiz8eEjwSGFG'; +const password = process.argv[2]; + +if (!password) { + console.error('❌ Verwendung: node test-password.js "dein-passwort"'); + process.exit(1); +} + +console.log('🔐 Teste Passwort-Verifizierung...'); +console.log('Hash:', hash); +console.log('Hash-Format:', hash.substring(0, 4)); +console.log(''); + +// bcrypt.compare kann $2y$ Hashes lesen (PHP-Format) +bcrypt.compare(password, hash) + .then(isMatch => { + if (isMatch) { + console.log('✅ Passwort ist KORREKT!'); + console.log(''); + console.log('Das Passwort-Hashing funktioniert.'); + console.log('Problem liegt woanders (z.B. Salt, Email-Matching, etc.)'); + } else { + console.log('❌ Passwort ist FALSCH!'); + console.log(''); + console.log('Entweder:'); + console.log('1. Falsches Passwort eingegeben'); + console.log('2. Hash wurde anders erstellt (Salt-Problem)'); + console.log('3. Passwort muss neu gesetzt werden'); + } + }) + .catch(err => { + console.error('❌ Fehler beim Vergleich:', err.message); + console.log(''); + console.log('Mögliche Ursachen:'); + console.log('- $2y$ Format wird nicht unterstützt'); + console.log('- Hash ist korrupt'); + }); +