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
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
dist/
|
|
||||||
public/
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
name: Deploy miriamgemeinde
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
env:
|
|
||||||
SSH_HOST: ${{ vars.PROD_HOST }}
|
|
||||||
SSH_PORT: ${{ vars.PROD_PORT }}
|
|
||||||
SSH_USER: ${{ vars.PROD_USER }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Show resolved non-secret config
|
|
||||||
run: |
|
|
||||||
echo "SSH_HOST=$SSH_HOST"
|
|
||||||
echo "SSH_PORT=$SSH_PORT"
|
|
||||||
echo "SSH_USER=$SSH_USER"
|
|
||||||
|
|
||||||
- name: Prepare SSH
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
printf '%s' "${{ secrets.PROD_SSH_KEY_B64 }}" | base64 -d > ~/.ssh/id_deploy
|
|
||||||
chmod 600 ~/.ssh/id_deploy
|
|
||||||
ssh-keygen -l -f ~/.ssh/id_deploy
|
|
||||||
ssh-keyscan -p "$SSH_PORT" "$SSH_HOST" >> ~/.ssh/known_hosts
|
|
||||||
|
|
||||||
- name: Test SSH connection
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
ssh -i ~/.ssh/id_deploy \
|
|
||||||
-o StrictHostKeyChecking=no \
|
|
||||||
-o BatchMode=yes \
|
|
||||||
-o ConnectTimeout=10 \
|
|
||||||
-p "$SSH_PORT" \
|
|
||||||
"$SSH_USER@$SSH_HOST" \
|
|
||||||
"echo SSH OK"
|
|
||||||
|
|
||||||
# If you need server-side preparation (e.g. ensure /var/... exists/permissions),
|
|
||||||
# add it in the remote command before running the update script.
|
|
||||||
- name: Run deployment script
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
ssh -i ~/.ssh/id_deploy \
|
|
||||||
-o StrictHostKeyChecking=no \
|
|
||||||
-o BatchMode=yes \
|
|
||||||
-o ConnectTimeout=10 \
|
|
||||||
-p "$SSH_PORT" \
|
|
||||||
"$SSH_USER@$SSH_HOST" \
|
|
||||||
"bash -lc 'set -euo pipefail; TS=\$(date +\"%Y-%m-%d_%H%M%S\"); SRC=\"/var/www/miriamgemeinde/public/images\"; DEST_BASE=\"/home/torsten/miriamgemeinde/backup/\$TS\"; mkdir -p \"\$DEST_BASE\"; if [ -d \"\$SRC\" ]; then cp -a \"\$SRC\" \"\$DEST_BASE/\"; echo \"Backed up \$SRC -> \$DEST_BASE/images\"; else echo \"WARN: \$SRC does not exist; skipping backup\"; fi; if [ -f /home/torsten/update-miriamgemeinde.sh ]; then bash /home/torsten/update-miriamgemeinde.sh; else echo \"ERROR: /home/torsten/update-miriamgemeinde.sh not found\"; exit 127; fi'"
|
|
||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -27,16 +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
|
||||||
|
|
||||||
# Vue Build-Artefakte (werden beim Deploy generiert)
|
|
||||||
/public/index.html
|
|
||||||
/public/assets/
|
|
||||||
public/js/
|
|
||||||
public/css/
|
|
||||||
public/**/*.map
|
|
||||||
|
|
||||||
# Uploads/Runtime-Dateien nicht versionieren
|
|
||||||
public/images/uploads/
|
|
||||||
actualize.sh
|
|
||||||
files/uploads/GD 24.08.2025-04.01.2026 Stand 12.08.2025.docx
|
|
||||||
.codex
|
|
||||||
|
|||||||
5
babel.config.js
Normal file
5
babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/cli-plugin-babel/preset'
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
{
|
{
|
||||||
"development": {
|
"development": {
|
||||||
"username": "miriam_user",
|
"username": "miriam_user",
|
||||||
"password": "hitomisan",
|
"password": "qTCTTWwpEwy3vPDU",
|
||||||
"database": "miriamgemeinde",
|
"database": "miriamgemeinde",
|
||||||
"host": "tsschulz.de",
|
"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,25 +1,8 @@
|
|||||||
const { Sequelize } = require('sequelize');
|
const { Sequelize } = require('sequelize');
|
||||||
require('dotenv').config();
|
|
||||||
|
|
||||||
const envName = process.env.NODE_ENV || 'development';
|
const sequelize = new Sequelize('miriamgemeinde', 'miriam_user', 'qTCTTWwpEwy3vPDU', {
|
||||||
const fileConfig = require('./config.json')[envName];
|
host: 'tsschulz.de',
|
||||||
|
dialect: 'mysql',
|
||||||
if (!fileConfig) {
|
|
||||||
throw new Error(
|
|
||||||
`[DB] Kein Eintrag in config/config.json für NODE_ENV="${envName}".`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const database = process.env.DB_NAME || fileConfig.database;
|
|
||||||
const username = process.env.DB_USER || fileConfig.username;
|
|
||||||
const password =
|
|
||||||
process.env.DB_PASSWORD === undefined ? fileConfig.password : process.env.DB_PASSWORD;
|
|
||||||
const host = process.env.DB_HOST || fileConfig.host;
|
|
||||||
|
|
||||||
const sequelizeOptions = {
|
|
||||||
host,
|
|
||||||
dialect: fileConfig.dialect || 'mysql',
|
|
||||||
dialectOptions: fileConfig.dialectOptions,
|
|
||||||
retry: {
|
retry: {
|
||||||
match: [
|
match: [
|
||||||
/ConnectionError/,
|
/ConnectionError/,
|
||||||
@@ -28,38 +11,24 @@ const sequelizeOptions = {
|
|||||||
/SequelizeHostNotFoundError/,
|
/SequelizeHostNotFoundError/,
|
||||||
/SequelizeHostNotReachableError/,
|
/SequelizeHostNotReachableError/,
|
||||||
/SequelizeInvalidConnectionError/,
|
/SequelizeInvalidConnectionError/,
|
||||||
/SequelizeConnectionTimedOutError/,
|
/SequelizeConnectionTimedOutError/
|
||||||
],
|
],
|
||||||
max: 5,
|
max: 5
|
||||||
},
|
},
|
||||||
pool: {
|
pool: {
|
||||||
max: 5,
|
max: 5,
|
||||||
min: 0,
|
min: 0,
|
||||||
acquire: 30000,
|
acquire: 30000,
|
||||||
idle: 10000,
|
idle: 10000
|
||||||
},
|
}
|
||||||
logging: process.env.DB_LOGGING === '1' ? console.log : false,
|
});
|
||||||
};
|
|
||||||
|
|
||||||
if (process.env.DB_PORT) {
|
|
||||||
sequelizeOptions.port = parseInt(process.env.DB_PORT, 10);
|
|
||||||
} else if (fileConfig.port) {
|
|
||||||
sequelizeOptions.port = fileConfig.port;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sequelize = new Sequelize(database, username, password, sequelizeOptions);
|
|
||||||
|
|
||||||
async function connectWithRetry() {
|
async function connectWithRetry() {
|
||||||
try {
|
try {
|
||||||
await sequelize.authenticate();
|
await sequelize.authenticate();
|
||||||
console.log(
|
console.log('Connection has been established successfully.');
|
||||||
`[DB] Verbindung OK — host=${host} database=${database} user=${username} (NODE_ENV=${envName})`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[DB] Verbindung fehlgeschlagen:', error.message);
|
console.error('Unable to connect to the database:', error);
|
||||||
console.error(
|
|
||||||
`[DB] Erwartete Quelle: config/config.json → "${envName}" oder Umgebungsvariablen DB_HOST, DB_USER, DB_PASSWORD, DB_NAME`
|
|
||||||
);
|
|
||||||
setTimeout(connectWithRetry, 5000);
|
setTimeout(connectWithRetry, 5000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,209 +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) => {
|
exports.login = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
const { name, email, password } = req.body;
|
const result = await AuthService.login(req.body);
|
||||||
if (!name || !email || !password) {
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
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;
|
exports.forgotPassword = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
let attempt = 0;
|
const result = await AuthService.forgotPassword(req.body.email);
|
||||||
let createdUser = null;
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
let lastError = null;
|
});
|
||||||
|
|
||||||
while (attempt < maxAttempts && !createdUser) {
|
exports.resetPassword = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
const result = await AuthService.resetPassword(req.body.token, req.body.password);
|
||||||
createdUser = await User.create({ name, email, password: hashedPassword, active: false });
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
} 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) {
|
exports.logout = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
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: process.env.JWT_EXPIRES_IN || '12h' }
|
|
||||||
);
|
|
||||||
return res.status(200).json({ message: 'Login erfolgreich', token, 'user': user });
|
|
||||||
} catch (error) {
|
|
||||||
return res.status(500).json({ message: 'Ein Fehler ist aufgetreten' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.forgotPassword = async (req, res) => {
|
|
||||||
const { email } = req.body;
|
|
||||||
if (!email) {
|
|
||||||
return res.status(400).json({ message: 'E-Mail-Adresse ist erforderlich' });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const user = await User.findOne({ where: { email } });
|
|
||||||
if (!user) {
|
|
||||||
// Aus Sicherheitsgründen immer Erfolg melden, auch wenn E-Mail nicht existiert
|
|
||||||
return res.status(200).json({ message: 'Falls die E-Mail-Adresse in unserem System registriert ist, erhalten Sie einen Link zum Zurücksetzen des Passworts.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alte Reset-Tokens für diesen User löschen
|
|
||||||
await PasswordResetToken.destroy({ where: { userId: user.id } });
|
|
||||||
|
|
||||||
// Neuen Reset-Token generieren
|
|
||||||
const token = crypto.randomBytes(32).toString('hex');
|
|
||||||
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 Stunde
|
|
||||||
|
|
||||||
await PasswordResetToken.create({
|
|
||||||
userId: user.id,
|
|
||||||
token,
|
|
||||||
expiresAt
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset-URL generieren
|
|
||||||
const resetUrl = `${process.env.FRONTEND_URL || 'http://localhost:8080'}/reset-password?token=${token}`;
|
|
||||||
|
|
||||||
// E-Mail versenden
|
|
||||||
const emailTemplate = getPasswordResetEmailTemplate(resetUrl, user.name);
|
|
||||||
|
|
||||||
const mailOptions = {
|
|
||||||
from: process.env.SMTP_FROM || 'noreply@miriamgemeinde.de',
|
|
||||||
to: email,
|
|
||||||
subject: emailTemplate.subject,
|
|
||||||
html: emailTemplate.html,
|
|
||||||
text: emailTemplate.text
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('=== EMAIL SENDING DEBUG ===');
|
|
||||||
console.log('From:', mailOptions.from);
|
|
||||||
console.log('To:', mailOptions.to);
|
|
||||||
console.log('Subject:', mailOptions.subject);
|
|
||||||
console.log('Reset URL:', resetUrl);
|
|
||||||
console.log('===========================');
|
|
||||||
|
|
||||||
await transporter.sendMail(mailOptions);
|
|
||||||
|
|
||||||
console.log('Password reset email sent to:', email);
|
|
||||||
return res.status(200).json({ message: 'Falls die E-Mail-Adresse in unserem System registriert ist, erhalten Sie einen Link zum Zurücksetzen des Passworts.' });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Forgot password error:', error);
|
|
||||||
return res.status(500).json({ message: 'Ein Fehler ist aufgetreten' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.resetPassword = async (req, res) => {
|
|
||||||
const { token, password } = req.body;
|
|
||||||
if (!token || !password) {
|
|
||||||
return res.status(400).json({ message: 'Token und neues Passwort sind erforderlich' });
|
|
||||||
}
|
|
||||||
if (password.length < 6) {
|
|
||||||
return res.status(400).json({ message: 'Passwort muss mindestens 6 Zeichen lang sein' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Token validieren
|
|
||||||
const resetToken = await PasswordResetToken.findOne({
|
|
||||||
where: {
|
|
||||||
token,
|
|
||||||
used: false,
|
|
||||||
expiresAt: {
|
|
||||||
[require('sequelize').Op.gt]: new Date()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
include: [{ model: User, as: 'user' }]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!resetToken) {
|
|
||||||
return res.status(400).json({ message: 'Ungültiger oder abgelaufener Token' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Passwort hashen und aktualisieren
|
|
||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
|
||||||
await User.update(
|
|
||||||
{ password: hashedPassword },
|
|
||||||
{ where: { id: resetToken.userId } }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Token als verwendet markieren
|
|
||||||
await resetToken.update({ used: true });
|
|
||||||
|
|
||||||
console.log('Password reset successful for user:', resetToken.userId);
|
|
||||||
return res.status(200).json({ message: 'Passwort erfolgreich zurückgesetzt' });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Reset password error:', error);
|
|
||||||
return res.status(500).json({ message: 'Ein Fehler ist aufgetreten' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.logout = async (req, res) => {
|
|
||||||
const authHeader = req.header('Authorization');
|
const 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,208 +1,32 @@
|
|||||||
const { Event, Institution, EventPlace, ContactPerson, EventType, EventContactPerson, sequelize } = require('../models');
|
const EventService = require('../services/EventService');
|
||||||
const { Op, fn, col, where: sequelizeWhere } = require('sequelize');
|
const ErrorHandler = require('../utils/ErrorHandler');
|
||||||
|
|
||||||
function buildUpcomingWhere() {
|
exports.getAllEvents = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
return {
|
const events = await EventService.getAllEvents();
|
||||||
[Op.or]: [
|
ErrorHandler.successResponse(res, events, 'Events erfolgreich abgerufen');
|
||||||
{ date: { [Op.eq]: null } },
|
});
|
||||||
// Fixed-date events: only today or future (date-only compare).
|
|
||||||
sequelizeWhere(fn('DATE', col('date')), { [Op.gte]: fn('CURDATE') }),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAllEvents = async (req, res) => {
|
exports.getEventById = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
const event = await EventService.getEventById(req.params.id);
|
||||||
const includePast = String(req.query?.includePast || '').toLowerCase();
|
ErrorHandler.successResponse(res, event, 'Event erfolgreich abgerufen');
|
||||||
const wantsPast = includePast === '1' || includePast === 'true' || includePast === 'yes';
|
});
|
||||||
|
|
||||||
const where = wantsPast
|
exports.filterEvents = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
? undefined
|
const result = await EventService.filterEvents(req.body);
|
||||||
: buildUpcomingWhere();
|
ErrorHandler.successResponse(res, result, 'Events erfolgreich gefiltert');
|
||||||
|
});
|
||||||
|
|
||||||
const events = await Event.findAll({
|
exports.createEvent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
where,
|
const event = await EventService.createEvent(req.body);
|
||||||
include: [
|
ErrorHandler.successResponse(res, event, 'Event erfolgreich erstellt', 201);
|
||||||
{ model: Institution, as: 'institution' },
|
});
|
||||||
{ model: EventPlace, as: 'eventPlace' },
|
|
||||||
{ model: EventType, as: 'eventType' },
|
|
||||||
{ model: ContactPerson, as: 'contactPersons', through: { attributes: [] } }
|
|
||||||
],
|
|
||||||
order: [
|
|
||||||
['date', 'ASC'],
|
|
||||||
['time', 'ASC'],
|
|
||||||
['name', 'ASC'],
|
|
||||||
],
|
|
||||||
});
|
|
||||||
res.json(events);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: 'Failed to fetch events' });
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterEvents = async (req, res) => {
|
exports.updateEvent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
const event = await EventService.updateEvent(req.params.id, req.body);
|
||||||
const request = req.body;
|
ErrorHandler.successResponse(res, event, 'Event erfolgreich aktualisiert');
|
||||||
const where = buildUpcomingWhere();
|
});
|
||||||
const order = [
|
|
||||||
['date', 'ASC'],
|
|
||||||
['time', 'ASC']
|
|
||||||
];
|
|
||||||
|
|
||||||
if (request.id === 'all') {
|
exports.deleteEvent = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
const events = await Event.findAll({
|
const result = await EventService.deleteEvent(req.params.id);
|
||||||
where,
|
ErrorHandler.successResponse(res, result, result.message);
|
||||||
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,
|
|
||||||
...buildUpcomingWhere(),
|
|
||||||
},
|
|
||||||
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 });
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: 'Failed to filter events' });
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const createEvent = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { contactPersonIds, ...eventData } = req.body;
|
|
||||||
eventData.alsoOnHomepage = eventData.alsoOnHomepage ?? 0;
|
|
||||||
const event = await Event.create(eventData);
|
|
||||||
if (contactPersonIds) {
|
|
||||||
await event.setContactPersons(contactPersonIds);
|
|
||||||
}
|
|
||||||
res.status(201).json(event);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: 'Failed to create event' });
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateEvent = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { contactPersonIds, ...eventData } = req.body;
|
|
||||||
const event = await Event.findByPk(id);
|
|
||||||
if (!event) {
|
|
||||||
return res.status(404).json({ error: 'Event not found' });
|
|
||||||
}
|
|
||||||
await event.update(eventData);
|
|
||||||
if (contactPersonIds) {
|
|
||||||
await event.setContactPersons(contactPersonIds);
|
|
||||||
}
|
|
||||||
res.status(200).json(event);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: 'Failed to update event' });
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteEvent = async (req, res) => {
|
|
||||||
const transaction = await sequelize.transaction();
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
// Erst Zuordnungen in der Join-Tabelle löschen, damit FK-Constraints erfüllt sind.
|
|
||||||
await EventContactPerson.destroy({
|
|
||||||
where: { event_id: id },
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleted = await Event.destroy({
|
|
||||||
where: { id: id },
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
if (deleted) {
|
|
||||||
await transaction.commit();
|
|
||||||
res.status(204).json();
|
|
||||||
} else {
|
|
||||||
await transaction.rollback();
|
|
||||||
res.status(404).json({ error: 'Event not found' });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
res.status(500).json({ error: 'Failed to delete event' });
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getAllEvents,
|
|
||||||
createEvent,
|
|
||||||
updateEvent,
|
|
||||||
deleteEvent,
|
|
||||||
filterEvents
|
|
||||||
};
|
|
||||||
@@ -16,7 +16,6 @@ const storage = multer.diskStorage({
|
|||||||
const upload = multer({ storage });
|
const upload = multer({ storage });
|
||||||
|
|
||||||
exports.uploadImage = upload.single('image');
|
exports.uploadImage = upload.single('image');
|
||||||
exports.uploadImages = upload.array('images');
|
|
||||||
|
|
||||||
exports.getAllPages = async (req, res) => {
|
exports.getAllPages = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -52,51 +51,6 @@ exports.saveImageDetails = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.saveImageDetailsBulk = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { baseTitle, description, page, startNumber } = req.body;
|
|
||||||
const files = req.files || [];
|
|
||||||
|
|
||||||
if (!baseTitle || !String(baseTitle).trim()) {
|
|
||||||
return res.status(400).json({ error: 'Bitte einen Basis-Titel angeben.' });
|
|
||||||
}
|
|
||||||
if (files.length === 0) {
|
|
||||||
return res.status(400).json({ error: 'Bitte mindestens ein Bild auswählen.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageItem = page ? await Page.findAll({ where: { link: page } }) : [];
|
|
||||||
const pageId = pageItem && pageItem[0] ? pageItem[0].id : null;
|
|
||||||
const firstNumber = Number.parseInt(startNumber, 10);
|
|
||||||
const startAt = Number.isFinite(firstNumber) ? firstNumber : 1;
|
|
||||||
|
|
||||||
const createdImages = [];
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
const file = files[i];
|
|
||||||
const runningNumber = startAt + i;
|
|
||||||
const title = `${String(baseTitle).trim()} ${runningNumber}`;
|
|
||||||
const newImage = await Image.create({
|
|
||||||
id: uuidv4(),
|
|
||||||
filename: file.filename,
|
|
||||||
title,
|
|
||||||
description: description || '',
|
|
||||||
pageId,
|
|
||||||
});
|
|
||||||
createdImages.push(newImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Menübild nur dann setzen, wenn genau ein Bild hochgeladen wurde.
|
|
||||||
if (page && createdImages.length === 1) {
|
|
||||||
const imageUrl = `/uploads/${createdImages[0].filename}`;
|
|
||||||
await MenuItem.update({ image: imageUrl }, { where: { link: page } });
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(201).json(createdImages);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Bulk-Upload der Bilder:', error);
|
|
||||||
res.status(500).json({ error: 'Fehler beim Bulk-Upload der Bilder' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.getImages = async (req, res) => {
|
exports.getImages = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const images = await Image.findAll({ order: [['title', 'ASC']] });
|
const images = await Image.findAll({ order: [['title', 'ASC']] });
|
||||||
|
|||||||
@@ -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,31 +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) {
|
|
||||||
console.error('getMenuData:', error);
|
|
||||||
res.status(500).json({ error: 'Menü konnte nicht geladen werden (Datenbank nicht erreichbar).' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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)
|
});
|
||||||
const safeUser = {
|
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
email: user.email,
|
|
||||||
active: user.active,
|
|
||||||
created_at: user.created_at
|
|
||||||
};
|
|
||||||
|
|
||||||
res.status(201).json(safeUser);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating user:', error);
|
|
||||||
res.status(500).json({ message: 'Error creating user' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.updateUser = async (req, res) => {
|
exports.updateUser = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
UserValidator.validateId(req.params.id);
|
||||||
const user = await User.findByPk(req.params.id);
|
UserValidator.validateUpdateUser(req.body);
|
||||||
if (user) {
|
const user = await UserService.updateUser(req.params.id, req.body);
|
||||||
// Erstelle eine Kopie der Request-Daten ohne sensible Felder
|
ErrorHandler.successResponse(res, user, 'Benutzer erfolgreich aktualisiert');
|
||||||
const updateData = { ...req.body };
|
});
|
||||||
|
|
||||||
// Entferne sensible Felder, die niemals über diese Route geändert werden dürfen
|
|
||||||
delete updateData.password;
|
|
||||||
delete updateData.id;
|
|
||||||
delete updateData.created_at;
|
|
||||||
|
|
||||||
// Setze updated_at auf aktuelle Zeit
|
|
||||||
updateData.updated_at = new Date();
|
|
||||||
|
|
||||||
// Logging für Debugging
|
|
||||||
console.log('Updating user:', req.params.id, 'with data:', updateData);
|
|
||||||
|
|
||||||
await user.update(updateData);
|
|
||||||
|
|
||||||
// Sichere User-Daten zurückgeben (ohne Passwort)
|
|
||||||
const safeUser = {
|
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
email: user.email,
|
|
||||||
active: user.active,
|
|
||||||
created_at: user.created_at
|
|
||||||
};
|
|
||||||
|
|
||||||
res.status(200).json(safeUser);
|
|
||||||
} else {
|
|
||||||
res.status(404).json({ message: 'User not found' });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating user:', error);
|
|
||||||
res.status(500).json({ message: 'Error updating user' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.deleteUser = async (req, res) => {
|
exports.deleteUser = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
try {
|
UserValidator.validateId(req.params.id);
|
||||||
const user = await User.findByPk(req.params.id);
|
await UserService.deleteUser(req.params.id);
|
||||||
if (user) {
|
ErrorHandler.successResponse(res, null, 'Benutzer erfolgreich gelöscht');
|
||||||
await user.destroy();
|
});
|
||||||
res.status(200).json({ message: 'User deleted successfully' });
|
|
||||||
} else {
|
// Neue Route für Passwort-Änderung
|
||||||
res.status(404).json({ message: 'User not found' });
|
exports.changePassword = ErrorHandler.asyncHandler(async (req, res) => {
|
||||||
}
|
const { currentPassword, newPassword } = req.body;
|
||||||
} catch (error) {
|
UserValidator.validateId(req.params.id);
|
||||||
console.error('Error deleting user:', error);
|
UserValidator.validatePasswordChange(currentPassword, newPassword);
|
||||||
res.status(500).json({ message: 'Error deleting user' });
|
await UserService.changePassword(req.params.id, currentPassword, newPassword);
|
||||||
}
|
ErrorHandler.successResponse(res, null, 'Passwort erfolgreich geändert');
|
||||||
};
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,88 +0,0 @@
|
|||||||
const { WorshipLeader } = require('../models');
|
|
||||||
const { Op } = require('sequelize');
|
|
||||||
|
|
||||||
function normalizeLeaderPayload(body) {
|
|
||||||
const code = String(body.code || '').trim();
|
|
||||||
const name = String(body.name || '').trim();
|
|
||||||
const aliases = String(body.aliases || '').trim();
|
|
||||||
const active = body.active === undefined ? true : !!body.active;
|
|
||||||
return { code, name, aliases, active };
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getAllWorshipLeaders = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const includeInactive = String(req.query?.includeInactive || '').toLowerCase();
|
|
||||||
const wantsInactive = includeInactive === '1' || includeInactive === 'true' || includeInactive === 'yes';
|
|
||||||
|
|
||||||
const where = wantsInactive ? undefined : { active: true };
|
|
||||||
const leaders = await WorshipLeader.findAll({
|
|
||||||
where,
|
|
||||||
order: [['code', 'ASC']],
|
|
||||||
});
|
|
||||||
res.json(leaders);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('getAllWorshipLeaders:', error);
|
|
||||||
res.status(500).json({ error: 'Failed to fetch worship leaders' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.createWorshipLeader = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const payload = normalizeLeaderPayload(req.body || {});
|
|
||||||
if (!payload.code || !payload.name) {
|
|
||||||
return res.status(400).json({ message: 'code und name sind Pflichtfelder.' });
|
|
||||||
}
|
|
||||||
const existing = await WorshipLeader.findOne({ where: { code: payload.code } });
|
|
||||||
if (existing) {
|
|
||||||
return res.status(409).json({ message: `Kürzel "${payload.code}" existiert bereits.` });
|
|
||||||
}
|
|
||||||
const created = await WorshipLeader.create(payload);
|
|
||||||
res.status(201).json(created);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('createWorshipLeader:', error);
|
|
||||||
res.status(500).json({ error: 'Failed to create worship leader' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.updateWorshipLeader = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const leader = await WorshipLeader.findByPk(id);
|
|
||||||
if (!leader) {
|
|
||||||
return res.status(404).json({ message: 'Worship leader not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = normalizeLeaderPayload(req.body || {});
|
|
||||||
if (!payload.code || !payload.name) {
|
|
||||||
return res.status(400).json({ message: 'code und name sind Pflichtfelder.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const codeClash = await WorshipLeader.findOne({
|
|
||||||
where: { code: payload.code, id: { [Op.ne]: id } },
|
|
||||||
});
|
|
||||||
if (codeClash) {
|
|
||||||
return res.status(409).json({ message: `Kürzel "${payload.code}" existiert bereits.` });
|
|
||||||
}
|
|
||||||
|
|
||||||
await leader.update(payload);
|
|
||||||
res.json(leader);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('updateWorshipLeader:', error);
|
|
||||||
res.status(500).json({ error: 'Failed to update worship leader' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.deleteWorshipLeader = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const deleted = await WorshipLeader.destroy({ where: { id } });
|
|
||||||
if (!deleted) {
|
|
||||||
return res.status(404).json({ message: 'Worship leader not found' });
|
|
||||||
}
|
|
||||||
res.status(204).json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('deleteWorshipLeader:', error);
|
|
||||||
res.status(500).json({ error: 'Failed to delete worship leader' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
69
deploy.sh
69
deploy.sh
@@ -1,69 +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; }
|
|
||||||
|
|
||||||
ensure_command() {
|
|
||||||
local cmd="$1"
|
|
||||||
command -v "$cmd" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
bootstrap_node() {
|
|
||||||
# In non-interactive SSH sessions, node/npm might not be on PATH (e.g. nvm in .bashrc).
|
|
||||||
if ensure_command npm && ensure_command node; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if [ -s "$HOME/.nvm/nvm.sh" ]; then
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
. "$HOME/.nvm/nvm.sh"
|
|
||||||
# Prefer default alias if configured, otherwise keep current.
|
|
||||||
nvm use --silent default >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
bootstrap_node
|
|
||||||
if ! ensure_command git; then err "git not found in PATH"; exit 127; fi
|
|
||||||
if ! ensure_command npm; then err "npm not found in PATH"; exit 127; fi
|
|
||||||
|
|
||||||
log "Fetching latest changes..."
|
|
||||||
git fetch --all --prune || { err "git fetch failed"; exit 1; }
|
|
||||||
|
|
||||||
log "Cleaning generated frontend artifacts..."
|
|
||||||
git restore -- package-lock.json package.json 2>/dev/null || true
|
|
||||||
git restore -- public/index.html 2>/dev/null || true
|
|
||||||
git clean -fd -- dist public/assets || { err "cleanup 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..."
|
|
||||||
if [ "${EUID:-$(id -u)}" -eq 0 ]; then
|
|
||||||
systemctl restart miriamgemeinde || { err "service restart failed"; exit 1; }
|
|
||||||
else
|
|
||||||
# Non-interactive deploys (CI) must not prompt for a sudo password.
|
|
||||||
# Configure on the server (recommended):
|
|
||||||
# sudo visudo -f /etc/sudoers.d/miriamgemeinde
|
|
||||||
# torsten ALL=NOPASSWD:/bin/systemctl restart miriamgemeinde,/bin/systemctl status miriamgemeinde
|
|
||||||
sudo -n systemctl restart miriamgemeinde || {
|
|
||||||
err "service restart failed (no password prompt). Configure passwordless sudo for 'systemctl restart miriamgemeinde' or run deploy as root."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Deployment completed successfully."
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
# Konzept: Modernisierung von Design und Bedienbarkeit
|
|
||||||
|
|
||||||
**Projekt:** Evangelische Miriamgemeinde Frankfurt (Vue.js-Webauftritt)
|
|
||||||
**Stand:** April 2026
|
|
||||||
**Ziel:** Zeitgemäße, klare Oberfläche mit hoher Vertrauenswürdigkeit; kirchlich-seriös, ohne „Startup-Optik“.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Zielbild und Leitlinien
|
|
||||||
|
|
||||||
### 1.1 Positionierung
|
|
||||||
|
|
||||||
Die Website ist **Informations- und Gemeinschaftsangebot** einer evangelischen Gemeinde. Sie soll:
|
|
||||||
|
|
||||||
- **verlässlich und ruhig** wirken (kein visuelles „Rauschen“),
|
|
||||||
- **inhaltlich im Vordergrund** stehen (Typografie, Lesbarkeit, klare Hierarchie),
|
|
||||||
- **digital souverän** wirken (gute Struktur, schnelle Orientierung, respektvolle Hilfen für alle Nutzergruppen).
|
|
||||||
|
|
||||||
### 1.2 Nicht verhandelbar: EKHN-Violett
|
|
||||||
|
|
||||||
Die **Grundfarbe EKHN-Violett** bleibt die primäre Markenfarbe. Im Code aktuell u. a. als `#9400ff` mit Hover `#7a00d1` genutzt (Navigation). Diese Farbe wird **nicht ersetzt oder „neu interpretiert“**.
|
|
||||||
|
|
||||||
- Sie wird als **CSS-Design-Token** zentral definiert (z. B. `--color-ekhn-violet`, `--color-ekhn-violet-hover`), damit alle Komponenten konsistent darauf zugreifen.
|
|
||||||
- **Abstufungen** (heller für Hintergründe, transparenter für Overlays) sind **zulässig**, solange die wahrgenommene Marke **dieselbe Violett-Identität** bleibt.
|
|
||||||
- Kontrast zu Text und Icons muss **WCAG-konform** sein (siehe Abschnitt 7).
|
|
||||||
|
|
||||||
### 1.3 Seriosität vs. Modernität
|
|
||||||
|
|
||||||
| Modern (gewünscht) | Vermeiden (für kirchlichen Kontext) |
|
|
||||||
|--------------------|-------------------------------------|
|
|
||||||
| Klares Raster, viel Weißraum | Neon-Verläufe, Spielereien |
|
|
||||||
| Ruhige, lesbare Schrift | Display-Fonts, übertriebene Größen |
|
|
||||||
| Deutliche Fokuszustände (Tastatur) | Aggressive Animationen |
|
|
||||||
| Einheitliche Komponenten | Zufällige Abstände und Stile pro Seite |
|
|
||||||
| Verständliche Navigation | „Experimentelle“ Menüs ohne klare Labels |
|
|
||||||
|
|
||||||
**Leitmotiv:** *Ruhige Sachlichkeit mit warmer, einladender Sprache in der UI (Beschriftungen, Fehlermeldungen, leere Zustände).*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Kurze Ist-Analyse (Ausgangslage)
|
|
||||||
|
|
||||||
Aus dem aktuellen Aufbau (u. a. `AppComponent.vue`, `NavbarComponent.vue`, `HeaderComponent.vue`, `FooterComponent.vue`):
|
|
||||||
|
|
||||||
- **Typografie:** durchgängig `Arial, sans-serif` – funktional, aber wenig Profil; keine skalierbare Typo-Skala.
|
|
||||||
- **Layout:** starre `min-width: 1000px` in der Hauptspalte begünstigt horizontales Scrollen auf Tablets/kleineren Viewports; Zwei-Spalten-Logik mit Breakpoints ist vorhanden, sollte aber **inhaltlich und technisch** weiterentwickelt werden.
|
|
||||||
- **Farben:** Violett in der Navigation; Footer dunkelblau (`#0b1735`); rechte Spalte hellblau (`#d9e2f3`); Header mit Schlagschatten in Lavendeltönen – teils **uneinheitlich** zur Markenfarbe.
|
|
||||||
- **Navigation:** Hamburger/Menü-Button unter 768px; Dropdowns mit Hover – auf Touch-Geräten und für Tastaturnutzer ist hier **Verbesserungspotenzial** (Fokus, ARIA, Touch-Targets).
|
|
||||||
- **Footer:** Login-Link in Grau auf dunklem Grund – **Kontrast** prüfen und ggf. anpassen (ohne Marke zu verändern).
|
|
||||||
|
|
||||||
Diese Punkte fließen als konkrete Maßnahmen in die Phasenplanung (Abschnitt 9) ein.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Design-System: Farben
|
|
||||||
|
|
||||||
### 3.1 Primär (unverändert)
|
|
||||||
|
|
||||||
| Token (Vorschlag) | Verwendung | Hex (Ist) |
|
|
||||||
|-------------------|------------|-----------|
|
|
||||||
| `--color-brand-primary` | Navigationsleiste, primäre Buttons, aktive Zustände | `#9400ff` |
|
|
||||||
| `--color-brand-primary-hover` | Hover, aktive Menüpunkte | `#7a00d1` |
|
|
||||||
|
|
||||||
*Hinweis:* Falls das offizielle EKHN-Handbuch eine minimal abweichende Hex-Angabe vorsieht, **eine** kanonische Quelle festlegen und nur diese verwenden – weiterhin **kein** Wechsel zu einer anderen Farbfamilie.
|
|
||||||
|
|
||||||
### 3.2 Neutrale Flächen (ergänzend, nicht markenersetzend)
|
|
||||||
|
|
||||||
- **Hintergrund Seite:** `#ffffff` oder sehr helles Neutral (`#f8f9fb`), konsistent.
|
|
||||||
- **Sekundärflächen** (Karten, rechte Spalte, Infoboxen): dezentes Grau oder ein **sehr zurückhaltendes Violett-Grau** (z. B. Mischung aus Weiß mit 3–6 % Primärfarbe), damit die Seite **ruhig** bleibt und nicht „bunt“ wirkt.
|
|
||||||
- **Text:** nahezu schwarz für Fließtext (`#1a1a1a` bis `#222`), sekundäre Texte etwas heller – immer mit Kontrastprüfung.
|
|
||||||
|
|
||||||
### 3.3 Akzent (optional, sparsam)
|
|
||||||
|
|
||||||
- **Links** im Fließtext: z. B. unterstrichen oder klar farbig abgesetzt; Primärviolett oder eine **eine** abgestimmte dunklere Violett-Nuance für Lesbarkeit auf Weiß.
|
|
||||||
- **Erfolg/Warnung/Fehler:** Standard-Semantik (Grün/Gelb/Rot) nur für Status – **nicht** als neue Hauptfarbe neben dem Violett.
|
|
||||||
|
|
||||||
### 3.4 Footer
|
|
||||||
|
|
||||||
- Dunkler Footer kann bleiben; **Links und Fokus** müssen gut lesbar sein. Primärviolett für Hover/Fokus auf dunklem Grund nur, wenn der Kontrast stimmt – sonch neutrale helle Linkfarbe + sichtbarer Fokusring.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Typografie
|
|
||||||
|
|
||||||
### 4.1 Schriftwahl
|
|
||||||
|
|
||||||
- **Primärschrift:** eine gut lesbare **System- oder Webschrift** mit neutral-seriösem Charakter, z. B.:
|
|
||||||
- *Source Sans 3*, *Inter*, *Open Sans* oder **beibehaltene Arial** nach einheitlicher Skala – Entscheidung in Phase 1 an **Performance** und **Corporate-Vorgaben** binden.
|
|
||||||
- **Überschriften:** dieselbe Familie mit klarer Gewichtsstaffelung (z. B. 600/700), keine verspielten Display-Schnitte.
|
|
||||||
|
|
||||||
### 4.2 Skala (Beispiel)
|
|
||||||
|
|
||||||
| Stufe | Verwendung | Größe (Orientierung, rem) |
|
|
||||||
|-------|------------|---------------------------|
|
|
||||||
| H1 | Seitentitel (nicht auf jeder Unterseite doppelt mit Logo-Text kollidieren) | `1.75–2rem` |
|
|
||||||
| H2 | Abschnitte | `1.35–1.5rem` |
|
|
||||||
| H3 | Unterabschnitte | `1.15–1.25rem` |
|
|
||||||
| Body | Fließtext | `1rem`, Zeilenlänge max. ca. 65–75 Zeichen |
|
|
||||||
| Klein | Meta, Fußnoten | `0.875rem` mit ausreichend Kontrast |
|
|
||||||
|
|
||||||
### 4.3 Regeln
|
|
||||||
|
|
||||||
- **Keine** reine Großschreibung für lange Menütexte.
|
|
||||||
- **Zeilenabstand** für Fließtext mindestens ca. 1,5.
|
|
||||||
- **Kontrast** von Überschriften und Text zu Hintergrund einhalten (WCAG 2.1 AA).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Layout und Raster
|
|
||||||
|
|
||||||
### 5.1 Container
|
|
||||||
|
|
||||||
- Maximalbreite für Lesbarkeit (z. B. `min(100%, 72rem)`) mit **symmetrischem Innenabstand**.
|
|
||||||
- **Keine** feste `min-width` im vierstelligen Pixelbereich ohne Scroll-Alternative; stattdessen **flexibles Grid** + sinnvolle Mindestbreiten nur dort, wo nötig (Tabellen).
|
|
||||||
|
|
||||||
### 5.2 Breakpoints (Orientierung)
|
|
||||||
|
|
||||||
- **Mobil:** < 640px – eine Spalte, Navigation als klares Overlay oder ausklappbare Liste mit großen Touch-Zielen.
|
|
||||||
- **Tablet:** 640–1024px – ggf. weiterhin eine Spalte oder kompakte Sidebar.
|
|
||||||
- **Desktop:** > 1024px – Zwei-Spalten-Layout optional; rechte Spalte für Bilder/Termine **nicht** zwingend über volle Höhe „eingesperrt“, wenn das inhaltlich sinnvoller ist.
|
|
||||||
|
|
||||||
### 5.3 Weißraum
|
|
||||||
|
|
||||||
- Einheitliches Spacing-System (z. B. Vielfache von 4px oder 0,25rem): Abstände zwischen Blöcken, in Karten, zwischen Formularfeldern.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Navigation und Bedienung
|
|
||||||
|
|
||||||
### 6.1 Hauptnavigation
|
|
||||||
|
|
||||||
- **Desktop:** horizontale Leiste mit Vollfarbe EKHN-Violett; aktiver Eintrag **deutlich** (Unterstreichung, Hintergrund oder starker Kontrast – weiterhin im Violett-System).
|
|
||||||
- **Touch:** Menüpunkte mindestens ca. **44×44 px** Klickfläche.
|
|
||||||
- **Untermenüs:** Hover für Maus; für Tastatur **Escape** schließt; **Fokus** sichtbar im gesamten Menübaum.
|
|
||||||
- **Mobile:** „Menü“-Button durch **Icon + Text** oder klares Label; Animationen **kurz** (< 200ms).
|
|
||||||
|
|
||||||
### 6.2 Orientierung
|
|
||||||
|
|
||||||
- Optional: **Brotkrumen** bei tieferen Seiten (Gemeindeleben → Gruppen → …), dezent unterhalb des Headers.
|
|
||||||
- Seitentitel konsistent: eine H1 pro Seite, semantisch korrekt.
|
|
||||||
|
|
||||||
### 6.3 Footer
|
|
||||||
|
|
||||||
- Impressum/Datenschutz bleiben gut auffindbar; gleiche visuelle Gewichtung wie bisher, mit verbessertem Kontrast und Fokus.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Barrierefreiheit (WCAG 2.1 Level AA als Ziel)
|
|
||||||
|
|
||||||
- **Kontrast:** Text auf Violett/Weiß/Grau messen (Tools: axe, Lighthouse, WebAIM Contrast Checker).
|
|
||||||
- **Tastatur:** alle interaktiven Elemente erreichbar; sichtbarer `:focus-visible`.
|
|
||||||
- **Screenreader:** Landmarks (`header`, `nav`, `main`, `footer`), Überschriftenhierarchie, `aria-expanded` für Untermenüs, sinnvolle `alt`-Texte bei Bildern.
|
|
||||||
- **Bewegung:** `prefers-reduced-motion` respektieren (Animationen abschwächen oder deaktivieren).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Komponentenbibliothek (schrittweise)
|
|
||||||
|
|
||||||
Einheitliche Bausteine reduzieren Streuung und erleichtern Wartung:
|
|
||||||
|
|
||||||
| Komponente | Anforderungen |
|
|
||||||
|------------|----------------|
|
|
||||||
| **Primärbutton** | Violett-Hintergrund, weißer Text, Hover, disabled-Zustand, Fokusring |
|
|
||||||
| **Sekundärbutton** | Outline in Violett oder neutral, gleiche Höhe wie Primär |
|
|
||||||
| **Karte** (Termine, News) | Klarer Titel, Datum, Link „weiterlesen“, ruhiger Schatten oder Rahmen |
|
|
||||||
| **Formularfelder** | Labels sichtbar, Fehler näch am Feld, keine rein farblichen Fehlerhinweise |
|
|
||||||
| **Tabellen** (Admin) | Zeilenwechsel, ausreichend Zellpadding, horizontales Scrollen auf schmalen Screens |
|
|
||||||
|
|
||||||
Technisch: zentrale **CSS-Variablen** + ggf. wiederverwendbare Vue-Komponenten oder Utility-Klassen – ohne das Projekt unnötig zu überfrachten.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Umsetzung in Phasen
|
|
||||||
|
|
||||||
### Phase 1 – Fundament (geringes Risiko, hoher Nutzen)
|
|
||||||
|
|
||||||
- Design-Tokens (Farben, Abstände, Schriftgrößen) in einer **globalen** Styleschicht.
|
|
||||||
- Entfernen/Ersetzen problematischer Layout-Regeln (`min-width` Hauptspalte), **Responsive** testen.
|
|
||||||
- Typografie-Skala und Basisabstände vereinheitlichen.
|
|
||||||
- Kontrast Footer/Links prüfen und anpassen.
|
|
||||||
|
|
||||||
### Phase 2 – Navigation & Chrome
|
|
||||||
|
|
||||||
- Navbar optisch verfeinern (Padding, aktive Zustände, Touch) bei **unverändertem** `#9400ff` / Hover.
|
|
||||||
- Header (Titelzeile): Schlagschatten/Lavendel **an Markensystem anbinden** oder reduzieren für seriösere Wirkung.
|
|
||||||
- Optional Brotkrumen für tiefe Seiten.
|
|
||||||
|
|
||||||
### Phase 3 – Inhaltsmodule
|
|
||||||
|
|
||||||
- Termine, Gottesdienste, Kontakt: Kartenlayout, konsistente Datumdarstellung (`date-fns` ist bereits im Projekt).
|
|
||||||
- Bilder: feste Aspect-Ratios oder `object-fit`, damit keine Layout-Sprünge entstehen.
|
|
||||||
|
|
||||||
### Phase 4 – Feinschliff
|
|
||||||
|
|
||||||
- Mikro-Interaktionen (Hover, Fokus) konsistent.
|
|
||||||
- Performance: Schriftarten, Bildgrößen, Lazy Loading wo sinnvoll.
|
|
||||||
- Finale Accessibility-Prüfung.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Erfolgskriterien (messbar / reviewbar)
|
|
||||||
|
|
||||||
- Keine horizontale Scrollbarkeit bei Standard-Viewports durch feste Mindestbreiten.
|
|
||||||
- Lighthouse-Accessibility-Score deutlich verbessert (Ziel: **≥ 90**, wo technisch möglich).
|
|
||||||
- **Manuelle** Tastatur- und Screenreader-Stichprobe auf Startseite, Navigation, einem Formular.
|
|
||||||
- **Visuelles Review** mit 2–3 Stakeholdern: „wirkt seriös, klar, kirchlich passend“ – **ohne** neue Hauptfarbe neben EKHN-Violett.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Explizite Nicht-Ziele
|
|
||||||
|
|
||||||
- Kein Rebranding und kein Ersatz der Primärfarbe.
|
|
||||||
- Kein „Gamification“-Design oder verspielte Illustrationen als Hauptstil.
|
|
||||||
- Keine Einführung schwerer UI-Frameworks nur wegen Optik, wenn das Projekt schlank bleiben soll (abwägen mit Wartbarkeit).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. Nächster Schritt
|
|
||||||
|
|
||||||
Umsetzung beginnt mit **Phase 1**: globale Tokens + Layout-Fixes + Kontrast. Dieses Dokument dient als **Referenz** für alle weiteren UI-Änderungen und sollte bei größeren Designentscheidungen aktualisiert werden.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Dokument erstellt als Arbeitsgrundlage für die Modernisierung; bei Abweichungen von offiziellen EKHN-CD-Vorgaben immer die gültige kirchliche Markenrichtlinie Vorrang haben.*
|
|
||||||
20
index.html
20
index.html
@@ -1,20 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
<title>Miriamgemeinde</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>
|
|
||||||
<strong>
|
|
||||||
Diese Website funktioniert leider nicht richtig, wenn JavaScript deaktiviert ist. Bitte aktivieren Sie
|
|
||||||
JavaScript, um fortzufahren.
|
|
||||||
</strong>
|
|
||||||
</noscript>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
/** @type {import('sequelize-cli').Migration} */
|
|
||||||
module.exports = {
|
|
||||||
async up(queryInterface, Sequelize) {
|
|
||||||
await queryInterface.createTable('worship_leaders', {
|
|
||||||
id: {
|
|
||||||
allowNull: false,
|
|
||||||
autoIncrement: true,
|
|
||||||
primaryKey: true,
|
|
||||||
type: Sequelize.INTEGER,
|
|
||||||
},
|
|
||||||
code: {
|
|
||||||
type: Sequelize.STRING(32),
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: Sequelize.STRING(255),
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
aliases: {
|
|
||||||
type: Sequelize.STRING(512),
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: '',
|
|
||||||
},
|
|
||||||
active: {
|
|
||||||
type: Sequelize.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: true,
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
allowNull: false,
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
allowNull: false,
|
|
||||||
type: Sequelize.DATE,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async down(queryInterface) {
|
|
||||||
await queryInterface.dropTable('worship_leaders');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
const { DataTypes } = require('sequelize');
|
|
||||||
|
|
||||||
module.exports = (sequelize) => {
|
|
||||||
const WorshipLeader = sequelize.define('WorshipLeader', {
|
|
||||||
code: {
|
|
||||||
type: DataTypes.STRING(32),
|
|
||||||
allowNull: false,
|
|
||||||
unique: true,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
aliases: {
|
|
||||||
// Comma-separated list of alternative codes (kept simple to avoid join tables).
|
|
||||||
type: DataTypes.STRING(512),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: '',
|
|
||||||
},
|
|
||||||
active: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: true,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
tableName: 'worship_leaders',
|
|
||||||
timestamps: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return WorshipLeader;
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -3,11 +3,20 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const Sequelize = require('sequelize');
|
const Sequelize = require('sequelize');
|
||||||
const sequelize = require('../config/database');
|
|
||||||
const basename = path.basename(__filename);
|
const basename = path.basename(__filename);
|
||||||
|
const env = process.env.NODE_ENV || 'development';
|
||||||
|
const config = require(__dirname + '/../config/config.json')[env];
|
||||||
const db = {};
|
const db = {};
|
||||||
|
|
||||||
fs.readdirSync(__dirname)
|
let sequelize;
|
||||||
|
if (config.use_env_variable) {
|
||||||
|
sequelize = new Sequelize(process.env[config.use_env_variable], config);
|
||||||
|
} else {
|
||||||
|
sequelize = new Sequelize(config.database, config.username, config.password, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs
|
||||||
|
.readdirSync(__dirname)
|
||||||
.filter(file => {
|
.filter(file => {
|
||||||
return (
|
return (
|
||||||
file.indexOf('.') !== 0 &&
|
file.indexOf('.') !== 0 &&
|
||||||
|
|||||||
32145
package-lock.json
generated
32145
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
76
package.json
76
package.json
@@ -3,81 +3,81 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vite build && npm run copy-dist",
|
"build": "vue-cli-service build && npm run copy-dist",
|
||||||
"preview": "vite preview",
|
|
||||||
"copy-dist": "cp -r dist/* public/",
|
"copy-dist": "cp -r dist/* public/",
|
||||||
"lint": "eslint src --ext .js,.vue",
|
"lint": "vue-cli-service lint"
|
||||||
"lint:all": "eslint . --ext .js,.vue"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconoir/vue": "^7.7.0",
|
"@iconoir/vue": "^7.7.0",
|
||||||
"@tiptap/extension-bold": "^3.22.2",
|
"@tiptap/extension-bold": "^2.4.0",
|
||||||
"@tiptap/extension-bullet-list": "^3.22.2",
|
"@tiptap/extension-bullet-list": "^2.4.0",
|
||||||
"@tiptap/extension-color": "^3.22.2",
|
"@tiptap/extension-color": "^2.4.0",
|
||||||
"@tiptap/extension-heading": "^3.22.2",
|
"@tiptap/extension-heading": "^2.4.0",
|
||||||
"@tiptap/extension-italic": "^3.22.2",
|
"@tiptap/extension-italic": "^2.4.0",
|
||||||
"@tiptap/extension-link": "^3.22.2",
|
"@tiptap/extension-link": "^2.4.0",
|
||||||
"@tiptap/extension-ordered-list": "^3.22.2",
|
"@tiptap/extension-ordered-list": "^2.4.0",
|
||||||
"@tiptap/extension-strike": "^3.22.2",
|
"@tiptap/extension-strike": "^2.4.0",
|
||||||
"@tiptap/extension-table": "^3.22.2",
|
"@tiptap/extension-table": "^2.4.0",
|
||||||
"@tiptap/extension-table-cell": "^3.22.2",
|
"@tiptap/extension-table-cell": "^2.4.0",
|
||||||
"@tiptap/extension-table-header": "^3.22.2",
|
"@tiptap/extension-table-header": "^2.4.0",
|
||||||
"@tiptap/extension-table-row": "^3.22.2",
|
"@tiptap/extension-table-row": "^2.4.0",
|
||||||
"@tiptap/extension-text-style": "^3.22.2",
|
"@tiptap/extension-text-style": "^2.4.0",
|
||||||
"@tiptap/extension-underline": "^3.22.2",
|
"@tiptap/extension-underline": "^2.4.0",
|
||||||
"@tiptap/starter-kit": "^3.22.2",
|
"@tiptap/starter-kit": "^2.4.0",
|
||||||
"@tiptap/vue-3": "^3.22.2",
|
"@tiptap/vue-3": "^2.4.0",
|
||||||
"@xmldom/xmldom": "^0.8.12",
|
"@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",
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"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": "^5.2.1",
|
"express": "^4.19.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jszip": "^3.10.1",
|
"moment": "^2.30.1",
|
||||||
"mammoth": "^1.11.0",
|
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql2": "^3.10.1",
|
"mysql2": "^3.10.1",
|
||||||
"nodemailer": "^7.0.6",
|
"nodemailer": "^7.0.6",
|
||||||
"pdf-parse": "^1.1.1",
|
"nodemon": "^3.1.3",
|
||||||
"sequelize": "^6.37.3",
|
"sequelize": "^6.37.3",
|
||||||
"sequelize-cli": "^6.6.2",
|
"sequelize-cli": "^6.6.2",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"vm-browserify": "^1.1.2",
|
"vm-browserify": "^1.1.2",
|
||||||
"vue": "^3.4.30",
|
"vue": "^3.4.30",
|
||||||
"vue-multiselect": "^3.0.0",
|
"vue-multiselect": "^3.0.0",
|
||||||
|
"vue-quill-editor": "^3.0.6",
|
||||||
"vue-router": "^4.3.3",
|
"vue-router": "^4.3.3",
|
||||||
"vuex": "^4.0.2"
|
"vuex": "^4.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^6.0.5",
|
"@babel/core": "^7.12.16",
|
||||||
"eslint": "^8.57.0",
|
"@babel/eslint-parser": "^7.12.16",
|
||||||
"eslint-plugin-vue": "^9.23.0",
|
"@vue/cli-plugin-babel": "~5.0.0",
|
||||||
"nodemon": "^3.1.3",
|
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||||
"vite": "^8.0.7",
|
"@vue/cli-service": "~5.0.0",
|
||||||
"vue-eslint-parser": "^9.4.3"
|
"crypto-browserify": "^3.12.0",
|
||||||
|
"eslint": "^7.32.0",
|
||||||
|
"eslint-plugin-vue": "^8.0.3",
|
||||||
|
"os-browserify": "^0.3.0",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
|
"stream-browserify": "^3.0.0",
|
||||||
|
"webpack": "^5.92.0"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true,
|
"root": true,
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
|
||||||
"es2021": true,
|
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:vue/vue3-essential",
|
"plugin:vue/vue3-essential",
|
||||||
"eslint:recommended"
|
"eslint:recommended"
|
||||||
],
|
],
|
||||||
"parser": "vue-eslint-parser",
|
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2022,
|
"parser": "@babel/eslint-parser"
|
||||||
"sourceType": "module"
|
|
||||||
},
|
},
|
||||||
"rules": {}
|
"rules": {}
|
||||||
},
|
},
|
||||||
|
|||||||
1
public/css/1099.a6cdcdf4.css
Normal file
1
public/css/1099.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/1258.13c489b0.css
Normal file
1
public/css/1258.13c489b0.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.register[data-v-4256bbfa]{max-width:400px;margin:auto}form[data-v-4256bbfa]{display:flex;flex-direction:column}label[data-v-4256bbfa]{margin-top:10px}button[data-v-4256bbfa]{margin-top:20px}
|
||||||
1
public/css/1565.5391dc21.css
Normal file
1
public/css/1565.5391dc21.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.privacy-policy[data-v-0c3320ea]{max-width:800px;margin:auto;padding:20px}h1[data-v-0c3320ea],h2[data-v-0c3320ea],h3[data-v-0c3320ea],h4[data-v-0c3320ea],h5[data-v-0c3320ea]{margin-top:20px;color:#333}p[data-v-0c3320ea]{line-height:1.6}ul[data-v-0c3320ea]{margin:10px 0;padding-left:20px}ul li[data-v-0c3320ea]{list-style-type:disc}a[data-v-0c3320ea]{color:#007bff;text-decoration:none}a[data-v-0c3320ea]:hover{text-decoration:underline}
|
||||||
1
public/css/1969.a6cdcdf4.css
Normal file
1
public/css/1969.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/2177.54e852c0.css
Normal file
1
public/css/2177.54e852c0.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}.previewinfo[data-v-9a71cbf6]{background-color:#000;color:#d00000;position:absolute;top:93px;left:0;padding:2px 10px;font-weight:700}
|
||||||
1
public/css/2353.9504c97b.css
Normal file
1
public/css/2353.9504c97b.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.upload-files[data-v-f2694614]{width:100%;margin:auto}.upload-files div[data-v-f2694614]{margin-bottom:10px}.file-list[data-v-f2694614]{list-style-type:none;padding:0;margin-top:20px}.file-list li[data-v-f2694614]{border-bottom:1px solid #ddd;padding:10px 0}.file-info[data-v-f2694614]{display:flex;justify-content:space-between;cursor:pointer}.file-title[data-v-f2694614]{font-weight:700}.file-name[data-v-f2694614]{color:#555}.file-date[data-v-f2694614]{color:#888}
|
||||||
1
public/css/2463.a6cdcdf4.css
Normal file
1
public/css/2463.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/2607.a6cdcdf4.css
Normal file
1
public/css/2607.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/2809.b682d961.css
Normal file
1
public/css/2809.b682d961.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.right-column[data-v-12df4c86]{background-color:#d9e2f3}.right-column h2[data-v-12df4c86]{text-align:center;color:#000}.right-column img[data-v-12df4c86]{display:block;margin:0 auto;max-width:100%;height:auto}
|
||||||
1
public/css/3214.a6cdcdf4.css
Normal file
1
public/css/3214.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/3299.5760daa0.css
Normal file
1
public/css/3299.5760daa0.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.menu-management[data-v-0e6a0522]{width:100%;margin:auto}.button-container[data-v-0e6a0522]{display:inline-flex;gap:10px;margin-bottom:20px}.tree-view[data-v-0e6a0522]{margin-top:20px}.tree-view ul[data-v-0e6a0522]{list-style-type:none;padding:0}.tree-view li[data-v-0e6a0522]{margin-bottom:5px;padding-left:20px}.tree-view .menu-item[data-v-0e6a0522]{display:inline-flex;width:100%;justify-content:space-between;align-items:center}.tree-view span[data-v-0e6a0522]{cursor:pointer;color:#000}.tree-view button[data-v-0e6a0522]{border:none;height:1.6em;padding:0 .5em;margin:1px;border-radius:5px}.tree-view span[data-v-0e6a0522]:hover{text-decoration:underline}.edit-form[data-v-0e6a0522]{margin-top:20px}.edit-form label[data-v-0e6a0522]{display:block;margin-bottom:5px;font-weight:700}.edit-form input[data-v-0e6a0522]:not([type=checkbox]){display:block;margin-bottom:10px}.edit-form .checkbox-container[data-v-0e6a0522]{display:flex;flex-direction:column;margin-right:10px}.edit-form .order-id[data-v-0e6a0522]{width:50px}.edit-form button[data-v-0e6a0522]{margin-top:5px}
|
||||||
1
public/css/3353.bdb3d500.css
Normal file
1
public/css/3353.bdb3d500.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.position-management[data-v-1684a375]{max-width:600px;margin:auto;padding:20px;border:1px solid #ccc;border-radius:5px}form[data-v-1684a375]{display:flex;flex-direction:column;margin-bottom:20px}label[data-v-1684a375]{margin-top:10px}input[data-v-1684a375]{margin-top:5px;margin-bottom:10px;padding:8px}button[data-v-1684a375]{margin-top:10px;padding:10px}table[data-v-1684a375]{width:100%;border-collapse:collapse;margin-top:20px}td[data-v-1684a375],th[data-v-1684a375]{border:1px solid #ccc;padding:10px;text-align:left}th[data-v-1684a375]{background-color:#f4f4f4}
|
||||||
1
public/css/3459.f3d026c5.css
Normal file
1
public/css/3459.f3d026c5.css
Normal file
File diff suppressed because one or more lines are too long
1
public/css/3484.32da9cb7.css
Normal file
1
public/css/3484.32da9cb7.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.dialog-overlay[data-v-21ade8c0]{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);display:flex;justify-content:center;align-items:center}.dialog[data-v-21ade8c0]{background:#fff;padding:20px;border-radius:5px;max-width:400px;width:100%;text-align:center}button[data-v-21ade8c0]{margin-top:20px}.login[data-v-40a158c0]{max-width:400px;margin:auto}form[data-v-40a158c0]{display:flex;flex-direction:column}label[data-v-40a158c0]{margin-top:10px}button[data-v-40a158c0]{margin-top:20px}
|
||||||
1
public/css/3550.a6cdcdf4.css
Normal file
1
public/css/3550.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/3715.a6cdcdf4.css
Normal file
1
public/css/3715.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/4039.be11324e.css
Normal file
1
public/css/4039.be11324e.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
div[data-v-334e7b82]{padding:20px}
|
||||||
1
public/css/466.a6cdcdf4.css
Normal file
1
public/css/466.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/4765.4bd58cd7.css
Normal file
1
public/css/4765.4bd58cd7.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.institution-management[data-v-ff992c44]{max-width:600px;margin:auto;padding:20px;border:1px solid #ccc;border-radius:5px}form[data-v-ff992c44]{display:flex;flex-direction:column;margin-bottom:20px}label[data-v-ff992c44]{margin-top:10px}input[data-v-ff992c44]{margin-top:5px;margin-bottom:10px;padding:8px}button[data-v-ff992c44]{margin-top:10px;padding:10px}table[data-v-ff992c44]{width:100%;border-collapse:collapse;margin-top:20px}td[data-v-ff992c44],th[data-v-ff992c44]{border:1px solid #ccc;padding:10px;text-align:left}th[data-v-ff992c44]{background-color:#f4f4f4}
|
||||||
1
public/css/480.78894bf8.css
Normal file
1
public/css/480.78894bf8.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
div[data-v-68b32234]{padding:20px}ul[data-v-68b32234]{list-style:none;padding:0;margin:0}li[data-v-68b32234]{padding:0;margin:0}
|
||||||
1
public/css/4908.1e896a7d.css
Normal file
1
public/css/4908.1e896a7d.css
Normal file
File diff suppressed because one or more lines are too long
1
public/css/5108.a6cdcdf4.css
Normal file
1
public/css/5108.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/5625.7ef4d708.css
Normal file
1
public/css/5625.7ef4d708.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.register[data-v-182f8976]{max-width:400px;margin:auto}form[data-v-182f8976]{display:flex;flex-direction:column}label[data-v-182f8976]{margin-top:10px}button[data-v-182f8976]{margin-top:20px}
|
||||||
1
public/css/5697.a6cdcdf4.css
Normal file
1
public/css/5697.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/6007.9e55a8df.css
Normal file
1
public/css/6007.9e55a8df.css
Normal file
File diff suppressed because one or more lines are too long
1
public/css/6107.a6cdcdf4.css
Normal file
1
public/css/6107.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/6959.d2acc39d.css
Normal file
1
public/css/6959.d2acc39d.css
Normal file
File diff suppressed because one or more lines are too long
1
public/css/7158.a6cdcdf4.css
Normal file
1
public/css/7158.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/7361.a6cdcdf4.css
Normal file
1
public/css/7361.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/7382.49bff9d4.css
Normal file
1
public/css/7382.49bff9d4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.dialog-overlay[data-v-21ade8c0]{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);display:flex;justify-content:center;align-items:center}.dialog[data-v-21ade8c0]{background:#fff;padding:20px;border-radius:5px;max-width:400px;width:100%;text-align:center}button[data-v-21ade8c0]{margin-top:20px}
|
||||||
1
public/css/757.5e87db7f.css
Normal file
1
public/css/757.5e87db7f.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
form div[data-v-454efcad]{margin-bottom:10px}.uploaded-image[data-v-454efcad]{display:inline-block;margin:0 0 .5em .5em;border:1px solid #e0e0e0;padding:10px}.uploaded-image input[data-v-454efcad],.uploaded-image textarea[data-v-454efcad]{width:100%;margin:5px 0}
|
||||||
1
public/css/7876.5b86c09c.css
Normal file
1
public/css/7876.5b86c09c.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.forgot-password[data-v-017249f5]{max-width:400px;margin:auto}form[data-v-017249f5]{display:flex;flex-direction:column}label[data-v-017249f5]{margin-top:10px}button[data-v-017249f5]{margin-top:20px}
|
||||||
1
public/css/8060.a6cdcdf4.css
Normal file
1
public/css/8060.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/8074.3a6e7316.css
Normal file
1
public/css/8074.3a6e7316.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.event-places-management[data-v-920e5ffc]{max-width:600px;margin:auto;padding:20px;border:1px solid #ccc;border-radius:5px}form[data-v-920e5ffc]{display:flex;flex-direction:column;margin-bottom:20px}label[data-v-920e5ffc]{margin-top:10px}input[data-v-920e5ffc]{margin-top:5px;margin-bottom:10px;padding:8px}button[data-v-920e5ffc]{margin-top:10px;padding:10px}table[data-v-920e5ffc]{width:100%;border-collapse:collapse;margin-top:20px}td[data-v-920e5ffc],th[data-v-920e5ffc]{border:1px solid #ccc;padding:10px;text-align:left}th[data-v-920e5ffc]{background-color:#f4f4f4}
|
||||||
1
public/css/8183.43cc4f81.css
Normal file
1
public/css/8183.43cc4f81.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.user-administration[data-v-a495c756]{padding:20px}.user-administration h1[data-v-a495c756],.user-administration h2[data-v-a495c756]{margin-bottom:20px}.user-administration form[data-v-a495c756]{display:flex;flex-direction:column;margin-bottom:20px}.user-administration label[data-v-a495c756]{margin-top:10px}.user-administration input[type=email][data-v-a495c756],.user-administration input[type=password][data-v-a495c756],.user-administration input[type=text][data-v-a495c756]{padding:5px;font-size:16px}.user-administration ul[data-v-a495c756]{list-style-type:none;padding:0}.user-administration li[data-v-a495c756]{padding:10px;border-bottom:1px solid #ddd;cursor:pointer}.user-administration li[data-v-a495c756]:hover{background-color:#f0f0f0}
|
||||||
1
public/css/830.a6cdcdf4.css
Normal file
1
public/css/830.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/8712.4386b7e7.css
Normal file
1
public/css/8712.4386b7e7.css
Normal file
File diff suppressed because one or more lines are too long
1
public/css/878.596e7cea.css
Normal file
1
public/css/878.596e7cea.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.impressum[data-v-612786fa]{max-width:800px;margin:auto;padding:20px}h1[data-v-612786fa],h2[data-v-612786fa],h3[data-v-612786fa],h4[data-v-612786fa]{margin-top:20px;color:#333}p[data-v-612786fa]{line-height:1.6}a[data-v-612786fa]{color:#007bff;text-decoration:none}a[data-v-612786fa]:hover{text-decoration:underline}
|
||||||
1
public/css/8848.a6cdcdf4.css
Normal file
1
public/css/8848.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/9238.a6cdcdf4.css
Normal file
1
public/css/9238.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/9524.a6cdcdf4.css
Normal file
1
public/css/9524.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/9738.a6cdcdf4.css
Normal file
1
public/css/9738.a6cdcdf4.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
table.worships[data-v-0a1907cc]{border-collapse:collapse;width:100%}table.worships td[data-v-0a1907cc]{border:1px solid #000;text-align:center}h3[data-v-0a1907cc]{margin:0}table.worships td div[data-v-0a1907cc]{margin:5px}.highlight-time[data-v-0a1907cc]{text-decoration:underline}.neighborhood-invitation[data-v-0a1907cc]{font-weight:700;color:#0020e0}.image[data-v-9b711a1e]{max-width:400px;max-height:300px}.event-name[data-v-2da2977b]{font-weight:700}.event-table[data-v-2da2977b]{border-collapse:collapse}.event-table td[data-v-2da2977b]{border:1px solid #000}.homepage[data-v-2da2977b]{border:1px solid #9400ff;padding:.5em;text-align:center}.description[data-v-2da2977b]{padding:.5em 0}.contact-box p[data-v-40357d31]{margin:0}span[data-v-fa4cadae]{cursor:pointer;color:blue;text-decoration:underline}
|
||||||
1
public/css/app.c2c4030a.css
Normal file
1
public/css/app.c2c4030a.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.navbar[data-v-7c384298]{background-color:#9400ff;overflow:visible;height:47px;display:flex;width:100%}.navbar ul[data-v-7c384298]{list-style-type:none;margin:0;padding:0;display:flex}.navbar li[data-v-7c384298]{position:relative}.navbar a[data-v-7c384298],.navbar li>span[data-v-7c384298]{display:block;color:#fff;text-align:center;padding:14px 20px;text-decoration:none;font-weight:700}.navbar a[data-v-7c384298]:hover{background-color:#7a00d1}.menu-icon[data-v-7c384298]{width:20px;height:20px;margin-right:5px}.dropdown-content[data-v-7c384298]{position:absolute;background-color:#9400ff;min-width:200px;z-index:1;top:100%;left:0;opacity:0;visibility:hidden;transition:opacity .2s ease-in-out,visibility .2s ease-in-out;box-shadow:2px 2px 4px #666}.dropdown-content a[data-v-7c384298]{color:#fff;padding:12px 16px;text-decoration:none;display:block;text-align:left}.dropdown-content a[data-v-7c384298]:hover{background-color:#7a00d1}.navbar li:hover .dropdown-content[data-v-7c384298]{opacity:1;visibility:visible}.fade-enter-active[data-v-7c384298],.fade-leave-active[data-v-7c384298]{transition:opacity .2s ease-in-out,visibility .2s ease-in-out}.fade-enter[data-v-7c384298],.fade-leave-to[data-v-7c384298]{opacity:0;visibility:hidden}header[data-v-0a99f72e]{display:flex;flex-direction:column;width:100%;background-color:#e0bfff}.header-title[data-v-0a99f72e]{display:flex;align-items:center;justify-content:space-between;width:100%;padding:.3em .5em}header h1[data-v-0a99f72e]{margin:0}.reload-icon[data-v-0a99f72e]{font-size:16px;cursor:pointer;margin-left:10px;background-color:#e0bfff;color:#fff;padding:5px;border-radius:50%}.reload-icon[data-v-0a99f72e]:hover{color:#7a00d1}.footer[data-v-cc31ea16]{background-color:#0b1735;bottom:0;left:0;width:100%;padding:7px;justify-content:space-between}.footer[data-v-cc31ea16],.left-links[data-v-cc31ea16],.right-links[data-v-cc31ea16]{display:flex;align-items:center}.footer a[data-v-cc31ea16]{color:#fff;padding-right:20px;text-decoration:none}.footer a.login-link[data-v-cc31ea16]{color:#444}.footer a.logout-link[data-v-cc31ea16]{cursor:pointer}body,html{height:100%;margin:0;padding:0;background-color:#fff;font-family:Arial,sans-serif;width:100%;overflow:hidden}#app{display:flex;flex-direction:column;height:100%}.content-section{flex:1;display:flex;color:#000;overflow:hidden}.left-column,.right-column{flex:1;overflow-y:auto}.left-column{margin:.5em 0 .5em .5em;padding-right:.5em;background-color:#fff}.right-column{background-color:#d9e2f3}.right-column h2{text-align:center;color:#000}.right-column img{display:block;margin:0 auto;max-width:100%;height:auto}@media (max-width:768px){.content-section{flex-direction:column}.left-column,.right-column{padding:10px}}.htmleditor{background-color:#fff;width:calc(100% - 26px);height:31em;border:1px solid #000;margin:7px;padding:5px;overflow:auto}.htmleditor table{border:1px solid #e0e0e0;border-collapse:collapse}.htmleditor td,.htmleditor th{border:1px solid #e0e0e0}
|
||||||
BIN
public/images/homepage2.png
Normal file
BIN
public/images/homepage2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/images/uploads/0cf0624e-8394-49fc-a44d-3d2f4d876061.jpg
Normal file
BIN
public/images/uploads/0cf0624e-8394-49fc-a44d-3d2f4d876061.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 356 KiB |
BIN
public/images/uploads/3cb841cb-6764-4c27-b745-62efb91adb06.jpg
Normal file
BIN
public/images/uploads/3cb841cb-6764-4c27-b745-62efb91adb06.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 848 KiB |
BIN
public/images/uploads/44390f72-e407-4796-8cc7-a5253dae4ebd.jpg
Normal file
BIN
public/images/uploads/44390f72-e407-4796-8cc7-a5253dae4ebd.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
public/images/uploads/59b29477-6997-4d3c-9010-54ba7c0a7ba3.jpg
Normal file
BIN
public/images/uploads/59b29477-6997-4d3c-9010-54ba7c0a7ba3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
1
public/index.html
Normal file
1
public/index.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>miriamgemeinde</title><script defer="defer" src="/js/chunk-vendors.a58901d9.js"></script><script defer="defer" src="/js/app.2b3ac443.js"></script><link href="/css/app.c2c4030a.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors.a58901d9.js"></script><script defer="defer" src="/js/app.62331f73.js"></script><link href="/css/app.c2c4030a.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors.a58901d9.js"></script><script defer="defer" src="/js/app.f7f58406.js"></script><link href="/css/app.c2c4030a.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but miriamgemeinde doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
||||||
"extends": [
|
|
||||||
"config:recommended"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { getAllPages, uploadImage, uploadImages, saveImageDetails, saveImageDetailsBulk, getImages, getImagesByPage, getImageById, getImageByHash, updateImage } = require('../controllers/imageController');
|
const { getAllPages, uploadImage, saveImageDetails, getImages, getImagesByPage, getImageById, getImageByHash, updateImage } = require('../controllers/imageController');
|
||||||
const authMiddleware = require('../middleware/authMiddleware')
|
const authMiddleware = require('../middleware/authMiddleware')
|
||||||
|
|
||||||
router.post('/', authMiddleware, uploadImage, saveImageDetails);
|
router.post('/', authMiddleware, uploadImage, saveImageDetails);
|
||||||
router.post('/bulk', authMiddleware, uploadImages, saveImageDetailsBulk);
|
|
||||||
router.get('/', authMiddleware, getImages);
|
router.get('/', authMiddleware, getImages);
|
||||||
router.get('/page/:pageId', getImagesByPage);
|
router.get('/page/:pageId', getImagesByPage);
|
||||||
router.put('/hash/:id', getImageByHash);
|
router.put('/hash/:id', getImageByHash);
|
||||||
|
|||||||
@@ -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,17 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const router = express.Router();
|
|
||||||
const authMiddleware = require('../middleware/authMiddleware');
|
|
||||||
const {
|
|
||||||
getAllWorshipLeaders,
|
|
||||||
createWorshipLeader,
|
|
||||||
updateWorshipLeader,
|
|
||||||
deleteWorshipLeader,
|
|
||||||
} = require('../controllers/worshipLeaderController');
|
|
||||||
|
|
||||||
router.get('/', authMiddleware, getAllWorshipLeaders);
|
|
||||||
router.post('/', authMiddleware, createWorshipLeader);
|
|
||||||
router.put('/:id', authMiddleware, updateWorshipLeader);
|
|
||||||
router.delete('/:id', authMiddleware, deleteWorshipLeader);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
|
|
||||||
@@ -1,19 +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, importWorshipsNbrCsv, importWorshipsNbrPlanning, uploadImportFile, exportWorships, saveImportedWorships, importNewsletterPdf } = 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/nbr-planning', authMiddleware, uploadImportFile, importWorshipsNbrPlanning);
|
|
||||||
router.post('/import/nbr-csv', authMiddleware, uploadImportFile, importWorshipsNbrCsv);
|
|
||||||
router.post('/import/newsletter-pdf', authMiddleware, uploadImportFile, importNewsletterPdf);
|
|
||||||
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;
|
||||||
|
|||||||
99
server.js
99
server.js
@@ -1,13 +1,8 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const bodyParser = require('body-parser');
|
const bodyParser = require('body-parser');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const http = require('http');
|
const https = require('https');
|
||||||
require('dotenv').config();
|
const fs = require('fs');
|
||||||
|
|
||||||
// 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');
|
||||||
@@ -18,85 +13,16 @@ const institutionRouter = require('./routes/institutions');
|
|||||||
const eventRouter = require('./routes/event');
|
const eventRouter = require('./routes/event');
|
||||||
const menuDataRouter = require('./routes/menuData');
|
const menuDataRouter = require('./routes/menuData');
|
||||||
const worshipRouter = require('./routes/worships');
|
const worshipRouter = require('./routes/worships');
|
||||||
const worshipLeadersRouter = require('./routes/worshipLeaders');
|
|
||||||
const pageRouter = require('./routes/pages');
|
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.BACKEND_PORT || 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']
|
|
||||||
}));
|
|
||||||
// Express 5 nutzt path-to-regexp v6; '*' ist dort als Pattern ungueltig.
|
|
||||||
app.options('/{*any}', 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);
|
||||||
@@ -107,15 +33,22 @@ app.use('/api/institutions', institutionRouter);
|
|||||||
app.use('/api/events', eventRouter);
|
app.use('/api/events', eventRouter);
|
||||||
app.use('/api/menu-data', menuDataRouter);
|
app.use('/api/menu-data', menuDataRouter);
|
||||||
app.use('/api/worships', worshipRouter);
|
app.use('/api/worships', worshipRouter);
|
||||||
app.use('/api/worship-leaders', worshipLeadersRouter);
|
|
||||||
app.use('/api/page-content', pageRouter);
|
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 = {
|
||||||
|
key: fs.readFileSync('server.key'),
|
||||||
|
cert: fs.readFileSync('server.cert'),
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
sequelize.sync().then(() => {
|
sequelize.sync().then(() => {
|
||||||
app.listen(PORT, '0.0.0.0', () => {
|
/* https.createServer(options, app).listen(PORT, () => {
|
||||||
console.log(`Server läuft auf Port ${PORT} (IPv4 und IPv6)`);
|
console.log(`Server läuft auf Port ${PORT}`);
|
||||||
|
});*/
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
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`);
|
|
||||||
|
|
||||||
@@ -62,36 +62,49 @@ export default {
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/* Layout: globale Typografie/Farben in design-tokens.css */
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #ffffff;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden; /* Prevent horizontal scrolling */
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.content-section {
|
.content-section {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
color: var(--color-text);
|
color: #000;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-column {
|
.left-column {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 1000px;
|
||||||
max-width: 100%;
|
margin: 0.5em 0 0.5em 0.5em;
|
||||||
margin: var(--space-2) 0 var(--space-2) var(--space-2);
|
padding-right: 0.5em;
|
||||||
padding-right: var(--space-2);
|
background-color: #ffffff;
|
||||||
background-color: var(--color-bg-page);
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-column {
|
.right-column {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
background-color: #d9e2f3;
|
||||||
background-color: var(--color-bg-sidebar);
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
margin: 0 var(--space-2) var(--space-2) 0;
|
margin: 0 7px 7px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-column h2 {
|
.right-column h2 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--color-text);
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-column img {
|
.right-column img {
|
||||||
@@ -104,8 +117,8 @@ export default {
|
|||||||
.right-column-overlay {
|
.right-column-overlay {
|
||||||
max-height: 150px;
|
max-height: 150px;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
margin-top: var(--space-3);
|
margin-top: 10px;
|
||||||
background-color: var(--color-bg-sidebar);
|
background-color: #d9e2f3;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -123,9 +136,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.left-column {
|
.left-column {
|
||||||
padding: var(--space-3);
|
padding: 10px;
|
||||||
margin-left: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-column {
|
.right-column {
|
||||||
@@ -135,7 +146,7 @@ export default {
|
|||||||
.right-column-overlay {
|
.right-column-overlay {
|
||||||
display: flex;
|
display: flex;
|
||||||
max-height: 150px;
|
max-height: 150px;
|
||||||
background-color: var(--color-bg-page);
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-column-overlay img {
|
.right-column-overlay img {
|
||||||
@@ -152,7 +163,7 @@ export default {
|
|||||||
|
|
||||||
.left-column,
|
.left-column,
|
||||||
.right-column {
|
.right-column {
|
||||||
padding: var(--space-3);
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-column {
|
.right-column {
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
/**
|
|
||||||
* Phase 3 – Inhaltsmodule als Karten (ruhig, seriös, wiederverwendbar).
|
|
||||||
*/
|
|
||||||
.mg-stack {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mg-card {
|
|
||||||
background: var(--color-bg-page);
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mg-card:hover {
|
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.07);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mg-card--highlight {
|
|
||||||
border-color: var(--color-brand-primary);
|
|
||||||
box-shadow: 0 0 0 1px rgba(148, 0, 255, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mg-card__grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 160px 1fr;
|
|
||||||
gap: var(--space-4);
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.mg-card__grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mg-media {
|
|
||||||
width: 100%;
|
|
||||||
aspect-ratio: 4 / 3;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--color-bg-subtle);
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mg-media > img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: block;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mg-title {
|
|
||||||
margin: 0 0 var(--space-2) 0;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mg-meta {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: var(--space-2) var(--space-4);
|
|
||||||
margin: 0 0 var(--space-2) 0;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mg-meta strong {
|
|
||||||
color: var(--color-text);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mg-text {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mg-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-1);
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(255, 255, 255, 0.18);
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
color: var(--color-text);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mg-accent-left {
|
|
||||||
border-left: 6px solid rgba(0, 0, 0, 0.12);
|
|
||||||
padding-left: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
/**
|
|
||||||
* Design-Tokens Phase 1 – Farben (EKHN-Violett unverändert), Abstände, Typografie.
|
|
||||||
* Globale Basistyles für html/body/#app.
|
|
||||||
*/
|
|
||||||
:root {
|
|
||||||
/* Marke EKHN – Primärfarben fix */
|
|
||||||
--color-brand-primary: #9400ff;
|
|
||||||
--color-brand-primary-hover: #7a00d1;
|
|
||||||
--color-brand-tint: #e0bfff;
|
|
||||||
|
|
||||||
/* Neutrale Flächen & Text */
|
|
||||||
--color-bg-page: #ffffff;
|
|
||||||
--color-bg-subtle: #f8f9fb;
|
|
||||||
--color-bg-sidebar: #d9e2f3;
|
|
||||||
--color-text: #1a1a1a;
|
|
||||||
--color-text-muted: #4a4a4a;
|
|
||||||
|
|
||||||
/* Footer (dunkler Grund) */
|
|
||||||
--color-footer-bg: #0b1735;
|
|
||||||
--color-footer-link: #e8edf5;
|
|
||||||
--color-footer-link-hover: #ffffff;
|
|
||||||
|
|
||||||
/* Abstände (4px-Raster) */
|
|
||||||
--space-1: 0.25rem;
|
|
||||||
--space-2: 0.5rem;
|
|
||||||
--space-3: 0.75rem;
|
|
||||||
--space-4: 1rem;
|
|
||||||
--space-5: 1.25rem;
|
|
||||||
--space-6: 1.5rem;
|
|
||||||
--space-8: 2rem;
|
|
||||||
|
|
||||||
/* Typografie */
|
|
||||||
--font-family-base: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial,
|
|
||||||
sans-serif;
|
|
||||||
--font-size-body: 1rem;
|
|
||||||
--line-height-body: 1.5;
|
|
||||||
--font-size-h1: clamp(1.25rem, 2.5vw, 1.75rem);
|
|
||||||
--font-size-h2: clamp(1.15rem, 2vw, 1.35rem);
|
|
||||||
--font-size-h3: clamp(1.05rem, 1.5vw, 1.2rem);
|
|
||||||
|
|
||||||
--shadow-dropdown: 2px 2px 6px rgba(0, 0, 0, 0.22);
|
|
||||||
--transition-fast: 140ms ease;
|
|
||||||
--transition-standard: 220ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: var(--color-bg-page);
|
|
||||||
color: var(--color-text);
|
|
||||||
font-family: var(--font-family-base);
|
|
||||||
font-size: var(--font-size-body);
|
|
||||||
line-height: var(--line-height-body);
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sanfte Standard-Hierarchie; Komponenten können gezielt überschreiben */
|
|
||||||
:where(h1) {
|
|
||||||
font-size: var(--font-size-h1);
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(h2) {
|
|
||||||
font-size: var(--font-size-h2);
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(h3) {
|
|
||||||
font-size: var(--font-size-h3);
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
:focus-visible {
|
|
||||||
outline: 2px solid var(--color-brand-primary);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a,
|
|
||||||
button,
|
|
||||||
[role="button"] {
|
|
||||||
transition:
|
|
||||||
color var(--transition-fast),
|
|
||||||
background-color var(--transition-fast),
|
|
||||||
border-color var(--transition-fast),
|
|
||||||
box-shadow var(--transition-standard),
|
|
||||||
transform var(--transition-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Schutz vor List-Styles in vue-multiselect-Dropdowns */
|
|
||||||
.multiselect__content-wrapper ul,
|
|
||||||
.multiselect__content {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiselect__content li,
|
|
||||||
.multiselect__element {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
animation-duration: 0.01ms !important;
|
|
||||||
animation-iteration-count: 1 !important;
|
|
||||||
transition-duration: 0.01ms !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
68
src/axios.js
68
src/axios.js
@@ -1,51 +1,15 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import store from './store';
|
||||||
|
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);
|
|
||||||
|
|
||||||
function clearStoredLogin() {
|
|
||||||
localStorage.removeItem('isLoggedIn');
|
|
||||||
localStorage.removeItem('user');
|
|
||||||
localStorage.removeItem('token');
|
|
||||||
delete axios.defaults.headers.common.Authorization;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTokenPayload(token) {
|
|
||||||
try {
|
|
||||||
const payload = token.split('.')[1];
|
|
||||||
const normalized = payload.replace(/-/g, '+').replace(/_/g, '/');
|
|
||||||
const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, '=');
|
|
||||||
return JSON.parse(atob(padded));
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTokenUsable(token) {
|
|
||||||
if (!token || token === 'undefined' || token === 'null') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const payload = getTokenPayload(token);
|
|
||||||
if (!payload || !payload.exp) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return payload.exp * 1000 > Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
axios.interceptors.request.use(
|
axios.interceptors.request.use(
|
||||||
config => {
|
config => {
|
||||||
const token = localStorage.getItem('token');
|
const token = store.state.token;
|
||||||
config.headers = config.headers || {};
|
|
||||||
if (token) {
|
if (token) {
|
||||||
if (isTokenUsable(token)) {
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
|
||||||
} else {
|
|
||||||
clearStoredLogin();
|
|
||||||
delete config.headers.Authorization;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
@@ -59,23 +23,9 @@ axios.interceptors.response.use(
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
const requestUrl = error.config?.url || '';
|
if (error.response && error.response.status === 401) {
|
||||||
const isLoginRequest = requestUrl.includes('/auth/login');
|
store.dispatch('logout');
|
||||||
const isLogoutRequest = requestUrl.includes('/auth/logout');
|
router.push('/auth/login');
|
||||||
const hasStoredToken = isTokenUsable(localStorage.getItem('token'));
|
|
||||||
const hadAuthHeader = !!error.config?.headers?.Authorization;
|
|
||||||
|
|
||||||
if (
|
|
||||||
error.response &&
|
|
||||||
error.response.status === 401 &&
|
|
||||||
!isLoginRequest &&
|
|
||||||
!isLogoutRequest &&
|
|
||||||
(hasStoredToken || hadAuthHeader)
|
|
||||||
) {
|
|
||||||
clearStoredLogin();
|
|
||||||
if (window.location.pathname !== '/auth/login') {
|
|
||||||
window.location.replace('/auth/login');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
<template>
|
|
||||||
<nav v-if="crumbs.length" class="breadcrumbs" aria-label="Brotkrumen">
|
|
||||||
<ol>
|
|
||||||
<li v-for="(c, idx) in crumbs" :key="c.to + '-' + idx">
|
|
||||||
<router-link v-if="idx < crumbs.length - 1" :to="c.to">{{ c.label }}</router-link>
|
|
||||||
<span v-else aria-current="page">{{ c.label }}</span>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
import { useStore } from 'vuex';
|
|
||||||
|
|
||||||
function findLabel(menuItems, link) {
|
|
||||||
for (const item of menuItems) {
|
|
||||||
if (item?.link === link) {
|
|
||||||
return item.pageTitle || item.name || '';
|
|
||||||
}
|
|
||||||
if (item?.submenu?.length) {
|
|
||||||
const found = findLabel(item.submenu, link);
|
|
||||||
if (found) return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'BreadcrumbsComponent',
|
|
||||||
setup() {
|
|
||||||
const route = useRoute();
|
|
||||||
const store = useStore();
|
|
||||||
|
|
||||||
const crumbs = computed(() => {
|
|
||||||
const path = route.path || '/';
|
|
||||||
const list = [{ label: 'Startseite', to: '/' }];
|
|
||||||
|
|
||||||
if (path === '/') return [];
|
|
||||||
|
|
||||||
const label = findLabel(store.state.menuData || [], path);
|
|
||||||
list.push({ label: label || 'Seite', to: path });
|
|
||||||
return list;
|
|
||||||
});
|
|
||||||
|
|
||||||
return { crumbs };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.breadcrumbs {
|
|
||||||
margin: 0 var(--space-3) var(--space-2) var(--space-3);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumbs ol {
|
|
||||||
list-style: none;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: var(--space-2);
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumbs li {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumbs li:not(:last-child)::after {
|
|
||||||
content: '›';
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumbs a {
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumbs a:hover,
|
|
||||||
.breadcrumbs a:focus-visible {
|
|
||||||
color: var(--color-text);
|
|
||||||
text-decoration: underline;
|
|
||||||
text-underline-offset: 2px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user