chore: complete update
This commit is contained in:
340
src/Application/Admin/Dashboard.php
Normal file
340
src/Application/Admin/Dashboard.php
Normal file
@@ -0,0 +1,340 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Admin;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Auth\Auth;
|
||||
use App\Framework\Config\TypedConfiguration;
|
||||
use App\Framework\Core\VersionInfo;
|
||||
use App\Framework\DI\DefaultContainer;
|
||||
use App\Framework\Http\HttpResponse;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\Response;
|
||||
use App\Framework\Meta\MetaData;
|
||||
use App\Framework\Meta\OpenGraphTypeWebsite;
|
||||
use App\Framework\Performance\MemoryUsageTracker;
|
||||
use App\Framework\Router\Result\ViewResult;
|
||||
use App\Framework\Http\Session\SessionManager;
|
||||
use App\Framework\Http\Status;
|
||||
use Dom\HTMLDocument;
|
||||
|
||||
final readonly class Dashboard
|
||||
{
|
||||
public function __construct(
|
||||
private DefaultContainer $container,
|
||||
private VersionInfo $versionInfo,
|
||||
private MemoryUsageTracker $memoryTracker,
|
||||
private TypedConfiguration $config,
|
||||
) {}
|
||||
|
||||
#[Auth]
|
||||
#[Route(path: '/admin', method: Method::GET)]
|
||||
public function show(): ViewResult
|
||||
{
|
||||
$stats = [
|
||||
'frameworkVersion' => $this->versionInfo->getVersion(),
|
||||
'phpVersion' => PHP_VERSION,
|
||||
'memoryUsage' => $this->formatBytes(memory_get_usage(true)),
|
||||
'peakMemoryUsage' => $this->formatBytes(memory_get_peak_usage(true)),
|
||||
'serverInfo' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown',
|
||||
'serverTime' => date('Y-m-d H:i:s'),
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'operatingSystem' => PHP_OS,
|
||||
'loadedExtensions' => $this->getLoadedExtensions(),
|
||||
'sessionCount' => $this->getActiveSessionCount(),
|
||||
'uptime' => $this->getServerUptime(),
|
||||
'servicesCount' => 4,#count($this->container->getServiceIds()),
|
||||
];
|
||||
|
||||
#debug($stats);
|
||||
|
||||
return new ViewResult(
|
||||
template: 'dashboard',
|
||||
metaData: new MetaData('Admin Dashboard'),
|
||||
data: [
|
||||
'title' => 'Admin Dashboard',
|
||||
'stats' => $stats
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[Auth]
|
||||
#[Route(path: '/admin/routes', method: Method::GET)]
|
||||
public function routes(): ViewResult
|
||||
{
|
||||
$routeRegistry = $this->container->get('App\Framework\Router\RouteRegistry');
|
||||
$routes = $routeRegistry->getRoutes();
|
||||
|
||||
// Sort routes by path for better readability
|
||||
usort($routes, function($a, $b) {
|
||||
return strcmp($a->path, $b->path);
|
||||
});
|
||||
|
||||
return new ViewResult(
|
||||
template: 'admin/routes',
|
||||
metaData: new MetaData('', ''),
|
||||
data: [
|
||||
'title' => 'Routen-Übersicht',
|
||||
'routes' => $routes
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[Auth]
|
||||
#[Route(path: '/admin/services', method: Method::GET)]
|
||||
public function services(): ViewResult
|
||||
{
|
||||
$services = $this->container->getServiceIds();
|
||||
sort($services);
|
||||
|
||||
return new ViewResult(
|
||||
template: 'admin/services',
|
||||
data: [
|
||||
'title' => 'Registrierte Dienste',
|
||||
'services' => $services
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[Auth]
|
||||
#[Route(path: '/admin/environment', method: Method::GET)]
|
||||
public function environment(): ViewResult
|
||||
{
|
||||
$env = [];
|
||||
foreach ($_ENV as $key => $value) {
|
||||
// Maskiere sensible Daten
|
||||
if (str_contains(strtolower($key), 'password') ||
|
||||
str_contains(strtolower($key), 'secret') ||
|
||||
str_contains(strtolower($key), 'key')) {
|
||||
$value = '********';
|
||||
}
|
||||
$env[$key] = $value;
|
||||
}
|
||||
|
||||
ksort($env);
|
||||
|
||||
dd($env);
|
||||
|
||||
return new ViewResult(
|
||||
template: 'admin/environment',
|
||||
metaData: new MetaData('', ''),
|
||||
data: [
|
||||
'title' => 'Umgebungsvariablen',
|
||||
'env' => $env
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[Auth]
|
||||
#[Route('/admin/phpinfo')]
|
||||
#[Route(path: '/admin/phpinfo/{mode}', method: Method::GET)]
|
||||
public function phpInfo(int $mode = 1): Response
|
||||
{
|
||||
ob_start();
|
||||
phpinfo($mode);
|
||||
$phpinfo = ob_get_clean();
|
||||
|
||||
// Extraktion des <body> Inhalts, um nur den relevanten Teil anzuzeigen
|
||||
preg_match('/<body[^>]*>(.*?)<\/body>/si', $phpinfo, $matches);
|
||||
$body = $matches[1] ?? $phpinfo;
|
||||
|
||||
// Entfernen der Navigation-Links am Anfang
|
||||
#$body = preg_replace('/<div class="center">(.*?)<\/div>/si', '', $body, 1);
|
||||
|
||||
#debug($body);
|
||||
|
||||
// Hinzufügen von eigenen Styles
|
||||
$customStyles = '<style>
|
||||
.phpinfo { font-family: system-ui, sans-serif; line-height: 1.5; }
|
||||
.phpinfo table { border-collapse: collapse; width: 100%; margin-bottom: 1rem; }
|
||||
.phpinfo td, .phpinfo th { padding: 0.5rem; border: 1px solid #ddd; }
|
||||
.phpinfo h1, .phpinfo h2 { margin-bottom: 1rem; }
|
||||
.phpinfo hr { margin: 2rem 0; }
|
||||
</style>';
|
||||
|
||||
$responseBody = '<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PHP Info</title>
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
' . $customStyles . '
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<div class="admin-header">
|
||||
<h1>PHP Info</h1>
|
||||
<a href="/admin" class="btn">Zurück zum Dashboard</a>
|
||||
</div>
|
||||
<div class="admin-content">
|
||||
<div class="phpinfo">' . $body . '</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>';
|
||||
|
||||
echo $responseBody;
|
||||
die();
|
||||
|
||||
return new HttpResponse(status: Status::OK, body: $responseBody);
|
||||
}
|
||||
|
||||
#[Auth]
|
||||
#[Route(path: '/admin/performance', method: Method::GET)]
|
||||
public function performance(): ViewResult
|
||||
{
|
||||
$performanceData = [
|
||||
'currentMemoryUsage' => $this->formatBytes(memory_get_usage(true)),
|
||||
'peakMemoryUsage' => $this->formatBytes(memory_get_peak_usage(true)),
|
||||
'memoryLimit' => $this->formatBytes($this->getMemoryLimitInBytes()),
|
||||
'memoryUsagePercentage' => round((memory_get_usage(true) / $this->getMemoryLimitInBytes()) * 100, 2),
|
||||
'loadAverage' => function_exists('sys_getloadavg') ? sys_getloadavg() : ['N/A', 'N/A', 'N/A'],
|
||||
'opcacheEnabled' => function_exists('opcache_get_status') ? 'Ja' : 'Nein',
|
||||
'executionTime' => number_format(microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'], 4) . ' Sekunden',
|
||||
'includedFiles' => count(get_included_files()),
|
||||
];
|
||||
|
||||
if (function_exists('opcache_get_status')) {
|
||||
try {
|
||||
$opcacheStatus = opcache_get_status(false);
|
||||
if ($opcacheStatus !== false) {
|
||||
$performanceData['opcacheMemoryUsage'] = $this->formatBytes($opcacheStatus['memory_usage']['used_memory']);
|
||||
$performanceData['opcacheCacheHits'] = number_format($opcacheStatus['opcache_statistics']['hits']);
|
||||
$performanceData['opcacheMissRate'] = number_format($opcacheStatus['opcache_statistics']['misses'] /
|
||||
($opcacheStatus['opcache_statistics']['hits'] + $opcacheStatus['opcache_statistics']['misses']) * 100, 2) . '%';
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// OPCache Status konnte nicht abgerufen werden
|
||||
$performanceData['opcacheError'] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return new ViewResult(
|
||||
template: 'performance',
|
||||
data: [
|
||||
'title' => 'Performance-Daten',
|
||||
'performance' => $performanceData
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[Auth]
|
||||
#[Route(path: '/admin/redis', method: Method::GET)]
|
||||
public function redisInfo(): ViewResult
|
||||
{
|
||||
$redisInfo = [];
|
||||
|
||||
try {
|
||||
$redis = $this->container->get('Predis\Client');
|
||||
$info = $redis->info();
|
||||
$redisInfo['status'] = 'Verbunden';
|
||||
$redisInfo['version'] = $info['redis_version'];
|
||||
$redisInfo['uptime'] = $this->formatUptime((int)$info['uptime_in_seconds']);
|
||||
$redisInfo['memory'] = $this->formatBytes((int)$info['used_memory']);
|
||||
$redisInfo['peak_memory'] = $this->formatBytes((int)$info['used_memory_peak']);
|
||||
$redisInfo['clients'] = $info['connected_clients'];
|
||||
$redisInfo['keys'] = $redis->dbsize();
|
||||
|
||||
// Einige Schlüssel auflisten (begrenzt auf 50)
|
||||
$keys = $redis->keys('*');
|
||||
$redisInfo['key_sample'] = array_slice($keys, 0, 50);
|
||||
} catch (\Throwable $e) {
|
||||
$redisInfo['status'] = 'Fehler: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
return new ViewResult(
|
||||
template: 'admin/redis',
|
||||
data: [
|
||||
'title' => 'Redis Information',
|
||||
'redis' => $redisInfo
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function formatBytes(int $bytes, int $precision = 2): string
|
||||
{
|
||||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
|
||||
$bytes /= (1 << (10 * $pow));
|
||||
|
||||
return round($bytes, $precision) . ' ' . $units[$pow];
|
||||
}
|
||||
|
||||
private function getMemoryLimitInBytes(): int
|
||||
{
|
||||
$memoryLimit = ini_get('memory_limit');
|
||||
|
||||
if ($memoryLimit === '-1') {
|
||||
return PHP_INT_MAX;
|
||||
}
|
||||
|
||||
$value = (int) $memoryLimit;
|
||||
$unit = strtolower($memoryLimit[strlen($memoryLimit)-1]);
|
||||
|
||||
switch($unit) {
|
||||
case 'g':
|
||||
$value *= 1024;
|
||||
case 'm':
|
||||
$value *= 1024;
|
||||
case 'k':
|
||||
$value *= 1024;
|
||||
break;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function getLoadedExtensions(): array
|
||||
{
|
||||
$extensions = get_loaded_extensions();
|
||||
sort($extensions);
|
||||
return $extensions;
|
||||
}
|
||||
|
||||
private function getActiveSessionCount(): int
|
||||
{
|
||||
try {
|
||||
if ($this->container->has(SessionManager::class)) {
|
||||
$sessionManager = $this->container->get(SessionManager::class);
|
||||
// Diese Methode müsste implementiert werden
|
||||
return $sessionManager->getActiveSessionCount();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Silent fail
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function getServerUptime(): string
|
||||
{
|
||||
// Für Linux-Systeme
|
||||
if (function_exists('shell_exec') && stripos(PHP_OS, 'Linux') !== false) {
|
||||
$uptime = shell_exec('uptime -p');
|
||||
if ($uptime) {
|
||||
return $uptime;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return 'Nicht verfügbar';
|
||||
}
|
||||
|
||||
private function formatUptime(int $seconds): string
|
||||
{
|
||||
$days = floor($seconds / 86400);
|
||||
$hours = floor(($seconds % 86400) / 3600);
|
||||
$minutes = floor(($seconds % 3600) / 60);
|
||||
$remainingSeconds = $seconds % 60;
|
||||
|
||||
$result = '';
|
||||
if ($days > 0) {
|
||||
$result .= "$days Tage, ";
|
||||
}
|
||||
|
||||
return $result . sprintf('%02d:%02d:%02d', $hours, $minutes, $remainingSeconds);
|
||||
}
|
||||
}
|
||||
37
src/Application/Admin/Images.php
Normal file
37
src/Application/Admin/Images.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Admin;
|
||||
|
||||
use App\Domain\Media\Image;
|
||||
use App\Domain\Media\ImageRepository;
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Auth\Auth;
|
||||
|
||||
final readonly class Images
|
||||
{
|
||||
#[Auth]
|
||||
#[Route('/admin/images')]
|
||||
public function showAll(ImageRepository $imageRepository)
|
||||
{
|
||||
$images = $imageRepository->findAll();
|
||||
|
||||
echo "<div style='background-color: #222; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); grid-gap: 16px; padding: 16px;'>";
|
||||
|
||||
/** @var Image $image */
|
||||
foreach($images as $image) {
|
||||
echo sprintf("<a href='/images/%s'>", $image->filename);
|
||||
|
||||
echo "<img src='/images/" . $image->filename . "' style='width: 400px; aspect-ratio: 1; object-fit: cover; border-radius: 16px;'/>";
|
||||
|
||||
echo "</a>";
|
||||
|
||||
var_dump($image->variants[0]->filename ?? 'no variant');
|
||||
}
|
||||
|
||||
echo "</div>";
|
||||
|
||||
debug($images);
|
||||
die();
|
||||
}
|
||||
}
|
||||
20
src/Application/Admin/RoutesViewModel.php
Normal file
20
src/Application/Admin/RoutesViewModel.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Admin;
|
||||
|
||||
use App\Framework\View\Template;
|
||||
|
||||
#[Template('routes')]
|
||||
class RoutesViewModel
|
||||
{
|
||||
public string $name = 'Michael';
|
||||
public string $title = 'Routes';
|
||||
|
||||
public function __construct(
|
||||
public array $routes = []
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
37
src/Application/Admin/ShowDiscovery.php
Normal file
37
src/Application/Admin/ShowDiscovery.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Admin;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Auth\Auth;
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Discovery\Results\DiscoveryResults;
|
||||
use App\Framework\View\TemplateDiscoveryVisitor;
|
||||
|
||||
class ShowDiscovery
|
||||
{
|
||||
public function __construct(
|
||||
private DiscoveryResults $results,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
#[Auth]
|
||||
#[Route('/admin/discovery')]
|
||||
public function show(
|
||||
#Cache $cache
|
||||
)
|
||||
{
|
||||
$attributes = $this->results->getAllAttributeResults();
|
||||
|
||||
foreach ($attributes as $name => $attribute) {
|
||||
echo "Attribute: $name <br/>";
|
||||
echo "<ul>";
|
||||
foreach ($attribute as $result) {
|
||||
echo "<li>" . $result['class'] . '::'.($result['method'] ?? '').'()</li>';
|
||||
};
|
||||
echo "</ul>";
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
86
src/Application/Admin/ShowImageSlots.php
Normal file
86
src/Application/Admin/ShowImageSlots.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Admin;
|
||||
|
||||
use App\Domain\Media\ImageSlot;
|
||||
use App\Domain\Media\ImageSlotRepository;
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Auth\Auth;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\Meta\MetaData;
|
||||
use App\Framework\Router\Result\ViewResult;
|
||||
|
||||
class ShowImageSlots
|
||||
{
|
||||
public function __construct(
|
||||
private ImageSlotRepository $imageSlotRepository,
|
||||
) {}
|
||||
|
||||
#[Auth]
|
||||
#[Route('/admin/imageslots')]
|
||||
public function show()
|
||||
{
|
||||
$slots = $this->imageSlotRepository->getSlots();
|
||||
|
||||
/** @var ImageSlot $slot */
|
||||
foreach($slots as $slot) {
|
||||
#echo $slot->slotName . '<br/>';
|
||||
|
||||
if($slot->image !== null) {
|
||||
# echo $slot->image->filename . '<br/>';
|
||||
}
|
||||
|
||||
$slotName = $slot->slotName;
|
||||
|
||||
}
|
||||
|
||||
return new ViewResult('imageslots', new MetaData('Image Slots', 'Image Slots'), [
|
||||
'slotName' => $slotName,
|
||||
'slots' => $slots,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Auth]
|
||||
#[Route('/admin/imageslots/{slotName}', method: Method::POST)]
|
||||
public function update(string $slotName): ViewResult
|
||||
{
|
||||
$slot = $this->imageSlotRepository->findBySlotName(urldecode($slotName));
|
||||
|
||||
$slotName = $slot->slotName;
|
||||
|
||||
#echo "<input type='text' value='$slotName' />";
|
||||
|
||||
return new ViewResult('imageslot', new MetaData('Image Slot', 'Image Slots'), [
|
||||
'slotName' => $slotName,
|
||||
'id' => $slot->id,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Auth]
|
||||
#[Route('/admin/imageslots/create', method: Method::POST)]
|
||||
public function create(Request $request)
|
||||
{
|
||||
$name = $request->parsedBody->get('slotName');
|
||||
|
||||
$slot = new ImageSlot(0, $name, '');
|
||||
|
||||
$this->imageSlotRepository->save($slot);
|
||||
|
||||
debug($name);
|
||||
}
|
||||
|
||||
#[Auth]
|
||||
#[Route('/admin/imageslots/edit/{id}', method: Method::PUT)]
|
||||
public function edit(Request $request, string $id)
|
||||
{
|
||||
$name = $request->parsedBody->get('slotName');
|
||||
|
||||
$slot = $this->imageSlotRepository->findById((int)$id);
|
||||
|
||||
|
||||
$slot = new ImageSlot($slot->id, $name, $slot->imageId);
|
||||
|
||||
$this->imageSlotRepository->save($slot);
|
||||
}
|
||||
}
|
||||
146
src/Application/Admin/ShowImageUpload.php
Normal file
146
src/Application/Admin/ShowImageUpload.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Admin;
|
||||
|
||||
use App\Domain\Media\Image;
|
||||
use App\Domain\Media\ImageRepository;
|
||||
use App\Domain\Media\ImageResizer;
|
||||
use App\Domain\Media\ImageVariantRepository;
|
||||
use App\Domain\Media\SaveImageFile;
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Auth\Auth;
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Database\Transaction;
|
||||
use App\Framework\DateTime\SystemClock;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\Http\UploadedFile;
|
||||
use App\Framework\Ulid\StringConverter;
|
||||
use App\Framework\Ulid\Ulid;
|
||||
use Media\Services\ImageService;
|
||||
use function imagecopyresampled;
|
||||
use function imagecreatefromjpeg;
|
||||
use function imagecreatetruecolor;
|
||||
use function imagedestroy;
|
||||
use function imagejpeg;
|
||||
|
||||
class ShowImageUpload
|
||||
{
|
||||
public function __construct(
|
||||
private PathProvider $pathProvider,
|
||||
private StringConverter $stringConverter,
|
||||
) {}
|
||||
|
||||
#[Auth]
|
||||
#[Route('/upload')]
|
||||
public function __invoke()
|
||||
{
|
||||
$html = <<<HTML
|
||||
<form action="/upload" method="post" enctype="multipart/form-data">
|
||||
<label for="image">Bild hochladen:</label>
|
||||
<input type="file" id="image" name="image" accept="image/*" required/>
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
|
||||
HTML;
|
||||
|
||||
echo $html;
|
||||
die();
|
||||
}
|
||||
#[Auth]
|
||||
#[Route('/upload', Method::POST)]
|
||||
public function upload(Request $request, Ulid $ulid, ImageRepository $imageRepository, ImageVariantRepository $imageVariantRepository,)
|
||||
{
|
||||
try {
|
||||
/** @var UploadedFile $file */
|
||||
$file = $request->files->get('image');
|
||||
|
||||
|
||||
$storageFolder = $this->pathProvider->resolvePath('/storage');
|
||||
|
||||
// Todo: Use Clock instead of date();
|
||||
$uploadDirectory = sprintf('uploads/%s/%s/%s', date('Y'), date('m'), date('d'));
|
||||
|
||||
$ulid = (string)$ulid; //$this->stringConverter->encodeBase32($ulid);
|
||||
|
||||
$id = $ulid;
|
||||
|
||||
// Remove Timestamp
|
||||
$id = substr($id, 10);
|
||||
|
||||
$hash = hash_file('sha256', $file->tmpName);
|
||||
|
||||
// Prüfen, ob ein Bild mit diesem Hash bereits existiert
|
||||
$existingImage = $imageRepository->findByHash($hash);
|
||||
if ($existingImage !== null) {
|
||||
echo "<h2>Bild bereits vorhanden</h2>";
|
||||
echo "<p>Dieses Bild wurde bereits hochgeladen.</p>";
|
||||
echo "<p>Bild-ID: " . htmlspecialchars($existingImage->ulid) . "</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
$idStr = str_pad((string)$id, 9, '0', STR_PAD_LEFT);
|
||||
$filePathPattern = sprintf('%s/%s/%s',
|
||||
substr($idStr, 0, 3),
|
||||
substr($idStr, 3, 3),
|
||||
substr($idStr, 6, 3),
|
||||
);
|
||||
|
||||
$path = $storageFolder . '/' . $uploadDirectory . '/' . $filePathPattern . "/";
|
||||
|
||||
$filename = $idStr . '_' . $hash . "_";
|
||||
|
||||
#dd($path . $filename . 'variant.png');
|
||||
|
||||
$smallPath = $path . $filename . 'small.png';
|
||||
|
||||
|
||||
|
||||
|
||||
[$width, $height] = getimagesize($file->tmpName);
|
||||
|
||||
|
||||
|
||||
$image = new Image(
|
||||
ulid : $ulid,
|
||||
filename : $filename . 'original.jpg',
|
||||
originalFilename: $file->name,
|
||||
mimeType : $file->getMimeType(),
|
||||
fileSize : $file->size,
|
||||
width : $width,
|
||||
height : $height,
|
||||
hash : $hash,
|
||||
path : $path,
|
||||
altText : 'Some alt text',
|
||||
);
|
||||
|
||||
|
||||
|
||||
$imageRepository->save($image, $file->tmpName);
|
||||
|
||||
|
||||
|
||||
#$image = $imageRepository->findById("0197B2CD759501F08D60312AE62ACCFC");
|
||||
|
||||
#mkdir($path, 0755, true);
|
||||
$variant = new ImageResizer()($image, 50, 50);
|
||||
|
||||
$imageVariantRepository->save($variant);;
|
||||
|
||||
$href = "/images/".$variant->filename;
|
||||
echo "<a href='$href'>$href</a>";
|
||||
|
||||
|
||||
#new SaveImageFile()($image, $file->tmpName);;
|
||||
debug($variant->filename);
|
||||
dd($image);
|
||||
|
||||
|
||||
|
||||
} catch (\Exception $e) {
|
||||
echo "<h2>Fehler beim Upload:</h2>";
|
||||
echo "<p>" . htmlspecialchars($e->getMessage()) . "</p>";
|
||||
debug($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/Application/Admin/ShowRoutes.php
Normal file
56
src/Application/Admin/ShowRoutes.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Admin;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Auth\Auth;
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Core\RouteCache;
|
||||
use App\Framework\Discovery\Results\DiscoveryResults;
|
||||
use App\Framework\Meta\MetaData;
|
||||
use App\Framework\Router\Result\ViewResult;
|
||||
|
||||
final readonly class ShowRoutes
|
||||
{
|
||||
public function __construct(
|
||||
private PathProvider $pathProvider,
|
||||
private DiscoveryResults $processedResults
|
||||
){}
|
||||
|
||||
#[Auth]
|
||||
#[Route('/admin/routes')]
|
||||
public function show(): ViewResult
|
||||
{
|
||||
$routes = $this->processedResults->get(Route::class);
|
||||
|
||||
sort($routes);
|
||||
|
||||
/*
|
||||
|
||||
echo '<div style="font-family: monospace; font-size: 12px;">';
|
||||
|
||||
echo str_repeat('-', 50) . "<br/>";
|
||||
|
||||
foreach($routes as $route) {
|
||||
$path = $route['path'];
|
||||
$line = "|-- $path";
|
||||
|
||||
$times = 50 - mb_strlen($line);
|
||||
|
||||
$line .= str_repeat(' .', $times) . "|<br/>";
|
||||
|
||||
echo $line;
|
||||
|
||||
}
|
||||
|
||||
echo str_repeat('-', 50) . "<br/></div>";*/
|
||||
|
||||
return new ViewResult('routes',
|
||||
metaData: new MetaData('Routes', 'Routes'),
|
||||
data: [
|
||||
'name' => 'Michael',
|
||||
'title' => 'Routes',
|
||||
],
|
||||
model: new RoutesViewModel($routes));
|
||||
}
|
||||
}
|
||||
27
src/Application/Admin/templates/admin-main.view.php
Normal file
27
src/Application/Admin/templates/admin-main.view.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Admin</title>
|
||||
<meta name="description" content="Admin">
|
||||
<meta property="og:type" content="Admin">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!--<header>
|
||||
<h2>Admin</h2>
|
||||
</header>-->
|
||||
|
||||
<menu>
|
||||
<li><a href="/admin">Dashboard</a></li>
|
||||
</menu>
|
||||
|
||||
|
||||
<main></main>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
146
src/Application/Admin/templates/dashboard.view.php
Normal file
146
src/Application/Admin/templates/dashboard.view.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<layout src="admin-main"/>
|
||||
|
||||
<a href="/admin/routes">Routes</a>
|
||||
|
||||
<br>
|
||||
|
||||
<a href="/admin/imageslots">Image Slots</a>
|
||||
|
||||
|
||||
<div class="section">
|
||||
<h2>Basis-Cards (nur semantische Selektoren)</h2>
|
||||
<div class="demo-grid">
|
||||
<article class="card">
|
||||
<header>
|
||||
<div>
|
||||
<h3>Projekt Alpha</h3>
|
||||
<small>Erstellt am 10. Juli 2025</small>
|
||||
</div>
|
||||
<span role="status">Aktiv</span>
|
||||
</header>
|
||||
<main>
|
||||
<p>Diese Card nutzt nur semantische HTML-Elemente. Das Styling erfolgt über Selektoren wie <code>.card > header</code> und <code>.card h3</code>.</p>
|
||||
<p>Weniger Klassen, sauberer HTML-Code.</p>
|
||||
</main>
|
||||
<footer>
|
||||
<small>Letztes Update: heute</small>
|
||||
<div>
|
||||
<button>Öffnen</button>
|
||||
<button>Teilen</button>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<header>
|
||||
<h3>Einfache Card</h3>
|
||||
</header>
|
||||
<main>
|
||||
<p>Minimaler HTML-Code, maximale Semantik.</p>
|
||||
</main>
|
||||
<footer>
|
||||
<div>
|
||||
<button>Action</button>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Status-Varianten (Klassen für Varianten)</h2>
|
||||
<div class="demo-grid">
|
||||
<article class="card card--success">
|
||||
<header>
|
||||
<h3>Erfolg</h3>
|
||||
<span role="status">Abgeschlossen</span>
|
||||
</header>
|
||||
<main>
|
||||
<p>Success-Variante durch eine einzige Modifier-Klasse.</p>
|
||||
</main>
|
||||
<footer>
|
||||
<div>
|
||||
<button>Details</button>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<article class="card card--error">
|
||||
<header>
|
||||
<h3>Fehler</h3>
|
||||
<span role="status">Problem</span>
|
||||
</header>
|
||||
<main>
|
||||
<p>Error-Variante mit systematischen Farben.</p>
|
||||
</main>
|
||||
<footer>
|
||||
<div>
|
||||
<button>Beheben</button>
|
||||
<button>Ignorieren</button>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Größen-Varianten</h2>
|
||||
<div class="demo-grid">
|
||||
<article class="card card--compact">
|
||||
<header>
|
||||
<h3>Kompakt</h3>
|
||||
</header>
|
||||
<main>
|
||||
<p>Weniger Padding durch Modifier-Klasse.</p>
|
||||
</main>
|
||||
</article>
|
||||
|
||||
<article class="card card--spacious">
|
||||
<header>
|
||||
<h3>Großzügig</h3>
|
||||
</header>
|
||||
<main>
|
||||
<p>Mehr Weißraum für wichtige Inhalte.</p>
|
||||
</main>
|
||||
<footer>
|
||||
<div>
|
||||
<button>Hauptaktion</button>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Layout-Varianten</h2>
|
||||
<div class="demo-grid demo-grid--wide">
|
||||
<article class="card card--horizontal">
|
||||
<header>
|
||||
<h3>Horizontal</h3>
|
||||
</header>
|
||||
<main>
|
||||
<p>Horizontales Layout durch Modifier.</p>
|
||||
</main>
|
||||
<footer>
|
||||
<div>
|
||||
<button>Action</button>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<article class="card card--media">
|
||||
<img src="https://picsum.photos/400/200?random=2" alt="Demo">
|
||||
<header>
|
||||
<h3>Mit Media</h3>
|
||||
</header>
|
||||
<main>
|
||||
<p>Bild wird durch Selector <code>.card--media img</code> gestylt.</p>
|
||||
</main>
|
||||
<footer>
|
||||
<div>
|
||||
<button>Ansehen</button>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
11
src/Application/Admin/templates/imageslot.view.php
Normal file
11
src/Application/Admin/templates/imageslot.view.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<layout src="admin-main"/>
|
||||
|
||||
<form action='/admin/imageslots/edit/{{ id }}' method='post'>
|
||||
<input type='hidden' name='_method' value='PUT'/>
|
||||
|
||||
<label>Slot Name:
|
||||
<input type='text' name='slotName' value='{{ slotName }}'/>
|
||||
</label>
|
||||
<input type='submit' value='Update'/>
|
||||
|
||||
</form>
|
||||
21
src/Application/Admin/templates/imageslots.view.php
Normal file
21
src/Application/Admin/templates/imageslots.view.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<layout src="admin-main"/>
|
||||
|
||||
<for var="slot" in="slots">
|
||||
|
||||
<form action='/admin/imageslots/{{ slot.slotName }}' method='post'>
|
||||
|
||||
<h3>{{ slot.slotName }}</h3>
|
||||
<input type='submit' value='Update'/>
|
||||
|
||||
</form>
|
||||
|
||||
</for>
|
||||
|
||||
<form action='/admin/imageslots/create' method='post'>
|
||||
|
||||
<label>Slot Name:
|
||||
<input type='text' name='slotName' value=''/>
|
||||
</label>
|
||||
<input type='submit' value='Create'/>
|
||||
|
||||
</form>
|
||||
11
src/Application/Admin/templates/routes.view.php
Normal file
11
src/Application/Admin/templates/routes.view.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<layout src="admin-main"/>
|
||||
|
||||
<div>Routes Page</div>
|
||||
|
||||
<p>{{ name }}</p>
|
||||
|
||||
<ul>
|
||||
<for var="route" in="routes">
|
||||
<li><a href="{{ route.path }}">{{ route.path }} </a> <i> ({{ route.class }}) {{ route.attributes.0 }} </i> </li>
|
||||
</for>
|
||||
</ul>
|
||||
96
src/Application/Admin/views/admin/dashboard.php
Normal file
96
src/Application/Admin/views/admin/dashboard.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= $title ?></title>
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<div class="admin-header">
|
||||
<h1>Framework Admin Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div class="admin-nav">
|
||||
<a href="/admin" class="active">Dashboard</a>
|
||||
<a href="/admin/routes">Routen</a>
|
||||
<a href="/admin/services">Dienste</a>
|
||||
<a href="/admin/environment">Umgebung</a>
|
||||
<a href="/admin/performance">Performance</a>
|
||||
<a href="/admin/redis">Redis</a>
|
||||
<a href="/admin/phpinfo">PHP Info</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-content">
|
||||
<div class="dashboard-stats">
|
||||
<div class="stat-box">
|
||||
<h3>Framework Version</h3>
|
||||
<div class="stat-value"><?= $stats['frameworkVersion'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>PHP Version</h3>
|
||||
<div class="stat-value"><?= $stats['phpVersion'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Speicherverbrauch</h3>
|
||||
<div class="stat-value"><?= $stats['memoryUsage'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Max. Speicherverbrauch</h3>
|
||||
<div class="stat-value"><?= $stats['peakMemoryUsage'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Server</h3>
|
||||
<div class="stat-value"><?= $stats['serverInfo'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Serverzeit</h3>
|
||||
<div class="stat-value"><?= $stats['serverTime'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Zeitzone</h3>
|
||||
<div class="stat-value"><?= $stats['timezone'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Betriebssystem</h3>
|
||||
<div class="stat-value"><?= $stats['operatingSystem'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Server Uptime</h3>
|
||||
<div class="stat-value"><?= $stats['uptime'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Aktive Sessions</h3>
|
||||
<div class="stat-value"><?= $stats['sessionCount'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Registrierte Dienste</h3>
|
||||
<div class="stat-value"><?= $stats['servicesCount'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-section">
|
||||
<h2>PHP Erweiterungen</h2>
|
||||
<div class="extensions-list">
|
||||
<?php foreach($stats['loadedExtensions'] as $extension): ?>
|
||||
<span class="extension-badge"><?= $extension ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-footer">
|
||||
<p>© <?= date('Y') ?> Framework Admin</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
86
src/Application/Admin/views/admin/environment.php
Normal file
86
src/Application/Admin/views/admin/environment.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= $title ?></title>
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<div class="admin-header">
|
||||
<h1>Umgebungsvariablen</h1>
|
||||
</div>
|
||||
|
||||
<div class="admin-nav">
|
||||
<a href="/admin">Dashboard</a>
|
||||
<a href="/admin/routes">Routen</a>
|
||||
<a href="/admin/services">Dienste</a>
|
||||
<a href="/admin/environment" class="active">Umgebung</a>
|
||||
<a href="/admin/performance">Performance</a>
|
||||
<a href="/admin/redis">Redis</a>
|
||||
<a href="/admin/phpinfo">PHP Info</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-content">
|
||||
<div class="admin-tools">
|
||||
<input type="text" id="envFilter" placeholder="Variablen filtern..." class="search-input">
|
||||
<div class="filter-tags">
|
||||
<button class="filter-tag" data-prefix="APP_">APP_</button>
|
||||
<button class="filter-tag" data-prefix="DB_">DB_</button>
|
||||
<button class="filter-tag" data-prefix="REDIS_">REDIS_</button>
|
||||
<button class="filter-tag" data-prefix="RATE_LIMIT_">RATE_LIMIT_</button>
|
||||
<button class="filter-tag" data-prefix="PHP_">PHP_</button>
|
||||
<button class="filter-tag active" data-prefix="">Alle</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="admin-table" id="envTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Wert</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach($env as $key => $value): ?>
|
||||
<tr>
|
||||
<td><?= $key ?></td>
|
||||
<td><?= is_array($value) ? json_encode($value) : $value ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="admin-footer">
|
||||
<p>© <?= date('Y') ?> Framework Admin</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Filterung der Umgebungsvariablen
|
||||
document.getElementById('envFilter').addEventListener('input', filterTable);
|
||||
|
||||
// Tag-Filter
|
||||
document.querySelectorAll('.filter-tag').forEach(tag => {
|
||||
tag.addEventListener('click', function() {
|
||||
document.querySelectorAll('.filter-tag').forEach(t => t.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
const prefix = this.getAttribute('data-prefix');
|
||||
document.getElementById('envFilter').value = prefix;
|
||||
filterTable();
|
||||
});
|
||||
});
|
||||
|
||||
function filterTable() {
|
||||
const filterValue = document.getElementById('envFilter').value.toLowerCase();
|
||||
const rows = document.querySelectorAll('#envTable tbody tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
const key = row.cells[0].textContent.toLowerCase();
|
||||
row.style.display = key.includes(filterValue) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
125
src/Application/Admin/views/admin/performance.view.php
Normal file
125
src/Application/Admin/views/admin/performance.view.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= $title ?></title>
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<div class="admin-header">
|
||||
<h1>Performance-Übersicht</h1>
|
||||
</div>
|
||||
|
||||
<div class="admin-nav">
|
||||
<a href="/admin">Dashboard</a>
|
||||
<a href="/admin/routes">Routen</a>
|
||||
<a href="/admin/services">Dienste</a>
|
||||
<a href="/admin/environment">Umgebung</a>
|
||||
<a href="/admin/performance" class="active">Performance</a>
|
||||
<a href="/admin/redis">Redis</a>
|
||||
<a href="/admin/phpinfo">PHP Info</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-content">
|
||||
<div class="dashboard-stats">
|
||||
<div class="stat-box">
|
||||
<h3>Aktueller Speicherverbrauch</h3>
|
||||
<div class="stat-value"><?= $performance['currentMemoryUsage'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Maximaler Speicherverbrauch</h3>
|
||||
<div class="stat-value"><?= $performance['peakMemoryUsage'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Speicherlimit</h3>
|
||||
<div class="stat-value"><?= $performance['memoryLimit'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Speicherauslastung</h3>
|
||||
<div class="stat-value">
|
||||
<div class="progress-bar">
|
||||
<div class="progress" style="width: <?= $performance['memoryUsagePercentage'] ?>%"></div>
|
||||
</div>
|
||||
<div class="progress-value"><?= $performance['memoryUsagePercentage'] ?>%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Systemlast (1/5/15 min)</h3>
|
||||
<div class="stat-value">
|
||||
<?= $performance['loadAverage'][0] ?> /
|
||||
<?= $performance['loadAverage'][1] ?> /
|
||||
<?= $performance['loadAverage'][2] ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>OPCache aktiviert</h3>
|
||||
<div class="stat-value"><?= $performance['opcacheEnabled'] ?></div>
|
||||
</div>
|
||||
|
||||
<?php if (isset($performance['opcacheMemoryUsage'])): ?>
|
||||
<div class="stat-box">
|
||||
<h3>OPCache Speicherverbrauch</h3>
|
||||
<div class="stat-value"><?= $performance['opcacheMemoryUsage'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>OPCache Cache Hits</h3>
|
||||
<div class="stat-value"><?= $performance['opcacheCacheHits'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>OPCache Miss Rate</h3>
|
||||
<div class="stat-value"><?= $performance['opcacheMissRate'] ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Ausführungszeit</h3>
|
||||
<div class="stat-value"><?= $performance['executionTime'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Geladene Dateien</h3>
|
||||
<div class="stat-value"><?= $performance['includedFiles'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-section">
|
||||
<h2>Geladene Dateien</h2>
|
||||
<div class="admin-tools">
|
||||
<input type="text" id="fileFilter" placeholder="Dateien filtern..." class="search-input">
|
||||
</div>
|
||||
|
||||
<div class="file-list" id="fileList">
|
||||
<?php foreach(get_included_files() as $file): ?>
|
||||
<div class="file-item">
|
||||
<?= $file ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-footer">
|
||||
<p>© <?= date('Y') ?> Framework Admin</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('fileFilter').addEventListener('input', function() {
|
||||
const filterValue = this.value.toLowerCase();
|
||||
const items = document.querySelectorAll('#fileList .file-item');
|
||||
|
||||
items.forEach(item => {
|
||||
const text = item.textContent.toLowerCase();
|
||||
item.style.display = text.includes(filterValue) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
105
src/Application/Admin/views/admin/redis.view.php
Normal file
105
src/Application/Admin/views/admin/redis.view.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= $title ?></title>
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<div class="admin-header">
|
||||
<h1>Redis-Informationen</h1>
|
||||
</div>
|
||||
|
||||
<div class="admin-nav">
|
||||
<a href="/admin">Dashboard</a>
|
||||
<a href="/admin/routes">Routen</a>
|
||||
<a href="/admin/services">Dienste</a>
|
||||
<a href="/admin/environment">Umgebung</a>
|
||||
<a href="/admin/performance">Performance</a>
|
||||
<a href="/admin/redis" class="active">Redis</a>
|
||||
<a href="/admin/phpinfo">PHP Info</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-content">
|
||||
<?php if (isset($redis['status']) && $redis['status'] === 'Verbunden'): ?>
|
||||
<div class="dashboard-stats">
|
||||
<div class="stat-box">
|
||||
<h3>Status</h3>
|
||||
<div class="stat-value status-connected"><?= $redis['status'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Version</h3>
|
||||
<div class="stat-value"><?= $redis['version'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Uptime</h3>
|
||||
<div class="stat-value"><?= $redis['uptime'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Speicherverbrauch</h3>
|
||||
<div class="stat-value"><?= $redis['memory'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Max. Speicherverbrauch</h3>
|
||||
<div class="stat-value"><?= $redis['peak_memory'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Verbundene Clients</h3>
|
||||
<div class="stat-value"><?= $redis['clients'] ?></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-box">
|
||||
<h3>Anzahl Schlüssel</h3>
|
||||
<div class="stat-value"><?= $redis['keys'] ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-section">
|
||||
<h2>Schlüssel (max. 50 angezeigt)</h2>
|
||||
<div class="admin-tools">
|
||||
<input type="text" id="keyFilter" placeholder="Schlüssel filtern..." class="search-input">
|
||||
</div>
|
||||
|
||||
<div class="key-list" id="keyList">
|
||||
<?php if (empty($redis['key_sample'])): ?>
|
||||
<div class="empty-message">Keine Schlüssel vorhanden</div>
|
||||
<?php else: ?>
|
||||
<?php foreach($redis['key_sample'] as $key): ?>
|
||||
<div class="key-item">
|
||||
<?= $key ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="error-message">
|
||||
<h2>Redis-Verbindung fehlgeschlagen</h2>
|
||||
<p><?= $redis['status'] ?? 'Unbekannter Fehler' ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="admin-footer">
|
||||
<p>© <?= date('Y') ?> Framework Admin</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('keyFilter')?.addEventListener('input', function() {
|
||||
const filterValue = this.value.toLowerCase();
|
||||
const items = document.querySelectorAll('#keyList .key-item');
|
||||
|
||||
items.forEach(item => {
|
||||
const text = item.textContent.toLowerCase();
|
||||
item.style.display = text.includes(filterValue) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
69
src/Application/Admin/views/admin/routes.php
Normal file
69
src/Application/Admin/views/admin/routes.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= $title ?></title>
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<div class="admin-header">
|
||||
<h1>Routen-Übersicht</h1>
|
||||
</div>
|
||||
|
||||
<div class="admin-nav">
|
||||
<a href="/admin">Dashboard</a>
|
||||
<a href="/admin/routes" class="active">Routen</a>
|
||||
<a href="/admin/services">Dienste</a>
|
||||
<a href="/admin/environment">Umgebung</a>
|
||||
<a href="/admin/performance">Performance</a>
|
||||
<a href="/admin/redis">Redis</a>
|
||||
<a href="/admin/phpinfo">PHP Info</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-content">
|
||||
<div class="admin-tools">
|
||||
<input type="text" id="routeFilter" placeholder="Routen filtern..." class="search-input">
|
||||
</div>
|
||||
|
||||
<table class="admin-table" id="routesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Pfad</th>
|
||||
<th>Methode</th>
|
||||
<th>Controller</th>
|
||||
<th>Aktion</th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach($routes as $route): ?>
|
||||
<tr>
|
||||
<td><?= $route->path ?></td>
|
||||
<td><?= $route->method ?></td>
|
||||
<td><?= $route->controllerClass ?></td>
|
||||
<td><?= $route->methodName ?></td>
|
||||
<td><?= $route->name ?? '-' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="admin-footer">
|
||||
<p>© <?= date('Y') ?> Framework Admin</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('routeFilter').addEventListener('input', function() {
|
||||
const filterValue = this.value.toLowerCase();
|
||||
const rows = document.querySelectorAll('#routesTable tbody tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(filterValue) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
72
src/Application/Admin/views/admin/services.php
Normal file
72
src/Application/Admin/views/admin/services.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= $title ?></title>
|
||||
<link rel="stylesheet" href="/css/admin.css">
|
||||
</head>
|
||||
<body class="admin-page">
|
||||
<div class="admin-header">
|
||||
<h1>Registrierte Dienste</h1>
|
||||
</div>
|
||||
|
||||
<div class="admin-nav">
|
||||
<a href="/admin">Dashboard</a>
|
||||
<a href="/admin/routes">Routen</a>
|
||||
<a href="/admin/services" class="active">Dienste</a>
|
||||
<a href="/admin/environment">Umgebung</a>
|
||||
<a href="/admin/performance">Performance</a>
|
||||
<a href="/admin/redis">Redis</a>
|
||||
<a href="/admin/phpinfo">PHP Info</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-content">
|
||||
<div class="admin-tools">
|
||||
<input type="text" id="serviceFilter" placeholder="Dienste filtern..." class="search-input">
|
||||
<span class="services-count"><?= count($services) ?> Dienste insgesamt</span>
|
||||
</div>
|
||||
|
||||
<div class="service-list" id="serviceList">
|
||||
<?php foreach($services as $service): ?>
|
||||
<div class="service-item">
|
||||
<div class="service-name"><?= $service ?></div>
|
||||
<?php
|
||||
$parts = explode('\\', $service);
|
||||
$category = $parts[1] ?? 'Unknown';
|
||||
$subCategory = $parts[2] ?? '';
|
||||
?>
|
||||
<div class="service-category">
|
||||
<span class="category-badge"><?= $category ?></span>
|
||||
<?php if ($subCategory): ?>
|
||||
<span class="subcategory-badge"><?= $subCategory ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-footer">
|
||||
<p>© <?= date('Y') ?> Framework Admin</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('serviceFilter').addEventListener('input', function() {
|
||||
const filterValue = this.value.toLowerCase();
|
||||
const items = document.querySelectorAll('#serviceList .service-item');
|
||||
let visibleCount = 0;
|
||||
|
||||
items.forEach(item => {
|
||||
const text = item.textContent.toLowerCase();
|
||||
const isVisible = text.includes(filterValue);
|
||||
item.style.display = isVisible ? '' : 'none';
|
||||
if (isVisible) visibleCount++;
|
||||
});
|
||||
|
||||
document.querySelector('.services-count').textContent =
|
||||
visibleCount + ' von ' + <?= count($services) ?> + ' Diensten';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Api;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Router\ActionResult;
|
||||
|
||||
class IrkEndpoint
|
||||
{
|
||||
#[Route(method: 'GET', path: '/irk-impressum')]
|
||||
public function impressum()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#[Route(method: 'GET', path: '/irk-datenschutz')]
|
||||
public function datenschutz()
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
14
src/Application/Auth/LoginRequest.php
Normal file
14
src/Application/Auth/LoginRequest.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Auth;
|
||||
|
||||
use App\Framework\Http\ControllerRequest;
|
||||
use App\Framework\Validation\Rules\Email;
|
||||
|
||||
final readonly class LoginRequest implements ControllerRequest
|
||||
{
|
||||
#[Email]
|
||||
public string $email;
|
||||
|
||||
public string $password;
|
||||
}
|
||||
13
src/Application/Auth/LoginUser.php
Normal file
13
src/Application/Auth/LoginUser.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Auth;
|
||||
|
||||
class LoginUser
|
||||
{
|
||||
public function __construct(
|
||||
public string $email,
|
||||
public string $password
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
14
src/Application/Auth/LoginUserHandler.php
Normal file
14
src/Application/Auth/LoginUserHandler.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Auth;
|
||||
|
||||
use App\Framework\CommandBus\CommandHandler;
|
||||
|
||||
class LoginUserHandler
|
||||
{
|
||||
#[CommandHandler]
|
||||
public function __invoke(LoginUser $loginUser)
|
||||
{
|
||||
var_dump($loginUser);
|
||||
}
|
||||
}
|
||||
38
src/Application/Auth/ShowLogin.php
Normal file
38
src/Application/Auth/ShowLogin.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Auth;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Auth\Auth;
|
||||
use App\Framework\CommandBus\CommandBus;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Router\Result\ViewResult;
|
||||
|
||||
class ShowLogin
|
||||
{
|
||||
public function __construct(
|
||||
private CommandBus $commandBus,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
#[Auth]
|
||||
#[Route('/login')]
|
||||
public function __invoke()
|
||||
{
|
||||
return new ViewResult('loginform');
|
||||
|
||||
}
|
||||
|
||||
#[Auth]
|
||||
#[Route('/login', Method::POST)]
|
||||
public function login(LoginRequest $request)
|
||||
{
|
||||
$login = new LoginUser($request->email, $request->password);
|
||||
|
||||
$this->commandBus->dispatch($login);
|
||||
|
||||
dd($request);
|
||||
|
||||
}
|
||||
}
|
||||
9
src/Application/Auth/templates/loginform.view.php
Normal file
9
src/Application/Auth/templates/loginform.view.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<layout src="loginform"/>
|
||||
|
||||
<form action="/login" method="post">
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" id="email" name="email" required/>
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" required/>
|
||||
<input type="submit" value="Login" />
|
||||
</form>
|
||||
46
src/Application/Backend/RapidMail/Dashbord.php
Normal file
46
src/Application/Backend/RapidMail/Dashbord.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Backend\RapidMail;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Infrastructure\Api\RapidMail\Commands\UpdateRecipientCommand;
|
||||
use App\Infrastructure\Api\RapidMail\RapidMailClient;
|
||||
use App\Infrastructure\Api\RapidMail\Recipient;
|
||||
use App\Infrastructure\Api\RapidMail\RecipientId;
|
||||
|
||||
final readonly class Dashbord
|
||||
{
|
||||
public function __construct(
|
||||
private RapidMailClient $rapidMailClient
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route(path: '/rapidmail', name: 'rapidmail')]
|
||||
public function __invoke()
|
||||
{
|
||||
$all = $this->rapidMailClient->recipients->get(RecipientId::fromInt(629237));
|
||||
#debug($all);
|
||||
|
||||
|
||||
#debug($all->email);
|
||||
|
||||
$command = new UpdateRecipientCommand(
|
||||
$all->id,
|
||||
$all->email,
|
||||
'Test',
|
||||
);
|
||||
|
||||
$user = $this->rapidMailClient->recipients->updateWithCommand($command);
|
||||
|
||||
#$user = $this->rapidMailClient->recipients->update(629237, Recipient::fromArray($array));
|
||||
|
||||
#$data = $this->rapidMailClient->recipientLists->getAll(); //->get(776);
|
||||
|
||||
#$name = $data[0]->name;
|
||||
|
||||
#$data = $this->rapidMailClient->statistics->getMailingStats(776);
|
||||
|
||||
debug($user);
|
||||
}
|
||||
}
|
||||
14
src/Application/Contact/ContactRequest.php
Normal file
14
src/Application/Contact/ContactRequest.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Contact;
|
||||
|
||||
use App\Framework\Http\ControllerRequest;
|
||||
use App\Framework\Validation\Rules\Email;
|
||||
|
||||
class ContactRequest implements ControllerRequest
|
||||
{
|
||||
public string $name;
|
||||
#[Email]
|
||||
public string $email;
|
||||
public string $message;
|
||||
}
|
||||
47
src/Application/Contact/ShowContact.php
Normal file
47
src/Application/Contact/ShowContact.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Contact;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\CommandBus\CommandBus;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Meta\Keywords;
|
||||
use App\Framework\Meta\StaticPageMetaResolver;
|
||||
use App\Framework\Router\ActionResult;
|
||||
use App\Framework\Router\Result\ContentNegotiationResult;
|
||||
use App\Framework\Router\Result\ViewResult;
|
||||
|
||||
final readonly class ShowContact
|
||||
{
|
||||
#[Route(path: '/kontakt', name: 'contact')]
|
||||
public function __invoke(): ViewResult
|
||||
{
|
||||
return new ViewResult('contact',
|
||||
new StaticPageMetaResolver(
|
||||
'Kontakt',
|
||||
'Kontaktseite!',
|
||||
Keywords::fromStrings('Kontakt', 'Welt')
|
||||
)(),);
|
||||
}
|
||||
|
||||
#[Route(path: '/kontakt', method: Method::POST)]
|
||||
public function senden(ContactRequest $request, CommandBus $commandBus): ActionResult
|
||||
{
|
||||
|
||||
$command = new StoreContact(
|
||||
$request->email,
|
||||
$request->name,
|
||||
$request->subject ?? 'Kein Betreff angegeben',
|
||||
$request->message,
|
||||
);
|
||||
|
||||
$commandBus->dispatch($command);
|
||||
|
||||
dd($request);
|
||||
|
||||
return new ContentNegotiationResult(
|
||||
|
||||
);
|
||||
#return new ViewResult('contact-senden');
|
||||
}
|
||||
}
|
||||
14
src/Application/Contact/StoreContact.php
Normal file
14
src/Application/Contact/StoreContact.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Contact;
|
||||
|
||||
final class StoreContact
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $email,
|
||||
public string $subject,
|
||||
public string $message,
|
||||
) {}
|
||||
}
|
||||
22
src/Application/Contact/StoreContactHandler.php
Normal file
22
src/Application/Contact/StoreContactHandler.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Contact;
|
||||
|
||||
use App\Domain\Contact\ContactMessage;
|
||||
use App\Domain\Contact\ContactRepository;
|
||||
use App\Framework\CommandBus\CommandHandler;
|
||||
|
||||
final readonly class StoreContactHandler
|
||||
{
|
||||
public function __construct(
|
||||
private ContactRepository $contactRepository,
|
||||
) {}
|
||||
#[CommandHandler]
|
||||
public function __invoke(StoreContact $command): void
|
||||
{
|
||||
$message = new ContactMessage($command->name, $command->email, $command->message);
|
||||
|
||||
$this->contactRepository->save($message);
|
||||
}
|
||||
}
|
||||
74
src/Application/Contact/contact.view.php
Normal file
74
src/Application/Contact/contact.view.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<layout src="main"/>
|
||||
|
||||
<section>
|
||||
|
||||
<h1>Kontakt</h1>
|
||||
|
||||
|
||||
<form action="/kontakt" method="post">
|
||||
|
||||
<!-- Feld ist für Bots gedacht – normaler User sieht es nicht -->
|
||||
<div style="position: absolute; left: -10000px; top: auto;" aria-hidden="true">
|
||||
<label for="website">Ihre Website:</label>
|
||||
<input type="text" name="website" id="website" tabindex="-1" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div role="alert">
|
||||
Hallo Welt
|
||||
</div>
|
||||
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" name="name" id="name">
|
||||
|
||||
<label for="email">E-Mail:</label>
|
||||
<input type="email" name="email" id="email">
|
||||
|
||||
<label for="subject">Betreff:</label>
|
||||
<select name="subject" id="subject">
|
||||
<option value="1">Anfrage</option>
|
||||
<option value="2">Beschwerde</option>
|
||||
<option value="3">Anregung</option>
|
||||
<option value="4">Sonstiges</option>
|
||||
</select>
|
||||
|
||||
<label for="message">Nachricht:</label>
|
||||
<textarea name="message" id="message"></textarea>
|
||||
|
||||
|
||||
<input type="submit" value="Senden">
|
||||
|
||||
</form>
|
||||
|
||||
</section>
|
||||
|
||||
<style>
|
||||
div.box {
|
||||
background: red;
|
||||
|
||||
}
|
||||
|
||||
div.outer {
|
||||
--outer-radius: 24px;
|
||||
--padding: 8px;
|
||||
--inner-radius: calc(var(--outer-radius) - var(--padding));
|
||||
|
||||
border-radius: var(--outer-radius);
|
||||
padding: var(--padding);
|
||||
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
}
|
||||
|
||||
.inner {
|
||||
border-radius: var(--inner-radius);
|
||||
--padding: 1rem;
|
||||
padding:inherit;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="box outer" style="background: grey;">
|
||||
Testbox
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,15 @@ namespace App\Application\EPK;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Router\ActionResult;
|
||||
use App\Framework\Router\GenericActionResult;
|
||||
use App\Framework\Router\Result\ViewResult;
|
||||
use App\Framework\Router\ResultType;
|
||||
|
||||
class ShowEpk
|
||||
{
|
||||
#[Route(path: '/epk')]
|
||||
#[Route(path: '/epk', name: 'epk')]
|
||||
public function epk(): ActionResult
|
||||
{
|
||||
return new ActionResult(ResultType::Html, 'epk', ['text' => 'EPK!']);
|
||||
return new ViewResult('epk', new MetaData('title'));
|
||||
}
|
||||
}
|
||||
|
||||
108
src/Application/Http/Controllers/ChatController.php
Normal file
108
src/Application/Http/Controllers/ChatController.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Http\Controllers;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Auth\Auth;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\WebSocketConnection;
|
||||
use App\Framework\Router\Result\WebSocketResult;
|
||||
|
||||
final class ChatController
|
||||
{
|
||||
private array $connections = [];
|
||||
|
||||
#[Auth]
|
||||
#[Route(path: '/chat/websocket', method: Method::GET)]
|
||||
public function chatWebSocket(): WebSocketResult
|
||||
{
|
||||
return new WebSocketResult()
|
||||
->onConnect(function(WebSocketConnection $connection) {
|
||||
$this->connections[$connection->getId()] = $connection;
|
||||
|
||||
// Willkommensnachricht senden
|
||||
$connection->sendJson([
|
||||
'type' => 'system',
|
||||
'message' => 'Willkommen im Chat!',
|
||||
'timestamp' => time()
|
||||
]);
|
||||
|
||||
// Andere Benutzer benachrichtigen
|
||||
$this->broadcast([
|
||||
'type' => 'user_joined',
|
||||
'message' => 'Ein neuer Benutzer ist dem Chat beigetreten',
|
||||
'timestamp' => time()
|
||||
], $connection->getId());
|
||||
})
|
||||
->onMessage(function(WebSocketConnection $connection, string $message) {
|
||||
$data = json_decode($message, true);
|
||||
|
||||
if (!$data || !isset($data['type'])) {
|
||||
$connection->sendJson(['error' => 'Invalid message format']);
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($data['type']) {
|
||||
case 'chat_message':
|
||||
$this->handleChatMessage($connection, $data);
|
||||
break;
|
||||
case 'ping':
|
||||
$connection->sendJson(['type' => 'pong']);
|
||||
break;
|
||||
default:
|
||||
$connection->sendJson(['error' => 'Unknown message type']);
|
||||
}
|
||||
})
|
||||
->onClose(function(WebSocketConnection $connection, int $code, string $reason) {
|
||||
unset($this->connections[$connection->getId()]);
|
||||
|
||||
// Andere Benutzer benachrichtigen
|
||||
$this->broadcast([
|
||||
'type' => 'user_left',
|
||||
'message' => 'Ein Benutzer hat den Chat verlassen',
|
||||
'timestamp' => time()
|
||||
]);
|
||||
})
|
||||
->onError(function(WebSocketConnection $connection, \Throwable $error) {
|
||||
error_log("WebSocket error: " . $error->getMessage());
|
||||
$connection->close(1011, 'Internal server error');
|
||||
})
|
||||
->withSubprotocols(['chat'])
|
||||
->withMaxMessageSize(10240) // 10KB
|
||||
->withPingInterval(30); // 30 Sekunden
|
||||
}
|
||||
|
||||
private function handleChatMessage(WebSocketConnection $sender, array $data): void
|
||||
{
|
||||
if (!isset($data['message'])) {
|
||||
$sender->sendJson(['error' => 'Message content required']);
|
||||
return;
|
||||
}
|
||||
|
||||
$message = [
|
||||
'type' => 'chat_message',
|
||||
'user_id' => $sender->getId(),
|
||||
'message' => $data['message'],
|
||||
'timestamp' => time()
|
||||
];
|
||||
|
||||
// Nachricht an alle Verbindungen senden
|
||||
$this->broadcast($message);
|
||||
}
|
||||
|
||||
private function broadcast(array $message, ?string $excludeId = null): void
|
||||
{
|
||||
foreach ($this->connections as $id => $connection) {
|
||||
if ($excludeId && $id === $excludeId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($connection->isConnected()) {
|
||||
$connection->sendJson($message);
|
||||
} else {
|
||||
unset($this->connections[$id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
160
src/Application/Http/Controllers/NotificationController.php
Normal file
160
src/Application/Http/Controllers/NotificationController.php
Normal file
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Http\Controllers;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Auth\Auth;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\SseStream;
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Router\Result\SseResult;
|
||||
use App\Framework\Router\Result\SseResultWithCallback;
|
||||
|
||||
/**
|
||||
* Controller für Echtzeit-Benachrichtigungen über Server-Sent Events
|
||||
*/
|
||||
final class NotificationController
|
||||
{
|
||||
/**
|
||||
* Stellt einen SSE-Stream für allgemeine Benachrichtigungen bereit
|
||||
*/
|
||||
#[Auth]
|
||||
#[Route(path: '/api/notifications/stream', method: Method::GET)]
|
||||
public function notificationStream(): SseResult
|
||||
{
|
||||
// SSE-Result mit 3 Sekunden Retry-Intervall
|
||||
$result = new SseResult(Status::OK, 3000);
|
||||
|
||||
// Initiale Verbindungsbestätigung
|
||||
$result->addJsonEvent(
|
||||
['message' => 'Verbunden mit dem Benachrichtigungsstream'],
|
||||
'connected',
|
||||
'conn-' . uniqid()
|
||||
);
|
||||
|
||||
// Einige Beispiel-Benachrichtigungen beim Start senden
|
||||
$result->addJsonEvent(
|
||||
[
|
||||
'type' => 'info',
|
||||
'title' => 'Willkommen',
|
||||
'message' => 'Sie sind jetzt mit Echtzeit-Updates verbunden.',
|
||||
'timestamp' => time()
|
||||
],
|
||||
'notification',
|
||||
'notif-' . uniqid()
|
||||
);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stellt einen benutzer-spezifischen SSE-Stream bereit
|
||||
*/
|
||||
#[Auth]
|
||||
#[Route(path: '/api/notifications/user/{userId}', method: Method::GET)]
|
||||
public function userNotifications(int $userId): SseResult
|
||||
{
|
||||
// SSE-Result mit benutzerdefinierten Headern
|
||||
$result = new SseResult(Status::OK, 3000, [
|
||||
'X-User-ID' => (string)$userId
|
||||
]);
|
||||
|
||||
// Verbindungsbestätigung mit Benutzer-ID
|
||||
$result->addJsonEvent(
|
||||
[
|
||||
'message' => 'Verbunden mit dem Benutzer-Stream',
|
||||
'userId' => $userId,
|
||||
'timestamp' => time()
|
||||
],
|
||||
'connected',
|
||||
'conn-user-' . $userId
|
||||
);
|
||||
|
||||
// Aktuelle Benachrichtigungen für den Benutzer laden
|
||||
$notifications = $this->getUserNotifications($userId);
|
||||
|
||||
// Benachrichtigungen zum Stream hinzufügen
|
||||
foreach ($notifications as $notification) {
|
||||
$result->addJsonEvent($notification, 'notification', $notification['id']);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt Benachrichtigungen für einen bestimmten Benutzer zurück
|
||||
* In einer realen Anwendung würde diese Methode Daten aus einer Datenbank laden
|
||||
*/
|
||||
private function getUserNotifications(int $userId): array
|
||||
{
|
||||
// Beispiel-Benachrichtigungen
|
||||
return [
|
||||
[
|
||||
'id' => 'notif-' . uniqid(),
|
||||
'type' => 'message',
|
||||
'title' => 'Neue Nachricht',
|
||||
'message' => 'Sie haben eine neue Nachricht erhalten',
|
||||
'timestamp' => time() - 300 // Vor 5 Minuten
|
||||
],
|
||||
[
|
||||
'id' => 'notif-' . uniqid(),
|
||||
'type' => 'system',
|
||||
'title' => 'System-Update',
|
||||
'message' => 'Das System wurde aktualisiert',
|
||||
'timestamp' => time() - 3600 // Vor 1 Stunde
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stellt einen Live-Stream mit dynamischen Updates bereit
|
||||
*/
|
||||
#[Auth]
|
||||
#[Route(path: '/api/notifications/live', method: Method::GET)]
|
||||
public function liveNotifications(): SseResult
|
||||
{
|
||||
// Erweiterte SSE-Konfiguration mit Callback
|
||||
#$result = new SseResultWithCallback(Status::OK, 3000);
|
||||
|
||||
// Callback für dynamische Updates festlegen
|
||||
$callback = function(SseStream $stream) {
|
||||
// Simuliere neue Benachrichtigungen (mit 10% Wahrscheinlichkeit)
|
||||
if (rand(1, 10) === 1) {
|
||||
$notificationTypes = ['info', 'warning', 'update', 'message'];
|
||||
$type = $notificationTypes[array_rand($notificationTypes)];
|
||||
|
||||
$notification = [
|
||||
'id' => 'live-notif-' . uniqid(),
|
||||
'type' => $type,
|
||||
'title' => 'Neue ' . ucfirst($type) . '-Benachrichtigung',
|
||||
'message' => 'Dies ist eine dynamisch generierte Benachrichtigung vom Typ ' . $type,
|
||||
'timestamp' => time()
|
||||
];
|
||||
|
||||
$stream->sendJson($notification, 'notification', $notification['id']);
|
||||
|
||||
// Kleine Pause nach dem Senden, um das Testszenario zu simulieren
|
||||
sleep(1);
|
||||
}
|
||||
};
|
||||
|
||||
$result = new SseResult(
|
||||
retryInterval: 500,
|
||||
callback: $callback,
|
||||
maxDuration: 300,
|
||||
heartbeatInterval: 15,
|
||||
);
|
||||
|
||||
// Initiale Verbindungsbestätigung
|
||||
$result->addJsonEvent(
|
||||
['message' => 'Verbunden mit dem Live-Stream', 'timestamp' => time()],
|
||||
'connected',
|
||||
'live-' . uniqid()
|
||||
);
|
||||
|
||||
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
101
src/Application/Http/Controllers/QrCodeController.php
Normal file
101
src/Application/Http/Controllers/QrCodeController.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Http\Controllers;
|
||||
|
||||
use App\Application\Service\QrCodeService;
|
||||
use App\Domain\QrCode\ValueObject\ErrorCorrectionLevel;
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Auth\Auth;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Http\Response;
|
||||
|
||||
final class QrCodeController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly QrCodeService $qrCodeService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen QR-Code als SVG
|
||||
*/
|
||||
#[Auth]
|
||||
#[Route(path: '/api/qrcode/svg', method: Method::GET)]
|
||||
public function generateSvg(): Response
|
||||
{
|
||||
$data = $_GET['data'] ?? 'https://example.com';
|
||||
$errorLevel = $this->getErrorLevel($_GET['error_level'] ?? 'M');
|
||||
$moduleSize = (int) ($_GET['module_size'] ?? 4);
|
||||
$margin = (int) ($_GET['margin'] ?? 4);
|
||||
$foreground = $_GET['foreground'] ?? '#000000';
|
||||
$background = $_GET['background'] ?? '#FFFFFF';
|
||||
|
||||
$config = new QrCodeConfig($moduleSize, $margin, $foreground, $background);
|
||||
|
||||
$svg = $this->qrCodeService->generateSvg($data, $errorLevel, $config);
|
||||
|
||||
return new Response(
|
||||
body: $svg,
|
||||
status: Status::OK,
|
||||
headers: ['Content-Type' => 'image/svg+xml']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen QR-Code als PNG
|
||||
*/
|
||||
#[Auth]
|
||||
#[Route(path: '/api/qrcode/png', method: Method::GET)]
|
||||
public function generatePng(): Response
|
||||
{
|
||||
$data = $_GET['data'] ?? 'https://example.com';
|
||||
$errorLevel = $this->getErrorLevel($_GET['error_level'] ?? 'M');
|
||||
$moduleSize = (int) ($_GET['module_size'] ?? 4);
|
||||
$margin = (int) ($_GET['margin'] ?? 4);
|
||||
|
||||
$config = new QrCodeConfig($moduleSize, $margin);
|
||||
|
||||
$png = $this->qrCodeService->generatePng($data, $errorLevel, $config);
|
||||
|
||||
return new Response(
|
||||
body: $png,
|
||||
status: Status::OK,
|
||||
headers: ['Content-Type' => 'image/png']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen QR-Code als ASCII-Art
|
||||
*/
|
||||
#[Auth]
|
||||
#[Route(path: '/api/qrcode/ascii', method: Method::GET)]
|
||||
public function generateAscii(): Response
|
||||
{
|
||||
$data = $_GET['data'] ?? 'https://example.com';
|
||||
$errorLevel = $this->getErrorLevel($_GET['error_level'] ?? 'M');
|
||||
|
||||
$ascii = $this->qrCodeService->generateAscii($data, $errorLevel);
|
||||
|
||||
return new Response(
|
||||
body: "<pre>$ascii</pre>",
|
||||
status: Status::OK,
|
||||
headers: ['Content-Type' => 'text/html; charset=utf-8']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert einen String in ein ErrorCorrectionLevel-Enum
|
||||
*/
|
||||
private function getErrorLevel(string $level): ErrorCorrectionLevel
|
||||
{
|
||||
return match (strtoupper($level)) {
|
||||
'L' => ErrorCorrectionLevel::L,
|
||||
'Q' => ErrorCorrectionLevel::Q,
|
||||
'H' => ErrorCorrectionLevel::H,
|
||||
default => ErrorCorrectionLevel::M,
|
||||
};
|
||||
}
|
||||
}
|
||||
146
src/Application/Http/Smartlink.php
Normal file
146
src/Application/Http/Smartlink.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Http;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\CommandBus\CommandBus;
|
||||
use App\Framework\Http\HeaderKey;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Router\ActionResult;
|
||||
use App\Framework\Router\Result\ViewResult;
|
||||
use App\Framework\Router\Result\Redirect;
|
||||
use App\Framework\Meta\MetaData;
|
||||
use App\Framework\Smartlinks\Actions\ActionRegistry;
|
||||
use App\Framework\Smartlinks\Commands\ExecuteSmartlinkCommand;
|
||||
use App\Framework\Smartlinks\Commands\GenerateSmartlinkCommand;
|
||||
use App\Framework\Smartlinks\Commands\GenerateSmartlinkHandler;
|
||||
use App\Framework\Smartlinks\Services\SmartlinkService;
|
||||
use App\Framework\Smartlinks\SmartLinkToken;
|
||||
use App\Framework\Smartlinks\TokenAction;
|
||||
|
||||
final readonly class Smartlink
|
||||
{
|
||||
public function __construct(
|
||||
private CommandBus $commandBus,
|
||||
private ActionRegistry $actionRegistry,
|
||||
private SmartlinkService $smartlinkService,
|
||||
private GenerateSmartlinkHandler $handler,
|
||||
) {}
|
||||
|
||||
#[Route('/smartlink/{token}', method: Method::GET)]
|
||||
#[Route('/smartlink/{token}', method: Method::POST)]
|
||||
public function execute(string $token, Request $request): ActionResult
|
||||
{
|
||||
$command = new GenerateSmartlinkCommand(
|
||||
action: new TokenAction('email_verification'),
|
||||
payload: ['user_id' => 123, 'email' => 'user@example.com'],
|
||||
baseUrl: 'https://localhost'
|
||||
);
|
||||
|
||||
#debug($this->handler->handle($command));
|
||||
|
||||
try {
|
||||
$smartlinkToken = new SmartlinkToken($token);
|
||||
|
||||
// Token validieren
|
||||
$smartlinkData = $this->smartlinkService->validate($smartlinkToken);
|
||||
if (!$smartlinkData) {
|
||||
return new ViewResult(
|
||||
template: 'smartlinks-error',
|
||||
metaData: new MetaData(
|
||||
title: 'Ungültiger Link',
|
||||
description: 'Der Link ist ungültig oder abgelaufen'
|
||||
),
|
||||
data: [
|
||||
'error' => 'Ungültiger oder abgelaufener Link',
|
||||
'error_code' => 'INVALID_TOKEN'
|
||||
],
|
||||
status: Status::NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
// Action holen
|
||||
$action = $this->actionRegistry->get($smartlinkData->action);
|
||||
if (!$action) {
|
||||
return new ViewResult(
|
||||
template: 'smartlinks-error',
|
||||
metaData: new MetaData(
|
||||
title: 'Unbekannte Aktion',
|
||||
description: 'Die angeforderte Aktion ist nicht verfügbar'
|
||||
),
|
||||
data: [
|
||||
'error' => 'Unbekannte Aktion',
|
||||
'error_code' => 'UNKNOWN_ACTION'
|
||||
],
|
||||
status: Status::BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
// Context für Action vorbereiten
|
||||
$context = [
|
||||
'request_method' => $request->method->value,
|
||||
'request_data' => $request->parsedBody ?? [],
|
||||
'query_params' => $request->queryParameters ?? [],
|
||||
#'ip_address' => $request->serverEnvironment->ipAddress?->value,
|
||||
'user_agent' => $request->headers->getFirst(HeaderKey::USER_AGENT),
|
||||
'headers' => $request->headers
|
||||
];
|
||||
|
||||
// Command ausführen
|
||||
$command = new ExecuteSmartlinkCommand($smartlinkToken, $context);
|
||||
$result = $this->commandBus->dispatch($command);
|
||||
|
||||
// Ergebnis verarbeiten
|
||||
if (!$result->isSuccess()) {
|
||||
return new ViewResult(
|
||||
template: $action->getErrorTemplate(),
|
||||
metaData: new MetaData(
|
||||
title: 'Fehler bei Ausführung',
|
||||
description: $result->message
|
||||
),
|
||||
data: [
|
||||
'error' => $result->message,
|
||||
'errors' => $result->errors,
|
||||
'action' => $action->getName()
|
||||
],
|
||||
status: Status::BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
// Redirect oder View
|
||||
if ($result->hasRedirect()) {
|
||||
return new Redirect($result->redirectUrl);
|
||||
}
|
||||
|
||||
return new ViewResult(
|
||||
template: $action->getViewTemplate(),
|
||||
metaData: new MetaData(
|
||||
title: $action->getName(),
|
||||
description: $result->message
|
||||
),
|
||||
data: [
|
||||
'result' => $result,
|
||||
'action' => $action->getName(),
|
||||
'token' => $token
|
||||
]
|
||||
);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return new ViewResult(
|
||||
template: 'smartlinks-error',
|
||||
metaData: new MetaData(
|
||||
title: 'Systemfehler',
|
||||
description: 'Ein unerwarteter Fehler ist aufgetreten'
|
||||
),
|
||||
data: [
|
||||
'error' => 'Ein Fehler ist aufgetreten',
|
||||
'error_code' => 'SYSTEM_ERROR',
|
||||
'debug_message' => $e->getMessage() // Nur für Development
|
||||
],
|
||||
status: Status::INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/Application/Http/templates/smartlinks-error.view.php
Normal file
9
src/Application/Http/templates/smartlinks-error.view.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<layout src="main"/>
|
||||
<div class="error-container">
|
||||
<h1>Fehler</h1>
|
||||
<p>{{ error }}</p>
|
||||
|
||||
<?php if (isset($error_code)): ?>
|
||||
<p><small>Fehlercode: <?= htmlspecialchars($error_code) ?></small></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
48
src/Application/Media/AdaptiveVideoStream.php
Normal file
48
src/Application/Media/AdaptiveVideoStream.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Media;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Http\Responses\AdaptiveStreamResponse;
|
||||
use App\Framework\Http\Responses\StreamResponse;
|
||||
use App\Framework\Http\Streaming\AdaptiveStreamingController;
|
||||
|
||||
final readonly class AdaptiveVideoStream
|
||||
{
|
||||
public function __construct(
|
||||
private AdaptiveStreamingController $adaptiveStreamingController,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
#[Route('/videos/{videoId}/manifest')]
|
||||
public function manifestAutoFormat(string $videoId): AdaptiveStreamResponse
|
||||
{
|
||||
return $this->adaptiveStreamingController->manifest($videoId);
|
||||
}
|
||||
|
||||
#[Route('/videos/{videoId}/manifest.m3u8')]
|
||||
public function hlsManifest(string $videoId): AdaptiveStreamResponse
|
||||
{
|
||||
return $this->adaptiveStreamingController->hlsManifest($videoId);
|
||||
}
|
||||
|
||||
#[Route('/videos/{videoId}/manifest.mpd')]
|
||||
public function dashManifest(string $videoId): AdaptiveStreamResponse
|
||||
{
|
||||
return $this->adaptiveStreamingController->dashManifest($videoId);
|
||||
}
|
||||
|
||||
#[Route('/videos/{videoId}/{quality}.m3u8')]
|
||||
public function hlsMediaPlaylist(string $videoId, string $quality): AdaptiveStreamResponse
|
||||
{
|
||||
return $this->adaptiveStreamingController->hlsMediaPlaylist($videoId, $quality);
|
||||
}
|
||||
|
||||
#[Route('/videos/{videoId}/{quality}/{segment}/')]
|
||||
public function segment(string $videoId, string $quality, string $segment): StreamResponse
|
||||
{
|
||||
return $this->adaptiveStreamingController->segment($videoId, $quality, $segment);
|
||||
}
|
||||
}
|
||||
93
src/Application/Media/ShowImage.php
Normal file
93
src/Application/Media/ShowImage.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Media;
|
||||
|
||||
use App\Domain\Media\ImageRepository;
|
||||
use App\Domain\Media\ImageVariantRepository;
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Router\Result\FileResult;
|
||||
|
||||
final readonly class ShowImage
|
||||
{
|
||||
public function __construct(
|
||||
private PathProvider $pathProvider,
|
||||
private ImageRepository $imageRepository,
|
||||
private ImageVariantRepository $imageVariantRepository,
|
||||
) {}
|
||||
#[Route('/images/{filename}')]
|
||||
public function __invoke($filename): FileResult
|
||||
{
|
||||
$path = $this->pathProvider->resolvePath('storage/uploads/');
|
||||
|
||||
$image = $this->imageRepository->findByFilename($filename);
|
||||
|
||||
if($image === null) {
|
||||
$image = $this->imageVariantRepository->findByFilename($filename);
|
||||
}
|
||||
|
||||
if($image === null) {
|
||||
throw new \Exception('Image not found');
|
||||
}
|
||||
|
||||
$file = $image->path . $image->filename;
|
||||
|
||||
if (file_exists($file)) {
|
||||
header('Content-Type: image/jpeg');
|
||||
header('Content-Length: '.filesize($file));
|
||||
// optional: Caching-Header
|
||||
header('Cache-Control: public, max-age=86400');
|
||||
|
||||
// Hier: Download-Dateiname vorschlagen
|
||||
// inline = im Browser anzeigen; attachment = Download erzwingen
|
||||
header('Content-Disposition: inline; filename="' . $image->filename . '-2025"');
|
||||
// oder
|
||||
// header('Content-Disposition: attachment; filename="' . basename($filename) . '"');
|
||||
|
||||
// 1. Last-Modified & ETag (Conditional Requests)
|
||||
$lastModified = gmdate('D, d M Y H:i:s', filemtime($file)).' GMT';
|
||||
$eTag = '"'.md5_file($file).'"';
|
||||
header("Last-Modified: $lastModified");
|
||||
header("ETag: $eTag");
|
||||
|
||||
// 304 Not Modified – wenn Browser-Cache gültig
|
||||
if (
|
||||
(isset($_SERVER['HTTP_IF_NONE_MATCH']) && trim($_SERVER['HTTP_IF_NONE_MATCH']) === $eTag) ||
|
||||
(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $_SERVER['HTTP_IF_MODIFIED_SINCE'] === $lastModified)
|
||||
) {
|
||||
http_response_code(304);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Expires (optional, zusätzlich zu Cache-Control)
|
||||
$expireTime = gmdate('D, d M Y H:i:s', time() + 86400).' GMT';
|
||||
header("Expires: $expireTime");
|
||||
|
||||
// 3. Accept-Ranges (für Partial-Requests / Resuming)
|
||||
header('Accept-Ranges: bytes');
|
||||
|
||||
// 4. Vary (bei Content-Negotiation, z. B. für WebP)
|
||||
header('Vary: Accept');
|
||||
|
||||
// 5. Security-Header
|
||||
// Verhindert MIME-Sniffing
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
// Schutz gegen Clickjacking (falls du Bilder in iframes erlauben willst, ggf. anpassen)
|
||||
header('X-Frame-Options: SAMEORIGIN');
|
||||
// Referrer-Policy für Bildanfragen
|
||||
header('Referrer-Policy: no-referrer-when-downgrade');
|
||||
|
||||
// 6. Content-Security-Policy (CSP) – strengere Kontrolle, wenn du das Bild per <img> einbindest
|
||||
// Hier als Beispiel nur für Bilder aus eigenem Domain
|
||||
header("Content-Security-Policy: default-src 'self'; img-src 'self';");
|
||||
|
||||
readfile($file);
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
return new FileResult($file);
|
||||
}
|
||||
}
|
||||
45
src/Application/Media/ShowVideo.php
Normal file
45
src/Application/Media/ShowVideo.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Media;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\Http\Responses\MediaType;
|
||||
use App\Framework\Http\Responses\StreamResponse;
|
||||
use App\Framework\Http\Streaming\MimeTypeDetector;
|
||||
use App\Framework\Http\Streaming\RangeParser;
|
||||
|
||||
final readonly class ShowVideo
|
||||
{
|
||||
public function __construct(
|
||||
private PathProvider $pathProvider,
|
||||
) {}
|
||||
|
||||
#[Route('/videos/{filename}')]
|
||||
public function __invoke(string $filename): StreamResponse
|
||||
{
|
||||
$filePath = $this->pathProvider->resolvePath('/storage/uploads/videos/'.$filename);
|
||||
|
||||
#dd($path);
|
||||
|
||||
#$filePath = "/media/{$filename}";
|
||||
$fileSize = filesize($filePath);
|
||||
$mimeType = MimeTypeDetector::detect($filePath);
|
||||
|
||||
$range = null;
|
||||
if (isset($_SERVER['HTTP_RANGE'])) {
|
||||
$range = RangeParser::parseRange($_SERVER['HTTP_RANGE'], $fileSize);
|
||||
}
|
||||
|
||||
return new StreamResponse(
|
||||
filePath : $filePath,
|
||||
fileSize : $fileSize,
|
||||
mimeType : $mimeType,
|
||||
range : $range,
|
||||
mediaType: MediaType::fromMimeType($mimeType)
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
29
src/Application/Newsletter/SignUp/EventHandlerExample.php
Normal file
29
src/Application/Newsletter/SignUp/EventHandlerExample.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Newsletter\SignUp;
|
||||
|
||||
use App\Framework\EventBus\EventHandler;
|
||||
|
||||
/**
|
||||
* Beispiel für einen Event-Handler für das UserWasSignedUp-Event
|
||||
*/
|
||||
final readonly class EventHandlerExample
|
||||
{
|
||||
/**
|
||||
* Behandelt das UserWasSignedUp-Event
|
||||
*
|
||||
* @param UserWasSignedUp $event Das Event
|
||||
* @return void
|
||||
*/
|
||||
#[EventHandler]
|
||||
public function handleUserSignedUp(UserWasSignedUp $event): void
|
||||
{
|
||||
// Hier kann die Verarbeitung des Events implementiert werden
|
||||
// Zum Beispiel: E-Mail senden, Statistiken aktualisieren, etc.
|
||||
|
||||
error_log($event->email . ' wurde für den Newsletter angemeldet.');
|
||||
|
||||
#var_dump($event);
|
||||
}
|
||||
}
|
||||
42
src/Application/Newsletter/SignUp/NewsletterSignup.php
Normal file
42
src/Application/Newsletter/SignUp/NewsletterSignup.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Newsletter\SignUp;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\CommandBus\CommandBus;
|
||||
use App\Framework\CommandBus\DefaultCommandBus;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Router\Result\ContentNegotiationResult;
|
||||
use App\Framework\Router\Result\JsonResult;
|
||||
|
||||
final readonly class NewsletterSignup
|
||||
{
|
||||
public function __construct(
|
||||
public CommandBus $commandBus,
|
||||
) {}
|
||||
|
||||
#[Route(path: '/newsletter/register', method: Method::POST)]
|
||||
public function __invoke(NewsletterSignupRequest $request): ContentNegotiationResult
|
||||
{
|
||||
// Den internen Command ausführen
|
||||
$command = new SignupUserToNewsletter($request->name, $request->email);
|
||||
|
||||
$result = $this->commandBus->dispatch($command);
|
||||
|
||||
|
||||
// Hier könnten Sie das Ergebnis weiterverarbeiten oder loggen
|
||||
return new ContentNegotiationResult(
|
||||
jsonPayload: [
|
||||
'success' => true,
|
||||
'message' => 'Anmeldung erfolgreich!',
|
||||
'data' => [
|
||||
$request->name,
|
||||
$request->email,
|
||||
],
|
||||
],
|
||||
redirectTo: '/'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Newsletter\SignUp;
|
||||
|
||||
use App\Framework\CommandBus\CommandHandler;
|
||||
use App\Framework\EventBus\EventBus;
|
||||
use App\Infrastructure\Api\RapidMailClient;
|
||||
use Archive\Config\ApiConfig;
|
||||
|
||||
final readonly class NewsletterSignupHandler
|
||||
{
|
||||
public function __construct(
|
||||
private EventBus $eventBus,
|
||||
) {}
|
||||
|
||||
#[CommandHandler]
|
||||
public function __invoke(SignupUserToNewsletter $command):void
|
||||
{
|
||||
// RapidMail-Client erstellen und konfigurieren
|
||||
$client = new RapidMailClient(
|
||||
ApiConfig::RAPIDMAIL_USERNAME->value,
|
||||
ApiConfig::RAPIDMAIL_PASSWORD->value
|
||||
);
|
||||
|
||||
// Abonnent zur Liste hinzufügen
|
||||
$result = $client->addRecipient(
|
||||
$command->name,
|
||||
$command->email,
|
||||
ApiConfig::getRapidmailListId()
|
||||
);
|
||||
|
||||
error_log('CommandHandler für: '.$command->email);;
|
||||
|
||||
$this->eventBus->dispatch(new UserWasSignedUp($command->email));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Newsletter\SignUp;
|
||||
|
||||
use App\Framework\Http\ControllerRequest;
|
||||
use App\Framework\Validation\Rules\Email;
|
||||
use App\Framework\Validation\Rules\IsTrue;
|
||||
use App\Framework\Validation\Rules\Required;
|
||||
use App\Framework\Validation\Rules\StringLength;
|
||||
|
||||
final readonly class NewsletterSignupRequest implements ControllerRequest
|
||||
{
|
||||
#[Email]
|
||||
public string $email;
|
||||
|
||||
#[StringLength(min: 3, max: 255)]
|
||||
public string $name;
|
||||
#[IsTrue]
|
||||
public bool $consent;
|
||||
}
|
||||
15
src/Application/Newsletter/SignUp/SignupUserToNewsletter.php
Normal file
15
src/Application/Newsletter/SignUp/SignupUserToNewsletter.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Newsletter\SignUp;
|
||||
|
||||
use App\Framework\CommandBus\ShouldQueue;
|
||||
|
||||
#[ShouldQueue]
|
||||
final readonly class SignupUserToNewsletter
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $email,
|
||||
) {}
|
||||
}
|
||||
15
src/Application/Newsletter/SignUp/UserWasSignedUp.php
Normal file
15
src/Application/Newsletter/SignUp/UserWasSignedUp.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Newsletter\SignUp;
|
||||
|
||||
/**
|
||||
* Event, das ausgelöst wird, wenn ein Benutzer für den Newsletter angemeldet wurde
|
||||
*/
|
||||
final readonly class UserWasSignedUp
|
||||
{
|
||||
public function __construct(public string $email)
|
||||
{
|
||||
// Event-Daten können hier hinzugefügt werden
|
||||
}
|
||||
}
|
||||
8
src/Application/NewsletterSignup.http
Normal file
8
src/Application/NewsletterSignup.http
Normal file
@@ -0,0 +1,8 @@
|
||||
### POST request to example server
|
||||
POST https://localhost/newsletter/register
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
email = michael.schiemer@gmail.com &
|
||||
consent = 1
|
||||
|
||||
###
|
||||
21
src/Application/Security/Events/Access/CsrfViolation.php
Normal file
21
src/Application/Security/Events/Access/CsrfViolation.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\Access;
|
||||
|
||||
use App\Application\Security\SecurityEvent;
|
||||
use App\Application\Security\SecurityEventType;
|
||||
|
||||
final class CsrfViolation implements SecurityEvent
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $requestPath,
|
||||
public readonly string $method,
|
||||
) {}
|
||||
|
||||
public SecurityEventType $type {
|
||||
get {
|
||||
return SecurityEventType::CSRF_VIOLATION;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/Application/Security/Events/Auth/AccountLockedEvent.php
Normal file
51
src/Application/Security/Events/Auth/AccountLockedEvent.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\Auth;
|
||||
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel};
|
||||
|
||||
final class AccountLockedEvent
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $email,
|
||||
public readonly string $reason,
|
||||
public readonly int $failedAttempts
|
||||
) {}
|
||||
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier
|
||||
{
|
||||
return OWASPEventIdentifier::accountLocked($this->maskEmail($this->email));
|
||||
}
|
||||
|
||||
public function getOWASPLogLevel(): OWASPLogLevel
|
||||
{
|
||||
return OWASPLogLevel::ERROR;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "Account {$this->maskEmail($this->email)} locked";
|
||||
}
|
||||
|
||||
public function getEventData(): array
|
||||
{
|
||||
return [
|
||||
'username' => $this->maskEmail($this->email),
|
||||
'lock_reason' => $this->reason,
|
||||
'failed_attempts' => $this->failedAttempts
|
||||
];
|
||||
}
|
||||
|
||||
private function maskEmail(string $email): string
|
||||
{
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return $email;
|
||||
}
|
||||
|
||||
[$local, $domain] = explode('@', $email, 2);
|
||||
$maskedLocal = substr($local, 0, 2) . str_repeat('*', max(0, strlen($local) - 2));
|
||||
|
||||
return $maskedLocal . '@' . $domain;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\Auth;
|
||||
|
||||
use App\Application\Security\{OWASPSecurityEvent};
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
|
||||
|
||||
final class AuthenticationFailedEvent implements OWASPSecurityEvent
|
||||
{
|
||||
private MaskedEmail $maskedEmail;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $email,
|
||||
public readonly ?string $reason = null,
|
||||
public readonly int $failedAttempts = 1
|
||||
) {
|
||||
$this->maskedEmail = MaskedEmail::fromString($this->email);
|
||||
}
|
||||
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier
|
||||
{
|
||||
return OWASPEventIdentifier::authenticationFailure($this->maskedEmail->toString());
|
||||
}
|
||||
|
||||
public function getOWASPLogLevel(): OWASPLogLevel
|
||||
{
|
||||
return OWASPLogLevel::WARN;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "User {$this->maskedEmail->toString()} login failed" .
|
||||
($this->reason ? " - {$this->reason}" : '');
|
||||
}
|
||||
|
||||
public function getEventData(): array
|
||||
{
|
||||
return [
|
||||
'email' => $this->maskedEmail->toString(),
|
||||
'reason' => $this->reason,
|
||||
'failed_attempts' => $this->failedAttempts,
|
||||
'failure_reason' => $this->reason ?? 'invalid_credentials'
|
||||
];
|
||||
}
|
||||
|
||||
public function getMaskedEmail(): MaskedEmail
|
||||
{
|
||||
return $this->maskedEmail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\Auth;
|
||||
|
||||
use App\Application\Security\{OWASPSecurityEvent};
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
|
||||
|
||||
final class AuthenticationSuccessEvent implements OWASPSecurityEvent
|
||||
{
|
||||
private MaskedEmail $maskedEmail;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $email,
|
||||
public readonly string $sessionId,
|
||||
public readonly ?string $method = 'password'
|
||||
) {
|
||||
$this->maskedEmail = MaskedEmail::fromString($this->email);
|
||||
}
|
||||
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier
|
||||
{
|
||||
return OWASPEventIdentifier::authenticationSuccess($this->maskedEmail->toString());
|
||||
}
|
||||
|
||||
public function getOWASPLogLevel(): OWASPLogLevel
|
||||
{
|
||||
return OWASPLogLevel::INFO;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "User {$this->maskedEmail->toString()} login successfully";
|
||||
}
|
||||
|
||||
public function getEventData(): array
|
||||
{
|
||||
return [
|
||||
'username' => $this->maskedEmail->toString(),
|
||||
'session_id' => hash('sha256', $this->sessionId), // Session-ID hashen für Sicherheit
|
||||
'method' => $this->method
|
||||
];
|
||||
}
|
||||
|
||||
public function getMaskedEmail(): MaskedEmail
|
||||
{
|
||||
return $this->maskedEmail;
|
||||
}
|
||||
}
|
||||
20
src/Application/Security/Events/Auth/LoginFailed.php
Normal file
20
src/Application/Security/Events/Auth/LoginFailed.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\Auth;
|
||||
|
||||
use App\Application\Security\SecurityEvent;
|
||||
use App\Application\Security\SecurityEventType;
|
||||
|
||||
final class LoginFailed implements SecurityEvent
|
||||
{
|
||||
public function __construct(
|
||||
public string $email
|
||||
) {}
|
||||
|
||||
public SecurityEventType $type {
|
||||
get {
|
||||
return SecurityEventType::LOGIN_FAILED;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\Auth;
|
||||
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel};
|
||||
|
||||
final class PasswordChangedEvent
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $email,
|
||||
public readonly string $method = 'self_service'
|
||||
) {}
|
||||
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier
|
||||
{
|
||||
return OWASPEventIdentifier::passwordChange($this->maskEmail($this->email));
|
||||
}
|
||||
|
||||
public function getOWASPLogLevel(): OWASPLogLevel
|
||||
{
|
||||
return OWASPLogLevel::INFO;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "User {$this->maskEmail($this->email)} changed password";
|
||||
}
|
||||
|
||||
public function getEventData(): array
|
||||
{
|
||||
return [
|
||||
'username' => $this->maskEmail($this->email),
|
||||
'change_method' => $this->method
|
||||
];
|
||||
}
|
||||
|
||||
private function maskEmail(string $email): string
|
||||
{
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return $email;
|
||||
}
|
||||
|
||||
[$local, $domain] = explode('@', $email, 2);
|
||||
$maskedLocal = substr($local, 0, 2) . str_repeat('*', max(0, strlen($local) - 2));
|
||||
|
||||
return $maskedLocal . '@' . $domain;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\Auth;
|
||||
|
||||
use App\Application\Security\{OWASPSecurityEvent};
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
|
||||
|
||||
final class SessionTerminatedEvent implements OWASPSecurityEvent
|
||||
{
|
||||
private MaskedEmail $maskedEmail;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $email,
|
||||
public readonly string $sessionId,
|
||||
public readonly string $reason = 'logout'
|
||||
) {
|
||||
$this->maskedEmail = MaskedEmail::fromString($this->email);
|
||||
}
|
||||
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier
|
||||
{
|
||||
return OWASPEventIdentifier::sessionTermination($this->maskedEmail->toString());
|
||||
}
|
||||
|
||||
public function getOWASPLogLevel(): OWASPLogLevel
|
||||
{
|
||||
return OWASPLogLevel::INFO;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "User {$this->maskedEmail->toString()} logged out";
|
||||
}
|
||||
|
||||
public function getEventData(): array
|
||||
{
|
||||
return [
|
||||
'username' => $this->maskedEmail->toString(),
|
||||
'session_id' => hash('sha256', $this->sessionId),
|
||||
'termination_reason' => $this->reason
|
||||
];
|
||||
}
|
||||
|
||||
public function getMaskedEmail(): MaskedEmail
|
||||
{
|
||||
return $this->maskedEmail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\Authorization;
|
||||
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel};
|
||||
|
||||
final class AccessDeniedEvent
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $email,
|
||||
public readonly string $resource,
|
||||
public readonly string $action,
|
||||
public readonly ?string $requiredPermission = null
|
||||
) {}
|
||||
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier
|
||||
{
|
||||
return OWASPEventIdentifier::authorizationFailure(
|
||||
$this->maskEmail($this->email),
|
||||
$this->resource
|
||||
);
|
||||
}
|
||||
|
||||
public function getOWASPLogLevel(): OWASPLogLevel
|
||||
{
|
||||
return OWASPLogLevel::WARN;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "Access denied for user {$this->maskEmail($this->email)} to resource {$this->resource}";
|
||||
}
|
||||
|
||||
public function getEventData(): array
|
||||
{
|
||||
return [
|
||||
'username' => $this->maskEmail($this->email),
|
||||
'resource' => $this->resource,
|
||||
'action' => $this->action,
|
||||
'required_permission' => $this->requiredPermission
|
||||
];
|
||||
}
|
||||
|
||||
private function maskEmail(string $email): string
|
||||
{
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return $email;
|
||||
}
|
||||
|
||||
[$local, $domain] = explode('@', $email, 2);
|
||||
$maskedLocal = substr($local, 0, 2) . str_repeat('*', max(0, strlen($local) - 2));
|
||||
|
||||
return $maskedLocal . '@' . $domain;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\Authorization;
|
||||
|
||||
use App\Application\Security\{OWASPSecurityEvent};
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
|
||||
|
||||
final class PrivilegeEscalationEvent implements OWASPSecurityEvent
|
||||
{
|
||||
private MaskedEmail $maskedEmail;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $email,
|
||||
public readonly string $fromRole,
|
||||
public readonly string $toRole,
|
||||
public readonly string $method,
|
||||
public readonly bool $successful = false
|
||||
) {
|
||||
$this->maskedEmail = MaskedEmail::fromString($this->email);
|
||||
}
|
||||
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier
|
||||
{
|
||||
return OWASPEventIdentifier::privilegeEscalation(
|
||||
$this->maskedEmail->toString(),
|
||||
$this->fromRole,
|
||||
$this->toRole
|
||||
);
|
||||
}
|
||||
|
||||
public function getOWASPLogLevel(): OWASPLogLevel
|
||||
{
|
||||
return OWASPLogLevel::FATAL;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
$status = $this->successful ? 'successful' : 'attempted';
|
||||
return "Privilege escalation {$status} by user {$this->maskedEmail->toString()}";
|
||||
}
|
||||
|
||||
public function getEventData(): array
|
||||
{
|
||||
return [
|
||||
'username' => $this->maskedEmail->toString(),
|
||||
'from_role' => $this->fromRole,
|
||||
'to_role' => $this->toRole,
|
||||
'escalation_method' => $this->method,
|
||||
'successful' => $this->successful
|
||||
];
|
||||
}
|
||||
|
||||
public function getMaskedEmail(): MaskedEmail
|
||||
{
|
||||
return $this->maskedEmail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\Crypto;
|
||||
|
||||
use App\Application\Security\{OWASPSecurityEvent};
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
|
||||
|
||||
final class CryptographicFailureEvent implements OWASPSecurityEvent
|
||||
{
|
||||
private ?MaskedEmail $maskedEmail;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $operation,
|
||||
public readonly string $algorithm,
|
||||
public readonly string $errorMessage,
|
||||
public readonly ?string $email = null
|
||||
) {
|
||||
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
|
||||
}
|
||||
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier
|
||||
{
|
||||
return OWASPEventIdentifier::cryptographicFailure($this->operation);
|
||||
}
|
||||
|
||||
public function getOWASPLogLevel(): OWASPLogLevel
|
||||
{
|
||||
return OWASPLogLevel::ERROR;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "Cryptographic failure in {$this->operation}";
|
||||
}
|
||||
|
||||
public function getEventData(): array
|
||||
{
|
||||
return [
|
||||
'operation' => $this->operation,
|
||||
'algorithm' => $this->algorithm,
|
||||
'error_message' => $this->errorMessage,
|
||||
'username' => $this->maskedEmail?->toString() ?? 'system'
|
||||
];
|
||||
}
|
||||
|
||||
public function getMaskedEmail(): ?MaskedEmail
|
||||
{
|
||||
return $this->maskedEmail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\File;
|
||||
|
||||
use App\Application\Security\{OWASPSecurityEvent};
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
|
||||
|
||||
final class SuspiciousFileUploadEvent implements OWASPSecurityEvent
|
||||
{
|
||||
private ?MaskedEmail $maskedEmail;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $filename,
|
||||
public readonly string $mimeType,
|
||||
public readonly int $fileSize,
|
||||
public readonly string $suspicionReason,
|
||||
public readonly ?string $email = null
|
||||
) {
|
||||
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
|
||||
}
|
||||
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier
|
||||
{
|
||||
return OWASPEventIdentifier::fileUploadFailure($this->filename);
|
||||
}
|
||||
|
||||
public function getOWASPLogLevel(): OWASPLogLevel
|
||||
{
|
||||
return OWASPLogLevel::ERROR;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "Suspicious file upload: {$this->filename}";
|
||||
}
|
||||
|
||||
public function getEventData(): array
|
||||
{
|
||||
return [
|
||||
'filename' => $this->sanitizeFilename($this->filename),
|
||||
'mime_type' => $this->mimeType,
|
||||
'file_size' => $this->fileSize,
|
||||
'suspicion_reason' => $this->suspicionReason,
|
||||
'username' => $this->maskedEmail?->toString() ?? 'anonymous'
|
||||
];
|
||||
}
|
||||
|
||||
public function getMaskedEmail(): ?MaskedEmail
|
||||
{
|
||||
return $this->maskedEmail;
|
||||
}
|
||||
|
||||
private function sanitizeFilename(string $filename): string
|
||||
{
|
||||
return preg_replace('/[^\w\.\-]/', '_', $filename);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\Input;
|
||||
|
||||
use App\Application\Security\{OWASPSecurityEvent};
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
|
||||
|
||||
final class InputValidationFailureEvent implements OWASPSecurityEvent
|
||||
{
|
||||
private ?MaskedEmail $maskedEmail;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $fieldName,
|
||||
public readonly string $invalidValue,
|
||||
public readonly string $validationRule,
|
||||
public readonly ?string $email = null
|
||||
) {
|
||||
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
|
||||
}
|
||||
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier
|
||||
{
|
||||
return OWASPEventIdentifier::inputValidationFailure($this->fieldName);
|
||||
}
|
||||
|
||||
public function getOWASPLogLevel(): OWASPLogLevel
|
||||
{
|
||||
return OWASPLogLevel::WARN;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "Input validation failure for field {$this->fieldName}";
|
||||
}
|
||||
|
||||
public function getEventData(): array
|
||||
{
|
||||
return [
|
||||
'field_name' => $this->fieldName,
|
||||
'invalid_value' => $this->sanitizeForLog($this->invalidValue),
|
||||
'validation_rule' => $this->validationRule,
|
||||
'username' => $this->maskedEmail?->toString() ?? 'anonymous'
|
||||
];
|
||||
}
|
||||
|
||||
public function getMaskedEmail(): ?MaskedEmail
|
||||
{
|
||||
return $this->maskedEmail;
|
||||
}
|
||||
|
||||
private function sanitizeForLog(string $value): string
|
||||
{
|
||||
// Maximal 100 Zeichen und gefährliche Zeichen entfernen
|
||||
$sanitized = substr($value, 0, 100);
|
||||
return preg_replace('/[^\w\s\.\-@]/', '***', $sanitized);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\Input;
|
||||
|
||||
use App\Application\Security\{OWASPSecurityEvent};
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
|
||||
|
||||
final class MaliciousInputDetectedEvent implements OWASPSecurityEvent
|
||||
{
|
||||
private ?MaskedEmail $maskedEmail;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $fieldName,
|
||||
public readonly string $attackPattern,
|
||||
public readonly string $sanitizedValue,
|
||||
public readonly ?string $email = null
|
||||
) {
|
||||
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
|
||||
}
|
||||
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier
|
||||
{
|
||||
return OWASPEventIdentifier::maliciousInput($this->attackPattern);
|
||||
}
|
||||
|
||||
public function getOWASPLogLevel(): OWASPLogLevel
|
||||
{
|
||||
return OWASPLogLevel::ERROR;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "Malicious input detected: {$this->attackPattern}";
|
||||
}
|
||||
|
||||
public function getEventData(): array
|
||||
{
|
||||
return [
|
||||
'field_name' => $this->fieldName,
|
||||
'attack_pattern' => $this->attackPattern,
|
||||
'sanitized_value' => $this->sanitizedValue,
|
||||
'username' => $this->maskedEmail?->toString() ?? 'anonymous'
|
||||
];
|
||||
}
|
||||
|
||||
public function getMaskedEmail(): ?MaskedEmail
|
||||
{
|
||||
return $this->maskedEmail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\Input;
|
||||
|
||||
use App\Application\Security\{OWASPSecurityEvent};
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
|
||||
|
||||
final class SqlInjectionAttemptEvent implements OWASPSecurityEvent
|
||||
{
|
||||
private ?MaskedEmail $maskedEmail;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $attackPayload,
|
||||
public readonly string $targetField,
|
||||
public readonly string $detectionMethod,
|
||||
public readonly ?string $email = null
|
||||
) {
|
||||
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
|
||||
}
|
||||
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier
|
||||
{
|
||||
return OWASPEventIdentifier::maliciousInput('sql_injection');
|
||||
}
|
||||
|
||||
public function getOWASPLogLevel(): OWASPLogLevel
|
||||
{
|
||||
return OWASPLogLevel::ERROR;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "SQL injection attempt detected";
|
||||
}
|
||||
|
||||
public function getEventData(): array
|
||||
{
|
||||
return [
|
||||
'attack_payload' => $this->sanitizePayload($this->attackPayload),
|
||||
'target_field' => $this->targetField,
|
||||
'detection_method' => $this->detectionMethod,
|
||||
'username' => $this->maskedEmail?->toString() ?? 'anonymous'
|
||||
];
|
||||
}
|
||||
|
||||
public function getMaskedEmail(): ?MaskedEmail
|
||||
{
|
||||
return $this->maskedEmail;
|
||||
}
|
||||
|
||||
private function sanitizePayload(string $payload): string
|
||||
{
|
||||
// SQL-Injection-Payload nur teilweise loggen für Analyse
|
||||
return substr(preg_replace('/[^\w\s\'\";=\-\(\)]/', '***', $payload), 0, 200);
|
||||
}
|
||||
}
|
||||
57
src/Application/Security/Events/Input/XssAttemptEvent.php
Normal file
57
src/Application/Security/Events/Input/XssAttemptEvent.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\Input;
|
||||
|
||||
use App\Application\Security\{OWASPSecurityEvent};
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
|
||||
|
||||
final class XssAttemptEvent implements OWASPSecurityEvent
|
||||
{
|
||||
private ?MaskedEmail $maskedEmail;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $attackPayload,
|
||||
public readonly string $targetField,
|
||||
public readonly string $xssType,
|
||||
public readonly ?string $email = null
|
||||
) {
|
||||
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
|
||||
}
|
||||
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier
|
||||
{
|
||||
return OWASPEventIdentifier::maliciousInput('xss_attempt');
|
||||
}
|
||||
|
||||
public function getOWASPLogLevel(): OWASPLogLevel
|
||||
{
|
||||
return OWASPLogLevel::ERROR;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "XSS attempt detected: {$this->xssType}";
|
||||
}
|
||||
|
||||
public function getEventData(): array
|
||||
{
|
||||
return [
|
||||
'attack_payload' => $this->sanitizePayload($this->attackPayload),
|
||||
'target_field' => $this->targetField,
|
||||
'xss_type' => $this->xssType,
|
||||
'username' => $this->maskedEmail?->toString() ?? 'anonymous'
|
||||
];
|
||||
}
|
||||
|
||||
public function getMaskedEmail(): ?MaskedEmail
|
||||
{
|
||||
return $this->maskedEmail;
|
||||
}
|
||||
|
||||
private function sanitizePayload(string $payload): string
|
||||
{
|
||||
// HTML-Tags entfernen aber Struktur beibehalten für Analyse
|
||||
return substr(htmlspecialchars($payload, ENT_QUOTES, 'UTF-8'), 0, 200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\Network;
|
||||
|
||||
use App\Application\Security\{OWASPSecurityEvent};
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
|
||||
|
||||
final class SuspiciousNetworkActivityEvent implements OWASPSecurityEvent
|
||||
{
|
||||
private ?MaskedEmail $maskedEmail;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $sourceIp,
|
||||
public readonly string $activityType,
|
||||
public readonly int $requestCount,
|
||||
public readonly string $timeWindow,
|
||||
public readonly ?string $email = null
|
||||
) {
|
||||
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
|
||||
}
|
||||
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier
|
||||
{
|
||||
return OWASPEventIdentifier::suspiciousNetworkActivity($this->activityType);
|
||||
}
|
||||
|
||||
public function getOWASPLogLevel(): OWASPLogLevel
|
||||
{
|
||||
return OWASPLogLevel::WARN;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "Suspicious network activity detected: {$this->activityType}";
|
||||
}
|
||||
|
||||
public function getEventData(): array
|
||||
{
|
||||
return [
|
||||
'source_ip' => $this->sourceIp,
|
||||
'activity_type' => $this->activityType,
|
||||
'request_count' => $this->requestCount,
|
||||
'time_window' => $this->timeWindow,
|
||||
'username' => $this->maskedEmail?->toString() ?? 'anonymous'
|
||||
];
|
||||
}
|
||||
|
||||
public function getMaskedEmail(): ?MaskedEmail
|
||||
{
|
||||
return $this->maskedEmail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\Session;
|
||||
|
||||
use App\Application\Security\{OWASPSecurityEvent};
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
|
||||
|
||||
final class SessionFixationEvent implements OWASPSecurityEvent
|
||||
{
|
||||
private ?MaskedEmail $maskedEmail;
|
||||
|
||||
public function __construct(
|
||||
public readonly ?string $email,
|
||||
public readonly string $oldSessionId,
|
||||
public readonly string $newSessionId,
|
||||
public readonly string $attackVector
|
||||
) {
|
||||
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
|
||||
}
|
||||
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier
|
||||
{
|
||||
return OWASPEventIdentifier::sessionFixation($this->maskedEmail?->toString() ?? 'anonymous');
|
||||
}
|
||||
|
||||
public function getOWASPLogLevel(): OWASPLogLevel
|
||||
{
|
||||
return OWASPLogLevel::FATAL;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "Session fixation attack detected for user {$this->maskedEmail?->toString() ?? 'anonymous'}";
|
||||
}
|
||||
|
||||
public function getEventData(): array
|
||||
{
|
||||
return [
|
||||
'username' => $this->maskedEmail?->toString() ?? 'anonymous',
|
||||
'old_session_id' => hash('sha256', $this->oldSessionId),
|
||||
'new_session_id' => hash('sha256', $this->newSessionId),
|
||||
'attack_vector' => $this->attackVector
|
||||
];
|
||||
}
|
||||
|
||||
public function getMaskedEmail(): ?MaskedEmail
|
||||
{
|
||||
return $this->maskedEmail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\Session;
|
||||
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel};
|
||||
|
||||
final class SessionHijackingDetectedEvent
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $email,
|
||||
public readonly string $sessionId,
|
||||
public readonly string $evidence,
|
||||
public readonly ?string $suspiciousIp = null
|
||||
) {}
|
||||
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier
|
||||
{
|
||||
return OWASPEventIdentifier::sessionHijacking($this->maskEmail($this->email));
|
||||
}
|
||||
|
||||
public function getOWASPLogLevel(): OWASPLogLevel
|
||||
{
|
||||
return OWASPLogLevel::FATAL;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "Session hijacking detected for user {$this->maskEmail($this->email)}";
|
||||
}
|
||||
|
||||
public function getEventData(): array
|
||||
{
|
||||
return [
|
||||
'username' => $this->maskEmail($this->email),
|
||||
'session_id' => hash('sha256', $this->sessionId),
|
||||
'evidence' => $this->evidence,
|
||||
'suspicious_ip' => $this->suspiciousIp
|
||||
];
|
||||
}
|
||||
|
||||
private function maskEmail(string $email): string
|
||||
{
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return $email;
|
||||
}
|
||||
|
||||
[$local, $domain] = explode('@', $email, 2);
|
||||
$maskedLocal = substr($local, 0, 2) . str_repeat('*', max(0, strlen($local) - 2));
|
||||
|
||||
return $maskedLocal . '@' . $domain;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\Session;
|
||||
|
||||
use App\Application\Security\{OWASPSecurityEvent};
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
|
||||
|
||||
final class SessionTimeoutEvent implements OWASPSecurityEvent
|
||||
{
|
||||
private ?MaskedEmail $maskedEmail;
|
||||
|
||||
public function __construct(
|
||||
public readonly ?string $email,
|
||||
public readonly string $sessionId,
|
||||
public readonly int $inactivityDuration,
|
||||
public readonly string $reason = 'timeout'
|
||||
) {
|
||||
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
|
||||
}
|
||||
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier
|
||||
{
|
||||
return OWASPEventIdentifier::sessionTimeout($this->maskedEmail?->toString() ?? 'anonymous');
|
||||
}
|
||||
|
||||
public function getOWASPLogLevel(): OWASPLogLevel
|
||||
{
|
||||
return OWASPLogLevel::WARN;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "Session timeout for user {$this->maskedEmail?->toString() ?? 'anonymous'}";
|
||||
}
|
||||
|
||||
public function getEventData(): array
|
||||
{
|
||||
return [
|
||||
'username' => $this->maskedEmail?->toString() ?? 'anonymous',
|
||||
'session_id' => hash('sha256', $this->sessionId),
|
||||
'inactivity_duration' => $this->inactivityDuration,
|
||||
'timeout_reason' => $this->reason
|
||||
];
|
||||
}
|
||||
|
||||
public function getMaskedEmail(): ?MaskedEmail
|
||||
{
|
||||
return $this->maskedEmail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\System;
|
||||
|
||||
use App\Application\Security\{OWASPSecurityEvent};
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
|
||||
|
||||
final class SystemAnomalyEvent implements OWASPSecurityEvent
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $anomalyType,
|
||||
public readonly string $description,
|
||||
public readonly array $metrics,
|
||||
public readonly string $severity
|
||||
) {}
|
||||
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier
|
||||
{
|
||||
return OWASPEventIdentifier::systemAnomaly($this->anomalyType);
|
||||
}
|
||||
|
||||
public function getOWASPLogLevel(): OWASPLogLevel
|
||||
{
|
||||
return match ($this->severity) {
|
||||
'low' => OWASPLogLevel::INFO,
|
||||
'medium' => OWASPLogLevel::WARN,
|
||||
'high' => OWASPLogLevel::ERROR,
|
||||
'critical' => OWASPLogLevel::FATAL,
|
||||
default => OWASPLogLevel::WARN
|
||||
};
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "System anomaly detected: {$this->anomalyType}";
|
||||
}
|
||||
|
||||
public function getEventData(): array
|
||||
{
|
||||
return [
|
||||
'anomaly_type' => $this->anomalyType,
|
||||
'description' => $this->description,
|
||||
'metrics' => $this->metrics,
|
||||
'severity' => $this->severity
|
||||
];
|
||||
}
|
||||
}
|
||||
53
src/Application/Security/Events/Web/CsrfViolationEvent.php
Normal file
53
src/Application/Security/Events/Web/CsrfViolationEvent.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Events\Web;
|
||||
|
||||
use App\Application\Security\{OWASPSecurityEvent};
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel, MaskedEmail};
|
||||
|
||||
final class CsrfViolationEvent implements OWASPSecurityEvent
|
||||
{
|
||||
private ?MaskedEmail $maskedEmail;
|
||||
|
||||
public function __construct(
|
||||
public readonly string $requestPath,
|
||||
public readonly string $method,
|
||||
public readonly ?string $expectedToken = null,
|
||||
public readonly ?string $providedToken = null,
|
||||
public readonly ?string $email = null
|
||||
) {
|
||||
$this->maskedEmail = $this->email ? MaskedEmail::fromString($this->email) : null;
|
||||
}
|
||||
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier
|
||||
{
|
||||
return OWASPEventIdentifier::csrfViolation($this->requestPath);
|
||||
}
|
||||
|
||||
public function getOWASPLogLevel(): OWASPLogLevel
|
||||
{
|
||||
return OWASPLogLevel::ERROR;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return "CSRF token validation failed for {$this->method} {$this->requestPath}";
|
||||
}
|
||||
|
||||
public function getEventData(): array
|
||||
{
|
||||
return [
|
||||
'request_path' => $this->requestPath,
|
||||
'method' => $this->method,
|
||||
'expected_token_hash' => $this->expectedToken ? hash('sha256', $this->expectedToken) : null,
|
||||
'provided_token_hash' => $this->providedToken ? hash('sha256', $this->providedToken) : null,
|
||||
'username' => $this->maskedEmail?->toString() ?? 'anonymous'
|
||||
];
|
||||
}
|
||||
|
||||
public function getMaskedEmail(): ?MaskedEmail
|
||||
{
|
||||
return $this->maskedEmail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\ExceptionHandlers;
|
||||
|
||||
use App\Application\Security\Events\{
|
||||
Authorization\AccessDeniedEvent,
|
||||
Input\InputValidationFailureEvent,
|
||||
System\SystemAnomalyEvent,
|
||||
Crypto\CryptographicFailureEvent
|
||||
};
|
||||
use App\Framework\Core\Events\EventDispatcher;
|
||||
use App\Framework\Core\Exceptions\{
|
||||
ValidationException,
|
||||
CryptographicException
|
||||
};
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Finder\Exception\AccessDeniedException;
|
||||
use Throwable;
|
||||
|
||||
final class SecurityExceptionHandler
|
||||
{
|
||||
public function __construct(
|
||||
private EventDispatcher $eventDispatcher,
|
||||
private LoggerInterface $logger
|
||||
) {}
|
||||
|
||||
public function handle(Throwable $exception): void
|
||||
{
|
||||
match (true) {
|
||||
$exception instanceof AccessDeniedException => $this->handleAccessDenied($exception),
|
||||
$exception instanceof ValidationException => $this->handleValidationError($exception),
|
||||
$exception instanceof CryptographicException => $this->handleCryptographicError($exception),
|
||||
$exception instanceof \Error => $this->handleSystemError($exception),
|
||||
default => $this->handleGenericSecurityIssue($exception)
|
||||
};
|
||||
}
|
||||
|
||||
private function handleAccessDenied(AccessDeniedException $exception): void
|
||||
{
|
||||
$this->eventDispatcher->dispatch(new AccessDeniedEvent(
|
||||
email: $this->getCurrentUserEmail(),
|
||||
resource: $exception->getResource(),
|
||||
action: $exception->getAction(),
|
||||
requiredPermission: $exception->getRequiredPermission()
|
||||
));
|
||||
}
|
||||
|
||||
private function handleValidationError(ValidationException $exception): void
|
||||
{
|
||||
foreach ($exception->getErrors() as $field => $errors) {
|
||||
$this->eventDispatcher->dispatch(new InputValidationFailureEvent(
|
||||
fieldName: $field,
|
||||
invalidValue: $exception->getInvalidValue($field) ?? '',
|
||||
validationRule: implode(', ', $errors),
|
||||
email: $this->getCurrentUserEmail()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private function handleCryptographicError(CryptographicException $exception): void
|
||||
{
|
||||
$this->eventDispatcher->dispatch(new CryptographicFailureEvent(
|
||||
operation: $exception->getOperation(),
|
||||
algorithm: $exception->getAlgorithm(),
|
||||
errorMessage: $exception->getMessage(),
|
||||
email: $this->getCurrentUserEmail()
|
||||
));
|
||||
}
|
||||
|
||||
private function handleSystemError(\Error $exception): void
|
||||
{
|
||||
$this->eventDispatcher->dispatch(new SystemAnomalyEvent(
|
||||
anomalyType: 'system_error',
|
||||
description: $exception->getMessage(),
|
||||
metrics: [
|
||||
'error_type' => get_class($exception),
|
||||
'file' => $exception->getFile(),
|
||||
'line' => $exception->getLine(),
|
||||
'memory_usage' => memory_get_usage(true)
|
||||
],
|
||||
severity: 'high'
|
||||
));
|
||||
}
|
||||
|
||||
private function handleGenericSecurityIssue(Throwable $exception): void
|
||||
{
|
||||
// Nur bei tatsächlich sicherheitsrelevanten Exceptions loggen
|
||||
if ($this->isSecurityRelevant($exception)) {
|
||||
$this->logger->warning('Security-relevant exception detected', [
|
||||
'exception_class' => get_class($exception),
|
||||
'message' => $exception->getMessage(),
|
||||
'user_email' => $this->getCurrentUserEmail(),
|
||||
'security_context' => [
|
||||
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null,
|
||||
'request_uri' => $_SERVER['REQUEST_URI'] ?? null
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function isSecurityRelevant(Throwable $exception): bool
|
||||
{
|
||||
$securityRelevantClasses = [
|
||||
'AccessDeniedException',
|
||||
'AuthenticationException',
|
||||
'AuthorizationException',
|
||||
'ValidationException',
|
||||
'CryptographicException',
|
||||
'FileUploadException',
|
||||
'SessionException'
|
||||
];
|
||||
|
||||
$className = get_class($exception);
|
||||
foreach ($securityRelevantClasses as $securityClass) {
|
||||
if (str_contains($className, $securityClass)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Sicherheitsrelevante Nachrichten prüfen
|
||||
$message = strtolower($exception->getMessage());
|
||||
$securityKeywords = [
|
||||
'access denied', 'unauthorized', 'permission', 'authentication',
|
||||
'authorization', 'csrf', 'xss', 'injection', 'validation failed'
|
||||
];
|
||||
|
||||
foreach ($securityKeywords as $keyword) {
|
||||
if (str_contains($message, $keyword)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getCurrentUserEmail(): ?string
|
||||
{
|
||||
return $_SESSION['user_email'] ?? null;
|
||||
}
|
||||
}
|
||||
129
src/Application/Security/Guards/AuthenticationGuard.php
Normal file
129
src/Application/Security/Guards/AuthenticationGuard.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Guards;
|
||||
|
||||
use App\Framework\Database\Example\UserRepository;
|
||||
use App\Application\Security\Events\Auth\{
|
||||
AuthenticationSuccessEvent,
|
||||
AuthenticationFailedEvent,
|
||||
AccountLockedEvent
|
||||
};
|
||||
use App\Framework\Core\Events\EventDispatcher;
|
||||
use App\Domain\User\{User};
|
||||
|
||||
final class AuthenticationGuard
|
||||
{
|
||||
private const MAX_FAILED_ATTEMPTS = 5;
|
||||
private const LOCKOUT_DURATION = 900; // 15 Minuten
|
||||
|
||||
public function __construct(
|
||||
private EventDispatcher $eventDispatcher,
|
||||
private UserRepository $userRepository
|
||||
) {}
|
||||
|
||||
public function authenticate(string $email, string $password): ?User
|
||||
{
|
||||
try {
|
||||
$user = $this->userRepository->findByEmail($email);
|
||||
|
||||
if (!$user) {
|
||||
$this->dispatchFailedAttempt($email, 'user_not_found');
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->isAccountLocked($user)) {
|
||||
$this->dispatchAccountLocked($email, 'too_many_failed_attempts', $user->failed_attempts);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$this->verifyPassword($password, $user->password_hash)) {
|
||||
$this->handleFailedAttempt($user);
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->handleSuccessfulLogin($user);
|
||||
return $user;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->dispatchFailedAttempt($email, 'authentication_error');
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function logout(User $user): void
|
||||
{
|
||||
$sessionId = session_id();
|
||||
|
||||
// Session invalidieren
|
||||
session_destroy();
|
||||
|
||||
$this->eventDispatcher->dispatch(new \App\Application\Security\Events\Auth\SessionTerminatedEvent(
|
||||
email: $user->email,
|
||||
sessionId: $sessionId,
|
||||
reason: 'manual_logout'
|
||||
));
|
||||
}
|
||||
|
||||
private function handleSuccessfulLogin(User $user): void
|
||||
{
|
||||
// Failed attempts zurücksetzen
|
||||
$user->failed_attempts = 0;
|
||||
$user->last_login = new \DateTimeImmutable();
|
||||
$this->userRepository->save($user);
|
||||
|
||||
// Session regenerieren
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['user_id'] = $user->id;
|
||||
|
||||
$this->eventDispatcher->dispatch(new AuthenticationSuccessEvent(
|
||||
email: $user->email,
|
||||
sessionId: session_id(),
|
||||
method: 'password'
|
||||
));
|
||||
}
|
||||
|
||||
private function handleFailedAttempt(User $user): void
|
||||
{
|
||||
$user->failed_attempts++;
|
||||
$user->last_failed_attempt = new \DateTimeImmutable();
|
||||
|
||||
if ($user->failed_attempts >= self::MAX_FAILED_ATTEMPTS) {
|
||||
$user->locked_until = new \DateTimeImmutable('+' . self::LOCKOUT_DURATION . ' seconds');
|
||||
$this->userRepository->save($user);
|
||||
|
||||
$this->dispatchAccountLocked($user->email, 'max_attempts_exceeded', $user->failed_attempts);
|
||||
} else {
|
||||
$this->userRepository->save($user);
|
||||
$this->dispatchFailedAttempt($user->email, 'invalid_password', $user->failed_attempts);
|
||||
}
|
||||
}
|
||||
|
||||
private function dispatchFailedAttempt(string $email, string $reason, int $attempts = 1): void
|
||||
{
|
||||
$this->eventDispatcher->dispatch(new AuthenticationFailedEvent(
|
||||
email: $email,
|
||||
reason: $reason,
|
||||
failedAttempts: $attempts
|
||||
));
|
||||
}
|
||||
|
||||
private function dispatchAccountLocked(string $email, string $reason, int $attempts): void
|
||||
{
|
||||
$this->eventDispatcher->dispatch(new AccountLockedEvent(
|
||||
email: $email,
|
||||
reason: $reason,
|
||||
failedAttempts: $attempts
|
||||
));
|
||||
}
|
||||
|
||||
private function isAccountLocked(User $user): bool
|
||||
{
|
||||
return $user->locked_until && $user->locked_until > new \DateTimeImmutable();
|
||||
}
|
||||
|
||||
private function verifyPassword(string $password, string $hash): bool
|
||||
{
|
||||
return password_verify($password, $hash);
|
||||
}
|
||||
}
|
||||
106
src/Application/Security/Middleware/SecurityEventMiddleware.php
Normal file
106
src/Application/Security/Middleware/SecurityEventMiddleware.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Middleware;
|
||||
|
||||
use App\Application\Security\Events\{
|
||||
Web\CsrfViolationEvent,
|
||||
Network\SuspiciousNetworkActivityEvent,
|
||||
Input\InputValidationFailureEvent
|
||||
};
|
||||
use App\Framework\Core\Events\EventDispatcher;
|
||||
use Psr\Http\Message\{ServerRequestInterface, ResponseInterface};
|
||||
use Psr\Http\Server\{MiddlewareInterface, RequestHandlerInterface};
|
||||
|
||||
final class SecurityEventMiddleware implements MiddlewareInterface
|
||||
{
|
||||
private array $requestCounts = [];
|
||||
private const RATE_LIMIT_THRESHOLD = 100;
|
||||
private const TIME_WINDOW = 300; // 5 Minuten
|
||||
|
||||
public function __construct(
|
||||
private EventDispatcher $eventDispatcher
|
||||
) {}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$this->checkRateLimit($request);
|
||||
$this->validateCsrfToken($request);
|
||||
|
||||
$response = $handler->handle($request);
|
||||
|
||||
$this->analyzeResponse($request, $response);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function checkRateLimit(ServerRequestInterface $request): void
|
||||
{
|
||||
$clientIp = $this->getClientIp($request);
|
||||
$currentTime = time();
|
||||
|
||||
// Rate Limiting Check
|
||||
if (!isset($this->requestCounts[$clientIp])) {
|
||||
$this->requestCounts[$clientIp] = [];
|
||||
}
|
||||
|
||||
// Alte Einträge entfernen
|
||||
$this->requestCounts[$clientIp] = array_filter(
|
||||
$this->requestCounts[$clientIp],
|
||||
fn($timestamp) => $currentTime - $timestamp < self::TIME_WINDOW
|
||||
);
|
||||
|
||||
$this->requestCounts[$clientIp][] = $currentTime;
|
||||
|
||||
if (count($this->requestCounts[$clientIp]) > self::RATE_LIMIT_THRESHOLD) {
|
||||
$this->eventDispatcher->dispatch(new SuspiciousNetworkActivityEvent(
|
||||
sourceIp: $clientIp,
|
||||
activityType: 'rate_limit_exceeded',
|
||||
requestCount: count($this->requestCounts[$clientIp]),
|
||||
timeWindow: self::TIME_WINDOW . 's'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private function validateCsrfToken(ServerRequestInterface $request): void
|
||||
{
|
||||
$method = $request->getMethod();
|
||||
if (!in_array($method, ['POST', 'PUT', 'DELETE', 'PATCH'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sessionToken = $_SESSION['csrf_token'] ?? null;
|
||||
$requestToken = $request->getParsedBody()['csrf_token'] ??
|
||||
$request->getHeaderLine('X-CSRF-Token');
|
||||
|
||||
if (!$sessionToken || !$requestToken || !hash_equals($sessionToken, $requestToken)) {
|
||||
$this->eventDispatcher->dispatch(new CsrfViolationEvent(
|
||||
requestPath: $request->getUri()->getPath(),
|
||||
method: $method,
|
||||
expectedToken: $sessionToken,
|
||||
providedToken: $requestToken
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private function analyzeResponse(ServerRequestInterface $request, ResponseInterface $response): void
|
||||
{
|
||||
// Suspicious response patterns
|
||||
if ($response->getStatusCode() === 403) {
|
||||
// Access denied - könnte bereits von Authorization Middleware gehandhabt werden
|
||||
}
|
||||
|
||||
if ($response->getStatusCode() >= 500) {
|
||||
// Server errors - könnte auf Angriffe hindeuten
|
||||
}
|
||||
}
|
||||
|
||||
private function getClientIp(ServerRequestInterface $request): string
|
||||
{
|
||||
$serverParams = $request->getServerParams();
|
||||
return $serverParams['HTTP_X_FORWARDED_FOR'] ??
|
||||
$serverParams['HTTP_X_REAL_IP'] ??
|
||||
$serverParams['REMOTE_ADDR'] ??
|
||||
'unknown';
|
||||
}
|
||||
}
|
||||
29
src/Application/Security/OWASPSecurityEvent.php
Normal file
29
src/Application/Security/OWASPSecurityEvent.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security;
|
||||
|
||||
use App\Application\Security\ValueObjects\{OWASPEventIdentifier, OWASPLogLevel};
|
||||
|
||||
interface OWASPSecurityEvent
|
||||
{
|
||||
/**
|
||||
* Gibt den OWASP-konformen Event-Identifier zurück
|
||||
*/
|
||||
public function getOWASPEventIdentifier(): OWASPEventIdentifier;
|
||||
|
||||
/**
|
||||
* Gibt das entsprechende Log-Level zurück
|
||||
*/
|
||||
public function getOWASPLogLevel(): OWASPLogLevel;
|
||||
|
||||
/**
|
||||
* Gibt eine menschenlesbare Beschreibung des Events zurück
|
||||
*/
|
||||
public function getDescription(): string;
|
||||
|
||||
/**
|
||||
* Gibt strukturierte Event-Daten für das Logging zurück
|
||||
*/
|
||||
public function getEventData(): array;
|
||||
}
|
||||
210
src/Application/Security/OWASPSecurityEventFactory.php
Normal file
210
src/Application/Security/OWASPSecurityEventFactory.php
Normal file
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security;
|
||||
|
||||
use App\Application\Security\ValueObjects\{
|
||||
SecurityContext,
|
||||
RequestContext,
|
||||
OWASPLogFormat,
|
||||
OWASPEventIdentifier,
|
||||
OWASPLogLevel
|
||||
};
|
||||
|
||||
final class OWASPSecurityEventFactory
|
||||
{
|
||||
public function __construct(
|
||||
private string $applicationId = 'app.security'
|
||||
) {}
|
||||
|
||||
public function createFromSecurityEvent(
|
||||
SecurityEvent $event,
|
||||
SecurityContext $context,
|
||||
RequestContext $requestContext
|
||||
): OWASPLogFormat {
|
||||
$eventIdentifier = $this->createEventIdentifier($event);
|
||||
$logLevel = OWASPLogLevel::fromSecurityEventType($event->type);
|
||||
$description = $this->createDescription($event, $eventIdentifier);
|
||||
|
||||
return OWASPLogFormat::create(
|
||||
$this->applicationId,
|
||||
$eventIdentifier->toString(),
|
||||
$logLevel->value,
|
||||
$description,
|
||||
$context,
|
||||
$requestContext
|
||||
);
|
||||
}
|
||||
|
||||
private function createEventIdentifier(SecurityEvent $event): OWASPEventIdentifier
|
||||
{
|
||||
return match ($event->type) {
|
||||
SecurityEventType::LOGIN_FAILED => $this->createLoginFailedIdentifier($event),
|
||||
SecurityEventType::LOGIN_SUCCESS => $this->createLoginSuccessIdentifier($event),
|
||||
SecurityEventType::LOGOUT => $this->createLogoutIdentifier($event),
|
||||
SecurityEventType::PASSWORD_CHANGE => $this->createPasswordChangeIdentifier($event),
|
||||
SecurityEventType::ACCOUNT_LOCKED => $this->createAccountLockedIdentifier($event),
|
||||
SecurityEventType::ACCESS_DENIED => $this->createAccessDeniedIdentifier($event),
|
||||
SecurityEventType::PRIVILEGE_ESCALATION => $this->createPrivilegeEscalationIdentifier($event),
|
||||
SecurityEventType::DATA_ACCESS => $this->createDataAccessIdentifier($event),
|
||||
SecurityEventType::INJECTION_ATTEMPT => $this->createInjectionAttemptIdentifier($event),
|
||||
SecurityEventType::FILE_UPLOAD => $this->createFileUploadIdentifier($event),
|
||||
SecurityEventType::SESSION_HIJACK => $this->createSessionHijackIdentifier($event),
|
||||
SecurityEventType::SESSION_TIMEOUT => $this->createSessionTimeoutIdentifier($event),
|
||||
SecurityEventType::MALWARE_DETECTED => $this->createMalwareDetectedIdentifier($event),
|
||||
SecurityEventType::AUDIT_FAILURE => $this->createAuditFailureIdentifier($event),
|
||||
};
|
||||
}
|
||||
|
||||
private function createLoginFailedIdentifier(SecurityEvent $event): OWASPEventIdentifier
|
||||
{
|
||||
$username = $this->extractUsername($event);
|
||||
return OWASPEventIdentifier::authenticationFailure($username);
|
||||
}
|
||||
|
||||
private function createLoginSuccessIdentifier(SecurityEvent $event): OWASPEventIdentifier
|
||||
{
|
||||
$username = $this->extractUsername($event);
|
||||
return OWASPEventIdentifier::authenticationSuccess($username);
|
||||
}
|
||||
|
||||
private function createLogoutIdentifier(SecurityEvent $event): OWASPEventIdentifier
|
||||
{
|
||||
$username = $this->extractUsername($event);
|
||||
return OWASPEventIdentifier::sessionTermination($username);
|
||||
}
|
||||
|
||||
private function createPasswordChangeIdentifier(SecurityEvent $event): OWASPEventIdentifier
|
||||
{
|
||||
$username = $this->extractUsername($event);
|
||||
return OWASPEventIdentifier::passwordChange($username);
|
||||
}
|
||||
|
||||
private function createAccountLockedIdentifier(SecurityEvent $event): OWASPEventIdentifier
|
||||
{
|
||||
$username = $this->extractUsername($event);
|
||||
return OWASPEventIdentifier::accountLocked($username);
|
||||
}
|
||||
|
||||
private function createAccessDeniedIdentifier(SecurityEvent $event): OWASPEventIdentifier
|
||||
{
|
||||
$username = $this->extractUsername($event);
|
||||
$resource = $this->extractProperty($event, 'resource', 'unknown_resource');
|
||||
return OWASPEventIdentifier::authorizationFailure($username, $resource);
|
||||
}
|
||||
|
||||
private function createPrivilegeEscalationIdentifier(SecurityEvent $event): OWASPEventIdentifier
|
||||
{
|
||||
$username = $this->extractUsername($event);
|
||||
$fromRole = $this->extractProperty($event, 'fromRole', 'user');
|
||||
$toRole = $this->extractProperty($event, 'toRole', 'admin');
|
||||
return OWASPEventIdentifier::privilegeEscalation($username, $fromRole, $toRole);
|
||||
}
|
||||
|
||||
private function createDataAccessIdentifier(SecurityEvent $event): OWASPEventIdentifier
|
||||
{
|
||||
$field = $this->extractProperty($event, 'field', 'unknown');
|
||||
return OWASPEventIdentifier::inputValidationFailure($field);
|
||||
}
|
||||
|
||||
private function createInjectionAttemptIdentifier(SecurityEvent $event): OWASPEventIdentifier
|
||||
{
|
||||
$attackType = $this->extractProperty($event, 'attackType', 'injection');
|
||||
return OWASPEventIdentifier::maliciousInput($attackType);
|
||||
}
|
||||
|
||||
private function createFileUploadIdentifier(SecurityEvent $event): OWASPEventIdentifier
|
||||
{
|
||||
$filename = $this->extractProperty($event, 'filename', 'unknown.file');
|
||||
return OWASPEventIdentifier::fileUploadFailure($filename);
|
||||
}
|
||||
|
||||
private function createSessionHijackIdentifier(SecurityEvent $event): OWASPEventIdentifier
|
||||
{
|
||||
$username = $this->extractUsername($event);
|
||||
return OWASPEventIdentifier::sessionHijacking($username);
|
||||
}
|
||||
|
||||
private function createSessionTimeoutIdentifier(SecurityEvent $event): OWASPEventIdentifier
|
||||
{
|
||||
$username = $this->extractUsername($event);
|
||||
return OWASPEventIdentifier::sessionTimeout($username);
|
||||
}
|
||||
|
||||
private function createMalwareDetectedIdentifier(SecurityEvent $event): OWASPEventIdentifier
|
||||
{
|
||||
$malwareType = $this->extractProperty($event, 'malwareType', 'unknown');
|
||||
return OWASPEventIdentifier::malwareDetected($malwareType);
|
||||
}
|
||||
|
||||
private function createAuditFailureIdentifier(SecurityEvent $event): OWASPEventIdentifier
|
||||
{
|
||||
$eventType = $this->extractProperty($event, 'eventType', $event->type->value);
|
||||
return OWASPEventIdentifier::auditFailure($eventType);
|
||||
}
|
||||
|
||||
private function createDescription(SecurityEvent $event, OWASPEventIdentifier $identifier): string
|
||||
{
|
||||
return match ($event->type) {
|
||||
SecurityEventType::LOGIN_FAILED => "User {$this->extractUsername($event)} login failed",
|
||||
SecurityEventType::LOGIN_SUCCESS => "User {$this->extractUsername($event)} login successfully",
|
||||
SecurityEventType::LOGOUT => "User {$this->extractUsername($event)} logged out",
|
||||
SecurityEventType::PASSWORD_CHANGE => "User {$this->extractUsername($event)} changed password",
|
||||
SecurityEventType::ACCOUNT_LOCKED => "Account {$this->extractUsername($event)} locked",
|
||||
SecurityEventType::ACCESS_DENIED => "Access denied for user {$this->extractUsername($event)} to resource {$this->extractProperty($event, 'resource', 'unknown_resource')}",
|
||||
SecurityEventType::PRIVILEGE_ESCALATION => "Privilege escalation attempt by user {$this->extractUsername($event)}",
|
||||
SecurityEventType::DATA_ACCESS => "Input validation failure for field {$this->extractProperty($event, 'field', 'unknown')}",
|
||||
SecurityEventType::INJECTION_ATTEMPT => "Malicious input detected: {$this->extractProperty($event, 'attackType', 'injection')}",
|
||||
SecurityEventType::FILE_UPLOAD => "File upload failed: {$this->extractProperty($event, 'filename', 'unknown.file')}",
|
||||
SecurityEventType::SESSION_HIJACK => "Session hijacking detected for user {$this->extractUsername($event)}",
|
||||
SecurityEventType::SESSION_TIMEOUT => "Session timeout for user {$this->extractUsername($event)}",
|
||||
SecurityEventType::MALWARE_DETECTED => "Malware detected: {$this->extractProperty($event, 'malwareType', 'unknown')}",
|
||||
SecurityEventType::AUDIT_FAILURE => "Audit logging failure for event {$event->type->value}",
|
||||
};
|
||||
}
|
||||
|
||||
private function extractUsername(SecurityEvent $event): string
|
||||
{
|
||||
$usernameFields = ['username', 'email', 'user', 'userId'];
|
||||
|
||||
foreach ($usernameFields as $field) {
|
||||
$value = $this->extractProperty($event, $field);
|
||||
if ($value !== null) {
|
||||
return $this->maskEmail($value);
|
||||
}
|
||||
}
|
||||
|
||||
return 'anonymous';
|
||||
}
|
||||
|
||||
private function extractProperty(SecurityEvent $event, string $propertyName, ?string $default = null): ?string
|
||||
{
|
||||
try {
|
||||
$reflection = new \ReflectionObject($event);
|
||||
|
||||
if ($reflection->hasProperty($propertyName)) {
|
||||
$property = $reflection->getProperty($propertyName);
|
||||
$property->setAccessible(true);
|
||||
$value = $property->getValue($event);
|
||||
|
||||
return is_string($value) ? $value : (string)$value;
|
||||
}
|
||||
} catch (\Exception) {
|
||||
// Property nicht gefunden oder nicht lesbar
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
private function maskEmail(string $value): string
|
||||
{
|
||||
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
[$local, $domain] = explode('@', $value, 2);
|
||||
$maskedLocal = substr($local, 0, 2) . str_repeat('*', max(0, strlen($local) - 2));
|
||||
|
||||
return $maskedLocal . '@' . $domain;
|
||||
}
|
||||
}
|
||||
89
src/Application/Security/OWASPSecurityEventLogger.php
Normal file
89
src/Application/Security/OWASPSecurityEventLogger.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security;
|
||||
|
||||
use App\Application\Security\ValueObjects\{
|
||||
SecurityContext,
|
||||
RequestContext,
|
||||
OWASPLogFormat,
|
||||
OWASPLogLevel
|
||||
};
|
||||
use App\Framework\Core\Events\OnEvent;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
|
||||
final class OWASPSecurityEventLogger
|
||||
{
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private string $applicationId = 'app.security'
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Universeller Event-Handler für alle OWASP Security Events
|
||||
*/
|
||||
#[OnEvent]
|
||||
public function logSecurityEvent(OWASPSecurityEvent $event): void
|
||||
{
|
||||
try {
|
||||
$securityContext = SecurityContext::fromGlobals();
|
||||
$requestContext = RequestContext::fromGlobals();
|
||||
|
||||
$owaspLogFormat = $this->createOWASPLogFormat(
|
||||
$event,
|
||||
$securityContext,
|
||||
$requestContext
|
||||
);
|
||||
|
||||
// OWASP-konformes JSON-Log
|
||||
$this->logger->log(
|
||||
$this->mapLogLevel($event->getOWASPLogLevel()),
|
||||
$event->getDescription(),
|
||||
[
|
||||
'owasp_format' => $owaspLogFormat->toArray(),
|
||||
'event_class' => $event::class,
|
||||
'event_data' => $event->getEventData()
|
||||
]
|
||||
);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
// Fallback logging - niemals Security-Events verlieren
|
||||
$this->logger->critical('AUDIT_audit_failure:owasp_logger', [
|
||||
'datetime' => date('c'),
|
||||
'appid' => $this->applicationId,
|
||||
'event' => 'AUDIT_audit_failure:owasp_logger',
|
||||
'level' => 'FATAL',
|
||||
'description' => 'OWASP security event logging failed: ' . $e->getMessage(),
|
||||
'original_event_class' => $event::class,
|
||||
'error_trace' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function createOWASPLogFormat(
|
||||
OWASPSecurityEvent $event,
|
||||
SecurityContext $securityContext,
|
||||
RequestContext $requestContext
|
||||
): OWASPLogFormat {
|
||||
return OWASPLogFormat::create(
|
||||
$this->applicationId,
|
||||
$event->getOWASPEventIdentifier()->toString(),
|
||||
$event->getOWASPLogLevel()->value,
|
||||
$event->getDescription(),
|
||||
$securityContext,
|
||||
$requestContext
|
||||
);
|
||||
}
|
||||
|
||||
private function mapLogLevel(OWASPLogLevel $owaspLevel): string
|
||||
{
|
||||
return match ($owaspLevel) {
|
||||
OWASPLogLevel::DEBUG => 'debug',
|
||||
OWASPLogLevel::INFO => 'info',
|
||||
OWASPLogLevel::WARN => 'warning',
|
||||
OWASPLogLevel::ERROR => 'error',
|
||||
OWASPLogLevel::FATAL => 'critical'
|
||||
};
|
||||
}
|
||||
}
|
||||
53
src/Application/Security/SecurityContext.php
Normal file
53
src/Application/Security/SecurityContext.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security;
|
||||
|
||||
/**
|
||||
* Beispiel für Logeintrag:
|
||||
* {
|
||||
* 'timestamp': '2025-06-01T00:52:10Z',
|
||||
* 'event': 'auth.login.failed',
|
||||
* 'ip': '203.0.113.42',
|
||||
* 'user_agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
|
||||
* 'request_uri': '/login',
|
||||
* 'method': 'POST',
|
||||
* 'user_id': null,
|
||||
* 'email': 'j***@example.com',
|
||||
* 'session_id': 'abc123',
|
||||
* 'referrer': 'https://example.com/login',
|
||||
* 'result': 'fail',
|
||||
* 'request_id': 'req-3d92fcb45e'
|
||||
* }
|
||||
*
|
||||
*/
|
||||
|
||||
final readonly class SecurityContext
|
||||
{
|
||||
public function __construct(
|
||||
public string $ip,
|
||||
public ?string $userAgent,
|
||||
public string $method,
|
||||
public string $uri,
|
||||
public \DateTimeImmutable $timestamp,
|
||||
public ?string $userId = null,
|
||||
public ?string $sessionId = null,
|
||||
public ?string $referrer = null,
|
||||
public ?string $requestId = null
|
||||
) {}
|
||||
|
||||
public static function fromGlobals(): self
|
||||
{
|
||||
return new self(
|
||||
ip: $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
userAgent: $_SERVER['HTTP_USER_AGENT'] ?? null,
|
||||
method: $_SERVER['REQUEST_METHOD'] ?? 'CLI',
|
||||
uri: $_SERVER['REQUEST_URI'] ?? '-',
|
||||
timestamp: new \DateTimeImmutable(),
|
||||
userId: $_SESSION['user_id'] ?? null,
|
||||
sessionId: session_id() ?: null,
|
||||
referrer: $_SERVER['HTTP_REFERER'] ?? null,
|
||||
requestId: $_SERVER['HTTP_X_REQUEST_ID'] ?? bin2hex(random_bytes(8))
|
||||
);
|
||||
}
|
||||
}
|
||||
10
src/Application/Security/SecurityEvent.php
Normal file
10
src/Application/Security/SecurityEvent.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Security;
|
||||
|
||||
interface SecurityEvent
|
||||
{
|
||||
public SecurityEventType $type {
|
||||
get;
|
||||
}
|
||||
}
|
||||
84
src/Application/Security/SecurityEventLogger.php
Normal file
84
src/Application/Security/SecurityEventLogger.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security;
|
||||
|
||||
use App\Application\Security\ValueObjects\{SecurityContext as SecurityContextVO, RequestContext};
|
||||
use App\Framework\Core\Events\OnEvent;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
|
||||
final class SecurityEventLogger
|
||||
{
|
||||
public function __construct(
|
||||
private LoggerInterface $logger,
|
||||
private ?OWASPSecurityEventFactory $eventFactory = null
|
||||
) {
|
||||
$this->eventFactory ??= new OWASPSecurityEventFactory();
|
||||
}
|
||||
|
||||
#[OnEvent]
|
||||
public function log(SecurityEvent $event): void
|
||||
{
|
||||
try {
|
||||
// OWASP-konforme Logging
|
||||
$this->logOWASPFormat($event);
|
||||
|
||||
// Fallback: Ursprüngliches Format beibehalten
|
||||
$this->logLegacyFormat($event);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
// Fallback logging - niemals Security-Events verlieren
|
||||
$this->logger->critical('AUDIT_audit_failure:security_logger', [
|
||||
'datetime' => date('c'),
|
||||
'appid' => 'app.security',
|
||||
'event' => 'AUDIT_audit_failure:security_logger',
|
||||
'level' => 'FATAL',
|
||||
'description' => 'Security event logging failed: ' . $e->getMessage(),
|
||||
'original_event_type' => $event->type->value ?? 'UNKNOWN',
|
||||
'error_trace' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function logOWASPFormat(SecurityEvent $event): void
|
||||
{
|
||||
$securityContext = SecurityContextVO::fromGlobals();
|
||||
$requestContext = RequestContext::fromGlobals();
|
||||
|
||||
$owaspLogFormat = $this->eventFactory->createFromSecurityEvent(
|
||||
$event,
|
||||
$securityContext,
|
||||
$requestContext
|
||||
);
|
||||
|
||||
// Als strukturiertes JSON loggen
|
||||
$this->logger->info($owaspLogFormat->getDescription(), [
|
||||
'owasp_format' => $owaspLogFormat->toArray()
|
||||
]);
|
||||
}
|
||||
|
||||
private function logLegacyFormat(SecurityEvent $event): void
|
||||
{
|
||||
$context = SecurityContext::fromGlobals();
|
||||
$payload = $this->extractPayload($event);
|
||||
|
||||
$this->logger->warning($event->type->value, [
|
||||
...$payload,
|
||||
'ip' => $context->ip,
|
||||
'user_agent' => $context->userAgent,
|
||||
'timestamp' => $context->timestamp->format('c'),
|
||||
]);
|
||||
}
|
||||
|
||||
private function extractPayload(SecurityEvent $event): array
|
||||
{
|
||||
$data = [];
|
||||
$ref = new \ReflectionObject($event);
|
||||
foreach ($ref->getProperties() as $prop) {
|
||||
$prop->setAccessible(true);
|
||||
$data[$prop->getName()] = $prop->getValue($event);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
20
src/Application/Security/SecurityEventType.php
Normal file
20
src/Application/Security/SecurityEventType.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Application\Security;
|
||||
|
||||
enum SecurityEventType: string
|
||||
{
|
||||
case LOGIN_FAILED = 'auth.login.failed';
|
||||
case LOGIN_SUCCEEDED = 'auth.login.succeeded';
|
||||
case LOGOUT = 'auth.logout';
|
||||
|
||||
case PASSWORD_CHANGED = 'account.password.changed';
|
||||
case EMAIL_CHANGED = 'account.email.changed';
|
||||
case USER_DELETED = 'account.deleted';
|
||||
|
||||
case ACCESS_DENIED = 'access.denied';
|
||||
case CSRF_VIOLATION = 'access.csrf.violation';
|
||||
|
||||
case ADMIN_ACTION = 'system.admin.action';
|
||||
case CONFIG_CHANGED = 'system.config.changed';
|
||||
}
|
||||
125
src/Application/Security/Services/FileUploadSecurityService.php
Normal file
125
src/Application/Security/Services/FileUploadSecurityService.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Services;
|
||||
|
||||
use App\Application\Security\Events\File\SuspiciousFileUploadEvent;
|
||||
use App\Framework\Core\Events\EventDispatcher;
|
||||
|
||||
final class FileUploadSecurityService
|
||||
{
|
||||
private const ALLOWED_MIME_TYPES = [
|
||||
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
|
||||
'application/pdf', 'text/plain', 'text/csv',
|
||||
'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
];
|
||||
|
||||
private const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
private const DANGEROUS_EXTENSIONS = [
|
||||
'php', 'phtml', 'php3', 'php4', 'php5', 'pl', 'py', 'jsp', 'asp', 'sh', 'cgi',
|
||||
'exe', 'bat', 'com', 'scr', 'vbs', 'js', 'jar', 'war'
|
||||
];
|
||||
|
||||
private const MALWARE_SIGNATURES = [
|
||||
'eval(', 'base64_decode(', 'system(', 'exec(', 'shell_exec(',
|
||||
'<?php', '<%', '<script', 'javascript:'
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private EventDispatcher $eventDispatcher
|
||||
) {}
|
||||
|
||||
public function validateUpload(array $file): bool
|
||||
{
|
||||
$userEmail = $_SESSION['user_email'] ?? null;
|
||||
$filename = $file['name'] ?? '';
|
||||
$tmpName = $file['tmp_name'] ?? '';
|
||||
$size = $file['size'] ?? 0;
|
||||
$error = $file['error'] ?? UPLOAD_ERR_NO_FILE;
|
||||
|
||||
// Upload-Fehler prüfen
|
||||
if ($error !== UPLOAD_ERR_OK) {
|
||||
$this->dispatchSuspiciousUpload($filename, 'unknown', $size, 'upload_error', $userEmail);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Dateigröße prüfen
|
||||
if ($size > self::MAX_FILE_SIZE) {
|
||||
$this->dispatchSuspiciousUpload($filename, 'unknown', $size, 'file_too_large', $userEmail);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Dateiendung prüfen
|
||||
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
if (in_array($extension, self::DANGEROUS_EXTENSIONS)) {
|
||||
$this->dispatchSuspiciousUpload($filename, 'unknown', $size, 'dangerous_extension', $userEmail);
|
||||
return false;
|
||||
}
|
||||
|
||||
// MIME-Type prüfen
|
||||
$mimeType = mime_content_type($tmpName);
|
||||
if (!in_array($mimeType, self::ALLOWED_MIME_TYPES)) {
|
||||
$this->dispatchSuspiciousUpload($filename, $mimeType, $size, 'forbidden_mime_type', $userEmail);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Dateiinhalt auf Malware-Signaturen prüfen
|
||||
if ($this->containsMalwareSignatures($tmpName)) {
|
||||
$this->dispatchSuspiciousUpload($filename, $mimeType, $size, 'malware_signatures_detected', $userEmail);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Double-Extension prüfen (z.B. file.jpg.php)
|
||||
if ($this->hasDoubleExtension($filename)) {
|
||||
$this->dispatchSuspiciousUpload($filename, $mimeType, $size, 'double_extension', $userEmail);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function containsMalwareSignatures(string $filePath): bool
|
||||
{
|
||||
$content = file_get_contents($filePath);
|
||||
if ($content === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (self::MALWARE_SIGNATURES as $signature) {
|
||||
if (stripos($content, $signature) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function hasDoubleExtension(string $filename): bool
|
||||
{
|
||||
$parts = explode('.', $filename);
|
||||
if (count($parts) < 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prüfe ob vorletzte "Extension" gefährlich ist
|
||||
$secondLastExtension = strtolower($parts[count($parts) - 2]);
|
||||
return in_array($secondLastExtension, self::DANGEROUS_EXTENSIONS);
|
||||
}
|
||||
|
||||
private function dispatchSuspiciousUpload(
|
||||
string $filename,
|
||||
string $mimeType,
|
||||
int $size,
|
||||
string $reason,
|
||||
?string $userEmail
|
||||
): void {
|
||||
$this->eventDispatcher->dispatch(new SuspiciousFileUploadEvent(
|
||||
filename: $filename,
|
||||
mimeType: $mimeType,
|
||||
fileSize: $size,
|
||||
suspicionReason: $reason,
|
||||
email: $userEmail
|
||||
));
|
||||
}
|
||||
}
|
||||
125
src/Application/Security/Services/InputValidationService.php
Normal file
125
src/Application/Security/Services/InputValidationService.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\Services;
|
||||
|
||||
use App\Application\Security\Events\Input\{
|
||||
InputValidationFailureEvent,
|
||||
SqlInjectionAttemptEvent,
|
||||
XssAttemptEvent,
|
||||
MaliciousInputDetectedEvent
|
||||
};
|
||||
use App\Framework\Core\Events\EventDispatcher;
|
||||
|
||||
final class InputValidationService
|
||||
{
|
||||
private const SQL_INJECTION_PATTERNS = [
|
||||
'/(\bunion\b.*\bselect\b)/i',
|
||||
'/(\bdrop\b.*\btable\b)/i',
|
||||
'/(\binsert\b.*\binto\b)/i',
|
||||
'/(\bdelete\b.*\bfrom\b)/i',
|
||||
'/(\bupdate\b.*\bset\b)/i',
|
||||
'/(\b(or|and)\b.*[\'"].*[\'"].*=.*[\'"].*[\'"])/i'
|
||||
];
|
||||
|
||||
private const XSS_PATTERNS = [
|
||||
'/<script[^>]*>.*?<\/script>/is',
|
||||
'/<iframe[^>]*>.*?<\/iframe>/is',
|
||||
'/javascript:/i',
|
||||
'/on\w+\s*=/i',
|
||||
'/<object[^>]*>.*?<\/object>/is'
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private EventDispatcher $eventDispatcher
|
||||
) {}
|
||||
|
||||
public function validateInput(string $fieldName, mixed $value, array $rules = []): bool
|
||||
{
|
||||
$stringValue = (string) $value;
|
||||
$userEmail = $_SESSION['user_email'] ?? null;
|
||||
|
||||
// SQL Injection Detection
|
||||
if ($this->detectSqlInjection($stringValue)) {
|
||||
$this->eventDispatcher->dispatch(new SqlInjectionAttemptEvent(
|
||||
attackPayload: $stringValue,
|
||||
targetField: $fieldName,
|
||||
detectionMethod: 'pattern_matching',
|
||||
email: $userEmail
|
||||
));
|
||||
return false;
|
||||
}
|
||||
|
||||
// XSS Detection
|
||||
if ($this->detectXss($stringValue)) {
|
||||
$this->eventDispatcher->dispatch(new XssAttemptEvent(
|
||||
attackPayload: $stringValue,
|
||||
targetField: $fieldName,
|
||||
xssType: 'reflected_xss',
|
||||
email: $userEmail
|
||||
));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Standard Validation Rules
|
||||
foreach ($rules as $rule => $parameter) {
|
||||
if (!$this->applyRule($rule, $stringValue, $parameter)) {
|
||||
$this->eventDispatcher->dispatch(new InputValidationFailureEvent(
|
||||
fieldName: $fieldName,
|
||||
invalidValue: $stringValue,
|
||||
validationRule: $rule,
|
||||
email: $userEmail
|
||||
));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function sanitizeInput(string $input, string $context = 'html'): string
|
||||
{
|
||||
return match ($context) {
|
||||
'html' => htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
||||
'sql' => addslashes($input),
|
||||
'url' => urlencode($input),
|
||||
'javascript' => json_encode($input, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT),
|
||||
default => filter_var($input, FILTER_SANITIZE_SPECIAL_CHARS)
|
||||
};
|
||||
}
|
||||
|
||||
private function detectSqlInjection(string $input): bool
|
||||
{
|
||||
foreach (self::SQL_INJECTION_PATTERNS as $pattern) {
|
||||
if (preg_match($pattern, $input)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function detectXss(string $input): bool
|
||||
{
|
||||
foreach (self::XSS_PATTERNS as $pattern) {
|
||||
if (preg_match($pattern, $input)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function applyRule(string $rule, string $value, mixed $parameter): bool
|
||||
{
|
||||
return match ($rule) {
|
||||
'required' => !empty($value),
|
||||
'email' => filter_var($value, FILTER_VALIDATE_EMAIL) !== false,
|
||||
'min_length' => strlen($value) >= $parameter,
|
||||
'max_length' => strlen($value) <= $parameter,
|
||||
'numeric' => is_numeric($value),
|
||||
'alpha' => ctype_alpha($value),
|
||||
'alphanumeric' => ctype_alnum($value),
|
||||
'url' => filter_var($value, FILTER_VALIDATE_URL) !== false,
|
||||
default => true
|
||||
};
|
||||
}
|
||||
}
|
||||
121
src/Application/Security/ValueObjects/MaskedEmail.php
Normal file
121
src/Application/Security/ValueObjects/MaskedEmail.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\ValueObjects;
|
||||
|
||||
final class MaskedEmail
|
||||
{
|
||||
private function __construct(
|
||||
private string $maskedValue,
|
||||
private string $original
|
||||
) {}
|
||||
|
||||
public static function fromString(string $email): self
|
||||
{
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
// Nicht-E-Mail-Strings nur minimal maskieren
|
||||
return new self(
|
||||
self::maskGenericString($email),
|
||||
$email
|
||||
);
|
||||
}
|
||||
|
||||
return new self(
|
||||
self::maskEmailAddress($email),
|
||||
$email
|
||||
);
|
||||
}
|
||||
|
||||
public static function fromStringWithStrategy(string $email, MaskingStrategy $strategy): self
|
||||
{
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return new self(
|
||||
self::maskGenericString($email),
|
||||
$email
|
||||
);
|
||||
}
|
||||
|
||||
$maskedValue = match ($strategy) {
|
||||
MaskingStrategy::STANDARD => self::maskEmailAddress($email),
|
||||
MaskingStrategy::PARTIAL => self::maskEmailPartial($email),
|
||||
MaskingStrategy::DOMAIN_ONLY => self::maskDomainOnly($email),
|
||||
MaskingStrategy::HASH => self::hashEmail($email)
|
||||
};
|
||||
|
||||
return new self($maskedValue, $email);
|
||||
}
|
||||
|
||||
public function getMaskedValue(): string
|
||||
{
|
||||
return $this->maskedValue;
|
||||
}
|
||||
|
||||
public function getOriginal(): string
|
||||
{
|
||||
return $this->original;
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->maskedValue;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->maskedValue;
|
||||
}
|
||||
|
||||
private static function maskEmailAddress(string $email): string
|
||||
{
|
||||
[$local, $domain] = explode('@', $email, 2);
|
||||
|
||||
if (strlen($local) <= 2) {
|
||||
return str_repeat('*', strlen($local)) . '@' . $domain;
|
||||
}
|
||||
|
||||
$maskedLocal = substr($local, 0, 2) . str_repeat('*', strlen($local) - 2);
|
||||
return $maskedLocal . '@' . $domain;
|
||||
}
|
||||
|
||||
private static function maskEmailPartial(string $email): string
|
||||
{
|
||||
[$local, $domain] = explode('@', $email, 2);
|
||||
|
||||
if (strlen($local) <= 3) {
|
||||
return $local[0] . str_repeat('*', max(0, strlen($local) - 1)) . '@' . $domain;
|
||||
}
|
||||
|
||||
$maskedLocal = $local[0] . str_repeat('*', strlen($local) - 2) . substr($local, -1);
|
||||
return $maskedLocal . '@' . $domain;
|
||||
}
|
||||
|
||||
private static function maskDomainOnly(string $email): string
|
||||
{
|
||||
[$local, $domain] = explode('@', $email, 2);
|
||||
|
||||
// Domain verschleiern, aber Local-Part zeigen
|
||||
$domainParts = explode('.', $domain);
|
||||
if (count($domainParts) > 1) {
|
||||
$tld = array_pop($domainParts);
|
||||
$maskedDomain = str_repeat('*', strlen(implode('.', $domainParts))) . '.' . $tld;
|
||||
} else {
|
||||
$maskedDomain = str_repeat('*', strlen($domain));
|
||||
}
|
||||
|
||||
return $local . '@' . $maskedDomain;
|
||||
}
|
||||
|
||||
private static function hashEmail(string $email): string
|
||||
{
|
||||
return 'user_' . substr(hash('sha256', $email), 0, 8);
|
||||
}
|
||||
|
||||
private static function maskGenericString(string $input): string
|
||||
{
|
||||
if (strlen($input) <= 3) {
|
||||
return str_repeat('*', strlen($input));
|
||||
}
|
||||
|
||||
return substr($input, 0, 2) . str_repeat('*', strlen($input) - 2);
|
||||
}
|
||||
}
|
||||
22
src/Application/Security/ValueObjects/MaskingStrategy.php
Normal file
22
src/Application/Security/ValueObjects/MaskingStrategy.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\ValueObjects;
|
||||
|
||||
enum MaskingStrategy: string
|
||||
{
|
||||
case STANDARD = 'standard'; // jo***@example.com
|
||||
case PARTIAL = 'partial'; // j***n@example.com
|
||||
case DOMAIN_ONLY = 'domain_only'; // john@*******.com
|
||||
case HASH = 'hash'; // user_a1b2c3d4
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::STANDARD => 'Standard email masking (first 2 chars + asterisks)',
|
||||
self::PARTIAL => 'Partial masking (first + last char visible)',
|
||||
self::DOMAIN_ONLY => 'Mask domain only, keep local part',
|
||||
self::HASH => 'Replace with hash-based identifier'
|
||||
};
|
||||
}
|
||||
}
|
||||
119
src/Application/Security/ValueObjects/OWASPEventIdentifier.php
Normal file
119
src/Application/Security/ValueObjects/OWASPEventIdentifier.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\ValueObjects;
|
||||
|
||||
final class OWASPEventIdentifier
|
||||
{
|
||||
private function __construct(
|
||||
private string $category,
|
||||
private string $action,
|
||||
private ?string $subject = null
|
||||
) {}
|
||||
|
||||
public static function authenticationSuccess(string $username): self
|
||||
{
|
||||
return new self('AUTHN', 'login_success', $username);
|
||||
}
|
||||
|
||||
public static function authenticationFailure(string $username): self
|
||||
{
|
||||
return new self('AUTHN', 'login_failure', $username);
|
||||
}
|
||||
|
||||
public static function sessionTermination(string $username): self
|
||||
{
|
||||
return new self('AUTHN', 'logout', $username);
|
||||
}
|
||||
|
||||
public static function passwordChange(string $username): self
|
||||
{
|
||||
return new self('AUTHN', 'password_change', $username);
|
||||
}
|
||||
|
||||
public static function accountLocked(string $username): self
|
||||
{
|
||||
return new self('AUTHN', 'account_locked', $username);
|
||||
}
|
||||
|
||||
public static function authorizationFailure(string $username, string $resource): self
|
||||
{
|
||||
return new self('AUTHZ', 'access_denied', "{$username}:{$resource}");
|
||||
}
|
||||
|
||||
public static function privilegeEscalation(string $username, string $fromRole, string $toRole): self
|
||||
{
|
||||
return new self('AUTHZ', 'privilege_escalation', "{$username}:{$fromRole}>{$toRole}");
|
||||
}
|
||||
|
||||
public static function inputValidationFailure(string $field): self
|
||||
{
|
||||
return new self('INPUT', 'validation_failure', $field);
|
||||
}
|
||||
|
||||
public static function maliciousInput(string $attackType): self
|
||||
{
|
||||
return new self('INPUT', 'malicious_input', $attackType);
|
||||
}
|
||||
|
||||
public static function fileUploadFailure(string $filename): self
|
||||
{
|
||||
return new self('INPUT', 'file_upload_failure', $filename);
|
||||
}
|
||||
|
||||
public static function sessionHijacking(string $username): self
|
||||
{
|
||||
return new self('SESS', 'session_hijacking', $username);
|
||||
}
|
||||
|
||||
public static function sessionTimeout(string $username): self
|
||||
{
|
||||
return new self('SESS', 'session_timeout', $username);
|
||||
}
|
||||
|
||||
public static function malwareDetected(string $malwareType): self
|
||||
{
|
||||
return new self('MALICIOUS', 'malware_detected', $malwareType);
|
||||
}
|
||||
|
||||
public static function auditFailure(string $eventType): self
|
||||
{
|
||||
return new self('AUDIT', 'audit_failure', $eventType);
|
||||
}
|
||||
|
||||
public static function sessionFixation(string $username): self
|
||||
{
|
||||
return new self('SESS', 'session_fixation', $username);
|
||||
}
|
||||
|
||||
public static function cryptographicFailure(string $operation): self
|
||||
{
|
||||
return new self('CRYPTO', 'crypto_failure', $operation);
|
||||
}
|
||||
|
||||
public static function suspiciousNetworkActivity(string $activityType): self
|
||||
{
|
||||
return new self('NETWORK', 'suspicious_activity', $activityType);
|
||||
}
|
||||
|
||||
public static function systemAnomaly(string $anomalyType): self
|
||||
{
|
||||
return new self('SYSTEM', 'anomaly_detected', $anomalyType);
|
||||
}
|
||||
|
||||
public static function csrfViolation(string $requestPath): self
|
||||
{
|
||||
return new self('WEB', 'csrf_violation', $requestPath);
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
$identifier = "{$this->category}_{$this->action}";
|
||||
|
||||
if ($this->subject !== null) {
|
||||
$identifier .= ":{$this->subject}";
|
||||
}
|
||||
|
||||
return $identifier;
|
||||
}
|
||||
}
|
||||
110
src/Application/Security/ValueObjects/OWASPLogFormat.php
Normal file
110
src/Application/Security/ValueObjects/OWASPLogFormat.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\ValueObjects;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
|
||||
final class OWASPLogFormat
|
||||
{
|
||||
public function __construct(
|
||||
private DateTimeImmutable $datetime,
|
||||
private string $appid,
|
||||
private string $event,
|
||||
private string $level,
|
||||
private string $description,
|
||||
private ?string $useragent = null,
|
||||
private ?string $sourceIp = null,
|
||||
private ?string $hostIp = null,
|
||||
private ?string $hostname = null,
|
||||
private ?string $protocol = null,
|
||||
private ?string $port = null,
|
||||
private ?string $requestUri = null,
|
||||
private ?string $requestMethod = null,
|
||||
private ?string $region = null,
|
||||
private ?string $geo = null
|
||||
) {}
|
||||
|
||||
public static function create(
|
||||
string $appid,
|
||||
string $event,
|
||||
string $level,
|
||||
string $description,
|
||||
SecurityContext $context,
|
||||
RequestContext $requestContext
|
||||
): self {
|
||||
return new self(
|
||||
new DateTimeImmutable(),
|
||||
$appid,
|
||||
$event,
|
||||
$level,
|
||||
$description,
|
||||
$context->getUserAgent(),
|
||||
$context->getSourceIp(),
|
||||
$requestContext->getHostIp(),
|
||||
$requestContext->getHostname(),
|
||||
$requestContext->getProtocol(),
|
||||
$requestContext->getPort(),
|
||||
$requestContext->getRequestUri(),
|
||||
$requestContext->getRequestMethod(),
|
||||
$requestContext->getRegion(),
|
||||
$requestContext->getGeo()
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$data = [
|
||||
'datetime' => $this->datetime->format(DateTimeInterface::ATOM),
|
||||
'appid' => $this->appid,
|
||||
'event' => $this->event,
|
||||
'level' => $this->level,
|
||||
'description' => $this->description
|
||||
];
|
||||
|
||||
// Nur nicht-null Werte hinzufügen
|
||||
if ($this->useragent !== null) {
|
||||
$data['useragent'] = $this->useragent;
|
||||
}
|
||||
if ($this->sourceIp !== null) {
|
||||
$data['source_ip'] = $this->sourceIp;
|
||||
}
|
||||
if ($this->hostIp !== null) {
|
||||
$data['host_ip'] = $this->hostIp;
|
||||
}
|
||||
if ($this->hostname !== null) {
|
||||
$data['hostname'] = $this->hostname;
|
||||
}
|
||||
if ($this->protocol !== null) {
|
||||
$data['protocol'] = $this->protocol;
|
||||
}
|
||||
if ($this->port !== null) {
|
||||
$data['port'] = $this->port;
|
||||
}
|
||||
if ($this->requestUri !== null) {
|
||||
$data['request_uri'] = $this->requestUri;
|
||||
}
|
||||
if ($this->requestMethod !== null) {
|
||||
$data['request_method'] = $this->requestMethod;
|
||||
}
|
||||
if ($this->region !== null) {
|
||||
$data['region'] = $this->region;
|
||||
}
|
||||
if ($this->geo !== null) {
|
||||
$data['geo'] = $this->geo;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function toJson(): string
|
||||
{
|
||||
return json_encode($this->toArray(), JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
}
|
||||
39
src/Application/Security/ValueObjects/OWASPLogLevel.php
Normal file
39
src/Application/Security/ValueObjects/OWASPLogLevel.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\ValueObjects;
|
||||
|
||||
use App\Application\Security\SecurityEventType;
|
||||
|
||||
enum OWASPLogLevel: string
|
||||
{
|
||||
case DEBUG = 'DEBUG';
|
||||
case INFO = 'INFO';
|
||||
case WARN = 'WARN';
|
||||
case ERROR = 'ERROR';
|
||||
case FATAL = 'FATAL';
|
||||
|
||||
public static function fromSecurityEventType(SecurityEventType $type): self
|
||||
{
|
||||
return match ($type) {
|
||||
SecurityEventType::LOGIN_SUCCESS,
|
||||
SecurityEventType::LOGOUT,
|
||||
SecurityEventType::PASSWORD_CHANGE => self::INFO,
|
||||
|
||||
SecurityEventType::LOGIN_FAILED,
|
||||
SecurityEventType::ACCESS_DENIED,
|
||||
SecurityEventType::SESSION_TIMEOUT => self::WARN,
|
||||
|
||||
SecurityEventType::INJECTION_ATTEMPT,
|
||||
SecurityEventType::MALWARE_DETECTED,
|
||||
SecurityEventType::ACCOUNT_LOCKED,
|
||||
SecurityEventType::FILE_UPLOAD => self::ERROR,
|
||||
|
||||
SecurityEventType::PRIVILEGE_ESCALATION,
|
||||
SecurityEventType::SESSION_HIJACK,
|
||||
SecurityEventType::AUDIT_FAILURE => self::FATAL,
|
||||
|
||||
default => self::INFO
|
||||
};
|
||||
}
|
||||
}
|
||||
72
src/Application/Security/ValueObjects/RequestContext.php
Normal file
72
src/Application/Security/ValueObjects/RequestContext.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\ValueObjects;
|
||||
|
||||
final class RequestContext
|
||||
{
|
||||
private function __construct(
|
||||
private ?string $hostIp,
|
||||
private ?string $hostname,
|
||||
private ?string $protocol,
|
||||
private ?string $port,
|
||||
private ?string $requestUri,
|
||||
private ?string $requestMethod,
|
||||
private ?string $region,
|
||||
private ?string $geo
|
||||
) {}
|
||||
|
||||
public static function fromGlobals(): self
|
||||
{
|
||||
return new self(
|
||||
$_SERVER['SERVER_ADDR'] ?? null,
|
||||
$_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? null,
|
||||
isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http',
|
||||
$_SERVER['SERVER_PORT'] ?? null,
|
||||
$_SERVER['REQUEST_URI'] ?? null,
|
||||
$_SERVER['REQUEST_METHOD'] ?? null,
|
||||
$_ENV['AWS_REGION'] ?? $_ENV['REGION'] ?? null,
|
||||
$_ENV['GEO_REGION'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
public function getHostIp(): ?string
|
||||
{
|
||||
return $this->hostIp;
|
||||
}
|
||||
|
||||
public function getHostname(): ?string
|
||||
{
|
||||
return $this->hostname;
|
||||
}
|
||||
|
||||
public function getProtocol(): ?string
|
||||
{
|
||||
return $this->protocol;
|
||||
}
|
||||
|
||||
public function getPort(): ?string
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
public function getRequestUri(): ?string
|
||||
{
|
||||
return $this->requestUri;
|
||||
}
|
||||
|
||||
public function getRequestMethod(): ?string
|
||||
{
|
||||
return $this->requestMethod;
|
||||
}
|
||||
|
||||
public function getRegion(): ?string
|
||||
{
|
||||
return $this->region;
|
||||
}
|
||||
|
||||
public function getGeo(): ?string
|
||||
{
|
||||
return $this->geo;
|
||||
}
|
||||
}
|
||||
77
src/Application/Security/ValueObjects/SecurityContext.php
Normal file
77
src/Application/Security/ValueObjects/SecurityContext.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Security\ValueObjects;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
final class SecurityContext
|
||||
{
|
||||
private function __construct(
|
||||
private ?string $sourceIp,
|
||||
private ?string $userAgent,
|
||||
private ?string $sessionId,
|
||||
private ?string $requestId,
|
||||
private ?string $userId,
|
||||
private DateTimeImmutable $timestamp
|
||||
) {}
|
||||
|
||||
public static function fromGlobals(): self
|
||||
{
|
||||
return new self(
|
||||
$_SERVER['REMOTE_ADDR'] ?? null,
|
||||
$_SERVER['HTTP_USER_AGENT'] ?? null,
|
||||
session_id() ?: null,
|
||||
$_SERVER['HTTP_X_REQUEST_ID'] ?? null,
|
||||
$_SESSION['user_id'] ?? null,
|
||||
new DateTimeImmutable()
|
||||
);
|
||||
}
|
||||
|
||||
public static function create(
|
||||
?string $sourceIp = null,
|
||||
?string $userAgent = null,
|
||||
?string $sessionId = null,
|
||||
?string $requestId = null,
|
||||
?string $userId = null
|
||||
): self {
|
||||
return new self(
|
||||
$sourceIp,
|
||||
$userAgent,
|
||||
$sessionId,
|
||||
$requestId,
|
||||
$userId,
|
||||
new DateTimeImmutable()
|
||||
);
|
||||
}
|
||||
|
||||
public function getSourceIp(): ?string
|
||||
{
|
||||
return $this->sourceIp;
|
||||
}
|
||||
|
||||
public function getUserAgent(): ?string
|
||||
{
|
||||
return $this->userAgent;
|
||||
}
|
||||
|
||||
public function getSessionId(): ?string
|
||||
{
|
||||
return $this->sessionId;
|
||||
}
|
||||
|
||||
public function getRequestId(): ?string
|
||||
{
|
||||
return $this->requestId;
|
||||
}
|
||||
|
||||
public function getUserId(): ?string
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function getTimestamp(): DateTimeImmutable
|
||||
{
|
||||
return $this->timestamp;
|
||||
}
|
||||
}
|
||||
102
src/Application/Service/QrCodeService.php
Normal file
102
src/Application/Service/QrCodeService.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Service;
|
||||
|
||||
use App\Domain\QrCode\QrCode;
|
||||
use App\Domain\QrCode\Service\QrCodeEncoder;
|
||||
use App\Domain\QrCode\Service\QrCodeGenerator;
|
||||
use App\Domain\QrCode\Service\QrCodeRenderer;
|
||||
use App\Domain\QrCode\ValueObject\ErrorCorrectionLevel;
|
||||
use App\Domain\QrCode\ValueObject\QrCodeConfig;
|
||||
use App\Domain\QrCode\ValueObject\QrCodeMatrix;
|
||||
use App\Domain\QrCode\ValueObject\QrCodeVersion;
|
||||
|
||||
readonly class QrCodeService
|
||||
{
|
||||
public function __construct(
|
||||
private QrCodeGenerator $generator
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen QR-Code für die angegebenen Daten
|
||||
*
|
||||
* @param string $data Die zu kodierenden Daten
|
||||
* @param ErrorCorrectionLevel $errorLevel Fehlerkorrektur-Level
|
||||
* @param int|null $version Spezifische QR-Code-Version (optional)
|
||||
* @return QrCodeMatrix Die generierte QR-Code-Matrix
|
||||
*/
|
||||
public function generateQrCode(
|
||||
string $data,
|
||||
ErrorCorrectionLevel $errorLevel = ErrorCorrectionLevel::M,
|
||||
?int $version = null
|
||||
): QrCodeMatrix {
|
||||
$qrCodeVersion = $version ? new QrCodeVersion($version) : null;
|
||||
$qrCode = new QrCode($data, $errorLevel, $qrCodeVersion);
|
||||
|
||||
return $this->generator->generate($qrCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen QR-Code als SVG
|
||||
*
|
||||
* @param string $data Die zu kodierenden Daten
|
||||
* @param ErrorCorrectionLevel $errorLevel Fehlerkorrektur-Level
|
||||
* @param QrCodeConfig|null $config Konfigurationseinstellungen
|
||||
* @return string SVG-Darstellung des QR-Codes
|
||||
*/
|
||||
public function generateSvg(
|
||||
string $data,
|
||||
ErrorCorrectionLevel $errorLevel = ErrorCorrectionLevel::M,
|
||||
?QrCodeConfig $config = null
|
||||
): string {
|
||||
$matrix = $this->generateQrCode($data, $errorLevel);
|
||||
$config = $config ?? new QrCodeConfig();
|
||||
|
||||
return $matrix->toSvg(
|
||||
$config->getModuleSize(),
|
||||
$config->getMargin(),
|
||||
$config->getForegroundColor(),
|
||||
$config->getBackgroundColor()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen QR-Code als PNG
|
||||
*
|
||||
* @param string $data Die zu kodierenden Daten
|
||||
* @param ErrorCorrectionLevel $errorLevel Fehlerkorrektur-Level
|
||||
* @param QrCodeConfig|null $config Konfigurationseinstellungen
|
||||
* @return string PNG-Binärdaten des QR-Codes
|
||||
*/
|
||||
public function generatePng(
|
||||
string $data,
|
||||
ErrorCorrectionLevel $errorLevel = ErrorCorrectionLevel::M,
|
||||
?QrCodeConfig $config = null
|
||||
): string {
|
||||
$matrix = $this->generateQrCode($data, $errorLevel);
|
||||
$config = $config ?? new QrCodeConfig();
|
||||
|
||||
return $matrix->toPng(
|
||||
$config->getModuleSize(),
|
||||
$config->getMargin()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen QR-Code als ASCII-Art
|
||||
*
|
||||
* @param string $data Die zu kodierenden Daten
|
||||
* @param ErrorCorrectionLevel $errorLevel Fehlerkorrektur-Level
|
||||
* @return string ASCII-Darstellung des QR-Codes
|
||||
*/
|
||||
public function generateAscii(
|
||||
string $data,
|
||||
ErrorCorrectionLevel $errorLevel = ErrorCorrectionLevel::M
|
||||
): string {
|
||||
$matrix = $this->generateQrCode($data, $errorLevel);
|
||||
return $matrix->toAscii();
|
||||
}
|
||||
}
|
||||
117
src/Application/Shopify/CustomerController.php
Normal file
117
src/Application/Shopify/CustomerController.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Shopify;
|
||||
|
||||
use App\Framework\Api\ApiException;
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Router\Result\JsonResult;
|
||||
use App\Infrastructure\Api\ShopifyClient;
|
||||
use Archive\Config\ApiConfig;
|
||||
|
||||
final class CustomerController
|
||||
{
|
||||
private ShopifyClient $client;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->client = new ShopifyClient(
|
||||
ApiConfig::SHOPIFY_SHOP_DOMAIN->value,
|
||||
ApiConfig::SHOPIFY_ACCESS_TOKEN->value,
|
||||
ApiConfig::SHOPIFY_API_VERSION->value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft alle Kunden ab
|
||||
*/
|
||||
#[Route(path: '/api/shopify/customers', method: Method::GET)]
|
||||
public function listCustomers(): JsonResult
|
||||
{
|
||||
try {
|
||||
$options = [
|
||||
'limit' => 50,
|
||||
'fields' => 'id,email,first_name,last_name,orders_count,total_spent'
|
||||
];
|
||||
|
||||
$customers = $this->client->getCustomers($options);
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $customers
|
||||
]);
|
||||
} catch (ApiException $e) {
|
||||
$result = new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$result->status = Status::from($e->getCode() ?: 500);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft einen einzelnen Kunden ab
|
||||
*/
|
||||
#[Route(path: '/api/shopify/customers/{id}', method: Method::GET)]
|
||||
public function getCustomer(int $id): JsonResult
|
||||
{
|
||||
try {
|
||||
$customer = $this->client->getCustomer($id);
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $customer
|
||||
]);
|
||||
} catch (ApiException $e) {
|
||||
$result = new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$result->status = Status::from($e->getCode() ?: 500);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen Kunden
|
||||
*/
|
||||
#[Route(path: '/api/shopify/customers', method: Method::POST)]
|
||||
public function createCustomer(CustomerRequest $request): JsonResult
|
||||
{
|
||||
try {
|
||||
$customerData = [
|
||||
'first_name' => $request->firstName,
|
||||
'last_name' => $request->lastName,
|
||||
'email' => $request->email,
|
||||
'phone' => $request->phone ?? null,
|
||||
'verified_email' => $request->verifiedEmail ?? true,
|
||||
'addresses' => $request->addresses ?? [],
|
||||
'password' => $request->password ?? null,
|
||||
'password_confirmation' => $request->password ?? null,
|
||||
'send_email_welcome' => $request->sendWelcomeEmail ?? false
|
||||
];
|
||||
|
||||
// Entferne leere Felder
|
||||
$customerData = array_filter($customerData, fn($value) => $value !== null);
|
||||
|
||||
$customer = $this->client->createCustomer($customerData);
|
||||
|
||||
$result = new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $customer
|
||||
]);
|
||||
$result->status = Status::CREATED;
|
||||
return $result;
|
||||
} catch (ApiException $e) {
|
||||
$result = new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$result->status = Status::from($e->getCode() ?: 500);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Application/Shopify/CustomerRequest.php
Normal file
18
src/Application/Shopify/CustomerRequest.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Shopify;
|
||||
|
||||
final readonly class CustomerRequest
|
||||
{
|
||||
public function __construct(
|
||||
public string $firstName,
|
||||
public string $lastName,
|
||||
public string $email,
|
||||
public ?string $phone = null,
|
||||
public ?bool $verifiedEmail = null,
|
||||
public ?array $addresses = null,
|
||||
public ?string $password = null,
|
||||
public ?bool $sendWelcomeEmail = null
|
||||
) {}
|
||||
}
|
||||
118
src/Application/Shopify/OrderController.php
Normal file
118
src/Application/Shopify/OrderController.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Shopify;
|
||||
|
||||
use App\Framework\Api\ApiException;
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Router\Result\JsonResult;
|
||||
use App\Infrastructure\Api\ShopifyClient;
|
||||
use Archive\Config\ApiConfig;
|
||||
|
||||
final class OrderController
|
||||
{
|
||||
private ShopifyClient $client;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->client = new ShopifyClient(
|
||||
ApiConfig::SHOPIFY_SHOP_DOMAIN->value,
|
||||
ApiConfig::SHOPIFY_ACCESS_TOKEN->value,
|
||||
ApiConfig::SHOPIFY_API_VERSION->value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft alle Bestellungen ab
|
||||
*/
|
||||
#[Route(path: '/api/shopify/orders', method: Method::GET)]
|
||||
public function listOrders(): JsonResult
|
||||
{
|
||||
try {
|
||||
$options = [
|
||||
'limit' => 50,
|
||||
'status' => 'any',
|
||||
'fields' => 'id,order_number,customer,total_price,created_at,financial_status'
|
||||
];
|
||||
|
||||
$orders = $this->client->getOrders($options);
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $orders
|
||||
]);
|
||||
} catch (ApiException $e) {
|
||||
$result = new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$result->status = Status::from($e->getCode() ?: 500);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft eine einzelne Bestellung ab
|
||||
*/
|
||||
#[Route(path: '/api/shopify/orders/{id}', method: Method::GET)]
|
||||
public function getOrder(int $id): JsonResult
|
||||
{
|
||||
try {
|
||||
$order = $this->client->getOrder($id);
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $order
|
||||
]);
|
||||
} catch (ApiException $e) {
|
||||
$result = new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$result->status = Status::from($e->getCode() ?: 500);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine neue Bestellung
|
||||
*/
|
||||
#[Route(path: '/api/shopify/orders', method: Method::POST)]
|
||||
public function createOrder(OrderRequest $request): JsonResult
|
||||
{
|
||||
try {
|
||||
$orderData = [
|
||||
'line_items' => $request->lineItems,
|
||||
'customer' => $request->customer ?? null,
|
||||
'shipping_address' => $request->shippingAddress ?? null,
|
||||
'billing_address' => $request->billingAddress ?? null,
|
||||
'financial_status' => $request->financialStatus ?? 'pending',
|
||||
'fulfillment_status' => $request->fulfillmentStatus ?? null,
|
||||
'tags' => $request->tags ?? '',
|
||||
'note' => $request->note ?? null,
|
||||
'email' => $request->email ?? null
|
||||
];
|
||||
|
||||
// Entferne leere Felder
|
||||
$orderData = array_filter($orderData, fn($value) => $value !== null);
|
||||
|
||||
$order = $this->client->createOrder($orderData);
|
||||
|
||||
$result = new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $order
|
||||
]);
|
||||
$result->status = Status::CREATED;
|
||||
return $result;
|
||||
} catch (ApiException $e) {
|
||||
$result = new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$result->status = Status::from($e->getCode() ?: 500);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/Application/Shopify/OrderRequest.php
Normal file
19
src/Application/Shopify/OrderRequest.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Shopify;
|
||||
|
||||
final class OrderRequest
|
||||
{
|
||||
public function __construct(
|
||||
public readonly array $lineItems,
|
||||
public readonly ?array $customer = null,
|
||||
public readonly ?array $shippingAddress = null,
|
||||
public readonly ?array $billingAddress = null,
|
||||
public readonly ?string $financialStatus = null,
|
||||
public readonly ?string $fulfillmentStatus = null,
|
||||
public readonly ?string $tags = null,
|
||||
public readonly ?string $note = null,
|
||||
public readonly ?string $email = null
|
||||
) {}
|
||||
}
|
||||
192
src/Application/Shopify/ProductController.php
Normal file
192
src/Application/Shopify/ProductController.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Shopify;
|
||||
|
||||
use App\Framework\Api\ApiException;
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Router\Result\JsonResult;
|
||||
use App\Infrastructure\Api\ShopifyClient;
|
||||
use Archive\Config\ApiConfig;
|
||||
|
||||
final class ProductController
|
||||
{
|
||||
private ShopifyClient $client;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->client = new ShopifyClient(
|
||||
ApiConfig::SHOPIFY_SHOP_DOMAIN->value,
|
||||
ApiConfig::SHOPIFY_ACCESS_TOKEN->value,
|
||||
ApiConfig::SHOPIFY_API_VERSION->value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft alle Produkte aus dem Shopify-Shop ab
|
||||
*/
|
||||
#[Route(path: '/api/shopify/products', method: Method::GET)]
|
||||
public function listProducts(): JsonResult
|
||||
{
|
||||
try {
|
||||
$options = [
|
||||
'limit' => 50,
|
||||
'fields' => 'id,title,variants,images,status,published_at'
|
||||
];
|
||||
|
||||
$products = $this->client->getProducts($options);
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $products
|
||||
]);
|
||||
} catch (ApiException $e) {
|
||||
$result = new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$result->status = Status::from($e->getCode() ?: 500);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft ein einzelnes Produkt anhand seiner ID ab
|
||||
*/
|
||||
#[Route(path: '/api/shopify/products/{id}', method: Method::GET)]
|
||||
public function getProduct(int $id): JsonResult
|
||||
{
|
||||
try {
|
||||
$product = $this->client->getProduct($id);
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $product
|
||||
]);
|
||||
} catch (ApiException $e) {
|
||||
$result = new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$result->status = Status::from($e->getCode() ?: 500);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt ein neues Produkt
|
||||
*/
|
||||
#[Route(path: '/api/shopify/products', method: Method::POST)]
|
||||
public function createProduct(ProductRequest $request): JsonResult
|
||||
{
|
||||
try {
|
||||
$product = $this->client->createProduct([
|
||||
'title' => $request->title,
|
||||
'body_html' => $request->description ?? '',
|
||||
'vendor' => $request->vendor ?? 'Custom Shop',
|
||||
'product_type' => $request->productType ?? '',
|
||||
'tags' => $request->tags ?? '',
|
||||
'variants' => $request->variants ?? [],
|
||||
'options' => $request->options ?? [],
|
||||
'images' => $request->images ?? []
|
||||
]);
|
||||
|
||||
$result = new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $product
|
||||
]);
|
||||
$result->status = Status::CREATED;
|
||||
return $result;
|
||||
} catch (ApiException $e) {
|
||||
$result = new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$result->status = Status::from($e->getCode() ?: 500);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert ein bestehendes Produkt
|
||||
*/
|
||||
#[Route(path: '/api/shopify/products/{id}', method: Method::PUT)]
|
||||
public function updateProduct(int $id, ProductRequest $request): JsonResult
|
||||
{
|
||||
try {
|
||||
$productData = [];
|
||||
|
||||
// Nur die Felder aktualisieren, die tatsächlich im Request enthalten sind
|
||||
if (isset($request->title)) $productData['title'] = $request->title;
|
||||
if (isset($request->description)) $productData['body_html'] = $request->description;
|
||||
if (isset($request->vendor)) $productData['vendor'] = $request->vendor;
|
||||
if (isset($request->productType)) $productData['product_type'] = $request->productType;
|
||||
if (isset($request->tags)) $productData['tags'] = $request->tags;
|
||||
if (isset($request->variants)) $productData['variants'] = $request->variants;
|
||||
if (isset($request->options)) $productData['options'] = $request->options;
|
||||
if (isset($request->images)) $productData['images'] = $request->images;
|
||||
|
||||
$product = $this->client->updateProduct($id, $productData);
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $product
|
||||
]);
|
||||
} catch (ApiException $e) {
|
||||
$result = new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$result->status = Status::from($e->getCode() ?: 500);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht ein Produkt
|
||||
*/
|
||||
#[Route(path: '/api/shopify/products/{id}', method: Method::DELETE)]
|
||||
public function deleteProduct(int $id): JsonResult
|
||||
{
|
||||
try {
|
||||
$success = $this->client->deleteProduct($id);
|
||||
|
||||
return new JsonResult([
|
||||
'success' => $success,
|
||||
'message' => $success ? 'Produkt erfolgreich gelöscht' : 'Produkt konnte nicht gelöscht werden'
|
||||
]);
|
||||
} catch (ApiException $e) {
|
||||
$result = new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$result->status = Status::from($e->getCode() ?: 500);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht nach Produkten
|
||||
*/
|
||||
#[Route(path: '/api/shopify/products/search', method: Method::GET)]
|
||||
public function searchProducts(string $query): JsonResult
|
||||
{
|
||||
try {
|
||||
$products = $this->client->searchProducts($query);
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $products
|
||||
]);
|
||||
} catch (ApiException $e) {
|
||||
$result = new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$result->status = Status::from($e->getCode() ?: 500);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Application/Shopify/ProductRequest.php
Normal file
18
src/Application/Shopify/ProductRequest.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Shopify;
|
||||
|
||||
final class ProductRequest
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $title,
|
||||
public readonly ?string $description = null,
|
||||
public readonly ?string $vendor = null,
|
||||
public readonly ?string $productType = null,
|
||||
public readonly ?string $tags = null,
|
||||
public readonly ?array $variants = null,
|
||||
public readonly ?array $options = null,
|
||||
public readonly ?array $images = null
|
||||
) {}
|
||||
}
|
||||
101
src/Application/Shopify/ShopController.php
Normal file
101
src/Application/Shopify/ShopController.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Shopify;
|
||||
|
||||
use App\Framework\Api\ApiException;
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Router\Result\JsonResult;
|
||||
use App\Infrastructure\Api\ShopifyClient;
|
||||
use Archive\Config\ApiConfig;
|
||||
|
||||
final class ShopController
|
||||
{
|
||||
private ShopifyClient $client;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->client = new ShopifyClient(
|
||||
ApiConfig::SHOPIFY_SHOP_DOMAIN->value,
|
||||
ApiConfig::SHOPIFY_ACCESS_TOKEN->value,
|
||||
ApiConfig::SHOPIFY_API_VERSION->value
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft Informationen über den Shop ab
|
||||
*/
|
||||
#[Route(path: '/api/shopify/shop', method: Method::GET)]
|
||||
public function getShopInfo(): JsonResult
|
||||
{
|
||||
try {
|
||||
$shopInfo = $this->client->getShopInfo();
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $shopInfo
|
||||
]);
|
||||
} catch (ApiException $e) {
|
||||
$result = new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$result->status = Status::from($e->getCode() ?: 500);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft die Webhooks des Shops ab
|
||||
*/
|
||||
#[Route(path: '/api/shopify/webhooks', method: Method::GET)]
|
||||
public function getWebhooks(): JsonResult
|
||||
{
|
||||
try {
|
||||
$webhooks = $this->client->getWebhooks();
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $webhooks
|
||||
]);
|
||||
} catch (ApiException $e) {
|
||||
$result = new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$result->status = Status::from($e->getCode() ?: 500);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen Webhook
|
||||
*/
|
||||
#[Route(path: '/api/shopify/webhooks', method: Method::POST)]
|
||||
public function createWebhook(WebhookRequest $request): JsonResult
|
||||
{
|
||||
try {
|
||||
$webhook = $this->client->createWebhook(
|
||||
$request->topic,
|
||||
$request->address,
|
||||
$request->format ?? 'json'
|
||||
);
|
||||
|
||||
$result = new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $webhook
|
||||
]);
|
||||
$result->status = Status::CREATED;
|
||||
return $result;
|
||||
} catch (ApiException $e) {
|
||||
$result = new JsonResult([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
$result->status = Status::from($e->getCode() ?: 500);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
103
src/Application/Shopify/ShopifyWebhookHandler.php
Normal file
103
src/Application/Shopify/ShopifyWebhookHandler.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Shopify;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Router\Result\JsonResult;
|
||||
|
||||
final class ShopifyWebhookHandler
|
||||
{
|
||||
/**
|
||||
* Verarbeitet eingehende Shopify-Webhooks
|
||||
*
|
||||
* Hinweis: Shopify überprüft die Authentizität von Webhooks mit dem X-Shopify-Hmac-Sha256 Header
|
||||
*/
|
||||
#[Route(path: '/webhook/shopify', method: Method::POST)]
|
||||
public function handleWebhook(Request $request): JsonResult
|
||||
{
|
||||
// Webhook-Thema aus dem Header lesen
|
||||
$topic = $request->headers->get('X-Shopify-Topic')[0] ?? null;
|
||||
$shopDomain = $request->headers->get('X-Shopify-Shop-Domain')[0] ?? null;
|
||||
$hmac = $request->headers->get('X-Shopify-Hmac-Sha256')[0] ?? null;
|
||||
|
||||
// Validiere den HMAC, um sicherzustellen, dass der Request von Shopify kommt
|
||||
$rawData = $request->body;
|
||||
|
||||
if (!$this->validateWebhookHmac($hmac, $rawData)) {
|
||||
$result = new JsonResult(['error' => 'Ungültiger HMAC']);
|
||||
$result->status = Status::UNAUTHORIZED;
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Daten verarbeiten
|
||||
$data = json_decode($rawData, true);
|
||||
|
||||
// Je nach Topic unterschiedlich verarbeiten
|
||||
switch ($topic) {
|
||||
case 'orders/create':
|
||||
$this->processOrderCreated($data);
|
||||
break;
|
||||
case 'products/create':
|
||||
case 'products/update':
|
||||
$this->processProductUpdate($data);
|
||||
break;
|
||||
case 'customers/create':
|
||||
$this->processCustomerCreated($data);
|
||||
break;
|
||||
// Weitere Webhook-Themen...
|
||||
default:
|
||||
// Unbekanntes Thema, loggen oder ignorieren
|
||||
break;
|
||||
}
|
||||
|
||||
// Shopify erwartet eine erfolgreiche Antwort (2xx)
|
||||
return new JsonResult(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert den HMAC-Header
|
||||
*/
|
||||
private function validateWebhookHmac(?string $hmac, string $data): bool
|
||||
{
|
||||
if (!$hmac) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Das Shared Secret sollte eigentlich in ApiConfig sein
|
||||
$secret = 'dein_webhook_shared_secret'; // ODER aus ApiConfig holen
|
||||
$calculatedHmac = base64_encode(hash_hmac('sha256', $data, $secret, true));
|
||||
|
||||
return hash_equals($calculatedHmac, $hmac);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet eine neu erstellte Bestellung
|
||||
*/
|
||||
private function processOrderCreated(array $orderData): void
|
||||
{
|
||||
// Hier die Logik für neue Bestellungen implementieren
|
||||
// z.B. in eigenes System übertragen, E-Mails versenden, etc.
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet Produkt-Updates
|
||||
*/
|
||||
private function processProductUpdate(array $productData): void
|
||||
{
|
||||
// Hier die Logik für Produkt-Updates implementieren
|
||||
// z.B. Lagerbestand in eigenem System aktualisieren
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet einen neu erstellten Kunden
|
||||
*/
|
||||
private function processCustomerCreated(array $customerData): void
|
||||
{
|
||||
// Hier die Logik für neue Kunden implementieren
|
||||
// z.B. in CRM-System übertragen, Newsletter-Anmeldung
|
||||
}
|
||||
}
|
||||
13
src/Application/Shopify/WebhookRequest.php
Normal file
13
src/Application/Shopify/WebhookRequest.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Shopify;
|
||||
|
||||
final class WebhookRequest
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $topic,
|
||||
public readonly string $address,
|
||||
public readonly ?string $format = 'json'
|
||||
) {}
|
||||
}
|
||||
28
src/Application/System/BootLogger.php
Normal file
28
src/Application/System/BootLogger.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\System;
|
||||
|
||||
use App\Framework\Core\Events\ApplicationBooted;
|
||||
use App\Framework\Core\Events\OnEvent;
|
||||
use App\Framework\Logging\DefaultLogger;
|
||||
|
||||
final readonly class BootLogger
|
||||
{
|
||||
public function __construct(
|
||||
private DefaultLogger $logger
|
||||
) {}
|
||||
|
||||
#[OnEvent]
|
||||
public function log(ApplicationBooted $event): void
|
||||
{
|
||||
#echo "[Boot] {$event->bootTime->format('H:i:s')} | Env: {$event->environment}\n";
|
||||
|
||||
$this->logger->info('Application booted', [
|
||||
'boot_time' => $event->bootTime->format('Y-m-d H:i:s.u'),
|
||||
'environment' => $event->environment,
|
||||
'memory_usage' => memory_get_usage(true),
|
||||
'peak_memory' => memory_get_peak_usage(true)
|
||||
]);
|
||||
}
|
||||
}
|
||||
24
src/Application/System/ErrorLogger.php
Normal file
24
src/Application/System/ErrorLogger.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\System;
|
||||
|
||||
use App\Framework\Core\Events\ErrorOccurred;
|
||||
use App\Framework\Core\Events\OnEvent;
|
||||
|
||||
final readonly class ErrorLogger
|
||||
{
|
||||
/**
|
||||
* Logger für aufgetretene Fehler
|
||||
*/
|
||||
#[OnEvent(priority: 10)]
|
||||
public function logError(ErrorOccurred $event): void
|
||||
{
|
||||
$time = $event->occurredAt->format('Y-m-d H:i:s');
|
||||
$requestId = $event->requestId ? "[Request: {$event->requestId}]" : '';
|
||||
$message = "[{$time}] ERROR {$requestId} {$event->context}: {$event->error->getMessage()}";
|
||||
|
||||
// In einer produktiven Umgebung würden wir hier in eine Datei oder einen Service loggen
|
||||
error_log($message);
|
||||
}
|
||||
}
|
||||
30
src/Application/System/UserRegistrationLogger.php
Normal file
30
src/Application/System/UserRegistrationLogger.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\System;
|
||||
|
||||
use App\Framework\Core\Events\OnEvent;
|
||||
use App\Framework\Core\Events\UserRegistered;
|
||||
|
||||
/**
|
||||
* Logger für Benutzerregistrierungen
|
||||
*/
|
||||
final readonly class UserRegistrationLogger
|
||||
{
|
||||
/**
|
||||
* Protokolliert die Registrierung eines neuen Benutzers
|
||||
*/
|
||||
#[OnEvent]
|
||||
public function logUserRegistration(UserRegistered $event): void
|
||||
{
|
||||
$message = sprintf(
|
||||
"[%s] Neuer Benutzer registriert: %s (%s)",
|
||||
$event->occurredAt->format('Y-m-d H:i:s'),
|
||||
$event->username,
|
||||
$event->email
|
||||
);
|
||||
|
||||
// In der Produktion würden wir in eine Datei oder einen Service loggen
|
||||
error_log($message);
|
||||
}
|
||||
}
|
||||
9
src/Application/Website/DemoQuery.php
Normal file
9
src/Application/Website/DemoQuery.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Website;
|
||||
|
||||
final class DemoQuery
|
||||
{
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user