Add email functionality to AuthService; implement password reset email feature with nodemailer, including transporter initialization and email template for user notifications.

This commit is contained in:
Torsten Schulz (local)
2025-10-19 12:58:06 +02:00
parent 31d5d95a78
commit 2fa84d88be
5 changed files with 522 additions and 0 deletions

View File

@@ -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! 🚀

96
backend/reset-user-password.js Executable file
View File

@@ -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();

View File

@@ -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: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #333;">Passwort zurücksetzen</h2>
<p>Hallo,</p>
<p>du hast eine Passwort-Zurücksetzen-Anfrage für dein TimeClock-Konto gestellt.</p>
<p>
<a href="${resetUrl}" style="display: inline-block; padding: 12px 24px; background-color: #4CAF50; color: white; text-decoration: none; border-radius: 4px; margin: 16px 0;">
Passwort zurücksetzen
</a>
</p>
<p style="color: #666; font-size: 14px;">
Oder kopiere diesen Link in deinen Browser:<br>
<code style="background: #f5f5f5; padding: 4px 8px; border-radius: 4px; display: inline-block; margin-top: 8px;">${resetUrl}</code>
</p>
<p style="color: #666; font-size: 14px;">
<strong>Dieser Link ist 1 Stunde gültig.</strong>
</p>
<p style="color: #666; font-size: 14px;">
Falls du diese Anfrage nicht gestellt hast, ignoriere diese E-Mail.
</p>
<hr style="border: none; border-top: 1px solid #ddd; margin: 24px 0;">
<p style="color: #999; font-size: 12px;">
TimeClock Zeiterfassung<br>
<a href="${frontendUrl}">${frontendUrl}</a>
</p>
</div>
`
};
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;
}

115
backend/test-email.js Executable file
View File

@@ -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: `
<h2>TimeClock v3 - Test E-Mail</h2>
<p>Dies ist eine Test-E-Mail von TimeClock v3.</p>
<p><strong>Wenn du diese E-Mail erhältst, funktioniert die E-Mail-Konfiguration korrekt!</strong></p>
<p>Gesendet am: ${new Date().toLocaleString('de-DE')}</p>
<hr>
<p style="color: #666; font-size: 12px;">
TimeClock v3 Zeiterfassung<br>
<a href="${process.env.FRONTEND_URL || 'https://stechuhr3.tsschulz.de'}">${process.env.FRONTEND_URL || 'https://stechuhr3.tsschulz.de'}</a>
</p>
`
});
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();

45
backend/test-password.js Normal file
View File

@@ -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');
});