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));
+}
+||DSML||parameter>
+||DSML||invoke>
+||DSML||tool_calls>
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');
-
@@ -189,15 +164,11 @@ include('navbar.php');
Für dieses Datum existiert bereits mindestens ein Termin:
-
-
-
-
@@ -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');
-
-
-
-
@@ -680,15 +672,13 @@ include('navbar.php');
-
-
-
@@ -702,15 +692,11 @@ include('navbar.php');
Für dieses Datum existiert bereits mindestens ein Termin:
-
-
-
-
@@ -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'])){