weitere Bearibeitung

This commit is contained in:
2026-06-17 16:45:14 +02:00
parent 06645f1e9c
commit 1891ec0a51
38 changed files with 2720 additions and 109 deletions
+2 -2
View File
@@ -68,11 +68,11 @@ final class Auth
return false;
}
$this->rehashPasswordIfNeeded((int) $membership['id'], (string) $membership['password_hash'], $password);
$this->rehashPasswordIfNeeded((int) $membership['user_id'], (string) $membership['password_hash'], $password);
$this->session->regenerate();
$this->session->put('auth', [
'user_id' => (int) $membership['id'],
'user_id' => (int) $membership['user_id'],
'tenant_id' => (int) $membership['tenant_id'],
'tenant_slug' => $membership['tenant_slug'],
'tenant_role' => $membership['role'],
+1 -1
View File
@@ -5,7 +5,7 @@ namespace App\Core;
use PDO;
use PDOException;
final class Database
final class Database implements DatabaseInterface
{
private ?PDO $pdo = null;
+17
View File
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Core;
use PDO;
/**
* Gemeinsames Interface für Database (MySQL) und DatabaseSqlite.
* Ermöglicht den Betrieb beider Backends ohne Type-Errors.
*/
interface DatabaseInterface
{
public function pdo(): PDO;
public function isConfigured(): bool;
}
+123
View File
@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace App\Core;
use PDO;
use PDOException;
use RuntimeException;
/**
* SQLite-basierte Datenbank für Test-/Entwicklungsumgebungen.
* Ersetzt die MySQL-Database-Klasse und emuliert MySQL-spezifische Features.
*/
final class DatabaseSqlite implements DatabaseInterface
{
private ?PDO $pdo = null;
private readonly string $dbPath;
public function __construct(private readonly array $config = [])
{
$this->dbPath = $this->config['path']
?? dirname(__DIR__, 2) . '/storage/database.sqlite';
}
public function pdo(): PDO
{
if ($this->pdo instanceof PDO) {
return $this->pdo;
}
$dbDir = dirname($this->dbPath);
if (!is_dir($dbDir) && !mkdir($dbDir, 0775, true) && !is_dir($dbDir)) {
throw new RuntimeException('Das Datenbank-Verzeichnis konnte nicht erstellt werden: ' . $dbDir);
}
try {
$this->pdo = new PDO(
'sqlite:' . $this->dbPath,
null,
null,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
$this->pdo->exec('PRAGMA journal_mode=WAL');
$this->pdo->exec('PRAGMA foreign_keys=ON');
$this->pdo->exec('PRAGMA busy_timeout=5000');
} catch (PDOException $exception) {
throw new PDOException(
'SQLite connection failed: ' . $exception->getMessage(),
(int) $exception->getCode(),
$exception
);
}
return $this->pdo;
}
public function isConfigured(): bool
{
return $this->dbPath !== '';
}
/**
* Führt schema.sqlite.sql aus, um die Datenbank zu initialisieren.
*/
public function initializeSchema(): void
{
$schemaFile = dirname(__DIR__, 2) . '/database/schema.sqlite.sql';
if (!is_file($schemaFile)) {
throw new RuntimeException('SQLite-Schema nicht gefunden: ' . $schemaFile);
}
$schema = file_get_contents($schemaFile);
if (!is_string($schema) || $schema === '') {
throw new RuntimeException('SQLite-Schema konnte nicht geladen werden.');
}
$this->pdo()->exec($schema);
}
/**
* SQLite-kompatibler UPSERT: INSERT OR REPLACE + SELECT als Alternative zu ON DUPLICATE KEY.
*/
public function upsertRateLimit(string $actionKey, string $bucketKey, string $now): void
{
$pdo = $this->pdo();
$existing = $pdo->prepare(
'SELECT id FROM rate_limits WHERE action_key = :action_key AND bucket_key = :bucket_key LIMIT 1'
);
$existing->execute([
'action_key' => $actionKey,
'bucket_key' => $bucketKey,
]);
if (!$existing->fetchColumn()) {
$insert = $pdo->prepare(
'INSERT INTO rate_limits (action_key, bucket_key, attempts, window_start, last_attempt_at, created_at)
VALUES (:action_key, :bucket_key, 0, :window_start, :last_attempt_at, :created_at)'
);
$insert->execute([
'action_key' => $actionKey,
'bucket_key' => $bucketKey,
'window_start' => $now,
'last_attempt_at' => $now,
'created_at' => $now,
]);
}
}
public function getDbPath(): string
{
return $this->dbPath;
}
}
+9
View File
@@ -6,6 +6,15 @@ final class Response
{
public static function redirect(string $url): never
{
if (str_starts_with($url, '/') && function_exists('url')) {
$url = \url($url);
} else {
$prefix = $_SERVER['KAFFEEKASSE_PROXY_PREFIX'] ?? '';
if ($prefix !== '' && str_starts_with($url, '/') && !str_starts_with($url, $prefix)) {
$url = $prefix . $url;
}
}
header('Location: ' . $url);
exit;
}