Compare commits

...

18 Commits

Author SHA1 Message Date
stepan 2e4c10d084 [security] Opravena chyba session_regenerate_id po session_destroy při obnově z remember me cookie 2026-03-19 09:36:41 +01:00
stepan cb3e7a2969 [ui] CSS – opravena mezera mezi labelem a inputem (line-height na labelu) 2026-03-18 23:27:44 +01:00
stepan af6df61128 [ui] CSS – Montserrat 300/500, IBM Plex Serif 200/0.875rem, h1 větší, h2 barevnější, h3 světlejší pozadí, inline tlačítka stejně široká, odstraněna tmavá čára pod h1 2026-03-18 23:23:52 +01:00
stepan 78be7ec1c2 [ui] CSS – Montserrat 100 ve třech velikostech, IBM Plex Serif 0.78rem, zaoblení bloků a stíny 2026-03-18 23:03:29 +01:00
stepan 52bb0208a9 [ui] CSS – Montserrat + IBM Plex Serif, oddělovače jako mezery #141414, symetrické padding bloků, odstraněny veškeré hr čáry 2026-03-18 22:32:49 +01:00
stepan f6e5695ef9 [ui] Aktualizován CSS styl – sekce odděleny mezerou místo hr, tmavší červená jako pozadí, Barlow Condensed + Source Serif 4, mezera label→input odstraněna 2026-03-18 22:17:33 +01:00
stepan 876cda8ab4 přesun css do / 2026-03-18 22:01:40 +01:00
stepan f1c73b41a7 [ui] Přidán CSS styl pro auth systém – tmavé pozadí, červený akcent, centrovaný pruh 2026-03-18 21:55:55 +01:00
stepan 3c9267b1c5 [docs] Přidána dokumentace systému přihlašování pro použití v jiných projektech 2026-03-18 17:03:37 +01:00
stepan 19c92c552d [ui] Přidána ukázková stránka s veřejným i chráněným obsahem (soubor index.php) 2026-03-18 16:50:18 +01:00
stepan cc4cd26a1e konfigurace
- nastaveno define('VYZADOVAT_PRIHLASENI', false);
- defaultně není striktně vyžadováno přihlášení - co se zobrazí a co ne, rozhodne chráněná stránka
2026-03-18 16:47:41 +01:00
stepan d4b7b01fe7 [auth] Opraveny PHP tagy v úvodním komentáři – způsobovaly výpis kódu jako text 2026-03-18 15:59:22 +01:00
stepan d26f66f9a8 [admin] Ochrana prvního uživatele před smazáním, odkaz na admin v $auth_logout_html 2026-03-18 12:39:35 +01:00
stepan 40289394d2 [auth] $auth_logout_html – přidán odkaz na admin rozhraní pro admina 2026-03-18 12:38:42 +01:00
stepan ad0916f25b [config] Přidána konstanta AUTH_ADMIN_URL 2026-03-18 12:36:59 +01:00
stepan 691ecf6af5 [auth] Doplněn úvodní komentář o všech dostupných proměnných včetně příkladů použití 2026-03-17 16:06:26 +01:00
stepan 1a3b1a369e Merge branch 'main' of https://git.svach.eu/stepan/MSPPPPaM 2026-03-17 16:05:13 +01:00
stepan 79c2a13266 úprava index.php - přihlášen / nepřihlášen 2026-03-17 16:04:32 +01:00
6 changed files with 1308 additions and 237 deletions
+23 -1
View File
@@ -174,10 +174,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$chyba = 'Neplatné ID uživatele.'; $chyba = 'Neplatné ID uživatele.';
} elseif ($uzivatel_id === (int) $auth_uzivatel['id']) { } elseif ($uzivatel_id === (int) $auth_uzivatel['id']) {
$chyba = 'Nemůžeš smazat svůj vlastní účet.'; $chyba = 'Nemůžeš smazat svůj vlastní účet.';
} elseif ($uzivatel_id === 1) {
// Uživatel s id=1 je první admin vytvořený při instalaci
// jeho smazání by mohlo zanechat systém bez správce
$chyba = 'Prvního uživatele systému nelze smazat.';
} }
if (empty($chyba)) { if (empty($chyba)) {
try { try {
// Díky FOREIGN KEY s ON DELETE CASCADE se automaticky
// smažou i záznamy v navázaných tabulkách
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
DELETE FROM `" . DB_TABULKA_UZIVATELE . "` DELETE FROM `" . DB_TABULKA_UZIVATELE . "`
WHERE `id` = :id WHERE `id` = :id
@@ -284,6 +290,7 @@ try {
<td><?php echo $u['admin'] ? 'ANO' : 'ne'; ?></td> <td><?php echo $u['admin'] ? 'ANO' : 'ne'; ?></td>
<td><?php echo htmlspecialchars($u['vytvoreno']); ?></td> <td><?php echo htmlspecialchars($u['vytvoreno']); ?></td>
<td> <td>
<!-- Formulář pro změnu hesla -->
<form method="POST" action="" style="display:inline;"> <form method="POST" action="" style="display:inline;">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>"> <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="akce" value="zmena_hesla"> <input type="hidden" name="akce" value="zmena_hesla">
@@ -293,7 +300,14 @@ try {
<button type="submit">Změnit heslo</button> <button type="submit">Změnit heslo</button>
</form> </form>
&nbsp; &nbsp;
<?php if ($u['id'] !== $auth_uzivatel['id']): ?> <?php
// Tlačítko Smazat se nezobrazí pokud:
// - jde o samotného přihlášeného admina
// - jde o prvního uživatele systému (id = 1)
$nelze_smazat = ($u['id'] === $auth_uzivatel['id'])
|| ((int) $u['id'] === 1);
if (!$nelze_smazat):
?>
<form method="POST" action="" style="display:inline;" <form method="POST" action="" style="display:inline;"
onsubmit="return confirm('Opravdu smazat uživatele <?php echo htmlspecialchars(addslashes($u['email'])); ?>? Tato akce je nevratná.');"> onsubmit="return confirm('Opravdu smazat uživatele <?php echo htmlspecialchars(addslashes($u['email'])); ?>? Tato akce je nevratná.');">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>"> <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
@@ -301,6 +315,13 @@ try {
<input type="hidden" name="uzivatel_id" value="<?php echo htmlspecialchars($u['id']); ?>"> <input type="hidden" name="uzivatel_id" value="<?php echo htmlspecialchars($u['id']); ?>">
<button type="submit">Smazat</button> <button type="submit">Smazat</button>
</form> </form>
<?php else: ?>
<!-- Vysvětlení proč nelze smazat -->
<?php if ((int) $u['id'] === 1): ?>
<em>(první uživatel systému nelze smazat)</em>
<?php else: ?>
<em>(vlastní účet nelze smazat)</em>
<?php endif; ?>
<?php endif; ?> <?php endif; ?>
</td> </td>
</tr> </tr>
@@ -381,6 +402,7 @@ try {
<tr><td>PROJEKT_URL</td><td><?php echo htmlspecialchars(PROJEKT_URL); ?></td></tr> <tr><td>PROJEKT_URL</td><td><?php echo htmlspecialchars(PROJEKT_URL); ?></td></tr>
<tr><td>AUTH_LOGIN_URL</td><td><?php echo htmlspecialchars(AUTH_LOGIN_URL); ?></td></tr> <tr><td>AUTH_LOGIN_URL</td><td><?php echo htmlspecialchars(AUTH_LOGIN_URL); ?></td></tr>
<tr><td>AUTH_LOGOUT_URL</td><td><?php echo htmlspecialchars(AUTH_LOGOUT_URL); ?></td></tr> <tr><td>AUTH_LOGOUT_URL</td><td><?php echo htmlspecialchars(AUTH_LOGOUT_URL); ?></td></tr>
<tr><td>AUTH_ADMIN_URL</td><td><?php echo htmlspecialchars(AUTH_ADMIN_URL); ?></td></tr>
<tr><td>AUTH_REGISTRACE_URL</td><td><?php echo htmlspecialchars(AUTH_REGISTRACE_URL); ?></td></tr> <tr><td>AUTH_REGISTRACE_URL</td><td><?php echo htmlspecialchars(AUTH_REGISTRACE_URL); ?></td></tr>
<tr><td>AUTH_RESET_URL</td><td><?php echo htmlspecialchars(AUTH_RESET_URL); ?></td></tr> <tr><td>AUTH_RESET_URL</td><td><?php echo htmlspecialchars(AUTH_RESET_URL); ?></td></tr>
<tr><td>AUTH_REDIRECT_PO_PRIHLASENI</td><td><?php echo htmlspecialchars(AUTH_REDIRECT_PO_PRIHLASENI); ?></td></tr> <tr><td>AUTH_REDIRECT_PO_PRIHLASENI</td><td><?php echo htmlspecialchars(AUTH_REDIRECT_PO_PRIHLASENI); ?></td></tr>
+326 -228
View File
@@ -1,270 +1,368 @@
<?php <?php
// ============================================================ // ============================================================
// PŘIHLAŠOVACÍ STRÁNKA // HLAVNÍ SOUBOR AUTENTIZACE
// ============================================================
// Tento soubor vložíš na začátek každé stránky, kterou chceš
// chránit přihlášením:
//
// require_once 'auth/auth.php';
//
// Po jeho načtení máš k dispozici tyto proměnné:
//
// $auth_prihlasen ... true / false (vždy nastaveno)
// $auth_uzivatel['id'] ... ID přihlášeného uživatele (nebo null)
// $auth_uzivatel['email'] ... email přihlášeného uživatele (nebo null)
// $auth_uzivatel['admin'] ... true / false (vždy nastaveno)
//
// $auth_logout_html ... HTML s tlačítkem "Odhlásit se"
// a (pro admina) odkazem na admin rozhraní.
// Neprázdný pouze pokud je uživatel přihlášen.
// Použití: echo $auth_logout_html;
//
// $auth_login_html ... HTML formulář pro přihlášení (email, heslo,
// zapamatovat si mě, CSRF token).
// Neprázdný pouze pokud uživatel NENÍ přihlášen.
// Data odesílá na AUTH_LOGIN_URL (login.php),
// které po přihlášení přesměruje zpět na
// aktuální stránku.
// Použití: echo $auth_login_html;
//
// Typické použití stránka vyžadující přihlášení
// (VYZADOVAT_PRIHLASENI = true v config.php):
//
// require_once 'auth/auth.php';
// // Sem se dostane jen přihlášený uživatel.
// // Přihlášený email: $auth_uzivatel['email']
// // Odhlašovací tlačítko: echo $auth_logout_html;
//
// Typické použití stránka s omezeným přístupem
// (VYZADOVAT_PRIHLASENI = false v config.php):
//
// require_once 'auth/auth.php';
// // if ($auth_prihlasen): zobraz přihlášený obsah + echo $auth_logout_html;
// // else: zobraz veřejný obsah + echo $auth_login_html;
//
// ============================================================ // ============================================================
// Zpracovává přihlášení z dvou zdrojů:
// 1) Vlastní formulář na této stránce (login.php)
// 2) Formulář $auth_login_html vložený na jiné stránce
// (v tom případě přijde v POST i redirect_url)
require_once __DIR__ . '/config.php'; require_once __DIR__ . '/config.php';
require_once __DIR__ . '/db.php'; require_once __DIR__ . '/db.php';
// ------------------------------------------------------------
// SPUŠTĚNÍ SESSION
// ------------------------------------------------------------
// Nastavíme parametry session cookie ještě PŘED session_start().
// HttpOnly = JavaScript nemůže cookie číst (ochrana před XSS)
// SameSite = cookie se neposílá při požadavcích z jiných webů
// (ochrana před CSRF)
// Secure = cookie se posílá jen přes HTTPS (pokud web běží na HTTPS)
session_name(SESSION_NAZEV); session_name(SESSION_NAZEV);
session_set_cookie_params([ session_set_cookie_params([
'lifetime' => 0, 'lifetime' => 0, // 0 = cookie platí do zavření prohlížeče
// (trvalost zajišťuje remember me, ne session)
'path' => '/', 'path' => '/',
'httponly' => true, 'httponly' => true, // JavaScript k cookie nemá přístup
'samesite' => 'Strict', 'samesite' => 'Strict', // ochrana před CSRF
// 'secure' => true, // 'secure' => true, // odkomentuj pokud web běží na HTTPS
]); ]);
session_start(); session_start();
// Pokud je uživatel již přihlášen, přesměrujeme ho rovnou dál // ------------------------------------------------------------
if (isset($_SESSION['uzivatel_id'])) { // VÝCHOZÍ STAV uživatel není přihlášen
header('Location: ' . AUTH_REDIRECT_PO_PRIHLASENI); // ------------------------------------------------------------
// Tyto hodnoty jsou nastaveny vždy při každém načtení stránky.
// Teprve níže se případně přepíší na true / skutečné hodnoty.
$auth_prihlasen = false;
$auth_uzivatel = [
'id' => null,
'email' => null,
'admin' => false,
];
$auth_logout_html = '';
$auth_login_html = '';
// ------------------------------------------------------------
// KROK 1: Existuje platná session?
// ------------------------------------------------------------
// Session je nejrychlejší způsob ověření nevyžaduje dotaz do DB.
// Session data jsou uložena na serveru, uživatel je nemůže zfalšovat.
if (isset($_SESSION['uzivatel_id']) && isset($_SESSION['email'])) {
// Kontrola expirace session (nečinnost)
if (isset($_SESSION['posledni_aktivita']) &&
(time() - $_SESSION['posledni_aktivita']) > SESSION_EXPIRACE) {
// Session vypršela zrušíme ji.
// DŮLEŽITÉ: Po session_destroy() není žádná aktivní session.
// Krok 2 (remember me) to musí zohlednit viz níže.
$_SESSION = [];
session_destroy();
} else {
// Session je platná uživatel je přihlášen
$auth_prihlasen = true;
$auth_uzivatel = [
'id' => $_SESSION['uzivatel_id'],
'email' => $_SESSION['email'],
// Přetypování na bool ochrana pro případ, že by v session
// byla jiná hodnota než true/false
'admin' => (bool) $_SESSION['admin'],
];
// Aktualizujeme čas poslední aktivity
$_SESSION['posledni_aktivita'] = time();
}
}
// ------------------------------------------------------------
// KROK 2: Session neexistuje zkusíme remember me cookie
// ------------------------------------------------------------
// Cookie obsahuje pouze selector a token. Nikdy neobsahuje
// heslo, ID uživatele ani příznak admin. Vše citlivé je na serveru.
if (!$auth_prihlasen &&
isset($_COOKIE['auth_selector']) &&
isset($_COOKIE['auth_token'])) {
$cookie_selector = $_COOKIE['auth_selector'];
$cookie_token = $_COOKIE['auth_token'];
// Vyhledáme záznam v DB podle selectoru (přesná shoda, rychlé)
$stmt = $pdo->prepare("
SELECT
t.id AS token_id,
t.uzivatel_id,
t.token_hash,
t.expiruje,
u.email,
u.admin
FROM `" . DB_TABULKA_TOKENY . "` t
JOIN `" . DB_TABULKA_UZIVATELE . "` u ON u.id = t.uzivatel_id
WHERE t.selector = :selector
LIMIT 1
");
$stmt->execute([':selector' => $cookie_selector]);
$zaznam = $stmt->fetch();
$cookie_ok = false;
if ($zaznam) {
// Zkontrolujeme, zda token ještě nevypršel
if (time() < strtotime($zaznam['expiruje'])) {
// Ověříme tajný token pomocí password_verify().
// Tato funkce záměrně trvá stejně dlouho bez ohledu
// na výsledek chrání před timing útoky.
if (password_verify($cookie_token, $zaznam['token_hash'])) {
$cookie_ok = true;
}
}
}
if ($cookie_ok) {
// Cookie je platná obnovíme session.
//
// OPRAVA CHYBY: Pokud v kroku 1 vypršela session a byla zničena
// přes session_destroy(), není nyní žádná aktivní session.
// session_regenerate_id() by selhalo s warningem "no active session",
// ten warning by odeslal výstup, a tím by znemožnil setcookie()
// a header() níže.
//
// Řešení: pokud session není aktivní, spustíme ji znovu
// před voláním session_regenerate_id().
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
// Regenerujeme session ID (ochrana před session fixation)
session_regenerate_id(true);
$_SESSION['uzivatel_id'] = $zaznam['uzivatel_id'];
$_SESSION['email'] = $zaznam['email'];
$_SESSION['admin'] = (bool) $zaznam['admin'];
$_SESSION['posledni_aktivita'] = time();
// Vygenerujeme CSRF token pokud ještě neexistuje
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$auth_prihlasen = true;
$auth_uzivatel = [
'id' => $zaznam['uzivatel_id'],
'email' => $zaznam['email'],
'admin' => (bool) $zaznam['admin'],
];
// Prodloužíme platnost tokenu v DB i v cookie
$nova_expirace = time() + REMEMBER_EXPIRACE;
$nova_expirace_dt = date('Y-m-d H:i:s', $nova_expirace);
$stmt2 = $pdo->prepare("
UPDATE `" . DB_TABULKA_TOKENY . "`
SET `expiruje` = :expiruje
WHERE `id` = :id
");
$stmt2->execute([
':expiruje' => $nova_expirace_dt,
':id' => $zaznam['token_id'],
]);
// Prodloužíme cookie v prohlížeči
setcookie('auth_selector', $cookie_selector, [
'expires' => $nova_expirace,
'path' => '/',
'httponly' => true,
'samesite' => 'Strict',
// 'secure' => true,
]);
setcookie('auth_token', $cookie_token, [
'expires' => $nova_expirace,
'path' => '/',
'httponly' => true,
'samesite' => 'Strict',
// 'secure' => true,
]);
// Smažeme staré (expirované) tokeny z DB příležitostně
$stmt3 = $pdo->prepare("
DELETE FROM `" . DB_TABULKA_TOKENY . "`
WHERE `expiruje` < :ted
");
$stmt3->execute([':ted' => date('Y-m-d H:i:s')]);
} else {
// Cookie je neplatná nebo expirovaná smažeme ji
if ($zaznam) {
$stmt4 = $pdo->prepare("
DELETE FROM `" . DB_TABULKA_TOKENY . "`
WHERE `id` = :id
");
$stmt4->execute([':id' => $zaznam['token_id']]);
}
setcookie('auth_selector', '', [
'expires' => time() - 3600,
'path' => '/',
'httponly' => true,
'samesite' => 'Strict',
]);
setcookie('auth_token', '', [
'expires' => time() - 3600,
'path' => '/',
'httponly' => true,
'samesite' => 'Strict',
]);
}
}
// ------------------------------------------------------------
// KROK 3: Rozhodnutí přesměrovat nebo pokračovat?
// ------------------------------------------------------------
if (!$auth_prihlasen && VYZADOVAT_PRIHLASENI) {
// Uložíme URL aktuální stránky pro přesměrování po přihlášení
$aktualni_url = $_SERVER['REQUEST_URI'] ?? '';
if (!empty($aktualni_url)) {
$_SESSION['redirect_po_prihlaseni'] = $aktualni_url;
}
header('Location: ' . AUTH_LOGIN_URL);
exit; exit;
} }
// ------------------------------------------------------------ // ------------------------------------------------------------
// CSRF TOKEN // CSRF TOKEN zajistíme, že vždy existuje
// ------------------------------------------------------------ // ------------------------------------------------------------
if (empty($_SESSION['csrf_token'])) { if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32)); $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
} }
$csrf_token = $_SESSION['csrf_token'];
// ------------------------------------------------------------ // ------------------------------------------------------------
// ZPRACOVÁNÍ FORMULÁŘE // PŘIPRAVENÉ HTML PROMĚNNÉ
// ------------------------------------------------------------ // ------------------------------------------------------------
$chyba = ''; // -- Odhlašovací formulář ------------------------------------
$email_hodnota = ''; // Neprázdný pouze pokud je uživatel přihlášen.
// Pro admina obsahuje navíc odkaz na administrační rozhraní.
// Použití na stránce: echo $auth_logout_html;
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($auth_prihlasen) {
// -- Ověření CSRF tokenu ---------------------------------- $auth_logout_html = '';
$csrf_z_formulare = $_POST['csrf_token'] ?? '';
if (!hash_equals($csrf_token, $csrf_z_formulare)) { // Odkaz na admin rozhraní pouze pro admina
$chyba = 'Neplatný požadavek. Zkuste stránku obnovit a přihlásit se znovu.'; if ($auth_uzivatel['admin']) {
$auth_logout_html .=
'<a href="' . htmlspecialchars(AUTH_ADMIN_URL) . '">Administrace</a> | ';
} }
if (empty($chyba)) { // Přihlášený uživatel a tlačítko odhlášení
$auth_logout_html .=
$email = trim($_POST['email'] ?? ''); htmlspecialchars($auth_uzivatel['email'])
$heslo = $_POST['heslo'] ?? ''; . ' | '
$zapamatovat = isset($_POST['zapamatovat']); . '<form method="POST" action="' . htmlspecialchars(AUTH_LOGOUT_URL) . '"'
. ' style="display:inline;">'
$email_hodnota = htmlspecialchars($email); . '<input type="hidden" name="csrf_token" value="'
. htmlspecialchars($_SESSION['csrf_token']) . '">'
if (empty($email) || empty($heslo)) { . '<button type="submit">Odhlásit se</button>'
$chyba = 'Zadejte email a heslo.'; . '</form>';
}
}
if (empty($chyba)) {
// -- Kontrola brute force -----------------------------
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$okno_od = date('Y-m-d H:i:s', time() - BRUTE_OKNO);
$stmt = $pdo->prepare("
SELECT COUNT(*) AS pocet
FROM `" . DB_TABULKA_BRUTE . "`
WHERE (`ip_adresa` = :ip OR `email` = :email)
AND `cas` > :okno_od
");
$stmt->execute([
':ip' => $ip,
':email' => $email,
':okno_od' => $okno_od,
]);
$pocet_pokusu = $stmt->fetch()['pocet'];
if ($pocet_pokusu >= BRUTE_MAX_POKUSU) {
$chyba = 'Příliš mnoho neúspěšných pokusů. Zkuste to prosím za chvíli.';
}
}
if (empty($chyba)) {
// -- Ověření emailu a hesla v DB ----------------------
$stmt = $pdo->prepare("
SELECT `id`, `email`, `heslo`, `admin`
FROM `" . DB_TABULKA_UZIVATELE . "`
WHERE `email` = :email
LIMIT 1
");
$stmt->execute([':email' => $email]);
$uzivatel = $stmt->fetch();
// I když uživatel neexistuje, zavoláme password_verify na fiktivní
// hash aby útočník nemohl podle doby odpovědi poznat, zda email existuje
$fiktivni_hash = '$2y$10$abcdefghijklmnopqrstuuABCDEFGHIJKLMNOPQRSTUVWXYZ01234';
$hash_k_overeni = $uzivatel ? $uzivatel['heslo'] : $fiktivni_hash;
$heslo_ok = password_verify($heslo, $hash_k_overeni);
if (!$uzivatel || !$heslo_ok) {
// Zapíšeme neúspěšný pokus
$stmt2 = $pdo->prepare("
INSERT INTO `" . DB_TABULKA_BRUTE . "`
(`ip_adresa`, `email`)
VALUES
(:ip, :email)
");
$stmt2->execute([
':ip' => $ip,
':email' => $email,
]);
$chyba = 'Nesprávný email nebo heslo.';
} else {
// -- Přihlášení úspěšné ---------------------------
session_regenerate_id(true);
$_SESSION['uzivatel_id'] = $uzivatel['id'];
$_SESSION['email'] = $uzivatel['email'];
$_SESSION['admin'] = (bool) $uzivatel['admin'];
$_SESSION['posledni_aktivita'] = time();
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
// -- Remember me ----------------------------------
if ($zapamatovat) {
$selector = bin2hex(random_bytes(16));
$token = bin2hex(random_bytes(32));
$token_hash = password_hash($token, PASSWORD_DEFAULT);
$expiruje = date('Y-m-d H:i:s', time() + REMEMBER_EXPIRACE);
$stmt3 = $pdo->prepare("
INSERT INTO `" . DB_TABULKA_TOKENY . "`
(`uzivatel_id`, `selector`, `token_hash`, `expiruje`)
VALUES
(:uzivatel_id, :selector, :token_hash, :expiruje)
");
$stmt3->execute([
':uzivatel_id' => $uzivatel['id'],
':selector' => $selector,
':token_hash' => $token_hash,
':expiruje' => $expiruje,
]);
$cookie_expirace = time() + REMEMBER_EXPIRACE;
setcookie('auth_selector', $selector, [
'expires' => $cookie_expirace,
'path' => '/',
'httponly' => true,
'samesite' => 'Strict',
// 'secure' => true,
]);
setcookie('auth_token', $token, [
'expires' => $cookie_expirace,
'path' => '/',
'httponly' => true,
'samesite' => 'Strict',
// 'secure' => true,
]);
}
// -- Přesměrování po přihlášení -------------------
// Priorita zdrojů pro redirect URL:
// 1) redirect_url z POST (formulář byl na jiné stránce)
// 2) redirect_po_prihlaseni v session (uloženo v auth.php)
// 3) výchozí stránka z config.php
$redirect_z_post = trim($_POST['redirect_url'] ?? '');
$redirect_ze_session = $_SESSION['redirect_po_prihlaseni'] ?? '';
// Bezpečnostní kontrola redirect URL:
// Povolujeme pouze relativní URL začínající lomítkem,
// aby útočník nemohl přesměrovat na cizí web
// (tzv. open redirect útok)
if (!empty($redirect_z_post) && str_starts_with($redirect_z_post, '/')) {
$redirect = $redirect_z_post;
} elseif (!empty($redirect_ze_session) && str_starts_with($redirect_ze_session, '/')) {
$redirect = $redirect_ze_session;
} else {
$redirect = AUTH_REDIRECT_PO_PRIHLASENI;
}
unset($_SESSION['redirect_po_prihlaseni']);
header('Location: ' . $redirect);
exit;
}
}
} }
// ------------------------------------------------------------ // -- Přihlašovací formulář -----------------------------------
// HTML VÝSTUP // Neprázdný pouze pokud uživatel NENÍ přihlášen.
// ------------------------------------------------------------ // Odesílá data na login.php, které zpracuje přihlášení
?> // a přesměruje uživatele zpět na původní stránku.
<!DOCTYPE html> // Použití na stránce: echo $auth_login_html;
<html lang="cs">
<head>
<meta charset="UTF-8">
<title>Přihlášení <?php echo htmlspecialchars(PROJEKT_NAZEV); ?></title>
<?php if (AUTH_CSS !== ''): ?>
<link rel="stylesheet" href="<?php echo htmlspecialchars(AUTH_CSS); ?>">
<?php endif; ?>
</head>
<body>
<h1>Přihlášení</h1> if (!$auth_prihlasen) {
<h2><?php echo htmlspecialchars(PROJEKT_NAZEV); ?></h2>
<?php if (!empty($chyba)): ?> // Aktuální URL předáme login.php jako parametr,
<p><strong>Chyba: <?php echo htmlspecialchars($chyba); ?></strong></p> // aby nás po úspěšném přihlášení přesměroval zpět sem
<?php endif; ?> $aktualni_url = $_SERVER['REQUEST_URI'] ?? '';
<form method="POST" action=""> $auth_login_html =
'<form method="POST" action="' . htmlspecialchars(AUTH_LOGIN_URL) . '">'
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>"> // CSRF token ochrana před podvrženými požadavky
. '<input type="hidden" name="csrf_token" value="'
. htmlspecialchars($_SESSION['csrf_token']) . '">'
<p> // Aktuální URL login.php nás po přihlášení přesměruje zpět
<label for="email">Email:</label><br> . '<input type="hidden" name="redirect_url" value="'
<input . htmlspecialchars($aktualni_url) . '">'
type="email"
id="email"
name="email"
value="<?php echo $email_hodnota; ?>"
required
autocomplete="email"
>
</p>
<p> . '<p>'
<label for="heslo">Heslo:</label><br> . '<label for="auth-email">Email:</label><br>'
<input . '<input type="email" id="auth-email" name="email"'
type="password" . ' required autocomplete="email">'
id="heslo" . '</p>'
name="heslo"
required
autocomplete="current-password"
>
</p>
<p> . '<p>'
<label> . '<label for="auth-heslo">Heslo:</label><br>'
<input type="checkbox" name="zapamatovat" value="1"> . '<input type="password" id="auth-heslo" name="heslo"'
Zapamatovat si mě . ' required autocomplete="current-password">'
</label> . '</p>'
</p>
<p> . '<p>'
<button type="submit">Přihlásit se</button> . '<label>'
</p> . '<input type="checkbox" name="zapamatovat" value="1">'
. ' Zapamatovat si mě'
. '</label>'
. '</p>'
</form> . '<p>'
. '<button type="submit">Přihlásit se</button>'
. '</p>'
<?php if (REGISTRACE_OTEVRENA): ?> . '</form>';
<p><a href="<?php echo htmlspecialchars(AUTH_REGISTRACE_URL); ?>">Nemáš účet? Zaregistruj se.</a></p> }
<?php endif; ?>
<p><a href="<?php echo htmlspecialchars(AUTH_RESET_URL); ?>">Zapomněl jsi heslo?</a></p>
</body>
</html>
+5 -4
View File
@@ -41,7 +41,7 @@ define('DB_TABULKA_SLUZBA', 'users');
// ------------------------------------------------------------ // ------------------------------------------------------------
// Název projektu zobrazuje se na přihlašovací stránce apod. // Název projektu zobrazuje se na přihlašovací stránce apod.
define('PROJEKT_NAZEV', 'auth demo hoho'); define('PROJEKT_NAZEV', 'auth demo hoho ho');
// Základní URL projektu (bez lomítka na konci) // Základní URL projektu (bez lomítka na konci)
// Používá se např. v odkazech v emailech // Používá se např. v odkazech v emailech
@@ -50,6 +50,7 @@ define('PROJEKT_URL', 'https://authdemo.svach.eu');
// Cesty ke stránkám auth systému (relativní od kořene webu) // Cesty ke stránkám auth systému (relativní od kořene webu)
define('AUTH_LOGIN_URL', '/auth/login.php'); define('AUTH_LOGIN_URL', '/auth/login.php');
define('AUTH_LOGOUT_URL', '/auth/logout.php'); define('AUTH_LOGOUT_URL', '/auth/logout.php');
define('AUTH_ADMIN_URL', '/auth/admin.php');
define('AUTH_REGISTRACE_URL', '/auth/registrace.php'); define('AUTH_REGISTRACE_URL', '/auth/registrace.php');
define('AUTH_RESET_URL', '/auth/reset_hesla.php'); define('AUTH_RESET_URL', '/auth/reset_hesla.php');
define('AUTH_REDIRECT_PO_PRIHLASENI', '/index.php'); define('AUTH_REDIRECT_PO_PRIHLASENI', '/index.php');
@@ -73,7 +74,7 @@ define('REGISTRACE_OTEVRENA', true);
// false = nepřihlášený uživatel obsah VIDÍ (auth.php jen nastaví // false = nepřihlášený uživatel obsah VIDÍ (auth.php jen nastaví
// proměnné $auth_prihlasen a $auth_uzivatel), // proměnné $auth_prihlasen a $auth_uzivatel),
// stránka sama rozhodne, co mu ukáže // stránka sama rozhodne, co mu ukáže
define('VYZADOVAT_PRIHLASENI', true); define('VYZADOVAT_PRIHLASENI', false);
// ------------------------------------------------------------ // ------------------------------------------------------------
@@ -130,7 +131,7 @@ define('HESLO_MIN_DELKA', 8);
define('MAIL_ODESILATEL', 'admin@svach.eu'); define('MAIL_ODESILATEL', 'admin@svach.eu');
// Jméno odesílatele (zobrazí se v emailovém klientu) // Jméno odesílatele (zobrazí se v emailovém klientu)
define('MAIL_ODESILATEL_JMENO', 'authdemátor'); define('MAIL_ODESILATEL_JMENO', 'Název mého projektu');
// Testovací emailová adresa (pro tlačítko "test emailu" v admin.php) // Testovací emailová adresa (pro tlačítko "test emailu" v admin.php)
define('MAIL_TEST_ADRESA', 'admin@svach.eu'); define('MAIL_TEST_ADRESA', 'admin@svach.eu');
@@ -143,4 +144,4 @@ define('MAIL_TEST_ADRESA', 'admin@svach.eu');
// Cesta k vlastnímu CSS souboru pro stránky auth systému. // Cesta k vlastnímu CSS souboru pro stránky auth systému.
// Pokud nechceš vlastní styl, nastav na prázdný řetězec: '' // Pokud nechceš vlastní styl, nastav na prázdný řetězec: ''
// Příklad: '/css/auth-styl.css' // Příklad: '/css/auth-styl.css'
define('AUTH_CSS', ''); define('AUTH_CSS', '/css/auth-styl.css');
+641
View File
@@ -0,0 +1,641 @@
/* ============================================================
AUTH SYSTÉM STYL
============================================================
Umístění: libovolné (např. /css/auth-styl.css)
Aktivace: v auth/config.php nastavit
define('AUTH_CSS', '/css/auth-styl.css');
Funguje bez jakýchkoliv změn v PHP souborech.
Styluje čistě pomocí HTML elementů žádné třídy nejsou
potřeba.
============================================================ */
/* ------------------------------------------------------------
GOOGLE FONTS
Montserrat 300/500 tři velikosti (viz níže)
IBM Plex Serif 200 jedna velikost pro veškerý text
------------------------------------------------------------ */
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;500&family=IBM+Plex+Serif:ital,wght@0,200;1,200&display=swap');
/* ------------------------------------------------------------
TYPOGRAFICKÁ STUPNICE MONTSERRAT
============================================================
Velikost A 2.6rem ... hlavní nadpis stránky (h1), jednou, weight 300
Velikost B 1.05rem ... nadpisy sekcí (h2, h3), weight 300
Velikost C 0.78rem ... nejmenší (th weight 500, button/label weight 300)
= shodná s IBM Plex Serif optickou velikostí
IBM Plex Serif 200 ... 0.875rem pro veškerý ostatní text
------------------------------------------------------------ */
/* ------------------------------------------------------------
PROMĚNNÉ
------------------------------------------------------------ */
:root {
/* Pozadí */
--bg-hlavni: #141414; /* tělo stránky = barva mezer mezi bloky */
--bg-obsah: #222222; /* světlejší bloky obsahu */
--bg-prvek: #2a2a2a; /* inputy, tabulky, textarea */
--bg-prvek-focus: #313131; /* input při focusu */
/* Písmo */
--text-hlavni: #d0d0d0;
--text-jemny: #7a7a7a;
--text-nadpis: #eeeeee;
/* Akcent tmavě červená */
--akcent: #5c1010; /* pozadí tlačítek, th */
--akcent-hover: #7a1515; /* hover tlačítek */
--akcent-svetly: #c0392b; /* text, h1, h3, odkazy */
--akcent-okraj: #4a0d0d; /* okraje akcentových prvků */
/* Ocelová šedá */
--ocel: #3a4455; /* okraje inputů, tabulek */
--ocel-svetly: #6a7a90; /* labely, h2, pomocný text */
/* Stavy */
--chyba-text: #f08080;
--uspech-text: #68d391;
/* Rozměry */
--pruh-max: 620px;
--sekce-mezera: 1.6rem; /* výška tmavé mezery mezi bloky */
--sekce-padding: 1.4rem; /* vnitřní odsazení bloků */
--polomer-blok: 6px; /* zaoblení rohů bloků */
--polomer-prvek: 4px; /* zaoblení inputů, tlačítek */
/* Stíny */
--stin-blok: 0 2px 12px rgba(0, 0, 0, 0.45);
--stin-input: inset 0 1px 3px rgba(0, 0, 0, 0.3);
--stin-input-focus: 0 0 0 2px rgba(192, 57, 43, 0.25);
}
/* ------------------------------------------------------------
ZÁKLAD
------------------------------------------------------------ */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
scroll-behavior: smooth;
}
body {
background-color: var(--bg-hlavni);
color: var(--text-hlavni);
font-family: 'IBM Plex Serif', Georgia, serif;
font-size: 0.875rem; /* IBM Plex Serif trochu větší než dřív */
font-weight: 200;
line-height: 1.75;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 0;
animation: fadein 0.2s ease-in;
}
/* ------------------------------------------------------------
OBSAHOVÝ PRUH
------------------------------------------------------------ */
body > * {
width: 100%;
max-width: var(--pruh-max);
}
/* ------------------------------------------------------------
BLOKY OBSAHU světlejší pozadí, zaoblení, stín
------------------------------------------------------------ */
body > h1,
body > h2,
body > h3,
body > p,
body > form,
body > table,
body > section,
body > ol,
body > ul,
body > textarea {
background-color: var(--bg-obsah);
padding-left: var(--sekce-padding);
padding-right: var(--sekce-padding);
padding-top: var(--sekce-padding);
padding-bottom: var(--sekce-padding);
border-radius: var(--polomer-blok);
box-shadow: var(--stin-blok);
}
/* Tabulka padding jen nahoře/dole, šířku řídí buňky */
body > table {
padding-left: 0;
padding-right: 0;
overflow: hidden; /* zaoblení se projeví i na th/td */
}
/* Mezery každý blok dostane mezeru nahoře */
body > h1,
body > h2,
body > h3,
body > p,
body > form,
body > table,
body > section,
body > ol,
body > ul,
body > textarea {
margin-top: var(--sekce-mezera);
}
/* hr = čistá tmavá mezera, žádná čára */
body > hr {
border: none;
background: transparent;
height: 0;
padding: 0;
margin: 0;
box-shadow: none;
}
/* h2 těsně za h1 stejný blok, bez mezery nahoře */
body > h1 + h2 {
margin-top: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
padding-top: 0;
}
/* h1 když za ním následuje h2 zaoblení jen nahoře, bez čáry dole */
body > h1:has(+ h2) {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
padding-bottom: 0.3rem;
box-shadow: var(--stin-blok); /* stejný stín jako ostatní bloky, bez čáry */
}
/* h3 blok má o trochu světlejší pozadí než ostatní bloky */
body > h3 {
background-color: #292929;
}
body > *:last-child {
margin-bottom: var(--sekce-mezera);
}
/* Elementy uvnitř formuláře a sekcí bez extra pozadí a stínu */
form > *,
section > * {
background-color: transparent;
box-shadow: none;
border-radius: 0;
margin-top: 0;
padding-left: 0;
padding-right: 0;
}
/* ------------------------------------------------------------
TYPOGRAFIE MONTSERRAT
------------------------------------------------------------ */
/* === VELIKOST A: hlavní nadpis stránky, jednou === */
h1 {
font-family: 'Montserrat', sans-serif;
font-weight: 300;
font-size: 2.6rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--akcent-svetly);
line-height: 1.15;
}
/* === VELIKOST B: nadpisy sekcí === */
h2,
h3 {
font-family: 'Montserrat', sans-serif;
font-weight: 300;
font-size: 1.05rem;
letter-spacing: 0.14em;
text-transform: uppercase;
}
h2 {
color: #9ab0c8; /* světlejší modrošedá čitelná na tmavém i světlém */
}
h3 {
color: var(--akcent-svetly);
}
/* === VELIKOST C: th, button, label, indikátory ===
(shodná s IBM Plex Serif = 0.78rem, aby text vypadal stejně velký) */
p {
font-size: 0.875rem; /* IBM Plex Serif velikost */
}
strong {
color: var(--text-nadpis);
font-weight: 300;
}
p > strong:first-child {
color: var(--chyba-text);
}
em {
color: var(--text-jemny);
font-style: italic;
}
/* ------------------------------------------------------------
ODKAZY
------------------------------------------------------------ */
a {
color: var(--akcent-svetly);
text-decoration: none;
border-bottom: 1px solid var(--akcent-okraj);
transition: color 0.15s, border-color 0.15s;
}
a:hover,
a:focus {
color: #e74c3c;
border-bottom-color: #e74c3c;
outline: none;
}
/* ------------------------------------------------------------
FORMULÁŘE
------------------------------------------------------------ */
form {
width: 100%;
max-width: var(--pruh-max);
}
form p {
padding: 0;
margin-bottom: 0.9rem;
box-shadow: none;
border-radius: 0;
margin-top: 0;
}
/* === VELIKOST C: label === */
label {
display: block;
font-family: 'Montserrat', sans-serif;
font-weight: 300;
font-size: 0.78rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ocel-svetly);
margin-bottom: 0;
padding-bottom: 0;
line-height: 1; /* pevná výška řádku zabrání dědění 1.75 z body */
}
/* Inline label pro checkbox jiný styl */
label:has(input[type="checkbox"]) {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-family: 'IBM Plex Serif', Georgia, serif;
font-weight: 200;
font-size: 0.875rem;
text-transform: none;
letter-spacing: normal;
color: var(--text-hlavni);
cursor: pointer;
}
/* ------------------------------------------------------------
INPUTY
------------------------------------------------------------ */
input[type="text"],
input[type="email"],
input[type="password"] {
display: block;
width: 100%;
background-color: var(--bg-prvek);
color: var(--text-hlavni);
border: 1px solid var(--ocel);
border-radius: var(--polomer-prvek);
padding: 0.45rem 0.7rem;
font-size: 0.875rem;
font-family: 'IBM Plex Serif', Georgia, serif;
font-weight: 200;
box-shadow: var(--stin-input);
transition: border-color 0.15s, background-color 0.15s, box-shadow 0.15s;
outline: none;
margin-top: 0.15rem; /* minimální optický odstup od labelu */
margin-bottom: 0;
line-height: 1.4;
}
input[type="text"]:focus,
input[type="email"]:focus,
input[type="password"]:focus {
background-color: var(--bg-prvek-focus);
border-color: var(--akcent-svetly);
box-shadow: var(--stin-input), var(--stin-input-focus);
}
input[type="text"]::placeholder,
input[type="email"]::placeholder,
input[type="password"]::placeholder {
color: var(--ocel-svetly);
opacity: 0.6;
}
/* Inline inputy v tabulce admin.php */
table input[type="password"] {
width: auto;
display: inline-block;
padding: 0.2rem 0.4rem;
font-size: 0.78rem;
margin-top: 0;
}
input[type="checkbox"] {
width: 0.85rem;
height: 0.85rem;
accent-color: var(--akcent-svetly);
cursor: pointer;
flex-shrink: 0;
}
/* ------------------------------------------------------------
TEXTAREA
------------------------------------------------------------ */
textarea {
display: block;
width: 100%;
background-color: var(--bg-prvek);
color: #98c898; /* terminálová zelená pro SQL */
border: 1px solid var(--ocel);
border-radius: var(--polomer-prvek);
padding: 0.75rem;
font-family: 'Courier New', Courier, monospace;
font-size: 0.75rem;
line-height: 1.55;
resize: vertical;
outline: none;
box-shadow: var(--stin-input);
}
textarea:focus {
border-color: var(--akcent-svetly);
box-shadow: var(--stin-input), var(--stin-input-focus);
}
/* ------------------------------------------------------------
TLAČÍTKA VELIKOST C (0.78rem Montserrat)
------------------------------------------------------------ */
button[type="submit"],
button[type="button"] {
background-color: var(--akcent);
color: #e8e8e8;
border: 1px solid var(--akcent-okraj);
border-radius: var(--polomer-prvek);
padding: 0.48rem 1.1rem;
font-family: 'Montserrat', sans-serif;
font-weight: 300;
font-size: 0.78rem;
letter-spacing: 0.1em;
cursor: pointer;
transition: background-color 0.15s, border-color 0.15s, box-shadow 0.15s;
margin-top: 0.3rem;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
}
button[type="submit"]:hover,
button[type="button"]:hover {
background-color: var(--akcent-hover);
border-color: var(--akcent-hover);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
button[type="submit"]:focus,
button[type="button"]:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(192, 57, 43, 0.4);
}
button:disabled {
background-color: #1c1c1c;
color: var(--text-jemny);
border-color: #2a2a2a;
cursor: not-allowed;
box-shadow: none;
}
button:disabled:hover {
background-color: #1c1c1c;
border-color: #2a2a2a;
box-shadow: none;
}
/* Inline formuláře odhlášení, smazání v tabulce */
form[style*="display:inline"],
form[style*="display: inline"] {
display: inline;
width: auto;
margin: 0;
padding: 0;
background-color: transparent;
box-shadow: none;
border-radius: 0;
}
form[style*="display:inline"] button,
form[style*="display: inline"] button {
margin-top: 0;
padding: 0.2rem 0;
font-size: 0.78rem;
min-width: 7rem; /* stejná šířka pro Smazat i Změnit heslo */
text-align: center;
}
/* ------------------------------------------------------------
INDIKÁTOR SÍLY HESLA VELIKOST C
------------------------------------------------------------ */
#heslo-sila,
#novy-heslo-sila {
display: inline-block;
font-family: 'Montserrat', sans-serif;
font-weight: 300;
font-size: 0.78rem;
letter-spacing: 0.06em;
color: var(--text-jemny);
margin-top: 0.2rem;
}
#tlacitko-duvod,
#novy-tlacitko-duvod {
font-family: 'Montserrat', sans-serif;
font-weight: 300;
font-size: 0.78rem;
letter-spacing: 0.04em;
color: var(--text-jemny);
margin-left: 0.4rem;
}
/* ------------------------------------------------------------
TABULKY
------------------------------------------------------------ */
table {
width: 100%;
border-collapse: collapse;
font-size: 0.78rem;
}
/* === VELIKOST C: záhlaví tabulky weight 500 pro lepší čitelnost === */
table th {
font-family: 'Montserrat', sans-serif;
font-weight: 500;
font-size: 0.78rem;
letter-spacing: 0.1em;
text-transform: uppercase;
background-color: var(--akcent);
color: #e8e8e8;
text-align: left;
padding: 0.5rem 0.75rem;
border: 1px solid var(--akcent-okraj);
}
table td {
background-color: var(--bg-prvek);
color: var(--text-hlavni);
padding: 0.4rem 0.75rem;
border: 1px solid var(--ocel);
vertical-align: middle;
font-family: 'IBM Plex Serif', Georgia, serif;
font-weight: 200;
font-size: 0.875rem;
}
table tr:nth-child(even) td {
background-color: #262626;
}
table tr:hover td {
background-color: #2d2d2d;
transition: background-color 0.1s;
}
table td em {
color: var(--text-jemny);
font-style: italic;
}
/* ------------------------------------------------------------
KÓDOVÉ ELEMENTY
------------------------------------------------------------ */
code {
background-color: var(--bg-prvek);
color: #98c898;
font-family: 'Courier New', Courier, monospace;
font-size: 0.85em;
padding: 0.1em 0.35em;
border-radius: var(--polomer-prvek);
border: 1px solid var(--ocel);
}
/* ------------------------------------------------------------
SEZNAMY
------------------------------------------------------------ */
ol, ul {
padding-left: 1.4rem;
font-family: 'IBM Plex Serif', Georgia, serif;
font-weight: 200;
font-size: 0.875rem;
}
ol li, ul li {
margin-bottom: 0.25rem;
line-height: 1.65;
}
/* ------------------------------------------------------------
RESPONSIVNÍ DESIGN
------------------------------------------------------------ */
@media (max-width: 680px) {
:root {
--pruh-max: 100%;
--sekce-padding: 1rem;
--sekce-mezera: 1rem;
--polomer-blok: 0px; /* na mobilu bez zaoblení bloky jdou od kraje */
}
h1 {
font-size: 1.6rem;
}
h2, h3 {
font-size: 0.95rem;
}
table td form[style*="display:inline"],
table td form[style*="display: inline"] {
display: block;
margin-bottom: 0.3rem;
}
table td form[style*="display:inline"] button,
table td form[style*="display: inline"] button {
width: 100%;
}
/* iOS: font-size ≥ 16px zabrání zoomu při focusu */
input[type="text"],
input[type="email"],
input[type="password"] {
font-size: 1rem;
}
table input[type="password"] {
display: block;
width: 100%;
margin-bottom: 0.3rem;
font-size: 1rem;
}
}
@media (max-width: 400px) {
h1 {
font-size: 1.3rem;
letter-spacing: 0.08em;
}
button[type="submit"],
button[type="button"] {
width: 100%;
}
}
/* ------------------------------------------------------------
PŘECHOD PŘI NAČTENÍ
------------------------------------------------------------ */
@keyframes fadein {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
+167 -4
View File
@@ -1,12 +1,175 @@
<?php <?php
// ============================================================
// UKÁZKOVÁ STRÁNKA S CHRÁNĚNÝM OBSAHEM
// ============================================================
// Demonstruje použití auth systému s VYZADOVAT_PRIHLASENI = false.
// Stránka je dostupná všem, ale zobrazuje různý obsah podle
// toho, zda je uživatel přihlášen nebo ne.
//
// Předpoklad: v auth/config.php je nastaveno:
// define('VYZADOVAT_PRIHLASENI', false);
// ============================================================
require_once 'auth/auth.php'; require_once 'auth/auth.php';
// $auth_prihlasen, $auth_uzivatel, $auth_logout_html, $auth_login_html
// jsou nyní k dispozici díky auth.php
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="cs"> <html lang="cs">
<head><meta charset="UTF-8"><title>Chráněná stránka</title></head> <head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ukázková stránka <?php echo htmlspecialchars(PROJEKT_NAZEV); ?></title>
<?php if (AUTH_CSS !== ''): ?>
<link rel="stylesheet" href="<?php echo htmlspecialchars(AUTH_CSS); ?>">
<?php endif; ?>
</head>
<body> <body>
<p>Přihlášen: <?php echo htmlspecialchars($auth_uzivatel['email']); ?></p>
<?php echo $auth_logout_html; ?> <!-- ============================================================
<p>Zde je chráněný obsah.</p> HLAVIČKA zobrazena vždy
============================================================ -->
<header>
<hr>
<h1><?php echo htmlspecialchars(PROJEKT_NAZEV); ?></h1>
<!-- Sem můžeš vložit obrázkové logo, např.: -->
<!-- <img src="/img/logo.png" alt="Logo"> -->
<hr>
</header>
<!-- ============================================================
PŘIHLAŠOVACÍ BADGE
Nepřihlášeným: formulář (email, heslo, zapamatovat, odeslat)
+ odkaz na registraci (pokud povolena)
+ odkaz na zapomenuté heslo
Přihlášeným: email uživatele + tlačítko odhlášení
+ odkaz na administraci (pouze pro admina)
============================================================ -->
<section>
<h2>Přihlášení</h2>
<?php if ($auth_prihlasen): ?>
<?php
// Pro přihlášené zobrazíme $auth_logout_html, který obsahuje:
// - odkaz na administraci (pouze pro adminy)
// - email přihlášeného uživatele
// - tlačítko odhlášení
// Vše je sestaveno v auth.php
echo $auth_logout_html;
?>
<?php else: ?>
<?php
// Pro nepřihlášené zobrazíme $auth_login_html, který obsahuje:
// - pole pro email a heslo
// - checkbox "zapamatovat si mě"
// - tlačítko odeslat
// - skrytý CSRF token a redirect URL
// Formulář odesílá data na login.php, které nás po
// přihlášení přesměruje zpět sem
echo $auth_login_html;
?>
<?php if (REGISTRACE_OTEVRENA): ?>
<p><a href="<?php echo htmlspecialchars(AUTH_REGISTRACE_URL); ?>">Nemáš účet? Zaregistruj se.</a></p>
<?php endif; ?>
<p><a href="<?php echo htmlspecialchars(AUTH_RESET_URL); ?>">Zapomenuté heslo</a></p>
<?php endif; ?>
</section>
<hr>
<!-- ============================================================
NAVIGACE dvě varianty podle stavu přihlášení
============================================================ -->
<nav>
<?php if ($auth_prihlasen): ?>
<p>Navigace pro přihlášené uživatele:</p>
<ul>
<li><a href="#">Můj profil</a></li>
<li><a href="#">Moje nastavení</a></li>
<li><a href="#">Chráněná sekce A</a></li>
<li><a href="#">Chráněná sekce B</a></li>
<?php if ($auth_uzivatel['admin']): ?>
<li><a href="<?php echo htmlspecialchars(AUTH_ADMIN_URL); ?>">Administrace</a></li>
<?php endif; ?>
</ul>
<?php else: ?>
<p>Navigace pro nepřihlášené návštěvníky:</p>
<ul>
<li><a href="#">Úvod</a></li>
<li><a href="#">O projektu</a></li>
<li><a href="#">Veřejná sekce</a></li>
<?php if (REGISTRACE_OTEVRENA): ?>
<li><a href="<?php echo htmlspecialchars(AUTH_REGISTRACE_URL); ?>">Registrace</a></li>
<?php endif; ?>
</ul>
<?php endif; ?>
</nav>
<hr>
<!-- ============================================================
OBSAH STRÁNKY stránka sama rozhoduje co zobrazit
============================================================ -->
<main>
<?php if ($auth_prihlasen): ?>
<!-- Obsah pro přihlášené uživatele -->
<h2>Vítej, <?php echo htmlspecialchars($auth_uzivatel['email']); ?>!</h2>
<p>Toto je chráněný obsah, který vidí pouze přihlášení uživatelé.</p>
<p>Tvoje údaje z auth systému:
<ul>
<li>ID: <?php echo htmlspecialchars($auth_uzivatel['id']); ?></li>
<li>Email: <?php echo htmlspecialchars($auth_uzivatel['email']); ?></li>
<li>Admin: <?php echo $auth_uzivatel['admin'] ? 'ANO' : 'ne'; ?></li>
</ul>
</p>
<?php if ($auth_uzivatel['admin']): ?>
<p>Vidíš toto, protože jsi admin. <a href="<?php echo htmlspecialchars(AUTH_ADMIN_URL); ?>">Přejít do administrace.</a></p>
<?php endif; ?>
<?php else: ?>
<!-- Obsah pro nepřihlášené návštěvníky -->
<h2>Vítej na ukázkové stránce</h2>
<p>Toto je veřejný obsah, který vidí každý návštěvník.</p>
<p>Pro zobrazení chráněného obsahu se přihlas pomocí formuláře výše.</p>
<?php endif; ?>
</main>
<hr>
<!-- ============================================================
PATIČKA zobrazena vždy
============================================================ -->
<footer>
<p><?php echo htmlspecialchars(PROJEKT_NAZEV); ?>
<a href="<?php echo htmlspecialchars(PROJEKT_URL); ?>"><?php echo htmlspecialchars(PROJEKT_URL); ?></a></p>
<hr>
</footer>
</body> </body>
</html> </html>
+146
View File
@@ -0,0 +1,146 @@
# Systém přihlašování MSPPPPaM dokumentace pro implementaci
Tento dokument popisuje hotový systém přihlašování v PHP a MySQL,
který chci použít v novém projektu. Systém je připravený, neupravuj
jeho soubory pouze ho používej.
---
## Umístění souborů
Všechny soubory systému jsou ve složce `auth/` v kořenovém adresáři projektu.
---
## Seznam souborů a jejich funkce
| Soubor | Popis |
|---|---|
| `auth/config.php` | Veškerá konfigurace systému (DB přihlašovací údaje, URL, limity, přepínače). Upravuje se při nasazení. |
| `auth/db.php` | Připojení k MySQL přes PDO. Výsledkem je proměnná `$pdo`. |
| `auth/auth.php` | Hlavní soubor vkládá se na začátek chráněných stránek. Ověří přihlášení a nastaví proměnné (viz níže). |
| `auth/login.php` | Přihlašovací stránka s formulářem. Obsahuje ochranu proti brute force a CSRF. |
| `auth/logout.php` | Odhlášení zruší session i remember me cookie. Přesměruje na login. |
| `auth/registrace.php` | Registrace nového uživatele (pokud je povolena v config.php). |
| `auth/reset_hesla.php` | Formulář pro zaslání odkazu na obnovu hesla emailem. |
| `auth/nove_heslo.php` | Stránka z odkazu v emailu umožní zadat nové heslo. |
| `auth/admin.php` | Administrační rozhraní správa uživatelů, zobrazení konfigurace, test emailu. Přístupné pouze adminům. |
| `auth/mail.php` | Pomocná funkce `odesli_mail()` pro odesílání emailů přes PHP `mail()`. |
| `auth/install.php` | Jednorázový instalační skript vytvoří tabulky v DB a prvního admina. Po instalaci zablokován souborem `install.lock`. |
| `auth/js/heslo-sila.js` | JavaScript pro kontrolu síly hesla pomocí knihovny zxcvbn (Dropbox). Používají ho stránky s formuláři pro zadání hesla. |
---
## Jak použít systém na chráněné stránce
Na začátek každého PHP souboru, jehož obsah chci chránit, vložím:
```php
require_once 'auth/auth.php';
```
Po načtení jsou k dispozici tyto proměnné:
| Proměnná | Typ | Popis |
|---|---|---|
| `$auth_prihlasen` | `bool` | `true` pokud je uživatel přihlášen, jinak `false`. Nastaveno vždy. |
| `$auth_uzivatel['id']` | `int\|null` | ID přihlášeného uživatele, nebo `null` pokud není přihlášen. |
| `$auth_uzivatel['email']` | `string\|null` | Email přihlášeného uživatele, nebo `null`. |
| `$auth_uzivatel['admin']` | `bool` | `true` pokud je přihlášený uživatel admin, jinak `false`. Nastaveno vždy. |
| `$auth_logout_html` | `string` | Připravený HTML kód s emailem uživatele, tlačítkem odhlášení a (pro adminy) odkazem na administraci. Neprázdný pouze pokud je uživatel přihlášen. Použití: `echo $auth_logout_html;` |
| `$auth_login_html` | `string` | Připravený HTML formulář pro přihlášení (email, heslo, zapamatovat si mě, CSRF token). Neprázdný pouze pokud uživatel NENÍ přihlášen. Odesílá data na `login.php`, které po přihlášení přesměruje zpět na aktuální stránku. Použití: `echo $auth_login_html;` |
### Dva režimy fungování (nastavení v `auth/config.php`)
**`VYZADOVAT_PRIHLASENI = true`** (výchozí):
Nepřihlášený uživatel je automaticky přesměrován na přihlašovací stránku.
Na chráněnou stránku se dostane až po přihlášení.
**`VYZADOVAT_PRIHLASENI = false`**:
Stránka se zobrazí všem. Pomocí `$auth_prihlasen` si stránka sama
rozhodne, co nepřihlášenému uživateli ukáže.
---
## Databázové tabulky
Systém přihlašování vytváří a spravuje tyto tabulky:
### `auth_users` uživatelé přihlašovacího systému
| Sloupec | Typ | Popis |
|---|---|---|
| `id` | `INT UNSIGNED` | Primární klíč, auto increment |
| `email` | `VARCHAR(255)` | Email uživatele, unikátní |
| `heslo` | `VARCHAR(255)` | Bcrypt hash hesla |
| `admin` | `TINYINT(1)` | 0 = běžný uživatel, 1 = admin |
| `vytvoreno` | `DATETIME` | Datum a čas registrace |
### `auth_remember_tokens` tokeny pro "zapamatovat si mě"
Spravuje systém přihlašování automaticky. Není potřeba upravovat.
### `auth_brute_force` záznamy neúspěšných přihlášení
Spravuje systém přihlašování automaticky. Není potřeba upravovat.
### `auth_password_resets` tokeny pro obnovu hesla
Spravuje systém přihlašování automaticky. Není potřeba upravovat.
### `users` uživatelé konkrétní služby
Tato tabulka propojuje systém přihlašování s tvým projektem.
Systém přihlašování ji vytvoří při instalaci se základní strukturou:
| Sloupec | Typ | Popis |
|---|---|---|
| `id` | `INT UNSIGNED` | Primární klíč, auto increment |
| `uzivatel_id` | `INT UNSIGNED` | Cizí klíč na `auth_users.id` (ON DELETE CASCADE) |
**Tuto tabulku rozšíříš o vlastní sloupce** podle potřeb projektu
(jméno, telefon, nastavení, apod.). Systém přihlašování ji nebude
nijak měnit pouze při registraci nového uživatele do ní vloží
prázdný řádek s `uzivatel_id`.
Propojení s uživatelem přihlašovacího systému:
```sql
SELECT u.*, au.email
FROM users u
JOIN auth_users au ON au.id = u.uzivatel_id
WHERE u.uzivatel_id = :id
```
---
## Konfigurace (auth/config.php)
Při nasazení do projektu je potřeba vyplnit:
- `DB_HOST`, `DB_NAME`, `DB_USER`, `DB_PASS` přihlašovací údaje k MySQL
- `PROJEKT_NAZEV` název projektu (zobrazuje se na přihlašovací stránce)
- `PROJEKT_URL` základní URL webu (používá se v odkazech v emailech)
- `MAIL_ODESILATEL` emailová adresa odesílatele (musí být autorizována SPF záznamem domény)
- `MAIL_TEST_ADRESA` adresa pro testovací email v admin rozhraní
- `REGISTRACE_OTEVRENA` `true` = veřejná registrace, `false` = pouze admin zakládá uživatele
- `VYZADOVAT_PRIHLASENI` `true` = přesměrovat nepřihlášené, `false` = stránka rozhoduje sama
Cesty ke stránkám auth systému (pokud je složka `auth/` jinde než v kořeni webu):
- `AUTH_LOGIN_URL`, `AUTH_LOGOUT_URL`, `AUTH_ADMIN_URL`
- `AUTH_REGISTRACE_URL`, `AUTH_RESET_URL`
- `AUTH_REDIRECT_PO_PRIHLASENI` kam přesměrovat po přihlášení
---
## Bezpečnostní vlastnosti systému
Pro přehled systém řeší:
- Hesla hashována bcryptem (`password_hash` s `PASSWORD_DEFAULT`)
- Ochrana před SQL injection výhradně PDO prepared statements
- Ochrana před CSRF každý formulář obsahuje token ověřovaný na serveru
- Ochrana před brute force blokování po opakovaných neúspěšných pokusech
- Ochrana před session fixation `session_regenerate_id()` při přihlášení
- Ochrana před XSS `htmlspecialchars()` na všech výstupech
- Remember me bezpečný selector+token mechanismus (heslo ani ID není v cookie)
- Síla hesla kontrola přes zxcvbn (JavaScript) + minimální délka (PHP)
- Celá DB v `utf8mb4` plná podpora diakritiky a emoji