[FÁZE-3][admin] Přidáno administrační rozhraní

This commit is contained in:
stepan
2026-03-17 00:11:15 +01:00
parent 8526742d74
commit a4b4ad5475
+602
View File
@@ -0,0 +1,602 @@
<?php
// ============================================================
// ADMINISTRAČNÍ ROZHRANÍ
// ============================================================
// Tato stránka je dostupná pouze přihlášenému adminovi.
// Umožňuje:
// - zobrazit seznam uživatelů
// - založit nového uživatele
// - změnit heslo uživatele
// - smazat uživatele
// - zobrazit konfiguraci systému (citlivé údaje skryté)
// - odeslat testovací email
// ============================================================
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/mail.php';
require_once __DIR__ . '/auth.php';
// ------------------------------------------------------------
// KONTROLA: Je přihlášený uživatel admin?
// ------------------------------------------------------------
// auth.php již zajistil, že uživatel je přihlášen (nebo přesměroval).
// Zde navíc kontrolujeme příznak admin.
if (!$auth_uzivatel['admin']) {
// Uživatel je přihlášen, ale není admin
// zobrazíme obecnou chybu (nechceme prozradit, že stránka existuje)
http_response_code(404);
die('<!DOCTYPE html><html lang="cs"><head><meta charset="UTF-8"><title>Stránka nenalezena</title></head><body><p>Stránka nebyla nalezena.</p></body></html>');
}
// ------------------------------------------------------------
// CSRF TOKEN
// ------------------------------------------------------------
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$csrf_token = $_SESSION['csrf_token'];
// ------------------------------------------------------------
// ZPRACOVÁNÍ AKCÍ (POST požadavky)
// ------------------------------------------------------------
$zprava = ''; // zpráva o úspěchu
$chyba = ''; // chybová hláška
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// -- Ověření CSRF tokenu ----------------------------------
$csrf_z_formulare = $_POST['csrf_token'] ?? '';
if (!hash_equals($csrf_token, $csrf_z_formulare)) {
$chyba = 'Neplatný požadavek. Zkuste stránku obnovit.';
}
if (empty($chyba)) {
$akce = $_POST['akce'] ?? '';
// ====================================================
// AKCE: Založení nového uživatele
// ====================================================
if ($akce === 'novy_uzivatel') {
$email = trim($_POST['email'] ?? '');
$heslo = $_POST['heslo'] ?? '';
$heslo2 = $_POST['heslo2'] ?? '';
$admin = isset($_POST['admin']) ? 1 : 0;
// Validace emailu
if (empty($email)) {
$chyba = 'Email nesmí být prázdný.';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$chyba = 'Email není platná emailová adresa.';
}
// Validace hesla
if (empty($chyba) && empty($heslo)) {
$chyba = 'Heslo nesmí být prázdné.';
} elseif (empty($chyba) && mb_strlen($heslo) < HESLO_MIN_DELKA) {
$chyba = 'Heslo musí mít alespoň ' . HESLO_MIN_DELKA . ' znaků.';
} elseif (empty($chyba) && $heslo !== $heslo2) {
$chyba = 'Hesla se neshodují.';
}
if (empty($chyba)) {
// Kontrola, zda email již neexistuje
$stmt = $pdo->prepare("
SELECT COUNT(*) AS pocet
FROM `" . DB_TABULKA_UZIVATELE . "`
WHERE `email` = :email
");
$stmt->execute([':email' => $email]);
if ($stmt->fetch()['pocet'] > 0) {
$chyba = 'Uživatel s tímto emailem již existuje.';
}
}
if (empty($chyba)) {
try {
$heslo_hash = password_hash($heslo, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("
INSERT INTO `" . DB_TABULKA_UZIVATELE . "`
(`email`, `heslo`, `admin`)
VALUES
(:email, :heslo, :admin)
");
$stmt->execute([
':email' => $email,
':heslo' => $heslo_hash,
':admin' => $admin,
]);
$novy_id = $pdo->lastInsertId();
// Vytvoříme prázdný řádek v tabulce služby
if (DB_TABULKA_SLUZBA !== '') {
$stmt2 = $pdo->prepare("
INSERT INTO `" . DB_TABULKA_SLUZBA . "`
(`uzivatel_id`)
VALUES
(:uzivatel_id)
");
$stmt2->execute([':uzivatel_id' => $novy_id]);
}
$zprava = 'Uživatel ' . htmlspecialchars($email) . ' byl úspěšně vytvořen.';
} catch (PDOException $e) {
error_log('Admin chyba při vytváření uživatele: ' . $e->getMessage());
$chyba = 'Při vytváření uživatele došlo k chybě databáze.';
}
}
}
// ====================================================
// AKCE: Změna hesla uživatele
// ====================================================
elseif ($akce === 'zmena_hesla') {
$uzivatel_id = (int) ($_POST['uzivatel_id'] ?? 0);
$heslo = $_POST['heslo'] ?? '';
$heslo2 = $_POST['heslo2'] ?? '';
if ($uzivatel_id <= 0) {
$chyba = 'Neplatné ID uživatele.';
} elseif (empty($heslo)) {
$chyba = 'Heslo nesmí být prázdné.';
} elseif (mb_strlen($heslo) < HESLO_MIN_DELKA) {
$chyba = 'Heslo musí mít alespoň ' . HESLO_MIN_DELKA . ' znaků.';
} elseif ($heslo !== $heslo2) {
$chyba = 'Hesla se neshodují.';
}
if (empty($chyba)) {
try {
$heslo_hash = password_hash($heslo, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("
UPDATE `" . DB_TABULKA_UZIVATELE . "`
SET `heslo` = :heslo
WHERE `id` = :id
");
$stmt->execute([
':heslo' => $heslo_hash,
':id' => $uzivatel_id,
]);
// Po změně hesla smažeme všechny remember me tokeny
// tohoto uživatele odhlásíme ho ze všech zařízení
$stmt2 = $pdo->prepare("
DELETE FROM `" . DB_TABULKA_TOKENY . "`
WHERE `uzivatel_id` = :uzivatel_id
");
$stmt2->execute([':uzivatel_id' => $uzivatel_id]);
$zprava = 'Heslo uživatele bylo úspěšně změněno. Uživatel byl odhlášen ze všech zařízení.';
} catch (PDOException $e) {
error_log('Admin chyba při změně hesla: ' . $e->getMessage());
$chyba = 'Při změně hesla došlo k chybě databáze.';
}
}
}
// ====================================================
// AKCE: Smazání uživatele
// ====================================================
elseif ($akce === 'smazat_uzivatele') {
$uzivatel_id = (int) ($_POST['uzivatel_id'] ?? 0);
if ($uzivatel_id <= 0) {
$chyba = 'Neplatné ID uživatele.';
}
// Admin nesmí smazat sám sebe
if (empty($chyba) && $uzivatel_id === (int) $auth_uzivatel['id']) {
$chyba = 'Nemůžeš smazat svůj vlastní účet.';
}
if (empty($chyba)) {
try {
// Díky FOREIGN KEY s ON DELETE CASCADE se automaticky
// smažou i záznamy v auth_remember_tokens a auth_password_resets
// a v tabulce služby (users)
$stmt = $pdo->prepare("
DELETE FROM `" . DB_TABULKA_UZIVATELE . "`
WHERE `id` = :id
");
$stmt->execute([':id' => $uzivatel_id]);
$zprava = 'Uživatel byl úspěšně smazán.';
} catch (PDOException $e) {
error_log('Admin chyba při mazání uživatele: ' . $e->getMessage());
$chyba = 'Při mazání uživatele došlo k chybě databáze.';
}
}
}
// ====================================================
// AKCE: Testovací email
// ====================================================
elseif ($akce === 'test_email') {
$vysledek = odesli_mail(
MAIL_TEST_ADRESA,
'Testovací email ' . PROJEKT_NAZEV,
"Toto je testovací email ze systému přihlašování.\n\n"
. "Projekt: " . PROJEKT_NAZEV . "\n"
. "URL: " . PROJEKT_URL . "\n"
. "Čas odeslání: " . date('Y-m-d H:i:s') . "\n\n"
. "Pokud tento email vidíš, odesílání emailů funguje správně."
);
if ($vysledek) {
$zprava = 'Testovací email byl odeslán na ' . htmlspecialchars(MAIL_TEST_ADRESA) . '.';
} else {
$chyba = 'Odeslání testovacího emailu selhalo. Zkontroluj PHP error log a nastavení v config.php.';
}
}
}
}
// ------------------------------------------------------------
// NAČTENÍ SEZNAMU UŽIVATELŮ
// ------------------------------------------------------------
$uzivatele = [];
try {
$stmt = $pdo->query("
SELECT `id`, `email`, `admin`, `vytvoreno`
FROM `" . DB_TABULKA_UZIVATELE . "`
ORDER BY `vytvoreno` ASC
");
$uzivatele = $stmt->fetchAll();
} catch (PDOException $e) {
error_log('Admin chyba při načítání uživatelů: ' . $e->getMessage());
$chyba = 'Nepodařilo se načíst seznam uživatelů.';
}
// ------------------------------------------------------------
// HTML VÝSTUP
// ------------------------------------------------------------
?>
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<title>Administrace <?php echo htmlspecialchars(PROJEKT_NAZEV); ?></title>
<?php if (AUTH_CSS !== ''): ?>
<link rel="stylesheet" href="<?php echo htmlspecialchars(AUTH_CSS); ?>">
<?php endif; ?>
</head>
<body>
<h1>Administrace</h1>
<h2><?php echo htmlspecialchars(PROJEKT_NAZEV); ?></h2>
<p>Přihlášen jako: <strong><?php echo htmlspecialchars($auth_uzivatel['email']); ?></strong></p>
<?php echo $auth_logout_html; ?>
<?php if (!empty($zprava)): ?>
<p><strong>OK: <?php echo $zprava; ?></strong></p>
<?php endif; ?>
<?php if (!empty($chyba)): ?>
<p><strong>Chyba: <?php echo htmlspecialchars($chyba); ?></strong></p>
<?php endif; ?>
<hr>
<!-- ============================================================
SEZNAM UŽIVATELŮ
============================================================ -->
<h3>Seznam uživatelů</h3>
<?php if (empty($uzivatele)): ?>
<p>Žádní uživatelé.</p>
<?php else: ?>
<table border="1" cellpadding="5">
<tr>
<th>ID</th>
<th>Email</th>
<th>Admin</th>
<th>Registrován</th>
<th>Akce</th>
</tr>
<?php foreach ($uzivatele as $u): ?>
<tr>
<td><?php echo htmlspecialchars($u['id']); ?></td>
<td><?php echo htmlspecialchars($u['email']); ?></td>
<td><?php echo $u['admin'] ? 'ANO' : 'ne'; ?></td>
<td><?php echo htmlspecialchars($u['vytvoreno']); ?></td>
<td>
<!-- Formulář pro změnu hesla tohoto uživatele -->
<form method="POST" action="" style="display:inline;">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="akce" value="zmena_hesla">
<input type="hidden" name="uzivatel_id" value="<?php echo htmlspecialchars($u['id']); ?>">
<input
type="password"
name="heslo"
placeholder="nové heslo"
minlength="<?php echo (int) HESLO_MIN_DELKA; ?>"
required
>
<input
type="password"
name="heslo2"
placeholder="nové heslo znovu"
required
>
<button type="submit">Změnit heslo</button>
</form>
&nbsp;
<?php
// Admin nemůže smazat sám sebe skryjeme tlačítko
if ($u['id'] !== $auth_uzivatel['id']):
?>
<!-- Formulář pro smazání tohoto uživatele -->
<form method="POST" action="" style="display:inline;"
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="akce" value="smazat_uzivatele">
<input type="hidden" name="uzivatel_id" value="<?php echo htmlspecialchars($u['id']); ?>">
<button type="submit">Smazat</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
<hr>
<!-- ============================================================
FORMULÁŘ: Nový uživatel
============================================================ -->
<h3>Přidat nového uživatele</h3>
<form method="POST" action="">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="akce" value="novy_uzivatel">
<p>
<label for="novy-email">Email:</label><br>
<input
type="email"
id="novy-email"
name="email"
required
autocomplete="off"
>
</p>
<p>
<label for="novy-heslo">Heslo:</label><br>
<input
type="password"
id="novy-heslo"
name="heslo"
required
autocomplete="new-password"
>
<br>
<span id="novy-heslo-sila">Síla hesla: zadej heslo</span>
</p>
<p>
<label for="novy-heslo2">Heslo znovu:</label><br>
<input
type="password"
id="novy-heslo2"
name="heslo2"
required
autocomplete="new-password"
>
</p>
<p>
<label>
<input type="checkbox" name="admin" value="1">
Administrátor
</label>
</p>
<p>
<button type="submit" id="novy-tlacitko" disabled>
Vytvořit uživatele
</button>
<span id="novy-tlacitko-duvod"> (čekám na dostatečně silné heslo)</span>
</p>
</form>
<hr>
<!-- ============================================================
KONFIGURACE SYSTÉMU
============================================================ -->
<h3>Konfigurace systému</h3>
<p>Hodnoty jsou načteny z <code>auth/config.php</code>. Citlivé údaje jsou skryté zobrazíš je kliknutím.</p>
<table border="1" cellpadding="5">
<tr>
<th>Konstanta</th>
<th>Hodnota</th>
</tr>
<!-- Databáze -->
<tr><td>DB_HOST</td>
<td><?php echo htmlspecialchars(DB_HOST); ?></td></tr>
<tr><td>DB_NAME</td>
<td><?php echo htmlspecialchars(DB_NAME); ?></td></tr>
<tr><td>DB_USER</td>
<td><?php echo htmlspecialchars(DB_USER); ?></td></tr>
<tr><td>DB_PASS</td>
<td>
<!-- Heslo je skryté, zobrazí se až po kliknutí -->
<span id="db-pass-skryto">[skryto]
<button type="button" onclick="
document.getElementById('db-pass-skryto').style.display='none';
document.getElementById('db-pass-hodnota').style.display='inline';
">Zobrazit</button>
</span>
<span id="db-pass-hodnota" style="display:none;">
<?php echo htmlspecialchars(DB_PASS); ?>
</span>
</td>
</tr>
<tr><td>DB_TABULKA_UZIVATELE</td>
<td><?php echo htmlspecialchars(DB_TABULKA_UZIVATELE); ?></td></tr>
<tr><td>DB_TABULKA_TOKENY</td>
<td><?php echo htmlspecialchars(DB_TABULKA_TOKENY); ?></td></tr>
<tr><td>DB_TABULKA_BRUTE</td>
<td><?php echo htmlspecialchars(DB_TABULKA_BRUTE); ?></td></tr>
<tr><td>DB_TABULKA_RESET</td>
<td><?php echo htmlspecialchars(DB_TABULKA_RESET); ?></td></tr>
<tr><td>DB_TABULKA_SLUZBA</td>
<td><?php echo htmlspecialchars(DB_TABULKA_SLUZBA !== '' ? DB_TABULKA_SLUZBA : '(nevyužito)'); ?></td></tr>
<!-- Projekt -->
<tr><td>PROJEKT_NAZEV</td>
<td><?php echo htmlspecialchars(PROJEKT_NAZEV); ?></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_LOGOUT_URL</td>
<td><?php echo htmlspecialchars(AUTH_LOGOUT_URL); ?></td></tr>
<tr><td>AUTH_REDIRECT_PO_PRIHLASENI</td>
<td><?php echo htmlspecialchars(AUTH_REDIRECT_PO_PRIHLASENI); ?></td></tr>
<!-- Registrace a přístup -->
<tr><td>REGISTRACE_OTEVRENA</td>
<td><?php echo REGISTRACE_OTEVRENA ? 'true (otevřená)' : 'false (pouze admin)'; ?></td></tr>
<tr><td>VYZADOVAT_PRIHLASENI</td>
<td><?php echo VYZADOVAT_PRIHLASENI ? 'true (vždy přesměrovat)' : 'false (stránka rozhoduje sama)'; ?></td></tr>
<!-- Session -->
<tr><td>SESSION_NAZEV</td>
<td><?php echo htmlspecialchars(SESSION_NAZEV); ?></td></tr>
<tr><td>SESSION_EXPIRACE</td>
<td><?php echo (int) SESSION_EXPIRACE; ?> sekund
(<?php echo round(SESSION_EXPIRACE / 3600, 1); ?> hodin)</td></tr>
<!-- Remember me -->
<tr><td>REMEMBER_EXPIRACE</td>
<td><?php echo (int) REMEMBER_EXPIRACE; ?> sekund
(<?php echo round(REMEMBER_EXPIRACE / 86400, 1); ?> dní)</td></tr>
<!-- Brute force -->
<tr><td>BRUTE_MAX_POKUSU</td>
<td><?php echo (int) BRUTE_MAX_POKUSU; ?></td></tr>
<tr><td>BRUTE_OKNO</td>
<td><?php echo (int) BRUTE_OKNO; ?> sekund
(<?php echo round(BRUTE_OKNO / 60, 1); ?> minut)</td></tr>
<!-- Heslo -->
<tr><td>HESLO_MIN_SILA</td>
<td><?php echo (int) HESLO_MIN_SILA; ?> / 4</td></tr>
<tr><td>HESLO_MIN_DELKA</td>
<td><?php echo (int) HESLO_MIN_DELKA; ?> znaků</td></tr>
<!-- Email -->
<tr><td>MAIL_ODESILATEL</td>
<td><?php echo htmlspecialchars(MAIL_ODESILATEL); ?></td></tr>
<tr><td>MAIL_ODESILATEL_JMENO</td>
<td><?php echo htmlspecialchars(MAIL_ODESILATEL_JMENO); ?></td></tr>
<tr><td>MAIL_TEST_ADRESA</td>
<td><?php echo htmlspecialchars(MAIL_TEST_ADRESA); ?></td></tr>
<!-- Styl -->
<tr><td>AUTH_CSS</td>
<td><?php echo AUTH_CSS !== '' ? htmlspecialchars(AUTH_CSS) : '(nevyužito)'; ?></td></tr>
</table>
<hr>
<!-- ============================================================
TESTOVACÍ EMAIL
============================================================ -->
<h3>Test odesílání emailů</h3>
<p>Odešle testovací email na adresu <strong><?php echo htmlspecialchars(MAIL_TEST_ADRESA); ?></strong>
(nastaveno v config.php jako MAIL_TEST_ADRESA).</p>
<form method="POST" action="">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
<input type="hidden" name="akce" value="test_email">
<button type="submit">Odeslat testovací email</button>
</form>
<hr>
<!-- ============================================================
zxcvbn pro formulář nového uživatele
============================================================ -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/zxcvbn/4.4.2/zxcvbn.js"></script>
<script>
const MIN_SILA = <?php echo (int) HESLO_MIN_SILA; ?>;
const POPISKY_SILY = [
'Velmi slabé',
'Slabé',
'Průměrné',
'Silné',
'Velmi silné'
];
const inputHeslo = document.getElementById('novy-heslo');
const spanSila = document.getElementById('novy-heslo-sila');
const tlacitko = document.getElementById('novy-tlacitko');
const spanDuvod = document.getElementById('novy-tlacitko-duvod');
inputHeslo.addEventListener('input', function () {
const hodnota = this.value;
if (hodnota.length === 0) {
spanSila.textContent = 'Síla hesla: zadej heslo';
tlacitko.disabled = true;
spanDuvod.textContent = ' (čekám na dostatečně silné heslo)';
return;
}
const vysledek = zxcvbn(hodnota);
const skore = vysledek.score;
spanSila.textContent = 'Síla hesla: ' + POPISKY_SILY[skore] + ' (' + skore + '/4)';
if (skore >= MIN_SILA) {
tlacitko.disabled = false;
spanDuvod.textContent = '';
} else {
tlacitko.disabled = true;
spanDuvod.textContent = ' (heslo je příliš slabé, potřebuji alespoň ' + MIN_SILA + '/4)';
}
});
</script>
</body>
</html>