Compare commits

..
5 Commits
13 changed files with 39 additions and 88 deletions
Regular → Executable
-77
View File
@@ -1,77 +0,0 @@
# 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`
- Wochenfortschritt im Dashboard gegen das persoenliche Wochenziel
- 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. Wochenfortschritt im Dashboard kontrollieren.
7. Habit bearbeiten und loeschen.
8. 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.
+6
View File
@@ -2,6 +2,7 @@
class AuthController
{
// Zeigt das Login-Formular
public function showLogin(): void
{
View::render('auth/login', [
@@ -10,6 +11,7 @@ class AuthController
]);
}
// Prueft die Anmeldedaten und startet die Sitzung
public function login(): void
{
verify_csrf_token();
@@ -50,6 +52,7 @@ class AuthController
redirect('dashboard');
}
// Zeigt das Registrierungsformular
public function showRegister(): void
{
View::render('auth/register', [
@@ -58,6 +61,7 @@ class AuthController
]);
}
// Legt ein neues Konto an
public function register(): void
{
verify_csrf_token();
@@ -109,11 +113,13 @@ class AuthController
redirect('login');
}
// Beendet die Sitzung
public function logout(): void
{
verify_csrf_token();
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
+8
View File
@@ -2,6 +2,7 @@
class HabitController
{
// Zeigt das Dashboard mit allen Habits des Benutzers
public function dashboard(): void
{
require_login();
@@ -26,6 +27,7 @@ class HabitController
]);
}
// Zeigt das leere Formular zum Anlegen
public function create(): void
{
require_login();
@@ -37,6 +39,7 @@ class HabitController
]);
}
// Speichert einen neuen Habit
public function store(): void
{
require_login();
@@ -58,6 +61,7 @@ class HabitController
redirect('dashboard');
}
// Zeigt das Formular zum Bearbeiten
public function edit(): void
{
require_login();
@@ -76,6 +80,7 @@ class HabitController
]);
}
// Uebernimmt die Aenderungen eines bestehenden Habits
public function update(): void
{
require_login();
@@ -85,6 +90,7 @@ class HabitController
$data = $this->validatedData();
if (!empty($data['errors'])) {
// ID mitgeben, sonst zeigt das Formular auf keinen Datensatz mehr
View::render('habits/form', [
'habit' => array_merge($_POST, ['id' => $id]),
'categories' => Category::all(),
@@ -98,6 +104,7 @@ class HabitController
redirect('dashboard');
}
// Loescht einen Habit
public function delete(): void
{
require_login();
@@ -135,6 +142,7 @@ class HabitController
$errors['description'] = 'Die Beschreibung darf maximal 1000 Zeichen lang sein.';
}
// Gegen die Datenbank pruefen, damit keine erfundene ID durchkommt
if ($categoryId !== '' && !Category::exists((int) $categoryId)) {
$errors['category_id'] = 'Bitte waehle eine gueltige Kategorie aus.';
}
+9 -3
View File
@@ -2,6 +2,7 @@
class ReminderController
{
// Zeigt das Formular zum Anlegen einer Erinnerung
public function create(): void
{
require_login();
@@ -20,6 +21,7 @@ class ReminderController
]);
}
// Speichert eine neue Erinnerung
public function store(): void
{
require_login();
@@ -52,6 +54,7 @@ class ReminderController
redirect('dashboard');
}
// Zeigt das Formular zum Bearbeiten
public function edit(): void
{
require_login();
@@ -105,6 +108,7 @@ class ReminderController
redirect('dashboard');
}
// Loescht eine Erinnerung
public function delete(): void
{
require_login();
@@ -144,10 +148,12 @@ class ReminderController
$activeValue = $this->postString('is_active');
$errors = [];
if (!preg_match('/^(?:[01]\\d|2[0-3]):[0-5]\\d$/', $time)) {
// Uhrzeit muss dem Format HH:MM zwischen 00:00 und 23:59
if (!preg_match('/^(?:[01]\d|2[0-3]):[0-5]\d$/', $time)) {
$errors['reminder_time'] = 'Bitte gib eine Uhrzeit im Format HH:MM ein.';
}
// Leerer Wochentag ist erlaubt und bedeutet taeglich
if ($weekdayValue !== '' && !preg_match('/^[1-7]$/', $weekdayValue)) {
$errors['weekday'] = 'Bitte waehle einen gueltigen Wochentag aus.';
}
@@ -168,14 +174,14 @@ class ReminderController
{
$value = $_GET[$key] ?? '';
return is_string($value) && preg_match('/^[1-9]\\d*$/', $value) ? (int) $value : 0;
return is_string($value) && preg_match('/^[1-9]\d*$/', $value) ? (int) $value : 0;
}
private function postId(string $key): int
{
$value = $_POST[$key] ?? '';
return is_string($value) && preg_match('/^[1-9]\\d*$/', $value) ? (int) $value : 0;
return is_string($value) && preg_match('/^[1-9]\d*$/', $value) ? (int) $value : 0;
}
private function postString(string $key): string
Regular → Executable
+2
View File
@@ -1,7 +1,9 @@
<?php
// Stellt die Verbindung zur Datenbank bereit
class Database
{
// Verbindung wird zwischengespeichert damit pro Request nur eine entsteht
private static ?PDO $connection = null;
public static function connection(): PDO
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
+1
View File
@@ -49,6 +49,7 @@ class Habit
return $habit ?: null;
}
// Legt einen neuen Habit an
public static function create(int $userId, array $data): bool
{
$sql = 'INSERT INTO habits (user_id, category_id, title, description, target_per_week)
Regular → Executable
+3
View File
@@ -31,6 +31,7 @@ class Reminder
return $statement->fetchAll();
}
// Legt eine Erinnerung an
public static function createForHabit(int $habitId, int $userId, array $data): bool
{
$statement = Database::connection()->prepare(
@@ -50,6 +51,7 @@ class Reminder
return $statement->rowCount() === 1;
}
// Aktualisiert eine Erinnerung
public static function update(int $id, int $userId, array $data): bool
{
$statement = Database::connection()->prepare(
@@ -82,6 +84,7 @@ class Reminder
return $statement->execute(['id' => $id, 'user_id' => $userId]);
}
// Schaltet zwischen aktiv und pausiert um
public static function toggle(int $id, int $userId): bool
{
$statement = Database::connection()->prepare(
Regular → Executable
+2
View File
@@ -2,6 +2,7 @@
class User
{
// Legt einen neuen Benutzer an
public static function create(string $name, string $email, string $password): bool
{
$sql = 'INSERT INTO users (name, email, password_hash) VALUES (:name, :email, :password_hash)';
@@ -14,6 +15,7 @@ class User
]);
}
// Sucht einen Benutzer anhand der E-Mail
public static function findByEmail(string $email): ?array
{
$statement = Database::connection()->prepare('SELECT * FROM users WHERE email = :email LIMIT 1');
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File