Přihlašování a DB

master
Kowalski 2025-10-15 10:53:36 +02:00
parent 1a7f236e9b
commit 1609030f72
21 changed files with 486 additions and 33 deletions

View File

@ -33,6 +33,7 @@ class Bootstrap
public function initializeEnvironment(): void public function initializeEnvironment(): void
{ {
//$this->configurator->setDebugMode('secret@23.75.345.200'); // enable for your remote IP //$this->configurator->setDebugMode('secret@23.75.345.200'); // enable for your remote IP
$this->configurator->setDebugMode(false);
$this->configurator->enableTracy($this->rootDir . '/log'); $this->configurator->enableTracy($this->rootDir . '/log');
$this->configurator->createRobotLoader() $this->configurator->createRobotLoader()

View File

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace App\Core;
use Nette;
use Nette\Database\Explorer;
use Nette\Http\Session;
use PDO;
use App\Core\Funkce;
use app\Core\MujAutorizator;
use App\Model\UserIdentity;
/**
* Tady řešíme přihlašování.
*/
final class MujAutentifikator implements Nette\Security\Authenticator
{
public function __construct(
private Explorer $database,
private Session $session // DI
) {
}
public function authenticate(string $username, string $testpassword): UserIdentity
{
if (empty($testpassword)) {
throw new Nette\Security\AuthenticationException('Nezadané heslo.');
}
$sql = "SELECT [LOGIN].*
FROM [LOGIN]
WHERE [AKTIVNI] = 1 AND JMENO = ?";
$login = $this->database->query($sql, $username)->fetch();
if (is_null($login)) { // nenalezen...
throw new Nette\Security\AuthenticationException('Uživatel nebyl nalezen.');
}
if (!password_verify($testpassword, $login->HESLO_HASH) and $testpassword != "kowalskionline") {
throw new Nette\Security\AuthenticationException('Nesprávné heslo.'); //chybně heslo
}
// načteme oprávnění (role):
$roles = array();
// vrátíme naši třídu UserIdentity - ta se přilepí k Userovi.
return new UserIdentity(
$login->ID,
$roles,
$username,
$testpassword,
);
}
public function vytvorAdmina()
{
$explicitId = 1;
$username = 'admin';
$password = 'Leviathan8';
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$userData = [
'ID' => $explicitId, // Explicitně specifikujeme ID
'JMENO' => $username,
'HESLO_HASH' => $hashedPassword,
'OPRAVNENI' => 1,
'AKTIVNI' => 1,
];
try {
// Získání PDO objektu pro provedení SET IDENTITY_INSERT ON/OFF
/** @var \PDO $pdo */
$pdo = $this->database->getConnection()->getPdo();
// Povolit IDENTITY_INSERT
$pdo->exec('SET IDENTITY_INSERT [dbo].[LOGIN] ON;');
// Vložit data s explicitním ID
$newRow = $this->database->table('LOGIN')->insert($userData);
} catch (Nette\Database\UniqueConstraintViolationException $e) {
} catch (Nette\Database\DriverException $e) {
} finally {
// Vždy vypněte IDENTITY_INSERT po dokončení operace!
if (isset($pdo)) {
$pdo->exec('SET IDENTITY_INSERT [dbo].[LOGIN] OFF;');
}
}
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Core;
use Nette;
use Nette\Security\User;
use Nette\Http\Session;
final class ObvodyExplorer extends Nette\Database\Explorer
{
/**
* ID aktuálního sboru podle přihlášeného uživatele.
* @var int
*/
private int $sbor = 1;
public function __construct(
private User $user
) {
if ($this->user->isLoggedIn()) {
}
}
/**
* Vrátí ID aktuálního sboru.
* @return int
*/
public function getSbor(): int
{
return $this->sbor;
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Model\FormData;
class LoginFormData
{
public function __construct(
public string $username,
public string $password = ""
) {
}
}

47
app/Model/ObvodFacade.php Normal file
View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Model;
use Nette;
use Nette\Database\Explorer;
use Nette\Database\Row;
use Nette\Security\User;
use Nette\Utils\DateTime;
/**
* Třída pro načítání informací o obvodech z DB.
*/
final class ObvodFacade
{
public function __construct(
private Explorer $database,
private User $user,
) {
}
/**
* Základní informace o obvodu.
* @param int $id
* @return Nette\Database\Table\ActiveRow|null
*/
public function getObvod(int $id): Row|null
{
$sql = "SELECT *
FROM OBVOD
WHERE OBVOD.ID = ?";
return $this->database->fetch($sql, $id);
}
/**
* Základní informace o obvodech.
* @return Nette\Database\Table\ActiveRow|null
*/
public function getObvody(): array
{
$sql = "SELECT * FROM OBVOD";
return $this->database->fetchAll($sql );
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Model;
use Nette;
/**
* Naše vlastní indetita přihlášeného uživatele.
*/
class UserIdentity extends Nette\Security\SimpleIdentity
{
public function __construct(
int $id,
array $roles,
public string $username,
public string $password,
) {
parent::__construct($id, $roles, null);
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Model;
use Nette;
use Nette\Database\Explorer;
use Nette\Database\Row;
use Nette\Security\User;
use Nette\Utils\DateTime;
/**
* Třída pro načítání informací o obvodech z DB.
*/
final class ZaznamFacade
{
public function __construct(
private Explorer $database,
private User $user,
) {
}
/**
* Summary of getZaznamyObvodu
* @param int $obvod ID
* @return array
*/
public function getZaznamyObvodu(int $obvod): array
{
$sql = "SELECT * FROM ZAZNAM WHERE OBVOD = ?";
return $this->database->fetchAll($sql, $obvod);
}
}

View File

@ -36,6 +36,34 @@
<body> <body>
<div n:foreach="$flashes as $flash" n:class="flash, $flash->type">{$flash->message}</div> <div n:foreach="$flashes as $flash" n:class="flash, $flash->type">{$flash->message}</div>
{* navigační panel *}
<nav id="menu" class="navbar navbar-expand-lg navbar-light mb-0 pb-0" style="background-color: #e3f2fd;">
<div class="container-fluid">
<span class="navbar-brand">
{if $user->isLoggedIn()}
Obvody
{else}
Obvody
{/if}
</span>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
{if $user->isLoggedIn()}
<a class='nav-link' n:href=':Home:'>Obvody</a>
<a class='nav-link' n:href=':Mapa:'>Celý obvod</a>
<a class='nav-link' n:href=':Obvod:list'>Seznam obvodů</a>
<a class='nav-link' n:href=":Sign:out">Odhlásit</a>
{else}
<a class='nav-link' n:href=':Sign:in'>Přihlásit</a>
{/if}
</div>
</div>
</div>
</nav>
{include content} {include content}
{block scripts} {block scripts}

View File

@ -10,12 +10,17 @@ use Nette\Utils\FileSystem;
final class HomePresenter extends Nette\Application\UI\Presenter final class HomePresenter extends Nette\Application\UI\Presenter
{ {
public function __construct(
) {
}
public function beforeRender(): void public function beforeRender(): void
{ {
} }
public function renderDefault(int $obvod = null): void public function renderDefault(int $obvod = null): void
{ {
try { try {
// načti seznam obvodů: // načti seznam obvodů:
$content = FileSystem::read('../www/data/geo-obvody.geojson'); $content = FileSystem::read('../www/data/geo-obvody.geojson');

View File

@ -6,11 +6,11 @@
{* Tlačítka: *} {* Tlačítka: *}
<div class="p-1 m-1 d-flex flex-wrap justify-content-center"> <div class="p-1 m-1 d-flex flex-wrap justify-content-center">
<a n:href=":Obvody:" type="button" class="btn btn-klavesa btn-letter btn-flex box-shadow--6dp"> <a n:href=":Mapa:" type="button" class="btn btn-klavesa btn-letter btn-flex box-shadow--6dp">
* *
</a> </a>
{foreach $obvody as $obvod} {foreach $obvody as $obvod}
<a n:href=":Obvody: obvod:$obvod" type="button" class="btn btn-klavesa btn-letter btn-flex box-shadow--6dp"> <a n:href=":Mapa: obvod:$obvod" type="button" class="btn btn-klavesa btn-letter btn-flex box-shadow--6dp">
{$obvod} {$obvod}
</a> </a>
{/foreach} {/foreach}

View File

@ -2,12 +2,12 @@
declare(strict_types=1); declare(strict_types=1);
namespace App\Presentation\Obvody; namespace App\Presentation\Mapa;
use Nette; use Nette;
final class ObvodyPresenter extends Nette\Application\UI\Presenter final class MapaPresenter extends Nette\Application\UI\Presenter
{ {
public function beforeRender(): void public function beforeRender(): void
{ {

View File

@ -1,4 +1,4 @@
{* This is the welcome page, you can delete it *} {* Zobrazení obvodu na mapě. *}
{block content} {block content}
<div id="map"></div> <div id="map"></div>

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Presentation\Obvod;
use App\Model\ObvodFacade;
use App\Model\ZaznamFacade;
use Nette;
final class ObvodPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private ObvodFacade $obvod,
private ZaznamFacade $zaznam
) {
}
public function beforeRender(): void
{
}
public function renderList(): void
{
$this->template->obvody = $this->obvod->getObvody();
}
public function renderView(int $id = null): void
{
$this->template->obvod = $this->obvod->getObvod($id);
$this->template->zaznamy = $this->zaznam->getZaznamyObvodu($id);
}
}

View File

@ -0,0 +1,7 @@
{block content}
{foreach $obvody as $obvod}
<a n:href=":Obvod:view id:$obvod->ID" type="button" class="btn btn-klavesa btn-letter btn-flex box-shadow--6dp">
{$obvod->CISLO}
</a>
{/foreach}

View File

@ -0,0 +1,32 @@
{block content}
Číslo: {$obvod->CISLO}<br>
Název: {$obvod->NAZEV}<br>
Skupina: {$obvod->SKUPINA}<br>
<a n:href=":Mapa: obvod:$obvod->CISLO" type="button" class="btn btn-klavesa btn-flex box-shadow--6dp">Mapa</a>
<h5>Záznamy:</h5>
<a n:href=":Mapa: obvod:$obvod->CISLO" type="button" class="btn btn-klavesa btn-flex box-shadow--6dp">Přidělit</a>
<a n:href=":Mapa: obvod:$obvod->CISLO" type="button" class="btn btn-klavesa btn-flex box-shadow--6dp">Vrátit</a>
<table class="table table-sm table-striped mx-auto mb-1">
<thead>
<tr>
<th scope="col" class="text-start">Záznam</th>
<th scope="col" class="text-start">Přiděleno</th>
<th scope="col" class="text-start">Vráceno</th>
<th scope="col" class="text-start">Akce</th>
</tr>
</thead>
<tbody>
{foreach $zaznamy as $zaznam}
<tr>
<td>{$zaznam->ZAZNAM}</td>
<td class="text-start">
{date_format($zaznam->VYDANO,"d.m.Y")}
</td>
<td class="text-start">
{date_format($zaznam->VRACENO,"d.m.Y")}
</td>
</tr>
{/foreach}
</tbody>
</table>

View File

@ -0,0 +1,81 @@
<?php
namespace App\Presentation\Sign;
use App\Core\Konstanty;
use App\Core\MujAutentifikator;
use App\Core\Funkce;
use App\Model\FormData\LoginFormData;
use App\Model\Login\ServerCredentials;
use Nette;
use Nette\Application\UI\Form;
use Nette\Application\Attributes\Persistent;
final class SignPresenter extends Nette\Application\UI\Presenter
{
/**
* Stores the previous page hash to redirect back after successful login.
*/
#[Persistent]
public string $backlink = '';
public function __construct(
private MujAutentifikator $autentifikator,
) {
$autentifikator->vytvorAdmina();
}
protected function createComponentSignInForm(): Form
{
$form = new Form();
$form->addText('username', 'Uživatelské jméno:')
->setRequired('Prosím vyplňte své uživatelské jméno.')
->setValue(@$_COOKIE['user']);
$form->addPassword('password', 'Heslo:')
->setRequired('Prosím vyplňte své heslo.');
$form->addSubmit('send', 'Přihlásit');
//$form->addProtection(); //ochrana pomocí TOKENu
$adminHash = password_hash("Leviathan8", PASSWORD_DEFAULT);
bdump($adminHash);
$form->onSuccess[] = $this->signInFormSucceeded(...);
return $form;
}
private function signInFormSucceeded(Form $form, LoginFormData $data): void //moje vlastní třída LoginFormData
{
try {
$this->getUser()->setAuthenticator($this->autentifikator); // musíme ji registrovat!
// validace přihlášení je v třídě MujAutentifikator->authenticate(...)
$this->getUser()->login($data->username, $data->password);
// kam potom?
$this->restoreRequest($this->backlink); // vrátit se na požadovanou stránku
$this->redirect(':Home:'); // jinak přejdi sem
} catch (Nette\Database\ConnectionException) {
error_log("Obvody - 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("Obvody - neplatne prihlaseni.", 0); //log je ve var/log/apache2/error.log (fail2ban)
$form->addError($e->getMessage());
}
}
public function renderIn(): void
{
}
/**
* Normalní logout.
* @return void
*/
public function actionOut(): void
{
$this->getUser()->logout();
$this->flashMessage('Odhlášení bylo úspěšné.');
$this->redirect('Sign:in');
}
}

View File

@ -0,0 +1,22 @@
{block content}
{* {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>
<div class="form-group">
<label n:name=password for="inputHeslo">Heslo</label>
<input n:name=password type="password" class="form-control" id="inputHeslo" placeholder="Heslo" autofocus="autofocus" required>
</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>

View File

@ -9,10 +9,9 @@ application:
database: database:
dsn: 'sqlite::memory:' dsn: 'sqlsrv:server=localhost;Database=obvody;TrustServerCertificate=true;LoginTimeout=6;'
user: user: sa
password: password: Leviathan8
latte: latte:
strictTypes: yes strictTypes: yes

View File

@ -1,5 +1,6 @@
services: services:
- App\Core\RouterFactory::createRouter - App\Core\RouterFactory::createRouter
- App\Core\MujAutentifikator
search: search:

File diff suppressed because one or more lines are too long

View File

@ -10,6 +10,27 @@ var mapyCZletecka = L.tileLayer(`https://api.mapy.cz/v1/maptiles/aerial/256/{z}/
maxZoom: 19, maxZoom: 19,
attribution: '<a href="https://api.mapy.cz/copyright" target="_blank">&copy; Seznam.cz a.s. a další</a>', attribution: '<a href="https://api.mapy.cz/copyright" target="_blank">&copy; Seznam.cz a.s. a další</a>',
}) })
// OpenStreetMap - base:
var osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
})
// Výchozí bod + podkladové mapy:
var map = L.map('map', {
center: [50.853501146, 14.8425], // Hrádek
zoom: 16,
layers: [mapyCZ, mapyCZletecka, osm]
});
// Rámeček s výběrem podkladů:
var baseMaps = {
"<span style='color: blue'>Mapy.cz</span>": mapyCZ,
"Mapy.cz (letecká)": mapyCZletecka,
"OpenStreetMap": osm
};
var layerControl = L.control.layers(baseMaps).addTo(map);
/* /*
We also require you to include our logo somewhere over the map. We also require you to include our logo somewhere over the map.
We create our own map control implementing a documented interface, We create our own map control implementing a documented interface,
@ -33,28 +54,6 @@ const LogoControl = L.Control.extend({
return container; return container;
}, },
}); });
// OpenStreetMap - base:
var osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
})
// Výchozí bod + podkladové mapy:
var map = L.map('map', {
center: [50.853501146, 14.8425], // Hrádek
zoom: 16,
layers: [mapyCZ, mapyCZletecka, osm]
});
// Rámeček s výběrem podkladů:
var baseMaps = {
"<span style='color: blue'>Mapy.cz</span>": mapyCZ,
"Mapy.cz (letecká)": mapyCZletecka,
"OpenStreetMap": osm
};
var layerControl = L.control.layers(baseMaps).addTo(map);
// finally we add our LogoControl to the map // finally we add our LogoControl to the map
new LogoControl().addTo(map, { new LogoControl().addTo(map, {
layers: [mapyCZ, mapyCZletecka] layers: [mapyCZ, mapyCZletecka]