diff --git a/app/Core/DB/KufrikConnectin.php b/app/Core/DB/KufrikConnectin.php deleted file mode 100644 index e69de29..0000000 diff --git a/app/Model/Database/KufrikConnectionFactory.php b/app/Model/Database/KufrikConnectionFactory.php new file mode 100644 index 0000000..34e2213 --- /dev/null +++ b/app/Model/Database/KufrikConnectionFactory.php @@ -0,0 +1,69 @@ +baseConfig = $baseConfig; + $this->user = $user; + } + + public function createConnection(): Connection + { + if (!$this->user->isLoggedIn()) { + throw new \LogicException('Uživatel není přihlášen.'); + } + + $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; + + if (!$firmaId) { + throw new \LogicException('Uživatel nemá zvolenou žádnou aktivní firmu.'); + } + + // 2. Sestavení DSN pro konkrétní SQL Server a DB + $dsn = "sqlsrv:Server={$host};Database={$databaseName}"; + + // Přihlašovací údaje k SQL serveru (buď společné ze souboru, nebo dynamické) + $connection = new Connection( + $dsn, + $this->baseConfig['username'], + $this->baseConfig['password'], + [ + 'lazy' => false, + 'driverOptions' => [ + \PDO::SQLSRV_ATTR_ENCODING => \PDO::SQLSRV_ENCODING_UTF8, + ] + ] + ); + + // 3. Nastavení RLS kontextu pro firmu + $connection->query('EXEC sp_set_session_context @key = N\'FirmaId\', @value = ?', $firmaId); + + return $connection; + } + + public function createExplorer(): Explorer + { + $connection = $this->createConnection(); + $storage = new MemoryStorage(); + $structure = new Structure($connection, $storage); + + return new Explorer($connection, $structure); + } +} \ No newline at end of file diff --git a/app/Model/Security/Authentificator.php b/app/Model/Security/Authentificator.php new file mode 100644 index 0000000..6a83b00 --- /dev/null +++ b/app/Model/Security/Authentificator.php @@ -0,0 +1,50 @@ +masterDb = $masterDb; + $this->passwords = $passwords; + } + + public function authenticate(string $user, string $password): SimpleIdentity + { + // 1. Dotaz do centrální Master DB + $row = $this->masterDb->fetch( + 'SELECT ID, EMAIL, HESLO_HASH, JMENO, PRIJMENI, DATABASE_HOST, DATABASE_NAME, DEFAULT_FIRMA_ID + FROM dbo.UZIVATEL + WHERE EMAIL = ? AND AKTIVNI = 1', + $user + ); + + 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 + return new SimpleIdentity( + $row->ID, + null, // role (volitelně) + [ + '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, + ] + ); + } +} \ No newline at end of file diff --git a/app/UI/Sign/SignPresenter.php b/app/UI/Sign/SignPresenter.php index 63c48b6..c78535a 100644 --- a/app/UI/Sign/SignPresenter.php +++ b/app/UI/Sign/SignPresenter.php @@ -1,136 +1,60 @@ addEmail('email', 'E-mail:') + ->setRequired('Zadejte prosím váš e-mail.') + ->setHtmlAttribute('placeholder', 'jan@novak.cz'); - protected function createComponentSignInForm(): Form - { - $form = new Form(); - $form->addText('server', 'Server:') - ->setRequired('Prosím vyplňte server.') - ->setValue(@$_COOKIE['server']); + $form->addPassword('password', 'Heslo:') + ->setRequired('Zadejte prosím heslo.'); - $form->addText('username', 'Uživatelské jméno:') - ->setRequired('Prosím vyplňte své uživatelské jméno.') - ->setValue(@$_COOKIE['user']); + $form->addSubmit('send', 'Přihlásit se'); - $form->addPassword('password', 'Heslo:'); - // setRequired nepoužívat!! kvůli soft logoutu!!! - //->setRequired('Prosím vyplňte své heslo.'); + $form->onSuccess[] = [$this, 'signInFormSucceeded']; - $form->addSubmit('send', 'Přihlásit'); - //$form->addProtection(); //ochrana pomocí TOKENu + return $form; + } - $form->onSuccess[] = $this->signInFormSucceeded(...); - return $form; - } + /** + * Zpracování formuláře po odeslání + */ + public function signInFormSucceeded(Form $form, \stdClass $data): void + { + try { + // Zavolá náš Authenticator spojený s Master DB + $this->getUser()->login($data->email, $data->password); - private function signInFormSucceeded(Form $form, LoginFormData $data): void //moje vlastní třída LoginFormData - { - try { - // kontrola serverů: - if (!Funkce::is_cloud_ip($data->server)) { - error_log("QR Login - neplatny server.", 0); //log je ve var/log/apache2/error.log (fail2ban) - $form->addError('Neznámý server.'); - return; - } + $this->flashMessage('Přihlášení proběhlo úspěšně.', 'success'); + + // Přesměrování do hlavní části aplikace (např. HomePresenter) + $this->redirect('Home:'); - // sestav Credentials: - $credentials = new ServerCredentials($data->server); + } catch (AuthenticationException $e) { + $form->addError('Nespravný e-mail nebo heslo.'); + } + } - $autentifikator = new MujAutentifikator($credentials, $this->session); // naše vlastní autentifikace! - $this->getUser()->setAuthenticator($autentifikator); // musíme ji registrovat! - - $blacklist_check = $this->blackList->blacklist_check($data->server); - bdump($blacklist_check); - if (is_null($blacklist_check)) { - //nic, jako by prošel - } else { - // $blacklist_check->vysledek Returns 1, pokud není blacklistu. Když se tam nachází, tak 0. - // $blacklist_check->expire_date Kdy končí. - // $blacklist_check->expired_text Co jim zahlásíš. - if ($blacklist_check->vysledek == 0) { - setcookie("salt", "", time() - 3600); // smažeme v tomto případě kukínu ... - if (empty($blacklist_check->expired_text)) - throw new BlackListException("Licence pro použití PečAppky vypršela, nebo nám dlužíte peníze."); - else - throw new BlackListException($blacklist_check->expired_text); // custom text z blacklist DB - } - } - - // uložené heslo? - if (empty($data->password) and isset($_COOKIE['salt'])) { - $password = Funkce::decrypt_password($_COOKIE['salt']); - } else { - $password = $data->password; - } - // validace přihlášení je v třídě MujAutentifikator->authenticate(...) - $this->getUser()->login($data->username, $password); - - // kam potom? - $this->restoreRequest($this->backlink); // vrátit se na požadovanou stránku - $this->redirect(':Ctecka:'); // jinak přejdi sem - } catch (Nette\Database\ConnectionException) { - error_log("QR Login - neplatne prihlaseni.", 0); //log je ve var/log/apache2/error.log (fail2ban) - $form->addError('Neplatné přihlášení.'); - } catch (Nette\Security\AuthenticationException $e) { - error_log("QR Login - neplatne prihlaseni.", 0); //log je ve var/log/apache2/error.log (fail2ban) - $form->addError($e->getMessage()); - } catch (BlackListException $e2) { - $form->addError($e2->getMessage()); - } - } - - public function renderIn(): void - { - // ukázat či neukázat pole pro heslo? - $this->template->salt = isset($_COOKIE['salt']); - } - - /** - * Normalní logout. - * @return void - */ - public function actionOut(): void - { - $this->getUser()->logout(); - $this->flashMessage('Odhlášení bylo úspěšné.'); - $this->redirect('Sign:in'); - } - - /** - * Tvrdý logout. - * @return void - */ - public function actionHardOut(): void - { - $this->getUser()->logout(); - setcookie("salt", "", time() - 3600); // smažeme v tomto případě kukínu ... - - // smažeme vybranou DB ze session: - if ($this->session->hasSection(Konstanty::sectionDatabase)) { - $section = $this->session->getSection(Konstanty::sectionDatabase); - $section->remove(Konstanty::sessionCurrentDatabase); - } - - $this->flashMessage('Odhlášení bylo úspěšné.'); - $this->redirect('Sign:in'); - } + /** + * Akce pro odhlášení + */ + public function actionOut(): void + { + $this->getUser()->logout(true); // smaže i identitu + $this->flashMessage('Byli jste úspěšně odhlášeni.', 'info'); + $this->redirect('in'); + } } \ No newline at end of file diff --git a/config/common.neon b/config/common.neon index 266c20b..7f30097 100644 --- a/config/common.neon +++ b/config/common.neon @@ -1,4 +1,10 @@ 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" application: @@ -9,10 +15,10 @@ application: database: - dsn: 'sqlsrv:server=kmetix.cz;Database=kufrik;LoginTimeout=6;' + dsn: 'sqlsrv:server=localhost;Database=kufrik;LoginTimeout=3;' user: 'kufrik' - password: 'giteaKufrik_2024' - + password: 'giteaKufrik_2026!' + autowired: false # Vypnuto, abychom jako výchozí Explorer používali kufrikExplorer latte: strictTypes: yes diff --git a/config/services.neon b/config/services.neon index 7e90c2a..161c093 100644 --- a/config/services.neon +++ b/config/services.neon @@ -3,6 +3,20 @@ services: security.passwords: Nette\Security\Passwords(::PASSWORD_BCRYPT, [cost: 12]) + # Authenticator si vyžádá připojení k Master DB + - App\Model\Security\Authenticator(@database.default.connection) + + # Konfigurace pro klientskou KufrikConnectionFactory + kufrikConnectionFactory: + factory: App\Model\Database\KufrikConnectionFactory + arguments: + baseConfig: %mssql% + + # Hlavní databázový Explorer pro aplikaci + kufrikExplorer: + type: Nette\Database\Explorer + factory: @App\Model\Database\KufrikConnectionFactory::createExplorer() + search: - in: %appDir% classes: diff --git a/sql/KufrikDB.sql b/sql/KufrikDB.sql index 15b47e7..80cea68 100644 --- a/sql/KufrikDB.sql +++ b/sql/KufrikDB.sql @@ -1,206 +1,239 @@ -CREATE DATABASE [kufrik] - 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 ) +use master GO -ALTER DATABASE [kufrik] SET COMPATIBILITY_LEVEL = 150 + +DROP TABLE [kufrik] +GO + +CREATE DATABASE [kufrik] +-- 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] 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_2024', DEFAULT_DATABASE=[master], CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF -GO -use [master]; +CREATE LOGIN [kufrik] WITH PASSWORD=N'giteaKufrik_2026!', DEFAULT_DATABASE=[master], CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF GO USE [kufrik] GO CREATE USER [kufrik] FOR LOGIN [kufrik] -GO -USE [kufrik] -GO +GO ALTER USER [kufrik] WITH DEFAULT_SCHEMA=[dbo] GO -USE [kufrik] -GO ALTER ROLE [db_owner] ADD MEMBER [kufrik] GO -USE [kufrik] -GO -/****** Object: Table [dbo].[FAKTURA] Script Date: 31.07.2026 9:05:46 ******/ -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -CREATE TABLE [dbo].[FAKTURA]( - [ID] [int] IDENTITY(15,1) NOT NULL, - [DATUM_VYSTAVENI] [datetime] NOT NULL, - [DATUM_SPLATNOSTI] [datetime] NOT NULL, - [ZAKAZNIK] [int] NOT NULL, - [CISLO] [nvarchar](20) NOT NULL, - [D_PRAJM] [nvarchar](511) NULL, - [D_ULICE] [nvarchar](50) NULL, - [D_MESTO] [nvarchar](50) NULL, - [D_PSC] [nvarchar](50) NULL, - [D_IC] [nvarchar](8) NULL, - [D_DIC] [nvarchar](10) NULL, - [D_BANKA] [nvarchar](100) NULL, - [D_TELEFON] [nvarchar](15) NULL, - [D_EMAIL] [nvarchar](100) NULL, - [D_WEB] [nvarchar](100) NULL, - [O_PRAJM] [nvarchar](511) NULL, - [O_ULICE] [nvarchar](50) NULL, - [O_MESTO] [nvarchar](50) NULL, - [O_PSC] [nvarchar](5) NULL, - [O_IC] [nvarchar](8) NULL, - [O_DIC] [nvarchar](10) NULL, - [FORMA_UHRADY] [tinyint] NOT NULL, - [CISLO_ABS] [int] NOT NULL, - [CINNOSTI] [ntext] NULL, - CONSTRAINT [PK_FAKTURA] PRIMARY KEY CLUSTERED -( - [ID] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] -) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] -GO -/****** Object: Table [dbo].[KONFIGURACE] Script Date: 31.07.2026 9:05:46 ******/ -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -CREATE TABLE [dbo].[KONFIGURACE]( - [ID] [int] IDENTITY(2,1) NOT NULL, - [DATA] [ntext] NULL, - CONSTRAINT [PK_KONFIGURACE] PRIMARY KEY CLUSTERED -( - [ID] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] -) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] -GO -/****** Object: Table [dbo].[PLATBA] Script Date: 31.07.2026 9:05:46 ******/ -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -CREATE TABLE [dbo].[PLATBA]( - [ID] [int] IDENTITY(11,1) NOT NULL, - [FAKTURA] [int] NOT NULL, - [CASTKA] [money] NOT NULL, - [DATUM] [datetime] NOT NULL, - [POZNAMKA] [ntext] NULL, - CONSTRAINT [PK_PLATBA] PRIMARY KEY CLUSTERED -( - [ID] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] -) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] -GO -/****** Object: Table [dbo].[POLOZKA] Script Date: 31.07.2026 9:05:46 ******/ -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -CREATE TABLE [dbo].[POLOZKA]( - [ID] [int] IDENTITY(24,1) NOT NULL, - [FAKTURA] [int] NOT NULL, - [NAZEV] [nvarchar](255) NOT NULL, - [POCET] [float] NOT NULL, - [CENA_JEDNOTKA] [money] NOT NULL, - [SLEVA] [real] NULL, - [CENA] [money] NOT NULL, - [JEDNOTKA] [nvarchar](5) NULL, - CONSTRAINT [PK_POLOZKA] PRIMARY KEY CLUSTERED -( - [ID] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] -) ON [PRIMARY] -GO -/****** Object: Table [dbo].[UZIVATEL] Script Date: 31.07.2026 9:05:46 ******/ -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -CREATE TABLE [dbo].[UZIVATEL]( - [ID] [int] IDENTITY(2,1) NOT NULL, - [JMENO] [nvarchar](255) NULL, - [PRIJMENI] [nvarchar](255) NOT NULL, - [ULICE] [nvarchar](50) NULL, - [MESTO] [nvarchar](50) NULL, - [PSC] [nvarchar](5) NULL, - [IC] [nvarchar](8) NULL, - [DIC] [nvarchar](10) NULL, - [TELEFON] [nvarchar](15) NULL, - [EMAIL] [nvarchar](100) NULL, - [BANKA] [nvarchar](100) NULL, - [WEB] [nvarchar](100) NULL, - CONSTRAINT [PK_UZIVATEL] PRIMARY KEY CLUSTERED -( - [ID] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] -) ON [PRIMARY] -GO -/****** Object: Table [dbo].[VERZE] Script Date: 31.07.2026 9:05:46 ******/ -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -CREATE TABLE [dbo].[VERZE]( - [ID] [int] IDENTITY(15,1) NOT NULL, - [VERZE] [smallint] NOT NULL, - [DATUM] [datetime] NOT NULL, - CONSTRAINT [PK_VERZE] PRIMARY KEY CLUSTERED -( - [ID] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY], - CONSTRAINT [UQ__VERZE__ID] UNIQUE NONCLUSTERED -( - [ID] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY], - CONSTRAINT [UQ__VERZE__VERZE] UNIQUE NONCLUSTERED -( - [VERZE] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] -) ON [PRIMARY] -GO -/****** Object: Table [dbo].[ZAKAZNIK] Script Date: 31.07.2026 9:05:46 ******/ -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO -CREATE TABLE [dbo].[ZAKAZNIK]( - [ID] [int] IDENTITY(2,1) NOT NULL, - [JMENO] [nvarchar](255) NULL, - [PRIJMENI] [nvarchar](255) NOT NULL, - [ULICE] [nvarchar](50) NULL, - [MESTO] [nvarchar](50) NULL, - [PSC] [nvarchar](5) NULL, - [IC] [nvarchar](8) NULL, - [DIC] [nvarchar](10) NULL, - [TELEFON] [nvarchar](15) NULL, - [EMAIL] [nvarchar](100) NULL, - CONSTRAINT [PK_ZAKAZNIK] PRIMARY KEY CLUSTERED -( - [ID] ASC -)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] -) ON [PRIMARY] -GO -ALTER TABLE [dbo].[FAKTURA] ADD DEFAULT ((1)) FOR [FORMA_UHRADY] -GO -ALTER TABLE [dbo].[FAKTURA] WITH CHECK ADD CONSTRAINT [FK_ZAKAZNIK] FOREIGN KEY([ZAKAZNIK]) -REFERENCES [dbo].[ZAKAZNIK] ([ID]) -GO -ALTER TABLE [dbo].[FAKTURA] CHECK CONSTRAINT [FK_ZAKAZNIK] -GO -ALTER TABLE [dbo].[PLATBA] WITH CHECK ADD CONSTRAINT [FK_PLATBA_FAKTURA] FOREIGN KEY([FAKTURA]) -REFERENCES [dbo].[FAKTURA] ([ID]) -GO -ALTER TABLE [dbo].[PLATBA] CHECK CONSTRAINT [FK_PLATBA_FAKTURA] -GO -ALTER TABLE [dbo].[POLOZKA] WITH CHECK ADD CONSTRAINT [FK_PLOZKA_FAKTURA] FOREIGN KEY([FAKTURA]) -REFERENCES [dbo].[FAKTURA] ([ID]) -GO -ALTER TABLE [dbo].[POLOZKA] CHECK CONSTRAINT [FK_PLOZKA_FAKTURA] +-- 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]) +); + +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, + + -- 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, + [BANKA_KOD] [nvarchar](10) NULL, + [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) +-- ========================================== +CREATE TABLE [dbo].[ZAKAZNIK]( + [ID] [int] IDENTITY(2,1) NOT NULL, + [FIRMA_ID] [int] NOT NULL, -- Izolace dat (Tenant) + [JMENO] [nvarchar](255) NULL, + [PRIJMENI] [nvarchar](255) NOT NULL, + [ULICE] [nvarchar](50) NULL, + [MESTO] [nvarchar](50) NULL, + [PSC] [nvarchar](5) NULL, + [IC] [nvarchar](8) NULL, + [DIC] [nvarchar](10) NULL, + [TELEFON] [nvarchar](15) NULL, + [EMAIL] [nvarchar](100) NULL, + [AKTIVNI] [bit] NOT NULL CONSTRAINT [DF_ZAKAZNIK_AKTIVNI] DEFAULT ((1)), + + CONSTRAINT [PK_ZAKAZNIK] PRIMARY KEY CLUSTERED ([ID] ASC), + CONSTRAINT [FK_ZAKAZNIK_FIRMA] FOREIGN KEY ([FIRMA_ID]) REFERENCES [dbo].[FIRMA] ([ID]) +); +GO + +CREATE NONCLUSTERED INDEX [IX_ZAKAZNIK_FIRMA_ID] ON [dbo].[ZAKAZNIK]([FIRMA_ID] ASC); +GO + +-- ========================================== +-- 2. HLAVIČKA FAKTURY +-- ========================================== +CREATE TABLE [dbo].[FAKTURA]( + [ID] [int] IDENTITY(15,1) NOT NULL, + [FIRMA_ID] [int] NOT NULL, -- Izolace dat (Tenant) + [ZAKAZNIK] [int] NULL, -- Vazba na adresář (volitelná, pro historii) + + [CISLO] [nvarchar](20) NOT NULL, + [CISLO_ABS] [int] NOT NULL, -- Sekvenční pořadové číslo v roce + [FORMA_UHRADY] [tinyint] NOT NULL CONSTRAINT [DF_FAKTURA_FORMA_UHRADY] DEFAULT ((1)), + + [DATUM_VYSTAVENI] [date] NOT NULL, + [DATUM_SPLATNOSTI] [date] NOT NULL, + [DATUM_DUZP] [date] NULL, -- Doplněno pro DPH plnění + + -- Snapshot Dodavatele (OSVČ / Firma v čase vystavení) + [D_PRAJM] [nvarchar](511) NULL, + [D_ULICE] [nvarchar](50) NULL, + [D_MESTO] [nvarchar](50) NULL, + [D_PSC] [nvarchar](50) NULL, + [D_IC] [nvarchar](8) NULL, + [D_DIC] [nvarchar](10) NULL, + [D_BANKA] [nvarchar](100) NULL, + [D_TELEFON] [nvarchar](15) NULL, + [D_EMAIL] [nvarchar](100) NULL, + [D_WEB] [nvarchar](100) NULL, + + -- Snapshot Odběratele (Zákazník v čase vystavení) + [O_PRAJM] [nvarchar](511) NULL, + [O_ULICE] [nvarchar](50) NULL, + [O_MESTO] [nvarchar](50) NULL, + [O_PSC] [nvarchar](5) NULL, + [O_IC] [nvarchar](8) NULL, + [O_DIC] [nvarchar](10) NULL, + + [CINNOSTI] [nvarchar](max) NULL, -- Nahrazeno ntext -> nvarchar(max) + [STAV] [nvarchar](20) NOT NULL CONSTRAINT [DF_FAKTURA_STAV] DEFAULT ('ISSUED'), + + CONSTRAINT [PK_FAKTURA] PRIMARY KEY CLUSTERED ([ID] ASC), + CONSTRAINT [FK_FAKTURA_FIRMA] FOREIGN KEY ([FIRMA_ID]) REFERENCES [dbo].[FIRMA] ([ID]), + CONSTRAINT [FK_FAKTURA_ZAKAZNIK] FOREIGN KEY ([ZAKAZNIK]) REFERENCES [dbo].[ZAKAZNIK] ([ID]), + + -- Číslo faktury je unikátní vždy jen v rámci jedné firmy + CONSTRAINT [UQ_FAKTURA_FIRMA_CISLO] UNIQUE ([FIRMA_ID], [CISLO]) +); +GO + +CREATE NONCLUSTERED INDEX [IX_FAKTURA_FIRMA_DATUM] ON [dbo].[FAKTURA]([FIRMA_ID] ASC, [DATUM_VYSTAVENI] DESC); +GO + +-- ========================================== +-- 3. POLOŽKY FAKTURY +-- ========================================== +CREATE TABLE [dbo].[POLOZKA]( + [ID] [int] IDENTITY(24,1) NOT NULL, + [FAKTURA] [int] NOT NULL, + [FIRMA_ID] [int] NOT NULL, -- Duplikováno pro rychlé filtry a RLS + + [NAZEV] [nvarchar](255) NOT NULL, + [POCET] [float] NOT NULL DEFAULT 1, + [JEDNOTKA] [nvarchar](10) NULL DEFAULT 'ks', + [CENA_JEDNOTKA] [money] NOT NULL, + [SLEVA] [real] NULL DEFAULT 0, + [CENA] [money] NOT NULL, -- Celková cena položky po slevě + + CONSTRAINT [PK_POLOZKA] PRIMARY KEY CLUSTERED ([ID] ASC), + CONSTRAINT [FK_POLOZKA_FAKTURA] FOREIGN KEY ([FAKTURA]) REFERENCES [dbo].[FAKTURA] ([ID]) ON DELETE CASCADE, + CONSTRAINT [FK_POLOZKA_FIRMA] FOREIGN KEY ([FIRMA_ID]) REFERENCES [dbo].[FIRMA] ([ID]) +); +GO + +CREATE NONCLUSTERED INDEX [IX_POLOZKA_FAKTURA] ON [dbo].[POLOZKA]([FAKTURA] ASC); +GO + +-- ========================================== +-- 4. ÚHRADY / PLATBY FAKTUR +-- ========================================== +CREATE TABLE [dbo].[PLATBA]( + [ID] [int] IDENTITY(11,1) NOT NULL, + [FAKTURA] [int] NOT NULL, + [CASTKA] [money] NOT NULL, + [DATUM] [datetime2](7) NOT NULL DEFAULT SYSDATETIME(), + [POZNAMKA] [nvarchar](max) NULL, -- Nahrazeno ntext -> nvarchar(max) + + CONSTRAINT [PK_PLATBA] PRIMARY KEY CLUSTERED ([ID] ASC), + CONSTRAINT [FK_PLATBA_FAKTURA] FOREIGN KEY ([FAKTURA]) REFERENCES [dbo].[FAKTURA] ([ID]) ON DELETE CASCADE +); +GO + +CREATE NONCLUSTERED INDEX [IX_PLATBA_FAKTURA] ON [dbo].[PLATBA]([FAKTURA] ASC); +GO + + +-- ========================================== +-- 5. SECURITY +-- ========================================== + +-- 1. Vytvoření schématu pro bezpečnostní objekty +CREATE SCHEMA Security; +GO + +-- 2. Predikátová funkce pro kontrolu FirmaId +CREATE FUNCTION Security.fn_SecurityPredicate(@FirmaId INT) +RETURNS TABLE +WITH SCHEMABINDING +AS +RETURN SELECT 1 AS fn_SecurityPredicate_result +WHERE + -- Vyhodnotí se jako pravda pouze pokud se FIRMA_ID řádku shoduje se SESSION_CONTEXTu + @FirmaId = CAST(SESSION_CONTEXT(N'FirmaId') AS INT); +GO + +-- 3. Vytvoření Security Policy pro tabulky +CREATE SECURITY POLICY Security.FirmaSecurityPolicy +ADD FILTER PREDICATE Security.fn_SecurityPredicate(FIRMA_ID) ON dbo.FAKTURA, +ADD BLOCK PREDICATE Security.fn_SecurityPredicate(FIRMA_ID) ON dbo.FAKTURA, + +ADD FILTER PREDICATE Security.fn_SecurityPredicate(FIRMA_ID) ON dbo.ZAKAZNIK, +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 \ No newline at end of file