commit 4e4dfe66c638f49f961c638df8bc3b86bc9fe636 Author: Jacek Pyziak Date: Thu Jan 29 21:08:01 2026 +0100 first commit diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..b1aef07 --- /dev/null +++ b/.htaccess @@ -0,0 +1,9 @@ +RewriteEngine On +RewriteBase / + +# Nie przepisuj istniejących plików i katalogów +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d + +# Przepisz wszystko na index.php +RewriteRule ^(.*)$ index.php [QSA,L] diff --git a/.vscode/ftp-kr.json b/.vscode/ftp-kr.json new file mode 100644 index 0000000..a7d566e --- /dev/null +++ b/.vscode/ftp-kr.json @@ -0,0 +1,17 @@ +{ + "host": "host117523.hostido.net.pl", + "username": "www@pagedev.pl", + "password": "dP9frFL3nw469pXJJ5vG", + "remotePath": "/public_html/", + "protocol": "ftp", + "port": 21, + "fileNameEncoding": "utf8", + "autoUpload": true, + "autoDelete": false, + "autoDownload": false, + "ignoreRemoteModification": true, + "ignore": [ + ".git", + "/.vscode" + ] +} \ No newline at end of file diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json new file mode 100644 index 0000000..9899504 --- /dev/null +++ b/.vscode/ftp-kr.sync.cache.json @@ -0,0 +1,16 @@ +{ + "ftp://host117523.hostido.net.pl:21@www@pagedev.pl": { + "public_html": { + "cgi-bin": {}, + "models": { + "Database.php": { + "type": "-", + "size": 1094, + "lmtime": 1769712478164, + "modified": false + } + } + } + }, + "$version": 1 +} \ No newline at end of file diff --git a/.vscode/sftp.json b/.vscode/sftp.json new file mode 100644 index 0000000..3e9a53f --- /dev/null +++ b/.vscode/sftp.json @@ -0,0 +1,12 @@ +{ + "name": "host117523.hostido.net.pl", + "host": "host117523.hostido.net.pl", + "protocol": "ftp", + "port": 21, + "username": "www@pagedev.pl", + "password": "dP9frFL3nw469pXJJ5vG", + "remotePath": "/public_html/", + "uploadOnSave": false, + "useTempFile": false, + "openSsh": false +} diff --git a/PROJECT_LOG.md b/PROJECT_LOG.md new file mode 100644 index 0000000..0d4f5ed --- /dev/null +++ b/PROJECT_LOG.md @@ -0,0 +1,66 @@ +# Projekt: System 2FA + Notatnik (PHP MVC) + +## Cel projektu +- Prosta aplikacja logowania 2FA w PHP z notatnikiem użytkownika. +- MVC (kontrolery, modele, widoki). Bootstrap UI. + +## Najważniejsze funkcje (stan na dziś) +- Logowanie użytkownika + weryfikacja kodem 2FA (testowy kod w konsoli). +- Panel użytkownika po zalogowaniu. +- Notatnik: lista, dodawanie, edycja, usuwanie z ładnym modalem potwierdzenia. +- Kalendarz: widok miesięczny, dodawanie/edycja/usuwanie wydarzeń. + +## Dane testowe +- Login: projectpro +- Hasło: testowehaslo + +## Routing (przyjazne URL) +- /logowanie +- /uwierzytelnianie (POST) +- /weryfikacja +- /weryfikuj-kod (POST) +- /panel lub /pulpit +- /notatnik +- /notatka/nowa +- /notatka/edytuj?id=ID +- /notatka/zapisz (POST) +- /notatka/usun (POST) +- /kalendarz +- /wydarzenie/nowe +- /wydarzenie/edytuj?id=ID +- /wydarzenie/zapisz (POST) +- /wydarzenie/usun (POST) +- /wyloguj-sie +- /inicjalizacja + +## Baza danych (SQLite) +- users: użytkownicy (hasła haszowane) +- verification_codes: kody 2FA z wygasaniem +- notes: notatki per user (created_at/updated_at) +- calendar_events: wydarzenia per user (event_date, created_at/updated_at) + +## Struktura MVC +- controllers: InitController, LoginController, DashboardController, NotesController +- controllers: InitController, LoginController, DashboardController, NotesController, CalendarController +- models: Database, User, Notes, CalendarEvent +- views: login, verify, dashboard, notes/index, notes/form, calendar/index, calendar/form +- layout wspólny: views/layout.php + +## Wygląd UI +- Bootstrap 5 +- Dodatkowe style: public/css/style.css, public/css/notes.css, public/css/calendar.css +- JS: public/js/app.js + +## Ważne uwagi techniczne +- Layout obsługuje: $pageTitle, $bodyClass, $extraHead, $extraScript, $content. +- Widoki korzystają z layoutu i generują zawartość przez output buffering. +- Notatnik używa modala Bootstrap do potwierdzenia usunięcia. + +## Pliki kluczowe +- index.php: router i bootstrap aplikacji +- .htaccess: przepisywanie URL +- models/Notes.php: inicjalizacja tabeli notatek przy użyciu + +## Do dalszego pilnowania +- Utrzymywać spójność URL z routerem. +- Aktualizować ten plik po kolejnych zmianach. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3b36cdb --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# System Logowania 2FA + +Prosta aplikacja demonstracyjna logowania dwuskładnikowego w PHP. + +## Wymagania + +- PHP 7.4 lub nowszy +- SQLite3 +- Serwer WWW (Apache/Nginx) lub wbudowany serwer PHP + +## Instalacja + +1. Skopiuj pliki na serwer WWW +2. Upewnij się, że katalog `database/` ma uprawnienia do zapisu +3. Przejdź do `/inicjalizacja` (lub `index.php?action=init`) aby zainicjalizować bazę danych +4. Zaloguj się używając testowego konta + +## Przyjazne URLe + +Aplikacja wspiera przyjazne URLe (friendly URLs): + +- `/` lub `/logowanie` - ekran logowania +- `/uwierzytelnianie` - proces uwierzytelniania +- `/weryfikacja` - ekran weryfikacji 2FA +- `/weryfikuj-kod` - weryfikacja kodu +- `/panel` lub `/dashboard` - panel użytkownika +- `/wyloguj-sie` - wylogowanie +- `/inicjalizacja` - inicjalizacja bazy danych + +Stare URLe z parametrem `?action=` nadal działają dla kompatybilności. + +## Dane testowe + +- **Login:** projectpro +- **Hasło:** testowehaslo + +## Struktura projektu + +``` +projektphp/ +├── controllers/ # Kontrolery MVC +│ ├── InitController.php +│ ├── LoginController.php +│ └── DashboardController.php +├── models/ # Modele danych +│ ├── Database.php +│ └── User.php +├── views/ # Widoki PHP +│ ├── layout.php +│ ├── login.php +│ ├── verify.php +│ └── dashboard.php +├── public/ # Publiczne zasoby +│ ├── css/ +│ │ └── style.css +│ └── js/ +│ └── app.js +├── database/ # Baza danych SQLite +│ └── database.db +├── .htaccess # Konfiguracja Apache +└── index.php # Punkt wejścia +``` + +## Proces logowania + +1. Użytkownik wprowadza login i hasło +2. System generuje 6-cyfrowy kod weryfikacyjny +3. Kod wyświetlany jest w konsoli przeglądarki (tryb testowy) +4. Po wprowadzeniu prawidłowego kodu użytkownik jest zalogowany +5. Kod wygasa po 15 minutach + +## Uwagi + +- W trybie testowym kod weryfikacyjny jest wyświetlany w konsoli przeglądarki (F12) +- W produkcji kod powinien być wysyłany emailem +- Hasła są hashowane przy użyciu `password_hash()` +- Sesje są bezpiecznie zarządzane diff --git a/controllers/CalendarController.php b/controllers/CalendarController.php new file mode 100644 index 0000000..50dbee9 --- /dev/null +++ b/controllers/CalendarController.php @@ -0,0 +1,142 @@ +calendarModel = new CalendarEvent(); + } + + public function index() + { + $userId = $_SESSION['user_id']; + + $month = $_GET['month'] ?? date('Y-m'); + $monthDate = DateTime::createFromFormat('Y-m', $month); + if (!$monthDate) { + $monthDate = new DateTime('first day of this month'); + $month = $monthDate->format('Y-m'); + } + + $firstDay = (clone $monthDate)->modify('first day of this month'); + $daysInMonth = (int)$firstDay->format('t'); + $startWeekday = (int)$firstDay->format('N'); + + $prevMonth = (clone $monthDate)->modify('-1 month')->format('Y-m'); + $nextMonth = (clone $monthDate)->modify('+1 month')->format('Y-m'); + $monthLabel = $monthDate->format('F Y'); + + $selectedDate = $_GET['date'] ?? date('Y-m-d'); + + $events = $this->calendarModel->getByMonth($userId, $month); + $eventsByDate = []; + foreach ($events as $event) { + $eventsByDate[$event['event_date']][] = $event; + } + + $eventsForSelected = $this->calendarModel->getByDate($userId, $selectedDate); + + require_once __DIR__ . '/../views/calendar/index.php'; + } + + public function create() + { + $event = null; + $defaultDate = $_GET['date'] ?? date('Y-m-d'); + $returnMonth = $_GET['month'] ?? date('Y-m'); + + require_once __DIR__ . '/../views/calendar/form.php'; + } + + public function edit() + { + $eventId = $_GET['id'] ?? null; + if (!$eventId) { + $_SESSION['error'] = 'Nie podano ID wydarzenia'; + header('Location: /kalendarz'); + exit; + } + + $userId = $_SESSION['user_id']; + $event = $this->calendarModel->getById($eventId, $userId); + if (!$event) { + $_SESSION['error'] = 'Wydarzenie nie zostało znalezione'; + header('Location: /kalendarz'); + exit; + } + + $defaultDate = $event['event_date']; + $returnMonth = $_GET['month'] ?? date('Y-m', strtotime($event['event_date'])); + + require_once __DIR__ . '/../views/calendar/form.php'; + } + + public function save() + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /kalendarz'); + exit; + } + + $userId = $_SESSION['user_id']; + $eventId = $_POST['event_id'] ?? null; + $title = trim($_POST['title'] ?? ''); + $content = trim($_POST['content'] ?? ''); + $eventDate = $_POST['event_date'] ?? date('Y-m-d'); + $returnMonth = $_POST['return_month'] ?? date('Y-m'); + + if ($title === '') { + $_SESSION['error'] = 'Tytuł wydarzenia jest wymagany'; + $redirect = $eventId ? "/wydarzenie/edytuj?id=$eventId&month=$returnMonth" : "/wydarzenie/nowe?date=$eventDate&month=$returnMonth"; + header('Location: ' . $redirect); + exit; + } + + if ($eventId) { + $this->calendarModel->update($eventId, $userId, $title, $content, $eventDate); + $_SESSION['success'] = 'Wydarzenie zostało zaktualizowane'; + } else { + $this->calendarModel->create($userId, $title, $content, $eventDate); + $_SESSION['success'] = 'Wydarzenie zostało utworzone'; + } + + header('Location: /kalendarz?month=' . $returnMonth . '&date=' . $eventDate); + exit; + } + + public function delete() + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /kalendarz'); + exit; + } + + $eventId = $_POST['event_id'] ?? null; + $returnMonth = $_POST['return_month'] ?? date('Y-m'); + $returnDate = $_POST['return_date'] ?? date('Y-m-d'); + + if (!$eventId) { + $_SESSION['error'] = 'Nie podano ID wydarzenia'; + header('Location: /kalendarz'); + exit; + } + + $userId = $_SESSION['user_id']; + $result = $this->calendarModel->delete($eventId, $userId); + if ($result) { + $_SESSION['success'] = 'Wydarzenie zostało usunięte'; + } else { + $_SESSION['error'] = 'Nie udało się usunąć wydarzenia'; + } + + header('Location: /kalendarz?month=' . $returnMonth . '&date=' . $returnDate); + exit; + } +} diff --git a/controllers/DashboardController.php b/controllers/DashboardController.php new file mode 100644 index 0000000..52fd03d --- /dev/null +++ b/controllers/DashboardController.php @@ -0,0 +1,27 @@ +getUserById($_SESSION['user_id']); + + $calendarModel = new CalendarEvent(); + $today = new DateTime('today'); + $weekStart = (clone $today)->modify('monday this week'); + $weekEnd = (clone $weekStart)->modify('+6 days'); + $eventsThisWeek = $calendarModel->countByDateRange( + $_SESSION['user_id'], + $weekStart->format('Y-m-d'), + $weekEnd->format('Y-m-d') + ); + + require_once __DIR__ . '/../views/dashboard.php'; + } +} diff --git a/controllers/InitController.php b/controllers/InitController.php new file mode 100644 index 0000000..34f7a10 --- /dev/null +++ b/controllers/InitController.php @@ -0,0 +1,55 @@ +initDatabase(); + + echo " + + + + + Inicjalizacja bazy danych + + + +
+ + Przejdź do logowania +
+ + "; + } catch (Exception $e) { + echo " + + + + + Błąd inicjalizacji + + + +
+ +
+ + "; + } + } +} diff --git a/controllers/LoginController.php b/controllers/LoginController.php new file mode 100644 index 0000000..3b8774e --- /dev/null +++ b/controllers/LoginController.php @@ -0,0 +1,98 @@ +userModel = new User(); + } + + public function index() + { + if (isset($_SESSION['user_id'])) { + header('Location: /panel'); + exit; + } + + require_once __DIR__ . '/../views/login.php'; + } + + public function authenticate() + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /logowanie'); + exit; + } + + $username = $_POST['username'] ?? ''; + $password = $_POST['password'] ?? ''; + + $user = $this->userModel->authenticate($username, $password); + + if ($user) { + // Generowanie kodu weryfikacyjnego + $code = $this->userModel->generateVerificationCode($user['id']); + + // Zapisanie ID użytkownika w sesji tymczasowo + $_SESSION['pending_user_id'] = $user['id']; + $_SESSION['pending_username'] = $user['username']; + + // W rzeczywistości tutaj wysłalibyśmy email + // Dla testów kod będzie wyświetlony w konsoli przeglądarki + $_SESSION['test_code'] = $code; + + header('Location: /weryfikacja'); + exit; + } else { + $_SESSION['error'] = 'Nieprawidłowy login lub hasło'; + header('Location: /logowanie'); + exit; + } + } + + public function verify() + { + if (!isset($_SESSION['pending_user_id'])) { + header('Location: /logowanie'); + exit; + } + + require_once __DIR__ . '/../views/verify.php'; + } + + public function verifyCode() + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /logowanie'); + exit; + } + + if (!isset($_SESSION['pending_user_id'])) { + header('Location: /logowanie'); + exit; + } + + $code = $_POST['code'] ?? ''; + $userId = $_SESSION['pending_user_id']; + + if ($this->userModel->verifyCode($userId, $code)) { + // Zalogowanie użytkownika + $_SESSION['user_id'] = $userId; + $_SESSION['username'] = $_SESSION['pending_username']; + + // Czyszczenie danych tymczasowych + unset($_SESSION['pending_user_id']); + unset($_SESSION['pending_username']); + unset($_SESSION['test_code']); + + header('Location: /panel'); + exit; + } else { + $_SESSION['error'] = 'Nieprawidłowy kod weryfikacyjny lub kod wygasł'; + header('Location: /weryfikacja'); + exit; + } + } +} diff --git a/controllers/NotesController.php b/controllers/NotesController.php new file mode 100644 index 0000000..e147da2 --- /dev/null +++ b/controllers/NotesController.php @@ -0,0 +1,114 @@ +notesModel = new Notes(); + + // Sprawdzenie czy użytkownik jest zalogowany + if (!isset($_SESSION['user_id'])) { + header('Location: /logowanie'); + exit; + } + } + + public function index() + { + $userId = $_SESSION['user_id']; + $notes = $this->notesModel->getAllByUser($userId); + $notesCount = $this->notesModel->getCount($userId); + + require_once __DIR__ . '/../views/notes/index.php'; + } + + public function create() + { + $note = null; // Pusty formularz dla nowej notatki + require_once __DIR__ . '/../views/notes/form.php'; + } + + public function edit() + { + $noteId = $_GET['id'] ?? null; + + if (!$noteId) { + $_SESSION['error'] = 'Nie podano ID notatki'; + header('Location: /notatnik'); + exit; + } + + $userId = $_SESSION['user_id']; + $note = $this->notesModel->getById($noteId, $userId); + + if (!$note) { + $_SESSION['error'] = 'Notatka nie została znaleziona'; + header('Location: /notatnik'); + exit; + } + + require_once __DIR__ . '/../views/notes/form.php'; + } + + public function save() + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /notatnik'); + exit; + } + + $userId = $_SESSION['user_id']; + $noteId = $_POST['note_id'] ?? null; + $title = trim($_POST['title'] ?? ''); + $content = trim($_POST['content'] ?? ''); + + if (empty($title)) { + $_SESSION['error'] = 'Tytuł notatki jest wymagany'; + header('Location: ' . ($noteId ? "/notatka/edytuj?id=$noteId" : '/notatka/nowa')); + exit; + } + + if ($noteId) { + // Aktualizacja istniejącej notatki + $result = $this->notesModel->update($noteId, $userId, $title, $content); + $_SESSION['success'] = 'Notatka została zaktualizowana'; + } else { + // Tworzenie nowej notatki + $result = $this->notesModel->create($userId, $title, $content); + $_SESSION['success'] = 'Notatka została utworzona'; + } + + header('Location: /notatnik'); + exit; + } + + public function delete() + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Location: /notatnik'); + exit; + } + + $noteId = $_POST['note_id'] ?? null; + + if (!$noteId) { + $_SESSION['error'] = 'Nie podano ID notatki'; + header('Location: /notatnik'); + exit; + } + + $userId = $_SESSION['user_id']; + $result = $this->notesModel->delete($noteId, $userId); + + if ($result) { + $_SESSION['success'] = 'Notatka została usunięta'; + } else { + $_SESSION['error'] = 'Nie udało się usunąć notatki'; + } + + header('Location: /notatnik'); + exit; + } +} diff --git a/index.php b/index.php new file mode 100644 index 0000000..41babc2 --- /dev/null +++ b/index.php @@ -0,0 +1,157 @@ + 'login', + 'logowanie' => 'login', + 'uwierzytelnianie' => 'authenticate', + 'weryfikacja' => 'verify', + 'weryfikuj-kod' => 'verify-code', + 'panel' => 'dashboard', + 'dashboard' => 'dashboard', + 'pulpit' => 'dashboard', + 'wyloguj-sie' => 'logout', + 'inicjalizacja' => 'init', + 'notatnik' => 'notes', + 'notatki' => 'notes', + 'notatka/nowa' => 'note-create', + 'notatka/edytuj' => 'note-edit', + 'notatka/zapisz' => 'note-save', + 'notatka/usun' => 'note-delete', + 'kalendarz' => 'calendar', + 'wydarzenie/nowe' => 'event-create', + 'wydarzenie/edytuj' => 'event-edit', + 'wydarzenie/zapisz' => 'event-save', + 'wydarzenie/usun' => 'event-delete', +]; + +// Obsługa starych URLi z parametrem action dla kompatybilności +if (isset($_GET['action'])) { + $action = $_GET['action']; +} else { + $action = $routes[$path] ?? 'login'; +} + +switch ($action) { + case 'init': + $controller = new InitController(); + $controller->index(); + break; + + case 'login': + $controller = new LoginController(); + $controller->index(); + break; + + case 'authenticate': + $controller = new LoginController(); + $controller->authenticate(); + break; + + case 'verify': + $controller = new LoginController(); + $controller->verify(); + break; + + case 'verify-code': + $controller = new LoginController(); + $controller->verifyCode(); + break; + + case 'dashboard': + $controller = new DashboardController(); + $controller->index(); + break; + + case 'notes': + $controller = new NotesController(); + $controller->index(); + break; + + case 'note-create': + $controller = new NotesController(); + $controller->create(); + break; + + case 'note-edit': + $controller = new NotesController(); + $controller->edit(); + break; + + case 'note-save': + $controller = new NotesController(); + $controller->save(); + break; + + case 'note-delete': + $controller = new NotesController(); + $controller->delete(); + break; + + case 'calendar': + $controller = new CalendarController(); + $controller->index(); + break; + + case 'event-create': + $controller = new CalendarController(); + $controller->create(); + break; + + case 'event-edit': + $controller = new CalendarController(); + $controller->edit(); + break; + + case 'event-save': + $controller = new CalendarController(); + $controller->save(); + break; + + case 'event-delete': + $controller = new CalendarController(); + $controller->delete(); + break; + + case 'logout': + session_destroy(); + header('Location: /logowanie'); + exit; + break; + + default: + header('Location: /logowanie'); + exit; +} \ No newline at end of file diff --git a/models/CalendarEvent.php b/models/CalendarEvent.php new file mode 100644 index 0000000..5e1229b --- /dev/null +++ b/models/CalendarEvent.php @@ -0,0 +1,104 @@ +db = Database::getInstance()->getConnection(); + $this->initTable(); + } + + private function initTable() + { + $sql = "CREATE TABLE IF NOT EXISTS calendar_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + title TEXT NOT NULL, + content TEXT, + event_date DATE NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + )"; + + try { + $this->db->exec($sql); + } catch (PDOException $e) { + // Tabela już istnieje + } + } + + public function getByMonth($userId, $month) + { + $stmt = $this->db->prepare(" + SELECT * FROM calendar_events + WHERE user_id = ? AND strftime('%Y-%m', event_date) = ? + ORDER BY event_date ASC, id ASC + "); + $stmt->execute([$userId, $month]); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + public function getByDate($userId, $date) + { + $stmt = $this->db->prepare(" + SELECT * FROM calendar_events + WHERE user_id = ? AND event_date = ? + ORDER BY id ASC + "); + $stmt->execute([$userId, $date]); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + public function getById($id, $userId) + { + $stmt = $this->db->prepare(" + SELECT * FROM calendar_events + WHERE id = ? AND user_id = ? + "); + $stmt->execute([$id, $userId]); + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + public function create($userId, $title, $content, $eventDate) + { + $stmt = $this->db->prepare(" + INSERT INTO calendar_events (user_id, title, content, event_date) + VALUES (?, ?, ?, ?) + "); + return $stmt->execute([$userId, $title, $content, $eventDate]); + } + + public function update($id, $userId, $title, $content, $eventDate) + { + $stmt = $this->db->prepare(" + UPDATE calendar_events + SET title = ?, content = ?, event_date = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? AND user_id = ? + "); + return $stmt->execute([$title, $content, $eventDate, $id, $userId]); + } + + public function delete($id, $userId) + { + $stmt = $this->db->prepare(" + DELETE FROM calendar_events + WHERE id = ? AND user_id = ? + "); + return $stmt->execute([$id, $userId]); + } + + public function countByDateRange($userId, $startDate, $endDate) + { + $stmt = $this->db->prepare(" + SELECT COUNT(*) AS count + FROM calendar_events + WHERE user_id = ? AND event_date BETWEEN ? AND ? + "); + $stmt->execute([$userId, $startDate, $endDate]); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + return (int)$result['count']; + } +} diff --git a/models/Database.php b/models/Database.php new file mode 100644 index 0000000..f46e81f --- /dev/null +++ b/models/Database.php @@ -0,0 +1,73 @@ +dbPath = __DIR__ . '/../database/database.db'; + } + + public static function getInstance() + { + if (self::$instance === null) { + self::$instance = new self(); + } + return self::$instance; + } + + public function getConnection() + { + if ($this->connection === null) { + try { + $this->connection = new PDO('sqlite:' . $this->dbPath); + $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } catch (PDOException $e) { + die('Connection failed: ' . $e->getMessage()); + } + } + return $this->connection; + } + + public function initDatabase() + { + $db = $this->getConnection(); + + // Tworzenie tabeli użytkowników + $sql = "CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )"; + $db->exec($sql); + + // Tworzenie tabeli kodów weryfikacyjnych + $sql = "CREATE TABLE IF NOT EXISTS verification_codes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + code TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + expires_at DATETIME NOT NULL, + used INTEGER DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES users(id) + )"; + $db->exec($sql); + + // Dodawanie testowego użytkownika + $username = 'projectpro'; + $password = password_hash('testowehaslo', PASSWORD_DEFAULT); + + try { + $stmt = $db->prepare("INSERT INTO users (username, password) VALUES (?, ?)"); + $stmt->execute([$username, $password]); + } catch (PDOException $e) { + // Użytkownik już istnieje + } + + return true; + } +} diff --git a/models/Notes.php b/models/Notes.php new file mode 100644 index 0000000..ee4d11a --- /dev/null +++ b/models/Notes.php @@ -0,0 +1,92 @@ +db = Database::getInstance()->getConnection(); + $this->initTable(); + } + + private function initTable() + { + // Tworzenie tabeli notatek jeśli nie istnieje + $sql = "CREATE TABLE IF NOT EXISTS notes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + title TEXT NOT NULL, + content TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + )"; + + try { + $this->db->exec($sql); + } catch (PDOException $e) { + // Tabela już istnieje + } + } + + public function getAllByUser($userId) + { + $stmt = $this->db->prepare(" + SELECT * FROM notes + WHERE user_id = ? + ORDER BY updated_at DESC + "); + $stmt->execute([$userId]); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + public function getById($id, $userId) + { + $stmt = $this->db->prepare(" + SELECT * FROM notes + WHERE id = ? AND user_id = ? + "); + $stmt->execute([$id, $userId]); + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + public function create($userId, $title, $content) + { + $stmt = $this->db->prepare(" + INSERT INTO notes (user_id, title, content) + VALUES (?, ?, ?) + "); + return $stmt->execute([$userId, $title, $content]); + } + + public function update($id, $userId, $title, $content) + { + $stmt = $this->db->prepare(" + UPDATE notes + SET title = ?, content = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? AND user_id = ? + "); + return $stmt->execute([$title, $content, $id, $userId]); + } + + public function delete($id, $userId) + { + $stmt = $this->db->prepare(" + DELETE FROM notes + WHERE id = ? AND user_id = ? + "); + return $stmt->execute([$id, $userId]); + } + + public function getCount($userId) + { + $stmt = $this->db->prepare(" + SELECT COUNT(*) as count FROM notes + WHERE user_id = ? + "); + $stmt->execute([$userId]); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + return $result['count']; + } +} diff --git a/models/User.php b/models/User.php new file mode 100644 index 0000000..ad9141a --- /dev/null +++ b/models/User.php @@ -0,0 +1,70 @@ +db = Database::getInstance()->getConnection(); + } + + public function authenticate($username, $password) + { + $stmt = $this->db->prepare("SELECT * FROM users WHERE username = ?"); + $stmt->execute([$username]); + $user = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($user && password_verify($password, $user['password'])) { + return $user; + } + + return false; + } + + public function generateVerificationCode($userId) + { + // Generowanie 6-cyfrowego kodu + $code = sprintf('%06d', random_int(0, 999999)); + + // Ustawienie czasu wygaśnięcia (15 minut) + $expiresAt = date('Y-m-d H:i:s', strtotime('+15 minutes')); + + // Usuwanie starych nieużytych kodów dla tego użytkownika + $stmt = $this->db->prepare("DELETE FROM verification_codes WHERE user_id = ? AND used = 0"); + $stmt->execute([$userId]); + + // Zapisywanie nowego kodu + $stmt = $this->db->prepare("INSERT INTO verification_codes (user_id, code, expires_at) VALUES (?, ?, ?)"); + $stmt->execute([$userId, $code, $expiresAt]); + + return $code; + } + + public function verifyCode($userId, $code) + { + $stmt = $this->db->prepare(" + SELECT * FROM verification_codes + WHERE user_id = ? AND code = ? AND used = 0 AND expires_at > datetime('now') + ORDER BY created_at DESC LIMIT 1 + "); + $stmt->execute([$userId, $code]); + $verification = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($verification) { + // Oznaczenie kodu jako użyty + $stmt = $this->db->prepare("UPDATE verification_codes SET used = 1 WHERE id = ?"); + $stmt->execute([$verification['id']]); + return true; + } + + return false; + } + + public function getUserById($userId) + { + $stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?"); + $stmt->execute([$userId]); + return $stmt->fetch(PDO::FETCH_ASSOC); + } +} diff --git a/public/css/calendar.css b/public/css/calendar.css new file mode 100644 index 0000000..6cce623 --- /dev/null +++ b/public/css/calendar.css @@ -0,0 +1,81 @@ +/* Calendar styles */ + +.calendar-title { + font-weight: 600; +} + +.calendar-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 0.5rem; +} + +.calendar-weekday { + text-align: center; + font-weight: 600; + color: #6c757d; + padding: 0.5rem 0; +} + +.calendar-cell { + min-height: 110px; + border: 1px solid #e9ecef; + border-radius: 0.5rem; + padding: 0.5rem; + background: #fff; + position: relative; +} + +.calendar-cell--disabled { + background: #f8f9fa; + color: #adb5bd; +} + +.calendar-cell--selected { + border-color: #0d6efd; + box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.2); +} + +.calendar-day { + display: inline-block; + font-weight: 600; + color: #212529; + text-decoration: none; +} + +.calendar-day:hover { + text-decoration: underline; +} + +.calendar-events { + margin-top: 0.4rem; + font-size: 0.8rem; +} + +.calendar-event { + display: flex; + align-items: center; + gap: 0.25rem; + color: #0d6efd; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.calendar-event i { + font-size: 0.5rem; +} + +.calendar-event--more { + color: #6c757d; +} + +@media (max-width: 768px) { + .calendar-grid { + grid-template-columns: repeat(2, 1fr); + } + + .calendar-weekday { + display: none; + } +} diff --git a/public/css/notes.css b/public/css/notes.css new file mode 100644 index 0000000..edfd452 --- /dev/null +++ b/public/css/notes.css @@ -0,0 +1,137 @@ +/* Notes styles */ + +.note-card { + transition: transform 0.2s, box-shadow 0.2s; + border-radius: 0.5rem; +} + +.note-card:hover { + transform: translateY(-5px); + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; +} + +.note-content { + min-height: 100px; + color: #666; + font-size: 0.9rem; + line-height: 1.6; +} + +.note-meta { + border-top: 1px solid #eee; + padding-top: 0.5rem; +} + +/* Gradient backgrounds */ +.bg-gradient-info { + background: linear-gradient(135deg, #36d1dc 0%, #5b86e5 100%); +} + +.bg-gradient-success { + background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); +} + +.hover-shadow { + transition: all 0.3s ease; + cursor: pointer; +} + +.hover-shadow:hover { + transform: translateY(-5px); + box-shadow: 0 1rem 2rem rgba(0, 0, 0, 0.2) !important; +} + +/* Modal animations */ +.modal.fade .modal-dialog { + transition: transform 0.3s ease-out; + transform: scale(0.9); +} + +.modal.show .modal-dialog { + transform: scale(1); +} + +/* Navbar improvements */ +.navbar { + box-shadow: 0 2px 4px rgba(0,0,0,.1); +} + +.navbar-nav .nav-link { + transition: all 0.2s ease; + border-radius: 0.25rem; + margin: 0 0.25rem; +} + +.navbar-nav .nav-link:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +.navbar-nav .nav-link.active { + background-color: rgba(255, 255, 255, 0.15); + font-weight: 500; +} + +/* Card improvements */ +.card { + border: none; + border-radius: 0.75rem; +} + +/* Button improvements */ +.btn { + transition: all 0.2s ease; +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.btn:active { + transform: translateY(0); +} + +/* Empty state */ +.fa-inbox { + opacity: 0.3; +} + +/* Responsive textarea */ +textarea { + resize: vertical; + min-height: 200px; +} + +/* Delete modal styling */ +#deleteModal .modal-header { + border-top-left-radius: 0.75rem; + border-top-right-radius: 0.75rem; +} + +#deleteModal .modal-content { + border-radius: 0.75rem; + border: none; +} + +/* Alert animations */ +@keyframes slideIn { + from { + transform: translateY(-20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.alert { + animation: slideIn 0.3s ease-out; +} + +/* Note title truncation on cards */ +.note-card .card-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..48b32fb --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,115 @@ +/* Gradient Background */ +.bg-gradient-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; +} + +/* Login/Verify Card Styles */ +.card { + border-radius: 1rem; +} + +.card-body { + padding: 2rem; +} + +/* Form Styles */ +.form-control-user { + font-size: 0.9rem; + border-radius: 10rem; + padding: 1.5rem 1rem; +} + +.btn-user { + font-size: 0.9rem; + border-radius: 10rem; + padding: 0.75rem 1rem; +} + +/* Background Images for Login/Verify Cards */ +.bg-login-image { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background-size: cover; + background-position: center; + border-radius: 1rem 0 0 1rem; + position: relative; +} + +.bg-login-image::before { + content: "🔐"; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 6rem; + opacity: 0.3; +} + +.bg-verify-image { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + background-size: cover; + background-position: center; + border-radius: 1rem 0 0 1rem; + position: relative; +} + +.bg-verify-image::before { + content: "🔑"; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 6rem; + opacity: 0.3; +} + +/* Navbar Styles */ +.navbar-dark { + box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15); +} + +.navbar-brand { + font-weight: 700; + padding-left: 1rem; +} + +/* Alert Styles */ +.alert { + border-radius: 0.5rem; +} + +/* Card Animations */ +.card { + animation: fadeIn 0.5s ease-in; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Code Input Special Style */ +input[name="code"] { + font-size: 1.5rem; + letter-spacing: 0.5rem; + font-weight: bold; +} + +/* Dashboard Cards */ +.bg-light { + background-color: #f8f9fc !important; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .bg-login-image, + .bg-verify-image { + display: none !important; + } +} diff --git a/public/js/app.js b/public/js/app.js new file mode 100644 index 0000000..3151d75 --- /dev/null +++ b/public/js/app.js @@ -0,0 +1,50 @@ +// Simple app.js for additional functionality + +document.addEventListener('DOMContentLoaded', function() { + // Auto-dismiss alerts after 5 seconds + const alerts = document.querySelectorAll('.alert:not(.alert-info)'); + alerts.forEach(function(alert) { + setTimeout(function() { + const bsAlert = new bootstrap.Alert(alert); + bsAlert.close(); + }, 5000); + }); + + // Add loading state to forms + const forms = document.querySelectorAll('form'); + forms.forEach(function(form) { + form.addEventListener('submit', function(e) { + const submitBtn = form.querySelector('button[type="submit"]'); + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.innerHTML = ' Przetwarzanie...'; + } + }); + }); + + // Code input - accept only numbers + const codeInput = document.getElementById('code'); + if (codeInput) { + codeInput.addEventListener('keypress', function(e) { + // Allow only numbers + if (e.key < '0' || e.key > '9') { + e.preventDefault(); + } + }); + + // Auto-submit when 6 digits entered + codeInput.addEventListener('input', function(e) { + if (this.value.length === 6) { + // Optional: auto-submit after 6 digits + // this.form.submit(); + } + }); + } + + // Add smooth scroll behavior + document.documentElement.style.scrollBehavior = 'smooth'; +}); + +// Console styling for verification code (if shown) +console.log('%cSystem Logowania 2FA', 'color: #667eea; font-size: 24px; font-weight: bold;'); +console.log('%cAplikacja demonstracyjna', 'color: #666; font-size: 12px;'); diff --git a/views/calendar/form.php b/views/calendar/form.php new file mode 100644 index 0000000..1bf2e5b --- /dev/null +++ b/views/calendar/form.php @@ -0,0 +1,152 @@ +' + . ''; + +ob_start(); +?> + + +
+
+
+
+
+

+ +

+
+
+ + + + +
+ + + + + +
+ + +
+ +
+ + +
+ +
+ + + + Pole opcjonalne + +
+ +
+ + Powrót do kalendarza + + +
+
+
+ + + +
+
+
+
+ + diff --git a/views/calendar/index.php b/views/calendar/index.php new file mode 100644 index 0000000..1d964b0 --- /dev/null +++ b/views/calendar/index.php @@ -0,0 +1,221 @@ +' + . ''; + +ob_start(); +?> + + +
+ + + + + + + + + + +
+
Pon
+
Wto
+
Śro
+
Czw
+
Pią
+
Sob
+
Ndz
+ + = 1 && $cellDay <= $daysInMonth; + $dateStr = $isCurrentMonth ? $monthDate->format('Y-m') . '-' . str_pad($cellDay, 2, '0', STR_PAD_LEFT) : ''; + $isSelected = $isCurrentMonth && $dateStr === $selectedDate; + $eventsForDay = $isCurrentMonth && isset($eventsByDate[$dateStr]) ? $eventsByDate[$dateStr] : []; + ?> +
+ + + + + +
+ +
+ + +
+ + 2): ?> +
+ więcej
+ +
+ + +
+ +
+ +
+
+
+
+ Wydarzenia: +
+
+ +

Brak wydarzeń dla wybranego dnia.

+ +
+ +
+
+
+ + +
+

+ +

+
+
+ + + + +
+
+ +
+ +
+
+
+
+
+
+ Szybkie akcje +
+
+

Dodaj wydarzenie dla wybranego dnia:

+ + Dodaj wydarzenie + +
+
+
+
+
+ + + + + function confirmDeleteEvent(eventId, eventTitle) { + document.getElementById("deleteEventId").value = eventId; + document.getElementById("eventTitle").textContent = eventTitle; + const modal = new bootstrap.Modal(document.getElementById("deleteEventModal")); + modal.show(); + } +'; + +require __DIR__ . '/../layout.php'; +?> diff --git a/views/dashboard.php b/views/dashboard.php new file mode 100644 index 0000000..7a9be22 --- /dev/null +++ b/views/dashboard.php @@ -0,0 +1,144 @@ +'; + +ob_start(); +?> + + +
+
+
+
+
+

+ Panel główny +

+
+
+ + +
Szybki dostęp
+ + +
Bezpieczeństwo
+
+
+
+
+
+ Bezpieczne logowanie +
+

Twoje konto jest chronione uwierzytelnianiem dwuskładnikowym

+
+
+
+
+
+
+
+ Sesja aktywna +
+

Twoja sesja jest bezpiecznie zarządzana przez system

+
+
+
+
+
+ +
+
+
+
+ + diff --git a/views/layout.php b/views/layout.php new file mode 100644 index 0000000..001c6de --- /dev/null +++ b/views/layout.php @@ -0,0 +1,22 @@ + + + + + + <?php echo $pageTitle ?? 'Aplikacja'; ?> + + + + + + + + + + + + + + + + diff --git a/views/login.php b/views/login.php new file mode 100644 index 0000000..30a45ea --- /dev/null +++ b/views/login.php @@ -0,0 +1,63 @@ + +
+
+
+
+
+
+ +
+
+
+

System Logowania 2FA

+
+ + + + + +
+
+ +
+
+ +
+ +
+ +
+
+ + Testowy użytkownik: projectpro / testowehaslo + +
+
+
+
+
+
+
+
+
+ diff --git a/views/notes/form.php b/views/notes/form.php new file mode 100644 index 0000000..cad80c1 --- /dev/null +++ b/views/notes/form.php @@ -0,0 +1,147 @@ +' + . ''; + +ob_start(); +?> + + +
+
+
+
+
+

+ + +

+
+
+ + + + +
+ + + + +
+ + +
+ +
+ + + + Pole opcjonalne + +
+ +
+ + Powrót do listy + + +
+
+
+ + + +
+
+
+
+ + const textarea = document.getElementById("content"); + textarea.addEventListener("input", function() { + this.style.height = "auto"; + this.style.height = this.scrollHeight + "px"; + }); +'; + +require __DIR__ . '/../layout.php'; +?> diff --git a/views/notes/index.php b/views/notes/index.php new file mode 100644 index 0000000..cb879ab --- /dev/null +++ b/views/notes/index.php @@ -0,0 +1,170 @@ +' + . ''; + +ob_start(); +?> + + +
+ + + + + + + + +
+

Moje Notatki

+ + Nowa notatka + +
+ + +
+ +

Brak notatek

+

Stwórz swoją pierwszą notatkę, aby zacząć!

+ + Dodaj pierwszą notatkę + +
+ +
+ +
+
+
+
+ + +
+

+ 150 ? '...' : ''); + ?> +

+
+ + format('d.m.Y H:i'); + ?> +
+
+ + Edytuj + + +
+
+
+
+ +
+ +
+

Łącznie notatek:

+
+ +
+ + + + + function confirmDelete(noteId, noteTitle) { + document.getElementById("deleteNoteId").value = noteId; + document.getElementById("noteTitle").textContent = noteTitle; + + const deleteModal = new bootstrap.Modal(document.getElementById("deleteModal")); + deleteModal.show(); + } +'; + +require __DIR__ . '/../layout.php'; +?> \ No newline at end of file diff --git a/views/verify.php b/views/verify.php new file mode 100644 index 0000000..510e974 --- /dev/null +++ b/views/verify.php @@ -0,0 +1,78 @@ +'; + +ob_start(); +?> +
+
+
+
+
+
+
+
+
+
+

Weryfikacja dwuskładnikowa

+

Wprowadź kod weryfikacyjny

+
+ + + + + + + +
+
+ + Wprowadź 6-cyfrowy kod +
+ +
+ +
+ +
+
+
+
+
+
+
+
+ + // Wyświetlanie kodu weryfikacyjnego w konsoli (tylko dla testów) + ' . (isset($_SESSION['test_code']) ? "console.log('%c🔐 KOD WERYFIKACYJNY: " . $_SESSION['test_code'] . "', 'background: #222; color: #bada55; font-size: 20px; padding: 10px;');\n console.log('W produkcji kod zostałby wysłany emailem.');" : '') . ' + + // Auto-focus i formatowanie pola kodu + document.getElementById("code").addEventListener("input", function() { + this.value = this.value.replace(/[^0-9]/g, ""); + }); +'; + +require __DIR__ . '/layout.php'; +?>