MasterDB + layout

master
Kowalski 2026-08-03 09:56:09 +02:00
parent 5f2bb30ee4
commit 2398bd6aca
11 changed files with 513 additions and 126 deletions

View File

@ -11,11 +11,13 @@ use Nette\Security\User;
class KufrikConnectionFactory
{
private User $user;
private array $baseConfig;
private string $dbUser;
private string $dbPassword;
public function __construct(array $baseConfig, User $user)
public function __construct(string $dbUser, string $dbPassword, User $user)
{
$this->baseConfig = $baseConfig;
$this->dbUser = $dbUser;
$this->dbPassword = $dbPassword;
$this->user = $user;
}
@ -27,23 +29,21 @@ class KufrikConnectionFactory
$identity = $this->user->getIdentity();
// 1. Dynamické načtení z identity (každý uživatel může mít jiný server i DB!)
$host = $identity->db_host ?? $this->baseConfig['host'];
$databaseName = $identity->db_name ?? $this->baseConfig['default_db'];
$firmaId = $identity->firma_id ?? null;
// Server i Databáze se načtou z Identity přihlášeného uživatele (z tabulky LOGIN)
$host = $identity->server ?? null;
$databaseName = $identity->database ?? null;
if (!$firmaId) {
throw new \LogicException('Uživatel nemá zvolenou žádnou aktivní firmu.');
if (empty($host) || empty($databaseName)) {
throw new \LogicException('Přihlášený uživatel nemá v tabulce LOGIN definovaný SERVER nebo DATABASE.');
}
// 2. Sestavení DSN pro konkrétní SQL Server a DB
$dsn = "sqlsrv:Server={$host};Database={$databaseName}";
// Dynamický DSN řetězec
$dsn = "sqlsrv:Server={$host};Database={$databaseName};LoginTimeout=3;";
// Přihlašovací údaje k SQL serveru (buď společné ze souboru, nebo dynamické)
$connection = new Connection(
return new Connection(
$dsn,
$this->baseConfig['username'],
$this->baseConfig['password'],
$this->dbUser,
$this->dbPassword,
[
'lazy' => false,
'driverOptions' => [
@ -51,11 +51,6 @@ class KufrikConnectionFactory
]
]
);
// 3. Nastavení RLS kontextu pro firmu
$connection->query('EXEC sp_set_session_context @key = N\'FirmaId\', @value = ?', $firmaId);
return $connection;
}
public function createExplorer(): Explorer

View File

@ -28,8 +28,8 @@ class Authenticator implements IAuthenticator
// 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_NAME, CONNECTION_STRING_KEY
FROM dbo.UZIVATEL
'SELECT ID, EMAIL, HESLO_HASH, [DATABASE], [SERVER], FIRMA_ID
FROM dbo.LOGIN
WHERE EMAIL = ? AND AKTIVNI = 1',
$email
);
@ -43,18 +43,18 @@ class Authenticator implements IAuthenticator
// 2. Aktualizace času posledního přihlášení
$this->masterDb->query(
'UPDATE dbo.UZIVATEL SET POSLEDNI_PRIHLASENI = SYSDATETIME() WHERE ID = ?',
'UPDATE dbo.LOGIN 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
);
// $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;
//$firmaId = $firmaRow ? $firmaRow->FIRMA_ID : $row->ID;
// 4. Předání dat do Nette Identity
return new SimpleIdentity(
@ -62,10 +62,9 @@ class Authenticator implements IAuthenticator
null, // role
[
'email' => $row->EMAIL,
'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,
'database' => $row->DATABASE, // Nulllable -> KufrikConnectionFactory použije default_db
'server' => $row->SERVER,
'firma_id' => (int) $row->FIRMA_ID, // Zpřístupní $user->identity->firma_id
]
);
}

View File

@ -21,7 +21,7 @@
</head>
<body>
<!-- 1. HORNÍ LIŠTA -->
<!-- 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>
@ -34,23 +34,34 @@
<!-- Hlavní kategorie - viditelné pouze pro přihlášené -->
{if $user->isLoggedIn()}
<ul class="navbar-nav me-auto nav nav-tabs" id="mainCategoryTabs" role="tablist">
<!-- FAKTURY -->
<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 n:class="nav-link, $presenter->isLinkCurrent('Faktura:*') ? 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>
<!-- ZÁKAZNÍCI -->
<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 n:class="nav-link, $presenter->isLinkCurrent('Zakaznik:*') ? active"
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>
<!-- MOJE OSVČ -->
<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 n:class="nav-link, $presenter->isLinkCurrent('Firma:*') ? active"
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>
<!-- KUFŘÍK -->
<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 n:class="nav-link, $presenter->isLinkCurrent('Uzivatel:*') ? active"
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>
@ -95,7 +106,7 @@
<div class="tab-content" id="actionToolbarContent">
<!-- FAKTURY -->
<div class="tab-pane fade show active" id="toolbar-invoices" role="tabpanel">
<div n:class="tab-pane, fade, $presenter->isLinkCurrent('Faktura:*') ? '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">
@ -140,7 +151,7 @@
</div>
<!-- ZÁKAZNÍCI -->
<div class="tab-pane fade" id="toolbar-customers" role="tabpanel">
<div n:class="tab-pane, fade, $presenter->isLinkCurrent('Zakaznik:*') ? 'show active'" id="toolbar-customers" role="tabpanel">
<div class="action-buttons">
<button type="button" class="btn btn-outline-primary btn-action">
<i class="bi bi-people-fill"></i>
@ -154,12 +165,12 @@
</div>
<!-- MOJE OSVČ -->
<div class="tab-pane fade" id="toolbar-profile" role="tabpanel">
<div n:class="tab-pane, fade, $presenter->isLinkCurrent('Firma:*') ? 'show active'" id="toolbar-profile" role="tabpanel">
<div class="action-buttons">
<button type="button" class="btn btn-outline-primary btn-action">
<a n:href="Firma:edit" n:class="btn, btn-action, $presenter->isLinkCurrent('Firma:edit') ? 'btn-primary active' : 'btn-outline-primary'">
<i class="bi bi-pencil-square"></i>
<span>Upravit moje OSVČ</span>
</button>
</a>
<button type="button" class="btn btn-outline-secondary btn-action">
<i class="bi bi-gear-fill"></i>
<span>Nastavení</span>
@ -168,8 +179,12 @@
</div>
<!-- KUFŘÍK -->
<div class="tab-pane fade" id="toolbar-briefcase" role="tabpanel">
<div n:class="tab-pane, fade, $presenter->isLinkCurrent('Uzivatel:*') ? 'show active'" id="toolbar-briefcase" role="tabpanel">
<div class="action-buttons">
<a n:href="Uzivatel:edit" n:class="btn, btn-action, $presenter->isLinkCurrent('Uzivatel:edit') ? 'btn-primary active' : 'btn-outline-primary'">
<i class="bi bi-person-gear"></i>
<span>Profil a připojení</span>
</a>
<button type="button" class="btn btn-outline-info btn-action">
<i class="bi bi-question-circle-fill"></i>
<span>Nápověda</span>
@ -188,6 +203,31 @@
</div>
{/if}
<!-- Toasty -->
<div n:foreach="$flashes as $flash" class="alert alert-{$flash->type} alert-dismissible fade show auto-dismiss mb-1 shadow-sm" role="alert">
<i class="bi bi-check-circle-fill me-2"></i>
<strong>{$flash->message}</strong>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Najde všechny alerty s třídou auto-dismiss
const alerts = document.querySelectorAll('.alert.auto-dismiss');
alerts.forEach(alert => {
setTimeout(() => {
// Přidá plynulé vytracení přes Bootstrap třídu nebo JS transition
alert.classList.remove('show');
alert.classList.add('fade');
// Po dokončení animace prvek úplně odstraní z DOMu
setTimeout(() => alert.remove(), 500);
}, 3000); // 3000 ms = 3 vteřiny
});
});
</script>
<!-- 3. HLAVNÍ OBSAH -->
<main class="main-content">
<div class="content-card">

View File

@ -0,0 +1,25 @@
<?php
namespace App\UI\Faktura;
use App\UI\BasePresenter;
use Nette\Database\Explorer;
class FakturaPresenter extends BasePresenter
{
public function __construct(
private Explorer $kufrikExplorer
) {
parent::__construct();
}
public function actionDefault(): void
{
// Seznam faktur
}
public function actionAdd(): void
{
// Přidání faktury
}
}

View File

@ -0,0 +1,128 @@
<?php
namespace App\UI\Firma;
use App\UI\BasePresenter;
use Nette\Application\UI\Form;
use Nette\Database\Explorer;
class FirmaPresenter extends BasePresenter
{
/**
* Vstříkne se dynamický klientský Explorer
*/
public function __construct(
private Explorer $kufrikExplorer
) {
parent::__construct();
}
public function actionEdit(): void
{
$firmaId = $this->getUser()->getIdentity()->firma_id ?? null;
if (!$firmaId) {
$this->flashMessage('K účtu není přiřazena žádná aktivní firma.', 'danger');
$this->redirect('Home:');
}
$firma = $this->kufrikExplorer->table('FIRMA')->get($firmaId);
if (!$firma) {
$this->flashMessage('Firma nebyla v databázi nalezena.', 'danger');
$this->redirect('Home:');
}
// Predvyplnění formuláře
$this['firmaForm']->setDefaults([
'nazev' => $firma->NAZEV,
'ic' => $firma->IC,
'dic' => $firma->DIC,
'platce_dph' => (bool) $firma->PLATCE_DPH,
'telefon' => $firma->TELEFON,
'email' => $firma->EMAIL,
'ulice' => $firma->ULICE,
'mesto' => $firma->MESTO,
'psc' => $firma->PSC,
'cislo_uctu' => $firma->CISLO_UCTU,
'banka_kod' => $firma->BANKA_KOD,
'iban' => $firma->IBAN,
'bic' => $firma->BIC,
]);
}
protected function createComponentFirmaForm(): Form
{
$form = new Form();
// Základní údaje
$form->addText('nazev', 'Název firmy / OSVČ:')
->setRequired('Zadejte název firmy nebo Vaše jméno.');
$form->addText('ic', 'IČO:')
->setMaxLength(8);
$form->addText('dic', 'DIČ:')
->setMaxLength(12);
$form->addCheckbox('platce_dph', 'Jsem plátce DPH');
// Kontakty
$form->addText('telefon', 'Telefon:');
$form->addEmail('email', 'E-mail:');
// Sídlo
$form->addText('ulice', 'Ulice a ČP:');
$form->addText('mesto', 'Město:');
$form->addText('psc', 'PSČ:');
// Bankovní spojení
$form->addText('cislo_uctu', 'Číslo účtu:');
$form->addText('banka_kod', 'Kód banky:');
$form->addText('iban', 'IBAN:');
$form->addText('bic', 'BIC / SWIFT:');
$form->addSubmit('send', 'Uložit firmu');
$form->onSuccess[] = [$this, 'firmaFormSucceeded'];
return $form;
}
public function firmaFormSucceeded(Form $form, \stdClass $data): void
{
$firmaId = $this->getUser()->getIdentity()->firma_id;
try {
$this->kufrikExplorer->table('FIRMA')
->where('ID', $firmaId)
->update([
'NAZEV' => $data->nazev,
'IC' => $data->ic,
'DIC' => $data->dic,
'PLATCE_DPH' => $data->platce_dph ? 1 : 0,
'TELEFON' => $data->telefon,
'EMAIL' => $data->email,
'ULICE' => $data->ulice,
'MESTO' => $data->mesto,
'PSC' => $data->psc,
'CISLO_UCTU' => $data->cislo_uctu,
'BANKA_KOD' => $data->banka_kod,
'IBAN' => $data->iban,
'BIC' => $data->bic,
]);
// Potvrzení s časovým razítkem uložení
$casUlozeni = (new \DateTime())->format('H:i:s');
$this->flashMessage("Údaje firmy byly úspěšně uloženy v {$casUlozeni}.", 'success');
} catch (\Nette\Application\AbortException $e) {
throw $e;
} catch (\Throwable $e) {
$form->addError('Chyba při ukládání firmy: ' . $e->getMessage());
return;
}
$this->redirect('this');
}
}

107
app/UI/Firma/edit.latte Normal file
View File

@ -0,0 +1,107 @@
{block content}
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-md-10 col-lg-8">
<div class="content-card my-3">
<h2 class="h4 mb-3" style="color: var(--header-bg);">
<i class="bi bi-building me-2"></i>Moje OSVČ / Firma
</h2>
<hr style="border-color: var(--border-color); margin-bottom: 1.5rem;">
<form n:name="firmaForm" class="login-form">
<div n:if="$form->ownErrors" class="alert alert-danger mb-3">
<ul class="mb-0 ps-3">
<li n:foreach="$form->ownErrors as $error">{$error}</li>
</ul>
</div>
<!-- ZÁKLADNÍ ÚDAJE -->
<h6 class="fw-bold text-muted mb-3"><i class="bi bi-card-heading me-1"></i> Základní údaje</h6>
<div class="row">
<div class="col-md-6 form-group">
<label n:name="nazev">Název / Jméno OSVČ</label>
<input n:name="nazev" class="form-control">
</div>
<div class="col-md-3 form-group">
<label n:name="ic">IČO</label>
<input n:name="ic" class="form-control">
</div>
<div class="col-md-3 form-group">
<label n:name="dic">DIČ</label>
<input n:name="dic" class="form-control">
</div>
</div>
<div class="form-check mt-2 mb-3">
<input n:name="platce_dph" class="form-check-input">
<label n:name="platce_dph" class="form-check-label fw-bold">Plátce DPH</label>
</div>
<!-- KONTAKTY & SÍDLO -->
<h6 class="fw-bold text-muted mt-4 mb-3"><i class="bi bi-geo-alt me-1"></i> Sídlo a kontakty</h6>
<div class="row">
<div class="col-md-4 form-group">
<label n:name="ulice">Ulice a ČP</label>
<input n:name="ulice" class="form-control">
</div>
<div class="col-md-4 form-group">
<label n:name="mesto">Město</label>
<input n:name="mesto" class="form-control">
</div>
<div class="col-md-4 form-group">
<label n:name="psc">PSČ</label>
<input n:name="psc" class="form-control">
</div>
</div>
<div class="row mt-2">
<div class="col-md-6 form-group">
<label n:name="telefon">Telefon</label>
<input n:name="telefon" class="form-control">
</div>
<div class="col-md-6 form-group">
<label n:name="email">E-mail</label>
<input n:name="email" class="form-control">
</div>
</div>
<!-- BANKA -->
<h6 class="fw-bold text-muted mt-4 mb-3"><i class="bi bi-bank me-1"></i> Bankovní spojení</h6>
<div class="row">
<div class="col-md-8 form-group">
<label n:name="cislo_uctu">Číslo účtu</label>
<input n:name="cislo_uctu" class="form-control">
</div>
<div class="col-md-4 form-group">
<label n:name="banka_kod">Kód banky</label>
<input n:name="banka_kod" class="form-control">
</div>
</div>
<div class="row mt-2">
<div class="col-md-8 form-group">
<label n:name="iban">IBAN</label>
<input n:name="iban" class="form-control">
</div>
<div class="col-md-4 form-group">
<label n:name="bic">SWIFT / BIC</label>
<input n:name="bic" class="form-control">
</div>
</div>
<div class="d-flex justify-content-end gap-2 mt-4">
<button n:name="send" class="btn btn-success">
<i class="bi bi-check-lg me-1"></i> Uložit firmu
</button>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,25 @@
<?php
namespace App\UI\Uzivatel;
use App\UI\BasePresenter;
use Nette\Database\Explorer;
class UzivatelPresenter extends BasePresenter
{
public function __construct(
private Explorer $kufrikExplorer
) {
parent::__construct();
}
public function actionDefault(): void
{
}
public function actionAdd(): void
{
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\UI\Zakaznik;
use App\UI\BasePresenter;
use Nette\Database\Explorer;
class ZakaznikPresenter extends BasePresenter
{
public function __construct(
private Explorer $kufrikExplorer
) {
parent::__construct();
}
public function actionDefault(): void
{
// Seznam zákazníků
}
public function actionAdd(): void
{
// Přidání zákazníka
}
}

View File

@ -1,11 +1,8 @@
parameters:
# Přihlašovací údaje ke klientským DB serverům
mssql:
host: "localhost" # Výchozí záložní host
username: "kufrik"
password: "giteaKufrik_2026!"
default_db: "kufrik"
# Přihlašovací údaje k MS SQL Serveru pro klientské DB
clientDb:
user: kufrik
password: 'giteaKufrik_2026!'
application:
errorPresenter:
@ -15,7 +12,8 @@ application:
database:
dsn: 'sqlsrv:server=localhost;Database=kufrik;LoginTimeout=3;'
# Přihlašovací údaje k master DB
dsn: 'sqlsrv:server=localhost;Database=kufrik_master;LoginTimeout=3;'
user: 'kufrik'
password: 'giteaKufrik_2026!'
autowired: false # Vypnuto, abychom jako výchozí Explorer používali kufrikExplorer

View File

@ -6,16 +6,14 @@ services:
# Authenticator si vyžádá připojení k Master DB
- App\Model\Security\Authenticator(@database.default.connection)
# Konfigurace pro klientskou KufrikConnectionFactory
# 1. Factory pro klientskou databázi
kufrikConnectionFactory:
factory: App\Model\Database\KufrikConnectionFactory
arguments:
baseConfig: %mssql%
factory: App\Model\Database\KufrikConnectionFactory(%clientDb.user%, %clientDb.password%)
# Hlavní databázový Explorer pro aplikaci
# 2. Samotný Explorer pro klientská data (vyvolá metodu createExplorer)
kufrikExplorer:
type: Nette\Database\Explorer
factory: @App\Model\Database\KufrikConnectionFactory::createExplorer()
factory: @kufrikConnectionFactory::createExplorer()
search:
- in: %appDir%

View File

@ -1,8 +1,80 @@
use master
GO
DROP TABLE [kufrik]
-- ==========================================
-- MASTER databáze pro přihlašování a rozřazování
-- ==========================================
CREATE DATABASE [kufrik_master]
-- CONTAINMENT = NONE
-- ON PRIMARY
--( NAME = N'kufrik', FILENAME = N'/var/opt/mssql/data/kufrik.mdf' , SIZE = 8192KB , FILEGROWTH = 65536KB )
-- LOG ON
--( NAME = N'kufrik_log', FILENAME = N'/var/opt/mssql/data/kufrik_log.ldf' , SIZE = 8192KB , FILEGROWTH = 65536KB )
GO
ALTER DATABASE [kufrik_master] SET COMPATIBILITY_LEVEL = 160
GO
-- Pokud je AUTO_CLOSE zapnuté (ON), databáze se po odpojení posledního uživatele kompletně uzavře a uvolní paměť. Při dalším požadavku se pak musí znovu otevírat, což způsobuje výrazné zpoždění (latenci) prvního dotazu a zbytečnou zátěž na disk a CPU.
ALTER DATABASE [kufrik_master] SET AUTO_CLOSE OFF WITH NO_WAIT;
GO
-- user:
USE [master]
GO
CREATE LOGIN [kufrik] WITH PASSWORD=N'giteaKufrik_2026!', DEFAULT_DATABASE=[master], CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF
GO
USE [kufrik_master]
GO
CREATE USER [kufrik] FOR LOGIN [kufrik]
GO
ALTER USER [kufrik] WITH DEFAULT_SCHEMA=[dbo]
GO
ALTER ROLE [db_owner] ADD MEMBER [kufrik]
GO
-- TABULKA LOGIN
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[LOGIN]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[LOGIN] (
[ID] [int] IDENTITY(1,1) NOT NULL,
[EMAIL] [nvarchar](100) NOT NULL,
[HESLO_HASH] [nvarchar](255) NOT NULL,
[POSLEDNI_PRIHLASENI] [datetime2](7) NULL,
[AKTIVNI] [bit] NOT NULL CONSTRAINT [DF_LOGIN_AKTIVNI] DEFAULT 1,
[DATABASE] [nvarchar](128) NOT NULL,
[SERVER] [nvarchar](100) NOT NULL,
[FIRMA_ID] [int] NOT NULL, -- vazna na tabulku FIRMA v datové DB
CONSTRAINT [PK_LOGIN] PRIMARY KEY CLUSTERED ([ID] ASC),
CONSTRAINT [UQ_LOGIN_EMAIL] UNIQUE NONCLUSTERED ([EMAIL] ASC)
);
END
GO
SET IDENTITY_INSERT dbo.[LOGIN] ON;
INSERT INTO dbo.[LOGIN] (
[ID],
[EMAIL],
[HESLO_HASH],
[DATABASE],
[SERVER],
[FIRMA_ID]
)
VALUES (
1,
N'admin@kmetix.cz',
-- Heslo 'admin1234' (platný BCrypt hash pro Nette Passwords)
N'$2y$12$9SAWctZKp/JpYl7K.awSRehXOXVyRpu0ofReqvbm1jVjvdz41zH7a',
N'kufrik',
N'localhost',
1 -- Odkaz na FIRMA.ID = 1 v klientské databázi 'kufrik'
);
SET IDENTITY_INSERT dbo.[LOGIN] OFF;
GO
-- ==========================================
-- DATOVÁ databáze
-- ==========================================
CREATE DATABASE [kufrik]
-- CONTAINMENT = NONE
@ -13,16 +85,11 @@ CREATE DATABASE [kufrik]
GO
ALTER DATABASE [kufrik] SET COMPATIBILITY_LEVEL = 160
GO
--Pokud je AUTO_CLOSE zapnuté (ON), databáze se po odpojení posledního uživatele kompletně uzavře a uvolní paměť. Při dalším požadavku se pak musí znovu otevírat, což způsobuje výrazné zpoždění (latenci) prvního dotazu a zbytečnou zátěž na disk a CPU.
ALTER DATABASE [kufrik] SET AUTO_CLOSE OFF WITH NO_WAIT;
GO
--user:
USE [master]
GO
CREATE LOGIN [kufrik] WITH PASSWORD=N'giteaKufrik_2026!', DEFAULT_DATABASE=[master], CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF
GO
--user (stejný i pro master DB):
USE [kufrik]
GO
CREATE USER [kufrik] FOR LOGIN [kufrik]
@ -32,40 +99,25 @@ GO
ALTER ROLE [db_owner] ADD MEMBER [kufrik]
GO
-- 1. PŘIHLAŠOVACÍ ÚČET (Fyzická osoba)
CREATE TABLE [dbo].[UZIVATEL] (
[ID] [int] IDENTITY(1,1) NOT NULL,
[EMAIL] [nvarchar](100) NOT NULL,
[HESLO_HASH] [nvarchar](255) NOT NULL,
[JMENO] [nvarchar](100) NULL,
[PRIJMENI] [nvarchar](100) NULL,
[TELEFON] [nvarchar](20) NULL,
[POSLEDNI_PRIHLASENI] [datetime2](7) NULL,
[AKTIVNI] [bit] NOT NULL DEFAULT 1,
CONSTRAINT [PK_UZIVATEL] PRIMARY KEY CLUSTERED ([ID] ASC),
CONSTRAINT [UQ_UZIVATEL_EMAIL] UNIQUE ([EMAIL])
);
-- ==========================================
-- 1. FIRMA HLAVNÍ TABULKA - vázaná na LOGIN z master_DB (taky bude tenant - izolace dat)
-- ==========================================
ALTER TABLE [dbo].[UZIVATEL] ADD
-- Identifikátor nebo název cílové databáze
[DATABASE_NAME] [nvarchar](128) NULL,
-- Volitelně celý connection string (nebo klíč do konfigurace aplikace)
[CONNECTION_STRING_KEY] [nvarchar](100) NULL;
GO
-- 2. FIRMA / OSVČ (Fakturační jednotka = TENANT)
CREATE TABLE [dbo].[FIRMA] (
[ID] [int] IDENTITY(1,1) NOT NULL,
[NAZEV] [nvarchar](255) NOT NULL, -- Jméno a příjmení OSVČ nebo název s.r.o.
[IC] [nvarchar](8) NULL,
[DIC] [nvarchar](12) NULL,
[PLATCE_DPH] [bit] NOT NULL DEFAULT 1,
[PLATCE_DPH] [bit] NOT NULL DEFAULT 0,
-- Kontaktní údaje
[TELEFON] [nvarchar](20) NULL,
[EMAIL] [nvarchar](100) NULL,
-- Sídlo
[ULICE] [nvarchar](100) NULL,
[MESTO] [nvarchar](100) NULL,
[PSC] [nvarchar](10) NULL,
[ZEME_KOD] [nvarchar](2) NOT NULL DEFAULT 'CZ',
-- Banka pro fakturaci
[CISLO_UCTU] [nvarchar](30) NULL,
@ -73,19 +125,8 @@ CREATE TABLE [dbo].[FIRMA] (
[IBAN] [nvarchar](34) NULL,
[BIC] [nvarchar](11) NULL,
[DATUM_VYTVORENI] [datetime2](7) NOT NULL DEFAULT SYSDATETIME(),
CONSTRAINT [PK_FIRMA] PRIMARY KEY CLUSTERED ([ID] ASC)
);
-- 3. VAZBA UŽIVATEL <-> FIRMA (Práva k firmám)
CREATE TABLE [dbo].[UZIVATEL_FIRMA] (
[UZIVATEL_ID] [int] NOT NULL,
[FIRMA_ID] [int] NOT NULL,
[ROLE] [nvarchar](20) NOT NULL DEFAULT 'OWNER', -- 'OWNER', 'ACCOUNTANT', 'READONLY'
CONSTRAINT [PK_UZIVATEL_FIRMA] PRIMARY KEY CLUSTERED ([UZIVATEL_ID], [FIRMA_ID]),
CONSTRAINT [FK_UF_UZIVATEL] FOREIGN KEY ([UZIVATEL_ID]) REFERENCES [dbo].[UZIVATEL] ([ID]),
CONSTRAINT [FK_UF_FIRMA] FOREIGN KEY ([FIRMA_ID]) REFERENCES [dbo].[FIRMA] ([ID])
);
-- ==========================================
-- 1. ZÁKAZNÍCI / ODBĚRATELÉ (vázaní na Firmu)
@ -240,41 +281,47 @@ GO
-- ==========================================
-- Cvičná data a UZIVATEL
-- Cvičná data FIRMA
-- ==========================================
-- 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)
IF NOT EXISTS (SELECT 1 FROM dbo.[FIRMA] WHERE [ID] = 1)
BEGIN
INSERT INTO dbo.UZIVATEL (
[EMAIL],
[HESLO_HASH],
[JMENO],
[PRIJMENI],
SET IDENTITY_INSERT dbo.[FIRMA] ON;
INSERT INTO dbo.[FIRMA] (
[ID],
[NAZEV],
[IC],
[DIC],
[PLATCE_DPH],
[TELEFON],
[DATABASE_NAME],
[CONNECTION_STRING_KEY],
[AKTIVNI]
[EMAIL],
[ULICE],
[MESTO],
[PSC],
[CISLO_UCTU],
[BANKA_KOD],
[IBAN],
[BIC]
)
VALUES (
@Email,
@HesloHash,
N'Pavel',
N'Kowalski',
1,
N'Pavel Kowalski - Kmetix Systems',
N'12345678',
N'CZ12345678',
1,
N'+420123456789',
N'', -- Cílová databáze pro klientská data
NULL,
1 -- AKTIVNI = 1
N'admin@kmetix.cz',
N'Nádražní 123',
N'Liberec',
N'46001',
N'123456789/0800',
N'0800',
N'CZ6508000000000123456789',
N'GIGACZPX'
);
PRINT 'Uživatel admin@kmetix.cz byl úspěšně vytvořen s heslem: admin1234';
SET IDENTITY_INSERT dbo.[FIRMA] OFF;
PRINT N'Cvičná firma ID 1 byla úspěšně vložena do klientské DB.';
END
ELSE
BEGIN
PRINT 'Uživatel s tímto e-mailem již v databázi existuje.';
END
GO
GO