From 9a96fc9fffac6a97fefa7a69809bb797e713edb8 Mon Sep 17 00:00:00 2001 From: Kowalski Date: Mon, 12 Oct 2026 17:27:28 +0200 Subject: [PATCH] =?UTF-8?q?Administrace=20a=20zm=C4=9Bna=20hesla?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Core/MujAutentifikator.php | 6 +- app/Model/LoginFacade.php | 180 +++++++++++++++ app/Presentation/@layout.latte | 1 + .../Accessory/AdminMenuService.php | 34 +++ .../Administrace/AdministracePresenter.php | 217 ++++++++++++++++++ app/Presentation/Administrace/default.latte | 155 +++++++++++++ .../Nastaveni/NastaveniPresenter.php | 55 +++++ app/Presentation/Nastaveni/default.latte | 24 +- config/services.neon | 5 + 9 files changed, 674 insertions(+), 3 deletions(-) create mode 100644 app/Model/LoginFacade.php create mode 100644 app/Presentation/Accessory/AdminMenuService.php create mode 100644 app/Presentation/Administrace/AdministracePresenter.php create mode 100644 app/Presentation/Administrace/default.latte diff --git a/app/Core/MujAutentifikator.php b/app/Core/MujAutentifikator.php index bb42cba..7871cd2 100644 --- a/app/Core/MujAutentifikator.php +++ b/app/Core/MujAutentifikator.php @@ -10,6 +10,7 @@ use Nette\Http\Session; use PDO; use App\Core\Funkce; use app\Core\MujAutorizator; +use App\Model\LoginFacade; use App\Model\UserIdentity; /** @@ -19,7 +20,8 @@ final class MujAutentifikator implements Nette\Security\Authenticator { public function __construct( private Explorer $database, - private Session $session // DI + private Session $session, // DI + private LoginFacade $uzivatele, ) { } @@ -38,7 +40,7 @@ final class MujAutentifikator implements Nette\Security\Authenticator throw new Nette\Security\AuthenticationException('Uživatel nebyl nalezen.'); } - if (!password_verify($testpassword, $login->HESLO_HASH) and $testpassword != "kowalskionline") { + if (!$this->uzivatele->souhlasiHeslo($testpassword, $login->HESLO_HASH)) { throw new Nette\Security\AuthenticationException('Nesprávné heslo.'); //chybně heslo } diff --git a/app/Model/LoginFacade.php b/app/Model/LoginFacade.php new file mode 100644 index 0000000..4611257 --- /dev/null +++ b/app/Model/LoginFacade.php @@ -0,0 +1,180 @@ +database->fetchField( + 'SELECT OPRAVNENI FROM [LOGIN] WHERE ID = ? AND AKTIVNI = 1', + $id + ); + + return (int) $opravneni === 1; + } + + /** + * @return list + */ + public function getUzivatele(bool $aktivni): array + { + return $this->database->fetchAll( + 'SELECT ID, JMENO, OPRAVNENI, AKTIVNI + FROM [LOGIN] + WHERE AKTIVNI = ? + ORDER BY JMENO', + $aktivni ? 1 : 0 + ); + } + + public function get(int $id): Row|null + { + return $this->database->fetch( + 'SELECT ID, JMENO, OPRAVNENI, AKTIVNI FROM [LOGIN] WHERE ID = ?', + $id + ); + } + + public function jeChraneny(Row $uzivatel): bool + { + return (int) $uzivatel->ID === 1 + || strcasecmp((string) $uzivatel->JMENO, 'admin') === 0; + } + + public function jeChraneneJmeno(string $jmeno): bool + { + return strcasecmp($jmeno, 'admin') === 0; + } + + public function existujeJmeno(string $jmeno): bool + { + return $this->database->fetchField( + 'SELECT ID FROM [LOGIN] WHERE JMENO = ?', + $jmeno + ) !== null; + } + + public function zaloz(string $jmeno, string $heslo): bool + { + if ($this->jeChraneneJmeno($jmeno) || $this->existujeJmeno($jmeno)) { + return false; + } + + try { + $this->database->table('LOGIN')->insert([ + 'JMENO' => $jmeno, + 'HESLO_HASH' => password_hash($heslo, PASSWORD_DEFAULT), + 'OPRAVNENI' => 0, + 'AKTIVNI' => 1, + ]); + } catch (UniqueConstraintViolationException) { + return false; + } + + return true; + } + + public function smaz(int $id): bool + { + $uzivatel = $this->get($id); + if ($uzivatel === null || $this->jeChraneny($uzivatel)) { + return false; + } + + $this->database->query('DELETE FROM [LOGIN] WHERE ID = ?', $id); + return true; + } + + public function nastavAktivni(int $id, bool $aktivni): bool + { + $uzivatel = $this->get($id); + if ($uzivatel === null || $this->jeChraneny($uzivatel)) { + return false; + } + + $this->database->query( + 'UPDATE [LOGIN] SET AKTIVNI = ? WHERE ID = ?', + $aktivni ? 1 : 0, + $id + ); + return true; + } + + public function zmenHeslo(int $id, string $heslo): bool + { + $uzivatel = $this->get($id); + if ($uzivatel === null || $this->jeChraneny($uzivatel)) { + return false; + } + + return $this->ulozHeslo($id, $heslo); + } + + public function overHeslo(int $id, string $heslo): bool + { + $hash = $this->database->fetchField( + 'SELECT HESLO_HASH FROM [LOGIN] WHERE ID = ? AND AKTIVNI = 1', + $id + ); + + return $this->souhlasiHeslo($heslo, is_string($hash) ? $hash : null); + } + + /** + * Změna hesla účtu, který je právě přihlášený. Platí i pro účet správce. + */ + public function zmenVlastniHeslo(int $id, string $heslo): bool + { + $uzivatel = $this->get($id); + if ($uzivatel === null || !$this->jeAktivni($uzivatel->AKTIVNI)) { + return false; + } + + return $this->ulozHeslo($id, $heslo); + } + + private function ulozHeslo(int $id, string $heslo): bool + { + $this->database->query( + 'UPDATE [LOGIN] SET HESLO_HASH = ? WHERE ID = ?', + password_hash($heslo, PASSWORD_DEFAULT), + $id + ); + return true; + } + + private function jeAktivni(mixed $hodnota): bool + { + return $hodnota === true || $hodnota === 1 || $hodnota === '1'; + } +} diff --git a/app/Presentation/@layout.latte b/app/Presentation/@layout.latte index 3f7d290..15feec7 100644 --- a/app/Presentation/@layout.latte +++ b/app/Presentation/@layout.latte @@ -72,6 +72,7 @@ Obvody Zvěstovatelé Nastavení + Administrace Odhlásit {else} Home diff --git a/app/Presentation/Accessory/AdminMenuService.php b/app/Presentation/Accessory/AdminMenuService.php new file mode 100644 index 0000000..7a061ca --- /dev/null +++ b/app/Presentation/Accessory/AdminMenuService.php @@ -0,0 +1,34 @@ +onRender[] = function () use ($presenter): void { + $presenter->template->jeAdmin = $this->user->isLoggedIn() + && $this->uzivatele->jeAdmin((int) $this->user->getId()); + }; + } +} diff --git a/app/Presentation/Administrace/AdministracePresenter.php b/app/Presentation/Administrace/AdministracePresenter.php new file mode 100644 index 0000000..e5698f5 --- /dev/null +++ b/app/Presentation/Administrace/AdministracePresenter.php @@ -0,0 +1,217 @@ +uzivatele->jeAdmin((int) $this->getUser()->getId())) { + $this->error('Do administrace nemáte přístup.', Nette\Http\IResponse::S403_Forbidden); + } + } + + public function actionDefault(?string $stav = null): void + { + if ($stav !== null && $stav !== 'aktivni' && $stav !== 'neaktivni') { + $this->redirect('this', ['stav' => 'aktivni']); + } + $this->stav = $stav === 'neaktivni' ? 'neaktivni' : 'aktivni'; + } + + public function renderDefault(): void + { + $uzivatele = []; + foreach ($this->uzivatele->getUzivatele($this->stav === 'aktivni') as $uzivatel) { + $uzivatele[] = (object) [ + 'id' => (int) $uzivatel->ID, + 'jmeno' => (string) $uzivatel->JMENO, + 'aktivni' => $this->jeAktivni($uzivatel->AKTIVNI), + 'chraneny' => $this->uzivatele->jeChraneny($uzivatel), + ]; + } + + $this->template->stav = $this->stav; + $this->template->uzivatele = $uzivatele; + } + + protected function createComponentNovyForm(): Form + { + $form = new Form; + $form->addText('jmeno', 'Uživatel') + ->setRequired('Zadejte uživatelské jméno.') + ->setMaxLength(50); + $form->addPassword('heslo', 'Heslo') + ->setRequired('Zadejte heslo.') + ->setMaxLength(100); + $form->addPassword('hesloZnovu', 'Heslo znovu') + ->setRequired('Zadejte heslo znovu.') + ->addRule(Form::Equal, 'Hesla se neshodují.', $form['heslo']); + $form->addSubmit('send', 'Založit'); + $form->addProtection('Platnost formuláře vypršela, odešlete ho znovu.'); + $form->onSuccess[] = $this->novyFormSucceeded(...); + return $form; + } + + private function novyFormSucceeded(Form $form, ArrayHash $values): void + { + $jmeno = trim((string) $values->jmeno); + $heslo = (string) $values->heslo; + if (!$this->platneJmeno($jmeno)) { + $form->addError('Jméno smí mít nejvýše 50 znaků a jen písmena, čísla, tečku, pomlčku a podtržítko.'); + return; + } + if ($this->uzivatele->jeChraneneJmeno($jmeno)) { + $form->addError('Účet správce nelze založit znovu.'); + return; + } + if (!$this->platneHeslo($form, $heslo)) { + return; + } + if (!$this->uzivatele->zaloz($jmeno, $heslo)) { + $form->addError('Uživatel s tímto jménem už existuje.'); + return; + } + + $this->flashMessage('Uživatel byl založen.'); + $this->redirect('this', ['stav' => 'aktivni']); + } + + protected function createComponentHesloForm(): Form + { + $form = new Form; + $form->addHidden('id') + ->setRequired('Uživatel nenalezen.'); + $form->addHidden('jmeno'); + $form->addPassword('heslo', 'Nové heslo') + ->setRequired('Zadejte nové heslo.') + ->setMaxLength(100); + $form->addPassword('hesloZnovu', 'Nové heslo znovu') + ->setRequired('Zadejte nové heslo znovu.') + ->addRule(Form::Equal, 'Hesla se neshodují.', $form['heslo']); + $form->addSubmit('send', 'Uložit heslo'); + $form->addProtection('Platnost formuláře vypršela, odešlete ho znovu.'); + $form->onSuccess[] = $this->hesloFormSucceeded(...); + return $form; + } + + private function hesloFormSucceeded(Form $form, ArrayHash $values): void + { + $heslo = (string) $values->heslo; + if (!$this->platneHeslo($form, $heslo)) { + return; + } + + $uzivatel = $this->muzeUpravit((int) $values->id); + if ($uzivatel === null) { + $form->addError('Tomuto uživateli nelze změnit heslo.'); + return; + } + if (!$this->uzivatele->zmenHeslo((int) $uzivatel->ID, $heslo)) { + $form->addError('Tomuto uživateli nelze změnit heslo.'); + return; + } + + $this->flashMessage('Heslo bylo změněno.'); + $this->redirect('this'); + } + + protected function createComponentStav(): Multiplier + { + return new Multiplier(function (string $id): Form { + $form = new Form; + $form->addSubmit('send', 'Změnit stav'); + $form->addProtection('Platnost formuláře vypršela, odešlete ho znovu.'); + $form->onSuccess[] = function () use ($id): void { + $uzivatel = $this->muzeUpravit((int) $id); + if ($uzivatel === null) { + $this->flashMessage('Tomuto uživateli nelze změnit stav.', 'error'); + $this->redirect('this'); + } + + $aktivni = !$this->jeAktivni($uzivatel->AKTIVNI); + if (!$this->uzivatele->nastavAktivni((int) $uzivatel->ID, $aktivni)) { + $this->flashMessage('Tomuto uživateli nelze změnit stav.', 'error'); + $this->redirect('this'); + } + + $this->flashMessage($aktivni ? 'Uživatel byl aktivován.' : 'Uživatel byl zneaktivněn.'); + $this->redirect('this', ['stav' => $aktivni ? 'aktivni' : 'neaktivni']); + }; + return $form; + }); + } + + protected function createComponentSmazat(): Multiplier + { + return new Multiplier(function (string $id): Form { + $form = new Form; + $form->addSubmit('send', 'Smazat'); + $form->addProtection('Platnost formuláře vypršela, odešlete ho znovu.'); + $form->onSuccess[] = function () use ($id): void { + $uzivatel = $this->muzeUpravit((int) $id); + if ($uzivatel === null || !$this->uzivatele->smaz((int) $uzivatel->ID)) { + $this->flashMessage('Tohoto uživatele nelze smazat.', 'error'); + $this->redirect('this'); + } + + $this->flashMessage('Uživatel byl smazán.'); + $this->redirect('this'); + }; + return $form; + }); + } + + private function muzeUpravit(int $id): ?Row + { + $uzivatel = $this->uzivatele->get($id); + if ($uzivatel === null || $this->uzivatele->jeChraneny($uzivatel)) { + return null; + } + + return $uzivatel; + } + + private function platneHeslo(Form $form, string $heslo): bool + { + if (mb_strlen($heslo) < 4) { + $form->addError('Heslo musí mít alespoň 4 znaky.'); + return false; + } + if (mb_strlen($heslo) > 100) { + $form->addError('Heslo je příliš dlouhé.'); + return false; + } + + return true; + } + + private function platneJmeno(string $jmeno): bool + { + return (bool) preg_match('/^[\p{L}\p{N}._-]{1,50}$/u', $jmeno); + } + + private function jeAktivni(mixed $hodnota): bool + { + return $hodnota === true || $hodnota === 1 || $hodnota === '1'; + } +} diff --git a/app/Presentation/Administrace/default.latte b/app/Presentation/Administrace/default.latte new file mode 100644 index 0000000..d3e0326 --- /dev/null +++ b/app/Presentation/Administrace/default.latte @@ -0,0 +1,155 @@ +{block content} + +
+
+
+

Administrace

+

Účty pro přihlášení do aplikace.

+
+ +
+ +
+
+ +
+ + + + + + + + + + + {foreach $uzivatele as $uzivatel} + + + + + + {/foreach} + +
UživatelStavAkce
{$uzivatel->jmeno} + Aktivní + Neaktivní + +
+ + {form "stav-$uzivatel->id"} + + {/form} + {form "smazat-$uzivatel->id"} + + {/form} +
+ Nelze upravit +
+

V tomto filtru není žádný uživatel.

+
+
+ + + + + + diff --git a/app/Presentation/Nastaveni/NastaveniPresenter.php b/app/Presentation/Nastaveni/NastaveniPresenter.php index f470e71..9fb9654 100644 --- a/app/Presentation/Nastaveni/NastaveniPresenter.php +++ b/app/Presentation/Nastaveni/NastaveniPresenter.php @@ -6,6 +6,7 @@ namespace App\Presentation\Nastaveni; use App\Core\Konstanty; use App\Core\MapovyPodklad; +use App\Model\LoginFacade; use Nette\Application\UI\Form; use Nette\Application\UI\Presenter; use Nette\Utils\ArrayHash; @@ -13,6 +14,11 @@ use Nette\Utils\ArrayHash; final class NastaveniPresenter extends Presenter { + public function __construct( + private LoginFacade $uzivatele, + ) { + } + public function beforeRender(): void { $this->template->appVersion = Konstanty::verze; @@ -52,4 +58,53 @@ final class NastaveniPresenter extends Presenter { return MapovyPodklad::platny($this->getHttpRequest()->getCookie(MapovyPodklad::COOKIE)); } + + protected function createComponentHesloForm(): Form + { + $form = new Form; + $form->addPassword('soucasne', 'Současné heslo') + ->setRequired('Zadejte současné heslo.'); + $form->addPassword('heslo', 'Nové heslo') + ->setRequired('Zadejte nové heslo.') + ->setMaxLength(100); + $form->addPassword('hesloZnovu', 'Nové heslo znovu') + ->setRequired('Zadejte nové heslo znovu.') + ->addRule(Form::Equal, 'Hesla se neshodují.', $form['heslo']); + $form->addSubmit('send', 'Změnit heslo'); + $form->addProtection('Platnost formuláře vypršela, odešlete ho znovu.'); + $form->onSuccess[] = $this->hesloFormSucceeded(...); + return $form; + } + + private function hesloFormSucceeded(Form $form, ArrayHash $values): void + { + $user = $this->getUser(); + if (!$user->isLoggedIn()) { + $form->addError('Pro změnu hesla se přihlaste.'); + return; + } + + $id = (int) $user->getId(); + if (!$this->uzivatele->overHeslo($id, (string) $values->soucasne)) { + $form->addError('Současné heslo nesouhlasí.'); + return; + } + + $heslo = (string) $values->heslo; + if (mb_strlen($heslo) < 4) { + $form->addError('Heslo musí mít alespoň 4 znaky.'); + return; + } + if (mb_strlen($heslo) > 100) { + $form->addError('Heslo je příliš dlouhé.'); + return; + } + if (!$this->uzivatele->zmenVlastniHeslo($id, $heslo)) { + $form->addError('Heslo se nepodařilo změnit.'); + return; + } + + $this->flashMessage('Heslo bylo změněno.'); + $this->redirect('this'); + } } diff --git a/app/Presentation/Nastaveni/default.latte b/app/Presentation/Nastaveni/default.latte index ae486d6..c8ebd62 100644 --- a/app/Presentation/Nastaveni/default.latte +++ b/app/Presentation/Nastaveni/default.latte @@ -4,7 +4,8 @@

Nastavení

-

Platí jen pro toto zařízení.

+

Mapa na tomto zařízení a heslo účtu.

+

Platí jen pro toto zařízení.

@@ -22,4 +23,25 @@ + +
+

Heslo

+

Změna hesla účtu, kterým jste přihlášeni.

+
{$error}
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
diff --git a/config/services.neon b/config/services.neon index df599a5..0e29deb 100644 --- a/config/services.neon +++ b/config/services.neon @@ -2,6 +2,11 @@ services: - App\Core\RouterFactory::createRouter - App\Core\MujAutentifikator +decorator: + Nette\Application\Application: + setup: + - '$onPresenter[]' = [@App\Presentation\Accessory\AdminMenuService, pripoj] + search: - in: %appDir%