Změna hesla uživatele + GUI

master
Kowalski 2026-10-31 11:34:31 +01:00
parent e1da2619a8
commit eec6b692b2
5 changed files with 252 additions and 6 deletions

View File

@ -307,6 +307,40 @@ class DatabazeFacade
); );
} }
/**
* Změna přihlašovacího jména (e-mailu) přihlášeného uživatele.
*/
public function changeOwnEmail(int $loginId, string $email): string
{
$this->updateUserCredentials($loginId, $email, null);
return mb_strtolower(trim($email));
}
/**
* Změna hesla. Původní heslo musí sedět s uloženým hashem.
*/
public function changeOwnPassword(int $loginId, string $currentPassword, string $newPassword): void
{
$row = $this->masterDb->fetch(
'SELECT HESLO_HASH FROM dbo.LOGIN WHERE ID = ? AND AKTIVNI = 1',
$loginId
);
$hash = $row !== null ? (string) ($row->HESLO_HASH ?? '') : '';
if ($hash === '' || !$this->passwords->verify($currentPassword, $hash)) {
throw new \InvalidArgumentException('Původní heslo není správné.');
}
if (mb_strlen($newPassword) < 6) {
throw new \InvalidArgumentException('Heslo musí mít alespoň 6 znaků.');
}
$this->masterDb->query(
'UPDATE dbo.LOGIN SET HESLO_HASH = ? WHERE ID = ?',
$this->passwords->hash($newPassword),
$loginId
);
}
public function findLoginIdForFirma(string $database, int $firmaId, ?string $server = null): ?int public function findLoginIdForFirma(string $database, int $firmaId, ?string $server = null): ?int
{ {
$server ??= $this->tenantServer; $server ??= $this->tenantServer;

View File

@ -25,7 +25,7 @@
</script> </script>
{* naše vlastní styly *} {* naše vlastní styly *}
<link href="{$basePath}/css/layout.css?v={$appVersion ?? '1.0'}-7" rel="stylesheet"> <link href="{$basePath}/css/layout.css?v={$appVersion ?? '1.0'}-9" rel="stylesheet">
</head> </head>
<body> <body>
@ -77,10 +77,11 @@
<!-- Uživatel a Odhlášení / Přihlášení --> <!-- Uživatel a Odhlášení / Přihlášení -->
<div class="user-panel ms-auto"> <div class="user-panel ms-auto">
{if $user->isLoggedIn()} {if $user->isLoggedIn()}
{* Přihlášený uživatel *} {* Přihlášený uživatel — ikona / jméno → Nastavení *}
<span class="text-white d-none d-sm-inline me-2"> <a n:href="Nastaveni:default" class="user-panel-link text-white me-2" title="Nastavení">
<i class="bi bi-person-circle me-1"></i> {$user->identity->jmeno ?: $user->identity->email} <i class="bi bi-person-circle me-1"></i>
</span> <span class="d-none d-sm-inline">{$user->identity->jmeno ?: $user->identity->email}</span>
</a>
<a n:href="Sign:out" class="btn btn-sm btn-outline-light" title="Odhlásit se"> <a n:href="Sign:out" class="btn btn-sm btn-outline-light" title="Odhlásit se">
<i class="bi bi-box-arrow-right me-1"></i> Odhlásit <i class="bi bi-box-arrow-right me-1"></i> Odhlásit
</a> </a>

View File

@ -3,14 +3,17 @@
namespace App\UI\Nastaveni; namespace App\UI\Nastaveni;
use App\Model\Faktura\FakturaCislovani; use App\Model\Faktura\FakturaCislovani;
use App\Model\Facade\DatabazeFacade;
use App\Model\Facade\FakturaFacade; use App\Model\Facade\FakturaFacade;
use App\UI\BasePresenter; use App\UI\BasePresenter;
use Nette\Application\UI\Form; use Nette\Application\UI\Form;
use Nette\Security\SimpleIdentity;
class NastaveniPresenter extends BasePresenter class NastaveniPresenter extends BasePresenter
{ {
public function __construct( public function __construct(
private FakturaFacade $fakturaFacade private FakturaFacade $fakturaFacade,
private DatabazeFacade $databazeFacade,
) { ) {
parent::__construct(); parent::__construct();
} }
@ -29,6 +32,14 @@ class NastaveniPresenter extends BasePresenter
'obdobi' => $nastaveni['obdobi'], 'obdobi' => $nastaveni['obdobi'],
'splatnost' => $nastaveni['splatnostDny'], 'splatnost' => $nastaveni['splatnostDny'],
]); ]);
$login = $this->databazeFacade->getById((int) $this->getUser()->getId());
$email = $login !== null
? (string) $login->EMAIL
: (string) ($this->getUser()->getIdentity()->email ?? '');
$this['jmenoForm']->setDefaults([
'email' => $email,
]);
} }
public function renderDefault(): void public function renderDefault(): void
@ -36,6 +47,9 @@ class NastaveniPresenter extends BasePresenter
$firmaId = (int) $this->getUser()->getIdentity()->firma_id; $firmaId = (int) $this->getUser()->getIdentity()->firma_id;
$dalsi = $this->fakturaFacade->getNextCislo($firmaId); $dalsi = $this->fakturaFacade->getNextCislo($firmaId);
$this->template->prikladCisla = $dalsi['cislo']; $this->template->prikladCisla = $dalsi['cislo'];
$hesloForm = $this['hesloForm'];
$this->template->otevritHesloModal = $hesloForm->isSubmitted() && !$hesloForm->isSuccess();
} }
protected function createComponentNastaveniForm(): Form protected function createComponentNastaveniForm(): Form
@ -79,4 +93,87 @@ class NastaveniPresenter extends BasePresenter
$this->flashMessage('Nastavení faktur bylo uloženo.', 'success'); $this->flashMessage('Nastavení faktur bylo uloženo.', 'success');
$this->redirect('this'); $this->redirect('this');
} }
protected function createComponentJmenoForm(): Form
{
$form = new Form;
$form->addText('email', 'Uživatelské jméno:')
->setHtmlType('email')
->setHtmlAttribute('autocomplete', 'username')
->setRequired('Zadejte uživatelské jméno.')
->addRule(Form::Email, 'Zadejte platný e-mail.')
->addRule(Form::MaxLength, 'Uživatelské jméno může mít nejvýše 100 znaků.', 100);
$form->addSubmit('save', 'Uložit jméno');
$form->onSuccess[] = [$this, 'jmenoFormSucceeded'];
return $form;
}
public function jmenoFormSucceeded(Form $form, \stdClass $data): void
{
try {
$email = $this->databazeFacade->changeOwnEmail(
(int) $this->getUser()->getId(),
(string) $data->email
);
} catch (\InvalidArgumentException $e) {
$form->addError($e->getMessage());
return;
}
$this->replaceIdentityEmail($email);
$this->flashMessage('Uživatelské jméno bylo uloženo.', 'success');
$this->redirect('this');
}
protected function createComponentHesloForm(): Form
{
$form = new Form;
$form->addPassword('current', 'Původní heslo:')
->setHtmlAttribute('autocomplete', 'current-password')
->setRequired('Zadejte původní heslo.');
$form->addPassword('password', 'Nové heslo:')
->setHtmlAttribute('autocomplete', 'new-password')
->setRequired('Zadejte nové heslo.')
->addRule(Form::MinLength, 'Heslo musí mít alespoň 6 znaků.', 6);
$form->addPassword('passwordVerify', 'Nové heslo znovu:')
->setHtmlAttribute('autocomplete', 'new-password')
->setRequired('Zadejte nové heslo znovu.')
->addRule(Form::Equal, 'Hesla se neshodují.', $form['password']);
$form->addSubmit('save', 'Uložit heslo');
$form->onSuccess[] = [$this, 'hesloFormSucceeded'];
return $form;
}
public function hesloFormSucceeded(Form $form, \stdClass $data): void
{
try {
$this->databazeFacade->changeOwnPassword(
(int) $this->getUser()->getId(),
(string) $data->current,
(string) $data->password
);
} catch (\InvalidArgumentException $e) {
$form['current']->addError($e->getMessage());
return;
}
$this->flashMessage('Heslo bylo změněno.', 'success');
$this->redirect('this');
}
private function replaceIdentityEmail(string $email): void
{
$identity = $this->getUser()->getIdentity();
$data = $identity->getData();
$data['email'] = $email;
$this->getUser()->login(new SimpleIdentity($identity->getId(), $identity->getRoles(), $data));
}
} }

View File

@ -53,7 +53,99 @@
</button> </button>
</div> </div>
</form> </form>
<form n:name="jmenoForm" class="login-form mt-4">
<div n:if="$form->errors" class="alert alert-danger mb-3">
<ul class="mb-0 ps-3">
<li n:foreach="$form->errors as $error">{$error}</li>
</ul>
</div>
<fieldset class="border rounded p-3">
<legend class="float-none w-auto px-2 fs-6 fw-bold mb-0">Přihlášení</legend>
<div class="row align-items-center mb-3">
<label n:name="email" class="col-sm-5 col-form-label">Uživatelské jméno:</label>
<div class="col-sm-7">
<div class="input-group">
<input n:name="email" class="form-control">
<button n:name="save" class="btn btn-outline-primary">Uložit</button>
</div>
</div>
</div>
<div class="row align-items-center">
<div class="col-sm-5 col-form-label">Heslo:</div>
<div class="col-sm-7">
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#hesloModal">
<i class="bi bi-key me-1"></i> Změnit heslo
</button>
</div>
</div>
<p class="text-muted small mt-3 mb-0">
Uživatelské jméno je e-mail, kterým se přihlašujete.
</p>
</fieldset>
</form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" id="hesloModal" tabindex="-1" aria-hidden="true" aria-labelledby="hesloModalLabel">
<div class="modal-dialog">
<div class="modal-content">
<form n:name="hesloForm">
<div class="modal-header">
<h5 class="modal-title" id="hesloModalLabel">
<i class="bi bi-key me-1"></i>Změna hesla
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zavřít"></button>
</div>
<div class="modal-body">
<div n:if="$form->errors" class="alert alert-danger mb-3">
<ul class="mb-0 ps-3">
<li n:foreach="$form->errors as $error">{$error}</li>
</ul>
</div>
<div class="mb-3">
<label n:name="current" class="form-label">Původní heslo</label>
<input n:name="current" class="form-control">
</div>
<div class="mb-3">
<label n:name="password" class="form-label">Nové heslo</label>
<input n:name="password" class="form-control">
</div>
<div class="mb-0">
<label n:name="passwordVerify" class="form-label">Nové heslo znovu</label>
<input n:name="passwordVerify" class="form-control">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Zrušit</button>
<button n:name="save" class="btn btn-primary">Uložit heslo</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
var el = document.getElementById('hesloModal');
if (!el || typeof bootstrap === 'undefined') {
return;
}
el.addEventListener('shown.bs.modal', function () {
var input = el.querySelector('input[name="current"]');
if (input) {
input.focus();
}
});
{if $otevritHesloModal}
bootstrap.Modal.getOrCreateInstance(el).show();
{/if}
});
</script>

View File

@ -85,6 +85,24 @@ body {
margin-left: auto; margin-left: auto;
} }
.user-panel-link {
display: inline-flex;
align-items: center;
text-decoration: none;
opacity: 0.92;
border-radius: var(--radius-sm);
padding: 0.2rem 0.35rem;
transition: opacity 0.15s ease, background-color 0.15s ease;
}
.user-panel-link:hover,
.user-panel-link:focus-visible {
opacity: 1;
color: #fff;
background-color: rgba(255, 255, 255, 0.08);
text-decoration: none;
}
.user-panel .btn-outline-light { .user-panel .btn-outline-light {
border-color: #5d7390; border-color: #5d7390;
color: #e4ebf4; color: #e4ebf4;
@ -294,6 +312,10 @@ body {
margin-bottom: 0.35rem; margin-bottom: 0.35rem;
} }
.login-form .input-group > .form-control {
width: 1%;
}
.login-form .form-control { .login-form .form-control {
width: 100%; width: 100%;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;