Add user registration and login functionality for /adminer

This commit is contained in:
Fabian Schieder 2026-02-28 20:09:00 +01:00
parent 68bbf405e4
commit d8814a1dfc
5 changed files with 208 additions and 38 deletions

2
.env
View File

@ -3,7 +3,7 @@ DB_SERVERNAME=localhost
DB_PORT=3306
DB_USERNAME=FSST
DB_PASSWORD=L9wUNZZ9Qkbt
DB_DATABASE=FSS_T
DB_DATABASE=FabianWebsite
# Optional: Basis-URL (wenn du was dynamisch bauen willst)
APP_URL=https://fabianschieder.com

View File

@ -2,12 +2,20 @@
DB_SERVERNAME=localhost
DB_PORT=3306
DB_USERNAME=FSST
DB_PASSWORD=L9wUNZZ9Qkbt
DB_DATABASE=FSS_T
DB_PASSWORD=
DB_DATABASE=FabianWebsite
# Optional: Basis-URL (wenn du was dynamisch bauen willst)
APP_URL=https://fabianschieder.com
# Basic Auth für /adminer (zusätzlicher Schutz)
ADMINER_BASIC_USER=admin
ADMINER_BASIC_PASS=L9wUNZZ9Qkbt
# (Deprecated) Basic Auth für /adminer wird nicht mehr verwendet
ADMINER_BASIC_USER=
ADMINER_BASIC_PASS=
# App-Login für /adminer
# Registrierung erlauben? 1=ja, 0=nein
ADMINER_ALLOW_REGISTER=1
# Optional: initialen User automatisch anlegen (einmalig)
ADMINER_APP_SEED_USER=
ADMINER_APP_SEED_PASS=

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/user_auth.php';
// CLI smoke test: php adminer/_smoke_register.php username password
// Uses FabianWebsite via user_auth.php config.
$username = $argv[1] ?? 'smoketest_user';
$password = $argv[2] ?? 'Smoketest123!';
try {
adminer_app_bootstrap();
$r = adminer_app_try_register($username, $password, $password);
if (!empty($r['ok'])) {
echo "REGISTER OK\n";
} else {
echo "REGISTER FAIL: " . ($r['error'] ?? 'unknown') . "\n";
}
$l = adminer_app_try_login($username, $password);
if (!empty($l['ok'])) {
echo "LOGIN OK\n";
} else {
echo "LOGIN FAIL: " . ($l['error'] ?? 'unknown') . "\n";
}
} catch (Throwable $e) {
echo "EXCEPTION: " . $e->getMessage() . "\n";
exit(1);
}

View File

@ -14,6 +14,7 @@ try {
exit;
}
$appPage = (string)($_GET['page'] ?? 'login'); // login|register
$appAction = (string)($_GET['auth'] ?? '');
if ($appAction === 'logout') {
adminer_app_logout();
@ -32,27 +33,81 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') ==
exit;
}
$appError = (string)($res['error'] ?? 'Login fehlgeschlagen.');
$appPage = 'login';
}
// Handle registration
$appRegError = null;
$appRegOk = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string)($_POST['action'] ?? '') === 'app_register') {
$u = (string)($_POST['username'] ?? '');
$p1 = (string)($_POST['password'] ?? '');
$p2 = (string)($_POST['password2'] ?? '');
$res = adminer_app_try_register($u, $p1, $p2);
if (!empty($res['ok'])) {
// Auto-login after register
$loginRes = adminer_app_try_login($u, $p1);
if (!empty($loginRes['ok'])) {
header('Location: /adminer', true, 302);
exit;
}
$appRegOk = 'Registrierung erfolgreich. Bitte einloggen.';
$appPage = 'login';
} else {
$appRegError = (string)($res['error'] ?? 'Registrierung fehlgeschlagen.');
$appPage = 'register';
}
}
if (!adminer_app_is_logged_in()) {
$canRegister = adminer_app_allow_register();
$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>';
// Tabs
$body .= '<div style="display:flex;gap:10px;margin-bottom:14px">'
. '<a class="btn secondary" href="/adminer?page=login" style="text-decoration:none">Login</a>'
. ($canRegister ? '<a class="btn secondary" href="/adminer?page=register" style="text-decoration:none">Registrieren</a>' : '')
. '</div>';
if ($appRegOk) {
$body .= '<div class="ok">' . h($appRegOk) . '</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>';
if ($appPage === 'register') {
if (!$canRegister) {
$body .= '<div class="err">Registrierung ist deaktiviert.</div>';
} else {
if ($appRegError) $body .= '<div class="err">' . h($appRegError) . '</div><br>';
$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 .= '<h3>Registrieren</h3>';
$body .= '<form method="post">'
. '<input type="hidden" name="action" value="app_register">'
. '<label>Benutzername<br><input name="username" autocomplete="username"></label><br><br>'
. '<label>Passwort<br><input name="password" type="password" autocomplete="new-password"></label><br><br>'
. '<label>Passwort wiederholen<br><input name="password2" type="password" autocomplete="new-password"></label><br><br>'
. '<button class="btn" type="submit">Account erstellen</button>'
. '</form>';
$body .= '<div class="muted" style="margin-top:12px">Dein Account wird in <code>FabianWebsite.adminer_users</code> gespeichert.</div>';
}
} else {
if ($appError) $body .= '<div class="err">' . h($appError) . '</div><br>';
$body .= '<h3>Login</h3>';
$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>';
if ($canRegister) {
$body .= '<div class="muted" style="margin-top:12px">Noch kein Account? <a href="/adminer?page=register">Registrieren</a></div>';
}
}
$body .= '</div>';

View File

@ -1,18 +1,22 @@
<?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.
* Alles liegt in der DB "FabianWebsite".
*/
function adminer_app_session_start()
{
// CLI: keine Sessions/Headers
if (PHP_SAPI === 'cli') {
return;
}
if (session_status() === PHP_SESSION_NONE) {
ini_set('session.cookie_httponly', '1');
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
@ -22,32 +26,54 @@ function adminer_app_session_start()
}
}
function adminer_app_pdo()
function adminer_app_config()
{
$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 === '') {
// DB fest auf FabianWebsite, wie gewünscht.
$cfg = [
'host' => (string)env_get($vars, 'DB_SERVERNAME', 'localhost'),
'port' => (int)env_get($vars, 'DB_PORT', '3306'),
'user' => (string)env_get($vars, 'DB_USERNAME', ''),
'pass' => (string)env_get($vars, 'DB_PASSWORD', ''),
'db' => 'FabianWebsite',
// Registrierung optional abschaltbar
'allow_register' => ((string)env_get($vars, 'ADMINER_ALLOW_REGISTER', '1')) !== '0',
];
return $cfg;
}
function adminer_app_pdo()
{
$cfg = adminer_app_config();
if ($cfg['host'] === '' || (int)$cfg['port'] <= 0 || $cfg['user'] === '' || $cfg['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);
$dsn = sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', $cfg['host'], (int)$cfg['port'], $cfg['db']);
return new PDO($dsn, $user, $pass, [
return new PDO($dsn, $cfg['user'], $cfg['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
function adminer_validate_username($u)
{
$u = trim((string)$u);
if ($u === '') return [false, 'Benutzername darf nicht leer sein.'];
if (strlen($u) < 3) return [false, 'Benutzername ist zu kurz (min. 3 Zeichen).'];
if (strlen($u) > 50) return [false, 'Benutzername ist zu lang (max. 50 Zeichen).'];
if (!preg_match('/^[A-Za-z0-9_.-]+$/', $u)) return [false, 'Nur Buchstaben, Zahlen, Punkt, Unterstrich und Minus sind erlaubt.'];
return [true, null];
}
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,'
@ -64,12 +90,12 @@ function adminer_app_bootstrap()
if ($seedUser !== '' && $seedPass !== '') {
$stmt = $pdo->prepare('SELECT id FROM adminer_users WHERE username = ?');
$stmt->execute([$seedUser]);
$stmt->execute([(string)$seedUser]);
$exists = (bool)$stmt->fetchColumn();
if (!$exists) {
$hash = password_hash($seedPass, PASSWORD_DEFAULT);
$hash = password_hash((string)$seedPass, PASSWORD_DEFAULT);
$ins = $pdo->prepare('INSERT INTO adminer_users (username, password_hash) VALUES (?, ?)');
$ins->execute([$seedUser, $hash]);
$ins->execute([(string)$seedUser, $hash]);
}
}
}
@ -77,12 +103,18 @@ function adminer_app_bootstrap()
function adminer_app_is_logged_in()
{
adminer_app_session_start();
if (PHP_SAPI === 'cli') {
return false;
}
return !empty($_SESSION['adminer_app']['ok']);
}
function adminer_app_logout()
{
adminer_app_session_start();
if (PHP_SAPI === 'cli') {
return;
}
unset($_SESSION['adminer_app']);
}
@ -105,11 +137,54 @@ function adminer_app_try_login($username, $password)
}
adminer_app_session_start();
$_SESSION['adminer_app'] = [
'ok' => true,
'username' => $username,
'uid' => (int)$row['id'],
];
if (PHP_SAPI !== 'cli') {
$_SESSION['adminer_app'] = [
'ok' => true,
'username' => $username,
'uid' => (int)$row['id'],
];
}
return ['ok' => true, 'error' => null];
}
function adminer_app_allow_register()
{
$cfg = adminer_app_config();
return !empty($cfg['allow_register']);
}
function adminer_app_try_register($username, $password, $password2)
{
if (!adminer_app_allow_register()) {
return ['ok' => false, 'error' => 'Registrierung ist deaktiviert.'];
}
list($ok, $msg) = adminer_validate_username($username);
if (!$ok) return ['ok' => false, 'error' => $msg];
$password = (string)$password;
$password2 = (string)$password2;
if (strlen($password) < 8) {
return ['ok' => false, 'error' => 'Passwort ist zu kurz (min. 8 Zeichen).'];
}
if ($password !== $password2) {
return ['ok' => false, 'error' => 'Passwörter stimmen nicht überein.'];
}
$pdo = adminer_app_pdo();
// Unique check
$stmt = $pdo->prepare('SELECT id FROM adminer_users WHERE username = ?');
$stmt->execute([trim((string)$username)]);
if ($stmt->fetchColumn()) {
return ['ok' => false, 'error' => 'Benutzername ist bereits vergeben.'];
}
$hash = password_hash($password, PASSWORD_DEFAULT);
$ins = $pdo->prepare('INSERT INTO adminer_users (username, password_hash) VALUES (?, ?)');
$ins->execute([trim((string)$username), $hash]);
return ['ok' => true, 'error' => null];
}