Compare commits
1 Commits
main
...
77e3dbde82
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77e3dbde82 |
7
.env
Normal file
7
.env
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
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
19
.env.example
@@ -1,19 +0,0 @@
|
|||||||
# 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
2
.gitignore
vendored
@@ -27,5 +27,3 @@ server.key
|
|||||||
server.cert
|
server.cert
|
||||||
|
|
||||||
public/images/uploads/1ba24ea7-f52c-4179-896f-1909269cab58.jpg
|
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
|
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ module.exports = {
|
|||||||
presets: [
|
presets: [
|
||||||
'@vue/cli-plugin-babel/preset'
|
'@vue/cli-plugin-babel/preset'
|
||||||
]
|
]
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
{
|
{
|
||||||
"development": {
|
"development": {
|
||||||
"username": "miriamgemeinde",
|
"username": "miriam_user",
|
||||||
"password": "hitomisan",
|
"password": "qTCTTWwpEwy3vPDU",
|
||||||
"database": "miriamgemeinde",
|
"database": "miriamgemeinde",
|
||||||
"host": "localhost",
|
"host": "tsschulz.de",
|
||||||
"dialect": "mysql"
|
"dialect": "mysql"
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"username": "miriam_user",
|
"username": "miriam_user",
|
||||||
"password": "hitomisan",
|
"password": "qTCTTWwpEwy3vPDU",
|
||||||
"database": "miriamgemeinde",
|
"database": "miriamgemeinde",
|
||||||
"host": "tsschulz.de",
|
"host": "tsschulz.de",
|
||||||
"dialect": "mysql"
|
"dialect": "mysql"
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"username": "miriam_user",
|
"username": "miriam_user",
|
||||||
"password": "hitomisan",
|
"password": "qTCTTWwpEwy3vPDU",
|
||||||
"database": "miriamgemeinde",
|
"database": "miriamgemeinde",
|
||||||
"host": "tsschulz.de",
|
"host": "tsschulz.de",
|
||||||
"dialect": "mysql"
|
"dialect": "mysql"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const { Sequelize } = require('sequelize');
|
const { Sequelize } = require('sequelize');
|
||||||
|
|
||||||
const sequelize = new Sequelize('miriamgemeinde', 'miriamgemeinde', 'hitomisan', {
|
const sequelize = new Sequelize('miriamgemeinde', 'miriam_user', 'qTCTTWwpEwy3vPDU', {
|
||||||
host: 'localhost',
|
host: 'tsschulz.de',
|
||||||
dialect: 'mysql',
|
dialect: 'mysql',
|
||||||
retry: {
|
retry: {
|
||||||
match: [
|
match: [
|
||||||
@@ -26,7 +26,7 @@ const sequelize = new Sequelize('miriamgemeinde', 'miriamgemeinde', 'hitomisan',
|
|||||||
async function connectWithRetry() {
|
async function connectWithRetry() {
|
||||||
try {
|
try {
|
||||||
await sequelize.authenticate();
|
await sequelize.authenticate();
|
||||||
console.log(`Connection has been established successfully. Database server: ${sequelize.config.host}`);
|
console.log('Connection has been established successfully.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Unable to connect to the database:', error);
|
console.error('Unable to connect to the database:', error);
|
||||||
setTimeout(connectWithRetry, 5000);
|
setTimeout(connectWithRetry, 5000);
|
||||||
|
|||||||
@@ -1,205 +1,29 @@
|
|||||||
const bcrypt = require('bcryptjs');
|
const AuthService = require('../services/AuthService');
|
||||||
const { User, PasswordResetToken } = require('../models');
|
const ErrorHandler = require('../utils/ErrorHandler');
|
||||||
const jwt = require('jsonwebtoken');
|
|
||||||
const { addTokenToBlacklist } = require('../utils/blacklist');
|
|
||||||
const { transporter, getPasswordResetEmailTemplate } = require('../config/email');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
|
|
||||||
function delay(ms) {
|
exports.register = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
const result = await AuthService.register(req.body);
|
||||||
}
|
ErrorHandler.successResponse(res, result, 'Benutzer erfolgreich registriert', 201);
|
||||||
|
|
||||||
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 });
|
|
||||||
|
|
||||||
const maxAttempts = 3;
|
|
||||||
let attempt = 0;
|
|
||||||
let createdUser = null;
|
|
||||||
let lastError = null;
|
|
||||||
|
|
||||||
while (attempt < maxAttempts && !createdUser) {
|
|
||||||
try {
|
|
||||||
createdUser = await User.create({ name, email, password: hashedPassword, active: 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
exports.login = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
const resetUrl = `${process.env.FRONTEND_URL || 'http://localhost:8080'}/reset-password?token=${token}`;
|
const result = await AuthService.login(req.body);
|
||||||
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
// 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) {
|
exports.forgotPassword = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
return res.status(400).json({ message: 'Ungültiger oder abgelaufener Token' });
|
const result = await AuthService.forgotPassword(req.body.email);
|
||||||
}
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
|
});
|
||||||
|
|
||||||
// Passwort hashen und aktualisieren
|
exports.resetPassword = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
const result = await AuthService.resetPassword(req.body.token, req.body.password);
|
||||||
await User.update(
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
{ password: hashedPassword },
|
});
|
||||||
{ where: { id: resetToken.userId } }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Token als verwendet markieren
|
exports.logout = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
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 authHeader = req.header('Authorization');
|
||||||
if (!authHeader) {
|
const token = authHeader ? authHeader.replace('Bearer ', '') : null;
|
||||||
return res.status(400).json({ message: 'Kein Token bereitgestellt' });
|
const result = await AuthService.logout(token);
|
||||||
}
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
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' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -3,16 +3,7 @@ const { Op } = require('sequelize');
|
|||||||
|
|
||||||
const getAllContactPersons = async (req, res) => {
|
const getAllContactPersons = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const today = new Date();
|
|
||||||
today.setHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
const contactPersons = await ContactPerson.findAll({
|
const contactPersons = await ContactPerson.findAll({
|
||||||
where: {
|
|
||||||
[Op.or]: [
|
|
||||||
{ expiryDate: null },
|
|
||||||
{ expiryDate: { [Op.gte]: today } }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: Position,
|
model: Position,
|
||||||
@@ -88,14 +79,6 @@ const filterContactPersons = async (req, res) => {
|
|||||||
const where = {};
|
const where = {};
|
||||||
const having = [];
|
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') {
|
if (config.selection.id && config.selection.id === 'all') {
|
||||||
// No additional filter needed for "all"
|
// No additional filter needed for "all"
|
||||||
} else if (config.selection.id) {
|
} else if (config.selection.id) {
|
||||||
|
|||||||
@@ -1,188 +1,32 @@
|
|||||||
const { Event, Institution, EventPlace, ContactPerson, EventType } = require('../models');
|
const EventService = require('../services/EventService');
|
||||||
const { Op } = require('sequelize');
|
const ErrorHandler = require('../utils/ErrorHandler');
|
||||||
const moment = require('moment'); // Import von Moment.js
|
|
||||||
|
|
||||||
const getAllEvents = async (req, res) => {
|
exports.getAllEvents = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
const events = await EventService.getAllEvents();
|
||||||
const events = await Event.findAll({
|
ErrorHandler.successResponse(res, events, 'Events erfolgreich abgerufen');
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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']
|
|
||||||
];
|
|
||||||
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!request.id && !request.places && !request.types) {
|
|
||||||
return res.json({ events: [], places: [], types: [], contactPersons: [] });
|
|
||||||
}
|
|
||||||
|
|
||||||
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 });
|
exports.getEventById = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
} catch (error) {
|
const event = await EventService.getEventById(req.params.id);
|
||||||
res.status(500).json({ error: 'Failed to filter events' });
|
ErrorHandler.successResponse(res, event, 'Event erfolgreich abgerufen');
|
||||||
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 = {
|
exports.filterEvents = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
getAllEvents,
|
const result = await EventService.filterEvents(req.body);
|
||||||
createEvent,
|
ErrorHandler.successResponse(res, result, 'Events erfolgreich gefiltert');
|
||||||
updateEvent,
|
});
|
||||||
deleteEvent,
|
|
||||||
filterEvents
|
exports.createEvent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
};
|
const event = await EventService.createEvent(req.body);
|
||||||
|
ErrorHandler.successResponse(res, event, 'Event erfolgreich erstellt', 201);
|
||||||
|
});
|
||||||
|
|
||||||
|
exports.updateEvent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
|
const event = await EventService.updateEvent(req.params.id, req.body);
|
||||||
|
ErrorHandler.successResponse(res, event, 'Event erfolgreich aktualisiert');
|
||||||
|
});
|
||||||
|
|
||||||
|
exports.deleteEvent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
|
const result = await EventService.deleteEvent(req.params.id);
|
||||||
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
|
});
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
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 DayName"
|
|
||||||
const regex = /(\d{2}\.\d{2}\.\d{4})\s*(?: |\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(/ /g, ' ') // Ersetze
|
|
||||||
.replace(/ä/g, 'ä')
|
|
||||||
.replace(/ö/g, 'ö')
|
|
||||||
.replace(/ü/g, 'ü')
|
|
||||||
.replace(/Ä/g, 'Ä')
|
|
||||||
.replace(/Ö/g, 'Ö')
|
|
||||||
.replace(/Ü/g, 'Ü')
|
|
||||||
.replace(/ß/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
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,30 +1,12 @@
|
|||||||
const { MenuItem } = require('../models');
|
const MenuDataService = require('../services/MenuDataService');
|
||||||
const fetchMenuData = require('../utils/fetchMenuData');
|
const ErrorHandler = require('../utils/ErrorHandler');
|
||||||
|
|
||||||
exports.getMenuData = async (req, res) => {
|
exports.getMenuData = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
const menuData = await MenuDataService.getMenuData();
|
||||||
const menuData = await fetchMenuData();
|
ErrorHandler.successResponse(res, menuData, 'Menü-Daten erfolgreich abgerufen');
|
||||||
res.json(menuData);
|
});
|
||||||
} catch (error) {
|
|
||||||
res.status(500).send('Error fetching menu data');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.saveMenuData = async (req, res) => {
|
exports.saveMenuData = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
const result = await MenuDataService.saveMenuData(req.body);
|
||||||
const menuData = req.body;
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,48 +1,27 @@
|
|||||||
// controllers/pageController.js
|
const PageService = require('../services/PageService');
|
||||||
const { Page } = require('../models');
|
const ErrorHandler = require('../utils/ErrorHandler');
|
||||||
|
|
||||||
exports.getMenuData = async (req, res) => {
|
exports.getMenuData = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
const pages = await PageService.getAllPages();
|
||||||
const pages = await Page.findAll({
|
ErrorHandler.successResponse(res, pages, 'Seiten erfolgreich abgerufen');
|
||||||
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 = async (req, res) => {
|
exports.getPageContent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
const result = await PageService.getPageContent(req.query.link);
|
||||||
const page = await Page.findOne({
|
ErrorHandler.successResponse(res, result, 'Seiteninhalt erfolgreich abgerufen');
|
||||||
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 = async (req, res) => {
|
exports.savePageContent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
const result = await PageService.savePageContent(req.body);
|
||||||
const { link, name, content } = req.body;
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
let page = await Page.findOne({ where: { link } });
|
});
|
||||||
if (page) {
|
|
||||||
page.content = content;
|
exports.getPageById = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
page.name = name;
|
const page = await PageService.getPageById(req.params.id);
|
||||||
} else {
|
ErrorHandler.successResponse(res, page, 'Seite erfolgreich abgerufen');
|
||||||
page = await Page.create({ link, name, content });
|
});
|
||||||
}
|
|
||||||
await page.save();
|
exports.deletePage = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
res.json({ message: 'Seiteninhalt gespeichert', page });
|
const result = await PageService.deletePage(req.params.id);
|
||||||
} catch (error) {
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
console.error('Fehler beim Speichern des Seiteninhalts:', error);
|
});
|
||||||
res.status(500).json({ message: 'Fehler beim Speichern des Seiteninhalts' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,104 +1,42 @@
|
|||||||
const { User } = require('../models');
|
const UserService = require('../services/UserService');
|
||||||
|
const UserValidator = require('../validators/UserValidator');
|
||||||
|
const ErrorHandler = require('../utils/ErrorHandler');
|
||||||
|
|
||||||
exports.getAllUsers = async (req, res) => {
|
exports.getAllUsers = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
const users = await UserService.getAllUsers();
|
||||||
const users = await User.findAll({
|
ErrorHandler.successResponse(res, users, 'Benutzer erfolgreich abgerufen');
|
||||||
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 = async (req, res) => {
|
exports.getUserById = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
UserValidator.validateId(req.params.id);
|
||||||
const user = await User.findByPk(req.params.id, {
|
const user = await UserService.getUserById(req.params.id);
|
||||||
attributes: ['id', 'name', 'email', 'active', 'created_at'] // Passwort ausschließen
|
ErrorHandler.successResponse(res, user, 'Benutzer erfolgreich abgerufen');
|
||||||
});
|
});
|
||||||
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 = async (req, res) => {
|
exports.createUser = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
UserValidator.validateCreateUser(req.body);
|
||||||
const user = await User.create(req.body);
|
const user = await UserService.createUser(req.body);
|
||||||
|
ErrorHandler.successResponse(res, user, 'Benutzer erfolgreich erstellt', 201);
|
||||||
|
});
|
||||||
|
|
||||||
// Sichere User-Daten zurückgeben (ohne Passwort)
|
exports.updateUser = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
const safeUser = {
|
UserValidator.validateId(req.params.id);
|
||||||
id: user.id,
|
UserValidator.validateUpdateUser(req.body);
|
||||||
name: user.name,
|
const user = await UserService.updateUser(req.params.id, req.body);
|
||||||
email: user.email,
|
ErrorHandler.successResponse(res, user, 'Benutzer erfolgreich aktualisiert');
|
||||||
active: user.active,
|
});
|
||||||
created_at: user.created_at
|
|
||||||
};
|
|
||||||
|
|
||||||
res.status(201).json(safeUser);
|
exports.deleteUser = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
} catch (error) {
|
UserValidator.validateId(req.params.id);
|
||||||
console.error('Error creating user:', error);
|
await UserService.deleteUser(req.params.id);
|
||||||
res.status(500).json({ message: 'Error creating user' });
|
ErrorHandler.successResponse(res, null, 'Benutzer erfolgreich gelöscht');
|
||||||
}
|
});
|
||||||
};
|
|
||||||
|
|
||||||
exports.updateUser = async (req, res) => {
|
// Neue Route für Passwort-Änderung
|
||||||
try {
|
exports.changePassword = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
const user = await User.findByPk(req.params.id);
|
const { currentPassword, newPassword } = req.body;
|
||||||
if (user) {
|
UserValidator.validateId(req.params.id);
|
||||||
// Erstelle eine Kopie der Request-Daten ohne sensible Felder
|
UserValidator.validatePasswordChange(currentPassword, newPassword);
|
||||||
const updateData = { ...req.body };
|
await UserService.changePassword(req.params.id, currentPassword, newPassword);
|
||||||
|
ErrorHandler.successResponse(res, null, 'Passwort erfolgreich geändert');
|
||||||
// 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
31
deploy.sh
@@ -1,31 +0,0 @@
|
|||||||
#!/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."
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
'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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
'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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
'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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -25,10 +25,6 @@ module.exports = (sequelize) => {
|
|||||||
email: {
|
email: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true
|
allowNull: true
|
||||||
},
|
|
||||||
expiryDate: {
|
|
||||||
type: DataTypes.DATEONLY,
|
|
||||||
allowNull: true
|
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'contact_persons',
|
tableName: 'contact_persons',
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -61,17 +61,6 @@ module.exports = (sequelize) => {
|
|||||||
allowNull: true,
|
allowNull: true,
|
||||||
field: 'sacristan_service'
|
field: 'sacristan_service'
|
||||||
},
|
},
|
||||||
organPlaying: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true,
|
|
||||||
field: 'organ_playing'
|
|
||||||
},
|
|
||||||
approved: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
defaultValue: false,
|
|
||||||
allowNull: false,
|
|
||||||
field: 'approved'
|
|
||||||
},
|
|
||||||
}, {
|
}, {
|
||||||
tableName: 'worships',
|
tableName: 'worships',
|
||||||
timestamps: true
|
timestamps: true
|
||||||
|
|||||||
27353
package-lock.json
generated
27353
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@
|
|||||||
"@tiptap/extension-underline": "^2.4.0",
|
"@tiptap/extension-underline": "^2.4.0",
|
||||||
"@tiptap/starter-kit": "^2.4.0",
|
"@tiptap/starter-kit": "^2.4.0",
|
||||||
"@tiptap/vue-3": "^2.4.0",
|
"@tiptap/vue-3": "^2.4.0",
|
||||||
"@vue/cli": "^4.2.2",
|
"@vue/cli": "^5.0.8",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
@@ -34,12 +34,10 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"docx": "^9.5.1",
|
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mammoth": "^1.11.0",
|
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql2": "^3.10.1",
|
"mysql2": "^3.10.1",
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
||||||
"extends": [
|
|
||||||
"config:recommended"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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;
|
|
||||||
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { getAllUsers, createUser, updateUser, deleteUser, getUserById } = require('../controllers/userController');
|
const { getAllUsers, createUser, updateUser, deleteUser, getUserById, changePassword } = require('../controllers/userController');
|
||||||
const authMiddleware = require('../middleware/authMiddleware');
|
const authMiddleware = require('../middleware/authMiddleware');
|
||||||
|
|
||||||
router.get('/', authMiddleware, getAllUsers);
|
router.get('/', authMiddleware, getAllUsers);
|
||||||
router.get('/:id', authMiddleware, getUserById);
|
router.get('/:id', authMiddleware, getUserById);
|
||||||
router.post('/', authMiddleware, createUser);
|
router.post('/', authMiddleware, createUser);
|
||||||
router.put('/:id', authMiddleware, updateUser);
|
router.put('/:id', authMiddleware, updateUser);
|
||||||
|
router.put('/:id/change-password', authMiddleware, changePassword);
|
||||||
router.delete('/:id', authMiddleware, deleteUser);
|
router.delete('/:id', authMiddleware, deleteUser);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { getAllWorships, createWorship, updateWorship, deleteWorship, getFilteredWorships, getWorshipOptions, importWorships, uploadImportFile, exportWorships, saveImportedWorships } = require('../controllers/worshipController');
|
const { getAllWorships, createWorship, updateWorship, deleteWorship, getFilteredWorships } = require('../controllers/worshipController');
|
||||||
const authMiddleware = require('../middleware/authMiddleware');
|
const authMiddleware = require('../middleware/authMiddleware');
|
||||||
|
|
||||||
router.get('/', getAllWorships);
|
router.get('/', getAllWorships);
|
||||||
router.get('/options', getWorshipOptions);
|
|
||||||
router.post('/', authMiddleware, createWorship);
|
router.post('/', authMiddleware, createWorship);
|
||||||
router.post('/import', authMiddleware, uploadImportFile, importWorships);
|
|
||||||
router.post('/import/save', authMiddleware, saveImportedWorships);
|
|
||||||
router.put('/:id', authMiddleware, updateWorship);
|
router.put('/:id', authMiddleware, updateWorship);
|
||||||
router.delete('/:id', authMiddleware, deleteWorship);
|
router.delete('/:id', authMiddleware, deleteWorship);
|
||||||
router.get('/filtered', getFilteredWorships);
|
router.get('/filtered', getFilteredWorships);
|
||||||
router.get('/export', authMiddleware, exportWorships);
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
85
server.js
85
server.js
@@ -2,14 +2,7 @@ const express = require('express');
|
|||||||
const bodyParser = require('body-parser');
|
const bodyParser = require('body-parser');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
const http = require('http');
|
|
||||||
const fs = require('fs');
|
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 sequelize = require('./config/database');
|
||||||
const authRouter = require('./routes/auth');
|
const authRouter = require('./routes/auth');
|
||||||
const eventTypesRouter = require('./routes/eventtypes');
|
const eventTypesRouter = require('./routes/eventtypes');
|
||||||
@@ -24,79 +17,12 @@ const pageRouter = require('./routes/pages');
|
|||||||
const userRouter = require('./routes/users');
|
const userRouter = require('./routes/users');
|
||||||
const imageRouter = require('./routes/image');
|
const imageRouter = require('./routes/image');
|
||||||
const filesRouter = require('./routes/files');
|
const filesRouter = require('./routes/files');
|
||||||
const liturgicalDaysRouter = require('./routes/liturgicalDays');
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = parseInt(process.env.PORT, 10) || 3000;
|
const PORT = 3002;
|
||||||
|
|
||||||
// CORS mit Whitelist und tolerantem Fallback für fehlende Origin-Header
|
app.use(cors());
|
||||||
const allowedOrigins = (process.env.ALLOWED_ORIGINS || '')
|
app.use(bodyParser.json());
|
||||||
.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/auth', authRouter);
|
||||||
app.use('/api/event-types', eventTypesRouter);
|
app.use('/api/event-types', eventTypesRouter);
|
||||||
@@ -111,7 +37,6 @@ app.use('/api/page-content', pageRouter);
|
|||||||
app.use('/api/users', userRouter);
|
app.use('/api/users', userRouter);
|
||||||
app.use('/api/image', imageRouter);
|
app.use('/api/image', imageRouter);
|
||||||
app.use('/api/files', filesRouter);
|
app.use('/api/files', filesRouter);
|
||||||
app.use('/api/liturgical-days', liturgicalDaysRouter);
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
key: fs.readFileSync('server.key'),
|
key: fs.readFileSync('server.key'),
|
||||||
@@ -123,7 +48,7 @@ sequelize.sync().then(() => {
|
|||||||
/* https.createServer(options, app).listen(PORT, () => {
|
/* https.createServer(options, app).listen(PORT, () => {
|
||||||
console.log(`Server läuft auf Port ${PORT}`);
|
console.log(`Server läuft auf Port ${PORT}`);
|
||||||
});*/
|
});*/
|
||||||
app.listen(PORT, '0.0.0.0', () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Server läuft auf Port ${PORT} (IPv4 und IPv6)`);
|
console.log(`Server läuft auf Port ${PORT}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
186
services/AuthService.js
Normal file
186
services/AuthService.js
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
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();
|
||||||
276
services/EventService.js
Normal file
276
services/EventService.js
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
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();
|
||||||
101
services/MenuDataService.js
Normal file
101
services/MenuDataService.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
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();
|
||||||
132
services/PageService.js
Normal file
132
services/PageService.js
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
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();
|
||||||
140
services/UserService.js
Normal file
140
services/UserService.js
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
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();
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
-- Ablaufdatum zu Kontaktpersonen hinzufügen
|
|
||||||
ALTER TABLE `contact_persons`
|
|
||||||
ADD COLUMN `expiryDate` DATE NULL AFTER `email`;
|
|
||||||
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
-- 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`);
|
|
||||||
|
|
||||||
14
src/axios.js
14
src/axios.js
@@ -2,11 +2,8 @@ import axios from 'axios';
|
|||||||
import store from './store';
|
import store from './store';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
|
|
||||||
// Einheitliche Basis-URL:
|
axios.defaults.baseURL = process.env.VUE_APP_BACKEND_URL;
|
||||||
// - immer relativ zur aktuellen Origin
|
console.log(process.env.VUE_APP_BACKEND_URL);
|
||||||
// - kein absoluter http/https-Host → verhindert Mixed-Content-Probleme
|
|
||||||
axios.defaults.baseURL = '/api';
|
|
||||||
console.log('Axios baseURL:', axios.defaults.baseURL);
|
|
||||||
|
|
||||||
axios.interceptors.request.use(
|
axios.interceptors.request.use(
|
||||||
config => {
|
config => {
|
||||||
@@ -27,11 +24,8 @@ axios.interceptors.response.use(
|
|||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
if (error.response && error.response.status === 401) {
|
if (error.response && error.response.status === 401) {
|
||||||
store.dispatch('logout').then(() => {
|
store.dispatch('logout');
|
||||||
if (router.currentRoute.value.path !== '/auth/login') {
|
router.push('/auth/login');
|
||||||
router.replace('/auth/login');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,53 +36,27 @@ export default {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.dialog-overlay {
|
.dialog-overlay {
|
||||||
position: fixed;
|
top: calc(50% - 25em);
|
||||||
top: 0;
|
left: 5%;
|
||||||
left: 0;
|
width: 90%;
|
||||||
width: 100%;
|
height: 50em;
|
||||||
height: 100%;
|
background: rgba(0, 0, 0, .5);
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 1000;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog {
|
.dialog {
|
||||||
background: white;
|
background: white;
|
||||||
padding: 30px;
|
padding: 20px;
|
||||||
border-radius: 8px;
|
border-radius: 5px;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
width: 90%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog h2 {
|
button {
|
||||||
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;
|
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>
|
</style>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="left-links">
|
<div class="left-links">
|
||||||
<router-link class="login-link" to="/auth/login" v-if="!isLoggedIn">Login</router-link>
|
<router-link class="login-link" to="/auth/login" v-if="!isLoggedIn">Login</router-link>
|
||||||
<a v-if="isLoggedIn" @click="handleLogout" class="logout-link">Logout</a>
|
<a v-if="isLoggedIn" @click="logout" class="logout-link">Logout</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-links">
|
<div class="right-links">
|
||||||
<router-link to="/terms">Impressum</router-link>
|
<router-link to="/terms">Impressum</router-link>
|
||||||
|
|||||||
@@ -20,9 +20,6 @@
|
|||||||
<label for="email">Email:</label>
|
<label for="email">Email:</label>
|
||||||
<input type="email" id="email" v-model="localContactPerson.email">
|
<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>
|
<label for="positions">Positionen:</label>
|
||||||
<multiselect
|
<multiselect
|
||||||
v-model="selectedPositions"
|
v-model="selectedPositions"
|
||||||
@@ -60,7 +57,6 @@ export default {
|
|||||||
zipcode: '',
|
zipcode: '',
|
||||||
city: '',
|
city: '',
|
||||||
email: '',
|
email: '',
|
||||||
expiryDate: null,
|
|
||||||
positions: []
|
positions: []
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -115,7 +111,6 @@ export default {
|
|||||||
zipcode: '',
|
zipcode: '',
|
||||||
city: '',
|
city: '',
|
||||||
email: '',
|
email: '',
|
||||||
expiryDate: null,
|
|
||||||
positions: []
|
positions: []
|
||||||
};
|
};
|
||||||
this.selectedPositions = [];
|
this.selectedPositions = [];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="config && config.style === 'box' && contacts && contacts.length && contacts.length > 0">
|
<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">
|
<div v-for="contact in contacts" :key="contact.id" class="contact-box bottom-margin">
|
||||||
<p>{{ contact.name }} <span v-if="contact.expiryDate" class="expiry-date">(bis {{ formatDate(contact.expiryDate) }})</span></p>
|
<p>{{ contact.name }}</p>
|
||||||
<p v-if="displayOptions.includes('phone')">Telefon: {{ contact.phone }}</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('street')">Straße: {{ contact.street }}</p>
|
||||||
<p v-if="displayOptions.includes('zipcode')">Postleitzahl: {{ contact.zipcode }}</p>
|
<p v-if="displayOptions.includes('zipcode')">Postleitzahl: {{ contact.zipcode }}</p>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<span v-else-if="config.style === 'float' && contacts && contacts.length && contacts.length > 0">
|
<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">
|
<span v-for="contact in contacts" :key="contact.id" class="bottom-margin">
|
||||||
{{ contact.name }}<span v-if="contact.expiryDate" class="expiry-date"> (bis {{ formatDate(contact.expiryDate) }})</span>
|
{{ contact.name }}
|
||||||
<span v-if="displayOptions.includes('phone')">, Telefon: {{ contact.phone }}</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('street')">, Straße: {{ contact.street }}</span>
|
||||||
<span v-if="displayOptions.includes('zipcode')">, Postleitzahl: {{ contact.zipcode }}</span>
|
<span v-if="displayOptions.includes('zipcode')">, Postleitzahl: {{ contact.zipcode }}</span>
|
||||||
@@ -58,17 +58,6 @@ export default {
|
|||||||
this.loading = false;
|
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>
|
</script>
|
||||||
|
|
||||||
@@ -79,9 +68,4 @@ export default {
|
|||||||
.bottom-margin {
|
.bottom-margin {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
.expiry-date {
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: #666;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -312,15 +312,6 @@ export default {
|
|||||||
this.assignedImage = null;
|
this.assignedImage = null;
|
||||||
this.imageFilename = '';
|
this.imageFilename = '';
|
||||||
},
|
},
|
||||||
focusFirstField() {
|
|
||||||
// Fokussiert das erste Eingabefeld (Name)
|
|
||||||
this.$nextTick(() => {
|
|
||||||
const nameInput = document.getElementById('name');
|
|
||||||
if (nameInput) {
|
|
||||||
nameInput.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
formatTime(event.endTime) }}</span> Uhr</div>
|
formatTime(event.endTime) }}</span> Uhr</div>
|
||||||
<div v-if="shouldDisplay('place')">{{ event.eventPlace?.name }}</div>
|
<div v-if="shouldDisplay('place')">{{ event.eventPlace?.name }}</div>
|
||||||
<div v-if="shouldDisplay('description')" class="description">{{ event.description }}</div>
|
<div v-if="shouldDisplay('description')" class="description">{{ event.description }}</div>
|
||||||
<div v-if="shouldDisplay('contactPerson')">{{event.contactPersons.map(cp => formatContactPerson(cp)).join(', ')}}
|
<div v-if="shouldDisplay('contactPerson')">{{event.contactPersons.map(cp => cp.name).join(', ')}}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="shouldDisplay('institution')">{{ event.institution?.name }}</div>
|
<div v-if="shouldDisplay('institution')">{{ event.institution?.name }}</div>
|
||||||
<div v-if="shouldDisplay('type')">{{ event.eventType?.caption }}</div>
|
<div v-if="shouldDisplay('type')">{{ event.eventType?.caption }}</div>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
formatTime(events[0].endTime) }}</span> Uhr</div>
|
formatTime(events[0].endTime) }}</span> Uhr</div>
|
||||||
<div v-if="shouldDisplay('place')">{{ events[0].eventPlace?.name }}</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('description')" class="description">{{ events[0].description }}</div>
|
||||||
<div v-if="shouldDisplay('contactPerson')">{{events[0].contactPersons.map(cp => formatContactPerson(cp)).join(', ')}}
|
<div v-if="shouldDisplay('contactPerson')">{{events[0].contactPersons.map(cp => cp.name).join(', ')}}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="shouldDisplay('institution')">{{ events[0].institution?.name }}</div>
|
<div v-if="shouldDisplay('institution')">{{ events[0].institution?.name }}</div>
|
||||||
<div v-if="shouldDisplay('type')">{{ events[0].eventType?.caption }}</div>
|
<div v-if="shouldDisplay('type')">{{ events[0].eventType?.caption }}</div>
|
||||||
@@ -102,18 +102,6 @@ export default {
|
|||||||
const path = '/images/uploads/' + response.data.filename;
|
const path = '/images/uploads/' + response.data.filename;
|
||||||
console.log(path);
|
console.log(path);
|
||||||
return 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})`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,35 +11,21 @@
|
|||||||
<div v-if="worship.neighborInvitation" class="neighborhood-invitation">Einladung zum Gottesdienst im
|
<div v-if="worship.neighborInvitation" class="neighborhood-invitation">Einladung zum Gottesdienst im
|
||||||
Nachbarschaftsraum:</div>
|
Nachbarschaftsraum:</div>
|
||||||
<h3>
|
<h3>
|
||||||
<span
|
<span :class="worship.highlightTime ? 'highlight-time' : ''">{{ formatTime(worship.time)
|
||||||
:class="worship.highlightTime ? 'highlight-time' : ''"
|
}}</span> -
|
||||||
>{{ formatTime(worship.time) }}</span> -
|
{{ worship.title ? worship.title : `Gottesdienst in ${worship.eventPlace.name}` }}
|
||||||
{{
|
|
||||||
worship.title
|
|
||||||
? worship.title
|
|
||||||
: (worship.eventPlace && worship.eventPlace.name
|
|
||||||
? `Gottesdienst in ${worship.eventPlace.name}`
|
|
||||||
: 'Gottesdienst')
|
|
||||||
}}
|
|
||||||
</h3>
|
</h3>
|
||||||
<div v-if="worship.organizer">Gestaltung: {{ worship.organizer }}</div>
|
<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.sacristanService" class="internal-information">Küsterdienst: {{ worship.sacristanService }}</div>
|
||||||
<div v-if="worship.collection">Kollekte: {{ worship.collection }}</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.address }}</div>
|
||||||
<div
|
<div v-if="!worship.address && worship.eventPlace.id && worship.eventPlace.id">
|
||||||
v-if="!worship.address && worship.eventPlace && worship.eventPlace.id"
|
Adresse: {{ worship.eventPlace.name }}, {{ worship.eventPlace.street }}, {{
|
||||||
>
|
worship.eventPlace.city }}
|
||||||
Adresse: {{ worship.eventPlace.name }}, {{ worship.eventPlace.street }}, {{ worship.eventPlace.city }}
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="worship.selfInformation" class="selfinformation">
|
<div v-if="worship.selfInformation" class="selfinformation">Bitte informieren Sie sich auch auf den
|
||||||
Bitte informieren Sie sich auch auf den
|
<a v-if="worship.eventPlace.website" :href="worship.eventPlace.website" target="_blank">Internetseiten dieser Gemeinde!</a><span
|
||||||
<a
|
v-else>Internetseiten dieser Gemeinde!</span>
|
||||||
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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export default {
|
|||||||
zipcode: '',
|
zipcode: '',
|
||||||
city: '',
|
city: '',
|
||||||
email: '',
|
email: '',
|
||||||
expiryDate: null,
|
|
||||||
positions: []
|
positions: []
|
||||||
},
|
},
|
||||||
positions: []
|
positions: []
|
||||||
|
|||||||
@@ -8,29 +8,29 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<button @click="toggleHeading(3)" :class="{ 'is-active': editor && editor.isActive('heading', { level: 3 }) }">H3</button>
|
<button @click="toggleHeading(3)">H3</button>
|
||||||
<button @click="toggleHeading(4)" :class="{ 'is-active': editor && editor.isActive('heading', { level: 4 }) }">H4</button>
|
<button @click="toggleHeading(4)">H4</button>
|
||||||
<button @click="toggleHeading(5)" :class="{ 'is-active': editor && editor.isActive('heading', { level: 5 }) }">H5</button>
|
<button @click="toggleHeading(5)">H5</button>
|
||||||
<button @click="toggleHeading(6)" :class="{ 'is-active': editor && editor.isActive('heading', { level: 6 }) }">H6</button>
|
<button @click="toggleHeading(6)">H6</button>
|
||||||
<button @click="toggleBold()" :class="{ 'is-active': editor && editor.isActive('bold') }" width="24" height="24">
|
<button @click="toggleBold()" width="24" height="24">
|
||||||
<BoldIcon width="24" height="24" />
|
<BoldIcon width="24" height="24" />
|
||||||
</button>
|
</button>
|
||||||
<button @click="toggleItalic()" :class="{ 'is-active': editor && editor.isActive('italic') }">
|
<button @click="toggleItalic()">
|
||||||
<ItalicIcon width="24" height="24" />
|
<ItalicIcon width="24" height="24" />
|
||||||
</button>
|
</button>
|
||||||
<button @click="toggleUnderline()" :class="{ 'is-active': editor && editor.isActive('underline') }">
|
<button @click="toggleUnderline()">
|
||||||
<UnderlineIcon width="24" height="24" />
|
<UnderlineIcon width="24" height="24" />
|
||||||
</button>
|
</button>
|
||||||
<button @click="toggleStrike()" :class="{ 'is-active': editor && editor.isActive('strike') }">
|
<button @click="toggleStrike()">
|
||||||
<StrikethroughIcon width="24" height="24" />
|
<StrikethroughIcon width="24" height="24" />
|
||||||
</button>
|
</button>
|
||||||
<button @click="insertTable()">
|
<button @click="insertTable()">
|
||||||
<TableIcon width="24" height="24" />
|
<TableIcon width="24" height="24" />
|
||||||
</button>
|
</button>
|
||||||
<button @click="toggleBulletList()" :class="{ 'is-active': editor && editor.isActive('bulletList') }">
|
<button @click="toggleBulletList()">
|
||||||
<ListIcon width="24" height="24" />
|
<ListIcon width="24" height="24" />
|
||||||
</button>
|
</button>
|
||||||
<button @click="toggleOrderedList()" :class="{ 'is-active': editor && editor.isActive('orderedList') }">
|
<button @click="toggleOrderedList()">
|
||||||
<NumberedListLeftIcon width="24" height="24" />
|
<NumberedListLeftIcon width="24" height="24" />
|
||||||
</button>
|
</button>
|
||||||
<button @click="openAddImageDialog">
|
<button @click="openAddImageDialog">
|
||||||
@@ -511,15 +511,4 @@ export default {
|
|||||||
.align-top {
|
.align-top {
|
||||||
vertical-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>
|
</style>
|
||||||
|
|||||||
@@ -3,30 +3,12 @@
|
|||||||
<h2>Veranstaltungen</h2>
|
<h2>Veranstaltungen</h2>
|
||||||
<button @click="createEvent">Neue Veranstaltung</button>
|
<button @click="createEvent">Neue Veranstaltung</button>
|
||||||
<EventForm v-if="showForm"
|
<EventForm v-if="showForm"
|
||||||
ref="eventForm"
|
|
||||||
:event="selectedEvent"
|
:event="selectedEvent"
|
||||||
:institutions="institutions"
|
:institutions="institutions"
|
||||||
:eventPlaces="eventPlaces"
|
:eventPlaces="eventPlaces"
|
||||||
:contactPersons="contactPersons"
|
:contactPersons="contactPersons"
|
||||||
@saved="handleEventSaved"
|
@saved="handleEventSaved"
|
||||||
@cancelled="handleEventCancelled" />
|
@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>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -40,7 +22,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="event in filteredEvents" :key="event.id">
|
<tr v-for="event in events" :key="event.id">
|
||||||
<td>{{ event.name }}</td>
|
<td>{{ event.name }}</td>
|
||||||
<td>{{ getEventTypeCaption(event.eventTypeId) }}</td>
|
<td>{{ getEventTypeCaption(event.eventTypeId) }}</td>
|
||||||
<td>{{ event.date }}</td>
|
<td>{{ event.date }}</td>
|
||||||
@@ -73,52 +55,8 @@ export default {
|
|||||||
eventTypes: [],
|
eventTypes: [],
|
||||||
selectedEvent: null,
|
selectedEvent: null,
|
||||||
showForm: false,
|
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() {
|
async created() {
|
||||||
await this.fetchData();
|
await this.fetchData();
|
||||||
},
|
},
|
||||||
@@ -146,24 +84,10 @@ export default {
|
|||||||
createEvent() {
|
createEvent() {
|
||||||
this.selectedEvent = {};
|
this.selectedEvent = {};
|
||||||
this.showForm = true;
|
this.showForm = true;
|
||||||
this.scrollToFormAndFocus();
|
|
||||||
},
|
},
|
||||||
editEvent(event) {
|
editEvent(event) {
|
||||||
this.selectedEvent = { ...event };
|
this.selectedEvent = { ...event };
|
||||||
this.showForm = true;
|
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) {
|
async deleteEvent(id) {
|
||||||
try {
|
try {
|
||||||
@@ -186,7 +110,7 @@ export default {
|
|||||||
},
|
},
|
||||||
getWeekdayName(dayOfWeek) {
|
getWeekdayName(dayOfWeek) {
|
||||||
const weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
|
const weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
|
||||||
return weekdays[dayOfWeek];
|
return weekdays[dayOfWeek - 1];
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -198,40 +122,6 @@ export default {
|
|||||||
margin: 0 auto;
|
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 {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,14 +2,14 @@
|
|||||||
<div class="privacy-policy">
|
<div class="privacy-policy">
|
||||||
<h1>Datenschutzerklärung der Miriamgemeinde Frankfurt am Main</h1>
|
<h1>Datenschutzerklärung der Miriamgemeinde Frankfurt am Main</h1>
|
||||||
<p>
|
<p>
|
||||||
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.
|
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.
|
||||||
</p>
|
</p>
|
||||||
<h2>Anbieter:</h2>
|
<h2>Anbieter:</h2>
|
||||||
<p>
|
<p>
|
||||||
Miriamgemeinde Frankfurt am Main, Gemeindebüro Bonames<br />
|
Miriamgemeinde Frankfurt am Main, Gemeindebüro Bonames<br />
|
||||||
Kirchhofsweg 5, 60437 Frankfurt, Tel.: 50 14 17, Fax: 50 93 0148,<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 />
|
Email: <a href="mailto:Ev.Kirche-Bonames@t-online.de">Ev.Kirche-Bonames@t-online.de</a><br />
|
||||||
Inhaltlich Verantwortlicher gemäß § 6 DDG: Torsten Schulz
|
Inhaltlich Verantwortlicher gemäß § 6 MDStV: Torsten Schulz
|
||||||
</p>
|
</p>
|
||||||
<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.
|
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,23 +63,11 @@
|
|||||||
</p>
|
</p>
|
||||||
<h3>Ihr Recht auf Auskunft, Löschung, Sperrung</h3>
|
<h3>Ihr Recht auf Auskunft, Löschung, Sperrung</h3>
|
||||||
<p>
|
<p>
|
||||||
Sie haben als Nutzer nach dem Datenschutzgesetz der Evangelischen Kirche in Deutschland (DSG-EKD) folgende Rechte:
|
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.
|
||||||
</p>
|
</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>
|
<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.
|
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>
|
||||||
<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>
|
<h3>Der Datenschutzbeauftragte für den Datenschutz der Evangelischen Kirchen in Deutschland</h3>
|
||||||
<p>
|
<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
|
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
|
||||||
@@ -108,7 +96,7 @@
|
|||||||
<p>
|
<p>
|
||||||
Die Nutzer werden gebeten, sich regelmäßig über den Inhalt der Datenschutzerklärung zu informieren.
|
Die Nutzer werden gebeten, sich regelmäßig über den Inhalt der Datenschutzerklärung zu informieren.
|
||||||
</p>
|
</p>
|
||||||
<p>Stand: Januar 2025</p>
|
<p>Stand: 24. Mai 2018</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h3>Inhaltlich Verantwortlicher gemäß § 6 DDG:</h3>
|
<h3>Inhaltlich Verantwortlicher gemäß § 6 MDStV:</h3>
|
||||||
<p>Torsten Schulz</p>
|
<p>Torsten Schulz</p>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
11
src/main.js
11
src/main.js
@@ -5,18 +5,13 @@ import store from './store';
|
|||||||
import axios from './axios';
|
import axios from './axios';
|
||||||
import './assets/css/editor.css';
|
import './assets/css/editor.css';
|
||||||
|
|
||||||
// Menü-Daten über das konfigurierte Axios-Backend laden
|
|
||||||
async function fetchMenuData() {
|
async function fetchMenuData() {
|
||||||
const response = await axios.get('/menu-data');
|
const response = await fetch(process.env.VUE_APP_BACKEND_URL + '/menu-data');
|
||||||
return response.data;
|
return await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchMenuData()
|
fetchMenuData().then(menuData => {
|
||||||
.then(menuData => {
|
|
||||||
store.commit('setMenuData', menuData);
|
store.commit('setMenuData', menuData);
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Fehler beim Laden der Menü-Daten:', err);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const app = createApp(AppComponent);
|
const app = createApp(AppComponent);
|
||||||
|
|||||||
@@ -45,13 +45,8 @@ const router = createRouter({
|
|||||||
routes: []
|
routes: []
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verhindert endlose Wiederholungen von fehlgeschlagenen Menü-Ladeversuchen
|
|
||||||
let menuDataInitialized = false;
|
|
||||||
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
if (!menuDataInitialized) {
|
if (!store.state.menuData.length) {
|
||||||
menuDataInitialized = true;
|
|
||||||
|
|
||||||
await store.dispatch('loadMenuData');
|
await store.dispatch('loadMenuData');
|
||||||
const routes = generateRoutesFromMenu(store.state.menuData);
|
const routes = generateRoutesFromMenu(store.state.menuData);
|
||||||
routes.forEach(route => router.addRoute(route));
|
routes.forEach(route => router.addRoute(route));
|
||||||
@@ -70,12 +65,8 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
|
|
||||||
next({ ...to, replace: true });
|
next({ ...to, replace: true });
|
||||||
} else {
|
} 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) {
|
if (to.matched.some(record => record.meta.requiresAuth) && !store.getters.isLoggedIn) {
|
||||||
next('/auth/login');
|
next('/login');
|
||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createStore } from 'vuex';
|
import { createStore } from 'vuex';
|
||||||
import axios from '../axios';
|
import axios from 'axios';
|
||||||
import router from '../router';
|
import router from '../router';
|
||||||
|
|
||||||
let user = [];
|
let user = [];
|
||||||
@@ -35,6 +35,7 @@ export default createStore({
|
|||||||
localStorage.removeItem('isLoggedIn');
|
localStorage.removeItem('isLoggedIn');
|
||||||
localStorage.removeItem('user');
|
localStorage.removeItem('user');
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
|
router.push('/auth/login');
|
||||||
},
|
},
|
||||||
setMenuData(state, menuData) {
|
setMenuData(state, menuData) {
|
||||||
state.menuData = menuData;
|
state.menuData = menuData;
|
||||||
@@ -100,10 +101,6 @@ export default createStore({
|
|||||||
console.error('Fehler beim Logout:', error);
|
console.error('Fehler beim Logout:', error);
|
||||||
} finally {
|
} finally {
|
||||||
commit('logout');
|
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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
71
utils/ErrorHandler.js
Normal file
71
utils/ErrorHandler.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
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();
|
||||||
109
validators/UserValidator.js
Normal file
109
validators/UserValidator.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
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();
|
||||||
@@ -2,31 +2,12 @@ const { defineConfig } = require('@vue/cli-service');
|
|||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
|
|
||||||
module.exports = defineConfig({
|
module.exports = defineConfig({
|
||||||
transpileDependencies: [],
|
transpileDependencies: true,
|
||||||
devServer: {
|
devServer: {
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
// Port kann über VUE_APP_FRONTEND_PORT oder FRONTEND_PORT in .env gesetzt werden
|
port: 8080
|
||||||
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: {
|
configureWebpack: {
|
||||||
output: { clean: true },
|
|
||||||
cache: false,
|
|
||||||
resolve: {
|
resolve: {
|
||||||
fallback: {
|
fallback: {
|
||||||
"path": require.resolve("path-browserify"),
|
"path": require.resolve("path-browserify"),
|
||||||
@@ -42,11 +23,4 @@ module.exports = defineConfig({
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
chainWebpack: config => {
|
|
||||||
const rules = ['vue','js','ts','tsx','css','scss','sass','less','stylus'];
|
|
||||||
rules.forEach(rule => {
|
|
||||||
try { config.module.rule(rule).uses.delete('cache-loader'); } catch (e) {}
|
|
||||||
try { config.module.rule(rule).uses.delete('thread-loader'); } catch (e) {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user