From a75303f857b170feeeda7d6be6145776d4d14ab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reinhard=20X=2E=20F=C3=BCrst?= Date: Sat, 6 Jun 2026 08:48:15 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Version=201.10.0=20=E2=80=94=20DB-Zugri?= =?UTF-8?q?ff=20auf=20PHP-Bridge=20(DB4js=5Fall.php)=20umgestellt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib/db.ts entfernt, mysql2-Abhängigkeit gestrichen - lib/phpdb.ts: HTTP-Client für alle DB-Operationen via DB4js_all.php - Alle API-Routen und Server Actions auf phpdb.ts umgestellt - compose.yml / docker-compose.prod.yml: MySQL/phpMyAdmin-Container entfernt - app/api/DB4js_all.php/route.ts: Proxy für Statistik-AJAX-Calls - Statistik-Grafik liest ab 2026 live aus logbuch statt StatistikJahre - PHP 7.3-Kompatibilität: str_contains → strpos Co-Authored-By: Claude Sonnet 4.6 --- ANLEITUNG.md | 34 +----- app/admin/actions.ts | 34 ++---- app/api/DB4js_all.php/route.ts | 24 +++++ app/api/beos/route.ts | 6 +- app/api/fahrkosten/route.ts | 24 +---- app/api/logbuch/[id]/route.ts | 91 ++++------------ app/api/logbuch/route.ts | 132 ++++------------------- app/api/objekte/[id]/route.ts | 8 +- app/api/objekte/route.ts | 8 +- app/api/statistik/route.ts | 64 +----------- app/change-password/actions.ts | 7 +- compose.yml | 70 +------------ docker-compose.prod.yml | 45 -------- lib/auth.ts | 32 +----- lib/backup.ts | 25 ++--- lib/db.ts | 29 ------ lib/phpdb.ts | 185 +++++++++++++++++++++++++++++++++ package.json | 5 +- 18 files changed, 291 insertions(+), 532 deletions(-) create mode 100644 app/api/DB4js_all.php/route.ts delete mode 100644 lib/db.ts create mode 100644 lib/phpdb.ts diff --git a/ANLEITUNG.md b/ANLEITUNG.md index 11f2925..d021e46 100644 --- a/ANLEITUNG.md +++ b/ANLEITUNG.md @@ -19,7 +19,7 @@ Die App ist passwortgeschützt. Beim ersten Aufruf erscheint die Anmeldeseite. - **Kürzel**: das persönliche BEO-Kürzel (z. B. `RXF`) - **Passwort**: individuell gesetztes Passwort -Wurde das Passwort noch nicht geändert (Anzeige „Standard"), muss nach dem ersten Login sofort ein neues Passwort vergeben werden. Das Standard-Passwort lautet `welzheim`. +Wurde das Passwort noch nicht geändert, muss nach dem ersten Login sofort ein neues Passwort vergeben werden. Das Standard-Passwort lautet `welzheim`. --- @@ -141,6 +141,8 @@ Zeigt eine Monatstabelle mit Anzahl der Führungen und Besuchern, aufgeschlüsse - Kumulierte Besucher für die gesamte Sternwarte (alle Kuppeln) - Führungstage für die gesamte Sternwarte +- **Grafik** Über diesen Button kann die (bekannte) Statistik-Grafik aufgerufen werden. Sie wird in ein einem gesonderten Fenster angezeigt. Zurück zum Führungsbuch kommt man über den Tab-Wechsel im Browser. + --- ## 6. Drucken @@ -176,33 +178,3 @@ Zeigt alle bekannten Objekte mit ID, Name und Datum der letzten Verwendung. - **Objekt umbenennen**: Stift-Symbol ✎ in der Zeile anklicken, Namen ändern und mit **Speichern** bestätigen oder mit **Abbrechen** verwerfen. - **Objekt löschen**: × in der Zeile – es erscheint ein Bestätigungsdialog. Das Löschen ist **unwiderruflich** und entfernt das Objekt aus allen bestehenden Führungsbucheinträgen. -## Neue Features (Statistik Grafik Proxy) - -Dieses Release ergänzt eine serverseitige Proxy-Lösung für das interne Statistik-Portal, damit geschützte Diagramme sicher eingeblendet werden können, ohne Zugangsdaten im Browser zu speichern. - -- Was neu ist: - - Server-seitiger Proxy unter `/api/statistik/grafik` und Catch-all `/api/statistik/grafik/*`. - - Holt die Statistik-Seite mit Basic-Auth (serverseitig) und liefert sie an den Browser weiter. - - Schreibt die HTML-Antwort so um, dass relative Assets (CSS/JS/Images) über die Proxy-URL geladen werden (es wird ein `` eingefügt). - - Leitet auch AJAX-POSTs (z. B. `php/statistic.php`) weiter – Methoden und Bodies werden beibehalten. - - Entfernt framing-blockierende Header (z. B. `X-Frame-Options`, CSP-Meta-Tags) in der proxied HTML-Antwort. - -- Wichtige Environment-Variablen (nur serverseitig): - - `STATISTIK_GRAFIK_URL` — Basis-URL des internen Statistik-Portals (z. B. `https://sternwarte-welzheim.de/intern/statistik`). - - `STATISTIK_GRAFIK_USER` — Benutzername für Basic-Auth. - - `STATISTIK_GRAFIK_PASS` — Passwort für Basic-Auth. - -- UI-Änderung: - - Der `Grafik`-Button in der Statistik-Ansicht öffnet die Statistik jetzt in einem neuen Fenster (`window.open('/api/statistik/grafik', '_blank')`). Die vorherige iframe-Integration wurde entfernt, weil manche Browser (insbesondere Firefox und Safari) Probleme mit eingebetteten, geschützten Seiten machen. - -- Sicherheit & Hinweise: - - Setze die `STATISTIK_...` Variablen in deiner Server-Umgebung (Docker secrets oder im Host-Umfeld). Niemals Zugangsdaten ins Repository committen. - - Die Proxy-Route ist so konfiguriert, dass Assets und AJAX-Aufrufe über den gleichen Proxy laufen, damit die Seite vollständig funktioniert. - -Wenn du möchtest, pushe ich die Änderungen an `ANLEITUNG.md` in `origin/main` für dich. - -Optionales manuelles Deploy (falls Docker lokal verfügbar): - -```bash -./deploy.sh 1.7.8 -``` diff --git a/app/admin/actions.ts b/app/admin/actions.ts index 5ca135a..7dbb540 100644 --- a/app/admin/actions.ts +++ b/app/admin/actions.ts @@ -2,7 +2,9 @@ import { redirect } from 'next/navigation'; import { getSession } from '@/lib/session'; -import { query } from '@/lib/db'; +import * as phpdb from '@/lib/phpdb'; + +export type { BeoUser } from '@/lib/phpdb'; export interface ObjektRow { ID: number; @@ -13,28 +15,13 @@ export interface ObjektRow { export async function listObjekte(): Promise { const session = await getSession(); if (!session || !session.role?.includes('admin')) redirect('/'); - const rows = await query('SELECT ID, Name, LastUsed FROM objekte ORDER BY Name ASC', []); - return rows as ObjektRow[]; + return phpdb.listObjekteAdmin(); } -export interface BeoUser { - id: number; - kürzel: string | null; - name: string; - vorname: string | null; - role: string | null; - hasPw: boolean; -} - -export async function listUsers(): Promise { +export async function listUsers(): Promise { const session = await getSession(); if (!session || !session.role?.includes('admin')) redirect('/'); - - const rows = await query( - 'SELECT id, `kürzel`, name, vorname, role, (pw IS NOT NULL) AS hasPw FROM beos ORDER BY name, vorname', - [] - ) as (Omit & { hasPw: number })[]; - return rows.map(r => ({ ...r, hasPw: r.hasPw === 1 })); + return phpdb.listUsers(); } export async function resetPassword( @@ -46,16 +33,11 @@ export async function resetPassword( return { error: 'Keine Berechtigung.' }; } - const idRaw = formData.get('id'); - const id = Number(idRaw); + const id = Number(formData.get('id')); if (!id || isNaN(id)) { return { error: 'Ungültige Benutzer-ID.' }; } - await query( - 'UPDATE beos SET pw = NULL, MustChangePassword = 1 WHERE id = ?', - [id] - ); - + await phpdb.resetBeoPassword(id); return { success: 'Passwort wurde zurückgesetzt. Der Benutzer muss sich mit dem Standard-Passwort anmelden und es dann ändern.' }; } diff --git a/app/api/DB4js_all.php/route.ts b/app/api/DB4js_all.php/route.ts new file mode 100644 index 0000000..3aa0181 --- /dev/null +++ b/app/api/DB4js_all.php/route.ts @@ -0,0 +1,24 @@ +import { NextResponse } from 'next/server'; + +const DB_URL = (process.env.PHP_DB_URL ?? 'http://localhost:8080/DB4js_all.php') + .replace(/\?.*$/, ''); + +async function proxy(req: Request) { + const search = new URL(req.url).search; + const target = DB_URL + search; + const isReadOnly = req.method === 'GET' || req.method === 'HEAD'; + const upstream = await fetch(target, { + method: req.method, + headers: { 'content-type': req.headers.get('content-type') ?? 'application/x-www-form-urlencoded' }, + body: isReadOnly ? undefined : await req.arrayBuffer(), + cache: 'no-store', + }); + const body = await upstream.arrayBuffer(); + return new NextResponse(Buffer.from(body), { + status: upstream.status, + headers: { 'content-type': upstream.headers.get('content-type') ?? 'application/json' }, + }); +} + +export const GET = proxy; +export const POST = proxy; diff --git a/app/api/beos/route.ts b/app/api/beos/route.ts index 3ccef69..27cc473 100644 --- a/app/api/beos/route.ts +++ b/app/api/beos/route.ts @@ -1,14 +1,12 @@ import { NextResponse } from 'next/server'; -import { query } from '@/lib/db'; import { getSession } from '@/lib/session'; +import * as phpdb from '@/lib/phpdb'; export async function GET() { const session = await getSession(); if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 }); try { - const rows = await query( - 'SELECT id AS ID, `kürzel` AS Kuerzel, CONCAT(IFNULL(vorname, \'\'), IF(vorname IS NOT NULL, \' \', \'\'), name) AS Name FROM beos WHERE `kürzel` IS NOT NULL AND FIND_IN_SET(\'guide\', role) > 0 ORDER BY name ASC' - ) as { ID: number; Kuerzel: string; Name: string }[]; + const rows = await phpdb.getBeos(); return NextResponse.json(rows); } catch (error) { console.error('GET /api/beos:', error); diff --git a/app/api/fahrkosten/route.ts b/app/api/fahrkosten/route.ts index cff44fa..a765d3c 100644 --- a/app/api/fahrkosten/route.ts +++ b/app/api/fahrkosten/route.ts @@ -1,15 +1,8 @@ import { NextResponse } from 'next/server'; -import { query } from '@/lib/db'; import { getSession } from '@/lib/session'; +import * as phpdb from '@/lib/phpdb'; -export interface FahrkostenRow { - ID: number; - Kuerzel: string; - Name: string; - Anzahl: number; -} - -const EXCLUDED = "'PrF','Beob','BEOS','TD','ToT'"; +export type { FahrkostenRow } from '@/lib/phpdb'; export async function GET(req: Request) { const session = await getSession(); @@ -22,18 +15,7 @@ export async function GET(req: Request) { } try { - const rows = await query( - 'SELECT b.id AS ID, b.`kürzel` AS Kuerzel,' + - ' CONCAT(IFNULL(b.vorname, \'\'), IF(b.vorname IS NOT NULL, \' \', \'\'), b.name) AS Name,' + - ' COUNT(DISTINCT l.ID) AS Anzahl' + - ' FROM beos b' + - ' JOIN logbuch_beos lb ON lb.BeoID = b.id' + - ' JOIN logbuch l ON l.ID = lb.LogbuchID' + - ' WHERE l.Beginn >= ? AND l.ArtFuehrung NOT IN (' + EXCLUDED + ')' + - ' GROUP BY b.id, b.`kürzel`, b.name, b.vorname' + - ' ORDER BY b.name ASC', - [ab + ' 00:00:00'] - ) as FahrkostenRow[]; + const rows = await phpdb.getFahrkosten(ab); return NextResponse.json(rows); } catch (error) { console.error('GET /api/fahrkosten:', error); diff --git a/app/api/logbuch/[id]/route.ts b/app/api/logbuch/[id]/route.ts index 2edf820..1d8842c 100644 --- a/app/api/logbuch/[id]/route.ts +++ b/app/api/logbuch/[id]/route.ts @@ -1,8 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; -import { query, getPool } from '@/lib/db'; import { getSession } from '@/lib/session'; import { triggerBackup } from '@/lib/backup'; -import type { SelectedObjekt } from '@/types/logbuch'; +import * as phpdb from '@/lib/phpdb'; export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { const session = await getSession(); @@ -12,66 +11,25 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ const logbuchId = parseInt(id); try { - const existingRows = await query('SELECT ID FROM logbuch WHERE ID = ?', [logbuchId]) as { ID: number }[]; - if (existingRows.length === 0) { - return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 }); - } - - const isAdmin = session.role?.includes('admin'); - const beoRows = await query('SELECT COUNT(*) AS cnt FROM logbuch_beos WHERE LogbuchID = ? AND BeoID = ?', [logbuchId, session.beoId]) as { cnt: number }[]; - const isBeo = (beoRows[0]?.cnt ?? 0) > 0; - - if (!isAdmin && !isBeo) { - return NextResponse.json({ error: 'Keine Berechtigung zum Ändern dieses Eintrags' }, { status: 403 }); - } - const body = await request.json(); const { Kuppel, ArtFuehrung, SonderName, Beginn, Ende, Besucher, beoIds, objekte, Bemerkungen, Wetter } = body; - await getPool().execute( - 'UPDATE logbuch SET Kuppel=?, ArtFuehrung=?, SonderName=?, Beginn=?, Ende=?, Besucher=?,' + - ' Bemerkungen=?, WetterTemp=?, WetterFeuchte=?, WetterDruck=? WHERE ID=?', - [ - Kuppel, ArtFuehrung, SonderName || null, Beginn, Ende, - Besucher ?? 0, - Bemerkungen?.slice(0, 500) || null, - Wetter?.temp ?? null, - Wetter?.feuchte ?? null, - Wetter?.druck ?? null, - logbuchId, - ] - ); - - await query('DELETE FROM logbuch_beos WHERE LogbuchID = ?', [logbuchId]); - await query('DELETE FROM logbuch_objekte WHERE LogbuchID = ?', [logbuchId]); - - for (const beoId of (beoIds as number[]) || []) { - await query('INSERT INTO logbuch_beos (LogbuchID, BeoID) VALUES (?, ?)', [logbuchId, beoId]); - } - - for (const obj of (objekte as SelectedObjekt[]) || []) { - let objektId = obj.ID; - if (!objektId) { - const existing = await query('SELECT ID, Name FROM objekte WHERE LOWER(Name) = LOWER(?)', [obj.Name]) as { ID: number; Name: string }[]; - if (existing[0]) { - objektId = existing[0].ID; - } else { - const [ins] = await getPool().execute( - 'INSERT INTO objekte (Name) VALUES (?)', [obj.Name] - ) as [{ insertId: number }, unknown]; - objektId = ins.insertId; - } - } - await query('UPDATE objekte SET LastUsed = NOW() WHERE ID = ?', [objektId]); - await query( - 'INSERT INTO logbuch_objekte (LogbuchID, ObjektID) VALUES (?, ?)', - [logbuchId, objektId] - ); - } + await phpdb.updateLogbuch(logbuchId, session.beoId, session.role ?? '', { + Kuppel, ArtFuehrung, SonderName, Beginn, Ende, + Besucher: Besucher ?? 0, + beoIds: beoIds ?? [], + objekte: objekte ?? [], + Bemerkungen: Bemerkungen ?? null, + Wetter: Wetter ?? null, + }); triggerBackup(); return NextResponse.json({ ok: true }); - } catch (error) { + } catch (error: unknown) { + if (error instanceof Error) { + if (error.message.includes('404')) return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 }); + if (error.message.includes('403')) return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 }); + } console.error('PUT /api/logbuch/[id]:', error); return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 }); } @@ -85,22 +43,13 @@ export async function DELETE(_request: NextRequest, { params }: { params: Promis const logbuchId = parseInt(id); try { - const existingRows = await query('SELECT ID FROM logbuch WHERE ID = ?', [logbuchId]) as { ID: number }[]; - if (existingRows.length === 0) { - return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 }); - } - - const isAdmin = session.role?.includes('admin'); - const beoRows = await query('SELECT COUNT(*) AS cnt FROM logbuch_beos WHERE LogbuchID = ? AND BeoID = ?', [logbuchId, session.beoId]) as { cnt: number }[]; - const isBeo = (beoRows[0]?.cnt ?? 0) > 0; - - if (!isAdmin && !isBeo) { - return NextResponse.json({ error: 'Keine Berechtigung zum Löschen dieses Eintrags' }, { status: 403 }); - } - - await query('DELETE FROM logbuch WHERE ID = ?', [logbuchId]); + await phpdb.deleteLogbuch(logbuchId, session.beoId, session.role ?? ''); return NextResponse.json({ ok: true }); - } catch (error) { + } catch (error: unknown) { + if (error instanceof Error) { + if (error.message.includes('404')) return NextResponse.json({ error: 'Eintrag nicht gefunden' }, { status: 404 }); + if (error.message.includes('403')) return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 }); + } console.error('DELETE /api/logbuch/[id]:', error); return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 }); } diff --git a/app/api/logbuch/route.ts b/app/api/logbuch/route.ts index 6332cb4..b11ace6 100644 --- a/app/api/logbuch/route.ts +++ b/app/api/logbuch/route.ts @@ -1,90 +1,23 @@ import { NextRequest, NextResponse } from 'next/server'; -import { query, getPool } from '@/lib/db'; import { getSession } from '@/lib/session'; import { triggerBackup } from '@/lib/backup'; -import type { SelectedObjekt } from '@/types/logbuch'; - -const LIST_SQL = - 'SELECT' + - ' l.ID, l.Kuppel, l.ArtFuehrung,' + - " DATE_FORMAT(l.Beginn, '%Y-%m-%dT%H:%i') AS Beginn," + - " DATE_FORMAT(l.Ende, '%Y-%m-%dT%H:%i') AS Ende," + - ' l.Besucher, l.Bemerkungen, l.SonderName,' + - ' l.WetterTemp, l.WetterFeuchte, l.WetterDruck,' + - ' l.created_by, l.created_at,' + - ' creator.kuerzel AS created_by_kuerzel,' + - " GROUP_CONCAT(DISTINCT bk.kuerzel ORDER BY bk.kuerzel SEPARATOR ', ') AS BEOs," + - " GROUP_CONCAT(DISTINCT o.Name ORDER BY o.Name SEPARATOR ', ') AS Objekte" + - ' FROM logbuch l' + - ' LEFT JOIN (SELECT id, `kürzel` AS kuerzel FROM beos) creator ON creator.id = l.created_by' + - ' LEFT JOIN logbuch_beos lb ON lb.LogbuchID = l.ID' + - ' LEFT JOIN (SELECT id, `kürzel` AS kuerzel FROM beos) bk ON bk.id = lb.BeoID' + - ' LEFT JOIN logbuch_objekte lo ON lo.LogbuchID = l.ID' + - ' LEFT JOIN objekte o ON o.ID = lo.ObjektID' + - ' WHERE l.Kuppel = ?' + - ' GROUP BY l.ID'; +import * as phpdb from '@/lib/phpdb'; export async function GET(request: NextRequest) { const session = await getSession(); - if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 }); + if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 }); + const { searchParams } = new URL(request.url); const kuppel = searchParams.get('kuppel') || 'West'; - const limit = Math.min(parseInt(searchParams.get('limit') || '10') || 10, 500); + const limit = Math.min(parseInt(searchParams.get('limit') || '10') || 10, 500); const offset = Math.max(0, parseInt(searchParams.get('offset') || '0') || 0); - const month = searchParams.get('month') || ''; - const order = searchParams.get('order') === 'asc' ? 'ASC' : 'DESC'; - + const month = searchParams.get('month') || ''; + const order = searchParams.get('order') === 'asc' ? 'asc' : 'desc'; const search = (searchParams.get('search') || '').trim(); - if (search) { - const pattern = '%' + search + '%'; - const searchParams2 = [kuppel, pattern, pattern, pattern]; - const countSQL = - 'SELECT COUNT(*) AS total FROM (' + - 'SELECT l.ID FROM logbuch l' + - ' LEFT JOIN logbuch_beos lb ON lb.LogbuchID = l.ID' + - ' LEFT JOIN (SELECT id, `kürzel` AS kuerzel FROM beos) bk ON bk.id = lb.BeoID' + - ' LEFT JOIN logbuch_objekte lo ON lo.LogbuchID = l.ID' + - ' LEFT JOIN objekte o ON o.ID = lo.ObjektID' + - ' WHERE l.Kuppel = ?' + - ' GROUP BY l.ID' + - " HAVING (MAX(l.Bemerkungen) LIKE ? OR GROUP_CONCAT(DISTINCT bk.kuerzel ORDER BY bk.kuerzel SEPARATOR ', ') LIKE ? OR GROUP_CONCAT(DISTINCT o.Name ORDER BY o.Name SEPARATOR ', ') LIKE ?)" + - ') AS sub'; - const listSQL = LIST_SQL + - ' HAVING (MAX(l.Bemerkungen) LIKE ? OR BEOs LIKE ? OR Objekte LIKE ?)' + - ` ORDER BY l.Beginn DESC LIMIT ${limit} OFFSET ${offset}`; - try { - const [countRows, entries] = await Promise.all([ - query(countSQL, searchParams2) as Promise<{ total: number }[]>, - query(listSQL, searchParams2), - ]); - return NextResponse.json({ entries, total: (countRows as unknown as { total: number }[])[0]?.total ?? 0 }); - } catch (error) { - console.error('GET /api/logbuch (search):', error); - return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 }); - } - } - - let listWhere = 'WHERE l.Kuppel = ?'; - let countWhere = 'WHERE Kuppel = ?'; - let params: (string | number | null)[] = [kuppel]; - if (month && /^\d{4}-\d{2}$/.test(month)) { - const [y, m] = month.split('-').map(Number); - const start = `${y}-${String(m).padStart(2, '0')}-01`; - const nextM = m === 12 ? 1 : m + 1; - const nextY = m === 12 ? y + 1 : y; - const end = `${nextY}-${String(nextM).padStart(2, '0')}-01`; - listWhere += ' AND l.Beginn >= ? AND l.Beginn < ?'; - countWhere += ' AND Beginn >= ? AND Beginn < ?'; - params = [kuppel, start, end]; - } - try { - const [countRows, entries] = await Promise.all([ - query('SELECT COUNT(*) AS total FROM logbuch ' + countWhere, params) as Promise<{ total: number }[]>, - query(LIST_SQL.replace('WHERE l.Kuppel = ?', listWhere) + ` ORDER BY l.Beginn ${order} LIMIT ${limit} OFFSET ${offset}`, params), - ]); - return NextResponse.json({ entries, total: (countRows as unknown as { total: number }[])[0]?.total ?? 0 }); + const result = await phpdb.listLogbuch({ kuppel, limit, offset, month, search, order }); + return NextResponse.json(result); } catch (error) { console.error('GET /api/logbuch:', error); return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 }); @@ -99,47 +32,18 @@ export async function POST(request: NextRequest) { const body = await request.json(); const { Kuppel, ArtFuehrung, SonderName, Beginn, Ende, Besucher, beoIds, objekte, Bemerkungen, Wetter } = body; - const pool = getPool(); - const [result] = await pool.execute( - 'INSERT INTO logbuch (Kuppel, ArtFuehrung, SonderName, Beginn, Ende, Besucher, Bemerkungen, WetterTemp, WetterFeuchte, WetterDruck, created_by)' + - ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', - [ - Kuppel, ArtFuehrung, SonderName || null, Beginn, Ende, - Besucher ?? 0, - Bemerkungen?.slice(0, 500) || null, - Wetter?.temp ?? null, - Wetter?.feuchte ?? null, - Wetter?.druck ?? null, - session.beoId, - ] - ) as [{ insertId: number }, unknown]; - - const logbuchId = result.insertId; - - for (const beoId of (beoIds as number[]) || []) { - await query('INSERT INTO logbuch_beos (LogbuchID, BeoID) VALUES (?, ?)', [logbuchId, beoId]); - } - - for (const obj of (objekte as SelectedObjekt[]) || []) { - let objektId = obj.ID; - if (!objektId) { - const existing = await query('SELECT ID, Name FROM objekte WHERE LOWER(Name) = LOWER(?)', [obj.Name]) as { ID: number; Name: string }[]; - if (existing[0]) { - objektId = existing[0].ID; - } else { - const [ins] = await pool.execute('INSERT INTO objekte (Name) VALUES (?)', [obj.Name]) as [{ insertId: number }, unknown]; - objektId = ins.insertId; - } - } - await query('UPDATE objekte SET LastUsed = NOW() WHERE ID = ?', [objektId]); - await query( - 'INSERT INTO logbuch_objekte (LogbuchID, ObjektID) VALUES (?, ?)', - [logbuchId, objektId] - ); - } + const result = await phpdb.createLogbuch({ + Kuppel, ArtFuehrung, SonderName, Beginn, Ende, + Besucher: Besucher ?? 0, + beoIds: beoIds ?? [], + objekte: objekte ?? [], + Bemerkungen: Bemerkungen ?? null, + Wetter: Wetter ?? null, + created_by: session.beoId, + }); triggerBackup(); - return NextResponse.json({ id: logbuchId }, { status: 201 }); + return NextResponse.json(result, { status: 201 }); } catch (error) { console.error('POST /api/logbuch:', error); return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 }); diff --git a/app/api/objekte/[id]/route.ts b/app/api/objekte/[id]/route.ts index c9cd217..263dced 100644 --- a/app/api/objekte/[id]/route.ts +++ b/app/api/objekte/[id]/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; -import { query } from '@/lib/db'; import { getSession } from '@/lib/session'; +import * as phpdb from '@/lib/phpdb'; export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { const session = await getSession(); @@ -13,8 +13,8 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: const { name } = await req.json(); const trimmed = (name as string)?.trim(); if (!trimmed) return NextResponse.json({ error: 'Name darf nicht leer sein' }, { status: 400 }); - await query('UPDATE objekte SET Name = ? WHERE ID = ?', [trimmed, numId]); - return NextResponse.json({ ID: numId, Name: trimmed }); + const result = await phpdb.updateObjekt(numId, trimmed); + return NextResponse.json(result); } catch (error) { console.error('PUT /api/objekte/[id]:', error); return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 }); @@ -29,7 +29,7 @@ export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ const { id } = await params; const numId = Number(id); if (isNaN(numId)) return NextResponse.json({ error: 'Ungültige ID' }, { status: 400 }); - await query('DELETE FROM objekte WHERE ID = ?', [numId]); + await phpdb.deleteObjekt(numId); return NextResponse.json({ ok: true }); } catch (error) { console.error('DELETE /api/objekte/[id]:', error); diff --git a/app/api/objekte/route.ts b/app/api/objekte/route.ts index 0d50fbe..1e8a934 100644 --- a/app/api/objekte/route.ts +++ b/app/api/objekte/route.ts @@ -1,12 +1,12 @@ import { NextRequest, NextResponse } from 'next/server'; -import { query } from '@/lib/db'; import { getSession } from '@/lib/session'; +import * as phpdb from '@/lib/phpdb'; export async function GET() { const session = await getSession(); if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 }); try { - const rows = await query('SELECT ID, Name FROM objekte ORDER BY LastUsed DESC LIMIT 100'); + const rows = await phpdb.getObjekte(); return NextResponse.json(rows); } catch (error) { console.error('GET /api/objekte:', error); @@ -22,8 +22,8 @@ export async function POST(req: NextRequest) { const { name } = await req.json(); const trimmed = (name as string)?.trim(); if (!trimmed) return NextResponse.json({ error: 'Name darf nicht leer sein' }, { status: 400 }); - const result = await query('INSERT INTO objekte (Name) VALUES (?)', [trimmed]) as { insertId: number }; - return NextResponse.json({ ID: result.insertId, Name: trimmed }, { status: 201 }); + const result = await phpdb.createObjekt(trimmed); + return NextResponse.json(result, { status: 201 }); } catch (error) { console.error('POST /api/objekte:', error); return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 }); diff --git a/app/api/statistik/route.ts b/app/api/statistik/route.ts index 4be1943..fdf7f24 100644 --- a/app/api/statistik/route.ts +++ b/app/api/statistik/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; -import { query } from '@/lib/db'; import { getSession } from '@/lib/session'; +import * as phpdb from '@/lib/phpdb'; export async function GET(request: NextRequest) { const session = await getSession(); @@ -10,66 +10,8 @@ export async function GET(request: NextRequest) { const year = parseInt(searchParams.get('year') || String(new Date().getFullYear()), 10); try { - const monthlyRows = await query( - 'SELECT' + - ' MONTH(Beginn) AS monat,' + - " COUNT(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF') THEN 1 END) AS tageFuehrungen," + - " COUNT(CASE WHEN ArtFuehrung = 'Beob' THEN 1 END) AS tageBeob," + - " COUNT(CASE WHEN ArtFuehrung = 'TD' THEN 1 END) AS tageTD," + - " COUNT(CASE WHEN ArtFuehrung = 'Sonst' THEN 1 END) AS tageSonst," + - " COUNT(CASE WHEN ArtFuehrung = 'BEOS' THEN 1 END) AS tageBEOS," + - " COUNT(CASE WHEN ArtFuehrung = 'ToT' THEN 1 END) AS tagesToT," + - " COUNT(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF','Beob','TD','Sonst','BEOS','ToT') THEN 1 END) AS tageGesamt," + - " SUM(CASE WHEN ArtFuehrung = 'RF' THEN Besucher ELSE 0 END) AS besucherRF," + - " SUM(CASE WHEN ArtFuehrung = 'SF' THEN Besucher ELSE 0 END) AS besucherSF," + - " SUM(CASE WHEN ArtFuehrung = 'SonF' THEN Besucher ELSE 0 END) AS besucherSonF," + - " SUM(CASE WHEN ArtFuehrung = 'PrF' THEN Besucher ELSE 0 END) AS besucherPrF," + - " SUM(CASE WHEN ArtFuehrung = 'ToT' THEN Besucher ELSE 0 END) AS besucherToT," + - " SUM(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF','ToT') THEN Besucher ELSE 0 END) AS besucherGesamt" + - ' FROM logbuch' + - ' WHERE YEAR(Beginn) = ?' + - ' GROUP BY MONTH(Beginn)' + - ' ORDER BY monat', - [year] - ) as { - monat: number; - tageFuehrungen: number; tageBeob: number; tageTD: number; tageSonst: number; tageBEOS: number; tagesToT: number; tageGesamt: number; - besucherRF: number; besucherSF: number; besucherSonF: number; besucherPrF: number; - besucherToT: number; besucherGesamt: number; - }[]; - - const cumulativeRows = await query( - "SELECT SUM(CASE WHEN ArtFuehrung IN ('RF','SF','SonF','PrF','ToT') THEN Besucher ELSE 0 END) AS total" + - ' FROM logbuch WHERE YEAR(Beginn) = ?', - [year] - ) as { total: number | null }[]; - - const tageRows = await query( - "SELECT COUNT(*) AS tage FROM logbuch WHERE YEAR(Beginn) = ? AND ArtFuehrung IN ('RF','SF','SonF','PrF','Beob','TD','Sonst','BEOS','ToT')", - [year] - ) as { tage: number }[]; - - return NextResponse.json({ - monthly: monthlyRows.map((r) => ({ - monat: Number(r.monat), - tageFuehrungen: Number(r.tageFuehrungen), - tageBeob: Number(r.tageBeob), - tageTD: Number(r.tageTD), - tageSonst: Number(r.tageSonst), - tageBEOS: Number(r.tageBEOS), - tagesToT: Number(r.tagesToT), - tageGesamt: Number(r.tageGesamt), - besucherRF: Number(r.besucherRF), - besucherSF: Number(r.besucherSF), - besucherSonF: Number(r.besucherSonF), - besucherPrF: Number(r.besucherPrF), - besucherToT: Number(r.besucherToT), - besucherGesamt: Number(r.besucherGesamt), - })), - cumulative: Number(cumulativeRows[0]?.total ?? 0), - tage: Number(tageRows[0]?.tage ?? 0), - year, - }); + const result = await phpdb.getStatistik(year); + return NextResponse.json(result); } catch (error) { console.error('GET /api/statistik:', error); return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 }); diff --git a/app/change-password/actions.ts b/app/change-password/actions.ts index 7723da0..95a9616 100644 --- a/app/change-password/actions.ts +++ b/app/change-password/actions.ts @@ -3,7 +3,7 @@ import { redirect } from 'next/navigation'; import { getSession, createSession } from '@/lib/session'; import { hashPassword } from '@/lib/auth'; -import { query } from '@/lib/db'; +import { updateBeoPassword } from '@/lib/phpdb'; export async function changePassword( _prevState: { error: string } | undefined, @@ -28,10 +28,7 @@ export async function changePassword( } const hashed = await hashPassword(newPassword); - await query( - 'UPDATE beos SET pw = ?, MustChangePassword = 0 WHERE id = ?', - [hashed, session.beoId] - ); + await updateBeoPassword(session.beoId, hashed); await createSession({ kuerzel: session.kuerzel, diff --git a/compose.yml b/compose.yml index ff3375d..80435bd 100644 --- a/compose.yml +++ b/compose.yml @@ -1,79 +1,14 @@ services: - logbuch_mysql: - image: mysql:lts - container_name: logbuch_mysql - restart: unless-stopped - environment: - MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS} - MYSQL_DATABASE: ${DB_NAME} - MYSQL_USER: ${DB_USER} - MYSQL_PASSWORD: ${DB_PASS} - volumes: - - db_data:/var/lib/mysql - ports: - - "127.0.0.1:3336:3306" - networks: - - proxy - healthcheck: - test: - - CMD - - mysqladmin - - ping - - -h - - localhost - - -uroot - - -p${DB_ROOT_PASS} - interval: 10s - timeout: 5s - retries: 10 - # Kein Port nach außen — nur internes Netzwerk - - logbuch_phpmyadmin: - image: phpmyadmin:latest - container_name: logbuch_phpmyadmin - restart: unless-stopped - environment: - PMA_HOST: logbuch_mysql - PMA_PORT: 3306 - PMA_ABSOLUTE_URI: https://logbuch.fuerst-stuttgart.de/myadmin/ - depends_on: - logbuch_mysql: - condition: service_healthy - labels: - - traefik.enable=true - - traefik.http.routers.logbuch-pma.entrypoints=http - - traefik.http.routers.logbuch-pma.rule=Host(`logbuch.fuerst-stuttgart.de`) && PathPrefix(`/myadmin`) - - traefik.http.middlewares.logbuch-pma-https-redirect.redirectscheme.scheme=https - - traefik.http.routers.logbuch-pma.middlewares=logbuch-pma-https-redirect - - traefik.http.routers.logbuch-pma-secure.entrypoints=https - - traefik.http.routers.logbuch-pma-secure.rule=Host(`logbuch.fuerst-stuttgart.de`) && PathPrefix(`/myadmin`) - - traefik.http.routers.logbuch-pma-secure.tls=true - - traefik.http.routers.logbuch-pma-secure.middlewares=logbuch-pma-slash,logbuch-pma-strip - - traefik.http.middlewares.logbuch-pma-slash.redirectregex.regex=^https://logbuch\.fuerst-stuttgart\.de/myadmin$$ - - traefik.http.middlewares.logbuch-pma-slash.redirectregex.replacement=https://logbuch.fuerst-stuttgart.de/myadmin/ - - traefik.http.middlewares.logbuch-pma-strip.stripprefix.prefixes=/myadmin - - traefik.http.routers.logbuch-pma-secure.service=logbuch-pma - - traefik.http.services.logbuch-pma.loadbalancer.server.port=80 - networks: - - proxy - logbuch_app: image: docker.citysensor.de/logbuch:latest container_name: logbuch_app restart: unless-stopped environment: - DB_HOST: logbuch_mysql - DB_USER: ${DB_USER} - DB_PASS: ${DB_PASS} - DB_NAME: ${DB_NAME} - DB_PORT: 3306 + PHP_DB_URL: ${PHP_DB_URL} AUTH_SECRET: ${AUTH_SECRET} NODE_ENV: production ports: - 127.0.0.1:${APP_PORT:-3000}:3000 - depends_on: - logbuch_mysql: - condition: service_healthy labels: - traefik.enable=true - traefik.http.routers.logbuch.entrypoints=http @@ -88,6 +23,7 @@ services: networks: - proxy - gitea-internal + networks: proxy: name: dockge_default @@ -95,5 +31,3 @@ networks: gitea-internal: name: gitea_gitea-internal external: true -volumes: - db_data: null diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index e15a605..61cb8b7 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,64 +1,19 @@ services: - - logbuch_mysql: - image: mysql:lts - container_name: logbuch_mysql - restart: unless-stopped - environment: - MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS} - MYSQL_DATABASE: ${DB_NAME} - MYSQL_USER: ${DB_USER} - MYSQL_PASSWORD: ${DB_PASS} - volumes: - - db_data:/var/lib/mysql - networks: - - logbuch_net - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${DB_ROOT_PASS}"] - interval: 10s - timeout: 5s - retries: 10 - # Kein Port nach außen — nur internes Netzwerk - - logbuch_phpmyadmin: - image: phpmyadmin:latest - container_name: logbuch_phpmyadmin - restart: unless-stopped - environment: - PMA_HOST: logbuch_mysql - PMA_PORT: 3306 - PMA_ABSOLUTE_URI: https://logbuch.fuerst-stuttgart.de/myadmin/ - ports: - - "127.0.0.1:${PMA_PORT:-8080}:80" - depends_on: - logbuch_mysql: - condition: service_healthy - networks: - - logbuch_net - logbuch_app: image: docker.citysensor.de/logbuch:latest container_name: logbuch_app restart: unless-stopped env_file: .env environment: - DB_HOST: logbuch_mysql - DB_PORT: 3306 NODE_ENV: production BACKUP_SSH_KEY_PATH: /run/secrets/backup_ssh_key volumes: - ${BACKUP_SSH_KEY_FILE:-/dev/null}:/run/secrets/backup_ssh_key:ro ports: - "127.0.0.1:${APP_PORT:-3000}:3000" - depends_on: - logbuch_mysql: - condition: service_healthy networks: - logbuch_net networks: logbuch_net: driver: bridge - -volumes: - db_data: diff --git a/lib/auth.ts b/lib/auth.ts index 19771dd..d3c5748 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -1,34 +1,13 @@ import bcrypt from 'bcryptjs'; -import { query } from './db'; +import { getBeoByKuerzel, getBeoByName } from './phpdb'; -export interface Beo { - id: number; - name: string; - vorname: string | null; - kürzel: string | null; - pw: string | null; - MustChangePassword: number; - role: string | null; -} - -export async function getBeoByKuerzel(kuerzel: string): Promise { - const rows = await query( - 'SELECT id, name, vorname, `kürzel`, pw, MustChangePassword, role FROM beos WHERE `kürzel` = ?', - [kuerzel] - ) as Beo[]; - return rows[0] ?? null; -} +export type { Beo } from './phpdb'; +import type { Beo } from './phpdb'; export async function getBeoByLogin(login: string): Promise { - // First try exact Kürzel match, then case-insensitive Nachname match const byKuerzel = await getBeoByKuerzel(login); if (byKuerzel) return byKuerzel; - - const rows = await query( - 'SELECT id, name, vorname, `kürzel`, pw, MustChangePassword, role FROM beos WHERE LOWER(name) = LOWER(?)', - [login] - ) as Beo[]; - return rows[0] ?? null; + return getBeoByName(login); } export async function verifyCredentials( @@ -41,8 +20,7 @@ export async function verifyCredentials( if (!beo.pw) { const defaultPw = process.env.DEFAULT_PASSWORD; if (!defaultPw) throw new Error('DEFAULT_PASSWORD Umgebungsvariable ist nicht gesetzt!'); - const valid = password === defaultPw; - return { beo, valid }; + return { beo, valid: password === defaultPw }; } const valid = await bcrypt.compare(password, beo.pw); diff --git a/lib/backup.ts b/lib/backup.ts index 5ad6890..bbbfcc7 100644 --- a/lib/backup.ts +++ b/lib/backup.ts @@ -2,15 +2,14 @@ import { createWriteStream, mkdirSync, unlinkSync } from 'fs'; import { createGzip } from 'zlib'; import { join } from 'path'; import { spawn } from 'child_process'; -import { getPool } from './db'; +import { getBackupData } from './phpdb'; export function triggerBackup(): void { setImmediate(() => runBackup().catch((e) => console.error('[backup] Fehler:', e))); } async function dumpToFile(filePath: string): Promise { - const dbName = process.env.DB_NAME || 'sternwarte'; - const pool = getPool(); + const { tables } = await getBackupData(); const gzip = createGzip(); const file = createWriteStream(filePath); @@ -21,21 +20,12 @@ async function dumpToFile(filePath: string): Promise { ); const now = new Date().toISOString(); - await write(`-- Führungsbuch Backup ${now}\n-- Datenbank: ${dbName} (ohne Tabelle beos)\n\nSET FOREIGN_KEY_CHECKS=0;\n\n`); + await write(`-- Führungsbuch Backup ${now}\n-- Logbuch-Tabellen\n\nSET FOREIGN_KEY_CHECKS=0;\n\n`); - const [tableRows] = await pool.query('SHOW TABLES') as [Record[], unknown]; - const tables = tableRows - .map((r) => Object.values(r)[0]) - .filter((t) => t !== 'beos'); - - for (const table of tables) { - const [[createRow]] = await pool.query(`SHOW CREATE TABLE \`${table}\``) as [Record[], unknown]; - const createSql = Object.values(createRow)[1]; - - await write(`DROP TABLE IF EXISTS \`${table}\`;\n`); + for (const { name, createSql, rows } of tables) { + await write(`DROP TABLE IF EXISTS \`${name}\`;\n`); await write(`${createSql};\n\n`); - const [rows] = await pool.query(`SELECT * FROM \`${table}\``) as [Record[], unknown]; if (rows.length > 0) { const cols = Object.keys(rows[0]).map((c) => `\`${c}\``).join(', '); const batchSize = 200; @@ -45,11 +35,10 @@ async function dumpToFile(filePath: string): Promise { '(' + Object.values(row).map((v) => { if (v === null) return 'NULL'; if (typeof v === 'number') return String(v); - if (v instanceof Date) return `'${v.toISOString().slice(0, 19).replace('T', ' ')}'`; return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`; }).join(', ') + ')' ).join(',\n '); - await write(`INSERT INTO \`${table}\` (${cols}) VALUES\n ${values};\n`); + await write(`INSERT INTO \`${name}\` (${cols}) VALUES\n ${values};\n`); } await write('\n'); } @@ -99,7 +88,6 @@ async function runBackup(): Promise { '-o', 'ConnectTimeout=15', ]; - // Zielverzeichnis auf Remote anlegen falls nicht vorhanden await new Promise((resolve, reject) => { const ssh = spawn('ssh', [...sshOpts, sshHost, `mkdir -p ${remotePath}`]); ssh.on('error', reject); @@ -118,7 +106,6 @@ async function runBackup(): Promise { console.log(`[backup] ${filename} → ${sshHost}:${remotePath}`); - // Backups älter als 30 Tage auf Remote löschen await new Promise((resolve) => { const ssh = spawn('ssh', [ ...sshOpts, sshHost, diff --git a/lib/db.ts b/lib/db.ts deleted file mode 100644 index dd8bef3..0000000 --- a/lib/db.ts +++ /dev/null @@ -1,29 +0,0 @@ -import mysql from 'mysql2/promise'; -import type { QueryResult } from 'mysql2/promise'; - -const dbConfig = { - host: process.env.DB_HOST || 'mydbase_mysql', - port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306, - user: process.env.DB_USER, - password: process.env.DB_PASS, - database: process.env.DB_NAME || 'logbuch', - charset: 'utf8mb4', - waitForConnections: true, - connectionLimit: 10, - queueLimit: 0, -}; - -let pool: mysql.Pool | null = null; - -export function getPool() { - if (!pool) { - pool = mysql.createPool(dbConfig); - } - return pool; -} - -export async function query(sql: string, params?: (string | number | null)[]): Promise { - const p = getPool(); - const [rows] = await p.execute(sql, params || []); - return rows as QueryResult; -} diff --git a/lib/phpdb.ts b/lib/phpdb.ts new file mode 100644 index 0000000..1ac62b1 --- /dev/null +++ b/lib/phpdb.ts @@ -0,0 +1,185 @@ +import type { BeoOption, LogbuchEintrag, ObjektOption, SelectedObjekt, Wetter } from '@/types/logbuch'; + +const PHP_DB_URL = process.env.PHP_DB_URL ?? 'http://localhost:8080/DB4js_all.php'; + +async function call(cmd: string, params: object = {}): Promise { + const res = await fetch(PHP_DB_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cmd, ...params }), + cache: 'no-store', + }); + if (!res.ok) { + let detail = ''; + try { detail = await res.text(); } catch { /* ignore */ } + throw new Error(`DB4js ${cmd} HTTP ${res.status}: ${detail}`); + } + const json: unknown = await res.json(); + if (json && typeof json === 'object' && 'error' in json) { + throw new Error(`DB4js ${cmd}: ${(json as { error: string }).error}`); + } + return json as T; +} + +// ---- Auth / Benutzer ---- + +export interface Beo { + id: number; + name: string; + vorname: string | null; + 'kürzel': string | null; + pw: string | null; + MustChangePassword: number; + role: string | null; +} + +export interface BeoUser { + id: number; + 'kürzel': string | null; + name: string; + vorname: string | null; + role: string | null; + hasPw: boolean; +} + +export async function getBeoByKuerzel(kuerzel: string): Promise { + const r = await call<{ beo: Beo | null }>('LB_AUTH_KUERZEL', { kuerzel }); + return r.beo; +} + +export async function getBeoByName(name: string): Promise { + const r = await call<{ beo: Beo | null }>('LB_AUTH_NAME', { name }); + return r.beo; +} + +export async function updateBeoPassword(id: number, pwHash: string): Promise { + await call('LB_UPDATE_PW', { id, pw: pwHash }); +} + +export async function resetBeoPassword(id: number): Promise { + await call('LB_RESET_PW', { id }); +} + +export async function listUsers(): Promise { + return call('LB_LIST_USERS'); +} + +// ---- Logbuch ---- + +export interface ListLogbuchParams { + kuppel?: string; + limit?: number; + offset?: number; + month?: string; + search?: string; + order?: string; +} + +export async function listLogbuch( + params: ListLogbuchParams +): Promise<{ entries: LogbuchEintrag[]; total: number }> { + return call('LB_LIST_LOGBUCH', params); +} + +export interface CreateLogbuchData { + Kuppel: string; + ArtFuehrung: string; + SonderName?: string | null; + Beginn: string; + Ende: string; + Besucher?: number; + beoIds?: number[]; + objekte?: SelectedObjekt[]; + Bemerkungen?: string | null; + Wetter?: Partial | null; + created_by: number; +} + +export async function createLogbuch(data: CreateLogbuchData): Promise<{ id: number }> { + return call('LB_CREATE_LOGBUCH', data); +} + +export async function updateLogbuch( + id: number, + userId: number, + userRole: string, + data: Omit +): Promise { + await call('LB_UPDATE_LOGBUCH', { id, user_id: userId, user_role: userRole, ...data }); +} + +export async function deleteLogbuch( + id: number, + userId: number, + userRole: string +): Promise { + await call('LB_DELETE_LOGBUCH', { id, user_id: userId, user_role: userRole }); +} + +// ---- BEOs & Objekte ---- + +export async function getBeos(): Promise { + return call('LB_GET_BEOS'); +} + +export async function getObjekte(): Promise { + return call('LB_GET_OBJEKTE'); +} + +export async function createObjekt(name: string): Promise { + return call('LB_CREATE_OBJEKT', { name }); +} + +export async function updateObjekt(id: number, name: string): Promise { + return call('LB_UPDATE_OBJEKT', { id, name }); +} + +export async function deleteObjekt(id: number): Promise { + await call('LB_DELETE_OBJEKT', { id }); +} + +export async function listObjekteAdmin(): Promise<{ ID: number; Name: string; LastUsed: string | null }[]> { + return call('LB_LIST_OBJEKTE_ADMIN'); +} + +// ---- Auswertungen ---- + +export interface FahrkostenRow { + ID: number; + Kuerzel: string; + Name: string; + Anzahl: number; +} + +export async function getFahrkosten(ab: string): Promise { + return call('LB_FAHRKOSTEN', { ab }); +} + +export interface StatistikResult { + monthly: { + monat: number; + tageFuehrungen: number; tageBeob: number; tageTD: number; + tageSonst: number; tageBEOS: number; tagesToT: number; tageGesamt: number; + besucherRF: number; besucherSF: number; besucherSonF: number; + besucherPrF: number; besucherToT: number; besucherGesamt: number; + }[]; + cumulative: number; + tage: number; + year: number; +} + +export async function getStatistik(year: number): Promise { + return call('LB_STATISTIK', { year }); +} + +// ---- Backup ---- + +export interface BackupTable { + name: string; + createSql: string; + rows: Record[]; +} + +export async function getBackupData(): Promise<{ tables: BackupTable[] }> { + return call('LB_BACKUP_DATA'); +} diff --git a/package.json b/package.json index 0b2d796..68c33d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "logbuch", - "version": "1.9.1", + "version": "1.10.0", "private": true, "scripts": { "dev": "next dev", @@ -11,8 +11,7 @@ "dependencies": { "bcryptjs": "^3.0.3", "jose": "^6.2.2", - "mysql2": "^3.22.3", - "next": "16.1.6", +"next": "16.1.6", "react": "19.2.3", "react-dom": "19.2.3" },