Session-basierte Authentifizierung + Benutzerverwaltung
- HTTP Basic Auth durch cookie-basierte Session-Auth ersetzt (24 h absolutes Timeout) - Neue users-Tabelle (Migration 005) mit bcrypt-Passwort-Hashing - Login-/Logout-Routen und eigenständiges Login-Template - Ersteinrichtungsflow: erster Benutzer beim ersten Start direkt über /login anlegen - Neuer Menüpunkt "Benutzer" (/useradmin) zum Anlegen, Deaktivieren und Löschen von Benutzern - AUTH_USER/AUTH_PASS aus Konfiguration und docker-compose.yml entfernt
This commit is contained in:
parent
83276fff09
commit
a338631dbd
13 changed files with 508 additions and 24 deletions
|
|
@ -4,9 +4,5 @@ DB_NAME=steuerkram
|
|||
DB_USER=steuerkram
|
||||
DB_PASS=steuerkram
|
||||
|
||||
# HTTP Basic Auth
|
||||
AUTH_USER=admin
|
||||
AUTH_PASS=changeme
|
||||
|
||||
# App
|
||||
APP_SECRET=change-this-to-a-random-string
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ services:
|
|||
DB_NAME: steuerkram
|
||||
DB_USER: steuerkram
|
||||
DB_PASS: steuerkram
|
||||
AUTH_USER: admin
|
||||
AUTH_PASS: changeme
|
||||
APP_SECRET: change-this-in-production
|
||||
depends_on:
|
||||
db:
|
||||
|
|
|
|||
|
|
@ -284,9 +284,7 @@ echo -e " ${BOLD}${ENV_FILE}${NC}"
|
|||
echo ""
|
||||
echo " Folgende Werte sollten geändert werden:"
|
||||
echo " DB_PASS – Datenbankpasswort"
|
||||
echo " AUTH_USER – Benutzername für den Web-Zugang"
|
||||
echo " AUTH_PASS – Passwort für den Web-Zugang"
|
||||
echo " APP_SECRET – Zufälliger geheimer Schlüssel"
|
||||
echo " APP_SECRET – Zufälliger geheimer Schlüssel (für Sessions)"
|
||||
echo ""
|
||||
echo " Danach die Anwendung starten:"
|
||||
echo -e " ${BOLD}cd ${INSTALL_DIR} && docker compose up -d${NC}"
|
||||
|
|
|
|||
8
migrations/005_users.sql
Normal file
8
migrations/005_users.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-- Migration 005: Session-basierte Authentifizierung – users-Tabelle
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(64) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
|
@ -17,9 +17,15 @@ use Steuerkram\Controllers\InvoiceController;
|
|||
use Steuerkram\Controllers\UstvaController;
|
||||
use Steuerkram\Controllers\EuerController;
|
||||
use Steuerkram\Controllers\SettingsController;
|
||||
use Steuerkram\Controllers\LoginController;
|
||||
use Steuerkram\Controllers\UserAdminController;
|
||||
|
||||
Config::load();
|
||||
Auth::require();
|
||||
|
||||
$requestPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
|
||||
if ($requestPath !== '/login') {
|
||||
Auth::require();
|
||||
}
|
||||
|
||||
$router = new Router();
|
||||
|
||||
|
|
@ -93,4 +99,15 @@ $router->post('/settings', fn($p) => (new SettingsController())->update($p));
|
|||
$router->post('/settings/logo', fn($p) => (new SettingsController())->uploadLogo($p));
|
||||
$router->post('/settings/logo/delete', fn($p) => (new SettingsController())->deleteLogo($p));
|
||||
|
||||
// ── Login / Logout ────────────────────────────────────────────
|
||||
$router->get('/login', fn($p) => (new LoginController())->showLogin($p));
|
||||
$router->post('/login', fn($p) => (new LoginController())->handleLogin($p));
|
||||
$router->post('/logout', fn($p) => (new LoginController())->handleLogout($p));
|
||||
|
||||
// ── Benutzerverwaltung ────────────────────────────────────────
|
||||
$router->get('/useradmin', fn($p) => (new UserAdminController())->index($p));
|
||||
$router->post('/useradmin', fn($p) => (new UserAdminController())->store($p));
|
||||
$router->post('/useradmin/{id}/delete', fn($p) => (new UserAdminController())->delete($p));
|
||||
$router->post('/useradmin/{id}/toggle', fn($p) => (new UserAdminController())->toggleActive($p));
|
||||
|
||||
$router->dispatch();
|
||||
|
|
|
|||
52
src/Auth.php
52
src/Auth.php
|
|
@ -6,20 +6,50 @@ namespace Steuerkram;
|
|||
|
||||
class Auth
|
||||
{
|
||||
private const SESSION_LIFETIME = 86400; // 24 h absolutes Timeout
|
||||
|
||||
public static function require(): void
|
||||
{
|
||||
$user = Config::get('auth_user');
|
||||
$pass = Config::get('auth_pass');
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
if (
|
||||
!isset($_SERVER['PHP_AUTH_USER']) ||
|
||||
!hash_equals($user, $_SERVER['PHP_AUTH_USER']) ||
|
||||
!hash_equals($pass, $_SERVER['PHP_AUTH_PW'])
|
||||
) {
|
||||
header('WWW-Authenticate: Basic realm="Steuerkram"');
|
||||
header('HTTP/1.1 401 Unauthorized');
|
||||
echo '<!DOCTYPE html><html><body><h1>401 Unauthorized</h1></body></html>';
|
||||
exit;
|
||||
$userId = $_SESSION['user_id'] ?? null;
|
||||
$loginTime = $_SESSION['login_time'] ?? 0;
|
||||
|
||||
if (!$userId || (time() - (int)$loginTime) > self::SESSION_LIFETIME) {
|
||||
$_SESSION = [];
|
||||
session_destroy();
|
||||
View::redirect('/login');
|
||||
}
|
||||
}
|
||||
|
||||
public static function login(int $userId): void
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['user_id'] = $userId;
|
||||
$_SESSION['login_time'] = time();
|
||||
}
|
||||
|
||||
public static function logout(): void
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
$_SESSION = [];
|
||||
if (ini_get('session.use_cookies')) {
|
||||
$p = session_get_cookie_params();
|
||||
setcookie(session_name(), '', time() - 42000,
|
||||
$p['path'], $p['domain'], $p['secure'], $p['httponly']);
|
||||
}
|
||||
session_destroy();
|
||||
}
|
||||
|
||||
public static function userId(): ?int
|
||||
{
|
||||
return isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ class Config
|
|||
'db_name' => $_ENV['DB_NAME'] ?? getenv('DB_NAME') ?: 'steuerkram',
|
||||
'db_user' => $_ENV['DB_USER'] ?? getenv('DB_USER') ?: 'steuerkram',
|
||||
'db_pass' => $_ENV['DB_PASS'] ?? getenv('DB_PASS') ?: 'steuerkram',
|
||||
'auth_user' => $_ENV['AUTH_USER'] ?? getenv('AUTH_USER') ?: 'admin',
|
||||
'auth_pass' => $_ENV['AUTH_PASS'] ?? getenv('AUTH_PASS') ?: 'changeme',
|
||||
'app_secret' => $_ENV['APP_SECRET'] ?? getenv('APP_SECRET') ?: 'dev-secret',
|
||||
'upload_dir' => dirname(__DIR__) . '/public/static/uploads',
|
||||
'base_url' => '',
|
||||
|
|
|
|||
100
src/Controllers/LoginController.php
Normal file
100
src/Controllers/LoginController.php
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Steuerkram\Controllers;
|
||||
|
||||
use Steuerkram\Auth;
|
||||
use Steuerkram\Models\User;
|
||||
use Steuerkram\View;
|
||||
|
||||
class LoginController
|
||||
{
|
||||
public function showLogin(array $params): void
|
||||
{
|
||||
// Bereits eingeloggt → direkt weiter
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
if (!empty($_SESSION['user_id'])) {
|
||||
View::redirect('/');
|
||||
}
|
||||
|
||||
$setup = (User::count() === 0);
|
||||
$view = new View();
|
||||
$view->render('login.php', [
|
||||
'pageTitle' => $setup ? 'Ersteinrichtung' : 'Anmelden',
|
||||
'setup' => $setup,
|
||||
'error' => '',
|
||||
]);
|
||||
}
|
||||
|
||||
public function handleLogin(array $params): void
|
||||
{
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
// Ersteinrichtung: erster Benutzer anlegen
|
||||
if (User::count() === 0) {
|
||||
$errors = [];
|
||||
if ($username === '') $errors[] = 'Benutzername erforderlich.';
|
||||
if (strlen($password) < 8) $errors[] = 'Passwort muss mindestens 8 Zeichen haben.';
|
||||
$password2 = $_POST['password2'] ?? '';
|
||||
if ($password !== $password2) $errors[] = 'Passwörter stimmen nicht überein.';
|
||||
|
||||
if ($errors) {
|
||||
$view = new View();
|
||||
$view->render('login.php', [
|
||||
'pageTitle' => 'Ersteinrichtung',
|
||||
'setup' => true,
|
||||
'error' => implode(' ', $errors),
|
||||
'username' => $username,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
User::create($username, $password);
|
||||
$user = User::findByUsername($username);
|
||||
Auth::login((int)$user['id']);
|
||||
View::redirect('/');
|
||||
return;
|
||||
}
|
||||
|
||||
$error = $this->authenticate($username, $password);
|
||||
|
||||
if ($error !== null) {
|
||||
$view = new View();
|
||||
$view->render('login.php', [
|
||||
'pageTitle' => 'Anmelden',
|
||||
'setup' => false,
|
||||
'error' => $error,
|
||||
'username' => $username,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
View::redirect('/');
|
||||
}
|
||||
|
||||
public function handleLogout(array $params): void
|
||||
{
|
||||
Auth::logout();
|
||||
View::redirect('/login');
|
||||
}
|
||||
|
||||
private function authenticate(string $username, string $password): ?string
|
||||
{
|
||||
if ($username === '' || $password === '') {
|
||||
return 'Benutzername und Passwort erforderlich.';
|
||||
}
|
||||
|
||||
$user = User::findByUsername($username);
|
||||
|
||||
if (!$user || !$user['active'] || !password_verify($password, $user['password_hash'])) {
|
||||
return 'Ungültige Anmeldedaten oder Konto deaktiviert.';
|
||||
}
|
||||
|
||||
Auth::login((int)$user['id']);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
107
src/Controllers/UserAdminController.php
Normal file
107
src/Controllers/UserAdminController.php
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Steuerkram\Controllers;
|
||||
|
||||
use Steuerkram\Models\User;
|
||||
use Steuerkram\View;
|
||||
|
||||
class UserAdminController
|
||||
{
|
||||
public function index(array $params): void
|
||||
{
|
||||
$users = User::all();
|
||||
$view = new View();
|
||||
$view->render('layout/base.php', [
|
||||
'pageTitle' => 'Benutzerverwaltung',
|
||||
'content' => $this->renderView('useradmin/list', [
|
||||
'users' => $users,
|
||||
'errors' => [],
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(array $params): void
|
||||
{
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
$errors = [];
|
||||
|
||||
if ($username === '') {
|
||||
$errors['username'] = 'Pflichtfeld';
|
||||
} elseif (strlen($username) > 64) {
|
||||
$errors['username'] = 'Maximal 64 Zeichen';
|
||||
} elseif (User::findByUsername($username)) {
|
||||
$errors['username'] = 'Benutzername bereits vergeben';
|
||||
}
|
||||
|
||||
if (strlen($password) < 8) {
|
||||
$errors['password'] = 'Mindestens 8 Zeichen erforderlich';
|
||||
}
|
||||
|
||||
if ($errors) {
|
||||
$view = new View();
|
||||
$view->render('layout/base.php', [
|
||||
'pageTitle' => 'Benutzerverwaltung',
|
||||
'content' => $this->renderView('useradmin/list', [
|
||||
'users' => User::all(),
|
||||
'errors' => $errors,
|
||||
'formData' => ['username' => $username],
|
||||
]),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
User::create($username, $password);
|
||||
View::redirect('/useradmin', 'Benutzer angelegt.');
|
||||
}
|
||||
|
||||
public function delete(array $params): void
|
||||
{
|
||||
$id = (int)($params['id'] ?? 0);
|
||||
|
||||
// Letzten aktiven Benutzer nicht löschen
|
||||
if (User::count() <= 1) {
|
||||
View::redirect('/useradmin', 'Der letzte Benutzer kann nicht gelöscht werden.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
User::delete($id);
|
||||
View::redirect('/useradmin', 'Benutzer gelöscht.');
|
||||
}
|
||||
|
||||
public function toggleActive(array $params): void
|
||||
{
|
||||
$id = (int)($params['id'] ?? 0);
|
||||
$user = User::find($id);
|
||||
|
||||
if (!$user) {
|
||||
View::redirect('/useradmin', 'Benutzer nicht gefunden.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
$newState = !(bool)$user['active'];
|
||||
|
||||
// Letzten aktiven Benutzer nicht deaktivieren
|
||||
if (!$newState) {
|
||||
$activeCount = count(array_filter(User::all(), fn($u) => $u['active']));
|
||||
if ($activeCount <= 1) {
|
||||
View::redirect('/useradmin', 'Der letzte aktive Benutzer kann nicht deaktiviert werden.', 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
User::setActive($id, $newState);
|
||||
$msg = $newState ? 'Benutzer aktiviert.' : 'Benutzer deaktiviert.';
|
||||
View::redirect('/useradmin', $msg);
|
||||
}
|
||||
|
||||
private function renderView(string $template, array $data): string
|
||||
{
|
||||
extract($data, EXTR_SKIP);
|
||||
ob_start();
|
||||
include dirname(__DIR__) . '/Views/' . $template . '.php';
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
61
src/Models/User.php
Normal file
61
src/Models/User.php
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Steuerkram\Models;
|
||||
|
||||
use Steuerkram\Database;
|
||||
|
||||
class User
|
||||
{
|
||||
public static function all(): array
|
||||
{
|
||||
return Database::fetchAll(
|
||||
'SELECT id, username, active, created_at FROM users ORDER BY username'
|
||||
);
|
||||
}
|
||||
|
||||
public static function find(int $id): array|false
|
||||
{
|
||||
return Database::fetchOne(
|
||||
'SELECT id, username, active, created_at FROM users WHERE id = ?',
|
||||
[$id]
|
||||
);
|
||||
}
|
||||
|
||||
public static function findByUsername(string $username): array|false
|
||||
{
|
||||
return Database::fetchOne(
|
||||
'SELECT id, username, password_hash, active FROM users WHERE username = ?',
|
||||
[$username]
|
||||
);
|
||||
}
|
||||
|
||||
public static function create(string $username, string $password): void
|
||||
{
|
||||
$hash = password_hash($password, PASSWORD_BCRYPT);
|
||||
Database::execute(
|
||||
'INSERT INTO users (username, password_hash) VALUES (?, ?)',
|
||||
[$username, $hash]
|
||||
);
|
||||
}
|
||||
|
||||
public static function delete(int $id): void
|
||||
{
|
||||
Database::execute('DELETE FROM users WHERE id = ?', [$id]);
|
||||
}
|
||||
|
||||
public static function setActive(int $id, bool $active): void
|
||||
{
|
||||
Database::execute(
|
||||
'UPDATE users SET active = ? WHERE id = ?',
|
||||
[(int)$active, $id]
|
||||
);
|
||||
}
|
||||
|
||||
public static function count(): int
|
||||
{
|
||||
$row = Database::fetchOne('SELECT COUNT(*) AS n FROM users');
|
||||
return (int)($row['n'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -62,11 +62,22 @@ function navActive(string $prefix, string $current): string {
|
|||
<div class="sidebar-section">
|
||||
<div class="sidebar-label">Verwaltung</div>
|
||||
<nav>
|
||||
|
||||
<a href="/useradmin" class="<?= navActive('/useradmin', $path) ?>">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
|
||||
Benutzer
|
||||
</a>
|
||||
<a href="/settings" class="<?= navActive('/settings', $path) ?>">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93l-1.41 1.41M4.93 4.93l1.41 1.41M19.07 19.07l-1.41-1.41M4.93 19.07l1.41-1.41M12 2v2M12 20v2M2 12h2M20 12h2"/></svg>
|
||||
Einstellungen
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:auto;padding:1rem;">
|
||||
<form method="POST" action="/logout">
|
||||
<button type="submit" style="width:100%;background:transparent;border:1px solid #475569;color:var(--color-nav-text);border-radius:var(--radius);padding:.4rem .75rem;cursor:pointer;font-size:.85rem;">
|
||||
Abmelden
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
|
|
|
|||
83
src/Views/login.php
Normal file
83
src/Views/login.php
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Anmelden – Steuerkram</title>
|
||||
<link rel="stylesheet" href="/static/css/app.css">
|
||||
<link rel="icon" type="image/png" href="/static/favicon/favicon-96x96.png" sizes="96x96">
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon/favicon.svg">
|
||||
<link rel="shortcut icon" href="/static/favicon/favicon.ico">
|
||||
<style>
|
||||
body { display: flex; align-items: center; justify-content: center; min-height: 100vh; background: var(--color-bg); }
|
||||
.login-card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 2.5rem 2rem;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
}
|
||||
.login-brand {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-nav-bg);
|
||||
margin-bottom: 1.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
.login-brand span { color: var(--color-primary); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-card">
|
||||
<div class="login-brand">Steuer<span>kram</span></div>
|
||||
|
||||
<?php if (!empty($flash)): ?>
|
||||
<?php foreach ($flash as $f): ?>
|
||||
<div class="flash flash-<?= \Steuerkram\View::h($f['type']) ?>" style="margin-bottom:1rem;">
|
||||
<?= \Steuerkram\View::h($f['message']) ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($error)): ?>
|
||||
<div class="flash flash-error" style="margin-bottom:1rem;">
|
||||
<?= \Steuerkram\View::h($error) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($setup)): ?>
|
||||
<p style="margin-bottom:1.25rem;color:var(--color-muted);font-size:.9rem;">
|
||||
Kein Benutzer vorhanden. Legen Sie jetzt den ersten Administrator-Account an.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" action="/login">
|
||||
<div class="form-group">
|
||||
<label for="username">Benutzername</label>
|
||||
<input type="text" id="username" name="username"
|
||||
value="<?= \Steuerkram\View::h($username ?? '') ?>"
|
||||
autofocus autocomplete="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Passwort</label>
|
||||
<input type="password" id="password" name="password"
|
||||
autocomplete="<?= !empty($setup) ? 'new-password' : 'current-password' ?>" required>
|
||||
</div>
|
||||
<?php if (!empty($setup)): ?>
|
||||
<div class="form-group">
|
||||
<label for="password2">Passwort bestätigen</label>
|
||||
<input type="password" id="password2" name="password2"
|
||||
autocomplete="new-password" required>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div style="margin-top:1.5rem;">
|
||||
<button type="submit" class="btn btn-primary" style="width:100%;">
|
||||
<?= !empty($setup) ? 'Account anlegen' : 'Anmelden' ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
77
src/Views/useradmin/list.php
Normal file
77
src/Views/useradmin/list.php
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<?php use Steuerkram\View; $h = fn($v) => View::h($v); ?>
|
||||
|
||||
<div class="card" style="margin-bottom:1.5rem;">
|
||||
<div class="card-header"><div class="card-title">Neuen Benutzer anlegen</div></div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/useradmin">
|
||||
<div class="form-row">
|
||||
<div class="form-group" style="flex:1">
|
||||
<label>Benutzername</label>
|
||||
<input type="text" name="username"
|
||||
value="<?= $h($formData['username'] ?? '') ?>"
|
||||
class="<?= isset($errors['username']) ? 'input-error' : '' ?>"
|
||||
maxlength="64" required>
|
||||
<?php if (isset($errors['username'])): ?>
|
||||
<div class="error-text"><?= $h($errors['username']) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="form-group" style="flex:1">
|
||||
<label>Passwort</label>
|
||||
<input type="password" name="password"
|
||||
class="<?= isset($errors['password']) ? 'input-error' : '' ?>"
|
||||
autocomplete="new-password" required>
|
||||
<?php if (isset($errors['password'])): ?>
|
||||
<div class="error-text"><?= $h($errors['password']) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div style="padding-bottom:.05rem">
|
||||
<button type="submit" class="btn btn-primary">Anlegen</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><div class="card-title">Vorhandene Benutzer</div></div>
|
||||
<?php if (empty($users)): ?>
|
||||
<div class="card-body"><p style="color:var(--color-muted)">Keine Benutzer vorhanden.</p></div>
|
||||
<?php else: ?>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Benutzername</th>
|
||||
<th>Status</th>
|
||||
<th>Erstellt am</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $u): ?>
|
||||
<tr>
|
||||
<td><?= $h($u['username']) ?></td>
|
||||
<td>
|
||||
<?php if ($u['active']): ?>
|
||||
<span class="badge badge-green">Aktiv</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-red">Deaktiviert</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= $h(View::formatDate(substr($u['created_at'], 0, 10))) ?></td>
|
||||
<td style="text-align:right">
|
||||
<form method="POST" action="/useradmin/<?= (int)$u['id'] ?>/toggle" style="display:inline;">
|
||||
<button type="submit" class="btn btn-sm <?= $u['active'] ? 'btn-secondary' : 'btn-primary' ?>">
|
||||
<?= $u['active'] ? 'Deaktivieren' : 'Aktivieren' ?>
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="/useradmin/<?= (int)$u['id'] ?>/delete" style="display:inline;"
|
||||
onsubmit="return confirm('Benutzer «<?= $h($u['username']) ?>» wirklich löschen?')">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Löschen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
Loading…
Add table
Reference in a new issue