[FÁZE-1][install] Vícekrokový průběh instalace pro omezená DB práva (Wedos)

This commit is contained in:
stepan
2026-03-17 09:44:26 +01:00
parent 0504300c52
commit 47ae07946d
+276 -177
View File
@@ -6,11 +6,14 @@
// Vytvoří všechny potřebné tabulky v databázi a prvního
// administrátora.
//
// Průběh:
// Krok 1 pokus o automatické vytvoření tabulek
// Krok 2 pokud selže, zobrazí SQL pro ruční zadání do phpMyAdmin
// Krok 3 ověření, že tabulky existují a mají správnou strukturu
// Krok 4 formulář pro vytvoření prvního admina + zámek
//
// Po úspěšné instalaci se vytvoří soubor install.lock
// a tento skript již nepůjde spustit znovu.
//
// BEZPEČNOST: Po instalaci můžeš install.php smazat,
// nebo ho nechat install.lock ho zablokuje.
// ============================================================
require_once __DIR__ . '/config.php';
@@ -23,131 +26,187 @@ require_once __DIR__ . '/db.php';
$lock_soubor = __DIR__ . '/install.lock';
if (file_exists($lock_soubor)) {
die('<p>Instalace již proběhla. Pokud chceš instalaci opakovat, smaž soubor <code>auth/install.lock</code>. Pozor tím přijdeš o všechna data!</p>');
die('<!DOCTYPE html><html lang="cs"><head><meta charset="UTF-8">
<title>Instalace</title></head><body>
<p>Instalace již proběhla.</p>
<p>Pokud chceš instalaci opakovat, smaž soubor
<code>auth/install.lock</code>. Pozor tím přijdeš o všechna data!</p>
</body></html>');
}
// ------------------------------------------------------------
// ZPRACOVÁNÍ FORMULÁŘE (pokud byl odeslán)
// SQL PŘÍKAZY PRO VYTVOŘENÍ TABULEK
// ------------------------------------------------------------
// Definujeme je jako pole použijeme je jak pro automatické
// spuštění, tak pro zobrazení uživateli k ručnímu zadání.
$sql_tabulky = [
'auth_users' => "CREATE TABLE IF NOT EXISTS `" . DB_TABULKA_UZIVATELE . "` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`email` VARCHAR(255) NOT NULL,
`heslo` VARCHAR(255) NOT NULL,
`admin` TINYINT(1) NOT NULL DEFAULT 0,
`vytvoreno` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'auth_remember_tokens' => "CREATE TABLE IF NOT EXISTS `" . DB_TABULKA_TOKENY . "` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`uzivatel_id` INT UNSIGNED NOT NULL,
`selector` VARCHAR(32) NOT NULL,
`token_hash` VARCHAR(255) NOT NULL,
`expiruje` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `selector` (`selector`),
FOREIGN KEY (`uzivatel_id`)
REFERENCES `" . DB_TABULKA_UZIVATELE . "` (`id`)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'auth_brute_force' => "CREATE TABLE IF NOT EXISTS `" . DB_TABULKA_BRUTE . "` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`ip_adresa` VARCHAR(45) NOT NULL,
`email` VARCHAR(255) NOT NULL,
`cas` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `ip_adresa` (`ip_adresa`),
KEY `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'auth_password_resets' => "CREATE TABLE IF NOT EXISTS `" . DB_TABULKA_RESET . "` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`uzivatel_id` INT UNSIGNED NOT NULL,
`token_hash` VARCHAR(255) NOT NULL,
`expiruje` DATETIME NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (`uzivatel_id`)
REFERENCES `" . DB_TABULKA_UZIVATELE . "` (`id`)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
];
// Tabulka služby je volitelná
if (DB_TABULKA_SLUZBA !== '') {
$sql_tabulky['sluzba'] = "CREATE TABLE IF NOT EXISTS `" . DB_TABULKA_SLUZBA . "` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`uzivatel_id` INT UNSIGNED NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uzivatel_id` (`uzivatel_id`),
FOREIGN KEY (`uzivatel_id`)
REFERENCES `" . DB_TABULKA_UZIVATELE . "` (`id`)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
}
// ------------------------------------------------------------
// POMOCNÁ FUNKCE: Ověření existence tabulek
// ------------------------------------------------------------
// Zkontroluje, zda všechny potřebné tabulky existují v DB.
// Vrací pole chybějících tabulek (prázdné = vše OK).
function zkontroluj_tabulky(PDO $pdo): array
{
// Zjistíme, které tabulky existují v aktuální databázi
$stmt = $pdo->query("SHOW TABLES");
$existuje = $stmt->fetchAll(PDO::FETCH_COLUMN);
$potrebne = [
DB_TABULKA_UZIVATELE,
DB_TABULKA_TOKENY,
DB_TABULKA_BRUTE,
DB_TABULKA_RESET,
];
if (DB_TABULKA_SLUZBA !== '') {
$potrebne[] = DB_TABULKA_SLUZBA;
}
// Vrátíme seznam tabulek, které chybí
return array_diff($potrebne, $existuje);
}
// ------------------------------------------------------------
// URČENÍ AKTUÁLNÍHO KROKU
// ------------------------------------------------------------
$chyba = '';
$uspech = false;
$krok = 1; // výchozí krok
$chyba = '';
$zprava = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Krok se přenáší přes skryté pole ve formuláři
$krok_z_post = (int) ($_POST['krok'] ?? 0);
// ------------------------------------------------------------
// ZPRACOVÁNÍ AKCÍ
// ------------------------------------------------------------
// == KROK 1: Pokus o automatické vytvoření tabulek ============
if ($krok_z_post === 0 || $krok_z_post === 1) {
$auto_ok = true;
foreach ($sql_tabulky as $nazev => $sql) {
try {
$pdo->exec($sql);
} catch (PDOException $e) {
// Automatické vytvoření selhalo uživatel nemá práva
$auto_ok = false;
break;
}
}
if ($auto_ok) {
// Tabulky byly vytvořeny automaticky přeskočíme na krok 4
$krok = 4;
} else {
// Nemáme práva přejdeme na krok 2 (ruční SQL)
$krok = 2;
}
}
// == KROK 3: Ověření tabulek po ručním zadání SQL =============
elseif ($krok_z_post === 3) {
$chybejici = zkontroluj_tabulky($pdo);
if (empty($chybejici)) {
// Všechny tabulky existují přejdeme na krok 4
$krok = 4;
} else {
// Stále chybí tabulky zůstaneme na kroku 2/3
$krok = 3;
$chyba = 'Následující tabulky stále chybí: '
. implode(', ', $chybejici)
. '. Zkontroluj, zda jsi SQL správně zadal do phpMyAdmin.';
}
}
// == KROK 4: Vytvoření prvního admina =========================
elseif ($krok_z_post === 4) {
// Načtení hodnot z formuláře
$email = trim($_POST['email'] ?? '');
$heslo = $_POST['heslo'] ?? '';
$heslo2 = $_POST['heslo2'] ?? '';
// Validace emailu
// Validace
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)) {
} elseif (empty($heslo)) {
$chyba = 'Heslo nesmí být prázdné.';
} elseif (empty($chyba) && mb_strlen($heslo) < HESLO_MIN_DELKA) {
} elseif (mb_strlen($heslo) < HESLO_MIN_DELKA) {
$chyba = 'Heslo musí mít alespoň ' . HESLO_MIN_DELKA . ' znaků.';
} elseif (empty($chyba) && $heslo !== $heslo2) {
} elseif ($heslo !== $heslo2) {
$chyba = 'Hesla se neshodují.';
}
// Pokud není chyba, spustíme instalaci
if (empty($chyba)) {
try {
// ------------------------------------------------
// VYTVOŘENÍ TABULEK
// ------------------------------------------------
// Tabulka uživatelů přihlašovacího systému
// utf8mb4 = plná podpora Unicode (diakritika, emoji, ...)
$pdo->exec("
CREATE TABLE IF NOT EXISTS `" . DB_TABULKA_UZIVATELE . "` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`email` VARCHAR(255) NOT NULL,
`heslo` VARCHAR(255) NOT NULL,
`admin` TINYINT(1) NOT NULL DEFAULT 0,
`vytvoreno` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
");
// Tabulka remember me tokenů
// selector = veřejný identifikátor pro vyhledání záznamu v DB
// token_hash = bcrypt hash tajného tokenu (samotný token je jen v cookie)
$pdo->exec("
CREATE TABLE IF NOT EXISTS `" . DB_TABULKA_TOKENY . "` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`uzivatel_id` INT UNSIGNED NOT NULL,
`selector` VARCHAR(32) NOT NULL,
`token_hash` VARCHAR(255) NOT NULL,
`expiruje` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `selector` (`selector`),
FOREIGN KEY (`uzivatel_id`)
REFERENCES `" . DB_TABULKA_UZIVATELE . "` (`id`)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
");
// Tabulka brute force záznamů
// Ukládají se neúspěšné pokusy o přihlášení (IP adresa, email, čas)
$pdo->exec("
CREATE TABLE IF NOT EXISTS `" . DB_TABULKA_BRUTE . "` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`ip_adresa` VARCHAR(45) NOT NULL,
`email` VARCHAR(255) NOT NULL,
`cas` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `ip_adresa` (`ip_adresa`),
KEY `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
");
// Tabulka tokenů pro obnovu hesla
// Token je jednorázový a platí omezenou dobu (viz config.php)
$pdo->exec("
CREATE TABLE IF NOT EXISTS `" . DB_TABULKA_RESET . "` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`uzivatel_id` INT UNSIGNED NOT NULL,
`token_hash` VARCHAR(255) NOT NULL,
`expiruje` DATETIME NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (`uzivatel_id`)
REFERENCES `" . DB_TABULKA_UZIVATELE . "` (`id`)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
");
// Tabulka uživatelů konkrétní služby (pokud je nastavena v config.php)
// Tato tabulka slouží pro data specifická pro tvůj projekt.
// Zatím obsahuje jen vazbu na auth_users sloupce si přidáš sám.
if (DB_TABULKA_SLUZBA !== '') {
$pdo->exec("
CREATE TABLE IF NOT EXISTS `" . DB_TABULKA_SLUZBA . "` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`uzivatel_id` INT UNSIGNED NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uzivatel_id` (`uzivatel_id`),
FOREIGN KEY (`uzivatel_id`)
REFERENCES `" . DB_TABULKA_UZIVATELE . "` (`id`)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
");
}
// ------------------------------------------------
// VYTVOŘENÍ PRVNÍHO ADMIN UŽIVATELE
// ------------------------------------------------
// Heslo zahashujeme pomocí bcrypt nikdy neukládáme plaintext!
$heslo_hash = password_hash($heslo, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("
@@ -161,9 +220,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
':heslo' => $heslo_hash,
]);
$novy_uzivatel_id = $pdo->lastInsertId();
$novy_id = $pdo->lastInsertId();
// Pokud existuje tabulka služby, vytvoříme k novému uživateli prázdný řádek
if (DB_TABULKA_SLUZBA !== '') {
$stmt2 = $pdo->prepare("
INSERT INTO `" . DB_TABULKA_SLUZBA . "`
@@ -171,33 +229,34 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
VALUES
(:uzivatel_id)
");
$stmt2->execute([':uzivatel_id' => $novy_uzivatel_id]);
$stmt2->execute([':uzivatel_id' => $novy_id]);
}
// ------------------------------------------------
// VYTVOŘENÍ ZÁMKU zabránění opakované instalaci
// ------------------------------------------------
// Vytvoříme zámek instalace je hotová
file_put_contents($lock_soubor, 'Instalace proběhla: ' . date('Y-m-d H:i:s'));
$uspech = true;
$krok = 5; // krok 5 = úspěšné dokončení
} catch (PDOException $e) {
// Chybu zalogujeme, uživateli zobrazíme obecnou hlášku
error_log('Chyba při instalaci: ' . $e->getMessage());
$chyba = 'Při instalaci došlo k chybě databáze. Zkontroluj nastavení v config.php a PHP error log.';
} catch (Exception $e) {
error_log('Chyba při instalaci: ' . $e->getMessage());
$chyba = 'Při instalaci došlo k neočekávané chybě.';
error_log('Chyba při instalaci (admin): ' . $e->getMessage());
$chyba = 'Při vytváření admina došlo k chybě databáze. Zkontroluj PHP error log.';
$krok = 4;
}
} else {
// Validace selhala zůstaneme na kroku 4
$krok = 4;
}
}
// ------------------------------------------------------------
// HTML VÝSTUP
// SESTAVENÍ SQL TEXTU PRO ZOBRAZENÍ UŽIVATELI (krok 2)
// ------------------------------------------------------------
// Poznámka: žádné stylování vše je záměrně prosté,
// aby žádný prvek nebyl skrytý nebo překrytý.
$sql_pro_zobrazeni = implode(";\n\n", $sql_tabulky) . ';';
// ------------------------------------------------------------
// HTML VÝSTUP
// ------------------------------------------------------------
?>
<!DOCTYPE html>
@@ -211,23 +270,70 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<h1>Instalace systému přihlašování</h1>
<h2><?php echo htmlspecialchars(PROJEKT_NAZEV); ?></h2>
<?php if ($uspech): ?>
<?php if (!empty($chyba)): ?>
<p><strong>Chyba: <?php echo htmlspecialchars($chyba); ?></strong></p>
<?php endif; ?>
<p><strong>Instalace proběhla úspěšně!</strong></p>
<p>Tabulky byly vytvořeny a první administrátor byl založen.</p>
<p>Byl vytvořen soubor <code>auth/install.lock</code> instalaci již nelze spustit znovu.</p>
<p><a href="<?php echo htmlspecialchars(AUTH_LOGIN_URL); ?>">Přejít na přihlášení</a></p>
<?php
// ============================================================
// KROK 2: Ruční SQL pro phpMyAdmin
// ============================================================
if ($krok === 2): ?>
<?php else: ?>
<p>Nepodařilo se automaticky vytvořit tabulky v databázi
pravděpodobně nemá databázový uživatel dostatečná oprávnění
(CREATE TABLE).</p>
<p>Tento skript vytvoří potřebné tabulky v databázi a založí prvního administrátora.</p>
<p>Spusť ho pouze jednou. Po instalaci bude zablokován souborem <code>install.lock</code>.</p>
<p><strong>Postup:</strong></p>
<ol>
<li>Zkopíruj SQL níže.</li>
<li>Otevři phpMyAdmin a vyber svou databázi
(<code><?php echo htmlspecialchars(DB_NAME); ?></code>).</li>
<li>Klikni na záložku <strong>SQL</strong>, vlož SQL a spusť ho.</li>
<li>Vrať se sem a klikni na tlačítko níže.</li>
</ol>
<?php if (!empty($chyba)): ?>
<p><strong>Chyba: <?php echo htmlspecialchars($chyba); ?></strong></p>
<?php endif; ?>
<textarea rows="30" cols="100" onclick="this.select();"
style="font-family: monospace; font-size: 13px;"
><?php echo htmlspecialchars($sql_pro_zobrazeni); ?></textarea>
<p>
<form method="POST" action="">
<input type="hidden" name="krok" value="3">
<button type="submit">Zkontrolovat tabulky a pokračovat</button>
</form>
</p>
<?php
// ============================================================
// KROK 3: Tabulky stále chybí (chyba je zobrazena výše)
// ============================================================
elseif ($krok === 3): ?>
<p>Tabulky ještě nejsou v pořádku. Zkontroluj phpMyAdmin a zkus to znovu.</p>
<textarea rows="30" cols="100" onclick="this.select();"
style="font-family: monospace; font-size: 13px;"
><?php echo htmlspecialchars($sql_pro_zobrazeni); ?></textarea>
<p>
<form method="POST" action="">
<input type="hidden" name="krok" value="3">
<button type="submit">Zkontrolovat tabulky znovu</button>
</form>
</p>
<?php
// ============================================================
// KROK 4: Formulář pro vytvoření prvního admina
// ============================================================
elseif ($krok === 4): ?>
<p><strong>Tabulky jsou připraveny.</strong>
Nyní vytvoř prvního administrátora.</p>
<form method="POST" action="">
<input type="hidden" name="krok" value="4">
<p>
<label for="email">Email administrátora:</label><br>
@@ -249,7 +355,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
required
>
<br>
<!-- Indikátor síly hesla vyplní ho zxcvbn přes JavaScript -->
<span id="heslo-sila">Síla hesla: zadej heslo</span>
</p>
@@ -264,66 +369,60 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
</p>
<p>
<!-- Tlačítko je ve výchozím stavu zakázané.
JavaScript ho povolí, až heslo dosáhne požadované síly. -->
<button type="submit" id="tlacitko-odeslat" disabled>
Spustit instalaci
Dokončit instalaci
</button>
<span id="tlacitko-duvod"> (čekám na dostatečně silné heslo)</span>
</p>
</form>
<?php
// ============================================================
// KROK 5: Instalace úspěšně dokončena
// ============================================================
elseif ($krok === 5): ?>
<p><strong>Instalace proběhla úspěšně!</strong></p>
<p>Tabulky byly vytvořeny a první administrátor byl založen.</p>
<p>Byl vytvořen soubor <code>auth/install.lock</code>
instalaci již nelze spustit znovu.</p>
<p><a href="<?php echo htmlspecialchars(AUTH_LOGIN_URL); ?>">Přejít na přihlášení</a></p>
<?php endif; ?>
<!-- ============================================================
zxcvbn knihovna pro hodnocení síly hesla od Dropboxu
Načítáme z CDN. Pokud chceš offline použití, stáhni soubor
a změň src na lokální cestu.
============================================================ -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/zxcvbn/4.4.2/zxcvbn.js"></script>
<script>
// Minimální požadovaná síla hesla (přebíráme z PHP konfigurace)
const MIN_SILA = <?php echo (int) HESLO_MIN_SILA; ?>;
// Popisky síly hesla (0 = nejslabší, 4 = nejsilnější)
const POPISKY_SILY = [
'Velmi slabé',
'Slabé',
'Průměrné',
'Silné',
'Velmi silné'
];
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('heslo');
const spanSila = document.getElementById('heslo-sila');
const tlacitko = document.getElementById('tlacitko-odeslat');
const spanDuvod = document.getElementById('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;
}
// Spuštění zxcvbn hodnocení
const vysledek = zxcvbn(hodnota);
const skore = vysledek.score; // 04
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)';
}
});
// Prvky existují jen na kroku 4
if (inputHeslo) {
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>