From e041cc9aa0afd0f793c78b994f2d33df1208fc7a Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 9 Oct 2021 11:43:35 +0200 Subject: [PATCH 1/4] added web-project --- .gitignore | 1 + .htaccess | 1 + app/Bootstrap.php | 31 ++++++++++++ app/Core/RouterFactory.php | 21 ++++++++ app/UI/@layout.latte | 19 +++++++ app/UI/Error/Error4xx/403.latte | 7 +++ app/UI/Error/Error4xx/404.latte | 8 +++ app/UI/Error/Error4xx/410.latte | 6 +++ app/UI/Error/Error4xx/4xx.latte | 6 +++ app/UI/Error/Error4xx/Error4xxPresenter.php | 27 ++++++++++ app/UI/Error/Error5xx/500.phtml | 27 ++++++++++ app/UI/Error/Error5xx/503.phtml | 24 +++++++++ app/UI/Error/Error5xx/Error5xxPresenter.php | 37 ++++++++++++++ app/UI/Home/HomePresenter.php | 12 +++++ app/UI/Home/default.latte | 38 ++++++++++++++ bin/.gitignore | 2 + composer.json | 38 ++++++++++++++ config/common.neon | 24 +++++++++ config/services.neon | 9 ++++ log/.gitignore | 2 + readme.md | 52 ++++++++++++++++++++ temp/.gitignore | 2 + www/.htaccess | 41 +++++++++++++++ www/favicon.ico | Bin 0 -> 2550 bytes www/index.php | 10 ++++ www/robots.txt | 0 26 files changed, 445 insertions(+) create mode 100644 .htaccess create mode 100644 app/Bootstrap.php create mode 100644 app/Core/RouterFactory.php create mode 100644 app/UI/@layout.latte create mode 100644 app/UI/Error/Error4xx/403.latte create mode 100644 app/UI/Error/Error4xx/404.latte create mode 100644 app/UI/Error/Error4xx/410.latte create mode 100644 app/UI/Error/Error4xx/4xx.latte create mode 100644 app/UI/Error/Error4xx/Error4xxPresenter.php create mode 100644 app/UI/Error/Error5xx/500.phtml create mode 100644 app/UI/Error/Error5xx/503.phtml create mode 100644 app/UI/Error/Error5xx/Error5xxPresenter.php create mode 100644 app/UI/Home/HomePresenter.php create mode 100644 app/UI/Home/default.latte create mode 100644 bin/.gitignore create mode 100644 composer.json create mode 100644 config/common.neon create mode 100644 config/services.neon create mode 100644 log/.gitignore create mode 100644 readme.md create mode 100644 temp/.gitignore create mode 100644 www/.htaccess create mode 100644 www/favicon.ico create mode 100644 www/index.php create mode 100644 www/robots.txt diff --git a/.gitignore b/.gitignore index 22d0d82..7579f74 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ vendor +composer.lock diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..b66e808 --- /dev/null +++ b/.htaccess @@ -0,0 +1 @@ +Require all denied diff --git a/app/Bootstrap.php b/app/Bootstrap.php new file mode 100644 index 0000000..3aad930 --- /dev/null +++ b/app/Bootstrap.php @@ -0,0 +1,31 @@ +setDebugMode('secret@23.75.345.200'); // enable for your remote IP + $configurator->enableTracy($appDir . '/log'); + + $configurator->setTempDirectory($appDir . '/temp'); + + $configurator->createRobotLoader() + ->addDirectory(__DIR__) + ->register(); + + $configurator->addConfig($appDir . '/config/common.neon'); + $configurator->addConfig($appDir . '/config/services.neon'); + + return $configurator; + } +} diff --git a/app/Core/RouterFactory.php b/app/Core/RouterFactory.php new file mode 100644 index 0000000..3b1a285 --- /dev/null +++ b/app/Core/RouterFactory.php @@ -0,0 +1,21 @@ +addRoute('/[/]', 'Home:default'); + return $router; + } +} diff --git a/app/UI/@layout.latte b/app/UI/@layout.latte new file mode 100644 index 0000000..b8b55ae --- /dev/null +++ b/app/UI/@layout.latte @@ -0,0 +1,19 @@ + + + + + + + {ifset title}{include title|stripHtml} | {/ifset}Nette Web + + + +
{$flash->message}
+ + {include content} + + {block scripts} + + {/block} + + diff --git a/app/UI/Error/Error4xx/403.latte b/app/UI/Error/Error4xx/403.latte new file mode 100644 index 0000000..de00328 --- /dev/null +++ b/app/UI/Error/Error4xx/403.latte @@ -0,0 +1,7 @@ +{block content} +

Access Denied

+ +

You do not have permission to view this page. Please try contact the web +site administrator if you believe you should be able to view this page.

+ +

error 403

diff --git a/app/UI/Error/Error4xx/404.latte b/app/UI/Error/Error4xx/404.latte new file mode 100644 index 0000000..022001c --- /dev/null +++ b/app/UI/Error/Error4xx/404.latte @@ -0,0 +1,8 @@ +{block content} +

Page Not Found

+ +

The page you requested could not be found. It is possible that the address is +incorrect, or that the page no longer exists. Please use a search engine to find +what you are looking for.

+ +

error 404

diff --git a/app/UI/Error/Error4xx/410.latte b/app/UI/Error/Error4xx/410.latte new file mode 100644 index 0000000..99bde92 --- /dev/null +++ b/app/UI/Error/Error4xx/410.latte @@ -0,0 +1,6 @@ +{block content} +

Page Not Found

+ +

The page you requested has been taken off the site. We apologize for the inconvenience.

+ +

error 410

diff --git a/app/UI/Error/Error4xx/4xx.latte b/app/UI/Error/Error4xx/4xx.latte new file mode 100644 index 0000000..49e6127 --- /dev/null +++ b/app/UI/Error/Error4xx/4xx.latte @@ -0,0 +1,6 @@ +{block content} +

Oops...

+ +

Your browser sent a request that this server could not understand or process.

+ +

error {$httpCode}

diff --git a/app/UI/Error/Error4xx/Error4xxPresenter.php b/app/UI/Error/Error4xx/Error4xxPresenter.php new file mode 100644 index 0000000..f6ce621 --- /dev/null +++ b/app/UI/Error/Error4xx/Error4xxPresenter.php @@ -0,0 +1,27 @@ +getCode(); + $file = is_file($file = __DIR__ . "/$code.latte") + ? $file + : __DIR__ . '/4xx.latte'; + $this->template->httpCode = $code; + $this->template->setFile($file); + } +} diff --git a/app/UI/Error/Error5xx/500.phtml b/app/UI/Error/Error5xx/500.phtml new file mode 100644 index 0000000..a2b900c --- /dev/null +++ b/app/UI/Error/Error5xx/500.phtml @@ -0,0 +1,27 @@ + + + +Server Error + + + +
+
+

Server Error

+ +

We're sorry! The server encountered an internal error and + was unable to complete your request. Please try again later.

+ +

error 500

+
+
+ + diff --git a/app/UI/Error/Error5xx/503.phtml b/app/UI/Error/Error5xx/503.phtml new file mode 100644 index 0000000..f123919 --- /dev/null +++ b/app/UI/Error/Error5xx/503.phtml @@ -0,0 +1,24 @@ + + + + + + + + +Site is temporarily down for maintenance + +

We're Sorry

+ +

The site is temporarily down for maintenance. Please try again in a few minutes.

diff --git a/app/UI/Error/Error5xx/Error5xxPresenter.php b/app/UI/Error/Error5xx/Error5xxPresenter.php new file mode 100644 index 0000000..c12aca1 --- /dev/null +++ b/app/UI/Error/Error5xx/Error5xxPresenter.php @@ -0,0 +1,37 @@ +getParameter('exception'); + $this->logger->log($exception, ILogger::EXCEPTION); + + // Display a generic error message to the user + return new Responses\CallbackResponse(function (Http\IRequest $httpRequest, Http\IResponse $httpResponse): void { + if (preg_match('#^text/html(?:;|$)#', (string) $httpResponse->getHeader('Content-Type'))) { + require __DIR__ . '/500.phtml'; + } + }); + } +} diff --git a/app/UI/Home/HomePresenter.php b/app/UI/Home/HomePresenter.php new file mode 100644 index 0000000..0080666 --- /dev/null +++ b/app/UI/Home/HomePresenter.php @@ -0,0 +1,12 @@ + +

Congratulations!

+ + +
+

You have successfully created your Nette Web project.

+ +

+ If you are exploring Nette for the first time, you should read the + Quick Start, documentation, + blog and forum.

+ +

We hope you enjoy Nette!

+
+ + diff --git a/bin/.gitignore b/bin/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/bin/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..477c968 --- /dev/null +++ b/composer.json @@ -0,0 +1,38 @@ +{ + "name": "nette/web-project", + "description": "Nette: Standard Web Project", + "keywords": ["nette"], + "type": "project", + "license": ["MIT", "BSD-3-Clause", "GPL-2.0", "GPL-3.0"], + "require": { + "php": ">= 8.1", + "nette/application": "^3.2.3", + "nette/bootstrap": "^3.2", + "nette/caching": "^3.2", + "nette/database": "^3.2", + "nette/di": "^3.2", + "nette/forms": "^3.2", + "nette/http": "^3.3", + "nette/mail": "^4.0", + "nette/robot-loader": "^4.0", + "nette/security": "^3.2", + "nette/utils": "^4.0", + "latte/latte": "^3.0", + "tracy/tracy": "^2.10" + }, + "require-dev": { + "nette/tester": "^2.5", + "symfony/thanks": "^1" + }, + "autoload": { + "psr-4": { + "App\\": "app" + } + }, + "minimum-stability": "stable", + "config": { + "allow-plugins": { + "symfony/thanks": true + } + } +} diff --git a/config/common.neon b/config/common.neon new file mode 100644 index 0000000..4a3a141 --- /dev/null +++ b/config/common.neon @@ -0,0 +1,24 @@ +parameters: + + +application: + errorPresenter: + 4xx: Error:Error4xx + 5xx: Error:Error5xx + mapping: App\UI\*\**Presenter + + +database: + dsn: 'sqlite::memory:' + user: + password: + + +latte: + strictTypes: yes + + +di: + export: + parameters: no + tags: no diff --git a/config/services.neon b/config/services.neon new file mode 100644 index 0000000..03a7468 --- /dev/null +++ b/config/services.neon @@ -0,0 +1,9 @@ +services: + - App\Core\RouterFactory::createRouter + + +search: + - in: %appDir% + classes: + - *Factory + - *Facade diff --git a/log/.gitignore b/log/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/log/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..7a891dc --- /dev/null +++ b/readme.md @@ -0,0 +1,52 @@ +Nette Web Project +================= + +Welcome to the Nette Web Project! This is a basic skeleton application built using +[Nette](https://nette.org), ideal for kick-starting your new web projects. + +Nette is a renowned PHP web development framework, celebrated for its user-friendliness, +robust security, and outstanding performance. It's among the safest choices +for PHP frameworks out there. + +If Nette helps you, consider supporting it by [making a donation](https://nette.org/donate). +Thank you for your generosity! + + +Requirements +------------ + +This Web Project is compatible with Nette 3.2 and requires PHP 8.1. + + +Installation +------------ + +To install the Web Project, Composer is the recommended tool. If you're new to Composer, +follow [these instructions](https://doc.nette.org/composer). Then, run: + + composer create-project nette/web-project path/to/install + cd path/to/install + +Ensure the `temp/` and `log/` directories are writable. + + +Web Server Setup +---------------- + +To quickly dive in, use PHP's built-in server: + + php -S localhost:8000 -t www + +Then, open `http://localhost:8000` in your browser to view the welcome page. + +For Apache or Nginx users, configure a virtual host pointing to your project's `www/` directory. + +**Important Note:** Ensure `app/`, `config/`, `log/`, and `temp/` directories are not web-accessible. +Refer to [security warning](https://nette.org/security-warning) for more details. + + +Minimal Skeleton +---------------- + +For demonstrating issues or similar tasks, rather than starting a new project, use +this [minimal skeleton](https://github.com/nette/web-project/tree/minimal). diff --git a/temp/.gitignore b/temp/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/temp/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/www/.htaccess b/www/.htaccess new file mode 100644 index 0000000..fb50df4 --- /dev/null +++ b/www/.htaccess @@ -0,0 +1,41 @@ +# Apache configuration file (see https://httpd.apache.org/docs/current/mod/quickreference.html) + +# Allow access to all resources by default +Require all granted + +# Disable directory listing for security reasons + + Options -Indexes + + +# Enable pretty URLs (removing the need for "index.php" in the URL) + + RewriteEngine On + + # Uncomment the next line if you want to set the base URL for rewrites + # RewriteBase / + + # Force usage of HTTPS (secure connection). Uncomment if you have SSL setup. + # RewriteCond %{HTTPS} !on + # RewriteRule .? https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] + + # Permit requests to the '.well-known' directory (used for SSL verification and more) + RewriteRule ^\.well-known/.* - [L] + + # Block access to hidden files (starting with a dot) and URLs resembling WordPress admin paths + RewriteRule /\.|^\.|^wp- - [F] + + # Return 404 for missing files with specific extensions (images, scripts, styles, archives) + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule \.(pdf|js|mjs|ico|gif|jpg|jpeg|png|webp|avif|svg|css|rar|zip|7z|tar\.gz|map|eot|ttf|otf|woff|woff2)$ - [L] + + # Front controller pattern - all requests are routed through index.php + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule . index.php [L] + + +# Enable gzip compression for text files + + AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css application/javascript application/json application/xml application/rss+xml image/svg+xml + diff --git a/www/favicon.ico b/www/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b20cfd0f056c1c9d43100f612f4319c66e2adb94 GIT binary patch literal 2550 zcmcgsdpy=>6#qV`h?U#SZJVg3NgD48bB$?q*(P0BnsTYks)Y)n7Gs6xwl1`?sD@_U z*mR*3ue;!TGX-%KzT!aoOgA9#8p?H)WL?RL7J%`}#?vCTxIT)jQ5SY=6*49?+ zh&_ole$lYAvxC8Q5iS=MLnsttXlMv`?%ctjEsbHE`WM;pN*(R2gPpQVcsz^BU^|{g zdOB}*utia3=3%>|9bp}tc@~XfN=iy^4Uf>sN#&@~UN&lzy{)*3spzf3^BSR||YZz~*6I;1wF3`8Euwo$so^QQ+fPxAeUii#5Rap}?} zI637bDXAWlCQX8umlyQ(DzI){DFgxmGBPq~J0F>ud>9)Wx9 zfgL;esHkY7xZ6lhPR71{g~-j##nPo0;O%`5H*eOUySp2Q4mF^^zn|iS2n`K|hDI$; zovJ}&<4uS}_n@M34_mgBKvq^3>gwt+H#bLFSs9iuUk*P%KTMxqN_*>}udk1soE*f( z#nC;_e;ul; zt8w}?A7jS!QZIBOE2|m`3JTEH))x21tXZ>QWo3op;$o_0Df094;o;#yaal-9OC!E& zp{go?j7&KuP88z$^;>9YXrMdSVdKV9NJ~poO)8i?c`_KQLVJ5Vi*e)nQB_rs;o(29XHPk%O)EoSARo=m&6HP2wf#l4 zH4*=<7(aeI#SKGEO$`GB1JKmeL|tgHl| z&&T4$i{a|(iW4VJVAZNs@bU41udgpcLP8K89*&rp7&_}APxI`(?@mKQ1HHYy(9+Vvg9i`L z)zw8kr-!?D??PU_7j12A;_p^j`9AIG#>|;BVPaweGcz;P)YQPz(vteE0@l{nm@{V% zN=iy#Z*Nb1a0PSc&P8EiA)K9^k(ZYT7Z(>SS+WFfZf?lV&c@1>E8*$siS+bz>eGGj z_xGpXNy6^kyRmiaR>a1}A}lNn(b3U}jEsboln`Ub3Nc{<$N329V+>PWBGDrZP)!2^ zgH$uuXI=CzePr8-qdwAy^oBz6>qtwft{!^Z9;%1y^ZT^+>g7P{OXUFj7df)}wRucq zmlm@v@hbOa8Ko5wENykg4mNL*D*qf~3xUi}fK4-w!qLe|`Yf=;$Q%fcQ2~x_S{3dI z1exGnPg=`oSp@5O5E7$6zbZYJ`mKgNaD~lBlrvWRaz2EePA_9@`j2`n7s~VV^4Wnj z`nAb@jO`Kl`fYGqwwdB8k@2B3#Tt)Nf%6LZidBEm%>U zIp^f3xWHJNw^!P)mp({kCuGbESuBup*#~gvu}K%ttO=2*{sN2{JA-W!z>@{nZxqL{ zh=QL_y(oY&ani$2@YEN_aY*gFm*aH@{||iK?3yQO>z|YV>W4Sqo9I6p5G09PXfCPx z;&Zb&aX?Vev-kV*{k_d?**5~-RN@c`{2gue-TH}%`ch3}=ROC&rnaYAkVqm}+BD5V za{pA9-^FlZ;_>*&Z%E=g`&+o(oUB|tP7=3%{UlJbB85M$22KZ{7Oda^FYEk~m9tOZv=@F{=&ov#vg;K8};8MS`EFL7E3`erDkr Rce72createContainer(); +$application = $container->getByType(Nette\Application\Application::class); +$application->run(); diff --git a/www/robots.txt b/www/robots.txt new file mode 100644 index 0000000..e69de29 From 46f9c928d83456f9d102229e7fcef5ac0d9172ed Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 9 Oct 2021 11:44:37 +0200 Subject: [PATCH 2/4] implemented demo --- app/Bootstrap.php | 18 +++- app/Core/RouterFactory.php | 6 +- app/Model/UserFacade.php | 95 +++++++++++++++++ app/UI/@layout.latte | 8 +- app/UI/Accessory/FormFactory.php | 34 ++++++ app/UI/Accessory/RequireLoggedUser.php | 33 ++++++ app/UI/Dashboard/DashboardPresenter.php | 19 ++++ app/UI/Dashboard/default.latte | 6 ++ app/UI/Error/Error4xx/403.latte | 7 -- app/UI/Error/Error4xx/404.latte | 8 -- app/UI/Error/Error4xx/410.latte | 6 -- app/UI/Error/Error4xx/4xx.latte | 6 -- app/UI/Error/Error4xx/Error4xxPresenter.php | 27 ----- app/UI/Error/Error5xx/500.phtml | 27 ----- app/UI/Error/Error5xx/503.phtml | 24 ----- app/UI/Error/Error5xx/Error5xxPresenter.php | 37 ------- app/UI/Home/HomePresenter.php | 12 --- app/UI/Home/default.latte | 38 ------- app/UI/Sign/SignPresenter.php | 109 ++++++++++++++++++++ app/UI/Sign/in.latte | 8 ++ app/UI/Sign/out.latte | 6 ++ app/UI/Sign/up.latte | 8 ++ bin/.gitignore | 2 - bin/create-user.php | 31 ++++++ composer.json | 28 ++--- config/common.neon | 19 +--- config/services.neon | 2 + data/db.sqlite | Bin 0 -> 4096 bytes data/mysql.sql | 9 ++ readme.md | 69 ++++++------- www/index.php | 10 +- 31 files changed, 444 insertions(+), 268 deletions(-) create mode 100644 app/Model/UserFacade.php create mode 100644 app/UI/Accessory/FormFactory.php create mode 100644 app/UI/Accessory/RequireLoggedUser.php create mode 100644 app/UI/Dashboard/DashboardPresenter.php create mode 100644 app/UI/Dashboard/default.latte delete mode 100644 app/UI/Error/Error4xx/403.latte delete mode 100644 app/UI/Error/Error4xx/404.latte delete mode 100644 app/UI/Error/Error4xx/410.latte delete mode 100644 app/UI/Error/Error4xx/4xx.latte delete mode 100644 app/UI/Error/Error4xx/Error4xxPresenter.php delete mode 100644 app/UI/Error/Error5xx/500.phtml delete mode 100644 app/UI/Error/Error5xx/503.phtml delete mode 100644 app/UI/Error/Error5xx/Error5xxPresenter.php delete mode 100644 app/UI/Home/HomePresenter.php delete mode 100644 app/UI/Home/default.latte create mode 100644 app/UI/Sign/SignPresenter.php create mode 100644 app/UI/Sign/in.latte create mode 100644 app/UI/Sign/out.latte create mode 100644 app/UI/Sign/up.latte delete mode 100644 bin/.gitignore create mode 100644 bin/create-user.php create mode 100644 data/db.sqlite create mode 100644 data/mysql.sql diff --git a/app/Bootstrap.php b/app/Bootstrap.php index 3aad930..7a39742 100644 --- a/app/Bootstrap.php +++ b/app/Bootstrap.php @@ -7,22 +7,30 @@ use Nette\Bootstrap\Configurator; +/** + * Bootstrap class initializes application environment and DI container. + */ class Bootstrap { public static function boot(): Configurator { + // The configurator is responsible for setting up the application environment and services. + // Learn more at https://doc.nette.org/en/bootstrap $configurator = new Configurator; $appDir = dirname(__DIR__); - //$configurator->setDebugMode('secret@23.75.345.200'); // enable for your remote IP + // Nette is smart, and the development mode turns on automatically, + // or you can enable for a specific IP address it by uncommenting the following line: + // $configurator->setDebugMode('secret@23.75.345.200'); + + // Enables Tracy: the ultimate "swiss army knife" debugging tool. + // Learn more about Tracy at https://tracy.nette.org $configurator->enableTracy($appDir . '/log'); + // Set the directory for temporary files generated by Nette (e.g. compiled templates) $configurator->setTempDirectory($appDir . '/temp'); - $configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - + // Add configuration files $configurator->addConfig($appDir . '/config/common.neon'); $configurator->addConfig($appDir . '/config/services.neon'); diff --git a/app/Core/RouterFactory.php b/app/Core/RouterFactory.php index 3b1a285..369c55e 100644 --- a/app/Core/RouterFactory.php +++ b/app/Core/RouterFactory.php @@ -12,10 +12,14 @@ final class RouterFactory { use Nette\StaticClass; + /** + * Creates the main application router with defined routes. + */ public static function createRouter(): RouteList { $router = new RouteList; - $router->addRoute('/[/]', 'Home:default'); + // Default route that maps to the Dashboard + $router->addRoute('/', 'Dashboard:default'); return $router; } } diff --git a/app/Model/UserFacade.php b/app/Model/UserFacade.php new file mode 100644 index 0000000..9ac2dea --- /dev/null +++ b/app/Model/UserFacade.php @@ -0,0 +1,95 @@ +database->table(self::TableName) + ->where(self::ColumnName, $username) + ->fetch(); + + // Authentication checks + if (!$row) { + throw new Nette\Security\AuthenticationException('The username is incorrect.', self::IdentityNotFound); + + } elseif (!$this->passwords->verify($password, $row[self::ColumnPasswordHash])) { + throw new Nette\Security\AuthenticationException('The password is incorrect.', self::InvalidCredential); + + } elseif ($this->passwords->needsRehash($row[self::ColumnPasswordHash])) { + $row->update([ + self::ColumnPasswordHash => $this->passwords->hash($password), + ]); + } + + // Return user identity without the password hash + $arr = $row->toArray(); + unset($arr[self::ColumnPasswordHash]); + return new Nette\Security\SimpleIdentity($row[self::ColumnId], $row[self::ColumnRole], $arr); + } + + + /** + * Add a new user to the database. + * Throws a DuplicateNameException if the username is already taken. + */ + public function add(string $username, string $email, string $password): void + { + // Validate the email format + Nette\Utils\Validators::assert($email, 'email'); + + // Attempt to insert the new user into the database + try { + $this->database->table(self::TableName)->insert([ + self::ColumnName => $username, + self::ColumnPasswordHash => $this->passwords->hash($password), + self::ColumnEmail => $email, + ]); + } catch (Nette\Database\UniqueConstraintViolationException $e) { + throw new DuplicateNameException; + } + } +} + + +/** + * Custom exception for duplicate usernames. + */ +class DuplicateNameException extends \Exception +{ +} diff --git a/app/UI/@layout.latte b/app/UI/@layout.latte index b8b55ae..7c3f978 100644 --- a/app/UI/@layout.latte +++ b/app/UI/@layout.latte @@ -4,14 +4,18 @@ - {ifset title}{include title|stripHtml} | {/ifset}Nette Web + {* Page title with optional prefix from the child template *} + {ifset title}{include title|stripHtml} | {/ifset}User Login Example -
{$flash->message}
+ {* Flash messages display block *} +
{$flash->message}
+ {* Main content of the child template goes here *} {include content} + {* Scripts block; by default includes Nette Forms script for validation *} {block scripts} {/block} diff --git a/app/UI/Accessory/FormFactory.php b/app/UI/Accessory/FormFactory.php new file mode 100644 index 0000000..2e36176 --- /dev/null +++ b/app/UI/Accessory/FormFactory.php @@ -0,0 +1,34 @@ +user->isLoggedIn()) { + $form->addProtection(); + } + return $form; + } +} diff --git a/app/UI/Accessory/RequireLoggedUser.php b/app/UI/Accessory/RequireLoggedUser.php new file mode 100644 index 0000000..b8717fe --- /dev/null +++ b/app/UI/Accessory/RequireLoggedUser.php @@ -0,0 +1,33 @@ +onStartup[] = function () { + $user = $this->getUser(); + // If the user isn't logged in, redirect them to the sign-in page + if ($user->isLoggedIn()) { + return; + } elseif ($user->getLogoutReason() === $user::LogoutInactivity) { + $this->flashMessage('You have been signed out due to inactivity. Please sign in again.'); + $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); + } else { + $this->redirect('Sign:in'); + } + }; + } +} diff --git a/app/UI/Dashboard/DashboardPresenter.php b/app/UI/Dashboard/DashboardPresenter.php new file mode 100644 index 0000000..0b48715 --- /dev/null +++ b/app/UI/Dashboard/DashboardPresenter.php @@ -0,0 +1,19 @@ +Dashboard + +

If you see this page, it means you have successfully logged in.

+ +

(Sign out)

diff --git a/app/UI/Error/Error4xx/403.latte b/app/UI/Error/Error4xx/403.latte deleted file mode 100644 index de00328..0000000 --- a/app/UI/Error/Error4xx/403.latte +++ /dev/null @@ -1,7 +0,0 @@ -{block content} -

Access Denied

- -

You do not have permission to view this page. Please try contact the web -site administrator if you believe you should be able to view this page.

- -

error 403

diff --git a/app/UI/Error/Error4xx/404.latte b/app/UI/Error/Error4xx/404.latte deleted file mode 100644 index 022001c..0000000 --- a/app/UI/Error/Error4xx/404.latte +++ /dev/null @@ -1,8 +0,0 @@ -{block content} -

Page Not Found

- -

The page you requested could not be found. It is possible that the address is -incorrect, or that the page no longer exists. Please use a search engine to find -what you are looking for.

- -

error 404

diff --git a/app/UI/Error/Error4xx/410.latte b/app/UI/Error/Error4xx/410.latte deleted file mode 100644 index 99bde92..0000000 --- a/app/UI/Error/Error4xx/410.latte +++ /dev/null @@ -1,6 +0,0 @@ -{block content} -

Page Not Found

- -

The page you requested has been taken off the site. We apologize for the inconvenience.

- -

error 410

diff --git a/app/UI/Error/Error4xx/4xx.latte b/app/UI/Error/Error4xx/4xx.latte deleted file mode 100644 index 49e6127..0000000 --- a/app/UI/Error/Error4xx/4xx.latte +++ /dev/null @@ -1,6 +0,0 @@ -{block content} -

Oops...

- -

Your browser sent a request that this server could not understand or process.

- -

error {$httpCode}

diff --git a/app/UI/Error/Error4xx/Error4xxPresenter.php b/app/UI/Error/Error4xx/Error4xxPresenter.php deleted file mode 100644 index f6ce621..0000000 --- a/app/UI/Error/Error4xx/Error4xxPresenter.php +++ /dev/null @@ -1,27 +0,0 @@ -getCode(); - $file = is_file($file = __DIR__ . "/$code.latte") - ? $file - : __DIR__ . '/4xx.latte'; - $this->template->httpCode = $code; - $this->template->setFile($file); - } -} diff --git a/app/UI/Error/Error5xx/500.phtml b/app/UI/Error/Error5xx/500.phtml deleted file mode 100644 index a2b900c..0000000 --- a/app/UI/Error/Error5xx/500.phtml +++ /dev/null @@ -1,27 +0,0 @@ - - - -Server Error - - - -
-
-

Server Error

- -

We're sorry! The server encountered an internal error and - was unable to complete your request. Please try again later.

- -

error 500

-
-
- - diff --git a/app/UI/Error/Error5xx/503.phtml b/app/UI/Error/Error5xx/503.phtml deleted file mode 100644 index f123919..0000000 --- a/app/UI/Error/Error5xx/503.phtml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - -Site is temporarily down for maintenance - -

We're Sorry

- -

The site is temporarily down for maintenance. Please try again in a few minutes.

diff --git a/app/UI/Error/Error5xx/Error5xxPresenter.php b/app/UI/Error/Error5xx/Error5xxPresenter.php deleted file mode 100644 index c12aca1..0000000 --- a/app/UI/Error/Error5xx/Error5xxPresenter.php +++ /dev/null @@ -1,37 +0,0 @@ -getParameter('exception'); - $this->logger->log($exception, ILogger::EXCEPTION); - - // Display a generic error message to the user - return new Responses\CallbackResponse(function (Http\IRequest $httpRequest, Http\IResponse $httpResponse): void { - if (preg_match('#^text/html(?:;|$)#', (string) $httpResponse->getHeader('Content-Type'))) { - require __DIR__ . '/500.phtml'; - } - }); - } -} diff --git a/app/UI/Home/HomePresenter.php b/app/UI/Home/HomePresenter.php deleted file mode 100644 index 0080666..0000000 --- a/app/UI/Home/HomePresenter.php +++ /dev/null @@ -1,12 +0,0 @@ - -

Congratulations!

- - -
-

You have successfully created your Nette Web project.

- -

- If you are exploring Nette for the first time, you should read the - Quick Start, documentation, - blog and forum.

- -

We hope you enjoy Nette!

-
- - diff --git a/app/UI/Sign/SignPresenter.php b/app/UI/Sign/SignPresenter.php new file mode 100644 index 0000000..320ac1b --- /dev/null +++ b/app/UI/Sign/SignPresenter.php @@ -0,0 +1,109 @@ +formFactory->create(); + $form->addText('username', 'Username:') + ->setRequired('Please enter your username.'); + + $form->addPassword('password', 'Password:') + ->setRequired('Please enter your password.'); + + $form->addSubmit('send', 'Sign in'); + + // Handle form submission + $form->onSuccess[] = function (Form $form, \stdClass $data): void { + try { + // Attempt to login user + $this->getUser()->login($data->username, $data->password); + $this->restoreRequest($this->backlink); + $this->redirect('Dashboard:'); + } catch (Nette\Security\AuthenticationException) { + $form->addError('The username or password you entered is incorrect.'); + } + }; + + return $form; + } + + + /** + * Create a sign-up form with fields for username, email, and password. + * On successful submission, the user is redirected to the dashboard. + */ + protected function createComponentSignUpForm(): Form + { + $form = $this->formFactory->create(); + $form->addText('username', 'Pick a username:') + ->setRequired('Please pick a username.'); + + $form->addEmail('email', 'Your e-mail:') + ->setRequired('Please enter your e-mail.'); + + $form->addPassword('password', 'Create a password:') + ->setOption('description', sprintf('at least %d characters', $this->userFacade::PasswordMinLength)) + ->setRequired('Please create a password.') + ->addRule($form::MinLength, null, $this->userFacade::PasswordMinLength); + + $form->addSubmit('send', 'Sign up'); + + // Handle form submission + $form->onSuccess[] = function (Form $form, \stdClass $data): void { + try { + // Attempt to register a new user + $this->userFacade->add($data->username, $data->email, $data->password); + $this->redirect('Dashboard:'); + } catch (DuplicateNameException) { + // Handle the case where the username is already taken + $form['username']->addError('Username is already taken.'); + } + }; + + return $form; + } + + + /** + * Logs out the currently authenticated user. + */ + public function actionOut(): void + { + $this->getUser()->logout(); + } +} diff --git a/app/UI/Sign/in.latte b/app/UI/Sign/in.latte new file mode 100644 index 0000000..cc6efc3 --- /dev/null +++ b/app/UI/Sign/in.latte @@ -0,0 +1,8 @@ +{* The sign-in page *} + +{block content} +

Sign In

+ +{control signInForm} + +

Don't have an account yet? Sign up.

diff --git a/app/UI/Sign/out.latte b/app/UI/Sign/out.latte new file mode 100644 index 0000000..3607ad3 --- /dev/null +++ b/app/UI/Sign/out.latte @@ -0,0 +1,6 @@ +{* The sign-out page *} + +{block content} +

You have been signed out

+ +

Sign in to another account

diff --git a/app/UI/Sign/up.latte b/app/UI/Sign/up.latte new file mode 100644 index 0000000..9b2a495 --- /dev/null +++ b/app/UI/Sign/up.latte @@ -0,0 +1,8 @@ +{* The sign-up page *} + +{block content} +

Sign Up

+ +{control signUpForm} + +

Already have an account? Log in.

diff --git a/bin/.gitignore b/bin/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/bin/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/bin/create-user.php b/bin/create-user.php new file mode 100644 index 0000000..0772cd5 --- /dev/null +++ b/bin/create-user.php @@ -0,0 +1,31 @@ +createContainer(); + +if (!isset($argv[3])) { + echo ' +Add new user to database. + +Usage: create-user.php +'; + exit(1); +} + +[, $name, $email, $password] = $argv; + +$manager = $container->getByType(App\Model\UserFacade::class); + +try { + $manager->add($name, $email, $password); + echo "User $name was added.\n"; + +} catch (App\Model\DuplicateNameException $e) { + echo "Error: duplicate name.\n"; + exit(1); +} diff --git a/composer.json b/composer.json index 477c968..1ef4482 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,17 @@ { - "name": "nette/web-project", - "description": "Nette: Standard Web Project", - "keywords": ["nette"], + "name": "nette-examples/user-authentication", "type": "project", - "license": ["MIT", "BSD-3-Clause", "GPL-2.0", "GPL-3.0"], + "license": "BSD-3-Clause", + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], "require": { "php": ">= 8.1", "nette/application": "^3.2.3", @@ -13,26 +21,18 @@ "nette/di": "^3.2", "nette/forms": "^3.2", "nette/http": "^3.3", - "nette/mail": "^4.0", - "nette/robot-loader": "^4.0", "nette/security": "^3.2", "nette/utils": "^4.0", "latte/latte": "^3.0", "tracy/tracy": "^2.10" }, "require-dev": { - "nette/tester": "^2.5", - "symfony/thanks": "^1" + "nette/tester": "^2.5" }, "autoload": { "psr-4": { "App\\": "app" } }, - "minimum-stability": "stable", - "config": { - "allow-plugins": { - "symfony/thanks": true - } - } + "minimum-stability": "stable" } diff --git a/config/common.neon b/config/common.neon index 4a3a141..df776c4 100644 --- a/config/common.neon +++ b/config/common.neon @@ -1,24 +1,15 @@ +# Application parameters and settings. See https://doc.nette.org/configuring + parameters: application: - errorPresenter: - 4xx: Error:Error4xx - 5xx: Error:Error5xx + # Presenter mapping pattern mapping: App\UI\*\**Presenter database: - dsn: 'sqlite::memory:' + # SQLite database source location + dsn: 'sqlite:%rootDir%/data/db.sqlite' user: password: - - -latte: - strictTypes: yes - - -di: - export: - parameters: no - tags: no diff --git a/config/services.neon b/config/services.neon index 03a7468..d608f5c 100644 --- a/config/services.neon +++ b/config/services.neon @@ -1,3 +1,5 @@ +# Service registrations. See https://doc.nette.org/dependency-injection/services + services: - App\Core\RouterFactory::createRouter diff --git a/data/db.sqlite b/data/db.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..ebbe939053ec39ca53849156d8543c2e9a014996 GIT binary patch literal 4096 zcmeH}PfNov6u^_Vx-l4thvoX{Wh)zAcJOM|3dOGbV+#(1QMV|?wsUQVyG}o)$9)OU zreU4i6i*6cB(!~feR(f=zn9S5bS*!n@Gy=;FNG#j5XR^X03lRix5Dnc9kG{n9p{E$ zWCfiJQCV&xS$ad#LOx|ev>hVAO0yu%+Ct2iDp%3HNdnljoanOtBxwP?XTJJxS7AIb)-X5uj4*X zE13jrwGR?HnbK%Tw^Vr}wJl>U3)f(t8mpc%zmf}tkQ^7ECV%`r9-aoIbe685a&*sL zlDv###{9dDhMph%WQfNB&A+qil(bT|7T0(64y{cw6+u7{_+J8L79PtVh+U!y0)oK4 e6F4ri<6IHTq{z=J+5Q**|9vmI$WIXXnZPGw#jE%L literal 0 HcmV?d00001 diff --git a/data/mysql.sql b/data/mysql.sql new file mode 100644 index 0000000..2d49ff9 --- /dev/null +++ b/data/mysql.sql @@ -0,0 +1,9 @@ +CREATE TABLE `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `username` varchar(100) NOT NULL, + `password` varchar(100) NOT NULL, + `email` varchar(100) NOT NULL, + `role` varchar(100), + PRIMARY KEY (`id`), + UNIQUE KEY `username` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/readme.md b/readme.md index 7a891dc..8292db1 100644 --- a/readme.md +++ b/readme.md @@ -1,52 +1,47 @@ -Nette Web Project -================= +User Authentication (Nette example) +=================================== -Welcome to the Nette Web Project! This is a basic skeleton application built using -[Nette](https://nette.org), ideal for kick-starting your new web projects. +Example of user management. -Nette is a renowned PHP web development framework, celebrated for its user-friendliness, -robust security, and outstanding performance. It's among the safest choices -for PHP frameworks out there. - -If Nette helps you, consider supporting it by [making a donation](https://nette.org/donate). -Thank you for your generosity! - - -Requirements ------------- - -This Web Project is compatible with Nette 3.2 and requires PHP 8.1. +- User login, registration and logout (`SignPresenter`) +- Command line registration (`bin/create-user.php`) +- Authentication using database table (`UserFacade`) +- Password hashing +- Presenter requiring authentication (`DashboardPresenter`) using the `RequireLoggedUser` trait +- Rendering forms using Bootstrap CSS framework +- Automatic CSRF protection using a token when the user is logged in (`FormFactory`) +- Separation of form factories into independent classes (`SignInFormFactory`, `SignUpFormFactory`) +- Return to previous page after login (`SignPresenter::$backlink`) Installation ------------ -To install the Web Project, Composer is the recommended tool. If you're new to Composer, -follow [these instructions](https://doc.nette.org/composer). Then, run: - - composer create-project nette/web-project path/to/install - cd path/to/install - -Ensure the `temp/` and `log/` directories are writable. - - -Web Server Setup ----------------- +```shell +git clone https://github.com/nette-examples/user-authentication +cd user-authentication +composer install +``` -To quickly dive in, use PHP's built-in server: +Make directories `data/`, `temp/` and `log/` writable. - php -S localhost:8000 -t www +By default, SQLite is used as the database which is located in the `data/db.sqlite` file. If you would like to switch to a different database, configure access in the `config/local.neon` file: -Then, open `http://localhost:8000` in your browser to view the welcome page. +```neon +database: + dsn: 'mysql:host=127.0.0.1;dbname=***' + user: *** + password: *** +``` -For Apache or Nginx users, configure a virtual host pointing to your project's `www/` directory. +And then create the `users` table using SQL statements in the [data/mysql.sql](data/mysql.sql) file. -**Important Note:** Ensure `app/`, `config/`, `log/`, and `temp/` directories are not web-accessible. -Refer to [security warning](https://nette.org/security-warning) for more details. +The simplest way to get started is to start the built-in PHP server in the root directory of your project: +```shell +php -S localhost:8000 www/index.php +``` -Minimal Skeleton ----------------- +Then visit `http://localhost:8000` in your browser to see the welcome page. -For demonstrating issues or similar tasks, rather than starting a new project, use -this [minimal skeleton](https://github.com/nette/web-project/tree/minimal). +It requires PHP version 8.1 or newer. diff --git a/www/index.php b/www/index.php index 466a988..55c17b1 100644 --- a/www/index.php +++ b/www/index.php @@ -2,9 +2,17 @@ declare(strict_types=1); -require __DIR__ . '/../vendor/autoload.php'; +// Load the Composer autoloader +if (@!include __DIR__ . '/../vendor/autoload.php') { + die('Install Nette using `composer update`'); +} +// Initialize the application environment $configurator = App\Bootstrap::boot(); + +// Create the Dependency Injection container $container = $configurator->createContainer(); + +// Start the application and handle the incoming request $application = $container->getByType(Nette\Application\Application::class); $application->run(); From 31bb1cbe02295df17db7c162595530e0a8541698 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 17 Feb 2024 22:56:19 +0100 Subject: [PATCH 3/4] Bootstrap CSS --- app/UI/@layout.latte | 15 ++++++--- app/UI/Sign/in.latte | 2 +- app/UI/Sign/up.latte | 2 +- app/UI/form-bootstrap5.latte | 61 ++++++++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 app/UI/form-bootstrap5.latte diff --git a/app/UI/@layout.latte b/app/UI/@layout.latte index 7c3f978..8d9e02e 100644 --- a/app/UI/@layout.latte +++ b/app/UI/@layout.latte @@ -1,3 +1,5 @@ +{import 'form-bootstrap5.latte'} + @@ -6,14 +8,19 @@ {* Page title with optional prefix from the child template *} {ifset title}{include title|stripHtml} | {/ifset}User Login Example + + {* Link to the Bootstrap stylesheet for styling *} + - {* Flash messages display block *} -
{$flash->message}
+
+ {* Flash messages display block *} +
{$flash->message}
- {* Main content of the child template goes here *} - {include content} + {* Main content of the child template goes here *} + {include content} +
{* Scripts block; by default includes Nette Forms script for validation *} {block scripts} diff --git a/app/UI/Sign/in.latte b/app/UI/Sign/in.latte index cc6efc3..f6f99d0 100644 --- a/app/UI/Sign/in.latte +++ b/app/UI/Sign/in.latte @@ -3,6 +3,6 @@ {block content}

Sign In

-{control signInForm} +{include bootstrap-form signInForm}

Don't have an account yet? Sign up.

diff --git a/app/UI/Sign/up.latte b/app/UI/Sign/up.latte index 9b2a495..db6e7e4 100644 --- a/app/UI/Sign/up.latte +++ b/app/UI/Sign/up.latte @@ -3,6 +3,6 @@ {block content}

Sign Up

-{control signUpForm} +{include bootstrap-form signUpForm}

Already have an account? Log in.

diff --git a/app/UI/form-bootstrap5.latte b/app/UI/form-bootstrap5.latte new file mode 100644 index 0000000..f77c8cf --- /dev/null +++ b/app/UI/form-bootstrap5.latte @@ -0,0 +1,61 @@ +{* Generic form template for Bootstrap v5 *} + +{define bootstrap-form, $name} +
+ {* List for form-level error messages *} +
    +
  • {$error}
  • +
+ + {include controls $form->getControls()} +
+{/define} + + +{define local controls, array $controls} + {* Loop over form controls and render each one *} +
+ + {* Label for the control *} +
{label $control /}
+ +
+ {include control $control} + {if $control->getOption(type) === button} + {while $iterator->nextValue?->getOption(type) === button} + {input $iterator->nextValue class => "btn btn-secondary"} + {do $iterator->next()} + {/while} + {/if} + + {* Display control-level errors or descriptions, if present *} + {$control->error} + {$control->getOption(description)} +
+
+{/define} + + +{define local control, Nette\Forms\Controls\BaseControl $control} + {* Conditionally render controls based on their type with appropriate Bootstrap classes *} + {if $control->getOption(type) in [text, select, textarea, datetime, file]} + {input $control class => form-control} + + {elseif $control->getOption(type) === button} + {input $control class => "btn btn-primary"} + + {elseif $control->getOption(type) in [checkbox, radio]} + {var $items = $control instanceof Nette\Forms\Controls\Checkbox ? [''] : $control->getItems()} +
+ {input $control:$key class => form-check-input}{label $control:$key class => form-check-label /} +
+ + {elseif $control->getOption(type) === color} + {input $control class => "form-control form-control-color"} + + {else} + {input $control} + {/if} +{/define} From 60c0cc2e4f02eabeb9ed1bf892e547a20e0319e8 Mon Sep 17 00:00:00 2001 From: JWB Date: Sat, 12 Oct 2024 18:59:53 -0400 Subject: [PATCH 4/4] Select dropdowns use form-select class and not form-control As per the docs: [Bootstrap 5 docs](https://getbootstrap.com/docs/5.3/forms/select/) --- app/UI/form-bootstrap5.latte | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/UI/form-bootstrap5.latte b/app/UI/form-bootstrap5.latte index f77c8cf..d68125f 100644 --- a/app/UI/form-bootstrap5.latte +++ b/app/UI/form-bootstrap5.latte @@ -40,9 +40,12 @@ {define local control, Nette\Forms\Controls\BaseControl $control} {* Conditionally render controls based on their type with appropriate Bootstrap classes *} - {if $control->getOption(type) in [text, select, textarea, datetime, file]} + {if $control->getOption(type) in [text, textarea, datetime, file]} {input $control class => form-control} + {elseif $control->getOption(type) === select} + {input $control class => form-select} + {elseif $control->getOption(type) === button} {input $control class => "btn btn-primary"}