Standortplaner/server.js

953 lines
37 KiB
JavaScript

require('dotenv').config({ override: true });
const express = require('express');
const { Pool } = require('pg');
const bodyParser = require('body-parser');
const cors = require('cors');
const path = require('node:path');
const app = express();
app.disable('x-powered-by');
const port = process.env.PORT || 3000;
app.use(cors({
origin: ['http://localhost:3000', 'http://localhost:5173', 'http://127.0.0.1:3000', 'http://127.0.0.1:5173'],
credentials: true
}));
app.use(bodyParser.json());
// Serve static files from the root directory
app.use(express.static(path.join(__dirname, './')));
const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD
});
// In-memory logs for remote debugging
const serverLogs = [];
function log(msg) {
const entry = `[${new Date().toISOString()}] ${msg}`;
console.log(entry);
serverLogs.push(entry);
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';
// Debug-Ausgabe der Authentik-Header
const authHeaders = {};
let hasAuthentikHeaders = false;
for (const key of Object.keys(req.headers)) {
if (key.startsWith('x-authentik-')) {
authHeaders[key] = req.headers[key];
hasAuthentikHeaders = true;
}
}
if (hasAuthentikHeaders) {
log(`Authentik Headers empfangen: ${JSON.stringify(authHeaders)}`);
}
if (!hasAuthentikHeaders) {
// 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;
}
const username = req.headers['x-authentik-username'] || req.headers['x-authentik-email'] || req.headers['x-authentik-name'] || 'authentik-user';
const projectsHeader = req.headers['x-authentik-meta-projekte'];
// Projekte aus Attribut-Header parsen (Komma-separiert)
let allowedProjects = [];
if (projectsHeader) {
allowedProjects = projectsHeader.split(',').map(p => p.trim()).filter(Boolean);
}
// Gruppen aus Authentik-Header extrahieren und ebenfalls als Projekte behandeln
const groupsHeader = req.headers['x-authentik-groups'];
if (groupsHeader) {
// Authentik might use | or , depending on proxy settings. Replace commas with pipes to be safe.
const normalizedGroupsHeader = groupsHeader.replace(/,/g, '|');
const groups = normalizedGroupsHeader.split('|').map(g => g.trim()).filter(Boolean);
for (const group of groups) {
// Alle Gruppen einfach als erlaubte Projekte hinzufügen.
// Was kein Projekt ist (z.B. "portal-users"), wird später beim DB-Abgleich einfach ignoriert.
if (!allowedProjects.includes(group)) {
allowedProjects.push(group);
}
}
}
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;
}
// Hilfsfunktion zum Normalisieren (entfernt Bindestriche, Unterstriche und Leerzeichen)
const normalizeStr = (str) => {
if (!str) return '';
return str.toLowerCase().replace(/[-_ ]/g, '').trim();
};
// Zuerst prüfen, ob die angeforderte ID/Name direkt im Header steht
const normalizedRequested = normalizeStr(requestedProjectIdentifier);
const hasDirectAccess = auth.projects.some(p => normalizeStr(p) === 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 res = await client.query(
'SELECT id, name FROM geodaten.projekte WHERE LOWER(REPLACE(REPLACE(REPLACE(name, \'-\', \'\'), \'_\', \'\'), \' \', \'\')) = $1',
[normalizedRequested]
);
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 = normalizeStr(p);
return normalizedP === normalizeStr(projectId) ||
normalizedP === normalizeStr(projectName);
});
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(REPLACE(name, '-', ''), '_', ''), ' ', '')) = ANY($2::text[])
ORDER BY name`,
[originalIds, normalizedNames]
);
projects = result.rows;
}
res.json({
username: auth.username,
projects: projects,
isLocalDev: auth.isLocalDev,
debug_raw_groups: req.headers['x-authentik-groups'] || null,
debug_raw_headers: Object.keys(req.headers)
.filter(k => k.startsWith('x-authentik-'))
.reduce((obj, key) => {
obj[key] = req.headers[key];
return obj;
}, {})
});
} 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
app.get('/api/status', async (req, res) => {
try {
const result = await pool.query('SELECT NOW()');
res.json({
status: 'online',
database: 'connected',
time: result.rows[0].now,
schema: process.env.DB_SCHEMA || 'geodaten'
});
} catch (err) {
res.status(500).json({ status: 'error', database: 'disconnected', error: err.message });
}
});
// Debug-Endpunkt für HTTP Headers (hilfreich für Authentik-Fehlersuche)
app.get('/api/debug/headers', (req, res) => {
res.json({
all_headers: req.headers,
authentik_headers: Object.keys(req.headers)
.filter(k => k.startsWith('x-authentik-'))
.reduce((obj, key) => {
obj[key] = req.headers[key];
return obj;
}, {})
});
});
// Debug-Endpunkt um zu sehen, wie die Projekte in der DB heißen
app.get('/api/debug/db-projekte', async (req, res) => {
const client = await pool.connect();
try {
const result = await client.query('SELECT id, name FROM geodaten.projekte ORDER BY name');
res.json(result.rows);
} catch (err) {
res.status(500).json({ error: err.message });
} finally {
client.release();
}
});
app.get('/api/logs', (req, res) => {
res.type('text/plain').send(serverLogs.join('\n'));
});
// API to load available WEA types
app.get('/api/wea-types', async (req, res) => {
const client = await pool.connect();
try {
const result = await client.query('SELECT * FROM geodaten.anlagendaten_wea ORDER BY hersteller, anlagentyp, nabenhoehe');
res.status(200).json(result.rows);
} catch (e) {
log(`FEHLER beim Laden der Anlagentypen: ${e.message}`);
res.status(500).json({ error: e.message });
} finally {
client.release();
}
});
// API to save turbines
app.post('/api/wea', async (req, res) => {
const { projekt_id, turbines } = req.body;
const targetProject = projekt_id || 'BWSamern-Ohne';
const schema = process.env.DB_SCHEMA || 'geodaten';
log(`Empfange Save-Request für Projekt: ${targetProject} (${turbines?.length || 0} WEAs)`);
const client = await pool.connect();
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');
// 1. Bestehende WEAs für dieses Projekt löschen
const projectName = await resolveProjectName(client, targetProject);
const resolvedPid = await resolveProjectId(client, targetProject);
const delRes = await client.query(
`DELETE FROM ${schema}.wea_standorte WHERE projekt_id = $1 OR projekt_id = $2 OR projekt_id = $3`,
[targetProject, resolvedPid, projectName]
);
log(`Gelöscht: ${delRes.rowCount} bestehende Anlagen.`);
// 2. Neue WEAs einfügen
if (turbines && turbines.length > 0) {
for (const t of turbines) {
log(`Füge WEA ein: Nr=${t.nr}, Typ=${t.type}, Pos=${t.latlng?.lat},${t.latlng?.lng}`);
await client.query(
`INSERT INTO ${schema}.wea_standorte
(projekt_id, wea_nummer, hersteller, anlagentyp, nabenhoehe, rotordurchmesser, ksf_drehung, variante, nennleistung, geom)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, ST_Transform(ST_SetSRID(ST_MakePoint($10, $11), 4326), 25832))`,
[targetProject, t.nr, t.hersteller, t.type, t.hh, t.rd, t.ksfAngle, t.variant, t.mw, t.latlng.lng, t.latlng.lat]
);
}
}
await client.query('COMMIT');
log(`Erfolgreich gespeichert: ${turbines?.length || 0} WEAs.`);
res.json({ message: `${turbines?.length || 0} WEAs erfolgreich in Datenbank gespeichert.` });
} catch (err) {
await client.query('ROLLBACK');
log(`FEHLER beim Speichern: ${err.message}`);
res.status(500).json({ error: 'Datenbankfehler beim Speichern', details: err.message });
} finally {
client.release();
}
});
// API to load turbines
app.get('/api/wea/:projekt_id', async (req, res) => {
const { projekt_id } = req.params;
log(`Lade WEAs für Projekt: ${projekt_id}`);
const client = await pool.connect();
try {
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 projectName = await resolveProjectName(client, projekt_id);
const resolvedPid = await resolveProjectId(client, projekt_id);
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, nennleistung as mw,
ST_X(ST_Transform(geom, 4326)) as lng, ST_Y(ST_Transform(geom, 4326)) as lat
FROM geodaten.wea_standorte WHERE projekt_id = $1 OR projekt_id = $2 OR projekt_id = $3`,
[projekt_id, resolvedPid, projectName]
);
log(`Geladen: ${result.rowCount} Anlagen.`);
res.status(200).json(result.rows);
} catch (e) {
log(`FEHLER beim Laden: ${e.message}`);
res.status(500).json({ error: e.message });
} finally {
client.release();
}
});
// Hilfsfunktion zur Auflösung von Projekt-ID oder Name
async function resolveProjectId(client, input) {
if (!input) return null;
// Prüfen, ob input eine valide UUID ist
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (uuidRegex.test(input)) return input;
// Normalisierte Suche (Kleinschreibung, alle Bindestriche, Unterstriche und Leerzeichen entfernen)
const normalized = input.toLowerCase().replace(/[-_ ]/g, '');
const res = await client.query(
'SELECT id FROM geodaten.projekte WHERE LOWER(REPLACE(REPLACE(REPLACE(name, \'-\', \'\'), \'_\', \'\'), \' \', \'\')) = $1',
[normalized]
);
return res.rows.length > 0 ? res.rows[0].id : null;
}
// Hilfsfunktion zur Auflösung des Projekt-Namens anhand der UUID oder des Namens
async function resolveProjectName(client, input) {
if (!input) return null;
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (uuidRegex.test(input)) {
const res = await client.query('SELECT name FROM geodaten.projekte WHERE id = $1', [input]);
return res.rows.length > 0 ? res.rows[0].name : input;
}
return input;
}
// API zur Flächensicherung von Eigentümern
app.post('/api/sicherung', async (req, res) => {
const { nachname, vorname, status, notiz, projekt_id } = req.body;
const targetStatus = status || 'Gesichert';
const schema = process.env.DB_SCHEMA || 'geodaten';
log(`Sicherungs-Request: ${vorname} ${nachname} Status='${targetStatus}' Notiz='${notiz}' für Projekt: ${projekt_id || 'unbekannt'}`);
const client = await pool.connect();
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');
// Suche nach dem Eigentümer in flaecheneigentuemer_alkis und aktualisiere status und notiz
const searchNachname = (nachname || '').trim();
const searchVorname = (vorname || '').trim();
// Normalisierte Suche (TRIM und COALESCE für ALKIS-Daten)
const updateRes = await client.query(
`UPDATE ${schema}.flaecheneigentuemer_alkis
SET status = $1, notiz = $2
WHERE
(TRIM(COALESCE("GNA", '')) = TRIM(COALESCE($3, '')) AND TRIM(COALESCE("VNA", '')) = TRIM(COALESCE($4, ''))) OR
(TRIM(COALESCE("VNA", '')) = TRIM(COALESCE($5, '')) AND TRIM(COALESCE("GNA", '')) = '') OR
(TRIM(COALESCE("GNA", '')) = TRIM(COALESCE($5, '')) AND TRIM(COALESCE("VNA", '')) = '') OR
(LOWER(TRIM(COALESCE("GNA", ''))) = LOWER(TRIM(COALESCE($3, ''))) AND LOWER(TRIM(COALESCE("VNA", ''))) = LOWER(TRIM(COALESCE($4, ''))))`,
[targetStatus, notiz, searchNachname, searchVorname, `${searchVorname} ${searchNachname}`.trim()]
);
if (updateRes.rowCount === 0) {
log(`Kein Eigentümer für ${vorname} ${nachname} gefunden.`);
await client.query('ROLLBACK');
return res.status(404).json({ error: `Eigentümer '${vorname} ${nachname}' nicht in der ALKIS-Tabelle gefunden.` });
}
await client.query('COMMIT');
log(`Erfolgreich: ${updateRes.rowCount || 1} Einträge in ALKIS aktualisiert.`);
res.json({
message: `Eigentümer ${vorname} ${nachname} erfolgreich aktualisiert.`,
count: updateRes.rowCount
});
} catch (err) {
await client.query('ROLLBACK');
log(`FEHLER bei Sicherung: ${err.message}`);
res.status(500).json({ error: 'Fehler bei der Flächensicherung', details: err.message });
} finally {
client.release();
}
});
// API zum Laden der Sicherungsstände
app.get('/api/sicherung/:projekt_id', async (req, res) => {
const { projekt_id } = req.params;
log(`Lade Sicherungsstände für Projekt: ${projekt_id}`);
const client = await pool.connect();
try {
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(
`SELECT a."VNA" as vorname, a."GNA" as nachname, a.status, a.notiz
FROM geodaten.flaecheneigentuemer_alkis a
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.`);
res.json(result.rows);
} catch (e) {
log(`FEHLER beim Laden der Stände: ${e.message}`);
res.status(500).json({ error: e.message });
} finally {
client.release();
}
});
// NEU: API zum Laden des ALKIS-Layers als GeoJSON
app.get('/api/layers/alkis', async (req, res) => {
const projectParam = req.query.project || req.query.projekt_id || req.query.projekt;
log(`Lade ALKIS-Layer aus Datenbank für Projekt: ${projectParam || 'alle'}`);
const client = await pool.connect();
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 = `
SELECT jsonb_build_object(
'type', 'Feature',
'id', a.id,
'geometry', ST_AsGeoJSON(ST_Transform(a.geom, 4326))::jsonb,
'properties', jsonb_build_object(
'id', a.id,
'vorname', a."VNA",
'nachname', a."GNA",
'FSK', a."FSK",
'PLZ', a."PLZ",
'ORP', a."ORP",
'STR', a."STR",
'HSN', a."HSN",
'FLN', a."FLN",
'ZAE', a."FSN__ZAE",
'NEN', a."FSN__NEN",
'status', a.status,
'notiz', a.notiz
)
) AS feature
FROM geodaten.flaecheneigentuemer_alkis a
`;
const params = [];
if (projectParam) {
const projId = await resolveProjectId(client, projectParam);
if (projId) {
query += ` JOIN geodaten.flaecheneigentuemer_alkis_zuweisung z ON a."FSK" = z.fsk WHERE z.projekt_id = $1`;
params.push(projId);
}
}
const sql = `
SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', COALESCE(jsonb_agg(features.feature), '[]'::jsonb)
)
FROM (
${query}
) features
`;
const result = await client.query(sql, params);
res.json(result.rows[0].jsonb_build_object);
} catch (err) {
log(`FEHLER beim Laden des ALKIS-Layers: ${err.message}`);
res.status(500).json({ error: err.message });
} finally {
client.release();
}
});
// NEU: API zum Laden der Potentialflächen (Gebietsumring) als GeoJSON
app.get('/api/layers/potentialflaechen', async (req, res) => {
const projectParam = req.query.project || req.query.projekt_id || req.query.projekt;
log(`Lade Potentialflächen-Layer aus Datenbank für Projekt: ${projectParam || 'alle'}`);
const client = await pool.connect();
try {
const hasAccess = await checkProjectAccess(req, projectParam, client);
if (!hasAccess) {
log(`Zugriff verweigert für Potentialflächen-Layer in Projekt: ${projectParam}`);
return res.status(403).json({ error: 'Keine Berechtigung für dieses Projekt.' });
}
let whereClause = '';
const params = [];
if (projectParam) {
const projId = await resolveProjectId(client, projectParam);
const projName = await resolveProjectName(client, projectParam);
whereClause = `WHERE projekt_id = $1`;
params.push(projectParam); // e.g. UUID
if (projId && projId !== projectParam) {
whereClause += ` OR projekt_id = $${params.length + 1}`;
params.push(projId);
}
if (projName && projName !== projectParam && projName !== projId) {
whereClause += ` OR projekt_id = $${params.length + 1}`;
params.push(projName);
}
}
const result = await client.query(
`SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', COALESCE(jsonb_agg(features.feature), '[]'::jsonb)
)
FROM (
SELECT jsonb_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(ST_Transform(geom, 4326))::jsonb,
'properties', jsonb_build_object(
'id', id,
'planungsstand', planungsstand
)
) AS feature
FROM geodaten."potentialflächen_wind"
${whereClause}
) features`,
params
);
res.json(result.rows[0].jsonb_build_object);
} catch (err) {
log(`FEHLER beim Laden des Potentialflächen-Layers: ${err.message}`);
res.status(500).json({ error: err.message });
} finally {
client.release();
}
});
// NEU: API zum Laden des Artennachweis-Layers als GeoJSON
app.get('/api/layers/artennachweis', async (req, res) => {
const projectParam = req.query.project || req.query.projekt_id || req.query.projekt;
log(`Lade Artennachweis-Layer aus Datenbank für Projekt: ${projectParam || 'alle'}`);
const client = await pool.connect();
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 = '';
const params = [];
if (projectParam) {
const projId = await resolveProjectId(client, projectParam);
const projName = await resolveProjectName(client, projectParam);
whereClause = `WHERE projekt_id = $1`;
params.push(projectParam); // e.g. 'test' or UUID
if (projId && projId !== projectParam) {
whereClause += ` OR projekt_id = $${params.length + 1}`;
params.push(projId);
}
if (projName && projName !== projectParam && projName !== projId) {
whereClause += ` OR projekt_id = $${params.length + 1}`;
params.push(projName);
}
}
const result = await client.query(
`SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', COALESCE(jsonb_agg(features.feature), '[]'::jsonb)
)
FROM (
SELECT jsonb_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(ST_Transform(geom, 4326))::jsonb,
'properties', jsonb_build_object(
'art', art
)
) AS feature
FROM geodaten.artennachweise
${whereClause}
) features`,
params
);
res.json(result.rows[0].jsonb_build_object);
} catch (err) {
log(`FEHLER beim Laden des Artennachweis-Layers: ${err.message}`);
res.status(500).json({ error: err.message });
} finally {
client.release();
}
});
// NEU: API zum Laden der Artennachweis Restriktionsabstände
app.get('/api/layers/artennachweise_restriktionen', async (req, res) => {
const projectParam = req.query.project || req.query.projekt_id || req.query.projekt;
log(`Lade Artennachweise-Restriktionen aus Datenbank für Projekt: ${projectParam || 'alle'}`);
const client = await pool.connect();
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 = '';
const params = [];
if (projectParam) {
const projId = await resolveProjectId(client, projectParam);
if (projId) {
whereClause = `WHERE EXISTS (
SELECT 1
FROM geodaten.flaecheneigentuemer_alkis alkis
JOIN geodaten.flaecheneigentuemer_alkis_zuweisung z ON alkis."FSK" = z.fsk
WHERE z.projekt_id = $1
AND ST_DWithin(a.geom, alkis.geom, 5000)
)`;
params.push(projId);
}
}
const result = await client.query(
`SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', COALESCE(jsonb_agg(features.feature), '[]'::jsonb)
)
FROM (
SELECT jsonb_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(ST_Transform(a.geom, 4326))::jsonb,
'properties', jsonb_build_object()
) AS feature
FROM geodaten.artennachweise_restriktion_abstand a
${whereClause}
) features`,
params
);
res.json(result.rows[0].jsonb_build_object);
} catch (err) {
log(`FEHLER beim Laden der Restriktionen: ${err.message}`);
res.status(500).json({ error: err.message });
} finally {
client.release();
}
});
// NEU: API zum Laden der Leitungsverläufe (Freileitung, Wasserstoff etc.)
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.' });
}
const projectParam = req.query.project;
if (!projectParam) {
return res.status(400).json({ error: 'Projekt ID (project) fehlt.' });
}
const client = await pool.connect();
try {
const hasAccess = await checkProjectAccess(req, projectParam, client);
if (!hasAccess) {
log(`Zugriff verweigert für Leitungsverläufe in Projekt: ${projectParam}`);
return res.status(403).json({ error: 'Keine Berechtigung für dieses Projekt.' });
}
const projId = await resolveProjectId(client, projectParam);
if (!projId) {
return res.status(404).json({ error: 'Projekt nicht gefunden.' });
}
log(`Lade Leitungsverläufe aus Datenbank für Projekt: ${projId}`);
const result = await client.query(
`SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', COALESCE(jsonb_agg(features.feature), '[]'::jsonb)
)
FROM (
SELECT jsonb_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(ST_Transform(geom, 4326))::jsonb,
'properties', jsonb_build_object(
'art', art,
'spannung', spannung,
'ltg_name', ltg_name,
'bezeichnun', bezeichnun,
'bemerkung', bemerkung,
'status', status,
'quelle', quelle,
'info', info,
'vorschlag', vorschlag
)
) AS feature
FROM geodaten.leitungsverleaufe
WHERE art ILIKE '%wasserstoff%' AND projekt_id = $1
UNION ALL
SELECT jsonb_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(ST_Transform(geom, 4326))::jsonb,
'properties', jsonb_build_object(
'art', 'freileitung',
'land', land,
'stand', stand,
'spannung', '380'
)
) AS feature
FROM geodaten."Freileitungen"
) features`,
[projId]
);
res.json(result.rows[0].jsonb_build_object);
} catch (err) {
log(`FEHLER beim Laden der Leitungsverläufe: ${err.message}`);
res.status(500).json({ error: err.message });
} finally {
client.release();
}
});
// NEU: API zum Laden der Gebäude
app.get('/api/layers/gebaeude', async (req, res) => {
const projectParam = req.query.project || req.query.projekt_id || req.query.projekt;
log(`Lade Gebäude aus Datenbank für Projekt: ${projectParam || 'alle'}`);
const client = await pool.connect();
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 = '';
const params = [];
if (projectParam) {
const projId = await resolveProjectId(client, projectParam);
if (projId) {
whereClause = `WHERE EXISTS (
SELECT 1
FROM geodaten.flaecheneigentuemer_alkis alkis
JOIN geodaten.flaecheneigentuemer_alkis_zuweisung z ON alkis."FSK" = z.fsk
WHERE z.projekt_id = $1
AND ST_DWithin(g.geom, alkis.geom, 5000)
)`;
params.push(projId);
}
}
const result = await client.query(
`SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', COALESCE(jsonb_agg(features.feature), '[]'::jsonb)
)
FROM (
SELECT jsonb_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(ST_Transform(g.geom, 4326))::jsonb,
'properties', jsonb_build_object()
) AS feature
FROM geodaten.gebaeude g
${whereClause}
) features`,
params
);
res.json(result.rows[0].jsonb_build_object);
} catch (err) {
log(`FEHLER beim Laden der Gebäude: ${err.message}`);
res.status(500).json({ error: err.message });
} finally {
client.release();
}
});
// NEU: API zum Laden der Wohngebäude
app.get('/api/layers/wohngebeaude', async (req, res) => {
const projectParam = req.query.project || req.query.projekt_id || req.query.projekt;
log(`Lade Wohngebäude aus Datenbank für Projekt: ${projectParam || 'alle'}`);
const client = await pool.connect();
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 = '';
const params = [];
if (projectParam) {
const projId = await resolveProjectId(client, projectParam);
if (projId) {
whereClause = `WHERE EXISTS (
SELECT 1
FROM geodaten.flaecheneigentuemer_alkis alkis
JOIN geodaten.flaecheneigentuemer_alkis_zuweisung z ON alkis."FSK" = z.fsk
WHERE z.projekt_id = $1
AND ST_DWithin(w.geom, alkis.geom, 5000)
)`;
params.push(projId);
}
}
const result = await client.query(
`SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', COALESCE(jsonb_agg(features.feature), '[]'::jsonb)
)
FROM (
SELECT jsonb_build_object(
'type', 'Feature',
'geometry', ST_AsGeoJSON(ST_Transform(w.geom, 4326))::jsonb,
'properties', jsonb_build_object()
) AS feature
FROM geodaten.wohngebeaude w
${whereClause}
) features`,
params
);
res.json(result.rows[0].jsonb_build_object);
} catch (err) {
log(`FEHLER beim Laden der Wohngebäude: ${err.message}`);
res.status(500).json({ error: err.message });
} finally {
client.release();
}
});
// API für Projekt-Statistiken (Fortschrittsanzeige)
app.get('/api/stats/:projekt_id', async (req, res) => {
const { projekt_id } = req.params;
log(`Lade Statistiken für Projekt: ${projekt_id}`);
const client = await pool.connect();
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);
if (!resolvedPid) {
throw new Error(`Projekt '${projekt_id}' konnte nicht gefunden werden.`);
}
// Verwende die vom User vorgeschlagene Logik, aber mit resolvedPid
// Wir gruppieren nach aktueller_status (da dies in der View v_projekt_sicherung so heißt)
const result = await client.query(
`SELECT
aktueller_status as status,
count(*) as anzahl,
ROUND(SUM(ST_Area(geom)) / 10000, 2) as hektar
FROM geodaten.v_projekt_sicherung
WHERE projekt_id = $1
GROUP BY aktueller_status`,
[resolvedPid]
);
log(`Statistiken berechnet: ${result.rows.length} Status-Gruppen.`);
res.json(result.rows);
} catch (e) {
log(`FEHLER beim Laden der Statistiken: ${e.message}`);
res.status(500).json({ error: e.message });
} finally {
client.release();
}
});
// Catch-all route to serve index.html for any other request (optional, good for SPAs)
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
app.listen(port, '0.0.0.0', () => {
console.log(`Server running at http://0.0.0.0:${port}`);
});