This commit is contained in:
2025-11-18 03:43:21 +01:00
parent beb7d9c868
commit 9b1f1f0710
16 changed files with 2369 additions and 576 deletions

284
src/Auth.php Normal file
View File

@@ -0,0 +1,284 @@
<?php
// src/auth.php
// Zentrale Auth-Logik: Session, CSRF, Login, Registrierung, aktueller Nutzer
declare(strict_types=1);
require_once __DIR__ . '/../config/db.php'; // Stellt $pdo (PDO) bereit
// --- Session Setup ---
if (session_status() !== PHP_SESSION_ACTIVE) {
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '', // Standard: aktuelle Domain
'secure' => $secure,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
}
// --- Sprache ermitteln / speichern ---
function auth_get_lang(): string {
if (!empty($_GET['lang'])) {
$_SESSION['lang'] = $_GET['lang'];
}
if (!empty($_SESSION['lang'])) {
return $_SESSION['lang'];
}
return 'en';
}
// --- CSRF-Token ---
function auth_csrf_token(): string {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function auth_verify_csrf(?string $token): bool {
if (empty($token) || empty($_SESSION['csrf_token'])) {
return false;
}
return hash_equals($_SESSION['csrf_token'], $token);
}
// --- PDO Helper ---
function auth_pdo(): PDO {
// $pdo kommt aus config/db.php
global $pdo;
if (!$pdo instanceof PDO) {
throw new RuntimeException('Database connection not available.');
}
return $pdo;
}
// --- Aktueller User ---
function auth_current_user(): ?array {
if (!empty($_SESSION['user_cache']) && is_array($_SESSION['user_cache'])) {
return $_SESSION['user_cache'];
}
if (empty($_SESSION['user_id'])) {
return null;
}
$pdo = auth_pdo();
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id LIMIT 1');
$stmt->execute([':id' => $_SESSION['user_id']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user) {
return null;
}
$_SESSION['user_cache'] = $user;
return $user;
}
function auth_require_login(): void {
if (!auth_current_user()) {
$lang = auth_get_lang();
header('Location: /login.php?lang=' . urlencode($lang));
exit;
}
}
// --- Avatar-Helfer ---
function auth_user_initials(array $user): string {
$name = $user['full_name'] ?? '';
if (trim($name) === '') {
$name = $user['username'] ?? $user['email'] ?? 'U';
}
$parts = preg_split('/\s+/', trim($name));
$initials = strtoupper(mb_substr($parts[0], 0, 1));
if (count($parts) > 1) {
$initials .= strtoupper(mb_substr(end($parts), 0, 1));
}
return $initials;
}
function auth_user_avatar_url(array $user): ?string {
if (!empty($user['avatar_path'])) {
return '/uploads/avatars/' . ltrim($user['avatar_path'], '/');
}
return null;
}
// --- Registrierung ---
function auth_register_user(
string $email,
string $username,
string $fullName,
string $password,
string $passwordConfirm,
string $preferredLang
): array {
$pdo = auth_pdo();
$errors = [];
$email = trim($email);
$username = trim($username);
$fullName = trim($fullName);
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors['email'] = 'Bitte eine gültige E-Mail-Adresse eingeben.';
}
if ($username === '' || !preg_match('/^[a-zA-Z0-9_.-]{3,32}$/', $username)) {
$errors['username'] = 'Username muss 332 Zeichen lang sein und darf nur Buchstaben, Zahlen, ., _, - enthalten.';
}
if (mb_strlen($fullName) < 3) {
$errors['full_name'] = 'Bitte einen vollständigen Namen angeben.';
}
if (mb_strlen($password) < 10) {
$errors['password'] = 'Passwort muss mindestens 10 Zeichen lang sein.';
}
if ($password !== $passwordConfirm) {
$errors['password_confirm'] = 'Passwörter stimmen nicht überein.';
}
$allowedLangs = ['de', 'en', 'it', 'fr'];
if (!in_array($preferredLang, $allowedLangs, true)) {
$preferredLang = 'en';
}
// E-Mail / Username bereits vergeben?
if (!$errors) {
$stmt = $pdo->prepare('SELECT email, username FROM users WHERE email = :email OR username = :username LIMIT 1');
$stmt->execute([
':email' => $email,
':username' => $username,
]);
$existing = $stmt->fetch(PDO::FETCH_ASSOC);
if ($existing) {
if (strcasecmp($existing['email'], $email) === 0) {
$errors['email'] = 'Diese E-Mail-Adresse wird bereits verwendet.';
}
if (strcasecmp($existing['username'], $username) === 0) {
$errors['username'] = 'Dieser Username ist bereits vergeben.';
}
}
}
if ($errors) {
return ['success' => false, 'errors' => $errors];
}
$hash = password_hash($password, PASSWORD_DEFAULT);
$now = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d H:i:s');
$stmt = $pdo->prepare('
INSERT INTO users (email, username, full_name, password_hash, preferred_lang, created_at, updated_at)
VALUES (:email, :username, :full_name, :password_hash, :preferred_lang, :created_at, :updated_at)
');
$stmt->execute([
':email' => $email,
':username' => $username,
':full_name' => $fullName,
':password_hash' => $hash,
':preferred_lang'=> $preferredLang,
':created_at' => $now,
':updated_at' => $now,
]);
$userId = (int)$pdo->lastInsertId();
$_SESSION['user_id'] = $userId;
unset($_SESSION['user_cache']); // neu laden beim nächsten Zugriff
$_SESSION['lang'] = $preferredLang;
return ['success' => true, 'errors' => []];
}
// --- Login ---
function auth_login(string $login, string $password): array {
$pdo = auth_pdo();
$errors = [];
$login = trim($login);
if ($login === '' || $password === '') {
$errors['login'] = 'Bitte Zugangsdaten vollständig ausfüllen.';
return ['success' => false, 'errors' => $errors];
}
$stmt = $pdo->prepare('
SELECT * FROM users
WHERE email = :login OR username = :login
LIMIT 1
');
$stmt->execute([':login' => $login]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user || !password_verify($password, $user['password_hash'])) {
$errors['login'] = 'E-Mail/Username oder Passwort ist falsch.';
return ['success' => false, 'errors' => $errors];
}
$_SESSION['user_id'] = (int)$user['id'];
unset($_SESSION['user_cache']);
if (!empty($user['preferred_lang'])) {
$_SESSION['lang'] = $user['preferred_lang'];
}
// Option: Password-Rehash, wenn Algorithmus veraltet
if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
$newHash = password_hash($password, PASSWORD_DEFAULT);
$upd = $pdo->prepare('UPDATE users SET password_hash = :hash WHERE id = :id');
$upd->execute([':hash' => $newHash, ':id' => $user['id']]);
}
return ['success' => true, 'errors' => []];
}
// --- Profil aktualisieren (Name, Sprache) ---
function auth_update_profile(int $userId, string $fullName, string $preferredLang): array {
$pdo = auth_pdo();
$errors = [];
$fullName = trim($fullName);
if (mb_strlen($fullName) < 3) {
$errors['full_name'] = 'Bitte einen gültigen Namen angeben.';
}
$allowedLangs = ['de', 'en', 'it', 'fr'];
if (!in_array($preferredLang, $allowedLangs, true)) {
$preferredLang = 'en';
}
if ($errors) {
return ['success' => false, 'errors' => $errors];
}
$now = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d H:i:s');
$stmt = $pdo->prepare('
UPDATE users
SET full_name = :full_name,
preferred_lang = :preferred_lang,
updated_at = :updated_at
WHERE id = :id
');
$stmt->execute([
':full_name' => $fullName,
':preferred_lang'=> $preferredLang,
':updated_at' => $now,
':id' => $userId,
]);
unset($_SESSION['user_cache']);
$_SESSION['lang'] = $preferredLang;
return ['success' => true, 'errors' => []];
}
// --- Logout ---
function auth_logout(): void {
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
}
session_destroy();
}

25
src/Database.php Normal file
View File

@@ -0,0 +1,25 @@
<?php
// src/Database.php
declare(strict_types=1);
class Database
{
private static ?PDO $pdo = null;
public static function getConnection(): PDO
{
if (self::$pdo === null) {
$config = require __DIR__ . '/../config/database.php';
self::$pdo = new PDO(
$config['dsn'],
$config['user'],
$config['password'],
$config['options']
);
}
return self::$pdo;
}
}

87
src/Session.php Normal file
View File

@@ -0,0 +1,87 @@
<?php
// src/Session.php
declare(strict_types=1);
class Session
{
public static function start(): void
{
if (session_status() !== PHP_SESSION_ACTIVE) {
// Etwas härtere Session-Cookies
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => isset($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
}
}
public static function regenerate(): void
{
if (session_status() === PHP_SESSION_ACTIVE) {
session_regenerate_id(true);
}
}
public static function set(string $key, mixed $value): void
{
$_SESSION[$key] = $value;
}
public static function get(string $key, mixed $default = null): mixed
{
return $_SESSION[$key] ?? $default;
}
public static function remove(string $key): void
{
unset($_SESSION[$key]);
}
public static function destroy(): void
{
if (session_status() === PHP_SESSION_ACTIVE) {
$_SESSION = [];
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params["path"],
$params["domain"],
$params["secure"],
$params["httponly"]
);
}
session_destroy();
}
}
public static function csrfToken(): string
{
self::start();
if (!isset($_SESSION['_csrf_token'])) {
$_SESSION['_csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['_csrf_token'];
}
public static function validateCsrf(?string $token): bool
{
self::start();
if (!isset($_SESSION['_csrf_token']) || !$token) {
return false;
}
$valid = hash_equals($_SESSION['_csrf_token'], $token);
if ($valid) {
// Optional: Token nach Benutzung rotieren
unset($_SESSION['_csrf_token']);
}
return $valid;
}
}