Compare commits
8
Commits
f09fdfa25e
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a469cbab1a | ||
|
|
0a23a18218 | ||
|
|
9eb8b1de23 | ||
|
|
f91feadedf | ||
|
|
6e1dcd9d1c | ||
|
|
b4811f4bd0 | ||
|
|
b23a1368a8 | ||
|
|
50d8e04063 |
@@ -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.
|
||||
|
||||
Regular → Executable
+6
@@ -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']);
|
||||
|
||||
Regular → Executable
+8
@@ -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.';
|
||||
}
|
||||
|
||||
Regular → Executable
+9
-3
@@ -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
@@ -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
Regular → Executable
Regular → Executable
+1
@@ -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
@@ -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
@@ -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
Regular → Executable
Regular → Executable
Reference in New Issue
Block a user