Compare commits

..

44 Commits

Author SHA1 Message Date
5f5fb83e9a Merge pull request 'Update dependency jsonwebtoken to v9.0.3' (#3) from renovate/jsonwebtoken-9.x-lockfile into main
Reviewed-on: #3
2025-12-19 16:14:10 +01:00
f15598b2c1 Merge pull request 'Update dependency body-parser to v1.20.4' (#2) from renovate/body-parser-1.x-lockfile into main
Reviewed-on: #2
2025-12-19 16:14:02 +01:00
979732d545 Update dependency jsonwebtoken to v9.0.3 2025-12-19 16:12:32 +01:00
d9e0b07f0c Update dependency body-parser to v1.20.4 2025-12-19 16:12:24 +01:00
b5ac5df38d Merge pull request 'Configure Renovate' (#1) from renovate/configure into main
Reviewed-on: #1
2025-12-19 16:09:02 +01:00
c982f34823 Add renovate.json 2025-12-19 15:58:37 +01:00
Torsten Schulz (local)
c390228ed9 Update database configuration and improve WorshipRender component: Change database connection details to use 'miriamgemeinde' and 'localhost', and enhance conditional rendering in WorshipRender.vue for better handling of event place information. 2025-11-23 00:32:23 +01:00
Torsten Schulz (local)
9c924071f4 Update package-lock.json and configuration files: Upgrade lockfile version to 3, update @vue/cli-plugin-babel and @vue/cli-plugin-eslint to version 5.0.0, and change database connection details to use 'miriam_user' and host 'tsschulz.de' with the new password. 2025-11-23 00:24:05 +01:00
Torsten Schulz (local)
2d1ed43312 Refactor babel.config.js: Simplify Babel preset configuration by using the package name instead of a direct path for improved readability and maintainability. 2025-11-22 23:37:34 +01:00
Torsten Schulz (local)
11cf961ed8 Update package.json: Upgrade @vue/cli-plugin-babel and @vue/cli-plugin-eslint to version 5.0.0, removing the previous version 3.12.1 for better compatibility. 2025-11-22 23:33:04 +01:00
Torsten Schulz (local)
cd9e645941 Update babel.config.js: Use direct path for Babel preset to ensure reliable resolution across different Node/npm versions. 2025-11-22 23:29:40 +01:00
Torsten Schulz (local)
7bd236aa82 Update package.json: Re-add @vue/cli-plugin-babel dependency to version 3.12.1 in devDependencies. 2025-11-22 23:24:53 +01:00
Torsten Schulz (local)
f96e0a1f22 Update package dependencies: Downgrade @vue/cli and its plugins to version 4.2.2 and 3.12.1 respectively, and update various Babel packages to version 7.28.5. Remove unused Apollo packages and add new dependencies for vue-jscodeshift-adapter and vue-sfc-descriptor-to-string. 2025-11-22 23:17:23 +01:00
Torsten Schulz (local)
45190ed7a5 Füge eine Überprüfung hinzu, um endlose Wiederholungen beim Laden von Menü-Daten zu verhindern: Implementiere eine Flagge, die sicherstellt, dass die Menü-Daten nur einmal geladen werden, bevor die Routen generiert werden. 2025-11-22 22:47:51 +01:00
Torsten Schulz (local)
79f2ca8382 Füge eine Überprüfung hinzu, um endlose Wiederholungen beim Laden von Menü-Daten zu verhindern: Implementiere eine Flagge, die sicherstellt, dass die Menü-Daten nur einmal geladen werden, bevor die Routen generiert werden. 2025-11-22 22:31:28 +01:00
Torsten Schulz (local)
550ed97a11 Aktualisiere die Axios-Konfiguration: Setze die Basis-URL standardmäßig auf '/api' und ermögliche die Verwendung einer Umgebungsvariablen in Entwicklungsumgebungen. Verhindere Mixed-Content-Probleme durch Umstellung auf HTTPS. Ändere den Import von Axios im Store, um die neue Konfiguration zu nutzen. 2025-11-22 22:23:08 +01:00
Torsten Schulz (local)
718bcabea3 Verbessere die Axios-Konfiguration: Setze die Basis-URL dynamisch basierend auf der Umgebung und verhindere Mixed-Content-Probleme durch Umstellung auf HTTPS. Aktualisiere die Menü-Datenabfrage, um Axios für den API-Zugriff zu verwenden und füge Fehlerbehandlung hinzu. 2025-11-22 22:16:50 +01:00
Torsten Schulz (local)
44c978f21e Füge Import- und Exportfunktionen für Gottesdienste hinzu: Implementiere die Möglichkeit, Gottesdienste aus .doc und .docx-Dateien zu importieren und in verschiedenen Formaten zu exportieren. Verbessere die Benutzeroberfläche des Worship Management-Formulars mit neuen Schaltflächen für Import und Export sowie Dialogen zur Bearbeitung importierter Daten. Aktualisiere die Datenbankstruktur, um neue Felder für die Genehmigung und das Orgelspiel zu unterstützen. 2025-11-22 22:07:36 +01:00
Torsten Schulz (local)
6c54bc9d49 Aktualisiere die Datenschutzerklärung und die Nutzungsbedingungen: Präzisiere die rechtlichen Grundlagen für den Datenschutz, füge eine detaillierte Auflistung der Nutzerrechte hinzu und aktualisiere das Datum der Datenschutzerklärung auf Januar 2025. 2025-10-27 09:35:09 +01:00
Torsten Schulz (local)
d4fab1ceb3 Füge Ablaufdatum zu Kontaktpersonen hinzu: Implementiere die Möglichkeit, ein Ablaufdatum für Kontaktpersonen zu speichern und anzuzeigen. Aktualisiere die Filterlogik, um nur nicht abgelaufene Kontaktpersonen anzuzeigen, und passe die Benutzeroberfläche an, um das Ablaufdatum darzustellen. 2025-10-07 17:45:45 +02:00
Torsten Schulz (local)
ead4dbdd3f Ändere die Hintergrundfarbe aktiver Schaltflächen im EditPagesComponent von grün zu dunkelgrau, um die Sichtbarkeit zu verbessern und ein einheitlicheres Design zu gewährleisten. 2025-10-07 17:40:33 +02:00
Torsten Schulz (local)
b18c911500 Verbessere die Benutzeroberfläche des EditPagesComponent: Füge aktive Klassen zu Schaltflächen hinzu, um den aktuellen Status der Textformatierungen anzuzeigen. Aktualisiere die Stile für aktive Schaltflächen zur besseren Sichtbarkeit. 2025-10-07 17:37:16 +02:00
Torsten Schulz (local)
5e4471a936 Optimiere das Worship Management-Formular: Entferne redundante Codeabschnitte, verbessere die Benutzeroberfläche durch Anpassungen der Abstände und Padding-Werte, und vereinheitliche die Struktur der Eingabefelder. Füge eine neue Auswahl für das Jahr hinzu, um die liturgischen Daten zu laden. 2025-10-07 17:22:39 +02:00
Torsten Schulz (local)
459dd3168a Verbessere die Auswahl von liturgischen Tagen im Worship Management: Filtere nur zukünftige Tage, formatiere die Anzeige mit Datum und Namen und aktualisiere die Logik zur Auswahl und Speicherung des liturgischen Tages. Optimiere die Handhabung von Tags und deren Zuordnung im Formular. 2025-10-07 17:20:17 +02:00
Torsten Schulz (local)
3af7089e06 Verbessere das Layout des Worship Management-Formulars: Ändere das Layout von Flexbox zu Grid, optimiere die Stile für Labels, Eingabefelder und Schaltflächen, um eine bessere Benutzererfahrung zu gewährleisten. 2025-10-07 17:18:37 +02:00
Torsten Schulz (local)
156d89a45d Füge Logik zur Aktualisierung des Datums basierend auf dem ausgewählten liturgischen Tag hinzu: Implementiere einen Watcher für die Änderung des Tagesnamens und vermeide Endlosschleifen durch ein Flag. Protokolliere das gesetzte Datum und den entsprechenden liturgischen Tag in der Konsole. 2025-10-07 17:16:42 +02:00
Torsten Schulz (local)
bf0b7f1dad Verbessere die Logik zur Ermittlung des liturgischen Tages: Normalisiere das Datum und vergleiche nur das Datum ohne Zeitstempel. Füge Konsolenausgaben hinzu, um die Ergebnisse der Suche nach liturgischen Tagen zu protokollieren. 2025-10-07 17:14:24 +02:00
Torsten Schulz (local)
6de8cac0bc Optimiere die Funktion zum Abrufen von Worship-Optionen: Reduziere die Anzahl der Datenbankabfragen, indem Organizer und SacristanService in einer Abfrage zusammengefasst werden. Verbessere die Fehlerbehandlung durch detailliertere Fehlermeldungen. 2025-10-07 17:10:18 +02:00
Torsten Schulz (local)
ecd03d29f4 Verbessere das Worship Management: Implementiere die Auswahl von liturgischen Tagen mit Multiselect und aktualisiere das Formular zur dynamischen Anzeige basierend auf dem Datum. 2025-10-07 17:06:48 +02:00
Torsten Schulz (local)
0238fffd3d Füge Unterstützung für liturgische Tage im Worship Management hinzu: Implementiere Multiselect für die Auswahl von Tag-Namen und lade die verfügbaren liturgischen Tage. Aktualisiere das Formular zur Anzeige und Auswahl des liturgischen Tages basierend auf dem Datum. 2025-10-07 17:03:58 +02:00
Torsten Schulz (local)
fd84112cef Füge neue Funktionalität zum Abrufen von Worship-Optionen hinzu: Implementiere Endpunkt für eindeutige Gestalter und Küsterdienste. Aktualisiere das Worship Management-Formular zur Verwendung von Multiselect-Komponenten für die Auswahl und Eingabe von Gestaltern und Küstern. Integriere die Fetch-Funktion für Worship-Optionen im Vue-Komponenten-Lifecycle. 2025-10-07 16:52:52 +02:00
Torsten Schulz (local)
3303c749e2 Füge 'actualize.sh' zur .gitignore hinzu, um sicherzustellen, dass die Datei nicht ins Repository aufgenommen wird. 2025-10-07 16:46:05 +02:00
Torsten Schulz (local)
9cadaf3f51 Füge Such- und Filteroptionen für Gottesdienste hinzu: Ermögliche die Suche nach Datum und die Anzeige vergangener Gottesdienste im Worship Management-Bereich. Implementiere eine Funktion zum Zurücksetzen der Suche. 2025-10-07 16:42:11 +02:00
Torsten Schulz (local)
5d32c46e38 Füge Such- und Filterfunktionen für Veranstaltungen hinzu: Ermögliche die Suche nach Name, Typ und Beschreibung sowie die Anzeige vergangener Events im Event-Management-Bereich. 2025-10-07 16:37:30 +02:00
Torsten Schulz (local)
7e6128dec4 Füge Funktionalität zum Fokussieren des ersten Eingabefelds im Event-Formular hinzu und implementiere das Scrollen zum Formular bei der Erstellung und Bearbeitung von Veranstaltungen. 2025-10-07 16:06:17 +02:00
Torsten Schulz (local)
cff48550ae Changed 2025-10-07 16:00:49 +02:00
Torsten Schulz (local)
044de56c4f Aktualisiere die webpack-Konfiguration in vue.config.js: Setze transpileDependencies auf ein leeres Array, füge eine Bereinigung des Ausgabeverzeichnisses hinzu, deaktiviere den Cache und entferne den cache-loader sowie thread-loader aus den Regeln für verschiedene Dateitypen, um die Build-Leistung zu optimieren. 2025-09-24 11:07:59 +02:00
Torsten Schulz (local)
56acf6be6a Bereinige den Import von filesRouter in server.js, um die Lesbarkeit des Codes zu verbessern. 2025-09-24 10:53:17 +02:00
Torsten Schulz (local)
002a08ab6a Füge Unterstützung für Umgebungsvariablen in server.js hinzu, indem dotenv konfiguriert wird. Ändere den Port auf den Wert aus der Umgebungsvariable oder auf 3000 als Fallback, um die Flexibilität der Konfiguration zu erhöhen. 2025-09-24 10:52:04 +02:00
Torsten Schulz (local)
bb29926c22 Ändere den Port in server.js von 3002 auf 3000, um die Konsistenz mit den Standardkonfigurationen zu gewährleisten. Füge Lizenzen für mehrere Abhängigkeiten in package-lock.json hinzu, um die rechtlichen Anforderungen zu erfüllen und die Transparenz zu erhöhen. 2025-09-24 10:51:32 +02:00
Torsten Schulz (local)
b1318f5a41 Implementiere CORS mit Whitelist und tolerantem Fallback für fehlende Origin-Header in server.js, um die Sicherheit und Flexibilität der API zu erhöhen. 2025-09-24 10:47:10 +02:00
Torsten Schulz (local)
bd1cf466e5 Verbessere die Benutzeroberfläche der Passwort-Zurücksetzen-Seite durch Anpassungen im Layout und füge Validierungslogik für die E-Mail-Adresse hinzu, um die Benutzererfahrung zu optimieren. 2025-09-24 10:44:30 +02:00
Torsten Schulz (local)
951fca95cf Remove .env from repository, add .env.example template 2025-09-24 10:37:28 +02:00
Torsten Schulz (local)
6b88a4602a Ändere den Status des neu erstellten Benutzers in der Benutzerregistrierung auf inaktiv, um die Sicherheit zu erhöhen und unbefugte Zugriffe zu verhindern. 2025-09-24 10:35:57 +02:00
55 changed files with 12928 additions and 19858 deletions

7
.env
View File

@@ -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
View 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

2
.gitignore vendored
View File

@@ -27,3 +27,5 @@ server.key
server.cert
public/images/uploads/1ba24ea7-f52c-4179-896f-1909269cab58.jpg
actualize.sh
files/uploads/GD 24.08.2025-04.01.2026 Stand 12.08.2025.docx

View File

@@ -2,4 +2,4 @@ module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
};

View File

@@ -1,21 +1,21 @@
{
"development": {
"username": "miriam_user",
"password": "qTCTTWwpEwy3vPDU",
"username": "miriamgemeinde",
"password": "hitomisan",
"database": "miriamgemeinde",
"host": "tsschulz.de",
"host": "localhost",
"dialect": "mysql"
},
"test": {
"username": "miriam_user",
"password": "qTCTTWwpEwy3vPDU",
"password": "hitomisan",
"database": "miriamgemeinde",
"host": "tsschulz.de",
"dialect": "mysql"
},
"production": {
"username": "miriam_user",
"password": "qTCTTWwpEwy3vPDU",
"password": "hitomisan",
"database": "miriamgemeinde",
"host": "tsschulz.de",
"dialect": "mysql"

View File

@@ -1,7 +1,7 @@
const { Sequelize } = require('sequelize');
const sequelize = new Sequelize('miriamgemeinde', 'miriam_user', 'qTCTTWwpEwy3vPDU', {
host: 'tsschulz.de',
const sequelize = new Sequelize('miriamgemeinde', 'miriamgemeinde', 'hitomisan', {
host: 'localhost',
dialect: 'mysql',
retry: {
match: [
@@ -26,7 +26,7 @@ const sequelize = new Sequelize('miriamgemeinde', 'miriam_user', 'qTCTTWwpEwy3vP
async function connectWithRetry() {
try {
await sequelize.authenticate();
console.log('Connection has been established successfully.');
console.log(`Connection has been established successfully. Database server: ${sequelize.config.host}`);
} catch (error) {
console.error('Unable to connect to the database:', error);
setTimeout(connectWithRetry, 5000);

View File

@@ -1,29 +1,205 @@
const AuthService = require('../services/AuthService');
const ErrorHandler = require('../utils/ErrorHandler');
const bcrypt = require('bcryptjs');
const { User, PasswordResetToken } = require('../models');
const jwt = require('jsonwebtoken');
const { addTokenToBlacklist } = require('../utils/blacklist');
const { transporter, getPasswordResetEmailTemplate } = require('../config/email');
const crypto = require('crypto');
exports.register = ErrorHandler.asyncHandler(async (req, res) => {
const result = await AuthService.register(req.body);
ErrorHandler.successResponse(res, result, 'Benutzer erfolgreich registriert', 201);
});
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
exports.login = ErrorHandler.asyncHandler(async (req, res) => {
const result = await AuthService.login(req.body);
ErrorHandler.successResponse(res, result, result.message);
});
exports.register = async (req, res) => {
const { name, email, password } = req.body;
if (!name || !email || !password) {
return res.status(400).json({ message: 'Alle Felder sind erforderlich' });
}
try {
const hashedPassword = await bcrypt.hash(password, 10);
console.log('Register: creating user', { email });
exports.forgotPassword = ErrorHandler.asyncHandler(async (req, res) => {
const result = await AuthService.forgotPassword(req.body.email);
ErrorHandler.successResponse(res, result, result.message);
});
const maxAttempts = 3;
let attempt = 0;
let createdUser = null;
let lastError = null;
exports.resetPassword = ErrorHandler.asyncHandler(async (req, res) => {
const result = await AuthService.resetPassword(req.body.token, req.body.password);
ErrorHandler.successResponse(res, result, result.message);
});
while (attempt < maxAttempts && !createdUser) {
try {
createdUser = await User.create({ name, email, password: hashedPassword, active: false });
} 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 delay(backoffMs);
attempt++;
continue;
}
throw err;
}
}
exports.logout = ErrorHandler.asyncHandler(async (req, res) => {
if (!createdUser && lastError) {
console.error('Register error (after retries):', lastError);
return res.status(503).json({ message: 'Zeitüberschreitung beim Zugriff auf die Datenbank. Bitte erneut versuchen.' });
}
console.log('Register: user created', { id: createdUser.id });
const safeUser = {
id: createdUser.id,
name: createdUser.name,
email: createdUser.email,
active: createdUser.active,
created_at: createdUser.created_at
};
return res.status(201).json({ message: 'Benutzer erfolgreich registriert', user: safeUser });
} catch (error) {
if (error.name === 'SequelizeUniqueConstraintError') {
return res.status(400).json({ message: 'Email-Adresse bereits in Verwendung' });
}
console.error('Register error:', error);
return res.status(500).json({ message: 'Ein Fehler ist aufgetreten', error: error.message });
}
};
exports.login = async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ message: 'Email und Passwort sind erforderlich' });
}
try {
const user = await User.findOne({ where: { email } });
if (!user) {
return res.status(401).json({ message: 'Ungültige Anmeldedaten' });
}
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
return res.status(401).json({ message: 'Ungültige Anmeldedaten' });
}
if (!user.active) {
return res.status(403).json({ message: 'Benutzerkonto ist nicht aktiv' });
}
const token = jwt.sign({ id: user.id, name: user.name, email: user.email }, 'zTxVgptmPl9!_dr%xxx9999(dd)', { expiresIn: '1h' });
return res.status(200).json({ message: 'Login erfolgreich', token, 'user': user });
} catch (error) {
return res.status(500).json({ message: 'Ein Fehler ist aufgetreten' });
}
};
exports.forgotPassword = async (req, res) => {
const { email } = req.body;
if (!email) {
return res.status(400).json({ message: 'E-Mail-Adresse ist erforderlich' });
}
try {
const user = await User.findOne({ where: { email } });
if (!user) {
// Aus Sicherheitsgründen immer Erfolg melden, auch wenn E-Mail nicht existiert
return res.status(200).json({ message: 'Falls die E-Mail-Adresse in unserem System registriert ist, erhalten Sie einen Link zum Zurücksetzen des Passworts.' });
}
// 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);
const mailOptions = {
from: process.env.SMTP_FROM || 'noreply@miriamgemeinde.de',
to: email,
subject: emailTemplate.subject,
html: emailTemplate.html,
text: emailTemplate.text
};
console.log('=== EMAIL SENDING DEBUG ===');
console.log('From:', mailOptions.from);
console.log('To:', mailOptions.to);
console.log('Subject:', mailOptions.subject);
console.log('Reset URL:', resetUrl);
console.log('===========================');
await transporter.sendMail(mailOptions);
console.log('Password reset email sent to:', email);
return res.status(200).json({ message: 'Falls die E-Mail-Adresse in unserem System registriert ist, erhalten Sie einen Link zum Zurücksetzen des Passworts.' });
} catch (error) {
console.error('Forgot password error:', error);
return res.status(500).json({ message: 'Ein Fehler ist aufgetreten' });
}
};
exports.resetPassword = async (req, res) => {
const { token, password } = req.body;
if (!token || !password) {
return res.status(400).json({ message: 'Token und neues Passwort sind erforderlich' });
}
if (password.length < 6) {
return res.status(400).json({ message: 'Passwort muss mindestens 6 Zeichen lang sein' });
}
try {
// Token validieren
const resetToken = await PasswordResetToken.findOne({
where: {
token,
used: false,
expiresAt: {
[require('sequelize').Op.gt]: new Date()
}
},
include: [{ model: User, as: 'user' }]
});
if (!resetToken) {
return res.status(400).json({ message: 'Ungültiger oder abgelaufener Token' });
}
// Passwort hashen und aktualisieren
const hashedPassword = await bcrypt.hash(password, 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 res.status(200).json({ message: 'Passwort erfolgreich zurückgesetzt' });
} catch (error) {
console.error('Reset password error:', error);
return res.status(500).json({ message: 'Ein Fehler ist aufgetreten' });
}
};
exports.logout = async (req, res) => {
const authHeader = req.header('Authorization');
const token = authHeader ? authHeader.replace('Bearer ', '') : null;
const result = await AuthService.logout(token);
ErrorHandler.successResponse(res, result, result.message);
});
if (!authHeader) {
return res.status(400).json({ message: 'Kein Token bereitgestellt' });
}
const token = authHeader.replace('Bearer ', '');
try {
addTokenToBlacklist(token);
return res.status(200).json({ message: 'Logout erfolgreich' });
} catch (error) {
console.log(error);
return res.status(500).json({ message: 'Ein Fehler ist beim Logout aufgetreten' });
}
};

View File

@@ -3,7 +3,16 @@ const { Op } = require('sequelize');
const getAllContactPersons = async (req, res) => {
try {
const today = new Date();
today.setHours(0, 0, 0, 0);
const contactPersons = await ContactPerson.findAll({
where: {
[Op.or]: [
{ expiryDate: null },
{ expiryDate: { [Op.gte]: today } }
]
},
include: [
{
model: Position,
@@ -79,6 +88,14 @@ const filterContactPersons = async (req, res) => {
const where = {};
const having = [];
// Filter für nicht abgelaufene Kontaktpersonen
const today = new Date();
today.setHours(0, 0, 0, 0);
where[Op.or] = [
{ expiryDate: null },
{ expiryDate: { [Op.gte]: today } }
];
if (config.selection.id && config.selection.id === 'all') {
// No additional filter needed for "all"
} else if (config.selection.id) {

View File

@@ -1,32 +1,188 @@
const EventService = require('../services/EventService');
const ErrorHandler = require('../utils/ErrorHandler');
const { Event, Institution, EventPlace, ContactPerson, EventType } = require('../models');
const { Op } = require('sequelize');
const moment = require('moment'); // Import von Moment.js
exports.getAllEvents = ErrorHandler.asyncHandler(async (req, res) => {
const events = await EventService.getAllEvents();
ErrorHandler.successResponse(res, events, 'Events erfolgreich abgerufen');
});
const getAllEvents = async (req, res) => {
try {
const events = await Event.findAll({
include: [
{ model: Institution, as: 'institution' },
{ model: EventPlace, as: 'eventPlace' },
{ model: EventType, as: 'eventType' },
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } }
],
order: ['name', 'date', 'time']
});
res.json(events);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch events' });
console.error(error);
}
};
exports.getEventById = ErrorHandler.asyncHandler(async (req, res) => {
const event = await EventService.getEventById(req.params.id);
ErrorHandler.successResponse(res, event, 'Event erfolgreich abgerufen');
});
const filterEvents = async (req, res) => {
try {
const request = req.body;
const where = {
[Op.or]: [
{
date: {
[Op.or]: [
{ [Op.gte]: moment().startOf('day').toDate() },
{ [Op.eq]: null }
]
}
},
{ dayOfWeek: { [Op.gte]: 0 } }
]
};
const order = [
['date', 'ASC'],
['time', 'ASC']
];
exports.filterEvents = ErrorHandler.asyncHandler(async (req, res) => {
const result = await EventService.filterEvents(req.body);
ErrorHandler.successResponse(res, result, 'Events erfolgreich gefiltert');
});
if (request.id === 'all') {
const events = await Event.findAll({
where,
include: [
{ model: Institution, as: 'institution' },
{ model: EventPlace, as: 'eventPlace' },
{ model: EventType, as: 'eventType' },
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } }
],
order: order,
logging: console.log // Log the generated SQL query
});
return res.json({ events });
}
exports.createEvent = ErrorHandler.asyncHandler(async (req, res) => {
const event = await EventService.createEvent(req.body);
ErrorHandler.successResponse(res, event, 'Event erfolgreich erstellt', 201);
});
if (request.id === 'home') {
const events = await Event.findAll({
where: {
alsoOnHomepage: 1,
date: { [Op.gte]: moment().startOf('day').toDate() }
},
include: [
{ model: Institution, as: 'institution' },
{ model: EventPlace, as: 'eventPlace' },
{ model: EventType, as: 'eventType' },
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } },
],
order: order,
});
return res.json({ events });
}
exports.updateEvent = ErrorHandler.asyncHandler(async (req, res) => {
const event = await EventService.updateEvent(req.params.id, req.body);
ErrorHandler.successResponse(res, event, 'Event erfolgreich aktualisiert');
});
if (!request.id && !request.places && !request.types) {
return res.json({ events: [], places: [], types: [], contactPersons: [] });
}
exports.deleteEvent = ErrorHandler.asyncHandler(async (req, res) => {
const result = await EventService.deleteEvent(req.params.id);
ErrorHandler.successResponse(res, result, result.message);
});
if (request.id) {
where.id = request.id;
}
if (request.places && request.places.length > 0) {
where.event_place_id = {
[Op.in]: request.places.map(id => parseInt(id))
};
}
if (request.types && request.types.length > 0) {
where.eventTypeId = {
[Op.in]: request.types.map(id => parseInt(id))
};
}
const events = await Event.findAll({
where,
include: [
{ model: Institution, as: 'institution' },
{ model: EventPlace, as: 'eventPlace' },
{ model: EventType, as: 'eventType' },
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } }
],
order: order,
});
const displayFields = request.display ? request.display : [];
const filteredEvents = events.map(event => {
const filteredEvent = { ...event.toJSON() };
if (!displayFields.includes('name')) delete filteredEvent.name;
if (!displayFields.includes('type')) delete filteredEvent.eventType;
if (!displayFields.includes('place')) delete filteredEvent.eventPlace;
if (!displayFields.includes('description')) delete filteredEvent.description;
if (!displayFields.includes('time')) delete filteredEvent.time;
if (!displayFields.includes('time')) delete filteredEvent.endTime;
if (!displayFields.includes('contactPerson')) delete filteredEvent.contactPersons;
if (!displayFields.includes('day')) delete filteredEvent.dayOfWeek;
if (!displayFields.includes('institution')) delete filteredEvent.institution;
return filteredEvent;
});
res.json({ events: filteredEvents });
} catch (error) {
res.status(500).json({ error: 'Failed to filter events' });
console.error(error);
}
};
const createEvent = async (req, res) => {
try {
const { contactPersonIds, ...eventData } = req.body;
eventData.alsoOnHomepage = eventData.alsoOnHomepage ?? 0;
const event = await Event.create(eventData);
if (contactPersonIds) {
await event.setContactPersons(contactPersonIds);
}
res.status(201).json(event);
} catch (error) {
res.status(500).json({ error: 'Failed to create event' });
console.error(error);
}
};
const updateEvent = async (req, res) => {
try {
const { id } = req.params;
const { contactPersonIds, ...eventData } = req.body;
const event = await Event.findByPk(id);
if (!event) {
return res.status(404).json({ error: 'Event not found' });
}
await event.update(eventData);
if (contactPersonIds) {
await event.setContactPersons(contactPersonIds);
}
res.status(200).json(event);
} catch (error) {
res.status(500).json({ error: 'Failed to update event' });
console.error(error);
}
};
const deleteEvent = async (req, res) => {
try {
const { id } = req.params;
const deleted = await Event.destroy({
where: { id: id }
});
if (deleted) {
res.status(204).json();
} else {
res.status(404).json({ error: 'Event not found' });
}
} catch (error) {
res.status(500).json({ error: 'Failed to delete event' });
console.error(error);
}
};
module.exports = {
getAllEvents,
createEvent,
updateEvent,
deleteEvent,
filterEvents
};

View File

@@ -0,0 +1,154 @@
const { LiturgicalDay } = require('../models');
const { Op } = require('sequelize');
const axios = require('axios');
// Alle liturgischen Tage abrufen
const getAllLiturgicalDays = async (req, res) => {
try {
const days = await LiturgicalDay.findAll({
order: [['date', 'ASC']]
});
res.status(200).json(days);
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Fehler beim Abrufen der liturgischen Tage' });
}
};
// Eindeutige Namen für Multiselect abrufen
const getLiturgicalDayNames = async (req, res) => {
try {
const days = await LiturgicalDay.findAll({
attributes: ['dayName'],
group: ['dayName'],
order: [['dayName', 'ASC']]
});
const names = days.map(day => day.dayName);
res.status(200).json(names);
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Fehler beim Abrufen der Tag-Namen' });
}
};
// HTML von liturgischem Kalender parsen und in DB speichern
const loadLiturgicalYear = async (req, res) => {
const { year } = req.body;
if (!year) {
return res.status(400).json({ message: 'Jahr ist erforderlich' });
}
const currentYear = new Date().getFullYear();
if (year < currentYear || year > currentYear + 2) {
return res.status(400).json({ message: 'Jahr muss zwischen aktuellem Jahr und 2 Jahren in der Zukunft liegen' });
}
try {
const url = `https://www.eike-fleer.de/liturgischer-kalender/${year}.htm`;
const response = await axios.get(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
});
const html = response.data;
// Parse HTML - suche nach Tabellenzeilen mit Datum und Name
// Format: "DD.MM.YYYY &nbsp; &nbsp; &nbsp; DayName"
const regex = /(\d{2}\.\d{2}\.\d{4})\s*(?:&nbsp;|\s)+(.+?)(?:<\/|$)/gi;
const matches = [...html.matchAll(regex)];
const liturgicalDays = [];
for (const match of matches) {
const dateStr = match[1]; // DD.MM.YYYY
let dayName = match[2];
// Bereinige den Tag-Namen von HTML-Tags und Entities
dayName = dayName
.replace(/<[^>]*>/g, '') // Entferne HTML-Tags
.replace(/&nbsp;/g, ' ') // Ersetze &nbsp;
.replace(/&auml;/g, 'ä')
.replace(/&ouml;/g, 'ö')
.replace(/&uuml;/g, 'ü')
.replace(/&Auml;/g, 'Ä')
.replace(/&Ouml;/g, 'Ö')
.replace(/&Uuml;/g, 'Ü')
.replace(/&szlig;/g, 'ß')
.trim();
// Konvertiere Datum von DD.MM.YYYY zu YYYY-MM-DD
const [day, month, yearPart] = dateStr.split('.');
const isoDate = `${yearPart}-${month}-${day}`;
if (dayName && dayName.length > 0) {
liturgicalDays.push({
date: isoDate,
dayName: dayName
});
}
}
if (liturgicalDays.length === 0) {
return res.status(500).json({ message: 'Keine liturgischen Tage gefunden. Möglicherweise hat sich das HTML-Format geändert.' });
}
// Speichere oder aktualisiere die Einträge
for (const day of liturgicalDays) {
await LiturgicalDay.upsert({
date: day.date,
dayName: day.dayName
});
}
res.status(200).json({
message: `${liturgicalDays.length} liturgische Tage für ${year} erfolgreich geladen`,
count: liturgicalDays.length
});
} catch (error) {
console.error('Fehler beim Laden der liturgischen Tage:', error);
if (error.response && error.response.status === 404) {
return res.status(404).json({ message: `Liturgischer Kalender für ${year} nicht gefunden` });
}
res.status(500).json({ message: 'Fehler beim Laden der liturgischen Tage', error: error.message });
}
};
// Einzelnen Tag erstellen
const createLiturgicalDay = async (req, res) => {
try {
const day = await LiturgicalDay.create(req.body);
res.status(201).json(day);
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Fehler beim Erstellen des liturgischen Tags' });
}
};
// Tag löschen
const deleteLiturgicalDay = async (req, res) => {
try {
const { id } = req.params;
const deleted = await LiturgicalDay.destroy({
where: { id }
});
if (deleted) {
res.status(200).json({ message: 'Liturgischer Tag erfolgreich gelöscht' });
} else {
res.status(404).json({ message: 'Liturgischer Tag nicht gefunden' });
}
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Fehler beim Löschen des liturgischen Tags' });
}
};
module.exports = {
getAllLiturgicalDays,
getLiturgicalDayNames,
loadLiturgicalYear,
createLiturgicalDay,
deleteLiturgicalDay
};

View File

@@ -1,12 +1,30 @@
const MenuDataService = require('../services/MenuDataService');
const ErrorHandler = require('../utils/ErrorHandler');
const { MenuItem } = require('../models');
const fetchMenuData = require('../utils/fetchMenuData');
exports.getMenuData = ErrorHandler.asyncHandler(async (req, res) => {
const menuData = await MenuDataService.getMenuData();
ErrorHandler.successResponse(res, menuData, 'Menü-Daten erfolgreich abgerufen');
});
exports.getMenuData = async (req, res) => {
try {
const menuData = await fetchMenuData();
res.json(menuData);
} catch (error) {
res.status(500).send('Error fetching menu data');
}
};
exports.saveMenuData = ErrorHandler.asyncHandler(async (req, res) => {
const result = await MenuDataService.saveMenuData(req.body);
ErrorHandler.successResponse(res, result, result.message);
});
exports.saveMenuData = async (req, res) => {
try {
const menuData = req.body;
const adjustedMenuData = menuData.map(item => {
item.parent_id = item.parent_id < 0 ? null : item.parent_id;
return item;
})
.sort((a, b) => (a.parent_id === null ? -1 : 1) - (b.parent_id === null ? -1 : 1));
await MenuItem.destroy({ where: {} });
for (const item of adjustedMenuData) {
await MenuItem.create(item);
}
res.status(200).send('Menü-Daten erfolgreich gespeichert');
} catch (error) {
console.error('Fehler beim Speichern der Menü-Daten:', error);
res.status(500).send('Fehler beim Speichern der Menü-Daten');
}
};

View File

@@ -1,27 +1,48 @@
const PageService = require('../services/PageService');
const ErrorHandler = require('../utils/ErrorHandler');
// controllers/pageController.js
const { Page } = require('../models');
exports.getMenuData = ErrorHandler.asyncHandler(async (req, res) => {
const pages = await PageService.getAllPages();
ErrorHandler.successResponse(res, pages, 'Seiten erfolgreich abgerufen');
});
exports.getMenuData = async (req, res) => {
try {
const pages = await Page.findAll({
attributes: ['link', 'name']
});
res.json(pages);
} catch (error) {
console.error('Fehler beim Abrufen der Seiten:', error);
res.status(500).json({ message: 'Fehler beim Abrufen der Seiten' });
}
};
exports.getPageContent = ErrorHandler.asyncHandler(async (req, res) => {
const result = await PageService.getPageContent(req.query.link);
ErrorHandler.successResponse(res, result, 'Seiteninhalt erfolgreich abgerufen');
});
exports.getPageContent = async (req, res) => {
try {
const page = await Page.findOne({
where: { link: req.query.link }
});
if (page) {
res.json({ content: page.content });
} else {
res.json({ content: "" });
}
} catch (error) {
console.error('Fehler beim Laden des Seiteninhalts:', error);
res.status(500).json({ message: 'Fehler beim Laden des Seiteninhalts' });
}
};
exports.savePageContent = ErrorHandler.asyncHandler(async (req, res) => {
const result = await PageService.savePageContent(req.body);
ErrorHandler.successResponse(res, result, result.message);
});
exports.getPageById = ErrorHandler.asyncHandler(async (req, res) => {
const page = await PageService.getPageById(req.params.id);
ErrorHandler.successResponse(res, page, 'Seite erfolgreich abgerufen');
});
exports.deletePage = ErrorHandler.asyncHandler(async (req, res) => {
const result = await PageService.deletePage(req.params.id);
ErrorHandler.successResponse(res, result, result.message);
});
exports.savePageContent = async (req, res) => {
try {
const { link, name, content } = req.body;
let page = await Page.findOne({ where: { link } });
if (page) {
page.content = content;
page.name = name;
} else {
page = await Page.create({ link, name, content });
}
await page.save();
res.json({ message: 'Seiteninhalt gespeichert', page });
} catch (error) {
console.error('Fehler beim Speichern des Seiteninhalts:', error);
res.status(500).json({ message: 'Fehler beim Speichern des Seiteninhalts' });
}
};

View File

@@ -1,42 +1,104 @@
const UserService = require('../services/UserService');
const UserValidator = require('../validators/UserValidator');
const ErrorHandler = require('../utils/ErrorHandler');
const { User } = require('../models');
exports.getAllUsers = ErrorHandler.asyncHandler(async (req, res) => {
const users = await UserService.getAllUsers();
ErrorHandler.successResponse(res, users, 'Benutzer erfolgreich abgerufen');
});
exports.getAllUsers = async (req, res) => {
try {
const users = await User.findAll({
order: [['name', 'ASC']],
attributes: ['id', 'name', 'email', 'active', 'created_at'] // Passwort ausschließen
});
res.status(200).json(users);
} catch (error) {
console.error('Error fetching users:', error);
res.status(500).json({ message: 'Error fetching users' });
}
};
exports.getUserById = ErrorHandler.asyncHandler(async (req, res) => {
UserValidator.validateId(req.params.id);
const user = await UserService.getUserById(req.params.id);
ErrorHandler.successResponse(res, user, 'Benutzer erfolgreich abgerufen');
});
exports.getUserById = async (req, res) => {
try {
const user = await User.findByPk(req.params.id, {
attributes: ['id', 'name', 'email', 'active', 'created_at'] // Passwort ausschließen
});
if (user) {
res.status(200).json(user);
} else {
res.status(404).json({ message: 'User not found' });
}
} catch (error) {
console.error('Error fetching user:', error);
res.status(500).json({ message: 'Error fetching user' });
}
};
exports.createUser = ErrorHandler.asyncHandler(async (req, res) => {
UserValidator.validateCreateUser(req.body);
const user = await UserService.createUser(req.body);
ErrorHandler.successResponse(res, user, 'Benutzer erfolgreich erstellt', 201);
});
exports.createUser = async (req, res) => {
try {
const user = await User.create(req.body);
exports.updateUser = ErrorHandler.asyncHandler(async (req, res) => {
UserValidator.validateId(req.params.id);
UserValidator.validateUpdateUser(req.body);
const user = await UserService.updateUser(req.params.id, req.body);
ErrorHandler.successResponse(res, user, 'Benutzer erfolgreich aktualisiert');
});
// Sichere User-Daten zurückgeben (ohne Passwort)
const safeUser = {
id: user.id,
name: user.name,
email: user.email,
active: user.active,
created_at: user.created_at
};
exports.deleteUser = ErrorHandler.asyncHandler(async (req, res) => {
UserValidator.validateId(req.params.id);
await UserService.deleteUser(req.params.id);
ErrorHandler.successResponse(res, null, 'Benutzer erfolgreich gelöscht');
});
res.status(201).json(safeUser);
} catch (error) {
console.error('Error creating user:', error);
res.status(500).json({ message: 'Error creating user' });
}
};
// Neue Route für Passwort-Änderung
exports.changePassword = ErrorHandler.asyncHandler(async (req, res) => {
const { currentPassword, newPassword } = req.body;
UserValidator.validateId(req.params.id);
UserValidator.validatePasswordChange(currentPassword, newPassword);
await UserService.changePassword(req.params.id, currentPassword, newPassword);
ErrorHandler.successResponse(res, null, 'Passwort erfolgreich geändert');
});
exports.updateUser = async (req, res) => {
try {
const user = await User.findByPk(req.params.id);
if (user) {
// Erstelle eine Kopie der Request-Daten ohne sensible Felder
const updateData = { ...req.body };
// Entferne sensible Felder, die niemals über diese Route geändert werden dürfen
delete updateData.password;
delete updateData.id;
delete updateData.created_at;
// Setze updated_at auf aktuelle Zeit
updateData.updated_at = new Date();
// Logging für Debugging
console.log('Updating user:', req.params.id, 'with data:', updateData);
await user.update(updateData);
// Sichere User-Daten zurückgeben (ohne Passwort)
const safeUser = {
id: user.id,
name: user.name,
email: user.email,
active: user.active,
created_at: user.created_at
};
res.status(200).json(safeUser);
} else {
res.status(404).json({ message: 'User not found' });
}
} catch (error) {
console.error('Error updating user:', error);
res.status(500).json({ message: 'Error updating user' });
}
};
exports.deleteUser = async (req, res) => {
try {
const user = await User.findByPk(req.params.id);
if (user) {
await user.destroy();
res.status(200).json({ message: 'User deleted successfully' });
} else {
res.status(404).json({ message: 'User not found' });
}
} catch (error) {
console.error('Error deleting user:', error);
res.status(500).json({ message: 'Error deleting user' });
}
};

File diff suppressed because it is too large Load Diff

31
deploy.sh Executable file
View 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."

View File

@@ -0,0 +1,28 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.createTable('liturgical_days', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
date: {
type: Sequelize.DATEONLY,
allowNull: false,
unique: true
},
dayName: {
type: Sequelize.STRING,
allowNull: false
}
});
},
async down (queryInterface, Sequelize) {
await queryInterface.dropTable('liturgical_days');
}
};

View File

@@ -0,0 +1,16 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('worships', 'organ_playing', {
type: Sequelize.STRING,
allowNull: true
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('worships', 'organ_playing');
}
};

View File

@@ -0,0 +1,17 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('worships', 'approved', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false
});
},
down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('worships', 'approved');
}
};

View File

@@ -25,6 +25,10 @@ module.exports = (sequelize) => {
email: {
type: DataTypes.STRING,
allowNull: true
},
expiryDate: {
type: DataTypes.DATEONLY,
allowNull: true
}
}, {
tableName: 'contact_persons',

24
models/LiturgicalDay.js Normal file
View File

@@ -0,0 +1,24 @@
module.exports = (sequelize, DataTypes) => {
const LiturgicalDay = sequelize.define('LiturgicalDay', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
date: {
type: DataTypes.DATEONLY,
allowNull: false,
unique: true
},
dayName: {
type: DataTypes.STRING,
allowNull: false
}
}, {
tableName: 'liturgical_days',
timestamps: false
});
return LiturgicalDay;
};

View File

@@ -61,6 +61,17 @@ module.exports = (sequelize) => {
allowNull: true,
field: 'sacristan_service'
},
organPlaying: {
type: DataTypes.STRING,
allowNull: true,
field: 'organ_playing'
},
approved: {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: false,
field: 'approved'
},
}, {
tableName: 'worships',
timestamps: true

27499
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@
"@tiptap/extension-underline": "^2.4.0",
"@tiptap/starter-kit": "^2.4.0",
"@tiptap/vue-3": "^2.4.0",
"@vue/cli": "^5.0.8",
"@vue/cli": "^4.2.2",
"axios": "^1.7.2",
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.2",
@@ -34,10 +34,12 @@
"cors": "^2.8.5",
"crypto": "^1.0.1",
"date-fns": "^3.6.0",
"docx": "^9.5.1",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"file-saver": "^2.0.5",
"jsonwebtoken": "^9.0.2",
"mammoth": "^1.11.0",
"moment": "^2.30.1",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.10.1",

6
renovate.json Normal file
View File

@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}

19
routes/liturgicalDays.js Normal file
View File

@@ -0,0 +1,19 @@
const express = require('express');
const router = express.Router();
const {
getAllLiturgicalDays,
getLiturgicalDayNames,
loadLiturgicalYear,
createLiturgicalDay,
deleteLiturgicalDay
} = require('../controllers/liturgicalDayController');
const authMiddleware = require('../middleware/authMiddleware');
router.get('/', getAllLiturgicalDays);
router.get('/names', getLiturgicalDayNames);
router.post('/load-year', authMiddleware, loadLiturgicalYear);
router.post('/', authMiddleware, createLiturgicalDay);
router.delete('/:id', authMiddleware, deleteLiturgicalDay);
module.exports = router;

View File

@@ -1,13 +1,12 @@
const express = require('express');
const router = express.Router();
const { getAllUsers, createUser, updateUser, deleteUser, getUserById, changePassword } = require('../controllers/userController');
const { getAllUsers, createUser, updateUser, deleteUser, getUserById } = require('../controllers/userController');
const authMiddleware = require('../middleware/authMiddleware');
router.get('/', authMiddleware, getAllUsers);
router.get('/:id', authMiddleware, getUserById);
router.post('/', authMiddleware, createUser);
router.put('/:id', authMiddleware, updateUser);
router.put('/:id/change-password', authMiddleware, changePassword);
router.delete('/:id', authMiddleware, deleteUser);
module.exports = router;

View File

@@ -1,12 +1,16 @@
const express = require('express');
const router = express.Router();
const { getAllWorships, createWorship, updateWorship, deleteWorship, getFilteredWorships } = require('../controllers/worshipController');
const { getAllWorships, createWorship, updateWorship, deleteWorship, getFilteredWorships, getWorshipOptions, importWorships, uploadImportFile, exportWorships, saveImportedWorships } = require('../controllers/worshipController');
const authMiddleware = require('../middleware/authMiddleware');
router.get('/', getAllWorships);
router.get('/options', getWorshipOptions);
router.post('/', authMiddleware, createWorship);
router.post('/import', authMiddleware, uploadImportFile, importWorships);
router.post('/import/save', authMiddleware, saveImportedWorships);
router.put('/:id', authMiddleware, updateWorship);
router.delete('/:id', authMiddleware, deleteWorship);
router.get('/filtered', getFilteredWorships);
router.get('/export', authMiddleware, exportWorships);
module.exports = router;

View File

@@ -2,7 +2,14 @@ const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const https = require('https');
const http = require('http');
const fs = require('fs');
require('dotenv').config();
// Erhöhe maxHttpHeaderSize für Node.js (Standard ist 8KB, erhöhe auf 16KB)
if (http.maxHeaderSize !== undefined) {
http.maxHeaderSize = 16384;
}
const sequelize = require('./config/database');
const authRouter = require('./routes/auth');
const eventTypesRouter = require('./routes/eventtypes');
@@ -17,12 +24,79 @@ const pageRouter = require('./routes/pages');
const userRouter = require('./routes/users');
const imageRouter = require('./routes/image');
const filesRouter = require('./routes/files');
const liturgicalDaysRouter = require('./routes/liturgicalDays');
const app = express();
const PORT = 3002;
const PORT = parseInt(process.env.PORT, 10) || 3000;
app.use(cors());
app.use(bodyParser.json());
// 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
}
// Prüfe exakte Übereinstimmung
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
// Für Entwicklung: Erlaube localhost und torstens auf jedem Port
try {
const originUrl = new URL(origin);
const hostname = originUrl.hostname.toLowerCase();
const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
const isTorstens = hostname === 'torstens' || hostname.includes('torstens');
if (isLocalhost || isTorstens) {
return callback(null, true);
}
} catch (e) {
// Falls URL-Parsing fehlschlägt, prüfe mit Regex
const isLocalhost = /^https?:\/\/(localhost|127\.0\.0\.1|::1)(:\d+)?$/.test(origin);
const isTorstens = /^https?:\/\/torstens(:\d+)?/.test(origin);
if (isLocalhost || isTorstens) {
return callback(null, true);
}
}
// Prüfe auch ohne Port (für Flexibilität)
const originWithoutPort = origin.replace(/:\d+$/, '');
const allowedWithoutPort = allowedOrigins.some(allowed => {
const allowedWithoutPort = allowed.replace(/:\d+$/, '');
return originWithoutPort === allowedWithoutPort;
});
if (allowedWithoutPort) {
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());
// Erhöhe Header-Limits für große Requests
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' }));
// Erhöhe maxHttpHeaderSize (Node.js 18.3.0+)
if (process.versions.node.split('.')[0] >= 18) {
require('http').maxHeaderSize = 16384; // 16KB (Standard ist 8KB)
}
app.use('/api/auth', authRouter);
app.use('/api/event-types', eventTypesRouter);
@@ -37,6 +111,7 @@ app.use('/api/page-content', pageRouter);
app.use('/api/users', userRouter);
app.use('/api/image', imageRouter);
app.use('/api/files', filesRouter);
app.use('/api/liturgical-days', liturgicalDaysRouter);
const options = {
key: fs.readFileSync('server.key'),
@@ -48,7 +123,7 @@ sequelize.sync().then(() => {
/* https.createServer(options, app).listen(PORT, () => {
console.log(`Server läuft auf Port ${PORT}`);
});*/
app.listen(PORT, () => {
console.log(`Server läuft auf Port ${PORT}`);
app.listen(PORT, '0.0.0.0', () => {
console.log(`Server läuft auf Port ${PORT} (IPv4 und IPv6)`);
});
});

View File

@@ -1,186 +0,0 @@
const bcrypt = require('bcryptjs');
const { User, PasswordResetToken } = require('../models');
const jwt = require('jsonwebtoken');
const { addTokenToBlacklist } = require('../utils/blacklist');
const { transporter, getPasswordResetEmailTemplate } = require('../config/email');
const crypto = require('crypto');
class AuthService {
/**
* User registrieren
*/
async register(userData) {
const { name, email, password } = userData;
if (!name || !email || !password) {
throw new Error('VALIDATION_ERROR: Alle Felder sind erforderlich');
}
// Prüfen ob E-Mail bereits existiert
const existingUser = await User.findOne({ where: { email } });
if (existingUser) {
throw new Error('EMAIL_ALREADY_EXISTS');
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = await User.create({
name,
email,
password: hashedPassword,
active: true
});
return this.getSafeUserData(user);
}
/**
* User einloggen
*/
async login(credentials) {
const { email, password } = credentials;
if (!email || !password) {
throw new Error('VALIDATION_ERROR: Email und Passwort sind erforderlich');
}
const user = await User.findOne({ where: { email } });
if (!user) {
throw new Error('INVALID_CREDENTIALS');
}
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
*/
async logout(token) {
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) {
throw new Error('VALIDATION_ERROR: E-Mail-Adresse ist erforderlich');
}
const user = await User.findOne({ where: { email } });
if (!user) {
// Aus Sicherheitsgründen immer Erfolg melden
return { message: 'Falls die E-Mail-Adresse in unserem System registriert ist, erhalten Sie einen Link zum Zurücksetzen des Passworts.' };
}
// 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
*/
async resetPassword(token, newPassword) {
if (!token || !newPassword) {
throw new Error('VALIDATION_ERROR: Token und neues Passwort sind erforderlich');
}
if (newPassword.length < 6) {
throw new Error('VALIDATION_ERROR: Passwort muss mindestens 6 Zeichen lang sein');
}
// Token validieren
const resetToken = await PasswordResetToken.findOne({
where: {
token,
used: false,
expiresAt: {
[require('sequelize').Op.gt]: new Date()
}
},
include: [{ model: User, as: 'user' }]
});
if (!resetToken) {
throw new Error('INVALID_RESET_TOKEN');
}
// 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)
*/
getSafeUserData(user) {
return {
id: user.id,
name: user.name,
email: user.email,
active: user.active,
created_at: user.created_at
};
}
}
module.exports = new AuthService();

View File

@@ -1,276 +0,0 @@
const { Event, Institution, EventPlace, ContactPerson, EventType } = require('../models');
const { Op } = require('sequelize');
const moment = require('moment');
class EventService {
/**
* Alle Events abrufen
*/
async getAllEvents() {
try {
const events = await Event.findAll({
include: [
{ model: Institution, as: 'institution' },
{ model: EventPlace, as: 'eventPlace' },
{ model: EventType, as: 'eventType' },
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } }
],
order: ['name', 'date', 'time']
});
return events;
} catch (error) {
console.error('Error fetching all events:', error);
throw new Error('EVENTS_FETCH_ERROR');
}
}
/**
* Event anhand ID abrufen
*/
async getEventById(id) {
try {
if (!id || isNaN(parseInt(id))) {
throw new Error('VALIDATION_ERROR: Ungültige ID');
}
const event = await Event.findByPk(id, {
include: [
{ model: Institution, as: 'institution' },
{ model: EventPlace, as: 'eventPlace' },
{ model: EventType, as: 'eventType' },
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } }
]
});
if (!event) {
throw new Error('EVENT_NOT_FOUND');
}
return event;
} catch (error) {
console.error('Error fetching event by ID:', error);
throw new Error('EVENT_FETCH_ERROR');
}
}
/**
* Events filtern
*/
async filterEvents(filterData) {
try {
const { id, places, types, display } = filterData;
// Basis-Where-Klausel für zukünftige Events
const where = {
[Op.or]: [
{
date: {
[Op.or]: [
{ [Op.gte]: moment().startOf('day').toDate() },
{ [Op.eq]: null }
]
}
},
{ dayOfWeek: { [Op.gte]: 0 } }
]
};
const order = [
['date', 'ASC'],
['time', 'ASC']
];
// Spezielle Filter
if (id === 'all') {
return await this._getAllFutureEvents(where, order);
}
if (id === 'home') {
return await this._getHomepageEvents(where, order);
}
if (!id && !places && !types) {
return { events: [], places: [], types: [], contactPersons: [] };
}
// Weitere Filter anwenden
if (id) {
where.id = id;
}
if (places && places.length > 0) {
where.event_place_id = {
[Op.in]: places.map(id => parseInt(id))
};
}
if (types && types.length > 0) {
where.eventTypeId = {
[Op.in]: types.map(id => parseInt(id))
};
}
const events = await Event.findAll({
where,
include: [
{ model: Institution, as: 'institution' },
{ model: EventPlace, as: 'eventPlace' },
{ model: EventType, as: 'eventType' },
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } }
],
order: order,
});
// Events basierend auf Display-Feldern filtern
const displayFields = display || [];
const filteredEvents = this._filterEventFields(events, displayFields);
return { events: filteredEvents };
} catch (error) {
console.error('Error filtering events:', error);
throw new Error('EVENT_FILTER_ERROR');
}
}
/**
* Event erstellen
*/
async createEvent(eventData) {
try {
const { contactPersonIds, ...eventDataWithoutContacts } = eventData;
// Validierung
if (!eventDataWithoutContacts.name) {
throw new Error('VALIDATION_ERROR: Event-Name ist erforderlich');
}
eventDataWithoutContacts.alsoOnHomepage = eventDataWithoutContacts.alsoOnHomepage ?? 0;
const event = await Event.create(eventDataWithoutContacts);
if (contactPersonIds && contactPersonIds.length > 0) {
await event.setContactPersons(contactPersonIds);
}
return event;
} catch (error) {
console.error('Error creating event:', error);
throw new Error('EVENT_CREATE_ERROR');
}
}
/**
* Event aktualisieren
*/
async updateEvent(id, eventData) {
try {
if (!id || isNaN(parseInt(id))) {
throw new Error('VALIDATION_ERROR: Ungültige ID');
}
const { contactPersonIds, ...eventDataWithoutContacts } = eventData;
const event = await Event.findByPk(id);
if (!event) {
throw new Error('EVENT_NOT_FOUND');
}
await event.update(eventDataWithoutContacts);
if (contactPersonIds !== undefined) {
await event.setContactPersons(contactPersonIds || []);
}
return event;
} catch (error) {
console.error('Error updating event:', error);
throw new Error('EVENT_UPDATE_ERROR');
}
}
/**
* Event löschen
*/
async deleteEvent(id) {
try {
if (!id || isNaN(parseInt(id))) {
throw new Error('VALIDATION_ERROR: Ungültige ID');
}
const event = await Event.findByPk(id);
if (!event) {
throw new Error('EVENT_NOT_FOUND');
}
await event.destroy();
return { message: 'Event erfolgreich gelöscht' };
} catch (error) {
console.error('Error deleting event:', error);
throw new Error('EVENT_DELETE_ERROR');
}
}
/**
* Alle zukünftigen Events abrufen
*/
async _getAllFutureEvents(where, order) {
const events = await Event.findAll({
where,
include: [
{ model: Institution, as: 'institution' },
{ model: EventPlace, as: 'eventPlace' },
{ model: EventType, as: 'eventType' },
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } }
],
order: order,
logging: console.log
});
return { events };
}
/**
* Homepage Events abrufen
*/
async _getHomepageEvents(where, order) {
const events = await Event.findAll({
where: {
alsoOnHomepage: 1,
date: { [Op.gte]: moment().startOf('day').toDate() }
},
include: [
{ model: Institution, as: 'institution' },
{ model: EventPlace, as: 'eventPlace' },
{ model: EventType, as: 'eventType' },
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } },
],
order: order,
});
return { events };
}
/**
* Event-Felder basierend auf Display-Feldern filtern
*/
_filterEventFields(events, displayFields) {
return events.map(event => {
const filteredEvent = { ...event.toJSON() };
if (!displayFields.includes('name')) delete filteredEvent.name;
if (!displayFields.includes('type')) delete filteredEvent.eventType;
if (!displayFields.includes('place')) delete filteredEvent.eventPlace;
if (!displayFields.includes('description')) delete filteredEvent.description;
if (!displayFields.includes('time')) delete filteredEvent.time;
if (!displayFields.includes('time')) delete filteredEvent.endTime;
if (!displayFields.includes('contactPerson')) delete filteredEvent.contactPersons;
if (!displayFields.includes('day')) delete filteredEvent.dayOfWeek;
if (!displayFields.includes('institution')) delete filteredEvent.institution;
return filteredEvent;
});
}
}
module.exports = new EventService();

View File

@@ -1,101 +0,0 @@
const { MenuItem } = require('../models');
class MenuDataService {
/**
* Alle Menü-Daten abrufen
*/
async getMenuData() {
try {
const menuItems = await MenuItem.findAll({
order: [['order_id', 'ASC']],
include: [{
model: MenuItem,
as: 'submenu',
required: false,
order: [['order_id', 'ASC']]
}]
});
const menuData = this.buildMenuStructure(menuItems);
return menuData;
} catch (error) {
console.error('Error fetching menu data:', error);
throw new Error('MENU_DATA_FETCH_ERROR');
}
}
/**
* Menü-Daten speichern
*/
async saveMenuData(menuData) {
try {
if (!Array.isArray(menuData)) {
throw new Error('VALIDATION_ERROR: Menü-Daten müssen ein Array sein');
}
// Menü-Daten anpassen
const adjustedMenuData = menuData.map(item => {
item.parent_id = item.parent_id < 0 ? null : item.parent_id;
return item;
}).sort((a, b) => (a.parent_id === null ? -1 : 1) - (b.parent_id === null ? -1 : 1));
// Alle bestehenden Menü-Items löschen
await MenuItem.destroy({ where: {} });
// Neue Menü-Items erstellen
for (const item of adjustedMenuData) {
await MenuItem.create(item);
}
return { message: 'Menü-Daten erfolgreich gespeichert' };
} catch (error) {
console.error('Error saving menu data:', error);
throw new Error('MENU_DATA_SAVE_ERROR');
}
}
/**
* Menü-Struktur aufbauen
*/
buildMenuStructure(menuItems) {
const menu = [];
const itemMap = {};
// Alle Items in Map speichern
menuItems.forEach(item => {
itemMap[item.id] = {
id: item.id,
name: item.name,
link: item.link,
component: item.component,
showInMenu: item.show_in_menu,
requiresAuth: item.requires_auth,
order_id: item.order_id,
pageTitle: item.page_title,
image: item.image,
submenu: []
};
});
// Hierarchie aufbauen
menuItems.forEach(item => {
if (item.parent_id) {
if (itemMap[item.parent_id]) {
itemMap[item.parent_id].submenu.push(itemMap[item.id]);
}
} else {
menu.push(itemMap[item.id]);
}
});
// Sortierung anwenden
menu.sort((a, b) => a.order_id - b.order_id);
menu.forEach(item => {
item.submenu.sort((a, b) => a.order_id - b.order_id);
});
return menu;
}
}
module.exports = new MenuDataService();

View File

@@ -1,132 +0,0 @@
const { Page } = require('../models');
class PageService {
/**
* Seiteninhalt anhand Link abrufen
*/
async getPageContent(link) {
try {
if (!link) {
throw new Error('VALIDATION_ERROR: Link ist erforderlich');
}
const page = await Page.findOne({ where: { link } });
if (!page) {
throw new Error('PAGE_NOT_FOUND');
}
return {
content: page.content || '',
title: page.title || '',
link: page.link
};
} catch (error) {
console.error('Error fetching page content:', error);
throw new Error('PAGE_CONTENT_FETCH_ERROR');
}
}
/**
* Seiteninhalt speichern
*/
async savePageContent(pageData) {
try {
const { link, name, content } = pageData;
if (!link || !name) {
throw new Error('VALIDATION_ERROR: Link und Name sind erforderlich');
}
// Prüfen ob Seite bereits existiert
const existingPage = await Page.findOne({ where: { link } });
if (existingPage) {
// Seite aktualisieren
await existingPage.update({
content: content || '',
title: name,
updated_at: new Date()
});
} else {
// Neue Seite erstellen
await Page.create({
link,
title: name,
content: content || '',
created_at: new Date(),
updated_at: new Date()
});
}
return { message: 'Seiteninhalt erfolgreich gespeichert' };
} catch (error) {
console.error('Error saving page content:', error);
throw new Error('PAGE_CONTENT_SAVE_ERROR');
}
}
/**
* Alle Seiten abrufen
*/
async getAllPages() {
try {
const pages = await Page.findAll({
order: [['title', 'ASC']],
attributes: ['id', 'link', 'title', 'created_at', 'updated_at']
});
return pages;
} catch (error) {
console.error('Error fetching all pages:', error);
throw new Error('PAGES_FETCH_ERROR');
}
}
/**
* Seite anhand ID abrufen
*/
async getPageById(id) {
try {
if (!id || isNaN(parseInt(id))) {
throw new Error('VALIDATION_ERROR: Ungültige ID');
}
const page = await Page.findByPk(id);
if (!page) {
throw new Error('PAGE_NOT_FOUND');
}
return page;
} catch (error) {
console.error('Error fetching page by ID:', error);
throw new Error('PAGE_FETCH_ERROR');
}
}
/**
* Seite löschen
*/
async deletePage(id) {
try {
if (!id || isNaN(parseInt(id))) {
throw new Error('VALIDATION_ERROR: Ungültige ID');
}
const page = await Page.findByPk(id);
if (!page) {
throw new Error('PAGE_NOT_FOUND');
}
await page.destroy();
return { message: 'Seite erfolgreich gelöscht' };
} catch (error) {
console.error('Error deleting page:', error);
throw new Error('PAGE_DELETE_ERROR');
}
}
}
module.exports = new PageService();

View File

@@ -1,140 +0,0 @@
const { User } = require('../models');
const bcrypt = require('bcryptjs');
class UserService {
/**
* Alle User abrufen (ohne sensible Daten)
*/
async getAllUsers() {
const users = await User.findAll({
order: [['name', 'ASC']],
attributes: ['id', 'name', 'email', 'active', 'created_at', 'updated_at']
});
return users;
}
/**
* User anhand ID abrufen (ohne sensible Daten)
*/
async getUserById(id) {
const user = await User.findByPk(id, {
attributes: ['id', 'name', 'email', 'active', 'created_at', 'updated_at']
});
if (!user) {
throw new Error('USER_NOT_FOUND');
}
return user;
}
/**
* Neuen User erstellen
*/
async createUser(userData) {
// Passwort hashen falls vorhanden
if (userData.password) {
userData.password = await bcrypt.hash(userData.password, 10);
}
const user = await User.create(userData);
// Sichere User-Daten zurückgeben
return this.getSafeUserData(user);
}
/**
* User aktualisieren (ohne sensible Felder)
*/
async updateUser(id, updateData) {
const user = await User.findByPk(id);
if (!user) {
throw new Error('USER_NOT_FOUND');
}
// Erstelle sichere Update-Daten
const safeUpdateData = this.getSafeUpdateData(updateData);
await user.update(safeUpdateData);
return this.getSafeUserData(user);
}
/**
* User löschen
*/
async deleteUser(id) {
const user = await User.findByPk(id);
if (!user) {
throw new Error('USER_NOT_FOUND');
}
await user.destroy();
return true;
}
/**
* User anhand E-Mail abrufen (für interne Verwendung)
*/
async getUserByEmail(email) {
return await User.findOne({ where: { email } });
}
/**
* Passwort ändern (separate Methode für sichere Passwort-Änderung)
*/
async changePassword(id, currentPassword, newPassword) {
const user = await User.findByPk(id);
if (!user) {
throw new Error('USER_NOT_FOUND');
}
// Aktuelles Passwort prüfen
const isValidPassword = await bcrypt.compare(currentPassword, user.password);
if (!isValidPassword) {
throw new Error('INVALID_CURRENT_PASSWORD');
}
// Neues Passwort hashen und speichern
const hashedPassword = await bcrypt.hash(newPassword, 10);
await user.update({ password: hashedPassword });
return true;
}
/**
* Sichere User-Daten extrahieren (ohne Passwort)
*/
getSafeUserData(user) {
return {
id: user.id,
name: user.name,
email: user.email,
active: user.active,
created_at: user.created_at,
updated_at: user.updated_at
};
}
/**
* Sichere Update-Daten erstellen (ohne sensible Felder)
*/
getSafeUpdateData(updateData) {
const safeData = { ...updateData };
// Entferne sensible Felder
delete safeData.password;
delete safeData.id;
delete safeData.created_at;
// Setze updated_at
safeData.updated_at = new Date();
return safeData;
}
}
module.exports = new UserService();

View File

@@ -0,0 +1,4 @@
-- Ablaufdatum zu Kontaktpersonen hinzufügen
ALTER TABLE `contact_persons`
ADD COLUMN `expiryDate` DATE NULL AFTER `email`;

View File

@@ -0,0 +1,12 @@
-- Tabelle für liturgische Kalendertage erstellen
CREATE TABLE IF NOT EXISTS `liturgical_days` (
`id` INT NOT NULL AUTO_INCREMENT,
`date` DATE NOT NULL,
`dayName` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_date` (`date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Index für schnellere Suche nach dayName
CREATE INDEX `idx_dayName` ON `liturgical_days` (`dayName`);

View File

@@ -2,8 +2,11 @@ import axios from 'axios';
import store from './store';
import router from './router';
axios.defaults.baseURL = process.env.VUE_APP_BACKEND_URL;
console.log(process.env.VUE_APP_BACKEND_URL);
// Einheitliche Basis-URL:
// - immer relativ zur aktuellen Origin
// - kein absoluter http/https-Host → verhindert Mixed-Content-Probleme
axios.defaults.baseURL = '/api';
console.log('Axios baseURL:', axios.defaults.baseURL);
axios.interceptors.request.use(
config => {
@@ -24,8 +27,11 @@ axios.interceptors.response.use(
},
error => {
if (error.response && error.response.status === 401) {
store.dispatch('logout');
router.push('/auth/login');
store.dispatch('logout').then(() => {
if (router.currentRoute.value.path !== '/auth/login') {
router.replace('/auth/login');
}
});
}
return Promise.reject(error);
}

View File

@@ -36,27 +36,53 @@ export default {
<style scoped>
.dialog-overlay {
top: calc(50% - 25em);
left: 5%;
width: 90%;
height: 50em;
background: rgba(0, 0, 0, .5);
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
overflow: auto;
z-index: 1000;
}
.dialog {
background: white;
padding: 20px;
border-radius: 5px;
padding: 30px;
border-radius: 8px;
max-width: 400px;
width: 100%;
width: 90%;
text-align: center;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
button {
.dialog h2 {
margin-top: 0;
margin-bottom: 15px;
color: #333;
font-size: 20px;
}
.dialog p {
margin: 15px 0;
color: #666;
line-height: 1.5;
}
.dialog button {
margin-top: 20px;
padding: 10px 20px;
background-color: #007BFF;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.dialog button:hover {
background-color: #0056b3;
}
</style>

View File

@@ -2,7 +2,7 @@
<footer class="footer">
<div class="left-links">
<router-link class="login-link" to="/auth/login" v-if="!isLoggedIn">Login</router-link>
<a v-if="isLoggedIn" @click="logout" class="logout-link">Logout</a>
<a v-if="isLoggedIn" @click="handleLogout" class="logout-link">Logout</a>
</div>
<div class="right-links">
<router-link to="/terms">Impressum</router-link>

View File

@@ -20,6 +20,9 @@
<label for="email">Email:</label>
<input type="email" id="email" v-model="localContactPerson.email">
<label for="expiryDate">Ablaufdatum (optional):</label>
<input type="date" id="expiryDate" v-model="localContactPerson.expiryDate">
<label for="positions">Positionen:</label>
<multiselect
v-model="selectedPositions"
@@ -57,6 +60,7 @@ export default {
zipcode: '',
city: '',
email: '',
expiryDate: null,
positions: []
})
},
@@ -111,6 +115,7 @@ export default {
zipcode: '',
city: '',
email: '',
expiryDate: null,
positions: []
};
this.selectedPositions = [];

View File

@@ -1,7 +1,7 @@
<template>
<div v-if="config && config.style === 'box' && contacts && contacts.length && contacts.length > 0">
<div v-for="contact in contacts" :key="contact.id" class="contact-box bottom-margin">
<p>{{ contact.name }}</p>
<p>{{ contact.name }} <span v-if="contact.expiryDate" class="expiry-date">(bis {{ formatDate(contact.expiryDate) }})</span></p>
<p v-if="displayOptions.includes('phone')">Telefon: {{ contact.phone }}</p>
<p v-if="displayOptions.includes('street')">Straße: {{ contact.street }}</p>
<p v-if="displayOptions.includes('zipcode')">Postleitzahl: {{ contact.zipcode }}</p>
@@ -13,7 +13,7 @@
</div>
<span v-else-if="config.style === 'float' && contacts && contacts.length && contacts.length > 0">
<span v-for="contact in contacts" :key="contact.id" class="bottom-margin">
{{ contact.name }}
{{ contact.name }}<span v-if="contact.expiryDate" class="expiry-date"> (bis {{ formatDate(contact.expiryDate) }})</span>
<span v-if="displayOptions.includes('phone')">, Telefon: {{ contact.phone }}</span>
<span v-if="displayOptions.includes('street')">, Straße: {{ contact.street }}</span>
<span v-if="displayOptions.includes('zipcode')">, Postleitzahl: {{ contact.zipcode }}</span>
@@ -58,6 +58,17 @@ export default {
this.loading = false;
}
},
methods: {
formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
}
}
};
</script>
@@ -68,4 +79,9 @@ export default {
.bottom-margin {
margin-bottom: 1rem;
}
.expiry-date {
font-size: 0.9em;
color: #666;
font-style: italic;
}
</style>

View File

@@ -312,6 +312,15 @@ export default {
this.assignedImage = null;
this.imageFilename = '';
},
focusFirstField() {
// Fokussiert das erste Eingabefeld (Name)
this.$nextTick(() => {
const nameInput = document.getElementById('name');
if (nameInput) {
nameInput.focus();
}
});
}
}
};
</script>

View File

@@ -11,7 +11,7 @@
formatTime(event.endTime) }}</span> Uhr</div>
<div v-if="shouldDisplay('place')">{{ event.eventPlace?.name }}</div>
<div v-if="shouldDisplay('description')" class="description">{{ event.description }}</div>
<div v-if="shouldDisplay('contactPerson')">{{event.contactPersons.map(cp => cp.name).join(', ')}}
<div v-if="shouldDisplay('contactPerson')">{{event.contactPersons.map(cp => formatContactPerson(cp)).join(', ')}}
</div>
<div v-if="shouldDisplay('institution')">{{ event.institution?.name }}</div>
<div v-if="shouldDisplay('type')">{{ event.eventType?.caption }}</div>
@@ -28,7 +28,7 @@
formatTime(events[0].endTime) }}</span> Uhr</div>
<div v-if="shouldDisplay('place')">{{ events[0].eventPlace?.name }}</div>
<div v-if="shouldDisplay('description')" class="description">{{ events[0].description }}</div>
<div v-if="shouldDisplay('contactPerson')">{{events[0].contactPersons.map(cp => cp.name).join(', ')}}
<div v-if="shouldDisplay('contactPerson')">{{events[0].contactPersons.map(cp => formatContactPerson(cp)).join(', ')}}
</div>
<div v-if="shouldDisplay('institution')">{{ events[0].institution?.name }}</div>
<div v-if="shouldDisplay('type')">{{ events[0].eventType?.caption }}</div>
@@ -102,6 +102,18 @@ export default {
const path = '/images/uploads/' + response.data.filename;
console.log(path);
return path;
},
formatContactPerson(contactPerson) {
if (!contactPerson.expiryDate) {
return contactPerson.name;
}
const date = new Date(contactPerson.expiryDate);
const formattedDate = date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
return `${contactPerson.name} (bis ${formattedDate})`;
}
}
};

View File

@@ -11,21 +11,35 @@
<div v-if="worship.neighborInvitation" class="neighborhood-invitation">Einladung zum Gottesdienst im
Nachbarschaftsraum:</div>
<h3>
<span :class="worship.highlightTime ? 'highlight-time' : ''">{{ formatTime(worship.time)
}}</span>&nbsp;-&nbsp;
{{ worship.title ? worship.title : `Gottesdienst in ${worship.eventPlace.name}` }}
<span
:class="worship.highlightTime ? 'highlight-time' : ''"
>{{ formatTime(worship.time) }}</span>&nbsp;-&nbsp;
{{
worship.title
? worship.title
: (worship.eventPlace && worship.eventPlace.name
? `Gottesdienst in ${worship.eventPlace.name}`
: 'Gottesdienst')
}}
</h3>
<div v-if="worship.organizer">Gestaltung: {{ worship.organizer }}</div>
<div v-if="worship.sacristanService" class="internal-information">Küsterdienst: {{ worship.sacristanService }}</div>
<div v-if="worship.collection">Kollekte: {{ worship.collection }}</div>
<div v-if="worship.organPlaying" class="internal-information">Orgelspiel: {{ worship.organPlaying }}</div>
<div v-if="worship.address">{{ worship.address }}</div>
<div v-if="!worship.address && worship.eventPlace.id && worship.eventPlace.id">
Adresse: {{ worship.eventPlace.name }}, {{ worship.eventPlace.street }}, {{
worship.eventPlace.city }}
<div
v-if="!worship.address && worship.eventPlace && worship.eventPlace.id"
>
Adresse: {{ worship.eventPlace.name }}, {{ worship.eventPlace.street }}, {{ worship.eventPlace.city }}
</div>
<div v-if="worship.selfInformation" class="selfinformation">Bitte informieren Sie sich auch auf den
<a v-if="worship.eventPlace.website" :href="worship.eventPlace.website" target="_blank">Internetseiten dieser Gemeinde!</a><span
v-else>Internetseiten dieser Gemeinde!</span>
<div v-if="worship.selfInformation" class="selfinformation">
Bitte informieren Sie sich auch auf den
<a
v-if="worship.eventPlace && worship.eventPlace.website"
:href="worship.eventPlace.website"
target="_blank"
>Internetseiten dieser Gemeinde!</a>
<span v-else>Internetseiten dieser Gemeinde!</span>
</div>
</td>
</tr>

View File

@@ -33,6 +33,7 @@ export default {
zipcode: '',
city: '',
email: '',
expiryDate: null,
positions: []
},
positions: []

View File

@@ -8,29 +8,29 @@
</select>
</div>
<div class="toolbar">
<button @click="toggleHeading(3)">H3</button>
<button @click="toggleHeading(4)">H4</button>
<button @click="toggleHeading(5)">H5</button>
<button @click="toggleHeading(6)">H6</button>
<button @click="toggleBold()" width="24" height="24">
<button @click="toggleHeading(3)" :class="{ 'is-active': editor && editor.isActive('heading', { level: 3 }) }">H3</button>
<button @click="toggleHeading(4)" :class="{ 'is-active': editor && editor.isActive('heading', { level: 4 }) }">H4</button>
<button @click="toggleHeading(5)" :class="{ 'is-active': editor && editor.isActive('heading', { level: 5 }) }">H5</button>
<button @click="toggleHeading(6)" :class="{ 'is-active': editor && editor.isActive('heading', { level: 6 }) }">H6</button>
<button @click="toggleBold()" :class="{ 'is-active': editor && editor.isActive('bold') }" width="24" height="24">
<BoldIcon width="24" height="24" />
</button>
<button @click="toggleItalic()">
<button @click="toggleItalic()" :class="{ 'is-active': editor && editor.isActive('italic') }">
<ItalicIcon width="24" height="24" />
</button>
<button @click="toggleUnderline()">
<button @click="toggleUnderline()" :class="{ 'is-active': editor && editor.isActive('underline') }">
<UnderlineIcon width="24" height="24" />
</button>
<button @click="toggleStrike()">
<button @click="toggleStrike()" :class="{ 'is-active': editor && editor.isActive('strike') }">
<StrikethroughIcon width="24" height="24" />
</button>
<button @click="insertTable()">
<TableIcon width="24" height="24" />
</button>
<button @click="toggleBulletList()">
<button @click="toggleBulletList()" :class="{ 'is-active': editor && editor.isActive('bulletList') }">
<ListIcon width="24" height="24" />
</button>
<button @click="toggleOrderedList()">
<button @click="toggleOrderedList()" :class="{ 'is-active': editor && editor.isActive('orderedList') }">
<NumberedListLeftIcon width="24" height="24" />
</button>
<button @click="openAddImageDialog">
@@ -511,4 +511,15 @@ export default {
.align-top {
vertical-align: top;
}
.toolbar button.is-active,
.table-toolbar button.is-active {
background-color: #333;
color: white;
}
.toolbar button.is-active svg,
.table-toolbar button.is-active svg {
fill: white;
}
</style>

View File

@@ -3,12 +3,30 @@
<h2>Veranstaltungen</h2>
<button @click="createEvent">Neue Veranstaltung</button>
<EventForm v-if="showForm"
ref="eventForm"
:event="selectedEvent"
:institutions="institutions"
:eventPlaces="eventPlaces"
:contactPersons="contactPersons"
@saved="handleEventSaved"
@cancelled="handleEventCancelled" />
<div class="filter-section">
<input
v-model="searchQuery"
type="text"
placeholder="Suche nach Name, Typ, Beschreibung..."
class="search-input"
/>
<label class="checkbox-label">
<input
v-model="showPastEvents"
type="checkbox"
/>
Vergangene Events anzeigen
</label>
</div>
<table>
<thead>
<tr>
@@ -22,7 +40,7 @@
</tr>
</thead>
<tbody>
<tr v-for="event in events" :key="event.id">
<tr v-for="event in filteredEvents" :key="event.id">
<td>{{ event.name }}</td>
<td>{{ getEventTypeCaption(event.eventTypeId) }}</td>
<td>{{ event.date }}</td>
@@ -55,8 +73,52 @@ export default {
eventTypes: [],
selectedEvent: null,
showForm: false,
searchQuery: '',
showPastEvents: false,
};
},
computed: {
filteredEvents() {
let filtered = this.events;
// Filter vergangene Events aus
if (!this.showPastEvents) {
const today = new Date();
today.setHours(0, 0, 0, 0);
filtered = filtered.filter(event => {
// Events mit Wochentag (ohne festes Datum) immer anzeigen
if (event.dayOfWeek !== null && event.dayOfWeek !== undefined && !event.date) {
return true;
}
// Events mit Datum: nur zukünftige oder heutige anzeigen
if (event.date) {
const eventDate = new Date(event.date);
eventDate.setHours(0, 0, 0, 0);
return eventDate >= today;
}
// Events ohne Datum und ohne Wochentag anzeigen
return true;
});
}
// Suchfilter anwenden
if (this.searchQuery.trim()) {
const query = this.searchQuery.toLowerCase();
filtered = filtered.filter(event => {
const name = event.name ? event.name.toLowerCase() : '';
const description = event.description ? event.description.toLowerCase() : '';
const eventType = this.getEventTypeCaption(event.eventTypeId).toLowerCase();
return name.includes(query) ||
description.includes(query) ||
eventType.includes(query);
});
}
return filtered;
}
},
async created() {
await this.fetchData();
},
@@ -84,10 +146,24 @@ export default {
createEvent() {
this.selectedEvent = {};
this.showForm = true;
this.scrollToFormAndFocus();
},
editEvent(event) {
this.selectedEvent = { ...event };
this.showForm = true;
this.scrollToFormAndFocus();
},
scrollToFormAndFocus() {
// Wartet auf das Rendern des Formulars und scrollt dann nach oben
this.$nextTick(() => {
// Nach oben scrollen
window.scrollTo({ top: 0, behavior: 'smooth' });
// Das erste Feld fokussieren
if (this.$refs.eventForm) {
this.$refs.eventForm.focusFirstField();
}
});
},
async deleteEvent(id) {
try {
@@ -110,7 +186,7 @@ export default {
},
getWeekdayName(dayOfWeek) {
const weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
return weekdays[dayOfWeek - 1];
return weekdays[dayOfWeek];
},
}
};
@@ -122,6 +198,40 @@ export default {
margin: 0 auto;
}
.filter-section {
margin: 20px 0;
display: flex;
gap: 20px;
align-items: center;
flex-wrap: wrap;
}
.search-input {
flex: 1;
min-width: 250px;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.search-input:focus {
outline: none;
border-color: #4CAF50;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
user-select: none;
}
.checkbox-label input[type="checkbox"] {
cursor: pointer;
}
table {
width: 100%;
border-collapse: collapse;

File diff suppressed because it is too large Load Diff

View File

@@ -2,14 +2,14 @@
<div class="privacy-policy">
<h1>Datenschutzerklärung der Miriamgemeinde Frankfurt am Main</h1>
<p>
Die Miriamgemeinde Frankfurt am Main nimmt den Schutz Ihrer persönlichen Daten sehr ernst und behandelt Ihre personenbezogenen Daten vertraulich und entsprechend der kirchlichen Datenschutzgesetze sowie dieser Datenschutzerklärung. Die Sicherheit Ihrer Daten steht für uns an erster Stelle.
Die Miriamgemeinde Frankfurt am Main nimmt den Schutz Ihrer persönlichen Daten sehr ernst und behandelt Ihre personenbezogenen Daten vertraulich und entsprechend dem Datenschutzgesetz der Evangelischen Kirche in Deutschland (DSG-EKD) sowie dieser Datenschutzerklärung. Die Sicherheit Ihrer Daten steht für uns an erster Stelle.
</p>
<h2>Anbieter:</h2>
<p>
Miriamgemeinde Frankfurt am Main, Gemeindebüro Bonames<br />
Kirchhofsweg 5, 60437 Frankfurt, Tel.: 50 14 17, Fax: 50 93 0148,<br />
Email: <a href="mailto:Ev.Kirche-Bonames@t-online.de">Ev.Kirche-Bonames@t-online.de</a><br />
Inhaltlich Verantwortlicher gemäß § 6 MDStV: Torsten Schulz
Inhaltlich Verantwortlicher gemäß § 6 DDG: Torsten Schulz
</p>
<p>
Die Nutzung der Webseite der Miriamgemeinde Frankfurt am Main ist in der Regel ohne Angabe personenbezogener Daten möglich. Soweit auf unseren Seiten personenbezogene Daten (beispielsweise Name, Anschrift oder E-Mail-Adressen) erhoben werden, erfolgt dies, soweit möglich, stets auf freiwilliger Basis. Diese Daten werden ohne Ihre ausdrückliche Zustimmung nicht an Dritte weitergegeben. Die nachfolgende Erklärung gibt Ihnen einen Überblick darüber, wie dieser Schutz gewährleistet werden soll und welche Art von Daten zu welchem Zweck von Ihnen erhoben werden.
@@ -63,11 +63,23 @@
</p>
<h3>Ihr Recht auf Auskunft, Löschung, Sperrung</h3>
<p>
Sie haben als Nutzer das Recht, Auskunft darüber zu verlangen, welche Daten über Sie bei uns gespeichert sind und zu welchem Zweck diese Speicherung erfolgt. Darüber hinaus können Sie unrichtige Daten berichtigen oder solche Daten löschen lassen, deren Speicherung unzulässig oder nicht mehr erforderlich ist. Sie haben die Rechte auf Datenübertragbarkeit, Einschränkung der Verarbeitung und Widerspruch. Außerdem haben Sie das Recht, sich bei der Aufsichtsbehörde über die stattfindende Datenverarbeitung zu beschweren. Zuständige Aufsichtsbehörde ist Der Beauftragte für den Datenschutz der EKD Adresse siehe unten.
Sie haben als Nutzer nach dem Datenschutzgesetz der Evangelischen Kirche in Deutschland (DSG-EKD) folgende Rechte:
</p>
<ul>
<li><strong>Recht auf Auskunft:</strong> Sie haben das Recht, Auskunft darüber zu verlangen, welche Daten über Sie bei uns gespeichert sind und zu welchem Zweck diese Speicherung erfolgt.</li>
<li><strong>Recht auf Berichtigung:</strong> Sie können unrichtige oder unvollständige Daten berichtigen lassen.</li>
<li><strong>Recht auf Löschung:</strong> Sie können die Löschung Ihrer Daten verlangen, wenn diese nicht mehr erforderlich sind oder wenn die Verarbeitung rechtswidrig war.</li>
<li><strong>Recht auf Einschränkung der Verarbeitung:</strong> Sie können eine Einschränkung der Verarbeitung Ihrer Daten verlangen.</li>
<li><strong>Recht auf Widerspruch:</strong> Sie haben das Recht, der Verarbeitung Ihrer personenbezogenen Daten zu widersprechen, wenn diese auf Grund von berechtigtem Interesse erfolgt.</li>
<li><strong>Recht auf Datenübertragbarkeit:</strong> Sie können verlangen, dass die Daten, die Sie bereitgestellt haben, in einem strukturierten, gängigen und maschinenlesbaren Format ausgehändigt werden.</li>
<li><strong>Recht auf Widerruf:</strong> Sie können jederzeit die Einwilligung zur Datenverarbeitung widerrufen, ohne dass die Rechtmäßigkeit der aufgrund der Einwilligung bis zum Widerruf erfolgten Verarbeitung berührt wird.</li>
</ul>
<p>
Sie erhalten jederzeit ohne Angabe von Gründen kostenfrei Auskunft über Ihre bei uns gespeicherten Daten. Sie können jederzeit Ihre bei uns erhobenen Daten sperren, berichtigen oder löschen lassen. Auch können Sie jederzeit die uns erteilte Einwilligung zur Datenerhebung und Verwendung ohne Angaben von Gründen widerrufen. Wenden Sie sich hierzu bitte an die auf dieser Seite angegebene Kontaktadresse des Datenschutzbeauftragten. Wir stehen Ihnen jederzeit gern für weitergehende Fragen zu unserem Hinweisen zum Datenschutz und zur Verarbeitung Ihrer persönlichen Daten zur Verfügung.
</p>
<p>
Außerdem haben Sie das Recht, sich bei der Aufsichtsbehörde über die stattfindende Datenverarbeitung zu beschweren. Zuständige Aufsichtsbehörde ist der Beauftragte für den Datenschutz der EKD Adresse siehe unten.
</p>
<h3>Der Datenschutzbeauftragte für den Datenschutz der Evangelischen Kirchen in Deutschland</h3>
<p>
Die Aufsicht über die Einhaltung der Vorschriften zum Datenschutz obliegt im kirchlichen Bereich dem Beauftragten für den Datenschutz der EKD. Für den Bereich der Evangelischen Kirche in Hessen und Nassau (EKHN) ist zuständig die Außenstelle Dortmund für die Datenschutzregion Mitte-West
@@ -96,7 +108,7 @@
<p>
Die Nutzer werden gebeten, sich regelmäßig über den Inhalt der Datenschutzerklärung zu informieren.
</p>
<p>Stand: 24. Mai 2018</p>
<p>Stand: Januar 2025</p>
</div>
</template>

View File

@@ -24,7 +24,7 @@
</p>
</section>
<section>
<h3>Inhaltlich Verantwortlicher gemäß § 6 MDStV:</h3>
<h3>Inhaltlich Verantwortlicher gemäß § 6 DDG:</h3>
<p>Torsten Schulz</p>
</section>
<section>

View File

@@ -5,14 +5,19 @@ import store from './store';
import axios from './axios';
import './assets/css/editor.css';
// Menü-Daten über das konfigurierte Axios-Backend laden
async function fetchMenuData() {
const response = await fetch(process.env.VUE_APP_BACKEND_URL + '/menu-data');
return await response.json();
const response = await axios.get('/menu-data');
return response.data;
}
fetchMenuData().then(menuData => {
store.commit('setMenuData', menuData);
});
fetchMenuData()
.then(menuData => {
store.commit('setMenuData', menuData);
})
.catch(err => {
console.error('Fehler beim Laden der Menü-Daten:', err);
});
const app = createApp(AppComponent);
app.use(router);

View File

@@ -45,8 +45,13 @@ const router = createRouter({
routes: []
});
// Verhindert endlose Wiederholungen von fehlgeschlagenen Menü-Ladeversuchen
let menuDataInitialized = false;
router.beforeEach(async (to, from, next) => {
if (!store.state.menuData.length) {
if (!menuDataInitialized) {
menuDataInitialized = true;
await store.dispatch('loadMenuData');
const routes = generateRoutesFromMenu(store.state.menuData);
routes.forEach(route => router.addRoute(route));
@@ -65,8 +70,12 @@ router.beforeEach(async (to, from, next) => {
next({ ...to, replace: true });
} else {
// Sicherstellen, dass die Login-Route immer verfügbar ist
if (!router.hasRoute('auth-login')) {
addAuthLoginRoute();
}
if (to.matched.some(record => record.meta.requiresAuth) && !store.getters.isLoggedIn) {
next('/login');
next('/auth/login');
} else {
next();
}

View File

@@ -1,5 +1,5 @@
import { createStore } from 'vuex';
import axios from 'axios';
import axios from '../axios';
import router from '../router';
let user = [];
@@ -35,7 +35,6 @@ export default createStore({
localStorage.removeItem('isLoggedIn');
localStorage.removeItem('user');
localStorage.removeItem('token');
router.push('/auth/login');
},
setMenuData(state, menuData) {
state.menuData = menuData;
@@ -101,6 +100,10 @@ export default createStore({
console.error('Fehler beim Logout:', error);
} finally {
commit('logout');
// Navigation nach Logout mit replace, damit die Login-Seite direkt erreichbar ist
if (router.currentRoute.value.path !== '/auth/login') {
router.replace('/auth/login');
}
}
}
},

View File

@@ -1,71 +0,0 @@
class ErrorHandler {
/**
* Error in HTTP Response umwandeln
*/
handleError(error, res) {
console.error('Error:', error);
// Validation Errors
if (error.message.startsWith('VALIDATION_ERROR:')) {
const message = error.message.replace('VALIDATION_ERROR: ', '');
return res.status(400).json({
success: false,
message: message,
type: 'VALIDATION_ERROR'
});
}
// Business Logic Errors
switch (error.message) {
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
*/
successResponse(res, data, message = 'Erfolgreich', statusCode = 200) {
return res.status(statusCode).json({
success: true,
message: message,
data: data
});
}
/**
* Async Error Wrapper für Controller
*/
asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
}
module.exports = new ErrorHandler();

View File

@@ -1,109 +0,0 @@
class UserValidator {
/**
* User-Erstellungsdaten validieren
*/
validateCreateUser(userData) {
const errors = [];
if (!userData.name || userData.name.trim().length < 2) {
errors.push('Name muss mindestens 2 Zeichen lang sein');
}
if (!userData.email || !this.isValidEmail(userData.email)) {
errors.push('Gültige E-Mail-Adresse ist erforderlich');
}
if (!userData.password || userData.password.length < 6) {
errors.push('Passwort muss mindestens 6 Zeichen lang sein');
}
if (errors.length > 0) {
throw new Error(`VALIDATION_ERROR: ${errors.join(', ')}`);
}
return true;
}
/**
* User-Update-Daten validieren
*/
validateUpdateUser(updateData) {
const errors = [];
if (updateData.name !== undefined && (updateData.name.trim().length < 2)) {
errors.push('Name muss mindestens 2 Zeichen lang sein');
}
if (updateData.email !== undefined && !this.isValidEmail(updateData.email)) {
errors.push('Gültige E-Mail-Adresse ist erforderlich');
}
if (updateData.active !== undefined && typeof updateData.active !== 'boolean') {
errors.push('Active muss ein Boolean-Wert sein');
}
// Warnung für sensible Felder
if (updateData.password !== undefined) {
throw new Error('VALIDATION_ERROR: Passwort kann nicht über diese Route geändert werden');
}
if (updateData.id !== undefined) {
throw new Error('VALIDATION_ERROR: ID kann nicht geändert werden');
}
if (updateData.created_at !== undefined) {
throw new Error('VALIDATION_ERROR: Erstellungsdatum kann nicht geändert werden');
}
if (errors.length > 0) {
throw new Error(`VALIDATION_ERROR: ${errors.join(', ')}`);
}
return true;
}
/**
* Passwort-Änderung validieren
*/
validatePasswordChange(currentPassword, newPassword) {
const errors = [];
if (!currentPassword) {
errors.push('Aktuelles Passwort ist erforderlich');
}
if (!newPassword || newPassword.length < 6) {
errors.push('Neues Passwort muss mindestens 6 Zeichen lang sein');
}
if (currentPassword === newPassword) {
errors.push('Neues Passwort muss sich vom aktuellen unterscheiden');
}
if (errors.length > 0) {
throw new Error(`VALIDATION_ERROR: ${errors.join(', ')}`);
}
return true;
}
/**
* E-Mail-Format validieren
*/
isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
/**
* ID validieren
*/
validateId(id) {
if (!id || isNaN(parseInt(id))) {
throw new Error('VALIDATION_ERROR: Ungültige ID');
}
return true;
}
}
module.exports = new UserValidator();

View File

@@ -2,12 +2,31 @@ const { defineConfig } = require('@vue/cli-service');
const webpack = require('webpack');
module.exports = defineConfig({
transpileDependencies: true,
transpileDependencies: [],
devServer: {
host: 'localhost',
port: 8080
// Port kann über VUE_APP_FRONTEND_PORT oder FRONTEND_PORT in .env gesetzt werden
port: parseInt(process.env.VUE_APP_FRONTEND_PORT || process.env.FRONTEND_PORT || '8080', 10),
// Proxy für API-Requests zum Backend-Server
// Backend sollte auf einem anderen Port laufen (z.B. 3010)
proxy: {
'/api': {
target: process.env.VUE_APP_BACKEND_PROXY || 'http://torstens:3010',
changeOrigin: true,
secure: false,
logLevel: 'debug',
// Erhöhe Header-Limits für Proxy
headers: {
'Connection': 'keep-alive'
},
// Erhöhe Timeout für große Requests
timeout: 60000
}
}
},
configureWebpack: {
output: { clean: true },
cache: false,
resolve: {
fallback: {
"path": require.resolve("path-browserify"),
@@ -23,4 +42,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) {}
});
}
});