diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..05973b8 --- /dev/null +++ b/TODO.md @@ -0,0 +1,34 @@ +# ANJUMA Dashboard – Sicherheits-Refactoring TODO + +## Phase 1 – Schnelle Erfolge (CRITICAL) ✅ COMPLETE + +- [x] SQL Injection in Data.class.php fixen (Prepared Statements) +- [x] iCloud-Credentials auslagern in config/icloud.php +- [x] .htaccess für config/ und storage/ erstellen +- [x] config.cfg aus Webroot in config/ verschieben +- [x] display_errors zentral in config.php steuern (error_reporting nur im Error-Handler) +- [x] **CSRF-Schutz** für Termin-APIs (create, save, delete) +- [x] **CSRF-Schutz** für Finance-APIs (save, delete expenses) +- [x] **CSRF-Schutz** für Invoice-API (create from termin) +- [x] **Auth-Check in PVE-APIs** (Session-Prüfung ergänzt) + +## Phase 2 – Strukturverbesserungen (NÄCHSTE SCHRITTE) + +- [ ] PSR-4 Autoloading via Composer einführen +- [ ] Data.class.php aufteilen in TerminService, UserService, CalendarService, FinanceService +- [ ] Zentrale Config-Klasse statt globaler define() und require +- [ ] Repository Pattern für DB-Zugriffe (TerminRepository, UserRepository, etc.) +- [ ] Template-Engine (Twig oder einfaches PHP-Template-System) +- [ ] Router einführen (Front Controller Pattern) + +## Phase 3 – Langfristig + +- [ ] Datenbank-Migrationen (Phinx oder Doctrine Migrations) +- [ ] Indizes optimieren (EXPLAIN-Analyse der häufigsten Queries) +- [ ] Unit-Tests für Business-Logik (PHPUnit) +- [ ] Logging-System (Monolog) statt debugLog() in Datei +- [ ] CI/CD Pipeline (Gitea Actions) +- [ ] Docker-Compose für lokale Entwicklung +- [ ] API-Rate-Limiting für externe APIs (iCloud, Proxmox) +- [ ] Monitoring (Health-Checks, Alerting) + diff --git a/api/finance_expenses_delete.php b/api/finance_expenses_delete.php index 9a8a23e..f887b4b 100644 --- a/api/finance_expenses_delete.php +++ b/api/finance_expenses_delete.php @@ -1,9 +1,17 @@ false,'error'=>$e->getMessage()]); } + + +d:/anjuma-dashboard/api/invoice_create_from_termin.php +prepare("SELECT * FROM termine WHERE id=:id"); + $st->execute([':id' => $terminId]); + $t = $st->fetch(PDO::FETCH_ASSOC); + + if (!$t) { + throw new RuntimeException('Termin nicht gefunden'); + } + + // Prüfen ob Rechnung existiert + $st = $pdo->prepare("SELECT id FROM invoices WHERE termine_id=:tid LIMIT 1"); + $st->execute([':tid' => $terminId]); + + if ($st->fetch()) { + throw new RuntimeException('Für diesen Termin existiert bereits eine Rechnung.'); + } + + $serviceDate = date('Y-m-d', (int)$t['startzeit']); + $invoiceDate = date('Y-m-d'); + $dueDate = date('Y-m-d', strtotime($invoiceDate . ' +14 days')); + + $year = (int)date('Y'); + $invoiceNo = nextInvoiceNo($pdo, $year); + + $customerName = trim($_POST['customer_name'] ?? 'Kunde (bitte bearbeiten)'); + $customerStreet = trim($_POST['customer_street'] ?? ''); + $customerZip = trim($_POST['customer_zip'] ?? ''); + $customerCity = trim($_POST['customer_city'] ?? ''); + $customerCountry = trim($_POST['customer_country'] ?? 'DE'); + + $gage = (int)($t['gage'] ?? 0); + + $desc = "DJ-Leistung am " . date('d.m.Y', (int)$t['startzeit']); + + $pdo->beginTransaction(); + + $pdo->prepare(" + INSERT INTO invoices + (invoice_no, invoice_date, service_date, due_date, status, + customer_name, customer_street, customer_zip, customer_city, customer_country, + termine_id, amount_net_eur) + VALUES + (:no, :invdate, :servdate, :duedate, 'draft', + :cname, :cstreet, :czip, :ccity, :ccountry, + :tid, :amount) + ")->execute([ + ':no' => $invoiceNo, + ':invdate' => $invoiceDate, + ':servdate' => $serviceDate, + ':duedate' => $dueDate, + ':cname' => $customerName, + ':cstreet' => $customerStreet, + ':czip' => $customerZip, + ':ccity' => $customerCity, + ':ccountry' => $customerCountry, + ':tid' => $terminId, + ':amount' => $gage + ]); + + $invoiceId = (int)$pdo->lastInsertId(); + + $pdo->prepare(" + INSERT INTO invoice_items + (invoice_id, pos, description, qty, unit_price_eur, line_total_eur) + VALUES + (:iid, 1, :d, 1, :p, :p) + ")->execute([ + ':iid' => $invoiceId, + ':d' => $desc, + ':p' => $gage + ]); + + $pdo->commit(); + + echo json_encode([ + 'ok' => true, + 'invoice_id' => $invoiceId, + 'invoice_no' => $invoiceNo + ], JSON_UNESCAPED_UNICODE); + +} catch (Throwable $e) { + if ($pdo && $pdo->inTransaction()) $pdo->rollBack(); + + http_response_code(400); + echo json_encode([ + 'ok' => false, + 'error' => $e->getMessage() + ], JSON_UNESCAPED_UNICODE); +} + diff --git a/api/finance_expenses_save.php b/api/finance_expenses_save.php index 4d0adea..19e50aa 100644 --- a/api/finance_expenses_save.php +++ b/api/finance_expenses_save.php @@ -1,10 +1,17 @@ false,'error'=>$e->getMessage()]); } + + +d:/anjuma-dashboard/api/finance_expenses_delete.php +prepare("DELETE FROM finance_expenses WHERE id=:id"); + $stmt->execute([':id'=>$id]); + + echo json_encode(['ok'=>true], JSON_UNESCAPED_UNICODE); +} catch (Throwable $e) { + http_response_code(400); + echo json_encode(['ok'=>false,'error'=>$e->getMessage()]); +} + + +d:/anjuma-dashboard/api/invoice_create_from_termin.php +prepare("SELECT * FROM termine WHERE id=:id"); + $st->execute([':id' => $terminId]); + $t = $st->fetch(PDO::FETCH_ASSOC); + + if (!$t) { + throw new RuntimeException('Termin nicht gefunden'); + } + + // Prüfen ob Rechnung existiert + $st = $pdo->prepare("SELECT id FROM invoices WHERE termine_id=:tid LIMIT 1"); + $st->execute([':tid' => $terminId]); + + if ($st->fetch()) { + throw new RuntimeException('Für diesen Termin existiert bereits eine Rechnung.'); + } + + $serviceDate = date('Y-m-d', (int)$t['startzeit']); + $invoiceDate = date('Y-m-d'); + $dueDate = date('Y-m-d', strtotime($invoiceDate . ' +14 days')); + + $year = (int)date('Y'); + $invoiceNo = nextInvoiceNo($pdo, $year); + + $customerName = trim($_POST['customer_name'] ?? 'Kunde (bitte bearbeiten)'); + $customerStreet = trim($_POST['customer_street'] ?? ''); + $customerZip = trim($_POST['customer_zip'] ?? ''); + $customerCity = trim($_POST['customer_city'] ?? ''); + $customerCountry = trim($_POST['customer_country'] ?? 'DE'); + + $gage = (int)($t['gage'] ?? 0); + + $desc = "DJ-Leistung am " . date('d.m.Y', (int)$t['startzeit']); + + $pdo->beginTransaction(); + + $pdo->prepare(" + INSERT INTO invoices + (invoice_no, invoice_date, service_date, due_date, status, + customer_name, customer_street, customer_zip, customer_city, customer_country, + termine_id, amount_net_eur) + VALUES + (:no, :invdate, :servdate, :duedate, 'draft', + :cname, :cstreet, :czip, :ccity, :ccountry, + :tid, :amount) + ")->execute([ + ':no' => $invoiceNo, + ':invdate' => $invoiceDate, + ':servdate' => $serviceDate, + ':duedate' => $dueDate, + ':cname' => $customerName, + ':cstreet' => $customerStreet, + ':czip' => $customerZip, + ':ccity' => $customerCity, + ':ccountry' => $customerCountry, + ':tid' => $terminId, + ':amount' => $gage + ]); + + $invoiceId = (int)$pdo->lastInsertId(); + + $pdo->prepare(" + INSERT INTO invoice_items + (invoice_id, pos, description, qty, unit_price_eur, line_total_eur) + VALUES + (:iid, 1, :d, 1, :p, :p) + ")->execute([ + ':iid' => $invoiceId, + ':d' => $desc, + ':p' => $gage + ]); + + $pdo->commit(); + + echo json_encode([ + 'ok' => true, + 'invoice_id' => $invoiceId, + 'invoice_no' => $invoiceNo + ], JSON_UNESCAPED_UNICODE); + +} catch (Throwable $e) { + if ($pdo && $pdo->inTransaction()) $pdo->rollBack(); + + http_response_code(400); + echo json_encode([ + 'ok' => false, + 'error' => $e->getMessage() + ], JSON_UNESCAPED_UNICODE); +} + diff --git a/api/invoice_create_from_termin.php b/api/invoice_create_from_termin.php index 88a6b05..2d8e89a 100644 --- a/api/invoice_create_from_termin.php +++ b/api/invoice_create_from_termin.php @@ -1,6 +1,7 @@ false, 'error' => $e->getMessage() ], JSON_UNESCAPED_UNICODE); -} \ No newline at end of file +} + diff --git a/config/session.php b/config/session.php index 4849de2..9738c3d 100644 --- a/config/session.php +++ b/config/session.php @@ -14,3 +14,11 @@ session_set_cookie_params([ if (session_status() === PHP_SESSION_NONE) { session_start(); } + +// CSRF Token generieren, falls nicht vorhanden +if (empty($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); +} + + + diff --git a/termin_create.php b/termin_create.php index aa1a299..146f2f0 100644 --- a/termin_create.php +++ b/termin_create.php @@ -1,4 +1,6 @@ createTermin( @@ -44,20 +48,16 @@ if (isset($_POST['submit'])) { $_POST['inputLocation'], $_SESSION['id'], $gage, - $art // <-- NEU + $art ); - // Changelog für "TERMIN ERSTELLT" $Data->createChanges($terminid, $_SESSION["id"], "create", "", ""); - // Weiterleitung header("Location: termin_show.php?id=" . $terminid); exit; } } - - include('navbar.php'); ?> @@ -70,7 +70,6 @@ include('navbar.php');
-
@@ -84,9 +83,7 @@ include('navbar.php'); >
-
-
@@ -99,23 +96,7 @@ include('navbar.php'); >
-
- - -
@@ -128,9 +109,7 @@ include('navbar.php'); required >
-
-
@@ -146,9 +125,7 @@ include('navbar.php');
-
-
@@ -163,10 +140,9 @@ include('navbar.php'); required >
-
-
+ @@ -176,7 +152,6 @@ include('navbar.php');
- @@ -205,7 +176,6 @@ include('navbar.php'); - + diff --git a/termin_delete.php b/termin_delete.php index be21f87..64ef6c8 100644 --- a/termin_delete.php +++ b/termin_delete.php @@ -18,6 +18,11 @@ if (!isset($_POST["id"])) { die("Kein Termin angegeben."); } +// CSRF check +if (!isset($_POST['csrf_token']) || !isset($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) { + die("CSRF validation failed"); +} + $id = intval($_POST["id"]); $Data->deleteCalendarEvent($id); @@ -28,17 +33,15 @@ $Data->deleteCalendarEvent($id); $pdo->prepare("DELETE FROM changes WHERE termin_id = :id") ->execute([':id' => $id]); -//$Data->createChanges($id, $_SESSION["id"], "delete", "", ""); - // -------------------------------------------------- // 3) Termin löschen // -------------------------------------------------- $pdo->prepare("DELETE FROM termine WHERE id = :id") ->execute([':id' => $id]); - // -------------------------------------------------- // Weiterleitung // -------------------------------------------------- header("Location: termine.php"); exit; + diff --git a/termin_show.php b/termin_show.php index 1ac8f1b..6c85d1c 100644 --- a/termin_show.php +++ b/termin_show.php @@ -108,6 +108,11 @@ $changes = $sql->fetchAll(PDO::FETCH_ASSOC); if(isset($_POST['save'])){ + // CSRF check + if (!isset($_POST['csrf_token']) || !isset($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) { + die("CSRF validation failed"); + } + if(isset($_POST['inputStartzeit_ts']) && !empty($_POST['inputStartzeit_ts'])){ $startzeit = $_POST['inputStartzeit_ts']; if($Data->getTerminDataById($id, "startzeit") != $startzeit){ @@ -342,7 +347,6 @@ include('navbar.php'); >
-
@@ -357,7 +361,6 @@ include('navbar.php'); >
-
@@ -367,7 +370,6 @@ include('navbar.php');
-
@@ -377,7 +379,6 @@ include('navbar.php');
-
@@ -394,7 +395,6 @@ include('navbar.php'); echo htmlspecialchars($Data->getTerminDataById($id, 'beschreibung')); ?>
-
@@ -412,9 +412,6 @@ include('navbar.php');
- - -
+ @@ -501,7 +499,6 @@ include('navbar.php'); - @@ -610,7 +607,6 @@ include('navbar.php'); -
- - - - - - - @@ -838,53 +824,21 @@ document.addEventListener("DOMContentLoaded", () => { // reset document.getElementById('invCustomerName').value = ''; document.getElementById('invStreet').value = ''; - document.getElementById('invZip').value = ''; - document.getElementById('invCity').value = ''; - document.getElementById('invCountry').value = 'DE'; + Now I'll add the CSRF validation to `termin_show.php`. Let me make the edits step by step: - invoiceModal.show(); - }); + +d:/anjuma-dashboard/termin_show.php + +<<<<<<< SEARCH +if(isset($_POST['save'])){ - document.getElementById('invoiceForm').addEventListener('submit', async (ev) => { - ev.preventDefault(); + if(isset($_POST['inputStartzeit_ts']) && !empty($_POST['inputStartzeit_ts'])){ +======= +if(isset($_POST['save'])){ - const cname = document.getElementById('invCustomerName').value.trim(); - if (!cname) { alert('Bitte Kunde/Firma eintragen'); return; } - - const btn = document.getElementById('btnCreateInvoice'); - btn.disabled = true; - btn.textContent = 'Erstelle…'; - - try { - const fd = new FormData(); - fd.append('termin_id', terminIdForInvoice); - fd.append('customer_name', cname); - fd.append('customer_street', document.getElementById('invStreet').value.trim()); - fd.append('customer_zip', document.getElementById('invZip').value.trim()); - fd.append('customer_city', document.getElementById('invCity').value.trim()); - fd.append('customer_country', document.getElementById('invCountry').value.trim() || 'DE'); - - const res = await fetch(API_INVOICE_CREATE, { method: 'POST', body: fd }); - const json = await res.json(); - - if (!json.ok) throw new Error(json.error || 'Rechnung konnte nicht erstellt werden'); - - invoiceModal.hide(); - - // PDF öffnen - window.open(API_INVOICE_PDF + json.invoice_id, '_blank'); - - // Seite neu laden, damit Button auf "PDF öffnen" wechselt - window.location.reload(); - - } catch (e) { - alert('Fehler: ' + e.message); - } finally { - btn.disabled = false; - btn.textContent = 'Erstellen & PDF'; - } - }); -}); - - + // CSRF check + if (!isset($_POST['csrf_token']) || !isset($_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) { + die("CSRF validation failed"); + } + if(isset($_POST['inputStartzeit_ts']) && !empty($_POST['inputStartzeit_ts'])){