Merge-Konflikt aufgelöst: AuthService und ErrorHandler wiederhergestellt
- AuthService mit allen Methoden (register, login, forgotPassword, resetPassword, logout) - ErrorHandler für zentrale Fehlerbehandlung - authController.js auf neue Clean Code Architektur umgestellt - Alle Authentifizierungsendpunkte verwenden jetzt Service Layer Pattern
This commit is contained in:
7
.env
7
.env
@@ -1,7 +0,0 @@
|
|||||||
SMTP_HOST=smtp.1blu.de
|
|
||||||
SMTP_PORT=465
|
|
||||||
SMTP_USER=e226079_0-kontakt
|
|
||||||
SMTP_PASS=hitomisan
|
|
||||||
SMTP_FROM=kontakt@tsschulz.de
|
|
||||||
FRONTEND_URL=http://localhost:8080
|
|
||||||
VUE_APP_BACKEND_URL=http://localhost:3002/api
|
|
||||||
19
.env.example
Normal file
19
.env.example
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# E-Mail-Konfiguration für Passwort-Reset
|
||||||
|
SMTP_HOST=smtp.1blu.de
|
||||||
|
SMTP_PORT=465
|
||||||
|
SMTP_USER=your-email@domain.com
|
||||||
|
SMTP_PASS=your-password
|
||||||
|
SMTP_FROM=noreply@miriamgemeinde.de
|
||||||
|
|
||||||
|
# Frontend-URL für Reset-Links
|
||||||
|
FRONTEND_URL=http://localhost:8080
|
||||||
|
|
||||||
|
# Backend-URL für das Frontend
|
||||||
|
VUE_APP_BACKEND_URL=http://localhost:3002/api
|
||||||
|
|
||||||
|
# Datenbank-Konfiguration (falls benötigt)
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_NAME=miriamgemeinde
|
||||||
|
DB_USER=miriam_user
|
||||||
|
DB_PASS=your-database-password
|
||||||
@@ -1,29 +1,59 @@
|
|||||||
const AuthService = require('../services/AuthService');
|
const AuthService = require('../services/AuthService');
|
||||||
const ErrorHandler = require('../utils/ErrorHandler');
|
const ErrorHandler = require('../utils/ErrorHandler');
|
||||||
|
|
||||||
exports.register = ErrorHandler.asyncHandler(async (req, res) => {
|
/**
|
||||||
const result = await AuthService.register(req.body);
|
* Controller für Authentifizierungsendpunkte
|
||||||
ErrorHandler.successResponse(res, result, 'Benutzer erfolgreich registriert', 201);
|
*/
|
||||||
});
|
class AuthController {
|
||||||
|
/**
|
||||||
|
* Benutzerregistrierung
|
||||||
|
*/
|
||||||
|
static register = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
|
const result = await AuthService.register(req.body);
|
||||||
|
ErrorHandler.successResponse(res, result, result.message, 201);
|
||||||
|
});
|
||||||
|
|
||||||
exports.login = ErrorHandler.asyncHandler(async (req, res) => {
|
/**
|
||||||
const result = await AuthService.login(req.body);
|
* Benutzeranmeldung
|
||||||
ErrorHandler.successResponse(res, result, result.message);
|
*/
|
||||||
});
|
static login = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
const result = await AuthService.login(email, password);
|
||||||
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
|
});
|
||||||
|
|
||||||
exports.forgotPassword = ErrorHandler.asyncHandler(async (req, res) => {
|
/**
|
||||||
const result = await AuthService.forgotPassword(req.body.email);
|
* Passwort vergessen
|
||||||
ErrorHandler.successResponse(res, result, result.message);
|
*/
|
||||||
});
|
static forgotPassword = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
|
const result = await AuthService.forgotPassword(req.body.email);
|
||||||
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
|
});
|
||||||
|
|
||||||
exports.resetPassword = ErrorHandler.asyncHandler(async (req, res) => {
|
/**
|
||||||
const result = await AuthService.resetPassword(req.body.token, req.body.password);
|
* Passwort zurücksetzen
|
||||||
ErrorHandler.successResponse(res, result, result.message);
|
*/
|
||||||
});
|
static resetPassword = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
|
const result = await AuthService.resetPassword(req.body.token, req.body.password);
|
||||||
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
|
});
|
||||||
|
|
||||||
exports.logout = ErrorHandler.asyncHandler(async (req, res) => {
|
/**
|
||||||
const authHeader = req.header('Authorization');
|
* Benutzerabmeldung
|
||||||
const token = authHeader ? authHeader.replace('Bearer ', '') : null;
|
*/
|
||||||
const result = await AuthService.logout(token);
|
static logout = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
ErrorHandler.successResponse(res, result, result.message);
|
const authHeader = req.header('Authorization');
|
||||||
});
|
const token = authHeader ? authHeader.replace('Bearer ', '') : null;
|
||||||
|
const result = await AuthService.logout(token);
|
||||||
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export der statischen Methoden für die Routen
|
||||||
|
module.exports = {
|
||||||
|
register: AuthController.register,
|
||||||
|
login: AuthController.login,
|
||||||
|
forgotPassword: AuthController.forgotPassword,
|
||||||
|
resetPassword: AuthController.resetPassword,
|
||||||
|
logout: AuthController.logout
|
||||||
|
};
|
||||||
31
deploy.sh
Executable file
31
deploy.sh
Executable file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Farben
|
||||||
|
GREEN="\033[0;32m"
|
||||||
|
RED="\033[0;31m"
|
||||||
|
NC="\033[0m"
|
||||||
|
|
||||||
|
log() { echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $*${NC}"; }
|
||||||
|
err() { echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] $*${NC}" 1>&2; }
|
||||||
|
|
||||||
|
log "Fetching latest changes..."
|
||||||
|
git fetch --all --prune || { err "git fetch failed"; exit 1; }
|
||||||
|
|
||||||
|
log "Pulling latest changes..."
|
||||||
|
git pull --ff-only || { err "git pull failed"; exit 1; }
|
||||||
|
|
||||||
|
log "Installing dependencies..."
|
||||||
|
npm ci || npm install || { err "npm install failed"; exit 1; }
|
||||||
|
|
||||||
|
log "Building frontend..."
|
||||||
|
npm run build || { err "build failed"; exit 1; }
|
||||||
|
|
||||||
|
log "Copying dist -> public..."
|
||||||
|
mkdir -p public || true
|
||||||
|
cp -R dist/* public/ || { err "copy dist failed"; exit 1; }
|
||||||
|
|
||||||
|
log "Restarting service miriamgemeinde..."
|
||||||
|
sudo systemctl restart miriamgemeinde || { err "service restart failed"; exit 1; }
|
||||||
|
|
||||||
|
log "Deployment completed successfully."
|
||||||
3960
package-lock.json
generated
3960
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
server.js
23
server.js
@@ -3,6 +3,7 @@ const bodyParser = require('body-parser');
|
|||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
require('dotenv').config();
|
||||||
const sequelize = require('./config/database');
|
const sequelize = require('./config/database');
|
||||||
const authRouter = require('./routes/auth');
|
const authRouter = require('./routes/auth');
|
||||||
const eventTypesRouter = require('./routes/eventtypes');
|
const eventTypesRouter = require('./routes/eventtypes');
|
||||||
@@ -19,9 +20,27 @@ const imageRouter = require('./routes/image');
|
|||||||
const filesRouter = require('./routes/files');
|
const filesRouter = require('./routes/files');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 3002;
|
const PORT = parseInt(process.env.PORT, 10) || 3000;
|
||||||
|
|
||||||
|
// CORS mit Whitelist und tolerantem Fallback für fehlende Origin-Header
|
||||||
|
const allowedOrigins = (process.env.ALLOWED_ORIGINS || '')
|
||||||
|
.split(',')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
app.use(cors({
|
||||||
|
origin: (origin, callback) => {
|
||||||
|
if (!origin) return callback(null, true); // z.B. Healthchecks/curl/Server-zu-Server
|
||||||
|
if (allowedOrigins.length === 0) return callback(null, true); // Fallback: alles erlauben
|
||||||
|
if (allowedOrigins.includes(origin)) return callback(null, true);
|
||||||
|
return callback(new Error('Not allowed by CORS'), false);
|
||||||
|
},
|
||||||
|
credentials: true,
|
||||||
|
methods: ['GET','POST','PUT','PATCH','DELETE','OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type','Authorization']
|
||||||
|
}));
|
||||||
|
app.options('*', cors());
|
||||||
|
|
||||||
app.use(cors());
|
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
app.use('/api/auth', authRouter);
|
app.use('/api/auth', authRouter);
|
||||||
|
|||||||
@@ -1,186 +1,292 @@
|
|||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const { User, PasswordResetToken } = require('../models');
|
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const { addTokenToBlacklist } = require('../utils/blacklist');
|
|
||||||
const { transporter, getPasswordResetEmailTemplate } = require('../config/email');
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
const { User, PasswordResetToken } = require('../models');
|
||||||
|
const { transporter, getPasswordResetEmailTemplate } = require('../config/email');
|
||||||
|
const { addTokenToBlacklist } = require('../utils/blacklist');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service für Authentifizierungslogik
|
||||||
|
*/
|
||||||
class AuthService {
|
class AuthService {
|
||||||
/**
|
/**
|
||||||
* User registrieren
|
* Benutzerregistrierung
|
||||||
|
* @param {Object} userData - Benutzerdaten
|
||||||
|
* @returns {Object} - Registrierungsergebnis
|
||||||
*/
|
*/
|
||||||
async register(userData) {
|
static async register(userData) {
|
||||||
const { name, email, password } = userData;
|
const { name, email, password } = userData;
|
||||||
|
|
||||||
|
// Validierung
|
||||||
if (!name || !email || !password) {
|
if (!name || !email || !password) {
|
||||||
throw new Error('VALIDATION_ERROR: Alle Felder sind erforderlich');
|
throw new Error('Alle Felder sind erforderlich');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfen ob E-Mail bereits existiert
|
if (password.length < 6) {
|
||||||
const existingUser = await User.findOne({ where: { email } });
|
throw new Error('Passwort muss mindestens 6 Zeichen lang sein');
|
||||||
if (existingUser) {
|
|
||||||
throw new Error('EMAIL_ALREADY_EXISTS');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
// Email-Format validieren
|
||||||
const user = await User.create({
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
name,
|
if (!emailRegex.test(email)) {
|
||||||
email,
|
throw new Error('Ungültige Email-Adresse');
|
||||||
password: hashedPassword,
|
}
|
||||||
active: true
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.getSafeUserData(user);
|
try {
|
||||||
|
// Passwort hashen
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
|
// Benutzer erstellen mit Retry-Logik für Lock-Timeout
|
||||||
|
const maxAttempts = 3;
|
||||||
|
let attempt = 0;
|
||||||
|
let createdUser = null;
|
||||||
|
let lastError = null;
|
||||||
|
|
||||||
|
while (attempt < maxAttempts && !createdUser) {
|
||||||
|
try {
|
||||||
|
createdUser = await User.create({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
password: hashedPassword,
|
||||||
|
active: true
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
lastError = err;
|
||||||
|
// Spezifisch auf Lock-Timeout reagieren und erneut versuchen
|
||||||
|
if ((err.code === 'ER_LOCK_WAIT_TIMEOUT' || err?.parent?.code === 'ER_LOCK_WAIT_TIMEOUT') && attempt < maxAttempts - 1) {
|
||||||
|
const backoffMs = 300 * (attempt + 1);
|
||||||
|
console.warn(`Register: ER_LOCK_WAIT_TIMEOUT, retry in ${backoffMs}ms (attempt ${attempt + 1}/${maxAttempts})`);
|
||||||
|
await this.delay(backoffMs);
|
||||||
|
attempt++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createdUser && lastError) {
|
||||||
|
throw new Error('Zeitüberschreitung beim Zugriff auf die Datenbank. Bitte erneut versuchen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sichere Benutzerdaten zurückgeben
|
||||||
|
const safeUser = {
|
||||||
|
id: createdUser.id,
|
||||||
|
name: createdUser.name,
|
||||||
|
email: createdUser.email,
|
||||||
|
active: createdUser.active,
|
||||||
|
created_at: createdUser.created_at
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Benutzer erfolgreich registriert',
|
||||||
|
user: safeUser
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === 'SequelizeUniqueConstraintError') {
|
||||||
|
throw new Error('Email-Adresse bereits in Verwendung');
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User einloggen
|
* Benutzeranmeldung
|
||||||
|
* @param {string} email - Email-Adresse
|
||||||
|
* @param {string} password - Passwort
|
||||||
|
* @returns {Object} - Login-Ergebnis
|
||||||
*/
|
*/
|
||||||
async login(credentials) {
|
static async login(email, password) {
|
||||||
const { email, password } = credentials;
|
|
||||||
|
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
throw new Error('VALIDATION_ERROR: Email und Passwort sind erforderlich');
|
throw new Error('Email und Passwort sind erforderlich');
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await User.findOne({ where: { email } });
|
try {
|
||||||
if (!user) {
|
// Benutzer suchen
|
||||||
throw new Error('INVALID_CREDENTIALS');
|
const user = await User.findOne({ where: { email } });
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('Ungültige Anmeldedaten');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passwort prüfen
|
||||||
|
const validPassword = await bcrypt.compare(password, user.password);
|
||||||
|
if (!validPassword) {
|
||||||
|
throw new Error('Ungültige Anmeldedaten');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benutzer aktiv?
|
||||||
|
if (!user.active) {
|
||||||
|
throw new Error('Benutzerkonto ist nicht aktiv');
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWT Token erstellen
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ id: user.id, name: user.name, email: user.email },
|
||||||
|
process.env.JWT_SECRET || 'zTxVgptmPl9!_dr%xxx9999(dd)',
|
||||||
|
{ expiresIn: '1h' }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sichere Benutzerdaten
|
||||||
|
const safeUser = {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
active: user.active,
|
||||||
|
created_at: user.created_at
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Login erfolgreich',
|
||||||
|
token,
|
||||||
|
user: safeUser
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validPassword = await bcrypt.compare(password, user.password);
|
|
||||||
if (!validPassword) {
|
|
||||||
throw new Error('INVALID_CREDENTIALS');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.active) {
|
|
||||||
throw new Error('ACCOUNT_INACTIVE');
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = jwt.sign(
|
|
||||||
{ id: user.id, name: user.name, email: user.email },
|
|
||||||
'zTxVgptmPl9!_dr%xxx9999(dd)',
|
|
||||||
{ expiresIn: '1h' }
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: 'Login erfolgreich',
|
|
||||||
token,
|
|
||||||
user: this.getSafeUserData(user)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User ausloggen
|
* Passwort vergessen - Email senden
|
||||||
|
* @param {string} email - Email-Adresse
|
||||||
|
* @returns {Object} - Ergebnis
|
||||||
*/
|
*/
|
||||||
async logout(token) {
|
static async forgotPassword(email) {
|
||||||
if (!token) {
|
|
||||||
throw new Error('VALIDATION_ERROR: Kein Token bereitgestellt');
|
|
||||||
}
|
|
||||||
|
|
||||||
addTokenToBlacklist(token);
|
|
||||||
return { message: 'Logout erfolgreich' };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Passwort vergessen - E-Mail senden
|
|
||||||
*/
|
|
||||||
async forgotPassword(email) {
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
throw new Error('VALIDATION_ERROR: E-Mail-Adresse ist erforderlich');
|
throw new Error('Email-Adresse ist erforderlich');
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await User.findOne({ where: { email } });
|
try {
|
||||||
if (!user) {
|
// Benutzer suchen
|
||||||
// Aus Sicherheitsgründen immer Erfolg melden
|
const user = await User.findOne({ where: { email } });
|
||||||
return { message: 'Falls die E-Mail-Adresse in unserem System registriert ist, erhalten Sie einen Link zum Zurücksetzen des Passworts.' };
|
if (!user) {
|
||||||
|
// Aus Sicherheitsgründen keine Information über Existenz preisgeben
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Falls die Email-Adresse registriert ist, wurde eine Passwort-Reset-Email gesendet'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alte Tokens löschen
|
||||||
|
await PasswordResetToken.destroy({ where: { userId: user.id } });
|
||||||
|
|
||||||
|
// Neuen Token erstellen
|
||||||
|
const token = crypto.randomBytes(32).toString('hex');
|
||||||
|
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 Stunde
|
||||||
|
|
||||||
|
await PasswordResetToken.create({
|
||||||
|
userId: user.id,
|
||||||
|
token,
|
||||||
|
expiresAt
|
||||||
|
});
|
||||||
|
|
||||||
|
// Email senden
|
||||||
|
const resetUrl = `${process.env.FRONTEND_URL || 'http://localhost:8080'}/reset-password?token=${token}`;
|
||||||
|
const emailTemplate = getPasswordResetEmailTemplate(user.name, resetUrl);
|
||||||
|
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: process.env.EMAIL_FROM || 'noreply@miriamgemeinde.de',
|
||||||
|
to: email,
|
||||||
|
subject: 'Passwort zurücksetzen - Miriam Gemeinde',
|
||||||
|
html: emailTemplate
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Falls die Email-Adresse registriert ist, wurde eine Passwort-Reset-Email gesendet'
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Forgot password error:', error);
|
||||||
|
throw new Error('Fehler beim Senden der Passwort-Reset-Email');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alte Reset-Tokens für diesen User löschen
|
|
||||||
await PasswordResetToken.destroy({ where: { userId: user.id } });
|
|
||||||
|
|
||||||
// Neuen Reset-Token generieren
|
|
||||||
const token = crypto.randomBytes(32).toString('hex');
|
|
||||||
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 Stunde
|
|
||||||
|
|
||||||
await PasswordResetToken.create({
|
|
||||||
userId: user.id,
|
|
||||||
token,
|
|
||||||
expiresAt
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset-URL generieren
|
|
||||||
const resetUrl = `${process.env.FRONTEND_URL || 'http://localhost:8080'}/reset-password?token=${token}`;
|
|
||||||
|
|
||||||
// E-Mail versenden
|
|
||||||
const emailTemplate = getPasswordResetEmailTemplate(resetUrl, user.name);
|
|
||||||
|
|
||||||
await transporter.sendMail({
|
|
||||||
from: process.env.SMTP_FROM || 'noreply@miriamgemeinde.de',
|
|
||||||
to: email,
|
|
||||||
subject: emailTemplate.subject,
|
|
||||||
html: emailTemplate.html,
|
|
||||||
text: emailTemplate.text
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Password reset email sent to:', email);
|
|
||||||
return { message: 'Falls die E-Mail-Adresse in unserem System registriert ist, erhalten Sie einen Link zum Zurücksetzen des Passworts.' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Passwort zurücksetzen
|
* Passwort zurücksetzen
|
||||||
|
* @param {string} token - Reset-Token
|
||||||
|
* @param {string} newPassword - Neues Passwort
|
||||||
|
* @returns {Object} - Ergebnis
|
||||||
*/
|
*/
|
||||||
async resetPassword(token, newPassword) {
|
static async resetPassword(token, newPassword) {
|
||||||
if (!token || !newPassword) {
|
if (!token || !newPassword) {
|
||||||
throw new Error('VALIDATION_ERROR: Token und neues Passwort sind erforderlich');
|
throw new Error('Token und neues Passwort sind erforderlich');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPassword.length < 6) {
|
if (newPassword.length < 6) {
|
||||||
throw new Error('VALIDATION_ERROR: Passwort muss mindestens 6 Zeichen lang sein');
|
throw new Error('Passwort muss mindestens 6 Zeichen lang sein');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token validieren
|
try {
|
||||||
const resetToken = await PasswordResetToken.findOne({
|
// Token suchen
|
||||||
where: {
|
const resetToken = await PasswordResetToken.findOne({
|
||||||
token,
|
where: { token, used: false },
|
||||||
used: false,
|
include: [{ model: User, as: 'user' }]
|
||||||
expiresAt: {
|
});
|
||||||
[require('sequelize').Op.gt]: new Date()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
include: [{ model: User, as: 'user' }]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!resetToken) {
|
if (!resetToken) {
|
||||||
throw new Error('INVALID_RESET_TOKEN');
|
throw new Error('Ungültiger oder bereits verwendeter Token');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token abgelaufen?
|
||||||
|
if (new Date() > resetToken.expiresAt) {
|
||||||
|
await resetToken.destroy();
|
||||||
|
throw new Error('Token ist abgelaufen');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neues Passwort hashen und speichern
|
||||||
|
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||||
|
await resetToken.user.update({ password: hashedPassword });
|
||||||
|
|
||||||
|
// Token als verwendet markieren
|
||||||
|
await resetToken.update({ used: true });
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Passwort erfolgreich zurückgesetzt'
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Reset password error:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Passwort hashen und aktualisieren
|
|
||||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
|
||||||
await User.update(
|
|
||||||
{ password: hashedPassword },
|
|
||||||
{ where: { id: resetToken.userId } }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Token als verwendet markieren
|
|
||||||
await resetToken.update({ used: true });
|
|
||||||
|
|
||||||
console.log('Password reset successful for user:', resetToken.userId);
|
|
||||||
return { message: 'Passwort erfolgreich zurückgesetzt' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sichere User-Daten extrahieren (ohne Passwort)
|
* Benutzerabmeldung
|
||||||
|
* @param {string} token - JWT Token
|
||||||
|
* @returns {Object} - Ergebnis
|
||||||
*/
|
*/
|
||||||
getSafeUserData(user) {
|
static async logout(token) {
|
||||||
return {
|
if (!token) {
|
||||||
id: user.id,
|
throw new Error('Token ist erforderlich');
|
||||||
name: user.name,
|
}
|
||||||
email: user.email,
|
|
||||||
active: user.active,
|
try {
|
||||||
created_at: user.created_at
|
// Token zur Blacklist hinzufügen
|
||||||
};
|
addTokenToBlacklist(token);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Erfolgreich abgemeldet'
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Logout error:', error);
|
||||||
|
throw new Error('Fehler beim Abmelden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion für Verzögerung
|
||||||
|
* @param {number} ms - Millisekunden
|
||||||
|
* @returns {Promise} - Promise mit Verzögerung
|
||||||
|
*/
|
||||||
|
static delay(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new AuthService();
|
module.exports = AuthService;
|
||||||
@@ -1,71 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* Zentrale Fehlerbehandlung für die Anwendung
|
||||||
|
*/
|
||||||
class ErrorHandler {
|
class ErrorHandler {
|
||||||
/**
|
/**
|
||||||
* Error in HTTP Response umwandeln
|
* Erstellt eine standardisierte Fehlerantwort
|
||||||
|
* @param {Object} res - Express Response Objekt
|
||||||
|
* @param {number} statusCode - HTTP Status Code
|
||||||
|
* @param {string} message - Fehlermeldung
|
||||||
|
* @param {Object} details - Zusätzliche Fehlerdetails (optional)
|
||||||
*/
|
*/
|
||||||
handleError(error, res) {
|
static errorResponse(res, statusCode, message, details = null) {
|
||||||
console.error('Error:', error);
|
const error = {
|
||||||
|
success: false,
|
||||||
|
message,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
// Validation Errors
|
if (details) {
|
||||||
if (error.message.startsWith('VALIDATION_ERROR:')) {
|
error.details = details;
|
||||||
const message = error.message.replace('VALIDATION_ERROR: ', '');
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: message,
|
|
||||||
type: 'VALIDATION_ERROR'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Business Logic Errors
|
console.error(`[${statusCode}] ${message}`, details ? details : '');
|
||||||
switch (error.message) {
|
return res.status(statusCode).json(error);
|
||||||
case 'USER_NOT_FOUND':
|
|
||||||
return res.status(404).json({
|
|
||||||
success: false,
|
|
||||||
message: 'Benutzer nicht gefunden',
|
|
||||||
type: 'NOT_FOUND'
|
|
||||||
});
|
|
||||||
|
|
||||||
case 'INVALID_CURRENT_PASSWORD':
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
message: 'Aktuelles Passwort ist falsch',
|
|
||||||
type: 'INVALID_PASSWORD'
|
|
||||||
});
|
|
||||||
|
|
||||||
case 'EMAIL_ALREADY_EXISTS':
|
|
||||||
return res.status(409).json({
|
|
||||||
success: false,
|
|
||||||
message: 'E-Mail-Adresse bereits vorhanden',
|
|
||||||
type: 'DUPLICATE_EMAIL'
|
|
||||||
});
|
|
||||||
|
|
||||||
default:
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: 'Ein interner Fehler ist aufgetreten',
|
|
||||||
type: 'INTERNAL_ERROR'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Success Response erstellen
|
* Erstellt eine erfolgreiche Antwort
|
||||||
|
* @param {Object} res - Express Response Objekt
|
||||||
|
* @param {Object} data - Antwortdaten
|
||||||
|
* @param {string} message - Erfolgsmeldung
|
||||||
|
* @param {number} statusCode - HTTP Status Code (default: 200)
|
||||||
*/
|
*/
|
||||||
successResponse(res, data, message = 'Erfolgreich', statusCode = 200) {
|
static successResponse(res, data, message = 'Erfolgreich', statusCode = 200) {
|
||||||
return res.status(statusCode).json({
|
const response = {
|
||||||
success: true,
|
success: true,
|
||||||
message: message,
|
message,
|
||||||
data: data
|
data,
|
||||||
});
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.status(statusCode).json(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Async Error Wrapper für Controller
|
* Wrapper für async Controller-Funktionen mit automatischer Fehlerbehandlung
|
||||||
|
* @param {Function} fn - Async Controller-Funktion
|
||||||
|
* @returns {Function} - Wrapped Controller-Funktion
|
||||||
*/
|
*/
|
||||||
asyncHandler(fn) {
|
static asyncHandler(fn) {
|
||||||
return (req, res, next) => {
|
return (req, res, next) => {
|
||||||
Promise.resolve(fn(req, res, next)).catch(next);
|
Promise.resolve(fn(req, res, next)).catch((error) => {
|
||||||
|
console.error('Unhandled error in async handler:', error);
|
||||||
|
this.errorResponse(res, 500, 'Ein unerwarteter Fehler ist aufgetreten', {
|
||||||
|
error: error.message,
|
||||||
|
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Behandelt Sequelize-Validierungsfehler
|
||||||
|
* @param {Object} error - Sequelize Error
|
||||||
|
* @returns {Object} - Standardisierte Fehlerantwort
|
||||||
|
*/
|
||||||
|
static handleSequelizeError(error) {
|
||||||
|
if (error.name === 'SequelizeValidationError') {
|
||||||
|
const validationErrors = error.errors.map(err => ({
|
||||||
|
field: err.path,
|
||||||
|
message: err.message,
|
||||||
|
value: err.value
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Validierungsfehler',
|
||||||
|
details: { validationErrors }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.name === 'SequelizeUniqueConstraintError') {
|
||||||
|
return {
|
||||||
|
statusCode: 409,
|
||||||
|
message: 'Eindeutigkeitsverletzung',
|
||||||
|
details: { field: error.errors[0]?.path }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.name === 'SequelizeForeignKeyConstraintError') {
|
||||||
|
return {
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Referenzfehler',
|
||||||
|
details: { field: error.fields[0] }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.name === 'SequelizeDatabaseError') {
|
||||||
|
return {
|
||||||
|
statusCode: 500,
|
||||||
|
message: 'Datenbankfehler',
|
||||||
|
details: { original: error.original?.message }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 500,
|
||||||
|
message: 'Datenbankfehler',
|
||||||
|
details: { error: error.message }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Behandelt JWT-Fehler
|
||||||
|
* @param {Object} error - JWT Error
|
||||||
|
* @returns {Object} - Standardisierte Fehlerantwort
|
||||||
|
*/
|
||||||
|
static handleJWTError(error) {
|
||||||
|
if (error.name === 'JsonWebTokenError') {
|
||||||
|
return {
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Ungültiger Token'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.name === 'TokenExpiredError') {
|
||||||
|
return {
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Token abgelaufen'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Authentifizierungsfehler'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new ErrorHandler();
|
module.exports = ErrorHandler;
|
||||||
@@ -2,12 +2,14 @@ const { defineConfig } = require('@vue/cli-service');
|
|||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
|
|
||||||
module.exports = defineConfig({
|
module.exports = defineConfig({
|
||||||
transpileDependencies: true,
|
transpileDependencies: [],
|
||||||
devServer: {
|
devServer: {
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 8080
|
port: 8080
|
||||||
},
|
},
|
||||||
configureWebpack: {
|
configureWebpack: {
|
||||||
|
output: { clean: true },
|
||||||
|
cache: false,
|
||||||
resolve: {
|
resolve: {
|
||||||
fallback: {
|
fallback: {
|
||||||
"path": require.resolve("path-browserify"),
|
"path": require.resolve("path-browserify"),
|
||||||
@@ -23,4 +25,11 @@ module.exports = defineConfig({
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
chainWebpack: config => {
|
||||||
|
const rules = ['vue','js','ts','tsx','css','scss','sass','less','stylus'];
|
||||||
|
rules.forEach(rule => {
|
||||||
|
try { config.module.rule(rule).uses.delete('cache-loader'); } catch (e) {}
|
||||||
|
try { config.module.rule(rule).uses.delete('thread-loader'); } catch (e) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user