feat: Version 1.10.0 — DB-Zugriff auf PHP-Bridge (DB4js_all.php) umgestellt
- 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 <noreply@anthropic.com>
This commit is contained in:
+5
-27
@@ -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<Beo | null> {
|
||||
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<Beo | null> {
|
||||
// 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);
|
||||
|
||||
+6
-19
@@ -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<void> {
|
||||
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<void> {
|
||||
);
|
||||
|
||||
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<string, string>[], 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<string, string>[], 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<string, unknown>[], 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<void> {
|
||||
'(' + 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<void> {
|
||||
'-o', 'ConnectTimeout=15',
|
||||
];
|
||||
|
||||
// Zielverzeichnis auf Remote anlegen falls nicht vorhanden
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const ssh = spawn('ssh', [...sshOpts, sshHost, `mkdir -p ${remotePath}`]);
|
||||
ssh.on('error', reject);
|
||||
@@ -118,7 +106,6 @@ async function runBackup(): Promise<void> {
|
||||
|
||||
console.log(`[backup] ${filename} → ${sshHost}:${remotePath}`);
|
||||
|
||||
// Backups älter als 30 Tage auf Remote löschen
|
||||
await new Promise<void>((resolve) => {
|
||||
const ssh = spawn('ssh', [
|
||||
...sshOpts, sshHost,
|
||||
|
||||
@@ -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<QueryResult> {
|
||||
const p = getPool();
|
||||
const [rows] = await p.execute(sql, params || []);
|
||||
return rows as QueryResult;
|
||||
}
|
||||
+185
@@ -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<T>(cmd: string, params: object = {}): Promise<T> {
|
||||
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<Beo | null> {
|
||||
const r = await call<{ beo: Beo | null }>('LB_AUTH_KUERZEL', { kuerzel });
|
||||
return r.beo;
|
||||
}
|
||||
|
||||
export async function getBeoByName(name: string): Promise<Beo | null> {
|
||||
const r = await call<{ beo: Beo | null }>('LB_AUTH_NAME', { name });
|
||||
return r.beo;
|
||||
}
|
||||
|
||||
export async function updateBeoPassword(id: number, pwHash: string): Promise<void> {
|
||||
await call('LB_UPDATE_PW', { id, pw: pwHash });
|
||||
}
|
||||
|
||||
export async function resetBeoPassword(id: number): Promise<void> {
|
||||
await call('LB_RESET_PW', { id });
|
||||
}
|
||||
|
||||
export async function listUsers(): Promise<BeoUser[]> {
|
||||
return call<BeoUser[]>('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<Wetter> | 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<CreateLogbuchData, 'created_by'>
|
||||
): Promise<void> {
|
||||
await call('LB_UPDATE_LOGBUCH', { id, user_id: userId, user_role: userRole, ...data });
|
||||
}
|
||||
|
||||
export async function deleteLogbuch(
|
||||
id: number,
|
||||
userId: number,
|
||||
userRole: string
|
||||
): Promise<void> {
|
||||
await call('LB_DELETE_LOGBUCH', { id, user_id: userId, user_role: userRole });
|
||||
}
|
||||
|
||||
// ---- BEOs & Objekte ----
|
||||
|
||||
export async function getBeos(): Promise<BeoOption[]> {
|
||||
return call('LB_GET_BEOS');
|
||||
}
|
||||
|
||||
export async function getObjekte(): Promise<ObjektOption[]> {
|
||||
return call('LB_GET_OBJEKTE');
|
||||
}
|
||||
|
||||
export async function createObjekt(name: string): Promise<ObjektOption> {
|
||||
return call('LB_CREATE_OBJEKT', { name });
|
||||
}
|
||||
|
||||
export async function updateObjekt(id: number, name: string): Promise<ObjektOption> {
|
||||
return call('LB_UPDATE_OBJEKT', { id, name });
|
||||
}
|
||||
|
||||
export async function deleteObjekt(id: number): Promise<void> {
|
||||
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<FahrkostenRow[]> {
|
||||
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<StatistikResult> {
|
||||
return call('LB_STATISTIK', { year });
|
||||
}
|
||||
|
||||
// ---- Backup ----
|
||||
|
||||
export interface BackupTable {
|
||||
name: string;
|
||||
createSql: string;
|
||||
rows: Record<string, string | number | null>[];
|
||||
}
|
||||
|
||||
export async function getBackupData(): Promise<{ tables: BackupTable[] }> {
|
||||
return call('LB_BACKUP_DATA');
|
||||
}
|
||||
Reference in New Issue
Block a user