Compare commits
36 Commits
backend-ov
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f5fb83e9a | |||
| f15598b2c1 | |||
| 979732d545 | |||
| d9e0b07f0c | |||
| b5ac5df38d | |||
| c982f34823 | |||
|
|
c390228ed9 | ||
|
|
9c924071f4 | ||
|
|
2d1ed43312 | ||
|
|
11cf961ed8 | ||
|
|
cd9e645941 | ||
|
|
7bd236aa82 | ||
|
|
f96e0a1f22 | ||
|
|
45190ed7a5 | ||
|
|
79f2ca8382 | ||
|
|
550ed97a11 | ||
|
|
718bcabea3 | ||
|
|
44c978f21e | ||
|
|
6c54bc9d49 | ||
|
|
d4fab1ceb3 | ||
|
|
ead4dbdd3f | ||
|
|
b18c911500 | ||
|
|
5e4471a936 | ||
|
|
459dd3168a | ||
|
|
3af7089e06 | ||
|
|
156d89a45d | ||
|
|
bf0b7f1dad | ||
|
|
6de8cac0bc | ||
|
|
ecd03d29f4 | ||
|
|
0238fffd3d | ||
|
|
fd84112cef | ||
|
|
3303c749e2 | ||
|
|
9cadaf3f51 | ||
|
|
5d32c46e38 | ||
|
|
7e6128dec4 | ||
|
|
cff48550ae |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -27,3 +27,5 @@ server.key
|
||||
server.cert
|
||||
|
||||
public/images/uploads/1ba24ea7-f52c-4179-896f-1909269cab58.jpg
|
||||
actualize.sh
|
||||
files/uploads/GD 24.08.2025-04.01.2026 Stand 12.08.2025.docx
|
||||
|
||||
@@ -2,4 +2,4 @@ module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
{
|
||||
"development": {
|
||||
"username": "miriam_user",
|
||||
"password": "qTCTTWwpEwy3vPDU",
|
||||
"username": "miriamgemeinde",
|
||||
"password": "hitomisan",
|
||||
"database": "miriamgemeinde",
|
||||
"host": "tsschulz.de",
|
||||
"host": "localhost",
|
||||
"dialect": "mysql"
|
||||
},
|
||||
"test": {
|
||||
"username": "miriam_user",
|
||||
"password": "qTCTTWwpEwy3vPDU",
|
||||
"password": "hitomisan",
|
||||
"database": "miriamgemeinde",
|
||||
"host": "tsschulz.de",
|
||||
"dialect": "mysql"
|
||||
},
|
||||
"production": {
|
||||
"username": "miriam_user",
|
||||
"password": "qTCTTWwpEwy3vPDU",
|
||||
"password": "hitomisan",
|
||||
"database": "miriamgemeinde",
|
||||
"host": "tsschulz.de",
|
||||
"dialect": "mysql"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const { Sequelize } = require('sequelize');
|
||||
|
||||
const sequelize = new Sequelize('miriamgemeinde', 'miriam_user', 'qTCTTWwpEwy3vPDU', {
|
||||
host: 'tsschulz.de',
|
||||
const sequelize = new Sequelize('miriamgemeinde', 'miriamgemeinde', 'hitomisan', {
|
||||
host: 'localhost',
|
||||
dialect: 'mysql',
|
||||
retry: {
|
||||
match: [
|
||||
@@ -26,7 +26,7 @@ const sequelize = new Sequelize('miriamgemeinde', 'miriam_user', 'qTCTTWwpEwy3vP
|
||||
async function connectWithRetry() {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('Connection has been established successfully.');
|
||||
console.log(`Connection has been established successfully. Database server: ${sequelize.config.host}`);
|
||||
} catch (error) {
|
||||
console.error('Unable to connect to the database:', error);
|
||||
setTimeout(connectWithRetry, 5000);
|
||||
|
||||
@@ -3,7 +3,16 @@ const { Op } = require('sequelize');
|
||||
|
||||
const getAllContactPersons = async (req, res) => {
|
||||
try {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const contactPersons = await ContactPerson.findAll({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ expiryDate: null },
|
||||
{ expiryDate: { [Op.gte]: today } }
|
||||
]
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Position,
|
||||
@@ -79,6 +88,14 @@ const filterContactPersons = async (req, res) => {
|
||||
const where = {};
|
||||
const having = [];
|
||||
|
||||
// Filter für nicht abgelaufene Kontaktpersonen
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
where[Op.or] = [
|
||||
{ expiryDate: null },
|
||||
{ expiryDate: { [Op.gte]: today } }
|
||||
];
|
||||
|
||||
if (config.selection.id && config.selection.id === 'all') {
|
||||
// No additional filter needed for "all"
|
||||
} else if (config.selection.id) {
|
||||
|
||||
154
controllers/liturgicalDayController.js
Normal file
154
controllers/liturgicalDayController.js
Normal file
@@ -0,0 +1,154 @@
|
||||
const { LiturgicalDay } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
const axios = require('axios');
|
||||
|
||||
// Alle liturgischen Tage abrufen
|
||||
const getAllLiturgicalDays = async (req, res) => {
|
||||
try {
|
||||
const days = await LiturgicalDay.findAll({
|
||||
order: [['date', 'ASC']]
|
||||
});
|
||||
res.status(200).json(days);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ message: 'Fehler beim Abrufen der liturgischen Tage' });
|
||||
}
|
||||
};
|
||||
|
||||
// Eindeutige Namen für Multiselect abrufen
|
||||
const getLiturgicalDayNames = async (req, res) => {
|
||||
try {
|
||||
const days = await LiturgicalDay.findAll({
|
||||
attributes: ['dayName'],
|
||||
group: ['dayName'],
|
||||
order: [['dayName', 'ASC']]
|
||||
});
|
||||
const names = days.map(day => day.dayName);
|
||||
res.status(200).json(names);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ message: 'Fehler beim Abrufen der Tag-Namen' });
|
||||
}
|
||||
};
|
||||
|
||||
// HTML von liturgischem Kalender parsen und in DB speichern
|
||||
const loadLiturgicalYear = async (req, res) => {
|
||||
const { year } = req.body;
|
||||
|
||||
if (!year) {
|
||||
return res.status(400).json({ message: 'Jahr ist erforderlich' });
|
||||
}
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
if (year < currentYear || year > currentYear + 2) {
|
||||
return res.status(400).json({ message: 'Jahr muss zwischen aktuellem Jahr und 2 Jahren in der Zukunft liegen' });
|
||||
}
|
||||
|
||||
try {
|
||||
const url = `https://www.eike-fleer.de/liturgischer-kalender/${year}.htm`;
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
}
|
||||
});
|
||||
|
||||
const html = response.data;
|
||||
|
||||
// Parse HTML - suche nach Tabellenzeilen mit Datum und Name
|
||||
// Format: "DD.MM.YYYY 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
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
28
migrations/20251007150137-create-liturgical-days.js
Normal file
28
migrations/20251007150137-create-liturgical-days.js
Normal file
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up (queryInterface, Sequelize) {
|
||||
await queryInterface.createTable('liturgical_days', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false
|
||||
},
|
||||
date: {
|
||||
type: Sequelize.DATEONLY,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
dayName: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async down (queryInterface, Sequelize) {
|
||||
await queryInterface.dropTable('liturgical_days');
|
||||
}
|
||||
};
|
||||
16
migrations/20251122134324-add-orgelspiel-to-worships.js
Normal file
16
migrations/20251122134324-add-orgelspiel-to-worships.js
Normal file
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('worships', 'organ_playing', {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('worships', 'organ_playing');
|
||||
}
|
||||
};
|
||||
|
||||
17
migrations/20251122140000-add-freigegeben-to-worships.js
Normal file
17
migrations/20251122140000-add-freigegeben-to-worships.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('worships', 'approved', {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('worships', 'approved');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,6 +25,10 @@ module.exports = (sequelize) => {
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
},
|
||||
expiryDate: {
|
||||
type: DataTypes.DATEONLY,
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
tableName: 'contact_persons',
|
||||
|
||||
24
models/LiturgicalDay.js
Normal file
24
models/LiturgicalDay.js
Normal file
@@ -0,0 +1,24 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const LiturgicalDay = sequelize.define('LiturgicalDay', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
date: {
|
||||
type: DataTypes.DATEONLY,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
dayName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'liturgical_days',
|
||||
timestamps: false
|
||||
});
|
||||
|
||||
return LiturgicalDay;
|
||||
};
|
||||
|
||||
@@ -61,6 +61,17 @@ module.exports = (sequelize) => {
|
||||
allowNull: true,
|
||||
field: 'sacristan_service'
|
||||
},
|
||||
organPlaying: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
field: 'organ_playing'
|
||||
},
|
||||
approved: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: false,
|
||||
field: 'approved'
|
||||
},
|
||||
}, {
|
||||
tableName: 'worships',
|
||||
timestamps: true
|
||||
|
||||
26043
package-lock.json
generated
26043
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@
|
||||
"@tiptap/extension-underline": "^2.4.0",
|
||||
"@tiptap/starter-kit": "^2.4.0",
|
||||
"@tiptap/vue-3": "^2.4.0",
|
||||
"@vue/cli": "^5.0.8",
|
||||
"@vue/cli": "^4.2.2",
|
||||
"axios": "^1.7.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.20.2",
|
||||
@@ -34,10 +34,12 @@
|
||||
"cors": "^2.8.5",
|
||||
"crypto": "^1.0.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"docx": "^9.5.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mammoth": "^1.11.0",
|
||||
"moment": "^2.30.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.10.1",
|
||||
|
||||
6
renovate.json
Normal file
6
renovate.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
]
|
||||
}
|
||||
19
routes/liturgicalDays.js
Normal file
19
routes/liturgicalDays.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const {
|
||||
getAllLiturgicalDays,
|
||||
getLiturgicalDayNames,
|
||||
loadLiturgicalYear,
|
||||
createLiturgicalDay,
|
||||
deleteLiturgicalDay
|
||||
} = require('../controllers/liturgicalDayController');
|
||||
const authMiddleware = require('../middleware/authMiddleware');
|
||||
|
||||
router.get('/', getAllLiturgicalDays);
|
||||
router.get('/names', getLiturgicalDayNames);
|
||||
router.post('/load-year', authMiddleware, loadLiturgicalYear);
|
||||
router.post('/', authMiddleware, createLiturgicalDay);
|
||||
router.delete('/:id', authMiddleware, deleteLiturgicalDay);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getAllWorships, createWorship, updateWorship, deleteWorship, getFilteredWorships } = require('../controllers/worshipController');
|
||||
const { getAllWorships, createWorship, updateWorship, deleteWorship, getFilteredWorships, getWorshipOptions, importWorships, uploadImportFile, exportWorships, saveImportedWorships } = require('../controllers/worshipController');
|
||||
const authMiddleware = require('../middleware/authMiddleware');
|
||||
|
||||
router.get('/', getAllWorships);
|
||||
router.get('/options', getWorshipOptions);
|
||||
router.post('/', authMiddleware, createWorship);
|
||||
router.post('/import', authMiddleware, uploadImportFile, importWorships);
|
||||
router.post('/import/save', authMiddleware, saveImportedWorships);
|
||||
router.put('/:id', authMiddleware, updateWorship);
|
||||
router.delete('/:id', authMiddleware, deleteWorship);
|
||||
router.get('/filtered', getFilteredWorships);
|
||||
router.get('/export', authMiddleware, exportWorships);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
68
server.js
68
server.js
@@ -2,8 +2,14 @@ const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const cors = require('cors');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
require('dotenv').config();
|
||||
|
||||
// Erhöhe maxHttpHeaderSize für Node.js (Standard ist 8KB, erhöhe auf 16KB)
|
||||
if (http.maxHeaderSize !== undefined) {
|
||||
http.maxHeaderSize = 16384;
|
||||
}
|
||||
const sequelize = require('./config/database');
|
||||
const authRouter = require('./routes/auth');
|
||||
const eventTypesRouter = require('./routes/eventtypes');
|
||||
@@ -18,6 +24,7 @@ const pageRouter = require('./routes/pages');
|
||||
const userRouter = require('./routes/users');
|
||||
const imageRouter = require('./routes/image');
|
||||
const filesRouter = require('./routes/files');
|
||||
const liturgicalDaysRouter = require('./routes/liturgicalDays');
|
||||
|
||||
const app = express();
|
||||
const PORT = parseInt(process.env.PORT, 10) || 3000;
|
||||
@@ -30,9 +37,50 @@ const allowedOrigins = (process.env.ALLOWED_ORIGINS || '')
|
||||
|
||||
app.use(cors({
|
||||
origin: (origin, callback) => {
|
||||
if (!origin) return callback(null, true); // z.B. Healthchecks/curl/Server-zu-Server
|
||||
if (allowedOrigins.length === 0) return callback(null, true); // Fallback: alles erlauben
|
||||
if (allowedOrigins.includes(origin)) return callback(null, true);
|
||||
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,
|
||||
@@ -41,7 +89,14 @@ app.use(cors({
|
||||
}));
|
||||
app.options('*', cors());
|
||||
|
||||
app.use(bodyParser.json());
|
||||
// Erhöhe Header-Limits für große Requests
|
||||
app.use(bodyParser.json({ limit: '50mb' }));
|
||||
app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' }));
|
||||
|
||||
// Erhöhe maxHttpHeaderSize (Node.js 18.3.0+)
|
||||
if (process.versions.node.split('.')[0] >= 18) {
|
||||
require('http').maxHeaderSize = 16384; // 16KB (Standard ist 8KB)
|
||||
}
|
||||
|
||||
app.use('/api/auth', authRouter);
|
||||
app.use('/api/event-types', eventTypesRouter);
|
||||
@@ -56,6 +111,7 @@ app.use('/api/page-content', pageRouter);
|
||||
app.use('/api/users', userRouter);
|
||||
app.use('/api/image', imageRouter);
|
||||
app.use('/api/files', filesRouter);
|
||||
app.use('/api/liturgical-days', liturgicalDaysRouter);
|
||||
|
||||
const options = {
|
||||
key: fs.readFileSync('server.key'),
|
||||
@@ -67,7 +123,7 @@ sequelize.sync().then(() => {
|
||||
/* https.createServer(options, app).listen(PORT, () => {
|
||||
console.log(`Server läuft auf Port ${PORT}`);
|
||||
});*/
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server läuft auf Port ${PORT}`);
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Server läuft auf Port ${PORT} (IPv4 und IPv6)`);
|
||||
});
|
||||
});
|
||||
|
||||
4
sql/add_expiry_date_to_contact_persons.sql
Normal file
4
sql/add_expiry_date_to_contact_persons.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- Ablaufdatum zu Kontaktpersonen hinzufügen
|
||||
ALTER TABLE `contact_persons`
|
||||
ADD COLUMN `expiryDate` DATE NULL AFTER `email`;
|
||||
|
||||
12
sql/create_liturgical_days.sql
Normal file
12
sql/create_liturgical_days.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
-- Tabelle für liturgische Kalendertage erstellen
|
||||
CREATE TABLE IF NOT EXISTS `liturgical_days` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`date` DATE NOT NULL,
|
||||
`dayName` VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `unique_date` (`date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Index für schnellere Suche nach dayName
|
||||
CREATE INDEX `idx_dayName` ON `liturgical_days` (`dayName`);
|
||||
|
||||
14
src/axios.js
14
src/axios.js
@@ -2,8 +2,11 @@ import axios from 'axios';
|
||||
import store from './store';
|
||||
import router from './router';
|
||||
|
||||
axios.defaults.baseURL = process.env.VUE_APP_BACKEND_URL;
|
||||
console.log(process.env.VUE_APP_BACKEND_URL);
|
||||
// Einheitliche Basis-URL:
|
||||
// - immer relativ zur aktuellen Origin
|
||||
// - kein absoluter http/https-Host → verhindert Mixed-Content-Probleme
|
||||
axios.defaults.baseURL = '/api';
|
||||
console.log('Axios baseURL:', axios.defaults.baseURL);
|
||||
|
||||
axios.interceptors.request.use(
|
||||
config => {
|
||||
@@ -24,8 +27,11 @@ axios.interceptors.response.use(
|
||||
},
|
||||
error => {
|
||||
if (error.response && error.response.status === 401) {
|
||||
store.dispatch('logout');
|
||||
router.push('/auth/login');
|
||||
store.dispatch('logout').then(() => {
|
||||
if (router.currentRoute.value.path !== '/auth/login') {
|
||||
router.replace('/auth/login');
|
||||
}
|
||||
});
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
@@ -36,27 +36,53 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.dialog-overlay {
|
||||
top: calc(50% - 25em);
|
||||
left: 5%;
|
||||
width: 90%;
|
||||
height: 50em;
|
||||
background: rgba(0, 0, 0, .5);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
button {
|
||||
.dialog h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.dialog p {
|
||||
margin: 15px 0;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.dialog button {
|
||||
margin-top: 20px;
|
||||
padding: 10px 20px;
|
||||
background-color: #007BFF;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dialog button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@
|
||||
<footer class="footer">
|
||||
<div class="left-links">
|
||||
<router-link class="login-link" to="/auth/login" v-if="!isLoggedIn">Login</router-link>
|
||||
<a v-if="isLoggedIn" @click="logout" class="logout-link">Logout</a>
|
||||
<a v-if="isLoggedIn" @click="handleLogout" class="logout-link">Logout</a>
|
||||
</div>
|
||||
<div class="right-links">
|
||||
<router-link to="/terms">Impressum</router-link>
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" id="email" v-model="localContactPerson.email">
|
||||
|
||||
<label for="expiryDate">Ablaufdatum (optional):</label>
|
||||
<input type="date" id="expiryDate" v-model="localContactPerson.expiryDate">
|
||||
|
||||
<label for="positions">Positionen:</label>
|
||||
<multiselect
|
||||
v-model="selectedPositions"
|
||||
@@ -57,6 +60,7 @@ export default {
|
||||
zipcode: '',
|
||||
city: '',
|
||||
email: '',
|
||||
expiryDate: null,
|
||||
positions: []
|
||||
})
|
||||
},
|
||||
@@ -111,6 +115,7 @@ export default {
|
||||
zipcode: '',
|
||||
city: '',
|
||||
email: '',
|
||||
expiryDate: null,
|
||||
positions: []
|
||||
};
|
||||
this.selectedPositions = [];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-if="config && config.style === 'box' && contacts && contacts.length && contacts.length > 0">
|
||||
<div v-for="contact in contacts" :key="contact.id" class="contact-box bottom-margin">
|
||||
<p>{{ contact.name }}</p>
|
||||
<p>{{ contact.name }} <span v-if="contact.expiryDate" class="expiry-date">(bis {{ formatDate(contact.expiryDate) }})</span></p>
|
||||
<p v-if="displayOptions.includes('phone')">Telefon: {{ contact.phone }}</p>
|
||||
<p v-if="displayOptions.includes('street')">Straße: {{ contact.street }}</p>
|
||||
<p v-if="displayOptions.includes('zipcode')">Postleitzahl: {{ contact.zipcode }}</p>
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
<span v-else-if="config.style === 'float' && contacts && contacts.length && contacts.length > 0">
|
||||
<span v-for="contact in contacts" :key="contact.id" class="bottom-margin">
|
||||
{{ contact.name }}
|
||||
{{ contact.name }}<span v-if="contact.expiryDate" class="expiry-date"> (bis {{ formatDate(contact.expiryDate) }})</span>
|
||||
<span v-if="displayOptions.includes('phone')">, Telefon: {{ contact.phone }}</span>
|
||||
<span v-if="displayOptions.includes('street')">, Straße: {{ contact.street }}</span>
|
||||
<span v-if="displayOptions.includes('zipcode')">, Postleitzahl: {{ contact.zipcode }}</span>
|
||||
@@ -58,6 +58,17 @@ export default {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -68,4 +79,9 @@ export default {
|
||||
.bottom-margin {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.expiry-date {
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
@@ -312,6 +312,15 @@ export default {
|
||||
this.assignedImage = null;
|
||||
this.imageFilename = '';
|
||||
},
|
||||
focusFirstField() {
|
||||
// Fokussiert das erste Eingabefeld (Name)
|
||||
this.$nextTick(() => {
|
||||
const nameInput = document.getElementById('name');
|
||||
if (nameInput) {
|
||||
nameInput.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
formatTime(event.endTime) }}</span> Uhr</div>
|
||||
<div v-if="shouldDisplay('place')">{{ event.eventPlace?.name }}</div>
|
||||
<div v-if="shouldDisplay('description')" class="description">{{ event.description }}</div>
|
||||
<div v-if="shouldDisplay('contactPerson')">{{event.contactPersons.map(cp => cp.name).join(', ')}}
|
||||
<div v-if="shouldDisplay('contactPerson')">{{event.contactPersons.map(cp => formatContactPerson(cp)).join(', ')}}
|
||||
</div>
|
||||
<div v-if="shouldDisplay('institution')">{{ event.institution?.name }}</div>
|
||||
<div v-if="shouldDisplay('type')">{{ event.eventType?.caption }}</div>
|
||||
@@ -28,7 +28,7 @@
|
||||
formatTime(events[0].endTime) }}</span> Uhr</div>
|
||||
<div v-if="shouldDisplay('place')">{{ events[0].eventPlace?.name }}</div>
|
||||
<div v-if="shouldDisplay('description')" class="description">{{ events[0].description }}</div>
|
||||
<div v-if="shouldDisplay('contactPerson')">{{events[0].contactPersons.map(cp => cp.name).join(', ')}}
|
||||
<div v-if="shouldDisplay('contactPerson')">{{events[0].contactPersons.map(cp => formatContactPerson(cp)).join(', ')}}
|
||||
</div>
|
||||
<div v-if="shouldDisplay('institution')">{{ events[0].institution?.name }}</div>
|
||||
<div v-if="shouldDisplay('type')">{{ events[0].eventType?.caption }}</div>
|
||||
@@ -102,6 +102,18 @@ export default {
|
||||
const path = '/images/uploads/' + response.data.filename;
|
||||
console.log(path);
|
||||
return path;
|
||||
},
|
||||
formatContactPerson(contactPerson) {
|
||||
if (!contactPerson.expiryDate) {
|
||||
return contactPerson.name;
|
||||
}
|
||||
const date = new Date(contactPerson.expiryDate);
|
||||
const formattedDate = date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
return `${contactPerson.name} (bis ${formattedDate})`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,21 +11,35 @@
|
||||
<div v-if="worship.neighborInvitation" class="neighborhood-invitation">Einladung zum Gottesdienst im
|
||||
Nachbarschaftsraum:</div>
|
||||
<h3>
|
||||
<span :class="worship.highlightTime ? 'highlight-time' : ''">{{ formatTime(worship.time)
|
||||
}}</span> -
|
||||
{{ worship.title ? worship.title : `Gottesdienst in ${worship.eventPlace.name}` }}
|
||||
<span
|
||||
:class="worship.highlightTime ? 'highlight-time' : ''"
|
||||
>{{ formatTime(worship.time) }}</span> -
|
||||
{{
|
||||
worship.title
|
||||
? worship.title
|
||||
: (worship.eventPlace && worship.eventPlace.name
|
||||
? `Gottesdienst in ${worship.eventPlace.name}`
|
||||
: 'Gottesdienst')
|
||||
}}
|
||||
</h3>
|
||||
<div v-if="worship.organizer">Gestaltung: {{ worship.organizer }}</div>
|
||||
<div v-if="worship.sacristanService" class="internal-information">Küsterdienst: {{ worship.sacristanService }}</div>
|
||||
<div v-if="worship.collection">Kollekte: {{ worship.collection }}</div>
|
||||
<div v-if="worship.organPlaying" class="internal-information">Orgelspiel: {{ worship.organPlaying }}</div>
|
||||
<div v-if="worship.address">{{ worship.address }}</div>
|
||||
<div v-if="!worship.address && worship.eventPlace.id && worship.eventPlace.id">
|
||||
Adresse: {{ worship.eventPlace.name }}, {{ worship.eventPlace.street }}, {{
|
||||
worship.eventPlace.city }}
|
||||
<div
|
||||
v-if="!worship.address && worship.eventPlace && worship.eventPlace.id"
|
||||
>
|
||||
Adresse: {{ worship.eventPlace.name }}, {{ worship.eventPlace.street }}, {{ worship.eventPlace.city }}
|
||||
</div>
|
||||
<div v-if="worship.selfInformation" class="selfinformation">Bitte informieren Sie sich auch auf den
|
||||
<a v-if="worship.eventPlace.website" :href="worship.eventPlace.website" target="_blank">Internetseiten dieser Gemeinde!</a><span
|
||||
v-else>Internetseiten dieser Gemeinde!</span>
|
||||
<div v-if="worship.selfInformation" class="selfinformation">
|
||||
Bitte informieren Sie sich auch auf den
|
||||
<a
|
||||
v-if="worship.eventPlace && worship.eventPlace.website"
|
||||
:href="worship.eventPlace.website"
|
||||
target="_blank"
|
||||
>Internetseiten dieser Gemeinde!</a>
|
||||
<span v-else>Internetseiten dieser Gemeinde!</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -33,6 +33,7 @@ export default {
|
||||
zipcode: '',
|
||||
city: '',
|
||||
email: '',
|
||||
expiryDate: null,
|
||||
positions: []
|
||||
},
|
||||
positions: []
|
||||
|
||||
@@ -8,29 +8,29 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<button @click="toggleHeading(3)">H3</button>
|
||||
<button @click="toggleHeading(4)">H4</button>
|
||||
<button @click="toggleHeading(5)">H5</button>
|
||||
<button @click="toggleHeading(6)">H6</button>
|
||||
<button @click="toggleBold()" width="24" height="24">
|
||||
<button @click="toggleHeading(3)" :class="{ 'is-active': editor && editor.isActive('heading', { level: 3 }) }">H3</button>
|
||||
<button @click="toggleHeading(4)" :class="{ 'is-active': editor && editor.isActive('heading', { level: 4 }) }">H4</button>
|
||||
<button @click="toggleHeading(5)" :class="{ 'is-active': editor && editor.isActive('heading', { level: 5 }) }">H5</button>
|
||||
<button @click="toggleHeading(6)" :class="{ 'is-active': editor && editor.isActive('heading', { level: 6 }) }">H6</button>
|
||||
<button @click="toggleBold()" :class="{ 'is-active': editor && editor.isActive('bold') }" width="24" height="24">
|
||||
<BoldIcon width="24" height="24" />
|
||||
</button>
|
||||
<button @click="toggleItalic()">
|
||||
<button @click="toggleItalic()" :class="{ 'is-active': editor && editor.isActive('italic') }">
|
||||
<ItalicIcon width="24" height="24" />
|
||||
</button>
|
||||
<button @click="toggleUnderline()">
|
||||
<button @click="toggleUnderline()" :class="{ 'is-active': editor && editor.isActive('underline') }">
|
||||
<UnderlineIcon width="24" height="24" />
|
||||
</button>
|
||||
<button @click="toggleStrike()">
|
||||
<button @click="toggleStrike()" :class="{ 'is-active': editor && editor.isActive('strike') }">
|
||||
<StrikethroughIcon width="24" height="24" />
|
||||
</button>
|
||||
<button @click="insertTable()">
|
||||
<TableIcon width="24" height="24" />
|
||||
</button>
|
||||
<button @click="toggleBulletList()">
|
||||
<button @click="toggleBulletList()" :class="{ 'is-active': editor && editor.isActive('bulletList') }">
|
||||
<ListIcon width="24" height="24" />
|
||||
</button>
|
||||
<button @click="toggleOrderedList()">
|
||||
<button @click="toggleOrderedList()" :class="{ 'is-active': editor && editor.isActive('orderedList') }">
|
||||
<NumberedListLeftIcon width="24" height="24" />
|
||||
</button>
|
||||
<button @click="openAddImageDialog">
|
||||
@@ -511,4 +511,15 @@ export default {
|
||||
.align-top {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.toolbar button.is-active,
|
||||
.table-toolbar button.is-active {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toolbar button.is-active svg,
|
||||
.table-toolbar button.is-active svg {
|
||||
fill: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,12 +3,30 @@
|
||||
<h2>Veranstaltungen</h2>
|
||||
<button @click="createEvent">Neue Veranstaltung</button>
|
||||
<EventForm v-if="showForm"
|
||||
ref="eventForm"
|
||||
:event="selectedEvent"
|
||||
:institutions="institutions"
|
||||
:eventPlaces="eventPlaces"
|
||||
:contactPersons="contactPersons"
|
||||
@saved="handleEventSaved"
|
||||
@cancelled="handleEventCancelled" />
|
||||
|
||||
<div class="filter-section">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Suche nach Name, Typ, Beschreibung..."
|
||||
class="search-input"
|
||||
/>
|
||||
<label class="checkbox-label">
|
||||
<input
|
||||
v-model="showPastEvents"
|
||||
type="checkbox"
|
||||
/>
|
||||
Vergangene Events anzeigen
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -22,7 +40,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="event in events" :key="event.id">
|
||||
<tr v-for="event in filteredEvents" :key="event.id">
|
||||
<td>{{ event.name }}</td>
|
||||
<td>{{ getEventTypeCaption(event.eventTypeId) }}</td>
|
||||
<td>{{ event.date }}</td>
|
||||
@@ -55,8 +73,52 @@ export default {
|
||||
eventTypes: [],
|
||||
selectedEvent: null,
|
||||
showForm: false,
|
||||
searchQuery: '',
|
||||
showPastEvents: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredEvents() {
|
||||
let filtered = this.events;
|
||||
|
||||
// Filter vergangene Events aus
|
||||
if (!this.showPastEvents) {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
filtered = filtered.filter(event => {
|
||||
// Events mit Wochentag (ohne festes Datum) immer anzeigen
|
||||
if (event.dayOfWeek !== null && event.dayOfWeek !== undefined && !event.date) {
|
||||
return true;
|
||||
}
|
||||
// Events mit Datum: nur zukünftige oder heutige anzeigen
|
||||
if (event.date) {
|
||||
const eventDate = new Date(event.date);
|
||||
eventDate.setHours(0, 0, 0, 0);
|
||||
return eventDate >= today;
|
||||
}
|
||||
// Events ohne Datum und ohne Wochentag anzeigen
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Suchfilter anwenden
|
||||
if (this.searchQuery.trim()) {
|
||||
const query = this.searchQuery.toLowerCase();
|
||||
filtered = filtered.filter(event => {
|
||||
const name = event.name ? event.name.toLowerCase() : '';
|
||||
const description = event.description ? event.description.toLowerCase() : '';
|
||||
const eventType = this.getEventTypeCaption(event.eventTypeId).toLowerCase();
|
||||
|
||||
return name.includes(query) ||
|
||||
description.includes(query) ||
|
||||
eventType.includes(query);
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await this.fetchData();
|
||||
},
|
||||
@@ -84,10 +146,24 @@ export default {
|
||||
createEvent() {
|
||||
this.selectedEvent = {};
|
||||
this.showForm = true;
|
||||
this.scrollToFormAndFocus();
|
||||
},
|
||||
editEvent(event) {
|
||||
this.selectedEvent = { ...event };
|
||||
this.showForm = true;
|
||||
this.scrollToFormAndFocus();
|
||||
},
|
||||
scrollToFormAndFocus() {
|
||||
// Wartet auf das Rendern des Formulars und scrollt dann nach oben
|
||||
this.$nextTick(() => {
|
||||
// Nach oben scrollen
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
|
||||
// Das erste Feld fokussieren
|
||||
if (this.$refs.eventForm) {
|
||||
this.$refs.eventForm.focusFirstField();
|
||||
}
|
||||
});
|
||||
},
|
||||
async deleteEvent(id) {
|
||||
try {
|
||||
@@ -110,7 +186,7 @@ export default {
|
||||
},
|
||||
getWeekdayName(dayOfWeek) {
|
||||
const weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
|
||||
return weekdays[dayOfWeek - 1];
|
||||
return weekdays[dayOfWeek];
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -122,6 +198,40 @@ export default {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: #4CAF50;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,14 +2,14 @@
|
||||
<div class="privacy-policy">
|
||||
<h1>Datenschutzerklärung der Miriamgemeinde Frankfurt am Main</h1>
|
||||
<p>
|
||||
Die Miriamgemeinde Frankfurt am Main nimmt den Schutz Ihrer persönlichen Daten sehr ernst und behandelt Ihre personenbezogenen Daten vertraulich und entsprechend der kirchlichen Datenschutzgesetze sowie dieser Datenschutzerklärung. Die Sicherheit Ihrer Daten steht für uns an erster Stelle.
|
||||
Die Miriamgemeinde Frankfurt am Main nimmt den Schutz Ihrer persönlichen Daten sehr ernst und behandelt Ihre personenbezogenen Daten vertraulich und entsprechend dem Datenschutzgesetz der Evangelischen Kirche in Deutschland (DSG-EKD) sowie dieser Datenschutzerklärung. Die Sicherheit Ihrer Daten steht für uns an erster Stelle.
|
||||
</p>
|
||||
<h2>Anbieter:</h2>
|
||||
<p>
|
||||
Miriamgemeinde Frankfurt am Main, Gemeindebüro Bonames<br />
|
||||
Kirchhofsweg 5, 60437 Frankfurt, Tel.: 50 14 17, Fax: 50 93 0148,<br />
|
||||
Email: <a href="mailto:Ev.Kirche-Bonames@t-online.de">Ev.Kirche-Bonames@t-online.de</a><br />
|
||||
Inhaltlich Verantwortlicher gemäß § 6 MDStV: Torsten Schulz
|
||||
Inhaltlich Verantwortlicher gemäß § 6 DDG: Torsten Schulz
|
||||
</p>
|
||||
<p>
|
||||
Die Nutzung der Webseite der Miriamgemeinde Frankfurt am Main ist in der Regel ohne Angabe personenbezogener Daten möglich. Soweit auf unseren Seiten personenbezogene Daten (beispielsweise Name, Anschrift oder E-Mail-Adressen) erhoben werden, erfolgt dies, soweit möglich, stets auf freiwilliger Basis. Diese Daten werden ohne Ihre ausdrückliche Zustimmung nicht an Dritte weitergegeben. Die nachfolgende Erklärung gibt Ihnen einen Überblick darüber, wie dieser Schutz gewährleistet werden soll und welche Art von Daten zu welchem Zweck von Ihnen erhoben werden.
|
||||
@@ -63,11 +63,23 @@
|
||||
</p>
|
||||
<h3>Ihr Recht auf Auskunft, Löschung, Sperrung</h3>
|
||||
<p>
|
||||
Sie haben als Nutzer das Recht, Auskunft darüber zu verlangen, welche Daten über Sie bei uns gespeichert sind und zu welchem Zweck diese Speicherung erfolgt. Darüber hinaus können Sie unrichtige Daten berichtigen oder solche Daten löschen lassen, deren Speicherung unzulässig oder nicht mehr erforderlich ist. Sie haben die Rechte auf Datenübertragbarkeit, Einschränkung der Verarbeitung und Widerspruch. Außerdem haben Sie das Recht, sich bei der Aufsichtsbehörde über die stattfindende Datenverarbeitung zu beschweren. Zuständige Aufsichtsbehörde ist Der Beauftragte für den Datenschutz der EKD – Adresse siehe unten.
|
||||
Sie haben als Nutzer nach dem Datenschutzgesetz der Evangelischen Kirche in Deutschland (DSG-EKD) folgende Rechte:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Recht auf Auskunft:</strong> Sie haben das Recht, Auskunft darüber zu verlangen, welche Daten über Sie bei uns gespeichert sind und zu welchem Zweck diese Speicherung erfolgt.</li>
|
||||
<li><strong>Recht auf Berichtigung:</strong> Sie können unrichtige oder unvollständige Daten berichtigen lassen.</li>
|
||||
<li><strong>Recht auf Löschung:</strong> Sie können die Löschung Ihrer Daten verlangen, wenn diese nicht mehr erforderlich sind oder wenn die Verarbeitung rechtswidrig war.</li>
|
||||
<li><strong>Recht auf Einschränkung der Verarbeitung:</strong> Sie können eine Einschränkung der Verarbeitung Ihrer Daten verlangen.</li>
|
||||
<li><strong>Recht auf Widerspruch:</strong> Sie haben das Recht, der Verarbeitung Ihrer personenbezogenen Daten zu widersprechen, wenn diese auf Grund von berechtigtem Interesse erfolgt.</li>
|
||||
<li><strong>Recht auf Datenübertragbarkeit:</strong> Sie können verlangen, dass die Daten, die Sie bereitgestellt haben, in einem strukturierten, gängigen und maschinenlesbaren Format ausgehändigt werden.</li>
|
||||
<li><strong>Recht auf Widerruf:</strong> Sie können jederzeit die Einwilligung zur Datenverarbeitung widerrufen, ohne dass die Rechtmäßigkeit der aufgrund der Einwilligung bis zum Widerruf erfolgten Verarbeitung berührt wird.</li>
|
||||
</ul>
|
||||
<p>
|
||||
Sie erhalten jederzeit ohne Angabe von Gründen kostenfrei Auskunft über Ihre bei uns gespeicherten Daten. Sie können jederzeit Ihre bei uns erhobenen Daten sperren, berichtigen oder löschen lassen. Auch können Sie jederzeit die uns erteilte Einwilligung zur Datenerhebung und Verwendung ohne Angaben von Gründen widerrufen. Wenden Sie sich hierzu bitte an die auf dieser Seite angegebene Kontaktadresse des Datenschutzbeauftragten. Wir stehen Ihnen jederzeit gern für weitergehende Fragen zu unserem Hinweisen zum Datenschutz und zur Verarbeitung Ihrer persönlichen Daten zur Verfügung.
|
||||
</p>
|
||||
<p>
|
||||
Außerdem haben Sie das Recht, sich bei der Aufsichtsbehörde über die stattfindende Datenverarbeitung zu beschweren. Zuständige Aufsichtsbehörde ist der Beauftragte für den Datenschutz der EKD – Adresse siehe unten.
|
||||
</p>
|
||||
<h3>Der Datenschutzbeauftragte für den Datenschutz der Evangelischen Kirchen in Deutschland</h3>
|
||||
<p>
|
||||
Die Aufsicht über die Einhaltung der Vorschriften zum Datenschutz obliegt im kirchlichen Bereich dem Beauftragten für den Datenschutz der EKD. Für den Bereich der Evangelischen Kirche in Hessen und Nassau (EKHN) ist zuständig die Außenstelle Dortmund für die Datenschutzregion Mitte-West
|
||||
@@ -96,7 +108,7 @@
|
||||
<p>
|
||||
Die Nutzer werden gebeten, sich regelmäßig über den Inhalt der Datenschutzerklärung zu informieren.
|
||||
</p>
|
||||
<p>Stand: 24. Mai 2018</p>
|
||||
<p>Stand: Januar 2025</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Inhaltlich Verantwortlicher gemäß § 6 MDStV:</h3>
|
||||
<h3>Inhaltlich Verantwortlicher gemäß § 6 DDG:</h3>
|
||||
<p>Torsten Schulz</p>
|
||||
</section>
|
||||
<section>
|
||||
|
||||
15
src/main.js
15
src/main.js
@@ -5,14 +5,19 @@ import store from './store';
|
||||
import axios from './axios';
|
||||
import './assets/css/editor.css';
|
||||
|
||||
// Menü-Daten über das konfigurierte Axios-Backend laden
|
||||
async function fetchMenuData() {
|
||||
const response = await fetch(process.env.VUE_APP_BACKEND_URL + '/menu-data');
|
||||
return await response.json();
|
||||
const response = await axios.get('/menu-data');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
fetchMenuData().then(menuData => {
|
||||
store.commit('setMenuData', menuData);
|
||||
});
|
||||
fetchMenuData()
|
||||
.then(menuData => {
|
||||
store.commit('setMenuData', menuData);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Fehler beim Laden der Menü-Daten:', err);
|
||||
});
|
||||
|
||||
const app = createApp(AppComponent);
|
||||
app.use(router);
|
||||
|
||||
@@ -45,8 +45,13 @@ const router = createRouter({
|
||||
routes: []
|
||||
});
|
||||
|
||||
// Verhindert endlose Wiederholungen von fehlgeschlagenen Menü-Ladeversuchen
|
||||
let menuDataInitialized = false;
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
if (!store.state.menuData.length) {
|
||||
if (!menuDataInitialized) {
|
||||
menuDataInitialized = true;
|
||||
|
||||
await store.dispatch('loadMenuData');
|
||||
const routes = generateRoutesFromMenu(store.state.menuData);
|
||||
routes.forEach(route => router.addRoute(route));
|
||||
@@ -65,8 +70,12 @@ router.beforeEach(async (to, from, next) => {
|
||||
|
||||
next({ ...to, replace: true });
|
||||
} else {
|
||||
// Sicherstellen, dass die Login-Route immer verfügbar ist
|
||||
if (!router.hasRoute('auth-login')) {
|
||||
addAuthLoginRoute();
|
||||
}
|
||||
if (to.matched.some(record => record.meta.requiresAuth) && !store.getters.isLoggedIn) {
|
||||
next('/login');
|
||||
next('/auth/login');
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createStore } from 'vuex';
|
||||
import axios from 'axios';
|
||||
import axios from '../axios';
|
||||
import router from '../router';
|
||||
|
||||
let user = [];
|
||||
@@ -35,7 +35,6 @@ export default createStore({
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('token');
|
||||
router.push('/auth/login');
|
||||
},
|
||||
setMenuData(state, menuData) {
|
||||
state.menuData = menuData;
|
||||
@@ -101,6 +100,10 @@ export default createStore({
|
||||
console.error('Fehler beim Logout:', error);
|
||||
} finally {
|
||||
commit('logout');
|
||||
// Navigation nach Logout mit replace, damit die Login-Seite direkt erreichbar ist
|
||||
if (router.currentRoute.value.path !== '/auth/login') {
|
||||
router.replace('/auth/login');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,7 +5,24 @@ module.exports = defineConfig({
|
||||
transpileDependencies: [],
|
||||
devServer: {
|
||||
host: 'localhost',
|
||||
port: 8080
|
||||
// Port kann über VUE_APP_FRONTEND_PORT oder FRONTEND_PORT in .env gesetzt werden
|
||||
port: parseInt(process.env.VUE_APP_FRONTEND_PORT || process.env.FRONTEND_PORT || '8080', 10),
|
||||
// Proxy für API-Requests zum Backend-Server
|
||||
// Backend sollte auf einem anderen Port laufen (z.B. 3010)
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: process.env.VUE_APP_BACKEND_PROXY || 'http://torstens:3010',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
logLevel: 'debug',
|
||||
// Erhöhe Header-Limits für Proxy
|
||||
headers: {
|
||||
'Connection': 'keep-alive'
|
||||
},
|
||||
// Erhöhe Timeout für große Requests
|
||||
timeout: 60000
|
||||
}
|
||||
}
|
||||
},
|
||||
configureWebpack: {
|
||||
output: { clean: true },
|
||||
|
||||
Reference in New Issue
Block a user