Přihlašování.

master
Kowalski 2026-07-31 16:23:08 +02:00
parent ea6ada8ce6
commit 5f2bb30ee4
7 changed files with 406 additions and 175 deletions

View File

@ -19,31 +19,53 @@ class Authenticator implements IAuthenticator
$this->passwords = $passwords; $this->passwords = $passwords;
} }
public function authenticate(string $user, string $password): SimpleIdentity /**
* Nette předává přihlašovací údaje jako pole [$email, $password]
*/
public function authenticate(array $credentials): SimpleIdentity
{ {
// 1. Dotaz do centrální Master DB [$email, $password] = $credentials;
// 1. Načtení uživatele podle tvého přesného schématu UZIVATEL
$row = $this->masterDb->fetch( $row = $this->masterDb->fetch(
'SELECT ID, EMAIL, HESLO_HASH, JMENO, PRIJMENI, DATABASE_HOST, DATABASE_NAME, DEFAULT_FIRMA_ID 'SELECT ID, EMAIL, HESLO_HASH, JMENO, PRIJMENI, DATABASE_NAME, CONNECTION_STRING_KEY
FROM dbo.UZIVATEL FROM dbo.UZIVATEL
WHERE EMAIL = ? AND AKTIVNI = 1', WHERE EMAIL = ? AND AKTIVNI = 1',
$user $email
); );
// $passwords = new Passwords(PASSWORD_BCRYPT, ['cost' => 12]);
// bdump($passwords->hash('admin1234'));
if (!$row || !$this->passwords->verify($password, $row->HESLO_HASH)) { if (!$row || !$this->passwords->verify($password, $row->HESLO_HASH)) {
throw new AuthenticationException('Nespravný e-mail nebo heslo.'); throw new AuthenticationException('Nespravný e-mail nebo heslo.');
} }
// 2. Všechny potřebné parametry pro KufrikConnectionFactory zabalíme do Nette Identity // 2. Aktualizace času posledního přihlášení
$this->masterDb->query(
'UPDATE dbo.UZIVATEL SET POSLEDNI_PRIHLASENI = SYSDATETIME() WHERE ID = ?',
$row->ID
);
// 3. Načtení výchozí FIRMA_ID z vazební tabulky UZIVATEL_FIRMA (pokud existuje)
$firmaRow = $this->masterDb->fetch(
'SELECT TOP 1 FIRMA_ID FROM dbo.UZIVATEL_FIRMA WHERE UZIVATEL_ID = ?',
$row->ID
);
// Fallback: Pokud vazební tabulka ještě není/nepoužívá se, použije se ID uživatele
$firmaId = $firmaRow ? $firmaRow->FIRMA_ID : $row->ID;
// 4. Předání dat do Nette Identity
return new SimpleIdentity( return new SimpleIdentity(
$row->ID, $row->ID,
null, // role (volitelně) null, // role
[ [
'email' => $row->EMAIL, 'email' => $row->EMAIL,
'jmeno' => $row->JMENO . ' ' . $row->PRIJMENI, 'jmeno' => trim(($row->JMENO ?? '') . ' ' . ($row->PRIJMENI ?? '')),
// Kufrik parametry: 'db_name' => $row->DATABASE_NAME, // Nulllable -> KufrikConnectionFactory použije default_db
'db_host' => $row->DATABASE_HOST ?? '127.0.0.1', // Umožňuje i RŮZNÉ SERVERY! 'connection_key' => $row->CONNECTION_STRING_KEY,
'db_name' => $row->DATABASE_NAME ?? 'Fakturace_MainDB', 'firma_id' => $firmaId,
'firma_id' => $row->DEFAULT_FIRMA_ID,
] ]
); );
} }

View File

@ -31,44 +31,55 @@
</button> </button>
<div class="collapse navbar-collapse" id="mainNavbar"> <div class="collapse navbar-collapse" id="mainNavbar">
<!-- Hlavní kategorie --> <!-- Hlavní kategorie - viditelné pouze pro přihlášené -->
{if $user->isLoggedIn()}
<ul class="navbar-nav me-auto nav nav-tabs" id="mainCategoryTabs" role="tablist"> <ul class="navbar-nav me-auto nav nav-tabs" id="mainCategoryTabs" role="tablist">
<li class="nav-item"> <li class="nav-item" role="presentation">
<button class="nav-link active" id="cat-invoices-tab" data-bs-toggle="tab" data-bs-target="#toolbar-invoices" type="button" role="tab"> <button class="nav-link active" id="cat-invoices-tab" data-bs-toggle="tab" data-bs-target="#toolbar-invoices" type="button" role="tab">
<i class="bi bi-file-earmark-text me-1"></i> Faktury <i class="bi bi-file-earmark-text me-1"></i> Faktury
</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item" role="presentation">
<button class="nav-link" id="cat-customers-tab" data-bs-toggle="tab" data-bs-target="#toolbar-customers" type="button" role="tab"> <button class="nav-link" id="cat-customers-tab" data-bs-toggle="tab" data-bs-target="#toolbar-customers" type="button" role="tab">
<i class="bi bi-people me-1"></i> Zákazníci <i class="bi bi-people me-1"></i> Zákazníci
</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item" role="presentation">
<button class="nav-link" id="cat-profile-tab" data-bs-toggle="tab" data-bs-target="#toolbar-profile" type="button" role="tab"> <button class="nav-link" id="cat-profile-tab" data-bs-toggle="tab" data-bs-target="#toolbar-profile" type="button" role="tab">
<i class="bi bi-building me-1"></i> Moje OSVČ <i class="bi bi-building me-1"></i> Moje OSVČ
</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item" role="presentation">
<button class="nav-link" id="cat-briefcase-tab" data-bs-toggle="tab" data-bs-target="#toolbar-briefcase" type="button" role="tab"> <button class="nav-link" id="cat-briefcase-tab" data-bs-toggle="tab" data-bs-target="#toolbar-briefcase" type="button" role="tab">
<i class="bi bi-box me-1"></i> Kufřík <i class="bi bi-box me-1"></i> Kufřík
</button> </button>
</li> </li>
</ul> </ul>
{/if}
<!-- Uživatel a Odhlášení --> <!-- Uživatel a Odhlášení / Přihlášení -->
<div class="user-panel"> <div class="user-panel ms-auto">
<span class="text-white d-none d-sm-inline"> {if $user->isLoggedIn()}
<i class="bi bi-person-circle me-1"></i> Jan Novák {* Přihlášený uživatel *}
<span class="text-white d-none d-sm-inline me-2">
<i class="bi bi-person-circle me-1"></i> {$user->identity->jmeno ?: $user->identity->email}
</span> </span>
<a href="#" 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>
{else}
{* Odhlášený / Nepřihlášený uživatel *}
<a n:href="Sign:in" class="btn btn-sm btn-outline-light" title="Přihlásit se">
<i class="bi bi-box-arrow-in-right me-1"></i> Přihlásit se
</a>
{/if}
</div> </div>
</div> </div>
</div> </div>
</nav> </nav>
<!-- 2. AKČNÍ LIŠTA (PODKATEGORIE) --> <!-- 2. AKČNÍ LIŠTA (PODKATEGORIE - zobrazeno jen přihlášeným) -->
{if $user->isLoggedIn()}
<div class="action-toolbar"> <div class="action-toolbar">
<div class="container-fluid"> <div class="container-fluid">
@ -87,29 +98,29 @@
<div class="tab-pane fade show active" id="toolbar-invoices" role="tabpanel"> <div class="tab-pane fade show active" id="toolbar-invoices" role="tabpanel">
<div class="toolbar-group"> <div class="toolbar-group">
<div class="action-buttons"> <div class="action-buttons">
<a href="#" class="btn btn-outline-primary btn-action"> <button type="button" class="btn btn-outline-primary btn-action">
<i class="bi bi-folder-fill"></i> <i class="bi bi-folder-fill"></i>
<span>Seznam faktur</span> <span>Seznam faktur</span>
</a> </button>
<a href="#" class="btn btn-success btn-action"> <button type="button" class="btn btn-success btn-action">
<i class="bi bi-plus-square-fill"></i> <i class="bi bi-plus-square-fill"></i>
<span>Přidat fakturu</span> <span>Přidat fakturu</span>
</a> </button>
<a href="#" class="btn btn-outline-secondary btn-action"> <button type="button" class="btn btn-outline-secondary btn-action" onclick="window.print()">
<i class="bi bi-printer-fill"></i> <i class="bi bi-printer-fill"></i>
<span>Tisk</span> <span>Tisk</span>
</a> </button>
<div class="v-divider d-none d-sm-block"></div> <div class="v-divider d-none d-sm-block"></div>
<a href="#" class="btn btn-outline-secondary btn-action"> <button type="button" class="btn btn-outline-secondary btn-action">
<i class="bi bi-bar-chart-line-fill text-warning"></i> <i class="bi bi-bar-chart-line-fill text-warning"></i>
<span>Grafy</span> <span>Grafy</span>
</a> </button>
<a href="#" class="btn btn-outline-secondary btn-action"> <button type="button" class="btn btn-outline-secondary btn-action">
<i class="bi bi-file-earmark-pdf-fill text-danger"></i> <i class="bi bi-file-earmark-pdf-fill text-danger"></i>
<span>Tiskové sestavy</span> <span>Tiskové sestavy</span>
</a> </button>
</div> </div>
<!-- Filtr pro Faktury --> <!-- Filtr pro Faktury -->
@ -131,39 +142,39 @@
<!-- ZÁKAZNÍCI --> <!-- ZÁKAZNÍCI -->
<div class="tab-pane fade" id="toolbar-customers" role="tabpanel"> <div class="tab-pane fade" id="toolbar-customers" role="tabpanel">
<div class="action-buttons"> <div class="action-buttons">
<a href="#" class="btn btn-outline-primary btn-action"> <button type="button" class="btn btn-outline-primary btn-action">
<i class="bi bi-people-fill"></i> <i class="bi bi-people-fill"></i>
<span>Seznam</span> <span>Seznam</span>
</a> </button>
<a href="#" class="btn btn-success btn-action"> <button type="button" class="btn btn-success btn-action">
<i class="bi bi-person-plus-fill"></i> <i class="bi bi-person-plus-fill"></i>
<span>Přidat zákazníka</span> <span>Přidat zákazníka</span>
</a> </button>
</div> </div>
</div> </div>
<!-- MOJE OSVČ --> <!-- MOJE OSVČ -->
<div class="tab-pane fade" id="toolbar-profile" role="tabpanel"> <div class="tab-pane fade" id="toolbar-profile" role="tabpanel">
<div class="action-buttons"> <div class="action-buttons">
<a href="#" class="btn btn-outline-primary btn-action"> <button type="button" class="btn btn-outline-primary btn-action">
<i class="bi bi-pencil-square"></i> <i class="bi bi-pencil-square"></i>
<span>Upravit moje OSVČ</span> <span>Upravit moje OSVČ</span>
</a> </button>
<a href="#" class="btn btn-outline-secondary btn-action"> <button type="button" class="btn btn-outline-secondary btn-action">
<i class="bi bi-gear-fill"></i> <i class="bi bi-gear-fill"></i>
<span>Nastavení</span> <span>Nastavení</span>
</a> </button>
</div> </div>
</div> </div>
<!-- KUFŘÍK --> <!-- KUFŘÍK -->
<div class="tab-pane fade" id="toolbar-briefcase" role="tabpanel"> <div class="tab-pane fade" id="toolbar-briefcase" role="tabpanel">
<div class="action-buttons"> <div class="action-buttons">
<a href="#" class="btn btn-outline-info btn-action"> <button type="button" class="btn btn-outline-info btn-action">
<i class="bi bi-question-circle-fill"></i> <i class="bi bi-question-circle-fill"></i>
<span>Nápověda</span> <span>Nápověda</span>
</a> </button>
<a href="https://facebook.com" target="_blank" class="btn btn-outline-primary btn-action"> <a href="https://facebook.com" target="_blank" rel="noopener noreferrer" class="btn btn-outline-primary btn-action">
<i class="bi bi-facebook"></i> <i class="bi bi-facebook"></i>
<span>Stránky - FB</span> <span>Stránky - FB</span>
</a> </a>
@ -175,6 +186,7 @@
</div> </div>
</div> </div>
{/if}
<!-- 3. HLAVNÍ OBSAH --> <!-- 3. HLAVNÍ OBSAH -->
<main class="main-content"> <main class="main-content">

18
app/UI/BasePresenter.php Normal file
View File

@ -0,0 +1,18 @@
<?php
namespace App\UI;
use Nette\Application\UI\Presenter;
abstract class BasePresenter extends Presenter
{
protected function startup(): void
{
parent::startup();
// Pokud uživatel není přihlášen a nepokouší se právě přihlásit
if (!$this->getUser()->isLoggedIn() && !$this->isLinkCurrent('Sign:in')) {
$this->redirect('Sign:in');
}
}
}

View File

@ -4,9 +4,10 @@ declare(strict_types=1);
namespace App\UI\Home; namespace App\UI\Home;
use App\UI\BasePresenter;
use Nette; use Nette;
final class HomePresenter extends Nette\Application\UI\Presenter final class HomePresenter extends BasePresenter
{ {
} }

View File

@ -1,23 +1,42 @@
{block content} {block content}
<div class="login-page">
<div class="login-card">
{* {control signInForm} *} <!-- Hlavička karty s logem/názvem -->
<div class="container-fluid"> <div class="login-header">
<section class="w-100 p-4 d-flex justify-content-center pb-4"> <h1 class="login-title">Kufřík</h1>
<form n:name=signInForm style="width: 22rem;"> <p class="login-subtitle">Přihlášení do fakturačního systému</p>
<div class="p-3 container-fluid ramecek-chyba" n:if="$form->hasErrors()" n:foreach="$form->getErrors() as $error"> </div>
<span>{$error}</span>
<!-- Zobrazení Chybových zpráv -->
<div n:foreach="$flashes as $flash" class="alert alert-{$flash->type}">
{$flash->message}
</div>
<!-- Nette Formulář -->
<form n:name="signInForm" class="login-form">
<!-- Chybové zprávy samotného formuláře -->
<ul class="error-list" n:ifcontent>
<li n:foreach="$form->ownErrors as $error">{$error}</li>
</ul>
<div class="form-group">
<label n:name="email">E-mailová adresa</label>
<input n:name="email" class="form-control" autofocus placeholder="jan@novak.cz">
</div> </div>
<div class="form-group"> <div class="form-group">
<label n:name=username for="inputUser">Uživatel</label> <label n:name="password">Heslo</label>
<input n:name=username type="text" class="form-control" id="inputUser" placeholder="Uživatel" required> <input n:name="password" class="form-control" placeholder="••••••••">
</div> </div>
<div class="form-group"> <div class="form-actions">
<label n:name=password for="inputHeslo" n:if="$salt == 0">Heslo</label> <button n:name="send" class="btn btn-primary btn-block">
<input n:name=password type="{if $salt == 0}password{else}hidden{/if}" class="form-control" id="inputHeslo" placeholder="Heslo" autofocus="autofocus" required> Přihlásit se
</button>
</div> </div>
<button type="submit" n:name=send class="btn btn-primary btn-xlarge w-100" style="margin-top: 10px;">Přihlásit</button>
</form> </form>
</section>
</div> </div>
</div>
{/block}

View File

@ -237,3 +237,44 @@ ADD FILTER PREDICATE Security.fn_SecurityPredicate(FIRMA_ID) ON dbo.POLOZKA,
ADD BLOCK PREDICATE Security.fn_SecurityPredicate(FIRMA_ID) ON dbo.POLOZKA ADD BLOCK PREDICATE Security.fn_SecurityPredicate(FIRMA_ID) ON dbo.POLOZKA
WITH (STATE = ON); WITH (STATE = ON);
GO GO
-- ==========================================
-- Cvičná data a UZIVATEL
-- ==========================================
-- Vygenerovaný platný BCrypt hash (cost: 12) pro heslo: admin1234
DECLARE @HesloHash NVARCHAR(255) = N'$2y$12$9SAWctZKp/JpYl7K.awSRehXOXVyRpu0ofReqvbm1jVjvdz41zH7a';
DECLARE @Email NVARCHAR(100) = N'admin@kmetix.cz';
IF NOT EXISTS (SELECT 1 FROM dbo.UZIVATEL WHERE EMAIL = @Email)
BEGIN
INSERT INTO dbo.UZIVATEL (
[EMAIL],
[HESLO_HASH],
[JMENO],
[PRIJMENI],
[TELEFON],
[DATABASE_NAME],
[CONNECTION_STRING_KEY],
[AKTIVNI]
)
VALUES (
@Email,
@HesloHash,
N'Pavel',
N'Kowalski',
N'+420123456789',
N'', -- Cílová databáze pro klientská data
NULL,
1 -- AKTIVNI = 1
);
PRINT 'Uživatel admin@kmetix.cz byl úspěšně vytvořen s heslem: admin1234';
END
ELSE
BEGIN
PRINT 'Uživatel s tímto e-mailem již v databázi existuje.';
END
GO

View File

@ -209,3 +209,121 @@ body {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
/* --- 6. SIGN-IN PAGE --- */
.login-page {
flex: 1 1 auto;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
background-color: var(--app-bg);
}
.login-card {
width: 100%;
max-width: 380px;
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 2rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
}
.login-header {
text-align: center;
margin-bottom: 1.75rem;
}
.login-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--header-bg); /* Tmavě břidlicová ze stejné palety */
margin: 0 0 0.25rem 0;
letter-spacing: -0.02em;
}
.login-subtitle {
font-size: 0.85rem;
color: var(--text-muted);
margin: 0;
}
.login-form .form-group {
margin-bottom: 1.15rem;
}
.login-form label {
display: block;
font-weight: 500;
font-size: 0.825rem;
color: var(--text-main);
margin-bottom: 0.35rem;
}
.login-form .form-control {
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
color: var(--text-main);
background-color: #fff;
border: 1px solid #cbd5e1;
border-radius: var(--radius-sm);
box-sizing: border-box;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.login-form .form-control:focus {
outline: none;
border-color: var(--primary-accent);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
.login-form .btn-primary {
width: 100%;
padding: 0.6rem;
font-size: 0.875rem;
font-weight: 600;
color: #fff;
background-color: var(--primary-accent);
border: 1px solid var(--primary-accent);
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.15s ease-in-out;
margin-top: 0.5rem;
}
.login-form .btn-primary:hover {
background-color: #2563eb;
border-color: #2563eb;
transform: translateY(-1px);
}
/* Chybové zprávy */
.error-list {
list-style: none;
padding: 0.5rem 0.75rem;
margin: 0 0 1rem 0;
background-color: #fef2f2;
border: 1px solid #fecaca;
border-radius: var(--radius-sm);
color: #dc2626;
font-size: 0.8rem;
}
.alert {
padding: 0.65rem 0.85rem;
border-radius: var(--radius-sm);
font-size: 0.825rem;
margin-bottom: 1rem;
}
.alert-danger, .alert-error {
background-color: #fef2f2;
border: 1px solid #fecaca;
color: #991b1b;
}
.alert-success {
background-color: #ecfdf5;
border: 1px solid #a7f3d0;
color: #065f46;
}