From 23416ee382d20e3739848652895086014b587b84 Mon Sep 17 00:00:00 2001 From: PBBFA24AAL Date: Fri, 10 Jul 2026 16:18:31 +0200 Subject: [PATCH] Initial habit tracker project --- .gitignore | 3 + README.md | 75 ++++++++ app/Controllers/AuthController.php | 75 ++++++++ app/Controllers/HabitController.php | 137 ++++++++++++++ app/Core/Database.php | 35 ++++ app/Core/View.php | 15 ++ app/Models/Category.php | 11 ++ app/Models/Habit.php | 110 +++++++++++ app/Models/User.php | 25 +++ app/Views/auth/login.php | 15 ++ app/Views/auth/register.php | 19 ++ app/Views/habits/dashboard.php | 67 +++++++ app/Views/habits/form.php | 58 ++++++ app/Views/layout/footer.php | 3 + app/Views/layout/header.php | 34 ++++ app/helpers.php | 82 +++++++++ config/config.example.php | 13 ++ database/schema.sql | 55 ++++++ public/assets/css/style.css | 275 ++++++++++++++++++++++++++++ public/index.php | 50 +++++ 20 files changed, 1157 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app/Controllers/AuthController.php create mode 100644 app/Controllers/HabitController.php create mode 100644 app/Core/Database.php create mode 100644 app/Core/View.php create mode 100644 app/Models/Category.php create mode 100644 app/Models/Habit.php create mode 100644 app/Models/User.php create mode 100644 app/Views/auth/login.php create mode 100644 app/Views/auth/register.php create mode 100644 app/Views/habits/dashboard.php create mode 100644 app/Views/habits/form.php create mode 100644 app/Views/layout/footer.php create mode 100644 app/Views/layout/header.php create mode 100644 app/helpers.php create mode 100644 config/config.example.php create mode 100644 database/schema.sql create mode 100644 public/assets/css/style.css create mode 100644 public/index.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a0b81a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +config/config.php +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..c9e2e9c --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# Habit Tracker + +Habit Tracker ist eine PHP-Webanwendung, mit der Benutzer eigene Gewohnheiten anlegen, bearbeiten, filtern und taeglich abhaken koennen. + +## Funktionen + +- Registrierung und Login mit Passwort-Hashing +- Geschuetzter Benutzerbereich per Session +- Habits erstellen, bearbeiten, loeschen und fuer heute abhaken +- Kategorien und Statusfilter im Dashboard +- Tagesprotokoll ueber `habit_logs` +- Responsive Oberflaeche fuer Desktop und Smartphone +- Serverseitige Validierung und klare Fehlermeldungen +- Schutz gegen SQL-Injection durch PDO Prepared Statements +- Schutz gegen XSS durch escaped HTML-Ausgabe +- CSRF-Schutz fuer schreibende Formulare + +## Technik + +- PHP +- HTML/CSS +- SQL +- MySQL/PDO +- MVC-Struktur + +## MVC-Struktur + +- `public/index.php`: Einstiegspunkt und Routing +- `app/Controllers/`: verarbeitet Anfragen und steuert den Ablauf +- `app/Models/`: kapselt Datenbankzugriffe +- `app/Views/`: erzeugt die HTML-Ausgabe +- `app/Core/`: gemeinsame Kernklassen fuer Datenbank und Views +- `app/helpers.php`: Hilfsfunktionen fuer Escaping, Sessions, Flash-Meldungen und CSRF +- `database/schema.sql`: Datenbankstruktur mit Startdaten + +## Datenbank + +Die Datenbank besteht aus 5 Tabellen: + +- `users`: Benutzerkonten +- `categories`: Habit-Kategorien +- `habits`: Gewohnheiten der Benutzer +- `habit_logs`: taegliche Erledigungen +- `reminders`: vorbereitete Erinnerungen pro Habit + +## Sicherheit + +- Passwoerter werden mit `password_hash()` gespeichert. +- Login prueft Passwoerter mit `password_verify()`. +- SQL-Abfragen nutzen vorbereitete PDO-Statements. +- Ausgaben werden mit `htmlspecialchars()` escaped. +- Schreibende Formulare nutzen CSRF-Tokens. +- Nach erfolgreichem Login wird die Session-ID erneuert. +- Die echte `config/config.php` wird nicht mit Git hochgeladen. + +## Einrichtung + +1. `database/schema.sql` in phpMyAdmin importieren. +2. `config/config.example.php` zu `config/config.php` kopieren. +3. Datenbankdaten in `config/config.php` eintragen. +4. Projekt ueber `public/index.php` starten. + +## Testablauf + +1. Benutzer registrieren. +2. Einloggen. +3. Habit mit Kategorie und Wochenziel erstellen. +4. Habit im Dashboard filtern. +5. Habit fuer heute abhaken. +6. Habit bearbeiten und loeschen. +7. Seite auf Smartphone-Breite pruefen. + +## Bewertungsbezug + +Das Projekt zeigt eine klare MVC-Aufteilung, Datenbankarbeit mit mehreren verknuepften Tabellen, serverseitige Validierung, Sicherheitsmassnahmen gegen XSS, SQL-Injection und CSRF sowie eine responsive Benutzeroberflaeche. diff --git a/app/Controllers/AuthController.php b/app/Controllers/AuthController.php new file mode 100644 index 0000000..7d7cbda --- /dev/null +++ b/app/Controllers/AuthController.php @@ -0,0 +1,75 @@ + $_GET['category_id'] ?? '', + 'status' => $_GET['status'] ?? '', + ]; + + View::render('habits/dashboard', [ + 'habits' => Habit::allForUser(current_user_id(), $filters), + 'categories' => Category::all(), + 'filters' => $filters, + ]); + } + + public function create(): void + { + require_login(); + + View::render('habits/form', [ + 'habit' => null, + 'categories' => Category::all(), + 'errors' => [], + ]); + } + + public function store(): void + { + require_login(); + verify_csrf_token(); + + $data = $this->validatedData(); + + if (!empty($data['errors'])) { + View::render('habits/form', [ + 'habit' => $_POST, + 'categories' => Category::all(), + 'errors' => $data['errors'], + ]); + return; + } + + Habit::create(current_user_id(), $data); + flash('success', 'Habit wurde erstellt.'); + redirect('dashboard'); + } + + public function edit(): void + { + require_login(); + + $habit = Habit::findForUser((int) ($_GET['id'] ?? 0), current_user_id()); + + if (!$habit) { + flash('error', 'Habit wurde nicht gefunden.'); + redirect('dashboard'); + } + + View::render('habits/form', [ + 'habit' => $habit, + 'categories' => Category::all(), + 'errors' => [], + ]); + } + + public function update(): void + { + require_login(); + verify_csrf_token(); + + $id = (int) ($_POST['id'] ?? 0); + $data = $this->validatedData(); + + if (!empty($data['errors'])) { + View::render('habits/form', [ + 'habit' => array_merge($_POST, ['id' => $id]), + 'categories' => Category::all(), + 'errors' => $data['errors'], + ]); + return; + } + + Habit::update($id, current_user_id(), $data); + flash('success', 'Habit wurde gespeichert.'); + redirect('dashboard'); + } + + public function delete(): void + { + require_login(); + verify_csrf_token(); + + Habit::delete((int) ($_POST['id'] ?? 0), current_user_id()); + flash('success', 'Habit wurde geloescht.'); + redirect('dashboard'); + } + + public function toggle(): void + { + require_login(); + verify_csrf_token(); + + Habit::toggleToday((int) ($_POST['id'] ?? 0), current_user_id()); + redirect('dashboard'); + } + + private function validatedData(): array + { + $title = trim($_POST['title'] ?? ''); + $description = trim($_POST['description'] ?? ''); + $categoryId = $_POST['category_id'] ?? ''; + $targetPerWeek = (int) ($_POST['target_per_week'] ?? 0); + $errors = []; + + if ($title === '') { + $errors['title'] = 'Bitte gib einen Namen fuer den Habit ein.'; + } + + if ($targetPerWeek < 1 || $targetPerWeek > 7) { + $errors['target_per_week'] = 'Bitte waehle einen Wert zwischen 1 und 7.'; + } + + return [ + 'title' => $title, + 'description' => $description, + 'category_id' => $categoryId, + 'target_per_week' => $targetPerWeek, + 'errors' => $errors, + ]; + } +} + diff --git a/app/Core/Database.php b/app/Core/Database.php new file mode 100644 index 0000000..325862c --- /dev/null +++ b/app/Core/Database.php @@ -0,0 +1,35 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ]); + } catch (PDOException $exception) { + throw new PDOException('Datenbankverbindung fehlgeschlagen.'); + } + + return self::$connection; + } +} diff --git a/app/Core/View.php b/app/Core/View.php new file mode 100644 index 0000000..5b0fe82 --- /dev/null +++ b/app/Core/View.php @@ -0,0 +1,15 @@ +query('SELECT id, name, color FROM categories ORDER BY name') + ->fetchAll(); + } +} diff --git a/app/Models/Habit.php b/app/Models/Habit.php new file mode 100644 index 0000000..77808ec --- /dev/null +++ b/app/Models/Habit.php @@ -0,0 +1,110 @@ + $userId]; + + if (!empty($filters['category_id'])) { + $where[] = 'h.category_id = :category_id'; + $params['category_id'] = (int) $filters['category_id']; + } + + if (($filters['status'] ?? '') === 'done') { + $where[] = 'hl.id IS NOT NULL'; + } elseif (($filters['status'] ?? '') === 'open') { + $where[] = 'hl.id IS NULL'; + } + + $sql = 'SELECT h.*, c.name AS category_name, c.color AS category_color, + CASE WHEN hl.id IS NULL THEN 0 ELSE 1 END AS done_today + FROM habits h + LEFT JOIN categories c ON c.id = h.category_id + LEFT JOIN habit_logs hl + ON hl.habit_id = h.id + AND hl.completed_on = CURRENT_DATE + WHERE ' . implode(' AND ', $where) . ' + ORDER BY h.created_at DESC'; + + $statement = Database::connection()->prepare($sql); + $statement->execute($params); + + return $statement->fetchAll(); + } + + public static function findForUser(int $id, int $userId): ?array + { + $statement = Database::connection()->prepare( + 'SELECT * FROM habits WHERE id = :id AND user_id = :user_id LIMIT 1' + ); + $statement->execute(['id' => $id, 'user_id' => $userId]); + + $habit = $statement->fetch(); + return $habit ?: null; + } + + public static function create(int $userId, array $data): bool + { + $sql = 'INSERT INTO habits (user_id, category_id, title, description, target_per_week) + VALUES (:user_id, :category_id, :title, :description, :target_per_week)'; + + return Database::connection()->prepare($sql)->execute([ + 'user_id' => $userId, + 'category_id' => $data['category_id'] ?: null, + 'title' => $data['title'], + 'description' => $data['description'], + 'target_per_week' => (int) $data['target_per_week'], + ]); + } + + public static function update(int $id, int $userId, array $data): bool + { + $sql = 'UPDATE habits + SET category_id = :category_id, + title = :title, + description = :description, + target_per_week = :target_per_week + WHERE id = :id AND user_id = :user_id'; + + return Database::connection()->prepare($sql)->execute([ + 'id' => $id, + 'user_id' => $userId, + 'category_id' => $data['category_id'] ?: null, + 'title' => $data['title'], + 'description' => $data['description'], + 'target_per_week' => (int) $data['target_per_week'], + ]); + } + + public static function delete(int $id, int $userId): bool + { + $statement = Database::connection()->prepare('DELETE FROM habits WHERE id = :id AND user_id = :user_id'); + + return $statement->execute(['id' => $id, 'user_id' => $userId]); + } + + public static function toggleToday(int $id, int $userId): bool + { + $habit = self::findForUser($id, $userId); + + if (!$habit) { + return false; + } + + $db = Database::connection(); + $check = $db->prepare( + 'SELECT id FROM habit_logs WHERE habit_id = :habit_id AND completed_on = CURRENT_DATE LIMIT 1' + ); + $check->execute(['habit_id' => $id]); + $log = $check->fetch(); + + if ($log) { + return $db->prepare('DELETE FROM habit_logs WHERE id = :id')->execute(['id' => $log['id']]); + } + + $insert = $db->prepare('INSERT INTO habit_logs (habit_id, completed_on) VALUES (:habit_id, CURRENT_DATE)'); + return $insert->execute(['habit_id' => $id]); + } +} diff --git a/app/Models/User.php b/app/Models/User.php new file mode 100644 index 0000000..32e57d2 --- /dev/null +++ b/app/Models/User.php @@ -0,0 +1,25 @@ +prepare($sql); + + return $statement->execute([ + 'name' => $name, + 'email' => $email, + 'password_hash' => password_hash($password, PASSWORD_DEFAULT), + ]); + } + + public static function findByEmail(string $email): ?array + { + $statement = Database::connection()->prepare('SELECT * FROM users WHERE email = :email LIMIT 1'); + $statement->execute(['email' => $email]); + + $user = $statement->fetch(); + return $user ?: null; + } +} diff --git a/app/Views/auth/login.php b/app/Views/auth/login.php new file mode 100644 index 0000000..38cf9cc --- /dev/null +++ b/app/Views/auth/login.php @@ -0,0 +1,15 @@ +
+

Anmelden

+
+ + + + + + + + +
+

Noch kein Konto? Jetzt registrieren

+
+ diff --git a/app/Views/auth/register.php b/app/Views/auth/register.php new file mode 100644 index 0000000..526058e --- /dev/null +++ b/app/Views/auth/register.php @@ -0,0 +1,19 @@ +
+

Registrieren

+
+ + + + + + + + + + Mindestens 8 Zeichen. + + +
+

Schon registriert? Zum Login

+
+ diff --git a/app/Views/habits/dashboard.php b/app/Views/habits/dashboard.php new file mode 100644 index 0000000..b70688f --- /dev/null +++ b/app/Views/habits/dashboard.php @@ -0,0 +1,67 @@ +
+
+

Heute

+

Deine Habits

+
+ Habit erstellen +
+ +
+ + + + + + + + + +
+ + +

Noch keine Habits gefunden. Erstelle deinen ersten Habit.

+ + +
+ +
+
+ +
+

+

+ + | + Ziel: x pro Woche + +
+
+ +
+
+ + + +
+ Bearbeiten +
+ + + +
+
+
+ +
+ diff --git a/app/Views/habits/form.php b/app/Views/habits/form.php new file mode 100644 index 0000000..f12b11c --- /dev/null +++ b/app/Views/habits/form.php @@ -0,0 +1,58 @@ + + +
+

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ diff --git a/app/Views/layout/footer.php b/app/Views/layout/footer.php new file mode 100644 index 0000000..9765cc0 --- /dev/null +++ b/app/Views/layout/footer.php @@ -0,0 +1,3 @@ + + + diff --git a/app/Views/layout/header.php b/app/Views/layout/header.php new file mode 100644 index 0000000..cdd1fcf --- /dev/null +++ b/app/Views/layout/header.php @@ -0,0 +1,34 @@ + + + + + + <?= e(config('app_name')) ?> + + + +
+ Habit Tracker + +
+ +
+ +

+ + + +

+ diff --git a/app/helpers.php b/app/helpers.php new file mode 100644 index 0000000..627acfa --- /dev/null +++ b/app/helpers.php @@ -0,0 +1,82 @@ +'; +} + +function verify_csrf_token(): void +{ + $token = $_POST['csrf_token'] ?? ''; + + if (!is_string($token) || !hash_equals($_SESSION['csrf_token'] ?? '', $token)) { + http_response_code(419); + echo 'Sicherheitspruefung fehlgeschlagen. Bitte lade die Seite neu.'; + exit; + } +} diff --git a/config/config.example.php b/config/config.example.php new file mode 100644 index 0000000..c1e0d8f --- /dev/null +++ b/config/config.example.php @@ -0,0 +1,13 @@ + 'Habit Tracker', + 'base_path' => '', + 'database' => [ + 'host' => 'localhost', + 'dbname' => 'habittracker', + 'user' => 'root', + 'password' => '', + 'charset' => 'utf8mb4', + ], +]; diff --git a/database/schema.sql b/database/schema.sql new file mode 100644 index 0000000..eb9b2a9 --- /dev/null +++ b/database/schema.sql @@ -0,0 +1,55 @@ +CREATE DATABASE IF NOT EXISTS pbbfa24aal_habittracker + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; + +USE pbbfa24aal_habittracker; + +CREATE TABLE users ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(190) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE categories ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(80) NOT NULL, + color VARCHAR(20) NOT NULL DEFAULT '#176b5b' +); + +CREATE TABLE habits ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + category_id INT NULL, + title VARCHAR(120) NOT NULL, + description TEXT NULL, + target_per_week TINYINT UNSIGNED NOT NULL DEFAULT 3, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL +); + +CREATE TABLE habit_logs ( + id INT AUTO_INCREMENT PRIMARY KEY, + habit_id INT NOT NULL, + completed_on DATE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY unique_habit_day (habit_id, completed_on), + FOREIGN KEY (habit_id) REFERENCES habits(id) ON DELETE CASCADE +); + +CREATE TABLE reminders ( + id INT AUTO_INCREMENT PRIMARY KEY, + habit_id INT NOT NULL, + reminder_time TIME NOT NULL, + weekday TINYINT UNSIGNED NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + FOREIGN KEY (habit_id) REFERENCES habits(id) ON DELETE CASCADE +); + +INSERT INTO categories (name, color) VALUES + ('Gesundheit', '#176b5b'), + ('Lernen', '#365c9a'), + ('Sport', '#b95c20'), + ('Alltag', '#6b4ba3'); diff --git a/public/assets/css/style.css b/public/assets/css/style.css new file mode 100644 index 0000000..167d24c --- /dev/null +++ b/public/assets/css/style.css @@ -0,0 +1,275 @@ +:root { + --bg: #f6f7f9; + --panel: #ffffff; + --text: #172033; + --muted: #657086; + --line: #d9dee8; + --primary: #176b5b; + --primary-dark: #0f4c40; + --danger: #b42318; + --success-bg: #e8f5ee; + --error-bg: #fdecec; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font-family: Arial, Helvetica, sans-serif; + line-height: 1.5; +} + +a { + color: var(--primary); + text-decoration-thickness: 2px; +} + +.topbar { + align-items: center; + background: var(--panel); + border-bottom: 1px solid var(--line); + display: flex; + justify-content: space-between; + min-height: 64px; + padding: 0 32px; +} + +.brand { + color: var(--text); + font-size: 20px; + font-weight: 700; + text-decoration: none; +} + +.nav { + display: flex; + gap: 18px; +} + +.logout-form { + margin: 0; +} + +.link-button { + background: transparent; + color: var(--primary); + min-height: 0; + padding: 0; + text-decoration: underline; + text-decoration-thickness: 2px; +} + +.link-button:hover { + background: transparent; + color: var(--primary-dark); +} + +.page { + margin: 0 auto; + max-width: 1080px; + padding: 32px 24px; +} + +.notice, +.empty, +.auth-panel, +.form-panel { + background: var(--panel); + border: 1px solid var(--line); + border-radius: 8px; + padding: 20px; +} + +.notice.success { + background: var(--success-bg); +} + +.notice.error { + background: var(--error-bg); + color: var(--danger); +} + +.auth-panel, +.form-panel { + max-width: 520px; +} + +.dashboard-head { + align-items: center; + display: flex; + justify-content: space-between; + margin-bottom: 24px; +} + +.eyebrow { + color: var(--muted); + font-size: 14px; + font-weight: 700; + margin: 0 0 4px; + text-transform: uppercase; +} + +h1, +h2 { + line-height: 1.2; + margin: 0; +} + +h1 { + font-size: 34px; +} + +h2 { + font-size: 20px; +} + +.button, +button { + background: var(--primary); + border: 0; + border-radius: 6px; + color: #ffffff; + cursor: pointer; + display: inline-block; + font: inherit; + font-weight: 700; + min-height: 44px; + padding: 10px 16px; + text-decoration: none; +} + +button:hover, +.button:hover { + background: var(--primary-dark); +} + +button.danger { + background: var(--danger); +} + +.form, +.filters { + display: grid; + gap: 10px; +} + +.form label, +.filters label { + font-weight: 700; +} + +.form label span { + color: var(--muted); + font-weight: 400; +} + +input, +select, +textarea { + border: 1px solid var(--line); + border-radius: 6px; + font: inherit; + min-height: 44px; + padding: 10px 12px; + width: 100%; +} + +textarea { + resize: vertical; +} + +.field-error { + color: var(--danger); + font-weight: 700; +} + +input.input-error { + border-color: var(--danger); +} + +.filters { + align-items: end; + background: var(--panel); + border: 1px solid var(--line); + border-radius: 8px; + grid-template-columns: 1fr 1fr auto; + margin-bottom: 20px; + padding: 16px; +} + +.habit-list { + display: grid; + gap: 14px; +} + +.habit-card { + align-items: center; + background: var(--panel); + border: 1px solid var(--line); + border-radius: 8px; + display: flex; + gap: 18px; + justify-content: space-between; + padding: 18px; +} + +.habit-card.done { + border-color: var(--primary); +} + +.habit-main { + display: flex; + gap: 14px; +} + +.habit-main p { + color: var(--muted); + margin: 8px 0; +} + +.category-dot { + border-radius: 999px; + flex: 0 0 14px; + height: 14px; + margin-top: 5px; + width: 14px; +} + +.habit-actions { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: flex-end; +} + +@media (max-width: 720px) { + .topbar, + .dashboard-head, + .habit-card { + align-items: stretch; + flex-direction: column; + } + + .topbar { + gap: 12px; + padding: 16px 20px; + } + + .nav { + flex-wrap: wrap; + } + + .filters { + grid-template-columns: 1fr; + } + + .habit-actions { + justify-content: flex-start; + } +} + diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..f47fe20 --- /dev/null +++ b/public/index.php @@ -0,0 +1,50 @@ + true, + 'samesite' => 'Lax', + 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', +]); +session_start(); + +require __DIR__ . '/../app/helpers.php'; +require __DIR__ . '/../app/Core/Database.php'; +require __DIR__ . '/../app/Core/View.php'; +require __DIR__ . '/../app/Models/User.php'; +require __DIR__ . '/../app/Models/Category.php'; +require __DIR__ . '/../app/Models/Habit.php'; +require __DIR__ . '/../app/Controllers/AuthController.php'; +require __DIR__ . '/../app/Controllers/HabitController.php'; + +$route = $_GET['route'] ?? 'dashboard'; +$method = $_SERVER['REQUEST_METHOD']; + +$routes = [ + 'GET' => [ + 'login' => [AuthController::class, 'showLogin'], + 'register' => [AuthController::class, 'showRegister'], + 'dashboard' => [HabitController::class, 'dashboard'], + 'habits/create' => [HabitController::class, 'create'], + 'habits/edit' => [HabitController::class, 'edit'], + ], + 'POST' => [ + 'login' => [AuthController::class, 'login'], + 'register' => [AuthController::class, 'register'], + 'logout' => [AuthController::class, 'logout'], + 'habits/store' => [HabitController::class, 'store'], + 'habits/update' => [HabitController::class, 'update'], + 'habits/delete' => [HabitController::class, 'delete'], + 'habits/toggle' => [HabitController::class, 'toggle'], + ], +]; + +if (!isset($routes[$method][$route])) { + http_response_code(404); + echo 'Seite nicht gefunden.'; + exit; +} + +[$controllerClass, $action] = $routes[$method][$route]; +(new $controllerClass())->$action();