Implement app-based login for /adminer with user management and session handling
This commit is contained in:
parent
441ca3a67a
commit
68bbf405e4
@ -2,17 +2,32 @@
|
|||||||
|
|
||||||
Dieses Projekt enthält eine kleine, selbst implementierte DB-Verwaltung unter `/adminer`.
|
Dieses Projekt enthält eine kleine, selbst implementierte DB-Verwaltung unter `/adminer`.
|
||||||
|
|
||||||
|
## Schutz / Login
|
||||||
|
Statt Browser-Basic-Auth gibt es eine eigene Login-Seite.
|
||||||
|
|
||||||
|
- User werden in der Datenbanktabelle `adminer_users` gespeichert.
|
||||||
|
- Passwörter werden gehasht (`password_hash`).
|
||||||
|
|
||||||
|
### Initialen User anlegen (Seed)
|
||||||
|
Setze in deiner lokalen `.env` (Projekt-Root) einmalig:
|
||||||
|
|
||||||
|
- `ADMINER_APP_SEED_USER=...`
|
||||||
|
- `ADMINER_APP_SEED_PASS=...`
|
||||||
|
|
||||||
|
Beim ersten Aufruf von `/adminer` wird (falls der User noch nicht existiert) automatisch ein Nutzer angelegt.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
1. Erstelle eine lokale `.env` im Projekt-Root (siehe `.env.example`).
|
1. Erstelle eine lokale `.env` im Projekt-Root (siehe `.env.example`).
|
||||||
2. Trage dort deine DB-Zugangsdaten ein.
|
2. Trage dort deine DB-Zugangsdaten ein (`DB_*`).
|
||||||
|
|
||||||
> Wichtig: `.env` wird durch `.gitignore` ignoriert.
|
> Wichtig: `.env` wird durch `.gitignore` ignoriert.
|
||||||
|
|
||||||
## Nutzung
|
## Nutzung
|
||||||
- Öffne im Browser: `/adminer`
|
- Öffne im Browser: `/adminer`
|
||||||
- Login erfolgt über das Formular.
|
- Schritt 1: App-Login
|
||||||
|
- Schritt 2: DB-Verbindung testen und Datenbanken laden
|
||||||
|
- Schritt 3: In gewünschte Datenbank einloggen
|
||||||
|
|
||||||
## Hinweise
|
## Hinweise
|
||||||
- Das Tool ist bewusst minimal (Tabellenliste + Browse + einfache SQL-Query).
|
- Das Tool ist bewusst minimal (Tabellenliste + Browse + einfache SQL-Query).
|
||||||
- Für produktive Nutzung bitte zusätzlich absichern (z.B. Basic Auth / IP-Allowlist).
|
- Für produktive Nutzung bitte zusätzlich absichern (z.B. IP-Allowlist, VPN).
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,66 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
require_once __DIR__ . '/basic_auth.php';
|
require_once __DIR__ . '/user_auth.php';
|
||||||
adminer_require_basic_auth();
|
require_once __DIR__ . '/views.php';
|
||||||
|
|
||||||
|
adminer_app_session_start();
|
||||||
|
|
||||||
|
// Bootstrap users table (+ optional seed)
|
||||||
|
try {
|
||||||
|
adminer_app_bootstrap();
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
admin_layout('DB-Verwaltung', '<div class="top"><h1>DB-Verwaltung</h1></div><div class="card"><div class="err">' . h($e->getMessage()) . '</div></div>');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$appAction = (string)($_GET['auth'] ?? '');
|
||||||
|
if ($appAction === 'logout') {
|
||||||
|
adminer_app_logout();
|
||||||
|
header('Location: /adminer', true, 302);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle app login
|
||||||
|
$appError = null;
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'app_login') {
|
||||||
|
$u = (string)($_POST['username'] ?? '');
|
||||||
|
$p = (string)($_POST['password'] ?? '');
|
||||||
|
$res = adminer_app_try_login($u, $p);
|
||||||
|
if (!empty($res['ok'])) {
|
||||||
|
header('Location: /adminer', true, 302);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$appError = (string)($res['error'] ?? 'Login fehlgeschlagen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!adminer_app_is_logged_in()) {
|
||||||
|
$body = '<div class="top"><h1>DB-Verwaltung</h1><span class="pill">Login</span></div>';
|
||||||
|
$body .= '<div class="card">';
|
||||||
|
$body .= '<p class="muted">Bitte melde dich an, um die DB-Verwaltung zu öffnen.</p>';
|
||||||
|
|
||||||
|
if ($appError) {
|
||||||
|
$body .= '<div class="err">' . h($appError) . '</div><br>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$body .= '<form method="post">'
|
||||||
|
. '<input type="hidden" name="action" value="app_login">'
|
||||||
|
. '<label>Benutzername<br><input name="username" autocomplete="username"></label><br><br>'
|
||||||
|
. '<label>Passwort<br><input name="password" type="password" autocomplete="current-password"></label><br><br>'
|
||||||
|
. '<button class="btn" type="submit">Anmelden</button>'
|
||||||
|
. '</form>';
|
||||||
|
|
||||||
|
$body .= '<hr style="border:0;border-top:1px solid rgba(255,255,255,.10);margin:16px 0">';
|
||||||
|
$body .= '<div class="muted">Admin-Hinweis: Du kannst initial einen User per .env seeden: '
|
||||||
|
. '<code>ADMINER_APP_SEED_USER</code> + <code>ADMINER_APP_SEED_PASS</code> (einmalig; wird nicht überschrieben).</div>';
|
||||||
|
|
||||||
|
$body .= '</div>';
|
||||||
|
|
||||||
|
admin_layout('DB-Verwaltung', $body);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
require_once __DIR__ . '/auth.php';
|
require_once __DIR__ . '/auth.php';
|
||||||
require_once __DIR__ . '/views.php';
|
|
||||||
|
|
||||||
admin_session_start();
|
admin_session_start();
|
||||||
|
|
||||||
@ -109,7 +164,7 @@ if (!admin_is_logged_in()) {
|
|||||||
$body .= "<div>";
|
$body .= "<div>";
|
||||||
$body .= "<h3>Hinweise</h3>";
|
$body .= "<h3>Hinweise</h3>";
|
||||||
$body .= "<ul class=\"muted\">";
|
$body .= "<ul class=\"muted\">";
|
||||||
$body .= "<li>Basic Auth ist aktiv (Credentials in <code>.env</code>).</li>";
|
$body .= "<li>Der Zugriff ist durch den App-Login geschützt.</li>";
|
||||||
$body .= "<li>Für produktive Nutzung zusätzlich mit IP-Allowlist kombinieren.</li>";
|
$body .= "<li>Für produktive Nutzung zusätzlich mit IP-Allowlist kombinieren.</li>";
|
||||||
$body .= "</ul>";
|
$body .= "</ul>";
|
||||||
$body .= "</div>";
|
$body .= "</div>";
|
||||||
@ -195,7 +250,10 @@ try {
|
|||||||
|
|
||||||
$body = '<div class="top">'
|
$body = '<div class="top">'
|
||||||
. '<div><h1>DB-Verwaltung</h1><div class="muted">eingeloggt</div></div>'
|
. '<div><h1>DB-Verwaltung</h1><div class="muted">eingeloggt</div></div>'
|
||||||
. '<div><a class="btn secondary" href="/adminer?a=logout">Logout</a></div>'
|
. '<div style="display:flex;gap:10px">'
|
||||||
|
. '<a class="btn secondary" href="/adminer?auth=logout">App-Logout</a>'
|
||||||
|
. '<a class="btn secondary" href="/adminer?a=logout">DB-Logout</a>'
|
||||||
|
. '</div>'
|
||||||
. '</div>';
|
. '</div>';
|
||||||
|
|
||||||
$body .= '<div class="grid">';
|
$body .= '<div class="grid">';
|
||||||
|
|||||||
116
adminer/user_auth.php
Normal file
116
adminer/user_auth.php
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/auth.php';
|
||||||
|
require_once __DIR__ . '/env.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App-Login (schöne Login-Seite) für /adminer.
|
||||||
|
* Nutzer werden in einer MySQL-Tabelle gespeichert.
|
||||||
|
*
|
||||||
|
* Wir nutzen die DB-Verbindungsdaten aus .env (DB_*), um die User-Tabelle zu hosten.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function adminer_app_session_start()
|
||||||
|
{
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
ini_set('session.cookie_httponly', '1');
|
||||||
|
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
||||||
|
ini_set('session.cookie_secure', '1');
|
||||||
|
}
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function adminer_app_pdo()
|
||||||
|
{
|
||||||
|
$vars = env_load(dirname(__DIR__) . '/.env');
|
||||||
|
$host = env_get($vars, 'DB_SERVERNAME', 'localhost');
|
||||||
|
$port = (int)env_get($vars, 'DB_PORT', '3306');
|
||||||
|
$user = env_get($vars, 'DB_USERNAME', '');
|
||||||
|
$pass = env_get($vars, 'DB_PASSWORD', '');
|
||||||
|
$db = env_get($vars, 'DB_DATABASE', '');
|
||||||
|
|
||||||
|
if ($host === '' || $port <= 0 || $user === '' || $db === '') {
|
||||||
|
throw new RuntimeException('DB_* ist in .env nicht vollständig gesetzt (für Admin-Login-User-Store).');
|
||||||
|
}
|
||||||
|
|
||||||
|
$dsn = sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', $host, $port, $db);
|
||||||
|
|
||||||
|
return new PDO($dsn, $user, $pass, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function adminer_app_bootstrap()
|
||||||
|
{
|
||||||
|
$pdo = adminer_app_pdo();
|
||||||
|
|
||||||
|
// Minimal users table
|
||||||
|
$pdo->exec(
|
||||||
|
'CREATE TABLE IF NOT EXISTS adminer_users ('
|
||||||
|
. 'id INT AUTO_INCREMENT PRIMARY KEY,'
|
||||||
|
. 'username VARCHAR(190) NOT NULL UNIQUE,'
|
||||||
|
. 'password_hash VARCHAR(255) NOT NULL,'
|
||||||
|
. 'created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP'
|
||||||
|
. ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Optional: auto-seed from .env ADMINER_APP_SEED_USER/PASS
|
||||||
|
$vars = env_load(dirname(__DIR__) . '/.env');
|
||||||
|
$seedUser = env_get($vars, 'ADMINER_APP_SEED_USER', '');
|
||||||
|
$seedPass = env_get($vars, 'ADMINER_APP_SEED_PASS', '');
|
||||||
|
|
||||||
|
if ($seedUser !== '' && $seedPass !== '') {
|
||||||
|
$stmt = $pdo->prepare('SELECT id FROM adminer_users WHERE username = ?');
|
||||||
|
$stmt->execute([$seedUser]);
|
||||||
|
$exists = (bool)$stmt->fetchColumn();
|
||||||
|
if (!$exists) {
|
||||||
|
$hash = password_hash($seedPass, PASSWORD_DEFAULT);
|
||||||
|
$ins = $pdo->prepare('INSERT INTO adminer_users (username, password_hash) VALUES (?, ?)');
|
||||||
|
$ins->execute([$seedUser, $hash]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function adminer_app_is_logged_in()
|
||||||
|
{
|
||||||
|
adminer_app_session_start();
|
||||||
|
return !empty($_SESSION['adminer_app']['ok']);
|
||||||
|
}
|
||||||
|
|
||||||
|
function adminer_app_logout()
|
||||||
|
{
|
||||||
|
adminer_app_session_start();
|
||||||
|
unset($_SESSION['adminer_app']);
|
||||||
|
}
|
||||||
|
|
||||||
|
function adminer_app_try_login($username, $password)
|
||||||
|
{
|
||||||
|
$username = trim((string)$username);
|
||||||
|
$password = (string)$password;
|
||||||
|
|
||||||
|
if ($username === '' || $password === '') {
|
||||||
|
return ['ok' => false, 'error' => 'Bitte Benutzername und Passwort eingeben.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = adminer_app_pdo();
|
||||||
|
$stmt = $pdo->prepare('SELECT id, password_hash FROM adminer_users WHERE username = ?');
|
||||||
|
$stmt->execute([$username]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$row || empty($row['password_hash']) || !password_verify($password, (string)$row['password_hash'])) {
|
||||||
|
return ['ok' => false, 'error' => 'Login fehlgeschlagen.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
adminer_app_session_start();
|
||||||
|
$_SESSION['adminer_app'] = [
|
||||||
|
'ok' => true,
|
||||||
|
'username' => $username,
|
||||||
|
'uid' => (int)$row['id'],
|
||||||
|
];
|
||||||
|
|
||||||
|
return ['ok' => true, 'error' => null];
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user