From b67542d15907754933a67a2fc63e2442222108a4 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Sat, 21 Feb 2026 17:51:34 +0100 Subject: [PATCH] feat: Implement user authentication and database migration system - Refactored AuthService to use UserRepository for user authentication. - Added .env file for environment configuration. - Created migration system with Migrator and ConnectionFactory classes. - Added database migration files for creating users table. - Implemented settings controller for managing database migrations. - Developed user repository for user data handling. - Created users controller for user management interface. - Added frontend standards and migration documentation. - Introduced reusable UI components and jQuery alerts module. --- .env | 14 + .env.example | 9 +- .vscode/ftp-kr.sync.cache.json | 1299 ++++++++++++++++- DOCS/FRONTEND_STANDARDS.md | 17 + DOCS/MIGRATIONS.md | 19 + bin/migrate.php | 48 + bootstrap/app.php | 2 +- composer.json | 3 +- config/app.php | 1 + config/auth.php | 12 - config/database.php | 14 + .../20260221_000001_create_users_table.sql | 8 + package.json | 2 + public/assets/css/app.css | 2 +- public/assets/css/login.css | 2 +- public/assets/css/modules/jquery-alerts.css | 1 + public/assets/js/modules/jquery-alerts.js | 72 + resources/lang/pl.php | 61 + .../modules/jquery-alerts/jquery-alerts.js | 72 + .../modules/jquery-alerts/jquery-alerts.scss | 58 + resources/scss/app.scss | 229 ++- resources/scss/login.scss | 71 +- resources/scss/shared/_ui-components.scss | 174 +++ resources/views/auth/login.php | 8 +- resources/views/layouts/app.php | 43 +- resources/views/settings/database.php | 95 ++ resources/views/users/index.php | 72 + routes/web.php | 10 + src/Core/Application.php | 30 +- src/Core/Database/ConnectionFactory.php | 38 + src/Core/Database/Migrator.php | 162 ++ src/Modules/Auth/AuthService.php | 23 +- src/Modules/Settings/SettingsController.php | 77 + src/Modules/Users/UserRepository.php | 103 ++ src/Modules/Users/UsersController.php | 95 ++ 35 files changed, 2733 insertions(+), 213 deletions(-) create mode 100644 .env create mode 100644 DOCS/FRONTEND_STANDARDS.md create mode 100644 DOCS/MIGRATIONS.md create mode 100644 bin/migrate.php delete mode 100644 config/auth.php create mode 100644 config/database.php create mode 100644 database/migrations/20260221_000001_create_users_table.sql create mode 100644 public/assets/css/modules/jquery-alerts.css create mode 100644 public/assets/js/modules/jquery-alerts.js create mode 100644 resources/modules/jquery-alerts/jquery-alerts.js create mode 100644 resources/modules/jquery-alerts/jquery-alerts.scss create mode 100644 resources/scss/shared/_ui-components.scss create mode 100644 resources/views/settings/database.php create mode 100644 resources/views/users/index.php create mode 100644 src/Core/Database/ConnectionFactory.php create mode 100644 src/Core/Database/Migrator.php create mode 100644 src/Modules/Settings/SettingsController.php create mode 100644 src/Modules/Users/UserRepository.php create mode 100644 src/Modules/Users/UsersController.php diff --git a/.env b/.env new file mode 100644 index 0000000..f7a5bb1 --- /dev/null +++ b/.env @@ -0,0 +1,14 @@ +APP_NAME=orderPRO +APP_ENV=local +APP_DEBUG=true +APP_URL=https://orderpro.projectpro.pl +SESSION_NAME=orderpro_session + +DB_CONNECTION=mysql +DB_HOST=localhost +DB_HOST_REMOTE=host700513.hostido.net.pl +DB_PORT=3306 +DB_DATABASE=host700513_orderpro +DB_USERNAME=host700513_orderpro +DB_PASSWORD=hrDNtUBg9grwZ7syN77S +DB_CHARSET=utf8mb4 diff --git a/.env.example b/.env.example index c77b3cd..84dab35 100644 --- a/.env.example +++ b/.env.example @@ -4,5 +4,10 @@ APP_DEBUG=true APP_URL=http://localhost:8000 SESSION_NAME=orderpro_session -ADMIN_EMAIL=admin@orderpro.local -ADMIN_PASSWORD_HASH=$2y$10$1eRQmrhEUWgKRZpG08dKOenG4eZrvLQnLdCUfKHrZ/5dzLvxpmRYC +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=orderpro +DB_USERNAME=root +DB_PASSWORD= +DB_CHARSET=utf8mb4 diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index 3764ae7..1db7d95 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -1,7 +1,14 @@ { "ftp://host700513.hostido.net.pl:21@www@orderpro.projectpro.pl": { "public_html": { - "bin": {}, + "bin": { + "build-assets.php": { + "type": "-", + "size": 1508, + "lmtime": 1771460522111, + "modified": false + } + }, "bootstrap": { "app.php": { "type": "-", @@ -13,14 +20,14 @@ "composer.json": { "type": "-", "size": 330, - "lmtime": 1771459542043, + "lmtime": 1771460587937, "modified": false }, "config": { "app.php": { "type": "-", - "size": 519, - "lmtime": 1771459563409, + "size": 622, + "lmtime": 1771460431365, "modified": false }, "auth.php": { @@ -37,14 +44,20 @@ "DOCS": { "BACKLOG_MIKROZADANIA.md": { "type": "-", - "size": 3824, - "lmtime": 1771459773090, + "size": 4262, + "lmtime": 1771460831573, "modified": false }, "PLAN_PROJEKTU.md": { "type": "-", - "size": 4607, - "lmtime": 1771459255987, + "size": 5181, + "lmtime": 1771460804927, + "modified": false + }, + "TODO.md": { + "type": "-", + "size": 132, + "lmtime": 1771460631339, "modified": false } }, @@ -54,19 +67,1209 @@ "lmtime": 1771459546785, "modified": false }, + ".htaccess": { + "type": "-", + "size": 275, + "lmtime": 1771459940730, + "modified": false + }, "index.php": { "type": "-", "size": 71, "lmtime": 1771459937874, "modified": false }, + "node_modules": { + ".bin": { + "sass": { + "type": "-", + "size": 373, + "lmtime": 1771460613205, + "modified": false + }, + "sass.cmd": { + "type": "-", + "size": 317, + "lmtime": 1771460613205, + "modified": false + }, + "sass.ps1": { + "type": "-", + "size": 773, + "lmtime": 1771460613205, + "modified": false + } + }, + "chokidar": { + "esm": { + "handler.d.ts": { + "type": "-", + "size": 3883, + "lmtime": 1771460613054, + "modified": false + }, + "handler.js": { + "type": "-", + "size": 24734, + "lmtime": 1771460613011, + "modified": false + }, + "index.d.ts": { + "type": "-", + "size": 8062, + "lmtime": 1771460613063, + "modified": false + }, + "index.js": { + "type": "-", + "size": 29229, + "lmtime": 1771460613033, + "modified": false + }, + "package.json": { + "type": "-", + "size": 43, + "lmtime": 1771460613042, + "modified": false + } + }, + "handler.d.ts": { + "type": "-", + "size": 3883, + "lmtime": 1771460613058, + "modified": false + }, + "handler.js": { + "type": "-", + "size": 25231, + "lmtime": 1771460613023, + "modified": false + }, + "index.d.ts": { + "type": "-", + "size": 8062, + "lmtime": 1771460613066, + "modified": false + }, + "index.js": { + "type": "-", + "size": 29865, + "lmtime": 1771460613038, + "modified": false + }, + "LICENSE": { + "type": "-", + "size": 1124, + "lmtime": 1771460612994, + "modified": false + }, + "package.json": { + "type": "-", + "size": 1517, + "lmtime": 1771460613046, + "modified": false + }, + "README.md": { + "type": "-", + "size": 13126, + "lmtime": 1771460613050, + "modified": false + } + }, + "detect-libc": { + "index.d.ts": { + "type": "-", + "size": 436, + "lmtime": 1771460613044, + "modified": false + }, + "lib": { + "detect-libc.js": { + "type": "-", + "size": 7503, + "lmtime": 1771460613008, + "modified": false + }, + "elf.js": { + "type": "-", + "size": 982, + "lmtime": 1771460613022, + "modified": false + }, + "filesystem.js": { + "type": "-", + "size": 1097, + "lmtime": 1771460613028, + "modified": false + }, + "process.js": { + "type": "-", + "size": 569, + "lmtime": 1771460613033, + "modified": false + } + }, + "LICENSE": { + "type": "-", + "size": 11357, + "lmtime": 1771460613000, + "modified": false + }, + "package.json": { + "type": "-", + "size": 1278, + "lmtime": 1771460613038, + "modified": false + }, + "README.md": { + "type": "-", + "size": 3215, + "lmtime": 1771460613041, + "modified": false + } + }, + "immutable": { + "dist": { + "immutable.d.ts": { + "type": "-", + "size": 166584, + "lmtime": 1771460613090, + "modified": false + }, + "immutable.es.js": { + "type": "-", + "size": 179381, + "lmtime": 1771460613049, + "modified": false + }, + "immutable.js": { + "type": "-", + "size": 202422, + "lmtime": 1771460613067, + "modified": false + }, + "immutable.js.flow": { + "type": "-", + "size": 62863, + "lmtime": 1771460613013, + "modified": false + }, + "immutable.min.js": { + "type": "-", + "size": 67173, + "lmtime": 1771460613074, + "modified": false + } + }, + "LICENSE": { + "type": "-", + "size": 1099, + "lmtime": 1771460612994, + "modified": false + }, + "package.json": { + "type": "-", + "size": 773, + "lmtime": 1771460613075, + "modified": false + }, + "README.md": { + "type": "-", + "size": 28275, + "lmtime": 1771460613079, + "modified": false + } + }, + "is-extglob": { + "index.js": { + "type": "-", + "size": 441, + "lmtime": 1771460613012, + "modified": false + }, + "LICENSE": { + "type": "-", + "size": 1087, + "lmtime": 1771460613006, + "modified": false + }, + "package.json": { + "type": "-", + "size": 1220, + "lmtime": 1771460612987, + "modified": false + }, + "README.md": { + "type": "-", + "size": 3469, + "lmtime": 1771460612997, + "modified": false + } + }, + "is-glob": { + "index.js": { + "type": "-", + "size": 3628, + "lmtime": 1771460613002, + "modified": false + }, + "LICENSE": { + "type": "-", + "size": 1088, + "lmtime": 1771460612993, + "modified": false + }, + "package.json": { + "type": "-", + "size": 1748, + "lmtime": 1771460613009, + "modified": false + }, + "README.md": { + "type": "-", + "size": 7145, + "lmtime": 1771460613018, + "modified": false + } + }, + "node-addon-api": { + "common.gypi": { + "type": "-", + "size": 724, + "lmtime": 1771460613010, + "modified": false + }, + "except.gypi": { + "type": "-", + "size": 560, + "lmtime": 1771460613017, + "modified": false + }, + "index.js": { + "type": "-", + "size": 377, + "lmtime": 1771460613079, + "modified": false + }, + "LICENSE.md": { + "type": "-", + "size": 1150, + "lmtime": 1771460613086, + "modified": false + }, + "napi.h": { + "type": "-", + "size": 115423, + "lmtime": 1771460613063, + "modified": false + }, + "napi-inl.deprecated.h": { + "type": "-", + "size": 6323, + "lmtime": 1771460613029, + "modified": false + }, + "napi-inl.h": { + "type": "-", + "size": 219411, + "lmtime": 1771460613051, + "modified": false + }, + "node_addon_api.gyp": { + "type": "-", + "size": 793, + "lmtime": 1771460612997, + "modified": false + }, + "node_api.gyp": { + "type": "-", + "size": 132, + "lmtime": 1771460613004, + "modified": false + }, + "noexcept.gypi": { + "type": "-", + "size": 639, + "lmtime": 1771460613026, + "modified": false + }, + "nothing.c": { + "type": "-", + "size": 0, + "lmtime": 1771460612987, + "modified": false + }, + "package.json": { + "type": "-", + "size": 10784, + "lmtime": 1771460613084, + "modified": false + }, + "package-support.json": { + "type": "-", + "size": 467, + "lmtime": 1771460613081, + "modified": false + }, + "README.md": { + "type": "-", + "size": 14050, + "lmtime": 1771460613089, + "modified": false + }, + "tools": { + "check-napi.js": { + "type": "-", + "size": 3176, + "lmtime": 1771460613067, + "modified": false + }, + "clang-format.js": { + "type": "-", + "size": 2002, + "lmtime": 1771460613070, + "modified": false + }, + "conversion.js": { + "type": "-", + "size": 15013, + "lmtime": 1771460613074, + "modified": false + }, + "eslint-format.js": { + "type": "-", + "size": 2071, + "lmtime": 1771460613077, + "modified": false + }, + "README.md": { + "type": "-", + "size": 3217, + "lmtime": 1771460613092, + "modified": false + } + } + }, + "@parcel": { + "watcher": { + "binding.gyp": { + "type": "-", + "size": 3112, + "lmtime": 1771460613094, + "modified": false + }, + "index.d.ts": { + "type": "-", + "size": 1127, + "lmtime": 1771460613150, + "modified": false + }, + "index.js": { + "type": "-", + "size": 1265, + "lmtime": 1771460613146, + "modified": false + }, + "index.js.flow": { + "type": "-", + "size": 998, + "lmtime": 1771460613093, + "modified": false + }, + "LICENSE": { + "type": "-", + "size": 1077, + "lmtime": 1771460612994, + "modified": false + }, + "package.json": { + "type": "-", + "size": 2202, + "lmtime": 1771460613148, + "modified": false + }, + "README.md": { + "type": "-", + "size": 7721, + "lmtime": 1771460613149, + "modified": false + }, + "scripts": { + "build-from-source.js": { + "type": "-", + "size": 285, + "lmtime": 1771460613145, + "modified": false + } + }, + "src": { + "Backend.cc": { + "type": "-", + "size": 4385, + "lmtime": 1771460613004, + "modified": false + }, + "Backend.hh": { + "type": "-", + "size": 874, + "lmtime": 1771460613102, + "modified": false + }, + "binding.cc": { + "type": "-", + "size": 7003, + "lmtime": 1771460613013, + "modified": false + }, + "Debounce.cc": { + "type": "-", + "size": 2552, + "lmtime": 1771460613039, + "modified": false + }, + "Debounce.hh": { + "type": "-", + "size": 883, + "lmtime": 1771460613110, + "modified": false + }, + "DirTree.cc": { + "type": "-", + "size": 4314, + "lmtime": 1771460613045, + "modified": false + }, + "DirTree.hh": { + "type": "-", + "size": 1095, + "lmtime": 1771460613112, + "modified": false + }, + "Event.hh": { + "type": "-", + "size": 2611, + "lmtime": 1771460613114, + "modified": false + }, + "Glob.cc": { + "type": "-", + "size": 539, + "lmtime": 1771460613058, + "modified": false + }, + "Glob.hh": { + "type": "-", + "size": 509, + "lmtime": 1771460613118, + "modified": false + }, + "kqueue": { + "KqueueBackend.cc": { + "type": "-", + "size": 8604, + "lmtime": 1771460613068, + "modified": false + }, + "KqueueBackend.hh": { + "type": "-", + "size": 937, + "lmtime": 1771460613126, + "modified": false + } + }, + "linux": { + "InotifyBackend.cc": { + "type": "-", + "size": 6980, + "lmtime": 1771460613063, + "modified": false + }, + "InotifyBackend.hh": { + "type": "-", + "size": 946, + "lmtime": 1771460613120, + "modified": false + } + }, + "macos": { + "FSEventsBackend.cc": { + "type": "-", + "size": 11213, + "lmtime": 1771460613049, + "modified": false + }, + "FSEventsBackend.hh": { + "type": "-", + "size": 565, + "lmtime": 1771460613116, + "modified": false + } + }, + "PromiseRunner.hh": { + "type": "-", + "size": 2517, + "lmtime": 1771460613129, + "modified": false + }, + "shared": { + "BruteForceBackend.cc": { + "type": "-", + "size": 1198, + "lmtime": 1771460613027, + "modified": false + }, + "BruteForceBackend.hh": { + "type": "-", + "size": 733, + "lmtime": 1771460613105, + "modified": false + } + }, + "Signal.hh": { + "type": "-", + "size": 816, + "lmtime": 1771460613131, + "modified": false + }, + "unix": { + "fts.cc": { + "type": "-", + "size": 1202, + "lmtime": 1771460613054, + "modified": false + }, + "legacy.cc": { + "type": "-", + "size": 2208, + "lmtime": 1771460613073, + "modified": false + } + }, + "wasm": { + "include.h": { + "type": "-", + "size": 3569, + "lmtime": 1771460613098, + "modified": false + }, + "WasmBackend.cc": { + "type": "-", + "size": 4101, + "lmtime": 1771460613076, + "modified": false + }, + "WasmBackend.hh": { + "type": "-", + "size": 963, + "lmtime": 1771460613136, + "modified": false + } + }, + "Watcher.cc": { + "type": "-", + "size": 6391, + "lmtime": 1771460613080, + "modified": false + }, + "Watcher.hh": { + "type": "-", + "size": 1876, + "lmtime": 1771460613138, + "modified": false + }, + "watchman": { + "BSER.cc": { + "type": "-", + "size": 8070, + "lmtime": 1771460613033, + "modified": false + }, + "BSER.hh": { + "type": "-", + "size": 1517, + "lmtime": 1771460613108, + "modified": false + }, + "IPC.hh": { + "type": "-", + "size": 4161, + "lmtime": 1771460613123, + "modified": false + }, + "WatchmanBackend.cc": { + "type": "-", + "size": 8605, + "lmtime": 1771460613084, + "modified": false + }, + "WatchmanBackend.hh": { + "type": "-", + "size": 971, + "lmtime": 1771460613141, + "modified": false + } + }, + "windows": { + "WindowsBackend.cc": { + "type": "-", + "size": 8075, + "lmtime": 1771460613091, + "modified": false + }, + "WindowsBackend.hh": { + "type": "-", + "size": 360, + "lmtime": 1771460613144, + "modified": false + }, + "win_utils.cc": { + "type": "-", + "size": 1267, + "lmtime": 1771460613088, + "modified": false + }, + "win_utils.hh": { + "type": "-", + "size": 238, + "lmtime": 1771460613142, + "modified": false + } + } + }, + "wrapper.js": { + "type": "-", + "size": 1863, + "lmtime": 1771460613147, + "modified": false + } + }, + "watcher-win32-x64": { + "LICENSE": { + "type": "-", + "size": 1077, + "lmtime": 1771460613162, + "modified": false + }, + "README.md": { + "type": "-", + "size": 106, + "lmtime": 1771460613164, + "modified": false + }, + "package.json": { + "type": "-", + "size": 605, + "lmtime": 1771460613162, + "modified": false + }, + "watcher.node": { + "type": "-", + "size": 523264, + "lmtime": 1771460613193, + "modified": false + } + } + }, + "picomatch": { + "index.js": { + "type": "-", + "size": 479, + "lmtime": 1771460613010, + "modified": false + }, + "lib": { + "constants.js": { + "type": "-", + "size": 4458, + "lmtime": 1771460613004, + "modified": false + }, + "parse.js": { + "type": "-", + "size": 27544, + "lmtime": 1771460613029, + "modified": false + }, + "picomatch.js": { + "type": "-", + "size": 9881, + "lmtime": 1771460613035, + "modified": false + }, + "scan.js": { + "type": "-", + "size": 9189, + "lmtime": 1771460613044, + "modified": false + }, + "utils.js": { + "type": "-", + "size": 1994, + "lmtime": 1771460613048, + "modified": false + } + }, + "LICENSE": { + "type": "-", + "size": 1091, + "lmtime": 1771460612992, + "modified": false + }, + "package.json": { + "type": "-", + "size": 1953, + "lmtime": 1771460613051, + "modified": false + }, + "posix.js": { + "type": "-", + "size": 60, + "lmtime": 1771460613039, + "modified": false + }, + "README.md": { + "type": "-", + "size": 28608, + "lmtime": 1771460613058, + "modified": false + } + }, + "readdirp": { + "esm": { + "index.d.ts": { + "type": "-", + "size": 3683, + "lmtime": 1771460613033, + "modified": false + }, + "index.js": { + "type": "-", + "size": 9554, + "lmtime": 1771460613000, + "modified": false + }, + "package.json": { + "type": "-", + "size": 43, + "lmtime": 1771460613015, + "modified": false + } + }, + "index.d.ts": { + "type": "-", + "size": 3683, + "lmtime": 1771460613038, + "modified": false + }, + "index.js": { + "type": "-", + "size": 9967, + "lmtime": 1771460613006, + "modified": false + }, + "LICENSE": { + "type": "-", + "size": 1114, + "lmtime": 1771460612987, + "modified": false + }, + "package.json": { + "type": "-", + "size": 1660, + "lmtime": 1771460613025, + "modified": false + }, + "README.md": { + "type": "-", + "size": 6423, + "lmtime": 1771460613028, + "modified": false + } + }, + "sass": { + "LICENSE": { + "type": "-", + "size": 87282, + "lmtime": 1771460613008, + "modified": false + }, + "package.json": { + "type": "-", + "size": 854, + "lmtime": 1771460613138, + "modified": false + }, + "README.md": { + "type": "-", + "size": 7403, + "lmtime": 1771460613140, + "modified": false + }, + "sass.dart.js": { + "type": "-", + "size": 5629956, + "lmtime": 1771460613131, + "modified": false + }, + "sass.default.cjs": { + "type": "-", + "size": 235, + "lmtime": 1771460613014, + "modified": false + }, + "sass.default.js": { + "type": "-", + "size": 2427, + "lmtime": 1771460613132, + "modified": false + }, + "sass.js": { + "type": "-", + "size": 657, + "lmtime": 1771460613136, + "modified": false + }, + "sass.node.js": { + "type": "-", + "size": 343, + "lmtime": 1771460613137, + "modified": false + }, + "sass.node.mjs": { + "type": "-", + "size": 5502, + "lmtime": 1771460613141, + "modified": false + }, + "types": { + "compile.d.ts": { + "type": "-", + "size": 11071, + "lmtime": 1771460613150, + "modified": false + }, + "deprecations.d.ts": { + "type": "-", + "size": 8076, + "lmtime": 1771460613151, + "modified": false + }, + "exception.d.ts": { + "type": "-", + "size": 1148, + "lmtime": 1771460613153, + "modified": false + }, + "importer.d.ts": { + "type": "-", + "size": 19073, + "lmtime": 1771460613165, + "modified": false + }, + "index.d.ts": { + "type": "-", + "size": 2756, + "lmtime": 1771460613169, + "modified": false + }, + "legacy": { + "exception.d.ts": { + "type": "-", + "size": 1862, + "lmtime": 1771460613154, + "modified": false + }, + "function.d.ts": { + "type": "-", + "size": 22983, + "lmtime": 1771460613156, + "modified": false + }, + "importer.d.ts": { + "type": "-", + "size": 6358, + "lmtime": 1771460613167, + "modified": false + }, + "options.d.ts": { + "type": "-", + "size": 22102, + "lmtime": 1771460613183, + "modified": false + }, + "plugin_this.d.ts": { + "type": "-", + "size": 2107, + "lmtime": 1771460613188, + "modified": false + }, + "render.d.ts": { + "type": "-", + "size": 4443, + "lmtime": 1771460613192, + "modified": false + } + }, + "logger": { + "index.d.ts": { + "type": "-", + "size": 3031, + "lmtime": 1771460613171, + "modified": false + }, + "source_location.d.ts": { + "type": "-", + "size": 486, + "lmtime": 1771460613194, + "modified": false + }, + "source_span.d.ts": { + "type": "-", + "size": 837, + "lmtime": 1771460613195, + "modified": false + } + }, + "options.d.ts": { + "type": "-", + "size": 16499, + "lmtime": 1771460613186, + "modified": false + }, + "util": { + "promise_or.d.ts": { + "type": "-", + "size": 660, + "lmtime": 1771460613190, + "modified": false + } + }, + "value": { + "argument_list.d.ts": { + "type": "-", + "size": 1592, + "lmtime": 1771460613142, + "modified": false + }, + "boolean.d.ts": { + "type": "-", + "size": 616, + "lmtime": 1771460613144, + "modified": false + }, + "calculation.d.ts": { + "type": "-", + "size": 4000, + "lmtime": 1771460613146, + "modified": false + }, + "color.d.ts": { + "type": "-", + "size": 17769, + "lmtime": 1771460613148, + "modified": false + }, + "function.d.ts": { + "type": "-", + "size": 863, + "lmtime": 1771460613162, + "modified": false + }, + "index.d.ts": { + "type": "-", + "size": 6728, + "lmtime": 1771460613172, + "modified": false + }, + "list.d.ts": { + "type": "-", + "size": 1479, + "lmtime": 1771460613174, + "modified": false + }, + "map.d.ts": { + "type": "-", + "size": 1115, + "lmtime": 1771460613176, + "modified": false + }, + "mixin.d.ts": { + "type": "-", + "size": 342, + "lmtime": 1771460613178, + "modified": false + }, + "number.d.ts": { + "type": "-", + "size": 11699, + "lmtime": 1771460613181, + "modified": false + }, + "string.d.ts": { + "type": "-", + "size": 3189, + "lmtime": 1771460613197, + "modified": false + } + } + } + }, + "source-map-js": { + "lib": { + "array-set.js": { + "type": "-", + "size": 3197, + "lmtime": 1771460613005, + "modified": false + }, + "base64.js": { + "type": "-", + "size": 1540, + "lmtime": 1771460613027, + "modified": false + }, + "base64-vlq.js": { + "type": "-", + "size": 4714, + "lmtime": 1771460613015, + "modified": false + }, + "binary-search.js": { + "type": "-", + "size": 4249, + "lmtime": 1771460613034, + "modified": false + }, + "mapping-list.js": { + "type": "-", + "size": 2339, + "lmtime": 1771460613039, + "modified": false + }, + "quick-sort.js": { + "type": "-", + "size": 4068, + "lmtime": 1771460613044, + "modified": false + }, + "source-map-consumer.d.ts": { + "type": "-", + "size": 40, + "lmtime": 1771460613085, + "modified": false + }, + "source-map-consumer.js": { + "type": "-", + "size": 41580, + "lmtime": 1771460613054, + "modified": false + }, + "source-map-generator.d.ts": { + "type": "-", + "size": 41, + "lmtime": 1771460613087, + "modified": false + }, + "source-map-generator.js": { + "type": "-", + "size": 14933, + "lmtime": 1771460613061, + "modified": false + }, + "source-node.d.ts": { + "type": "-", + "size": 33, + "lmtime": 1771460613092, + "modified": false + }, + "source-node.js": { + "type": "-", + "size": 13808, + "lmtime": 1771460613069, + "modified": false + }, + "util.js": { + "type": "-", + "size": 15403, + "lmtime": 1771460613075, + "modified": false + } + }, + "LICENSE": { + "type": "-", + "size": 1526, + "lmtime": 1771460612994, + "modified": false + }, + "package.json": { + "type": "-", + "size": 2548, + "lmtime": 1771460613077, + "modified": false + }, + "README.md": { + "type": "-", + "size": 26040, + "lmtime": 1771460613082, + "modified": false + }, + "source-map.d.ts": { + "type": "-", + "size": 3408, + "lmtime": 1771460613089, + "modified": false + }, + "source-map.js": { + "type": "-", + "size": 405, + "lmtime": 1771460613063, + "modified": false + } + }, + ".package-lock.json": { + "type": "-", + "size": 6405, + "lmtime": 1771460613355, + "modified": false + } + }, + "package.json": { + "type": "-", + "size": 671, + "lmtime": 1771460684115, + "modified": false + }, + "package-lock.json": { + "type": "-", + "size": 14106, + "lmtime": 1771460613352, + "modified": false + }, "public": { "assets": { "css": { + "app.css": { + "type": "-", + "size": 2295, + "lmtime": 1771460692659, + "modified": false + }, + "app.css.map": { + "type": "-", + "size": 791, + "lmtime": 1771460669726, + "modified": false + }, "login.css": { "type": "-", - "size": 3811, - "lmtime": 1771459717220, + "size": 2987, + "lmtime": 1771460693108, + "modified": false + }, + "login.css.map": { + "type": "-", + "size": 958, + "lmtime": 1771460670209, "modified": false } }, @@ -93,49 +1296,77 @@ } }, "resources": { + "lang": { + "pl.php": { + "type": "-", + "size": 1393, + "lmtime": 1771460496306, + "modified": false + } + }, "views": { "auth": { "login.php": { "type": "-", - "size": 1349, - "lmtime": 1771459707811, + "size": 1521, + "lmtime": 1771460485980, "modified": false } }, "dashboard": { "index.php": { "type": "-", - "size": 280, - "lmtime": 1771459711102, + "size": 308, + "lmtime": 1771460502477, "modified": false } }, "layouts": { "app.php": { "type": "-", - "size": 2151, - "lmtime": 1771459836968, + "size": 1014, + "lmtime": 1771460470614, "modified": false }, "auth.php": { "type": "-", - "size": 712, - "lmtime": 1771459686411, + "size": 765, + "lmtime": 1771460477300, "modified": false } } + }, + "scss": { + "app.scss": { + "type": "-", + "size": 2831, + "lmtime": 1771460151261, + "modified": false + }, + "login.scss": { + "type": "-", + "size": 3671, + "lmtime": 1771460219578, + "modified": false + } } }, "routes": { "web.php": { "type": "-", - "size": 1491, - "lmtime": 1771459832787, + "size": 1579, + "lmtime": 1771460461885, "modified": false } }, "src": { "Core": { + "Application.php": { + "type": "-", + "size": 4572, + "lmtime": 1771460438825, + "modified": false + }, "Http": { "Request.php": { "type": "-", @@ -150,6 +1381,14 @@ "modified": false } }, + "I18n": { + "Translator.php": { + "type": "-", + "size": 1676, + "lmtime": 1771460417151, + "modified": false + } + }, "Routing": { "Router.php": { "type": "-", @@ -195,24 +1434,18 @@ "View": { "Template.php": { "type": "-", - "size": 1448, - "lmtime": 1771459621837, + "size": 1685, + "lmtime": 1771460443424, "modified": false } - }, - "Application.php": { - "type": "-", - "size": 4232, - "lmtime": 1771459584783, - "modified": false } }, "Modules": { "Auth": { "AuthController.php": { "type": "-", - "size": 2324, - "lmtime": 1771459827414, + "size": 2470, + "lmtime": 1771460453479, "modified": false }, "AuthMiddleware.php": { @@ -279,10 +1512,10 @@ }, "tmp": {} }, - ".htaccess": { + ".gitignore": { "type": "-", - "size": 275, - "lmtime": 1771459940730, + "size": 82, + "lmtime": 1771460725903, "modified": false } } diff --git a/DOCS/FRONTEND_STANDARDS.md b/DOCS/FRONTEND_STANDARDS.md new file mode 100644 index 0000000..297de47 --- /dev/null +++ b/DOCS/FRONTEND_STANDARDS.md @@ -0,0 +1,17 @@ +# Frontend Standards + +## 1) Wspolne style UI +- Powtarzalne elementy (np. przyciski, tabele, paginacja, alerty, pola formularzy) trzymamy w: + - `resources/scss/shared/_ui-components.scss` +- Widoki maja korzystac z klas wspolnych (`btn`, `table`, `pagination`, `form-control`, `alert`) zamiast duplikowania stylu lokalnie. + +## 2) Moduly JS wielokrotnego uzycia +- Kazdy modul przenoszalny trzymamy w oddzielnym folderze: + - `resources/modules//` +- Minimalny zestaw plikow: + - `resources/modules//.js` + - `resources/modules//.scss` +- Modul ma byc niezalezny od logiki projektu (brak hardcoded sciezek i zaleznosci biznesowych). + +## 3) Przyklad +- Referencyjny modul: `resources/modules/jquery-alerts/` diff --git a/DOCS/MIGRATIONS.md b/DOCS/MIGRATIONS.md new file mode 100644 index 0000000..8c32225 --- /dev/null +++ b/DOCS/MIGRATIONS.md @@ -0,0 +1,19 @@ +# Migracje bazy danych + +## Zasada +- Kazda zmiana schematu bazy to nowy plik `.sql` w `database/migrations`. +- Pliki sa wykonywane rosnaco po nazwie. +- Wykonane migracje sa zapisywane w tabeli `migrations`. + +## Nazewnictwo plikow +- Format: `YYYYMMDD_HHMMSS_opis.sql` +- Przyklad: `20260221_000001_create_users_table.sql` + +## Uruchamianie +- CLI: `php bin/migrate.php` lub `composer migrate` +- Panel: `Ustawienia > Aktualizacja bazy danych > Wykonaj aktualizacje` + +## Kolejne migracje +1. Dodaj nowy plik SQL w `database/migrations`. +2. Wrzuc plik na serwer. +3. Uruchom aktualizacje z panelu albo z CLI. diff --git a/bin/migrate.php b/bin/migrate.php new file mode 100644 index 0000000..a58824b --- /dev/null +++ b/bin/migrate.php @@ -0,0 +1,48 @@ + $dbConfig */ +$dbConfig = require $basePath . '/config/database.php'; +$pdo = ConnectionFactory::make($dbConfig); +$migrator = new Migrator($pdo, $basePath . '/database/migrations'); + +try { + $status = $migrator->status(); + echo 'Pending migrations: ' . (string) ($status['pending'] ?? 0) . PHP_EOL; + + $result = $migrator->runPending(); + foreach ((array) ($result['logs'] ?? []) as $line) { + echo (string) $line . PHP_EOL; + } + + echo 'Migrations complete.' . PHP_EOL; +} catch (Throwable $exception) { + fwrite(STDERR, '[error] ' . $exception->getMessage() . PHP_EOL); + exit(1); +} diff --git a/bootstrap/app.php b/bootstrap/app.php index d8967e7..ba6520c 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -28,7 +28,7 @@ Env::load($basePath . '/.env'); $config = [ 'app' => require $basePath . '/config/app.php', - 'auth' => require $basePath . '/config/auth.php', + 'database' => require $basePath . '/config/database.php', ]; $app = new Application($basePath, $config); diff --git a/composer.json b/composer.json index fcba800..83c1514 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ } }, "scripts": { - "serve": "php -S localhost:8000 -t public public/index.php" + "serve": "php -S localhost:8000 -t public public/index.php", + "migrate": "php bin/migrate.php" } } diff --git a/config/app.php b/config/app.php index 05634c2..6595327 100644 --- a/config/app.php +++ b/config/app.php @@ -16,4 +16,5 @@ return [ 'view_path' => dirname(__DIR__) . '/resources/views', 'lang_path' => dirname(__DIR__) . '/resources/lang', 'log_path' => dirname(__DIR__) . '/storage/logs/app.log', + 'migrations_path' => dirname(__DIR__) . '/database/migrations', ]; diff --git a/config/auth.php b/config/auth.php deleted file mode 100644 index b0e52d0..0000000 --- a/config/auth.php +++ /dev/null @@ -1,12 +0,0 @@ - Env::get('ADMIN_EMAIL', 'admin@orderpro.local'), - 'admin_password_hash' => Env::get( - 'ADMIN_PASSWORD_HASH', - '$2y$10$1eRQmrhEUWgKRZpG08dKOenG4eZrvLQnLdCUfKHrZ/5dzLvxpmRYC' - ), -]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..606978e --- /dev/null +++ b/config/database.php @@ -0,0 +1,14 @@ + Env::get('DB_CONNECTION', 'mysql'), + 'host' => Env::get('DB_HOST', '127.0.0.1'), + 'port' => (int) Env::get('DB_PORT', '3306'), + 'database' => Env::get('DB_DATABASE', 'orderpro'), + 'username' => Env::get('DB_USERNAME', 'root'), + 'password' => Env::get('DB_PASSWORD', ''), + 'charset' => Env::get('DB_CHARSET', 'utf8mb4'), +]; diff --git a/database/migrations/20260221_000001_create_users_table.sql b/database/migrations/20260221_000001_create_users_table.sql new file mode 100644 index 0000000..2272f72 --- /dev/null +++ b/database/migrations/20260221_000001_create_users_table.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS users ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(120) NOT NULL, + email VARCHAR(190) NOT NULL, + password_hash VARCHAR(255) NOT NULL, + created_at DATETIME NOT NULL, + UNIQUE KEY users_email_unique (email) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/package.json b/package.json index 4a1fb1f..7287d69 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build:css": "sass --style=compressed --no-source-map resources/scss/app.scss public/assets/css/app.css && sass --style=compressed --no-source-map resources/scss/login.scss public/assets/css/login.css", + "build:modules": "sass --style=compressed --no-source-map resources/modules/jquery-alerts/jquery-alerts.scss public/assets/css/modules/jquery-alerts.css && copy /Y resources\\modules\\jquery-alerts\\jquery-alerts.js public\\assets\\js\\modules\\jquery-alerts.js", + "build:assets": "npm run build:css && npm run build:modules", "watch:css": "sass --watch --style=expanded --no-source-map resources/scss/app.scss:public/assets/css/app.css resources/scss/login.scss:public/assets/css/login.css" }, "keywords": [], diff --git a/public/assets/css/app.css b/public/assets/css/app.css index a9492fb..5bf04c0 100644 --- a/public/assets/css/app.css +++ b/public/assets/css/app.css @@ -1 +1 @@ -:root{--c-primary: #6690f4;--c-primary-dark: #3164db;--c-bg: #f4f6f9;--c-surface: #ffffff;--c-text: #4e5e6a;--c-text-strong: #2d3748;--c-muted: #718096;--c-border: #e2e8f0;--c-danger: #cc0000;--shadow-card: 0 1px 4px rgba(0, 0, 0, 0.06);--focus-ring: 0 0 0 3px rgba(102, 144, 244, 0.15)}*{box-sizing:border-box}html,body{min-height:100%}body{margin:0;font-family:"Roboto","Segoe UI",sans-serif;font-size:14px;color:var(--c-text);background:var(--c-bg)}a{color:var(--c-primary)}.topbar{height:56px;border-bottom:1px solid var(--c-border);background:var(--c-surface);display:flex;align-items:center;justify-content:space-between;padding:0 20px;position:sticky;top:0;z-index:100}.brand{font-size:22px;font-weight:300;letter-spacing:-0.02em;color:var(--c-text-strong)}.brand strong{font-weight:700}.logout-btn{min-height:38px;border:1px solid var(--c-border);border-radius:8px;padding:0 14px;font:inherit;font-weight:600;color:var(--c-text-strong);background:var(--c-surface);cursor:pointer;transition:border-color .2s ease,color .2s ease,background-color .2s ease}.logout-btn:hover{border-color:#cbd5e0;background:#f8fafc}.logout-btn:focus-visible{outline:none;box-shadow:var(--focus-ring);border-color:var(--c-primary)}.container{max-width:1120px;margin:24px auto;padding:0 18px 24px}.card{background:var(--c-surface);border-radius:10px;box-shadow:var(--shadow-card);padding:24px}.card h1{margin:0 0 10px;color:var(--c-text-strong);font-size:24px;font-weight:700}.muted{color:var(--c-muted)}.accent{color:var(--c-primary);font-weight:600}.btn{min-height:38px;padding:8px 16px;border:0;border-radius:8px;color:#fff;background:var(--c-primary);font:inherit;font-weight:600;text-decoration:none;cursor:pointer;transition:background-color .2s ease}.btn:hover{background:var(--c-primary-dark)}.btn:focus-visible{outline:none;box-shadow:var(--focus-ring)}.form-control{width:100%;min-height:38px;border:1px solid var(--c-border);border-radius:8px;padding:7px 12px;font:inherit;color:var(--c-text-strong);background:#fff;transition:border-color .2s ease,box-shadow .2s ease}.form-control:focus{outline:none;border-color:var(--c-primary);box-shadow:var(--focus-ring)}@media(max-width: 768px){.topbar{padding:0 14px}.brand{font-size:20px}.container{margin-top:16px;padding:0 14px 18px}.card{padding:18px}} +:root{--c-primary: #6690f4;--c-primary-dark: #3164db;--c-bg: #f4f6f9;--c-surface: #ffffff;--c-text: #4e5e6a;--c-text-strong: #2d3748;--c-muted: #718096;--c-border: #e2e8f0;--c-danger: #cc0000;--focus-ring: 0 0 0 3px rgba(102, 144, 244, 0.15);--shadow-card: 0 1px 4px rgba(0, 0, 0, 0.06)}.btn{display:inline-flex;align-items:center;justify-content:center;min-height:38px;padding:8px 16px;border:1px solid rgba(0,0,0,0);border-radius:8px;font:inherit;font-weight:600;text-decoration:none;cursor:pointer;transition:background-color .2s ease,border-color .2s ease,color .2s ease,transform .1s ease}.btn--primary{color:#fff;background:var(--c-primary)}.btn--primary:hover{background:var(--c-primary-dark)}.btn--secondary{color:var(--c-text-strong);border-color:var(--c-border);background:var(--c-surface)}.btn--secondary:hover{border-color:#cbd5e0;background:#f8fafc}.btn--block{width:100%}.btn:active{transform:translateY(1px)}.btn:focus-visible{outline:none;box-shadow:var(--focus-ring);border-color:var(--c-primary)}.form-control{width:100%;min-height:38px;border:1px solid var(--c-border);border-radius:8px;padding:7px 12px;font:inherit;color:var(--c-text-strong);background:#fff;transition:border-color .2s ease,box-shadow .2s ease}.form-control:focus{outline:none;border-color:var(--c-primary);box-shadow:var(--focus-ring)}.alert{padding:12px 14px;border-radius:8px;border:1px solid rgba(0,0,0,0);font-size:13px;min-height:44px}.alert--danger{border-color:#fed7d7;background:#fff5f5;color:var(--c-danger)}.alert--success{border-color:#b7ebcf;background:#f0fff6;color:#0f6b39}.alert--warning{border-color:#f7dd8b;background:#fff8e8;color:#815500}.form-field{display:grid;gap:7px}.field-label{color:var(--c-text-strong);font-size:13px;font-weight:600}.table-wrap{width:100%;overflow-x:auto}.table{width:100%;border-collapse:collapse;background:var(--c-surface)}.table th,.table td{padding:10px 12px;border-bottom:1px solid var(--c-border);text-align:left}.table th{color:var(--c-text-strong);font-weight:700;background:#f8fafc}.pagination{display:flex;align-items:center;flex-wrap:wrap;gap:8px}.pagination__item{display:inline-flex;align-items:center;justify-content:center;min-width:36px;height:36px;padding:0 10px;border-radius:8px;border:1px solid var(--c-border);color:var(--c-text-strong);background:var(--c-surface);text-decoration:none;font-weight:600}.pagination__item:hover{border-color:#cbd5e0;background:#f8fafc}.pagination__item.is-active{border-color:var(--c-primary);color:var(--c-primary);background:#edf2ff}*{box-sizing:border-box}html,body{min-height:100%}body{margin:0;font-family:"Roboto","Segoe UI",sans-serif;font-size:14px;color:var(--c-text);background:var(--c-bg)}a{color:var(--c-primary)}.app-shell{min-height:100vh;display:grid;grid-template-columns:260px 1fr}.sidebar{border-right:1px solid var(--c-border);border-right-color:#243041;background:#111a28;padding:18px 14px}.sidebar__brand{margin:4px 10px 16px;color:#e9f0ff;font-size:24px;font-weight:300;letter-spacing:-0.02em}.sidebar__brand strong{font-weight:700}.sidebar__nav{display:grid;gap:6px}.sidebar__link{border-radius:8px;padding:10px 12px;text-decoration:none;color:#cbd5e1;font-weight:600}.sidebar__link:hover{color:#f8fafc;background:#1b2a3f}.sidebar__link.is-active{color:#fff;background:#2e4f93}.app-main{min-width:0}.topbar{height:56px;border-bottom:1px solid var(--c-border);background:var(--c-surface);display:flex;align-items:center;justify-content:space-between;padding:0 20px;position:sticky;top:0;z-index:100}.brand{font-size:22px;font-weight:300;letter-spacing:-0.02em;color:var(--c-text-strong)}.brand strong{font-weight:700}.container{max-width:1120px;margin:24px auto;padding:0 18px 24px}.card{background:var(--c-surface);border-radius:10px;box-shadow:var(--shadow-card);padding:24px}.card h1{margin:0 0 10px;color:var(--c-text-strong);font-size:24px;font-weight:700}.muted{color:var(--c-muted)}.accent{color:var(--c-primary);font-weight:600}.users-form{display:grid;gap:14px;max-width:460px}.section-title{margin:0;color:var(--c-text-strong);font-size:20px;font-weight:700}.mt-12{margin-top:12px}.mt-16{margin-top:16px}.settings-grid{display:grid;grid-template-columns:repeat(3, minmax(0, 1fr));gap:12px}.settings-nav{display:flex;gap:8px;flex-wrap:wrap}.settings-nav__link{text-decoration:none;border:1px solid var(--c-border);border-radius:8px;padding:8px 12px;color:var(--c-text-strong);font-weight:600}.settings-nav__link:hover{background:#f8fafc}.settings-nav__link.is-active{border-color:var(--c-primary);color:var(--c-primary);background:#edf2ff}.settings-stat{border:1px solid var(--c-border);border-radius:8px;padding:12px;background:#f8fafc}.settings-stat__label{display:block;color:var(--c-muted);font-size:12px;margin-bottom:4px}.settings-stat__value{color:var(--c-text-strong);font-size:20px}.settings-logs{margin:0;padding:12px;border-radius:8px;border:1px solid var(--c-border);background:#0b1220;color:#d1d5db;font-size:12px;line-height:1.5;overflow:auto}@media(max-width: 768px){.app-shell{grid-template-columns:1fr}.sidebar{border-right:0;border-bottom:1px solid #243041;padding:14px}.sidebar__brand{margin:0 0 10px;font-size:22px}.sidebar__nav{display:flex;gap:8px;overflow-x:auto}.sidebar__link{white-space:nowrap}.topbar{padding:0 14px}.container{margin-top:16px;padding:0 14px 18px}.settings-grid{grid-template-columns:1fr}.card{padding:18px}} diff --git a/public/assets/css/login.css b/public/assets/css/login.css index 851e48e..ed66491 100644 --- a/public/assets/css/login.css +++ b/public/assets/css/login.css @@ -1 +1 @@ -:root{--c-primary: #6690f4;--c-primary-dark: #3164db;--c-bg: #f4f6f9;--c-surface: #ffffff;--c-text: #4e5e6a;--c-text-strong: #2d3748;--c-border: #e2e8f0;--c-muted: #718096;--c-danger: #cc0000;--focus-ring: 0 0 0 3px rgba(102, 144, 244, 0.15);--shadow-card: 0 20px 50px rgba(22, 34, 58, 0.14)}*{box-sizing:border-box}html,body{min-height:100%}body{margin:0;font-family:"Roboto","Segoe UI",sans-serif;color:var(--c-text);background:var(--c-bg);overflow-x:hidden}.bg-orb{position:fixed;width:460px;height:460px;border-radius:999px;filter:blur(28px);z-index:0;opacity:.45;pointer-events:none}.bg-orb-left{top:-200px;left:-180px;background:radial-gradient(circle, rgba(102, 144, 244, 0.48) 0%, rgba(102, 144, 244, 0) 70%)}.bg-orb-right{right:-200px;bottom:-220px;background:radial-gradient(circle, rgba(30, 42, 58, 0.36) 0%, rgba(30, 42, 58, 0) 70%)}.login-page{min-height:100vh;display:grid;place-items:center;padding:32px 20px;position:relative;z-index:1}.login-card{width:100%;max-width:430px;background:var(--c-surface);border:1px solid var(--c-border);border-radius:12px;box-shadow:var(--shadow-card);padding:34px 30px 28px;animation:card-enter 420ms ease-out}.login-header{margin-bottom:24px}.login-badge{display:inline-block;margin:0 0 14px;padding:5px 12px;border-radius:999px;border:1px solid #d9e2ff;background:#eef2ff;color:#3f5faf;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.06em}h1{margin:0;color:var(--c-text-strong);font-size:clamp(1.6rem,2.5vw,1.9rem);line-height:1.15;font-weight:700}.login-subtitle{margin:10px 0 0;font-size:15px;line-height:1.55;color:var(--c-muted)}.alert-error{margin-bottom:18px;padding:12px 14px;border-radius:8px;border:1px solid #fed7d7;background:#fff5f5;color:var(--c-danger);font-size:13px;min-height:44px}.alert-error-placeholder{opacity:.56}.login-form{display:grid;gap:16px}.form-field{display:grid;gap:7px}.field-label{color:var(--c-text-strong);font-size:13px;font-weight:600}input[type=email],input[type=password]{width:100%;height:46px;border:2px solid var(--c-border);border-radius:8px;padding:0 14px;font:inherit;color:var(--c-text-strong);background:#fff;transition:border-color .2s ease,box-shadow .2s ease}input[type=email]::placeholder,input[type=password]::placeholder{color:#cbd5e0}input[type=email]:focus,input[type=password]:focus{outline:none;border-color:var(--c-primary);box-shadow:var(--focus-ring)}.submit-btn{margin-top:2px;height:48px;border:0;border-radius:8px;font:inherit;font-size:15px;font-weight:600;color:#fff;background:var(--c-primary);cursor:pointer;transition:background-color .2s ease,transform .1s ease}.submit-btn:hover{background:var(--c-primary-dark)}.submit-btn:active{transform:translateY(1px)}.submit-btn:focus-visible{outline:none;box-shadow:var(--focus-ring)}@keyframes card-enter{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@media(max-width: 640px){.login-page{padding:18px 14px}.login-card{padding:24px 20px 20px}h1{font-size:1.55rem}} +:root{--c-primary: #6690f4;--c-primary-dark: #3164db;--c-bg: #f4f6f9;--c-surface: #ffffff;--c-text: #4e5e6a;--c-text-strong: #2d3748;--c-muted: #718096;--c-border: #e2e8f0;--c-danger: #cc0000;--focus-ring: 0 0 0 3px rgba(102, 144, 244, 0.15);--shadow-card: 0 1px 4px rgba(0, 0, 0, 0.06)}.btn{display:inline-flex;align-items:center;justify-content:center;min-height:38px;padding:8px 16px;border:1px solid rgba(0,0,0,0);border-radius:8px;font:inherit;font-weight:600;text-decoration:none;cursor:pointer;transition:background-color .2s ease,border-color .2s ease,color .2s ease,transform .1s ease}.btn--primary{color:#fff;background:var(--c-primary)}.btn--primary:hover{background:var(--c-primary-dark)}.btn--secondary{color:var(--c-text-strong);border-color:var(--c-border);background:var(--c-surface)}.btn--secondary:hover{border-color:#cbd5e0;background:#f8fafc}.btn--block{width:100%}.btn:active{transform:translateY(1px)}.btn:focus-visible{outline:none;box-shadow:var(--focus-ring);border-color:var(--c-primary)}.form-control{width:100%;min-height:38px;border:1px solid var(--c-border);border-radius:8px;padding:7px 12px;font:inherit;color:var(--c-text-strong);background:#fff;transition:border-color .2s ease,box-shadow .2s ease}.form-control:focus{outline:none;border-color:var(--c-primary);box-shadow:var(--focus-ring)}.alert{padding:12px 14px;border-radius:8px;border:1px solid rgba(0,0,0,0);font-size:13px;min-height:44px}.alert--danger{border-color:#fed7d7;background:#fff5f5;color:var(--c-danger)}.alert--success{border-color:#b7ebcf;background:#f0fff6;color:#0f6b39}.alert--warning{border-color:#f7dd8b;background:#fff8e8;color:#815500}.form-field{display:grid;gap:7px}.field-label{color:var(--c-text-strong);font-size:13px;font-weight:600}.table-wrap{width:100%;overflow-x:auto}.table{width:100%;border-collapse:collapse;background:var(--c-surface)}.table th,.table td{padding:10px 12px;border-bottom:1px solid var(--c-border);text-align:left}.table th{color:var(--c-text-strong);font-weight:700;background:#f8fafc}.pagination{display:flex;align-items:center;flex-wrap:wrap;gap:8px}.pagination__item{display:inline-flex;align-items:center;justify-content:center;min-width:36px;height:36px;padding:0 10px;border-radius:8px;border:1px solid var(--c-border);color:var(--c-text-strong);background:var(--c-surface);text-decoration:none;font-weight:600}.pagination__item:hover{border-color:#cbd5e0;background:#f8fafc}.pagination__item.is-active{border-color:var(--c-primary);color:var(--c-primary);background:#edf2ff}:root{--shadow-card: 0 20px 50px rgba(22, 34, 58, 0.14)}*{box-sizing:border-box}html,body{min-height:100%}body{margin:0;font-family:"Roboto","Segoe UI",sans-serif;color:var(--c-text);background:var(--c-bg);overflow-x:hidden}.bg-orb{position:fixed;width:460px;height:460px;border-radius:999px;filter:blur(28px);z-index:0;opacity:.45;pointer-events:none}.bg-orb-left{top:-200px;left:-180px;background:radial-gradient(circle, rgba(102, 144, 244, 0.48) 0%, rgba(102, 144, 244, 0) 70%)}.bg-orb-right{right:-200px;bottom:-220px;background:radial-gradient(circle, rgba(30, 42, 58, 0.36) 0%, rgba(30, 42, 58, 0) 70%)}.login-page{min-height:100vh;display:grid;place-items:center;padding:32px 20px;position:relative;z-index:1}.login-card{width:100%;max-width:430px;background:var(--c-surface);border:1px solid var(--c-border);border-radius:12px;box-shadow:var(--shadow-card);padding:34px 30px 28px;animation:card-enter 420ms ease-out}.login-header{margin-bottom:24px}.login-badge{display:inline-block;margin:0 0 14px;padding:5px 12px;border-radius:999px;border:1px solid #d9e2ff;background:#eef2ff;color:#3f5faf;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.06em}h1{margin:0;color:var(--c-text-strong);font-size:clamp(1.6rem,2.5vw,1.9rem);line-height:1.15;font-weight:700}.login-subtitle{margin:10px 0 0;font-size:15px;line-height:1.55;color:var(--c-muted)}.login-alert{margin-bottom:18px}.login-alert-placeholder{opacity:.56}.login-form{display:grid;gap:16px}.form-field{display:grid;gap:7px}.field-label{color:var(--c-text-strong);font-size:13px;font-weight:600}.login-form .form-control{min-height:46px;padding:0 14px;border-width:2px}.login-form .form-control::placeholder{color:#cbd5e0}.login-submit{margin-top:2px;font-size:15px;min-height:48px}@keyframes card-enter{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@media(max-width: 640px){.login-page{padding:18px 14px}.login-card{padding:24px 20px 20px}h1{font-size:1.55rem}} diff --git a/public/assets/css/modules/jquery-alerts.css b/public/assets/css/modules/jquery-alerts.css new file mode 100644 index 0000000..a7f26d7 --- /dev/null +++ b/public/assets/css/modules/jquery-alerts.css @@ -0,0 +1 @@ +.jq-alert{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:12px;padding:12px 14px;border:1px solid rgba(0,0,0,0);border-radius:8px;font-size:14px;opacity:0;transform:translateY(4px);transition:opacity .18s ease,transform .18s ease}.jq-alert.is-visible{opacity:1;transform:translateY(0)}.jq-alert--info{color:#1e3a8a;background:#eff6ff;border-color:#bfdbfe}.jq-alert--success{color:#065f46;background:#ecfdf5;border-color:#a7f3d0}.jq-alert--warning{color:#92400e;background:#fffbeb;border-color:#fde68a}.jq-alert--error{color:#991b1b;background:#fef2f2;border-color:#fecaca}.jq-alert__content{flex:1}.jq-alert__close{appearance:none;border:0;padding:0;line-height:1;font-size:18px;cursor:pointer;color:inherit;background:rgba(0,0,0,0)} diff --git a/public/assets/js/modules/jquery-alerts.js b/public/assets/js/modules/jquery-alerts.js new file mode 100644 index 0000000..72d8b44 --- /dev/null +++ b/public/assets/js/modules/jquery-alerts.js @@ -0,0 +1,72 @@ +"use strict"; + +(function (factory) { + if (typeof module === "object" && module.exports) { + module.exports = factory; + return; + } + + if (typeof window.jQuery !== "undefined") { + factory(window.jQuery); + } +})(function ($) { + if (!$ || !$.fn) { + return; + } + + const DEFAULTS = { + type: "info", + dismissible: true, + timeout: 0, + classPrefix: "jq-alert", + }; + + function removeAlert($el) { + $el.removeClass("is-visible"); + window.setTimeout(function () { + $el.remove(); + }, 180); + } + + $.fn.orderProAlert = function (options) { + const settings = $.extend({}, DEFAULTS, options); + + return this.each(function () { + const $host = $(this); + const $alert = $("
", { + class: settings.classPrefix + " " + settings.classPrefix + "--" + settings.type + " is-visible", + role: "alert", + }); + + const $content = $("
", { + class: settings.classPrefix + "__content", + text: String(settings.message || ""), + }); + + $alert.append($content); + + if (settings.dismissible) { + const $close = $(" + diff --git a/resources/views/layouts/app.php b/resources/views/layouts/app.php index 53aa435..9f3f817 100644 --- a/resources/views/layouts/app.php +++ b/resources/views/layouts/app.php @@ -10,16 +10,39 @@ -
-
-
- - -
-
+ +
+ + +
+
+
+ +
+
+ + +
+
+ +
+ +
+
+
diff --git a/resources/views/settings/database.php b/resources/views/settings/database.php new file mode 100644 index 0000000..d22a565 --- /dev/null +++ b/resources/views/settings/database.php @@ -0,0 +1,95 @@ + + +
+

+

+ +
+ +
+

+ + + + + + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + 0): ?> +
+ +
+ +
+ + +
+ +
+ +
+ +
+ +
+

+
+ + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+

+
+
+ diff --git a/resources/views/users/index.php b/resources/views/users/index.php new file mode 100644 index 0000000..ed51b4f --- /dev/null +++ b/resources/views/users/index.php @@ -0,0 +1,72 @@ +
+

+

+
+ +
+

+ + + + + + +
+ +
+ + +
+ + + + + + + + + +
+
+ +
+

+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
diff --git a/routes/web.php b/routes/web.php index 299a00f..45c809e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -7,6 +7,8 @@ use App\Core\Http\Response; use App\Core\Security\Csrf; use App\Modules\Auth\AuthController; use App\Modules\Auth\AuthMiddleware; +use App\Modules\Settings\SettingsController; +use App\Modules\Users\UsersController; return static function (Application $app): void { $router = $app->router(); @@ -15,6 +17,8 @@ return static function (Application $app): void { $translator = $app->translator(); $authController = new AuthController($template, $auth, $translator); + $usersController = new UsersController($template, $translator, $auth, $app->users()); + $settingsController = new SettingsController($template, $translator, $auth, $app->migrator()); $authMiddleware = new AuthMiddleware($auth); $router->get('/health', static fn (Request $request): Response => Response::json([ @@ -37,10 +41,16 @@ return static function (Application $app): void { $user = $auth->user(); $html = $template->render('dashboard/index', [ 'title' => $translator->get('dashboard.title'), + 'activeMenu' => 'dashboard', 'user' => $user, 'csrfToken' => Csrf::token(), ], 'layouts/app'); return Response::html($html); }, [$authMiddleware]); + + $router->get('/users', [$usersController, 'index'], [$authMiddleware]); + $router->post('/users', [$usersController, 'store'], [$authMiddleware]); + $router->get('/settings/database', [$settingsController, 'database'], [$authMiddleware]); + $router->post('/settings/database/migrate', [$settingsController, 'migrate'], [$authMiddleware]); }; diff --git a/src/Core/Application.php b/src/Core/Application.php index 535b2c5..6f02710 100644 --- a/src/Core/Application.php +++ b/src/Core/Application.php @@ -3,6 +3,8 @@ declare(strict_types=1); namespace App\Core; +use App\Core\Database\ConnectionFactory; +use App\Core\Database\Migrator; use App\Core\Http\Request; use App\Core\Http\Response; use App\Core\I18n\Translator; @@ -11,6 +13,8 @@ use App\Core\Support\Logger; use App\Core\Support\Session; use App\Core\View\Template; use App\Modules\Auth\AuthService; +use App\Modules\Users\UserRepository; +use PDO; use Throwable; final class Application @@ -18,6 +22,9 @@ final class Application private Router $router; private Template $template; private AuthService $authService; + private UserRepository $userRepository; + private Migrator $migrator; + private PDO $db; private Logger $logger; private Translator $translator; @@ -34,7 +41,13 @@ final class Application (string) $this->config('app.locale', 'pl') ); $this->template = new Template((string) $this->config('app.view_path'), $this->translator); - $this->authService = new AuthService((array) $this->config('auth', [])); + $this->db = ConnectionFactory::make((array) $this->config('database', [])); + $this->userRepository = new UserRepository($this->db); + $this->migrator = new Migrator( + $this->db, + (string) $this->config('app.migrations_path', $this->basePath('database/migrations')) + ); + $this->authService = new AuthService($this->userRepository); $this->logger = new Logger((string) $this->config('app.log_path')); } @@ -84,6 +97,21 @@ final class Application return $this->logger; } + public function users(): UserRepository + { + return $this->userRepository; + } + + public function db(): PDO + { + return $this->db; + } + + public function migrator(): Migrator + { + return $this->migrator; + } + public function translator(): Translator { return $this->translator; diff --git a/src/Core/Database/ConnectionFactory.php b/src/Core/Database/ConnectionFactory.php new file mode 100644 index 0000000..f86226d --- /dev/null +++ b/src/Core/Database/ConnectionFactory.php @@ -0,0 +1,38 @@ + $config + */ + public static function make(array $config): PDO + { + $driver = (string) ($config['driver'] ?? 'mysql'); + if ($driver !== 'mysql') { + throw new RuntimeException('Unsupported database driver: ' . $driver); + } + + $host = (string) ($config['host'] ?? '127.0.0.1'); + $port = (int) ($config['port'] ?? 3306); + $database = (string) ($config['database'] ?? ''); + $charset = (string) ($config['charset'] ?? 'utf8mb4'); + + if ($database === '') { + throw new RuntimeException('Database name is required.'); + } + + $dsn = sprintf('mysql:host=%s;port=%d;dbname=%s;charset=%s', $host, $port, $database, $charset); + + return new PDO($dsn, (string) ($config['username'] ?? ''), (string) ($config['password'] ?? ''), [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]); + } +} diff --git a/src/Core/Database/Migrator.php b/src/Core/Database/Migrator.php new file mode 100644 index 0000000..cf65692 --- /dev/null +++ b/src/Core/Database/Migrator.php @@ -0,0 +1,162 @@ + + * } + */ + public function status(): array + { + $this->ensureMigrationsTable(); + + $allFiles = $this->migrationFiles(); + $appliedFiles = $this->appliedFilenames(); + $pendingFiles = array_values(array_diff($allFiles, $appliedFiles)); + + return [ + 'total' => count($allFiles), + 'applied' => count($appliedFiles), + 'pending' => count($pendingFiles), + 'pending_files' => $pendingFiles, + ]; + } + + /** + * @return array{ + * executed:int, + * skipped:int, + * logs:array + * } + */ + public function runPending(): array + { + $this->acquireLock(); + + try { + $this->ensureMigrationsTable(); + + $allFiles = $this->migrationFiles(); + $appliedFiles = $this->appliedFilenames(); + $pendingFiles = array_values(array_diff($allFiles, $appliedFiles)); + + $insert = $this->pdo->prepare( + 'INSERT INTO migrations (filename, executed_at) VALUES (:filename, :executed_at)' + ); + + $executed = 0; + $skipped = 0; + $logs = []; + + foreach ($pendingFiles as $filename) { + $fullPath = rtrim($this->migrationsPath, '/\\') . DIRECTORY_SEPARATOR . $filename; + $sql = file_get_contents($fullPath); + if ($sql === false || trim($sql) === '') { + $skipped++; + $logs[] = '[skip-empty] ' . $filename; + continue; + } + + $this->pdo->beginTransaction(); + try { + $this->pdo->exec($sql); + $insert->execute([ + 'filename' => $filename, + 'executed_at' => date('Y-m-d H:i:s'), + ]); + $this->pdo->commit(); + $executed++; + $logs[] = '[ok] ' . $filename; + } catch (Throwable $exception) { + if ($this->pdo->inTransaction()) { + $this->pdo->rollBack(); + } + + $logs[] = '[error] ' . $filename . ' - ' . $exception->getMessage(); + throw $exception; + } + } + + return [ + 'executed' => $executed, + 'skipped' => $skipped, + 'logs' => $logs, + ]; + } finally { + $this->releaseLock(); + } + } + + private function ensureMigrationsTable(): void + { + $this->pdo->exec( + 'CREATE TABLE IF NOT EXISTS migrations ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + filename VARCHAR(190) NOT NULL, + executed_at DATETIME NOT NULL, + UNIQUE KEY migrations_filename_unique (filename) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' + ); + } + + private function acquireLock(): void + { + $statement = $this->pdo->query("SELECT GET_LOCK('orderpro_migrations_lock', 10)"); + $value = $statement !== false ? $statement->fetchColumn() : false; + if ((string) $value !== '1') { + throw new RuntimeException('Nie mozna uzyskac blokady migracji. Sprobuj ponownie za chwile.'); + } + } + + private function releaseLock(): void + { + $this->pdo->query("DO RELEASE_LOCK('orderpro_migrations_lock')"); + } + + /** + * @return array + */ + private function migrationFiles(): array + { + $files = glob(rtrim($this->migrationsPath, '/\\') . DIRECTORY_SEPARATOR . '*.sql'); + if (!is_array($files)) { + return []; + } + + $filenames = array_map(static fn (string $path): string => basename($path), $files); + sort($filenames); + + return $filenames; + } + + /** + * @return array + */ + private function appliedFilenames(): array + { + $statement = $this->pdo->query('SELECT filename FROM migrations ORDER BY filename'); + $rows = $statement->fetchAll(PDO::FETCH_COLUMN); + if (!is_array($rows)) { + return []; + } + + return array_values(array_map(static fn (mixed $value): string => (string) $value, $rows)); + } +} diff --git a/src/Modules/Auth/AuthService.php b/src/Modules/Auth/AuthService.php index b08fe77..242644b 100644 --- a/src/Modules/Auth/AuthService.php +++ b/src/Modules/Auth/AuthService.php @@ -4,39 +4,34 @@ declare(strict_types=1); namespace App\Modules\Auth; use App\Core\Support\Session; +use App\Modules\Users\UserRepository; final class AuthService { private const SESSION_USER_KEY = 'auth_user'; - /** - * @param array $config - */ - public function __construct(private readonly array $config) + public function __construct(private readonly UserRepository $users) { } public function attempt(string $email, string $password): bool { - $storedEmail = strtolower((string) ($this->config['admin_email'] ?? '')); - $storedHash = (string) ($this->config['admin_password_hash'] ?? ''); - - if ($storedEmail === '' || $storedHash === '') { + $storedUser = $this->users->findByEmail($email); + if ($storedUser === null) { return false; } - if (strtolower($email) !== $storedEmail) { - return false; - } - - if (!password_verify($password, $storedHash)) { + $storedHash = (string) ($storedUser['password_hash'] ?? ''); + if ($storedHash === '' || !password_verify($password, $storedHash)) { return false; } Session::regenerate(); $_SESSION[self::SESSION_USER_KEY] = [ - 'email' => $storedEmail, + 'id' => (int) ($storedUser['id'] ?? 0), + 'name' => (string) ($storedUser['name'] ?? ''), + 'email' => strtolower((string) ($storedUser['email'] ?? '')), 'login_at' => date(DATE_ATOM), ]; diff --git a/src/Modules/Settings/SettingsController.php b/src/Modules/Settings/SettingsController.php new file mode 100644 index 0000000..03f074c --- /dev/null +++ b/src/Modules/Settings/SettingsController.php @@ -0,0 +1,77 @@ +migrator->status(); + + $html = $this->template->render('settings/database', [ + 'title' => $this->translator->get('settings.database.title'), + 'activeMenu' => 'settings', + 'activeSettings' => 'database', + 'user' => $this->auth->user(), + 'csrfToken' => Csrf::token(), + 'status' => $status, + 'errorMessage' => (string) Flash::get('settings_error', ''), + 'successMessage' => (string) Flash::get('settings_success', ''), + 'runLogs' => (array) Flash::get('settings_migrate_logs', []), + ], 'layouts/app'); + + return Response::html($html); + } + + public function migrate(Request $request): Response + { + $csrfToken = (string) $request->input('_token', ''); + if (!Csrf::validate($csrfToken)) { + Flash::set('settings_error', $this->translator->get('auth.errors.csrf_expired')); + return Response::redirect('/settings/database'); + } + + try { + $result = $this->migrator->runPending(); + $executed = (int) ($result['executed'] ?? 0); + $skipped = (int) ($result['skipped'] ?? 0); + $logs = (array) ($result['logs'] ?? []); + + Flash::set( + 'settings_success', + $this->translator->get('settings.database.flash.updated', [ + 'executed' => (string) $executed, + 'skipped' => (string) $skipped, + ]) + ); + Flash::set('settings_migrate_logs', $logs); + } catch (Throwable $exception) { + Flash::set( + 'settings_error', + $this->translator->get('settings.database.flash.failed') . ' ' . $exception->getMessage() + ); + Flash::set('settings_migrate_logs', ['[error] ' . $exception->getMessage()]); + } + + return Response::redirect('/settings/database'); + } +} diff --git a/src/Modules/Users/UserRepository.php b/src/Modules/Users/UserRepository.php new file mode 100644 index 0000000..f414b94 --- /dev/null +++ b/src/Modules/Users/UserRepository.php @@ -0,0 +1,103 @@ +> + */ + public function all(): array + { + $statement = $this->pdo->query('SELECT id, name, email, created_at FROM users ORDER BY id DESC'); + $rows = $statement->fetchAll(); + + if (!is_array($rows)) { + return []; + } + + return array_map( + static fn (array $row): array => [ + 'id' => (int) ($row['id'] ?? 0), + 'name' => (string) ($row['name'] ?? ''), + 'email' => (string) ($row['email'] ?? ''), + 'created_at' => (string) ($row['created_at'] ?? ''), + ], + $rows + ); + } + + public function hasAny(): bool + { + $statement = $this->pdo->query('SELECT 1 FROM users LIMIT 1'); + return $statement->fetchColumn() !== false; + } + + /** + * @return array|null + */ + public function findByEmail(string $email): ?array + { + $normalizedEmail = strtolower(trim($email)); + if ($normalizedEmail === '') { + return null; + } + + $statement = $this->pdo->prepare( + 'SELECT id, name, email, password_hash, created_at FROM users WHERE email = :email LIMIT 1' + ); + $statement->execute(['email' => $normalizedEmail]); + + $row = $statement->fetch(); + if (!is_array($row)) { + return null; + } + + return [ + 'id' => (int) ($row['id'] ?? 0), + 'name' => (string) ($row['name'] ?? ''), + 'email' => (string) ($row['email'] ?? ''), + 'password_hash' => (string) ($row['password_hash'] ?? ''), + 'created_at' => (string) ($row['created_at'] ?? ''), + ]; + } + + /** + * @return array + */ + public function create(string $name, string $email, string $passwordHash): array + { + $createdAt = date('Y-m-d H:i:s'); + $normalizedName = trim($name); + $normalizedEmail = strtolower(trim($email)); + + $statement = $this->pdo->prepare( + 'INSERT INTO users (name, email, password_hash, created_at) VALUES (:name, :email, :password_hash, :created_at)' + ); + $statement->execute([ + 'name' => $normalizedName, + 'email' => $normalizedEmail, + 'password_hash' => $passwordHash, + 'created_at' => $createdAt, + ]); + + $id = (int) $this->pdo->lastInsertId(); + $record = [ + 'id' => $id, + 'name' => $normalizedName, + 'email' => $normalizedEmail, + 'password_hash' => $passwordHash, + 'created_at' => $createdAt, + ]; + + return $record; + } + +} diff --git a/src/Modules/Users/UsersController.php b/src/Modules/Users/UsersController.php new file mode 100644 index 0000000..b46c738 --- /dev/null +++ b/src/Modules/Users/UsersController.php @@ -0,0 +1,95 @@ + [ + 'id' => (int) ($user['id'] ?? 0), + 'name' => (string) ($user['name'] ?? ''), + 'email' => (string) ($user['email'] ?? ''), + 'created_at' => (string) ($user['created_at'] ?? ''), + ], + $this->users->all() + ); + + $html = $this->template->render('users/index', [ + 'title' => $this->translator->get('users.title'), + 'activeMenu' => 'users', + 'user' => $this->auth->user(), + 'csrfToken' => Csrf::token(), + 'users' => $records, + 'errorMessage' => (string) Flash::get('users_error', ''), + 'successMessage' => (string) Flash::get('users_success', ''), + 'oldName' => (string) Flash::get('users_old_name', ''), + 'oldEmail' => (string) Flash::get('users_old_email', ''), + ], 'layouts/app'); + + return Response::html($html); + } + + public function store(Request $request): Response + { + $csrfToken = (string) $request->input('_token', ''); + if (!Csrf::validate($csrfToken)) { + Flash::set('users_error', $this->translator->get('auth.errors.csrf_expired')); + return Response::redirect('/users'); + } + + $name = trim((string) $request->input('name', '')); + $email = strtolower(trim((string) $request->input('email', ''))); + $password = (string) $request->input('password', ''); + + Flash::set('users_old_name', $name); + Flash::set('users_old_email', $email); + + if (mb_strlen($name) < 2) { + Flash::set('users_error', $this->translator->get('users.validation.name_min')); + return Response::redirect('/users'); + } + + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + Flash::set('users_error', $this->translator->get('users.validation.email_invalid')); + return Response::redirect('/users'); + } + + if (strlen($password) < 8) { + Flash::set('users_error', $this->translator->get('users.validation.password_min')); + return Response::redirect('/users'); + } + + if ($this->users->findByEmail($email) !== null) { + Flash::set('users_error', $this->translator->get('users.validation.email_taken')); + return Response::redirect('/users'); + } + + $passwordHash = password_hash($password, PASSWORD_DEFAULT); + $this->users->create($name, $email, $passwordHash); + + Flash::set('users_success', $this->translator->get('users.flash.created')); + Flash::set('users_old_name', ''); + Flash::set('users_old_email', ''); + + return Response::redirect('/users'); + } +}