Initiales CMS: Deutschsprachiges Blog-System mit Admin-Bereich

Vollständiges, schlankes PHP/SQLite-CMS für IT-, KI- und Gaming-Inhalte:

- Core: DB-Singleton, Auth mit Passwort-Hashing, Session-Cookies,
  CSRF-Schutz, Login-Rate-Limit, Bild-Upload mit serverseitiger Validierung
- Admin: Dashboard, Artikel/Seiten-Verwaltung mit Quill WYSIWYG-Editor,
  Kategorien, Navigation (Drag & Drop), Medienbibliothek, Profil
- Frontend: Responsive Dark-Theme, Artikel-Grid, Kategorie-Filter,
  Archiv, Paginierung, SEO-Meta-Tags
- Sicherheit: Prepared Statements, HTML-Sanitizer, .htaccess-Schutz
  für sensible Verzeichnisse, PHP-Ausführungsschutz im Upload-Ordner
- Installation: install.php erstellt DB-Schema und Admin-Account

https://claude.ai/code/session_01Xsg4j2t4S9goMuWVpF3ezG
This commit is contained in:
Claude
2026-04-05 20:59:52 +00:00
commit 3c97192386
45 changed files with 2839 additions and 0 deletions

132
core/auth.php Normal file
View File

@@ -0,0 +1,132 @@
<?php
/**
* Authentifizierung, Session-Management, CSRF-Schutz, Rate-Limiting
*/
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/helpers.php';
function auth_start_session(): void
{
if (session_status() === PHP_SESSION_ACTIVE) {
return;
}
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
session_set_cookie_params([
'lifetime' => SESSION_LIFETIME,
'path' => '/',
'secure' => $secure,
'httponly' => true,
'samesite' => 'Strict',
]);
session_start();
// Session-ID alle 15 Minuten erneuern
if (!isset($_SESSION['_last_regeneration'])) {
$_SESSION['_last_regeneration'] = time();
} elseif (time() - $_SESSION['_last_regeneration'] > 900) {
session_regenerate_id(true);
$_SESSION['_last_regeneration'] = time();
}
}
function auth_is_logged_in(): bool
{
return !empty($_SESSION['user_id']);
}
function auth_require_login(): void
{
if (!auth_is_logged_in()) {
redirect('/admin/login.php');
}
}
function auth_login(string $username, string $password): bool
{
$stmt = db()->prepare('SELECT id, password_hash, display_name FROM users WHERE username = ?');
$stmt->execute([$username]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password_hash'])) {
auth_record_attempt();
return false;
}
// Session erneuern bei Login
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['display_name'] = $user['display_name'];
$_SESSION['_last_regeneration'] = time();
// Fehlversuche löschen
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$stmt = db()->prepare('DELETE FROM login_attempts WHERE ip_address = ?');
$stmt->execute([$ip]);
return true;
}
function auth_logout(): void
{
$_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();
}
function csrf_token(): string
{
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function csrf_field(): string
{
return '<input type="hidden" name="csrf_token" value="' . e(csrf_token()) . '">';
}
function csrf_verify(): void
{
$token = $_POST['csrf_token'] ?? '';
if (!hash_equals(csrf_token(), $token)) {
http_response_code(403);
die('Ungültiges CSRF-Token.');
}
}
function auth_check_rate_limit(): bool
{
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$stmt = db()->prepare(
'SELECT COUNT(*) FROM login_attempts WHERE ip_address = ? AND attempted_at > datetime("now", ?)'
);
$stmt->execute([$ip, '-' . LOGIN_LOCKOUT_MINUTES . ' minutes']);
$count = (int) $stmt->fetchColumn();
return $count < LOGIN_MAX_ATTEMPTS;
}
function auth_record_attempt(): void
{
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
$stmt = db()->prepare('INSERT INTO login_attempts (ip_address) VALUES (?)');
$stmt->execute([$ip]);
}
function auth_user_id(): ?int
{
return $_SESSION['user_id'] ?? null;
}
function auth_display_name(): string
{
return $_SESSION['display_name'] ?? 'Admin';
}