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