Initial commit of the Asset Management System, including project structure, Docker configuration, database migrations, and core application files. Added user authentication, asset management features, and basic UI components.

This commit is contained in:
2025-08-22 21:41:02 +02:00
parent b43a98f0ec
commit 677f70a19c
52 changed files with 5186 additions and 2 deletions

120
app/Core/Application.php Normal file
View File

@@ -0,0 +1,120 @@
<?php
namespace App\Core;
use App\Middleware\AuthMiddleware;
use App\Middleware\AdminMiddleware;
use App\Middleware\CsrfMiddleware;
class Application
{
private Router $router;
private Database $database;
private Session $session;
private array $middleware = [];
public function __construct()
{
$this->router = Router::getInstance();
$this->database = new Database();
$this->session = new Session();
// Register middleware
$this->registerMiddleware();
}
public function handle(): void
{
try {
$request = new Request();
$response = new Response();
// Apply global middleware
$this->applyMiddleware($request, $response);
// Route the request
$route = $this->router->match($request->getMethod(), $request->getPath());
if (!$route) {
$this->handleNotFound();
return;
}
// Apply route-specific middleware
if (isset($route['middleware'])) {
$this->applyRouteMiddleware($route['middleware'], $request, $response);
}
// Execute controller
$controller = new $route['controller']();
$method = $route['method'];
// Inject dependencies
$controller->setRequest($request);
$controller->setResponse($response);
$controller->setSession($this->session);
$controller->setDatabase($this->database);
// Execute with parameters
$params = $route['params'] ?? [];
$result = call_user_func_array([$controller, $method], $params);
// Send response
if ($result instanceof Response) {
$result->send();
} else {
$response->setContent($result);
$response->send();
}
} catch (\Exception $e) {
$this->handleError($e);
}
}
private function registerMiddleware(): void
{
$this->middleware = [
'auth' => AuthMiddleware::class,
'admin' => AdminMiddleware::class,
'csrf' => CsrfMiddleware::class,
];
}
private function applyMiddleware(Request $request, Response $response): void
{
// Apply CSRF middleware to all POST requests
if ($request->getMethod() === 'POST') {
$csrfMiddleware = new CsrfMiddleware();
$csrfMiddleware->handle($request, $response);
}
}
private function applyRouteMiddleware(array $middlewareNames, Request $request, Response $response): void
{
foreach ($middlewareNames as $name) {
if (isset($this->middleware[$name])) {
$middlewareClass = $this->middleware[$name];
$middleware = new $middlewareClass();
$middleware->handle($request, $response);
}
}
}
private function handleNotFound(): void
{
http_response_code(404);
require APP_PATH . '/Views/errors/404.php';
}
private function handleError(\Exception $e): void
{
if (APP_DEBUG) {
throw $e;
}
error_log($e->getMessage());
http_response_code(500);
require APP_PATH . '/Views/errors/500.php';
}
}

132
app/Core/Database.php Normal file
View File

@@ -0,0 +1,132 @@
<?php
namespace App\Core;
use PDO;
use PDOException;
class Database
{
private ?PDO $connection = null;
private static ?Database $instance = null;
public static function getInstance(): Database
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getConnection(): PDO
{
if ($this->connection === null) {
$this->connect();
}
return $this->connection;
}
private function connect(): void
{
try {
$dsn = sprintf(
'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4',
DB_HOST,
DB_PORT,
DB_NAME
);
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci"
];
$this->connection = new PDO($dsn, DB_USER, DB_PASS, $options);
} catch (PDOException $e) {
error_log('Database connection failed: ' . $e->getMessage());
throw new \Exception('Database connection failed');
}
}
public function query(string $sql, array $params = []): \PDOStatement
{
$stmt = $this->getConnection()->prepare($sql);
$stmt->execute($params);
return $stmt;
}
public function fetch(string $sql, array $params = []): ?array
{
$stmt = $this->query($sql, $params);
$result = $stmt->fetch();
return $result ?: null;
}
public function fetchAll(string $sql, array $params = []): array
{
$stmt = $this->query($sql, $params);
return $stmt->fetchAll();
}
public function insert(string $table, array $data): int
{
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
$this->query($sql, $data);
return (int) $this->getConnection()->lastInsertId();
}
public function update(string $table, array $data, string $where, array $whereParams = []): int
{
$setParts = [];
foreach (array_keys($data) as $column) {
$setParts[] = "{$column} = :{$column}";
}
$setClause = implode(', ', $setParts);
$sql = "UPDATE {$table} SET {$setClause} WHERE {$where}";
$params = array_merge($data, $whereParams);
$stmt = $this->query($sql, $params);
return $stmt->rowCount();
}
public function delete(string $table, string $where, array $params = []): int
{
$sql = "DELETE FROM {$table} WHERE {$where}";
$stmt = $this->query($sql, $params);
return $stmt->rowCount();
}
public function beginTransaction(): bool
{
return $this->getConnection()->beginTransaction();
}
public function commit(): bool
{
return $this->getConnection()->commit();
}
public function rollback(): bool
{
return $this->getConnection()->rollback();
}
public function inTransaction(): bool
{
return $this->getConnection()->inTransaction();
}
public function quote(string $value): string
{
return $this->getConnection()->quote($value);
}
}

182
app/Core/Request.php Normal file
View File

@@ -0,0 +1,182 @@
<?php
namespace App\Core;
class Request
{
private array $get;
private array $post;
private array $files;
private array $server;
private array $headers;
public function __construct()
{
$this->get = $_GET;
$this->post = $_POST;
$this->files = $_FILES;
$this->server = $_SERVER;
$this->headers = $this->getRequestHeaders();
}
public function getMethod(): string
{
return strtoupper($this->server['REQUEST_METHOD'] ?? 'GET');
}
public function getPath(): string
{
$path = $this->server['REQUEST_URI'] ?? '/';
$position = strpos($path, '?');
if ($position !== false) {
$path = substr($path, 0, $position);
}
return $path;
}
public function get(string $key, $default = null)
{
return $this->get[$key] ?? $default;
}
public function post(string $key, $default = null)
{
return $this->post[$key] ?? $default;
}
public function file(string $key)
{
return $this->files[$key] ?? null;
}
public function all(): array
{
return array_merge($this->get, $this->post);
}
public function only(array $keys): array
{
$data = $this->all();
return array_intersect_key($data, array_flip($keys));
}
public function except(array $keys): array
{
$data = $this->all();
return array_diff_key($data, array_flip($keys));
}
public function has(string $key): bool
{
return isset($this->get[$key]) || isset($this->post[$key]);
}
public function isPost(): bool
{
return $this->getMethod() === 'POST';
}
public function isGet(): bool
{
return $this->getMethod() === 'GET';
}
public function isAjax(): bool
{
return isset($this->headers['X-Requested-With']) &&
$this->headers['X-Requested-With'] === 'XMLHttpRequest';
}
public function getHeader(string $name): ?string
{
return $this->headers[$name] ?? null;
}
public function getHeaders(): array
{
return $this->headers;
}
public function getIp(): string
{
return $this->server['REMOTE_ADDR'] ?? '0.0.0.0';
}
public function getUserAgent(): string
{
return $this->server['HTTP_USER_AGENT'] ?? '';
}
public function getContentType(): string
{
return $this->server['CONTENT_TYPE'] ?? '';
}
public function getContentLength(): int
{
return (int) ($this->server['CONTENT_LENGTH'] ?? 0);
}
public function validate(array $rules): array
{
$errors = [];
$data = $this->all();
foreach ($rules as $field => $rule) {
$value = $data[$field] ?? null;
if (strpos($rule, 'required') !== false && empty($value)) {
$errors[$field][] = 'Das Feld ist erforderlich.';
continue;
}
if (!empty($value)) {
if (strpos($rule, 'email') !== false && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
$errors[$field][] = 'Ungültige E-Mail-Adresse.';
}
if (strpos($rule, 'min:') !== false) {
preg_match('/min:(\d+)/', $rule, $matches);
$min = (int) $matches[1];
if (strlen($value) < $min) {
$errors[$field][] = "Mindestens {$min} Zeichen erforderlich.";
}
}
if (strpos($rule, 'max:') !== false) {
preg_match('/max:(\d+)/', $rule, $matches);
$max = (int) $matches[1];
if (strlen($value) > $max) {
$errors[$field][] = "Maximal {$max} Zeichen erlaubt.";
}
}
if (strpos($rule, 'numeric') !== false && !is_numeric($value)) {
$errors[$field][] = 'Nur Zahlen erlaubt.';
}
if (strpos($rule, 'date') !== false && !strtotime($value)) {
$errors[$field][] = 'Ungültiges Datum.';
}
}
}
return $errors;
}
private function getRequestHeaders(): array
{
$headers = [];
foreach ($this->server as $key => $value) {
if (strpos($key, 'HTTP_') === 0) {
$header = str_replace('_', '-', strtolower(substr($key, 5)));
$headers[$header] = $value;
}
}
return $headers;
}
}

141
app/Core/Response.php Normal file
View File

@@ -0,0 +1,141 @@
<?php
namespace App\Core;
class Response
{
private string $content = '';
private int $statusCode = 200;
private array $headers = [];
private string $contentType = 'text/html; charset=utf-8';
public function setContent(string $content): self
{
$this->content = $content;
return $this;
}
public function getContent(): string
{
return $this->content;
}
public function setStatusCode(int $statusCode): self
{
$this->statusCode = $statusCode;
return $this;
}
public function getStatusCode(): int
{
return $this->statusCode;
}
public function setHeader(string $name, string $value): self
{
$this->headers[$name] = $value;
return $this;
}
public function setHeaders(array $headers): self
{
$this->headers = array_merge($this->headers, $headers);
return $this;
}
public function getHeaders(): array
{
return $this->headers;
}
public function setContentType(string $contentType): self
{
$this->contentType = $contentType;
return $this;
}
public function getContentType(): string
{
return $this->contentType;
}
public function redirect(string $url, int $statusCode = 302): self
{
$this->setHeader('Location', $url);
$this->setStatusCode($statusCode);
return $this;
}
public function json(array $data, int $statusCode = 200): self
{
$this->setContentType('application/json');
$this->setContent(json_encode($data, JSON_UNESCAPED_UNICODE));
$this->setStatusCode($statusCode);
return $this;
}
public function download(string $filePath, string $filename = null): self
{
if (!file_exists($filePath)) {
throw new \Exception('File not found: ' . $filePath);
}
$filename = $filename ?: basename($filePath);
$mimeType = mime_content_type($filePath) ?: 'application/octet-stream';
$this->setHeader('Content-Type', $mimeType);
$this->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"');
$this->setHeader('Content-Length', (string) filesize($filePath));
$this->setHeader('Cache-Control', 'no-cache, must-revalidate');
$this->setHeader('Pragma', 'no-cache');
$this->setContent(file_get_contents($filePath));
return $this;
}
public function send(): void
{
// Set status code
http_response_code($this->statusCode);
// Set content type
header('Content-Type: ' . $this->contentType);
// Set additional headers
foreach ($this->headers as $name => $value) {
header($name . ': ' . $value);
}
// Output content
echo $this->content;
exit;
}
public function notFound(string $message = 'Seite nicht gefunden'): self
{
$this->setStatusCode(404);
$this->setContent($message);
return $this;
}
public function forbidden(string $message = 'Zugriff verweigert'): self
{
$this->setStatusCode(403);
$this->setContent($message);
return $this;
}
public function unauthorized(string $message = 'Nicht autorisiert'): self
{
$this->setStatusCode(401);
$this->setContent($message);
return $this;
}
public function serverError(string $message = 'Interner Serverfehler'): self
{
$this->setStatusCode(500);
$this->setContent($message);
return $this;
}
}

108
app/Core/Router.php Normal file
View File

@@ -0,0 +1,108 @@
<?php
namespace App\Core;
class Router
{
private static ?Router $instance = null;
private array $routes = [];
private array $groups = [];
public static function getInstance(): Router
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function get(string $path, array $handler, array $options = []): void
{
$this->addRoute('GET', $path, $handler, $options);
}
public function post(string $path, array $handler, array $options = []): void
{
$this->addRoute('POST', $path, $handler, $options);
}
public function put(string $path, array $handler, array $options = []): void
{
$this->addRoute('PUT', $path, $handler, $options);
}
public function delete(string $path, array $handler, array $options = []): void
{
$this->addRoute('DELETE', $path, $handler, $options);
}
public function group(array $attributes, callable $callback): void
{
$this->groups[] = $attributes;
$callback($this);
array_pop($this->groups);
}
private function addRoute(string $method, string $path, array $handler, array $options = []): void
{
// Apply group middleware
$middleware = $options['middleware'] ?? [];
foreach ($this->groups as $group) {
if (isset($group['middleware'])) {
$middleware = array_merge($middleware, (array)$group['middleware']);
}
}
$route = [
'method' => $method,
'path' => $path,
'controller' => $handler[0],
'method' => $handler[1],
'middleware' => $middleware,
'pattern' => $this->buildPattern($path),
];
$this->routes[] = $route;
}
public function match(string $method, string $path): ?array
{
foreach ($this->routes as $route) {
if ($route['method'] !== $method) {
continue;
}
if (preg_match($route['pattern'], $path, $matches)) {
// Extract parameters
$params = [];
preg_match_all('/\{([^}]+)\}/', $route['path'], $paramNames);
for ($i = 0; $i < count($paramNames[1]); $i++) {
$paramName = $paramNames[1][$i];
$params[$paramName] = $matches[$i + 1] ?? null;
}
return [
'controller' => $route['controller'],
'method' => $route['method'],
'middleware' => $route['middleware'],
'params' => $params,
];
}
}
return null;
}
private function buildPattern(string $path): string
{
// Convert route parameters to regex pattern
$pattern = preg_replace('/\{([^}]+)\}/', '([^/]+)', $path);
return '#^' . $pattern . '$#';
}
public function getRoutes(): array
{
return $this->routes;
}
}

198
app/Core/Session.php Normal file
View File

@@ -0,0 +1,198 @@
<?php
namespace App\Core;
class Session
{
public function __construct()
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
public function set(string $key, $value): void
{
$_SESSION[$key] = $value;
}
public function get(string $key, $default = null)
{
return $_SESSION[$key] ?? $default;
}
public function has(string $key): bool
{
return isset($_SESSION[$key]);
}
public function remove(string $key): void
{
unset($_SESSION[$key]);
}
public function destroy(): void
{
session_destroy();
}
public function regenerate(): bool
{
return session_regenerate_id(true);
}
public function flash(string $key, $value): void
{
$_SESSION['flash'][$key] = $value;
}
public function getFlash(string $key, $default = null)
{
$value = $_SESSION['flash'][$key] ?? $default;
unset($_SESSION['flash'][$key]);
return $value;
}
public function hasFlash(string $key): bool
{
return isset($_SESSION['flash'][$key]);
}
public function getFlashMessages(): array
{
$messages = $_SESSION['flash'] ?? [];
unset($_SESSION['flash']);
return $messages;
}
public function setUser(array $user): void
{
$this->set('user', $user);
$this->regenerate();
}
public function getUser(): ?array
{
return $this->get('user');
}
public function isLoggedIn(): bool
{
return $this->has('user');
}
public function logout(): void
{
$this->remove('user');
$this->destroy();
}
public function getUserId(): ?int
{
$user = $this->getUser();
return $user['id'] ?? null;
}
public function getUserRole(): ?string
{
$user = $this->getUser();
return $user['role'] ?? null;
}
public function isAdmin(): bool
{
return $this->getUserRole() === 'admin';
}
public function isAuditor(): bool
{
return $this->getUserRole() === 'auditor';
}
public function isEmployee(): bool
{
return $this->getUserRole() === 'employee';
}
public function generateCsrfToken(): string
{
$token = bin2hex(random_bytes(32));
$this->set('csrf_token', $token);
$this->set('csrf_token_time', time());
return $token;
}
public function validateCsrfToken(string $token): bool
{
$storedToken = $this->get('csrf_token');
$tokenTime = $this->get('csrf_token_time', 0);
if (!$storedToken || !$tokenTime) {
return false;
}
// Check if token is expired (1 hour)
if (time() - $tokenTime > CSRF_TOKEN_LIFETIME) {
$this->remove('csrf_token');
$this->remove('csrf_token_time');
return false;
}
return hash_equals($storedToken, $token);
}
public function getCsrfToken(): string
{
$token = $this->get('csrf_token');
if (!$token) {
$token = $this->generateCsrfToken();
}
return $token;
}
public function setLocale(string $locale): void
{
$this->set('locale', $locale);
}
public function getLocale(): string
{
return $this->get('locale', 'de');
}
public function setLastActivity(): void
{
$this->set('last_activity', time());
}
public function isExpired(int $timeout = 3600): bool
{
$lastActivity = $this->get('last_activity', 0);
return (time() - $lastActivity) > $timeout;
}
public function setLoginAttempts(int $attempts): void
{
$this->set('login_attempts', $attempts);
$this->set('login_attempts_time', time());
}
public function getLoginAttempts(): int
{
$attempts = $this->get('login_attempts', 0);
$attemptsTime = $this->get('login_attempts_time', 0);
// Reset attempts if lockout time has passed
if (time() - $attemptsTime > LOGIN_LOCKOUT_TIME) {
$this->setLoginAttempts(0);
return 0;
}
return $attempts;
}
public function isLockedOut(): bool
{
return $this->getLoginAttempts() >= LOGIN_MAX_ATTEMPTS;
}
}