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;
}
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(
'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
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)) {
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(
$row->ID,
null, // role (volitelně)
null, // role
[
'email' => $row->EMAIL,
'jmeno' => $row->JMENO . ' ' . $row->PRIJMENI,
// Kufrik parametry:
'db_host' => $row->DATABASE_HOST ?? '127.0.0.1', // Umožňuje i RŮZNÉ SERVERY!
'db_name' => $row->DATABASE_NAME ?? 'Fakturace_MainDB',
'firma_id' => $row->DEFAULT_FIRMA_ID,
'jmeno' => trim(($row->JMENO ?? '') . ' ' . ($row->PRIJMENI ?? '')),
'db_name' => $row->DATABASE_NAME, // Nulllable -> KufrikConnectionFactory použije default_db
'connection_key' => $row->CONNECTION_STRING_KEY,
'firma_id' => $firmaId,
]
);
}

View File

@ -22,159 +22,171 @@
<body>
<!-- 1. HORNÍ LIŠTA -->
<nav class="navbar navbar-expand-lg navbar-dark app-header">
<div class="container-fluid">
<a class="navbar-brand" href="#"><i class="bi bi-briefcase-fill me-2"></i>Kufřík</a>
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#mainNavbar">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNavbar">
<!-- Hlavní kategorie -->
<ul class="navbar-nav me-auto nav nav-tabs" id="mainCategoryTabs" role="tablist">
<li class="nav-item">
<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
</button>
</li>
<li class="nav-item">
<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
</button>
</li>
<li class="nav-item">
<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Č
</button>
</li>
<li class="nav-item">
<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
</button>
</li>
</ul>
<nav class="navbar navbar-expand-lg navbar-dark app-header">
<div class="container-fluid">
<a class="navbar-brand" href="#"><i class="bi bi-briefcase-fill me-2"></i>Kufřík</a>
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#mainNavbar">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNavbar">
<!-- Hlavní kategorie - viditelné pouze pro přihlášené -->
{if $user->isLoggedIn()}
<ul class="navbar-nav me-auto nav nav-tabs" id="mainCategoryTabs" role="tablist">
<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">
<i class="bi bi-file-earmark-text me-1"></i> Faktury
</button>
</li>
<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">
<i class="bi bi-people me-1"></i> Zákazníci
</button>
</li>
<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">
<i class="bi bi-building me-1"></i> Moje OSVČ
</button>
</li>
<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">
<i class="bi bi-box me-1"></i> Kufřík
</button>
</li>
</ul>
{/if}
<!-- Uživatel a Odhlášení -->
<div class="user-panel">
<span class="text-white d-none d-sm-inline">
<i class="bi bi-person-circle me-1"></i> Jan Novák
</span>
<a href="#" class="btn btn-sm btn-outline-light" title="Odhlásit se">
<i class="bi bi-box-arrow-right me-1"></i> Odhlásit
</a>
</div>
</div>
</div>
</nav>
<!-- Uživatel a Odhlášení / Přihlášení -->
<div class="user-panel ms-auto">
{if $user->isLoggedIn()}
{* 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>
<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
</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>
</nav>
<!-- 2. AKČNÍ LIŠTA (PODKATEGORIE) -->
<div class="action-toolbar">
<div class="container-fluid">
<!-- Tlačítko pro mobilní rozbalení -->
<div class="d-lg-none d-flex justify-content-between align-items-center">
<span class="fw-bold text-muted small">Nástroje</span>
<button class="btn btn-outline-secondary btn-sm" type="button" data-bs-toggle="collapse" data-bs-target="#actionToolbarCollapse">
<i class="bi bi-three-dots-vertical"></i> Podkategorie
</button>
</div>
<!-- 2. AKČNÍ LIŠTA (PODKATEGORIE - zobrazeno jen přihlášeným) -->
{if $user->isLoggedIn()}
<div class="action-toolbar">
<div class="container-fluid">
<!-- Tlačítko pro mobilní rozbalení -->
<div class="d-lg-none d-flex justify-content-between align-items-center">
<span class="fw-bold text-muted small">Nástroje</span>
<button class="btn btn-outline-secondary btn-sm" type="button" data-bs-toggle="collapse" data-bs-target="#actionToolbarCollapse">
<i class="bi bi-three-dots-vertical"></i> Podkategorie
</button>
</div>
<div class="collapse d-lg-block mt-2 mt-lg-0" id="actionToolbarCollapse">
<div class="tab-content" id="actionToolbarContent">
<!-- FAKTURY -->
<div class="tab-pane fade show active" id="toolbar-invoices" role="tabpanel">
<div class="toolbar-group">
<div class="collapse d-lg-block mt-2 mt-lg-0" id="actionToolbarCollapse">
<div class="tab-content" id="actionToolbarContent">
<!-- FAKTURY -->
<div class="tab-pane fade show active" id="toolbar-invoices" role="tabpanel">
<div class="toolbar-group">
<div class="action-buttons">
<button type="button" class="btn btn-outline-primary btn-action">
<i class="bi bi-folder-fill"></i>
<span>Seznam faktur</span>
</button>
<button type="button" class="btn btn-success btn-action">
<i class="bi bi-plus-square-fill"></i>
<span>Přidat fakturu</span>
</button>
<button type="button" class="btn btn-outline-secondary btn-action" onclick="window.print()">
<i class="bi bi-printer-fill"></i>
<span>Tisk</span>
</button>
<div class="v-divider d-none d-sm-block"></div>
<button type="button" class="btn btn-outline-secondary btn-action">
<i class="bi bi-bar-chart-line-fill text-warning"></i>
<span>Grafy</span>
</button>
<button type="button" class="btn btn-outline-secondary btn-action">
<i class="bi bi-file-earmark-pdf-fill text-danger"></i>
<span>Tiskové sestavy</span>
</button>
</div>
<!-- Filtr pro Faktury -->
<form class="filter-panel ms-auto ms-lg-0">
<i class="bi bi-funnel-fill text-muted fs-5 ms-1"></i>
<select class="form-select form-select-sm" style="width: 85px;">
<option selected>2026</option>
<option>2025</option>
</select>
<select class="form-select form-select-sm" style="width: 140px;">
<option selected>(všechny měsíce)</option>
<option>Leden</option>
<option>Únor</option>
</select>
</form>
</div>
</div>
<!-- ZÁKAZNÍCI -->
<div class="tab-pane fade" id="toolbar-customers" role="tabpanel">
<div class="action-buttons">
<a href="#" class="btn btn-outline-primary btn-action">
<i class="bi bi-folder-fill"></i>
<span>Seznam faktur</span>
</a>
<a href="#" class="btn btn-success btn-action">
<i class="bi bi-plus-square-fill"></i>
<span>Přidat fakturu</span>
</a>
<a href="#" class="btn btn-outline-secondary btn-action">
<i class="bi bi-printer-fill"></i>
<span>Tisk</span>
</a>
<div class="v-divider d-none d-sm-block"></div>
<a href="#" class="btn btn-outline-secondary btn-action">
<i class="bi bi-bar-chart-line-fill text-warning"></i>
<span>Grafy</span>
</a>
<a href="#" class="btn btn-outline-secondary btn-action">
<i class="bi bi-file-earmark-pdf-fill text-danger"></i>
<span>Tiskové sestavy</span>
<button type="button" class="btn btn-outline-primary btn-action">
<i class="bi bi-people-fill"></i>
<span>Seznam</span>
</button>
<button type="button" class="btn btn-success btn-action">
<i class="bi bi-person-plus-fill"></i>
<span>Přidat zákazníka</span>
</button>
</div>
</div>
<!-- MOJE OSVČ -->
<div class="tab-pane fade" id="toolbar-profile" role="tabpanel">
<div class="action-buttons">
<button type="button" class="btn btn-outline-primary btn-action">
<i class="bi bi-pencil-square"></i>
<span>Upravit moje OSVČ</span>
</button>
<button type="button" class="btn btn-outline-secondary btn-action">
<i class="bi bi-gear-fill"></i>
<span>Nastavení</span>
</button>
</div>
</div>
<!-- KUFŘÍK -->
<div class="tab-pane fade" id="toolbar-briefcase" role="tabpanel">
<div class="action-buttons">
<button type="button" class="btn btn-outline-info btn-action">
<i class="bi bi-question-circle-fill"></i>
<span>Nápověda</span>
</button>
<a href="https://facebook.com" target="_blank" rel="noopener noreferrer" class="btn btn-outline-primary btn-action">
<i class="bi bi-facebook"></i>
<span>Stránky - FB</span>
</a>
</div>
<!-- Filtr pro Faktury -->
<form class="filter-panel ms-auto ms-lg-0">
<i class="bi bi-funnel-fill text-muted fs-5 ms-1"></i>
<select class="form-select form-select-sm" style="width: 85px;">
<option selected>2026</option>
<option>2025</option>
</select>
<select class="form-select form-select-sm" style="width: 140px;">
<option selected>(všechny měsíce)</option>
<option>Leden</option>
<option>Únor</option>
</select>
</form>
</div>
</div>
</div>
<!-- ZÁKAZNÍCI -->
<div class="tab-pane fade" id="toolbar-customers" role="tabpanel">
<div class="action-buttons">
<a href="#" class="btn btn-outline-primary btn-action">
<i class="bi bi-people-fill"></i>
<span>Seznam</span>
</a>
<a href="#" class="btn btn-success btn-action">
<i class="bi bi-person-plus-fill"></i>
<span>Přidat zákazníka</span>
</a>
</div>
</div>
<!-- MOJE OSVČ -->
<div class="tab-pane fade" id="toolbar-profile" role="tabpanel">
<div class="action-buttons">
<a href="#" class="btn btn-outline-primary btn-action">
<i class="bi bi-pencil-square"></i>
<span>Upravit moje OSVČ</span>
</a>
<a href="#" class="btn btn-outline-secondary btn-action">
<i class="bi bi-gear-fill"></i>
<span>Nastavení</span>
</a>
</div>
</div>
<!-- KUFŘÍK -->
<div class="tab-pane fade" id="toolbar-briefcase" role="tabpanel">
<div class="action-buttons">
<a href="#" class="btn btn-outline-info btn-action">
<i class="bi bi-question-circle-fill"></i>
<span>Nápověda</span>
</a>
<a href="https://facebook.com" target="_blank" class="btn btn-outline-primary btn-action">
<i class="bi bi-facebook"></i>
<span>Stránky - FB</span>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/if}
<!-- 3. HLAVNÍ OBSAH -->
<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;
use App\UI\BasePresenter;
use Nette;
final class HomePresenter extends Nette\Application\UI\Presenter
final class HomePresenter extends BasePresenter
{
}

View File

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

View File

@ -236,4 +236,45 @@ ADD BLOCK PREDICATE Security.fn_SecurityPredicate(FIRMA_ID) ON dbo.ZAKAZNIK,
ADD FILTER PREDICATE Security.fn_SecurityPredicate(FIRMA_ID) ON dbo.POLOZKA,
ADD BLOCK PREDICATE Security.fn_SecurityPredicate(FIRMA_ID) ON dbo.POLOZKA
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

@ -208,4 +208,122 @@ body {
display: flex;
align-items: center;
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;
}