feat: Add multi-project support with Authentik access headers and project selector
Deploy Standortplaner / deploy (push) Failing after 8s Details

This commit is contained in:
Johannes Baumeister 2026-07-02 17:07:21 +02:00
parent ba6ed0f058
commit f4a590b859
3 changed files with 381 additions and 30 deletions

178
app.js
View File

@ -14,12 +14,13 @@ document.addEventListener('DOMContentLoaded', async () => {
showAuxiliary: true, showAuxiliary: true,
isEditMode: false, isEditMode: false,
isLoading: true, isLoading: true,
hasLoadedFromDB: false hasLoadedFromDB: false,
currentProjektId: null,
allowedProjects: []
}; };
const getProjektId = () => { const getProjektId = () => {
const urlParams = new URLSearchParams(window.location.search); return state.currentProjektId || "BWSamern-Ohne";
return urlParams.get('project') || urlParams.get('projekt') || urlParams.get('projekt_id') || "BWSamern-Ohne";
}; };
const STATUS_MAP = { const STATUS_MAP = {
@ -2019,16 +2020,46 @@ document.addEventListener('DOMContentLoaded', async () => {
} }
} }
// Initialize Dynamic Layers and then load data // Dynamic projects loading and switching logic
initDynamicLayers().then(async () => { async function switchProject(projId) {
state.currentProjektId = projId;
// Quietly update the project URL parameter without reloading
const url = new URL(window.location);
url.searchParams.set('project', projId);
window.history.pushState({}, '', url);
const statusEl = document.getElementById('statusInfo');
if (statusEl) {
statusEl.innerText = `Projekt "${projId}" wird geladen...`;
}
// 1. Clear existing turbines
state.turbines.forEach(t => {
try {
if (t.layers && t.variant && variantLayers[t.variant]) {
Object.values(t.layers).forEach(l => variantLayers[t.variant].removeLayer(l));
}
} catch (e) {
console.error("Fehler beim Entfernen alter WEA-Layer:", e);
}
});
state.turbines = [];
// 2. Clear existing owner statuses
state.ownerStatuses = {};
try { try {
// Erst Status laden // 3. Load dynamic layers for new project
await initDynamicLayers();
// 4. Load owner statuses
await loadOwnerStatusesFromDB(); await loadOwnerStatusesFromDB();
// Dann WEAs laden // 5. Load turbines
await loadTurbinesFromDB(); await loadTurbinesFromDB();
// Automatisches Mapping für den Eigentümer-Layer prüfen // 6. Automatisches Mapping für den Eigentümer-Layer prüfen
const ownerLayerName = Object.keys(overlays).find(k => k.toLowerCase().includes('eigentümer')); const ownerLayerName = Object.keys(overlays).find(k => k.toLowerCase().includes('eigentümer'));
if (ownerLayerName && overlays[ownerLayerName]) { if (ownerLayerName && overlays[ownerLayerName]) {
const layer = overlays[ownerLayerName]; const layer = overlays[ownerLayerName];
@ -2047,7 +2078,7 @@ document.addEventListener('DOMContentLoaded', async () => {
} }
} }
// Auto-zoom to project parcels // 7. Auto-zoom to project parcels
if (ownerLayerName && overlays[ownerLayerName]) { if (ownerLayerName && overlays[ownerLayerName]) {
const layer = overlays[ownerLayerName]; const layer = overlays[ownerLayerName];
const layers = layer.getLayers(); const layers = layer.getLayers();
@ -2066,13 +2097,114 @@ document.addEventListener('DOMContentLoaded', async () => {
console.log(`Auto-Zoom auf ${matchedLayers.length} Flächen des Projekts ausgeführt.`); console.log(`Auto-Zoom auf ${matchedLayers.length} Flächen des Projekts ausgeführt.`);
} }
} }
if (statusEl) {
statusEl.innerText = `Projekt "${projId}" erfolgreich geladen.`;
}
} catch (e) { } catch (e) {
console.error("Fehler bei der Initialisierung des Planungstools:", e); console.error("Fehler beim Umschalten des Projekts:", e);
if (statusEl) {
statusEl.innerHTML = `<span style="color: #ff3333;">Fehler beim Laden des Projekts: ${e.message}</span>`;
}
}
}
// Initialize allowed projects and project selector
async function initializeProjects() {
const statusEl = document.getElementById('statusInfo');
try {
if (statusEl) statusEl.innerText = "Benutzerdaten werden geprüft...";
const response = await fetch('/api/user/projekte');
if (!response.ok) {
if (response.status === 401 || response.status === 403) {
throw new Error("Sie haben keine Berechtigung für dieses Planungstool.");
}
throw new Error(`HTTP Fehler ${response.status}`);
}
const data = await response.json();
state.allowedProjects = data.projects || [];
if (state.allowedProjects.length === 0) {
throw new Error("Ihnen wurden in Authentik keine Projekte zugewiesen.");
}
console.log(`Erlaubte Projekte geladen:`, state.allowedProjects);
// Determine initial project
const urlParams = new URLSearchParams(window.location.search);
const requestedProj = urlParams.get('project') || urlParams.get('projekt') || urlParams.get('projekt_id');
let initialProj = null;
if (requestedProj) {
// Check if requested project name or ID is in allowed list
const matches = state.allowedProjects.find(p =>
p.id.toLowerCase() === requestedProj.toLowerCase() ||
p.name.toLowerCase() === requestedProj.toLowerCase()
);
if (matches) {
initialProj = matches.id;
}
}
// Fallback to first project if not found or not specified
if (!initialProj) {
initialProj = state.allowedProjects[0].id;
}
// Setup project selector UI
const selectorContainer = document.getElementById('projectSelectorContainer');
const selector = document.getElementById('projectSelector');
if (state.allowedProjects.length > 1) {
// Populate dropdown
selector.innerHTML = '';
state.allowedProjects.forEach(p => {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.name;
selector.appendChild(opt);
});
selector.value = initialProj;
// Show dropdown container
if (selectorContainer) {
selectorContainer.style.display = 'flex';
}
// Handle change event
selector.addEventListener('change', async (e) => {
state.isLoading = true;
await switchProject(e.target.value);
state.isLoading = false;
});
} else {
// Hide dropdown container if only one project is allowed
if (selectorContainer) {
selectorContainer.style.display = 'none';
}
}
// Perform initial load
await switchProject(initialProj);
} catch (err) {
console.error("Initialisierungsfehler:", err);
if (statusEl) {
statusEl.innerHTML = `<b style="color: #ff3333;">Zugriff verweigert:</b> ${err.message}`;
}
// Block edit interactions
state.isEditMode = false;
const btnEdit = document.getElementById('btnToggleEditMode');
if (btnEdit) btnEdit.disabled = true;
} finally { } finally {
state.isLoading = false; state.isLoading = false;
console.log("Planungstool initialisiert, isLoading = false");
} }
}); }
// Start loading process
initializeProjects();
// Project Persistence // Project Persistence
const btnSave = document.getElementById('btnSaveProject'); const btnSave = document.getElementById('btnSaveProject');
@ -2190,19 +2322,19 @@ document.addEventListener('DOMContentLoaded', async () => {
const dbTurbines = await response.json(); const dbTurbines = await response.json();
console.log(`Datenbank-Response: ${dbTurbines.length} WEAs erhalten.`); console.log(`Datenbank-Response: ${dbTurbines.length} WEAs erhalten.`);
if (dbTurbines.length > 0) { // Clear existing turbines safely
// Clear existing turbines safely state.turbines.forEach(t => {
state.turbines.forEach(t => { try {
try { if (t.layers && t.variant && variantLayers[t.variant]) {
if (t.layers && t.variant && variantLayers[t.variant]) { Object.values(t.layers).forEach(l => variantLayers[t.variant].removeLayer(l));
Object.values(t.layers).forEach(l => variantLayers[t.variant].removeLayer(l));
}
} catch (e) {
console.error("Error clearing existing layers:", e);
} }
}); } catch (e) {
state.turbines = []; console.error("Error clearing existing layers:", e);
}
});
state.turbines = [];
if (dbTurbines && dbTurbines.length > 0) {
dbTurbines.forEach(t => { dbTurbines.forEach(t => {
try { try {
const latlng = L.latLng(t.lat, t.lng); const latlng = L.latLng(t.lat, t.lng);

View File

@ -25,6 +25,12 @@
<div class="sidebar-header" style="display: flex; flex-direction: column; align-items: center; gap: 15px;"> <div class="sidebar-header" style="display: flex; flex-direction: column; align-items: center; gap: 15px;">
<img src="Logos/20201202-ENWELO-Logo-4c_ohne_Claim.png" alt="ENWELO Logo" style="max-width: 85%; height: auto; display: block;"> <img src="Logos/20201202-ENWELO-Logo-4c_ohne_Claim.png" alt="ENWELO Logo" style="max-width: 85%; height: auto; display: block;">
<h1 style="margin: 0; text-align: center;">WindPlaner</h1> <h1 style="margin: 0; text-align: center;">WindPlaner</h1>
<div id="projectSelectorContainer" style="width: 100%; display: none; flex-direction: column; gap: 4px;">
<label for="projectSelector" class="label-small" style="font-size: 0.75rem; color: var(--text-dim); text-align: left; width: 100%;">Projekt wechseln</label>
<select id="projectSelector" class="input-styled" style="width: 100%; font-size: 0.9rem; padding: 6px; border-radius: 4px;">
<!-- Wird dynamisch per JS befüllt -->
</select>
</div>
</div> </div>
<div class="sidebar-content"> <div class="sidebar-content">

227
server.js
View File

@ -35,6 +35,143 @@ function log(msg) {
if (serverLogs.length > 50) serverLogs.shift(); if (serverLogs.length > 50) serverLogs.shift();
} }
// Hilfsfunktion zur Ermittlung der erlaubten Projekte aus Authentik-Headern
function getAllowedProjects(req) {
const isProd = process.env.NODE_ENV === 'production';
const username = req.headers['x-authentik-username'];
const projectsHeader = req.headers['x-authentik-meta-projekte'];
if (!username) {
// Lokale Entwicklung (Fallback)
if (!isProd) {
return {
username: 'local-dev-user',
projects: ['*'], // Sternchen bedeutet Zugriff auf alle Projekte
isLocalDev: true
};
}
// Im Produktivmodus ist ein fehlender Header ein Fehler
return null;
}
// Projekte aus Header parsen (Komma-separiert)
let allowedProjects = [];
if (projectsHeader) {
allowedProjects = projectsHeader.split(',').map(p => p.trim()).filter(Boolean);
}
return {
username,
projects: allowedProjects,
isLocalDev: false
};
}
// Hilfsfunktion zur Berechtigungsprüfung eines bestimmten Projekts
async function checkProjectAccess(req, requestedProjectIdentifier, client) {
const auth = getAllowedProjects(req);
if (!auth) return false;
// Wenn der Benutzer ein Admin/Entwickler mit '*' Wildcard ist, darf er alles sehen
if (auth.projects.includes('*')) {
return true;
}
if (!requestedProjectIdentifier) {
return false;
}
// Zuerst prüfen, ob die angeforderte ID/Name direkt im Header steht
const normalizedRequested = requestedProjectIdentifier.toLowerCase().trim();
const hasDirectAccess = auth.projects.some(p => p.toLowerCase().trim() === normalizedRequested);
if (hasDirectAccess) {
return true;
}
// Wenn kein direkter Treffer vorliegt, müssen wir prüfen, ob die UUID in der Datenbank aufgelöst
// und mit den erlaubten Projektnamen im Header übereinstimmt.
try {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
let projectId = null;
let projectName = null;
if (uuidRegex.test(requestedProjectIdentifier)) {
const res = await client.query('SELECT id, name FROM geodaten.projekte WHERE id = $1', [requestedProjectIdentifier]);
if (res.rows.length > 0) {
projectId = res.rows[0].id;
projectName = res.rows[0].name;
}
} else {
const normalized = requestedProjectIdentifier.toLowerCase().replace(/[-_]/g, '');
const res = await client.query(
'SELECT id, name FROM geodaten.projekte WHERE LOWER(REPLACE(REPLACE(name, \'-\', \'\'), \'_\', \'\')) = $1',
[normalized]
);
if (res.rows.length > 0) {
projectId = res.rows[0].id;
projectName = res.rows[0].name;
}
}
if (!projectId) return false;
// Prüfen, ob die aufgelöste ID oder der Projektname in der erlaubten Liste steht
const match = auth.projects.some(p => {
const normalizedP = p.toLowerCase().trim();
return normalizedP === projectId.toLowerCase() ||
normalizedP === (projectName || '').toLowerCase().trim();
});
return match;
} catch (e) {
log(`Fehler bei der Berechtigungsprüfung: ${e.message}`);
return false;
}
}
// API-Endpunkt zur Rückgabe der erlaubten Projekte des angemeldeten Nutzers
app.get('/api/user/projekte', async (req, res) => {
const auth = getAllowedProjects(req);
if (!auth) {
log('Unauthorized request to /api/user/projekte (missing headers)');
return res.status(401).json({ error: 'Nicht autorisiert. Authentik-Header fehlen.' });
}
log(`Lade erlaubte Projekte für User: ${auth.username} (Projekte: ${auth.projects.join(', ')})`);
const client = await pool.connect();
try {
let projects = [];
if (auth.projects.includes('*')) {
const result = await client.query('SELECT id, name FROM geodaten.projekte ORDER BY name');
projects = result.rows;
} else if (auth.projects.length > 0) {
const originalIds = auth.projects.filter(p => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(p));
const normalizedNames = auth.projects.map(p => p.toLowerCase().replace(/[-_]/g, '').trim());
const result = await client.query(
`SELECT id, name FROM geodaten.projekte
WHERE id::text = ANY($1::text[])
OR LOWER(REPLACE(REPLACE(name, '-', ''), '_', '')) = ANY($2::text[])
ORDER BY name`,
[originalIds, normalizedNames]
);
projects = result.rows;
}
res.json({
username: auth.username,
projects: projects,
isLocalDev: auth.isLocalDev
});
} catch (err) {
log(`FEHLER beim Laden der erlaubten Projekte: ${err.message}`);
res.status(500).json({ error: 'Datenbankfehler beim Laden der Projekte', details: err.message });
} finally {
client.release();
}
});
// Status-Endpunkt für Diagnose // Status-Endpunkt für Diagnose
app.get('/api/status', async (req, res) => { app.get('/api/status', async (req, res) => {
try { try {
@ -64,6 +201,12 @@ app.post('/api/wea', async (req, res) => {
const client = await pool.connect(); const client = await pool.connect();
try { try {
const hasAccess = await checkProjectAccess(req, targetProject, client);
if (!hasAccess) {
log(`Zugriff verweigert für Save-Request Projekt: ${targetProject}`);
return res.status(403).json({ error: 'Keine Berechtigung zum Speichern in diesem Projekt.' });
}
await client.query('BEGIN'); await client.query('BEGIN');
// 1. Bestehende WEAs für dieses Projekt löschen // 1. Bestehende WEAs für dieses Projekt löschen
@ -102,8 +245,15 @@ app.post('/api/wea', async (req, res) => {
app.get('/api/wea/:projekt_id', async (req, res) => { app.get('/api/wea/:projekt_id', async (req, res) => {
const { projekt_id } = req.params; const { projekt_id } = req.params;
log(`Lade WEAs für Projekt: ${projekt_id}`); log(`Lade WEAs für Projekt: ${projekt_id}`);
const client = await pool.connect();
try { try {
const result = await pool.query( const hasAccess = await checkProjectAccess(req, projekt_id, client);
if (!hasAccess) {
log(`Zugriff verweigert für Laden WEA Projekt: ${projekt_id}`);
return res.status(403).json({ error: 'Keine Berechtigung für dieses Projekt.' });
}
const result = await client.query(
`SELECT wea_nummer as nr, hersteller as hersteller, anlagentyp as type, nabenhoehe as hh, rotordurchmesser as rd, ksf_drehung as ksfAngle, variante as variant, `SELECT wea_nummer as nr, hersteller as hersteller, anlagentyp as type, nabenhoehe as hh, rotordurchmesser as rd, ksf_drehung as ksfAngle, variante as variant,
ST_X(ST_Transform(geom, 4326)) as lng, ST_Y(ST_Transform(geom, 4326)) as lat ST_X(ST_Transform(geom, 4326)) as lng, ST_Y(ST_Transform(geom, 4326)) as lat
FROM geodaten.wea_standorte WHERE projekt_id = $1`, FROM geodaten.wea_standorte WHERE projekt_id = $1`,
@ -114,6 +264,8 @@ app.get('/api/wea/:projekt_id', async (req, res) => {
} catch (e) { } catch (e) {
log(`FEHLER beim Laden: ${e.message}`); log(`FEHLER beim Laden: ${e.message}`);
res.status(500).json({ error: e.message }); res.status(500).json({ error: e.message });
} finally {
client.release();
} }
}); });
@ -137,14 +289,28 @@ async function resolveProjectId(client, input) {
// API zur Flächensicherung von Eigentümern // API zur Flächensicherung von Eigentümern
app.post('/api/sicherung', async (req, res) => { app.post('/api/sicherung', async (req, res) => {
const { nachname, vorname, status, notiz } = req.body; const { nachname, vorname, status, notiz, projekt_id } = req.body;
const targetStatus = status || 'Gesichert'; const targetStatus = status || 'Gesichert';
const schema = process.env.DB_SCHEMA || 'geodaten'; const schema = process.env.DB_SCHEMA || 'geodaten';
log(`Sicherungs-Request: ${vorname} ${nachname} Status='${targetStatus}' Notiz='${notiz}'`); log(`Sicherungs-Request: ${vorname} ${nachname} Status='${targetStatus}' Notiz='${notiz}' für Projekt: ${projekt_id || 'unbekannt'}`);
const client = await pool.connect(); const client = await pool.connect();
try { try {
if (projekt_id) {
const hasAccess = await checkProjectAccess(req, projekt_id, client);
if (!hasAccess) {
log(`Zugriff verweigert für Sicherungs-Request in Projekt: ${projekt_id}`);
return res.status(403).json({ error: 'Keine Berechtigung zur Flächensicherung in diesem Projekt.' });
}
} else {
const auth = getAllowedProjects(req);
if (!auth || (auth.projects.length === 0 && !auth.projects.includes('*'))) {
log('Zugriff verweigert: Keine zugewiesenen Projekte vorhanden');
return res.status(403).json({ error: 'Zugriff verweigert. Sie haben keine Projekte zugewiesen.' });
}
}
await client.query('BEGIN'); await client.query('BEGIN');
// Suche nach dem Eigentümer in flaecheneigentuemer_alkis und aktualisiere status und notiz // Suche nach dem Eigentümer in flaecheneigentuemer_alkis und aktualisiere status und notiz
@ -191,11 +357,23 @@ app.get('/api/sicherung/:projekt_id', async (req, res) => {
const client = await pool.connect(); const client = await pool.connect();
try { try {
// Wir laden nun direkt aus der ALKIS Tabelle, wie vom User gewünscht const hasAccess = await checkProjectAccess(req, projekt_id, client);
if (!hasAccess) {
log(`Zugriff verweigert für Laden Sicherungsstände Projekt: ${projekt_id}`);
return res.status(403).json({ error: 'Keine Berechtigung für dieses Projekt.' });
}
const resolvedPid = await resolveProjectId(client, projekt_id);
if (!resolvedPid) {
return res.json([]);
}
const result = await client.query( const result = await client.query(
`SELECT "VNA" as vorname, "GNA" as nachname, status, notiz `SELECT a."VNA" as vorname, a."GNA" as nachname, a.status, a.notiz
FROM geodaten.flaecheneigentuemer_alkis FROM geodaten.flaecheneigentuemer_alkis a
WHERE status IS NOT NULL OR notiz IS NOT NULL` JOIN geodaten.flaecheneigentuemer_alkis_zuweisung z ON a."FSK" = z.fsk
WHERE z.projekt_id = $1 AND (a.status IS NOT NULL OR a.notiz IS NOT NULL)`,
[resolvedPid]
); );
log(`Geladen: ${result.rowCount} Status-Einträge aus ALKIS.`); log(`Geladen: ${result.rowCount} Status-Einträge aus ALKIS.`);
@ -214,6 +392,11 @@ app.get('/api/layers/alkis', async (req, res) => {
log(`Lade ALKIS-Layer aus Datenbank für Projekt: ${projectParam || 'alle'}`); log(`Lade ALKIS-Layer aus Datenbank für Projekt: ${projectParam || 'alle'}`);
const client = await pool.connect(); const client = await pool.connect();
try { try {
const hasAccess = await checkProjectAccess(req, projectParam, client);
if (!hasAccess) {
log(`Zugriff verweigert für ALKIS-Layer in Projekt: ${projectParam}`);
return res.status(403).json({ error: 'Keine Berechtigung für dieses Projekt.' });
}
let query = ` let query = `
SELECT jsonb_build_object( SELECT jsonb_build_object(
'type', 'Feature', 'type', 'Feature',
@ -273,6 +456,11 @@ app.get('/api/layers/artennachweis', async (req, res) => {
log(`Lade Artennachweis-Layer aus Datenbank für Projekt: ${projectParam || 'alle'}`); log(`Lade Artennachweis-Layer aus Datenbank für Projekt: ${projectParam || 'alle'}`);
const client = await pool.connect(); const client = await pool.connect();
try { try {
const hasAccess = await checkProjectAccess(req, projectParam, client);
if (!hasAccess) {
log(`Zugriff verweigert für Artennachweis-Layer in Projekt: ${projectParam}`);
return res.status(403).json({ error: 'Keine Berechtigung für dieses Projekt.' });
}
let whereClause = ''; let whereClause = '';
const params = []; const params = [];
@ -320,6 +508,11 @@ app.get('/api/layers/artennachweise_restriktionen', async (req, res) => {
log(`Lade Artennachweise-Restriktionen aus Datenbank für Projekt: ${projectParam || 'alle'}`); log(`Lade Artennachweise-Restriktionen aus Datenbank für Projekt: ${projectParam || 'alle'}`);
const client = await pool.connect(); const client = await pool.connect();
try { try {
const hasAccess = await checkProjectAccess(req, projectParam, client);
if (!hasAccess) {
log(`Zugriff verweigert für Restriktionen in Projekt: ${projectParam}`);
return res.status(403).json({ error: 'Keine Berechtigung für dieses Projekt.' });
}
let whereClause = ''; let whereClause = '';
const params = []; const params = [];
@ -364,6 +557,11 @@ app.get('/api/layers/artennachweise_restriktionen', async (req, res) => {
// NEU: API zum Laden der Leitungsverläufe (Freileitung, Wasserstoff etc.) // NEU: API zum Laden der Leitungsverläufe (Freileitung, Wasserstoff etc.)
app.get('/api/layers/leitungsverlaeufe', async (req, res) => { app.get('/api/layers/leitungsverlaeufe', async (req, res) => {
const auth = getAllowedProjects(req);
if (!auth) {
log('Unauthorized request to /api/layers/leitungsverlaeufe');
return res.status(401).json({ error: 'Nicht autorisiert. Authentik-Header fehlen.' });
}
log("Lade Leitungsverläufe aus Datenbank..."); log("Lade Leitungsverläufe aus Datenbank...");
try { try {
const result = await pool.query( const result = await pool.query(
@ -418,6 +616,11 @@ app.get('/api/layers/gebaeude', async (req, res) => {
log(`Lade Gebäude aus Datenbank für Projekt: ${projectParam || 'alle'}`); log(`Lade Gebäude aus Datenbank für Projekt: ${projectParam || 'alle'}`);
const client = await pool.connect(); const client = await pool.connect();
try { try {
const hasAccess = await checkProjectAccess(req, projectParam, client);
if (!hasAccess) {
log(`Zugriff verweigert für Gebäude in Projekt: ${projectParam}`);
return res.status(403).json({ error: 'Keine Berechtigung für dieses Projekt.' });
}
let whereClause = ''; let whereClause = '';
const params = []; const params = [];
@ -466,6 +669,11 @@ app.get('/api/layers/wohngebeaude', async (req, res) => {
log(`Lade Wohngebäude aus Datenbank für Projekt: ${projectParam || 'alle'}`); log(`Lade Wohngebäude aus Datenbank für Projekt: ${projectParam || 'alle'}`);
const client = await pool.connect(); const client = await pool.connect();
try { try {
const hasAccess = await checkProjectAccess(req, projectParam, client);
if (!hasAccess) {
log(`Zugriff verweigert für Wohngebäude in Projekt: ${projectParam}`);
return res.status(403).json({ error: 'Keine Berechtigung für dieses Projekt.' });
}
let whereClause = ''; let whereClause = '';
const params = []; const params = [];
@ -515,6 +723,11 @@ app.get('/api/stats/:projekt_id', async (req, res) => {
const client = await pool.connect(); const client = await pool.connect();
try { try {
const hasAccess = await checkProjectAccess(req, projekt_id, client);
if (!hasAccess) {
log(`Zugriff verweigert für Statistiken in Projekt: ${projekt_id}`);
return res.status(403).json({ error: 'Keine Berechtigung für dieses Projekt.' });
}
const resolvedPid = await resolveProjectId(client, projekt_id); const resolvedPid = await resolveProjectId(client, projekt_id);
if (!resolvedPid) { if (!resolvedPid) {
throw new Error(`Projekt '${projekt_id}' konnte nicht gefunden werden.`); throw new Error(`Projekt '${projekt_id}' konnte nicht gefunden werden.`);