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

View File

@@ -0,0 +1,209 @@
<?php
namespace App\Controllers;
use App\Core\BaseController;
use App\Models\User;
use App\Models\PasswordReset;
class AuthController extends BaseController
{
public function showLogin(): string
{
if ($this->session->isLoggedIn()) {
return $this->redirect('/dashboard');
}
return $this->render('auth/login');
}
public function login(): Response
{
$email = $this->request->post('email');
$password = $this->request->post('password');
// Check if account is locked
if ($this->session->isLockedOut()) {
$this->flash('error', 'Account ist gesperrt. Bitte warten Sie 15 Minuten.');
return $this->redirect('/login');
}
// Validate input
$errors = $this->validate([
'email' => 'required|email',
'password' => 'required'
]);
if (!empty($errors)) {
foreach ($errors as $field => $fieldErrors) {
foreach ($fieldErrors as $error) {
$this->flash('error', $error);
}
}
return $this->redirect('/login');
}
// Find user
$user = (new User($this->database))->findByEmail($email);
if (!$user || !$user['active']) {
$this->incrementLoginAttempts();
$this->flash('error', 'Ungültige Anmeldedaten oder Account inaktiv.');
return $this->redirect('/login');
}
// Verify password
if (!password_verify($password, $user['passhash'])) {
$this->incrementLoginAttempts();
$this->flash('error', 'Ungültige Anmeldedaten.');
return $this->redirect('/login');
}
// Check if password needs rehash
if (password_needs_rehash($user['passhash'], PASSWORD_ARGON2ID)) {
$newHash = password_hash($password, PASSWORD_ARGON2ID);
(new User($this->database))->update($user['id'], ['passhash' => $newHash]);
}
// Reset login attempts
$this->session->setLoginAttempts(0);
// Set user session
$this->session->setUser($user);
$this->session->setLastActivity();
// Log audit
$this->logAudit('login', 'users', $user['id']);
$this->flash('success', 'Erfolgreich angemeldet.');
return $this->redirect('/dashboard');
}
public function logout(): Response
{
if ($this->session->isLoggedIn()) {
$userId = $this->session->getUserId();
$this->logAudit('logout', 'users', $userId);
}
$this->session->logout();
$this->flash('success', 'Erfolgreich abgemeldet.');
return $this->redirect('/login');
}
public function showForgotPassword(): string
{
return $this->render('auth/forgot-password');
}
public function forgotPassword(): Response
{
$email = $this->request->post('email');
$errors = $this->validate([
'email' => 'required|email'
]);
if (!empty($errors)) {
foreach ($errors as $field => $fieldErrors) {
foreach ($fieldErrors as $error) {
$this->flash('error', $error);
}
}
return $this->redirect('/password/forgot');
}
$user = (new User($this->database))->findByEmail($email);
if ($user && $user['active']) {
$token = bin2hex(random_bytes(32));
$expiresAt = date('Y-m-d H:i:s', strtotime('+1 hour'));
$passwordReset = new PasswordReset($this->database);
$passwordReset->create([
'user_id' => $user['id'],
'token' => $token,
'expires_at' => $expiresAt
]);
// Send email (in production, implement email service)
// $this->sendPasswordResetEmail($user['email'], $token);
}
// Always show success message for security
$this->flash('success', 'Falls die E-Mail-Adresse existiert, wurde ein Reset-Link gesendet.');
return $this->redirect('/login');
}
public function showResetPassword(): string
{
$token = $this->request->get('token');
if (!$token) {
$this->flash('error', 'Ungültiger Reset-Link.');
return $this->redirect('/login');
}
$passwordReset = new PasswordReset($this->database);
$reset = $passwordReset->findByToken($token);
if (!$reset || strtotime($reset['expires_at']) < time()) {
$this->flash('error', 'Reset-Link ist ungültig oder abgelaufen.');
return $this->redirect('/login');
}
return $this->render('auth/reset-password', ['token' => $token]);
}
public function resetPassword(): Response
{
$token = $this->request->post('token');
$password = $this->request->post('password');
$passwordConfirm = $this->request->post('password_confirm');
$errors = $this->validate([
'password' => 'required|min:8',
'password_confirm' => 'required'
]);
if ($password !== $passwordConfirm) {
$this->flash('error', 'Passwörter stimmen nicht überein.');
return $this->redirect('/password/reset?token=' . $token);
}
if (!empty($errors)) {
foreach ($errors as $field => $fieldErrors) {
foreach ($fieldErrors as $error) {
$this->flash('error', $error);
}
}
return $this->redirect('/password/reset?token=' . $token);
}
$passwordReset = new PasswordReset($this->database);
$reset = $passwordReset->findByToken($token);
if (!$reset || strtotime($reset['expires_at']) < time()) {
$this->flash('error', 'Reset-Link ist ungültig oder abgelaufen.');
return $this->redirect('/login');
}
// Update user password
$userModel = new User($this->database);
$userModel->update($reset['user_id'], [
'passhash' => password_hash($password, PASSWORD_ARGON2ID)
]);
// Delete used reset token
$passwordReset->delete($reset['id']);
$this->flash('success', 'Passwort erfolgreich geändert. Sie können sich jetzt anmelden.');
return $this->redirect('/login');
}
private function incrementLoginAttempts(): void
{
$attempts = $this->session->getLoginAttempts();
$this->session->setLoginAttempts($attempts + 1);
}
}

View File

@@ -0,0 +1,236 @@
<?php
namespace App\Controllers;
use App\Core\Request;
use App\Core\Response;
use App\Core\Session;
use App\Core\Database;
use App\Services\AuditService;
abstract class BaseController
{
protected Request $request;
protected Response $response;
protected Session $session;
protected Database $database;
protected AuditService $auditService;
public function setRequest(Request $request): void
{
$this->request = $request;
}
public function setResponse(Response $response): void
{
$this->response = $response;
}
public function setSession(Session $session): void
{
$this->session = $session;
}
public function setDatabase(Database $database): void
{
$this->database = $database;
$this->auditService = new AuditService($database, $session);
}
protected function render(string $view, array $data = []): string
{
// Extract data to variables
extract($data);
// Add common data
$user = $this->session->getUser();
$flashMessages = $this->session->getFlashMessages();
$csrfToken = $this->session->getCsrfToken();
$locale = $this->session->getLocale();
// Load language file
$lang = $this->loadLanguage($locale);
// Start output buffering
ob_start();
// Include the view file
$viewPath = APP_PATH . '/Views/' . $view . '.php';
if (!file_exists($viewPath)) {
throw new \Exception("View not found: {$view}");
}
require $viewPath;
// Return the buffered content
return ob_get_clean();
}
protected function renderLayout(string $view, array $data = []): string
{
$content = $this->render($view, $data);
// Add layout wrapper
$user = $this->session->getUser();
$flashMessages = $this->session->getFlashMessages();
$csrfToken = $this->session->getCsrfToken();
$locale = $this->session->getLocale();
$lang = $this->loadLanguage($locale);
ob_start();
require APP_PATH . '/Views/layouts/main.php';
return ob_get_clean();
}
protected function redirect(string $url): Response
{
return $this->response->redirect($url);
}
protected function json(array $data, int $statusCode = 200): Response
{
return $this->response->json($data, $statusCode);
}
protected function validate(array $rules): array
{
return $this->request->validate($rules);
}
protected function flash(string $key, string $message): void
{
$this->session->flash($key, $message);
}
protected function getUserId(): ?int
{
return $this->session->getUserId();
}
protected function getUserRole(): ?string
{
return $this->session->getUserRole();
}
protected function isAdmin(): bool
{
return $this->session->isAdmin();
}
protected function isAuditor(): bool
{
return $this->session->isAuditor();
}
protected function isEmployee(): bool
{
return $this->session->isEmployee();
}
protected function hasPermission(string $permission): bool
{
$role = $this->getUserRole();
switch ($permission) {
case 'admin':
return $role === 'admin';
case 'auditor':
return in_array($role, ['admin', 'auditor']);
case 'employee':
return in_array($role, ['admin', 'auditor', 'employee']);
default:
return false;
}
}
protected function paginate(string $sql, array $params = [], int $perPage = 20): array
{
$page = (int) ($this->request->get('page', 1));
$offset = ($page - 1) * $perPage;
// Get total count
$countSql = preg_replace('/SELECT .* FROM/', 'SELECT COUNT(*) as total FROM', $sql);
$countSql = preg_replace('/ORDER BY .*/', '', $countSql);
$countSql = preg_replace('/LIMIT .*/', '', $countSql);
$totalResult = $this->database->fetch($countSql, $params);
$total = $totalResult['total'] ?? 0;
// Get paginated data
$dataSql = $sql . " LIMIT {$perPage} OFFSET {$offset}";
$data = $this->database->fetchAll($dataSql, $params);
$totalPages = ceil($total / $perPage);
return [
'data' => $data,
'pagination' => [
'current_page' => $page,
'per_page' => $perPage,
'total' => $total,
'total_pages' => $totalPages,
'has_previous' => $page > 1,
'has_next' => $page < $totalPages,
'previous_page' => $page > 1 ? $page - 1 : null,
'next_page' => $page < $totalPages ? $page + 1 : null,
]
];
}
protected function logAudit(string $action, string $table, int $recordId, array $oldValue = null, array $newValue = null): void
{
$this->auditService->log($action, $table, $recordId, $oldValue, $newValue);
}
private function loadLanguage(string $locale): array
{
$langFile = LANG_PATH . '/' . $locale . '.php';
if (file_exists($langFile)) {
return require $langFile;
}
// Fallback to German
$langFile = LANG_PATH . '/de.php';
return file_exists($langFile) ? require $langFile : [];
}
protected function trans(string $key, array $params = []): string
{
$lang = $this->loadLanguage($this->session->getLocale());
$text = $lang[$key] ?? $key;
foreach ($params as $param => $value) {
$text = str_replace(':' . $param, $value, $text);
}
return $text;
}
protected function formatDate(string $date, string $format = 'd.m.Y'): string
{
return date($format, strtotime($date));
}
protected function formatDateTime(string $date, string $format = 'd.m.Y H:i'): string
{
return date($format, strtotime($date));
}
protected function formatCurrency(float $amount): string
{
return number_format($amount, 2, ',', '.') . ' €';
}
protected function sanitizeInput(string $input): string
{
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
}
protected function generateSlug(string $text): string
{
$text = strtolower($text);
$text = preg_replace('/[^a-z0-9\s-]/', '', $text);
$text = preg_replace('/[\s-]+/', '-', $text);
return trim($text, '-');
}
}

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;
}
}

378
app/Helpers/functions.php Normal file
View File

@@ -0,0 +1,378 @@
<?php
/**
* Helper Functions
*/
/**
* Escape HTML output
*/
function e(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
/**
* Generate CSRF token
*/
function csrf_token(): string
{
$session = new \App\Core\Session();
return $session->getCsrfToken();
}
/**
* Generate CSRF field
*/
function csrf_field(): string
{
return '<input type="hidden" name="csrf_token" value="' . csrf_token() . '">';
}
/**
* Format date
*/
function format_date(string $date, string $format = 'd.m.Y'): string
{
return date($format, strtotime($date));
}
/**
* Format datetime
*/
function format_datetime(string $date, string $format = 'd.m.Y H:i'): string
{
return date($format, strtotime($date));
}
/**
* Format currency
*/
function format_currency(float $amount): string
{
return number_format($amount, 2, ',', '.') . ' €';
}
/**
* Generate asset number
*/
function generate_asset_number(string $prefix = 'ASSET'): string
{
return $prefix . '-' . date('Y') . '-' . str_pad(rand(1, 9999), 4, '0', STR_PAD_LEFT);
}
/**
* Validate file upload
*/
function validate_file_upload(array $file, array $allowedTypes = [], int $maxSize = 52428800): array
{
$errors = [];
if ($file['error'] !== UPLOAD_ERR_OK) {
$errors[] = 'Upload error: ' . $file['error'];
return $errors;
}
if ($file['size'] > $maxSize) {
$errors[] = 'File too large. Maximum size: ' . format_bytes($maxSize);
}
if (!empty($allowedTypes)) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $allowedTypes)) {
$errors[] = 'File type not allowed. Allowed types: ' . implode(', ', $allowedTypes);
}
}
return $errors;
}
/**
* Format bytes to human readable
*/
function format_bytes(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
$bytes /= 1024;
}
return round($bytes, 2) . ' ' . $units[$i];
}
/**
* Generate random string
*/
function random_string(int $length = 32): string
{
return bin2hex(random_bytes($length / 2));
}
/**
* Check if user has permission
*/
function has_permission(string $permission): bool
{
$session = new \App\Core\Session();
$role = $session->getUserRole();
switch ($permission) {
case 'admin':
return $role === 'admin';
case 'auditor':
return in_array($role, ['admin', 'auditor']);
case 'employee':
return in_array($role, ['admin', 'auditor', 'employee']);
default:
return false;
}
}
/**
* Get user role name
*/
function get_role_name(string $role): string
{
$roles = [
'admin' => 'Administrator',
'auditor' => 'Auditor',
'employee' => 'Mitarbeiter'
];
return $roles[$role] ?? $role;
}
/**
* Get asset status name
*/
function get_asset_status_name(string $status): string
{
$statuses = [
'aktiv' => 'Aktiv',
'inaktiv' => 'Inaktiv',
'ausgemustert' => 'Ausgemustert'
];
return $statuses[$status] ?? $status;
}
/**
* Get asset condition name
*/
function get_asset_condition_name(string $condition): string
{
$conditions = [
'neu' => 'Neu',
'gut' => 'Gut',
'befriedigend' => 'Befriedigend',
'schlecht' => 'Schlecht',
'defekt' => 'Defekt'
];
return $conditions[$condition] ?? $condition;
}
/**
* Get inventory status name
*/
function get_inventory_status_name(string $status): string
{
$statuses = [
'offen' => 'Offen',
'abgeschlossen' => 'Abgeschlossen'
];
return $statuses[$status] ?? $status;
}
/**
* Get inventory item status name
*/
function get_inventory_item_status_name(string $status): string
{
$statuses = [
'gefunden' => 'Gefunden',
'nicht_gefunden' => 'Nicht gefunden',
'defekt' => 'Defekt',
'verschoben' => 'Verschoben'
];
return $statuses[$status] ?? $status;
}
/**
* Generate QR code data for asset
*/
function generate_qr_data(array $asset): string
{
return json_encode([
'id' => $asset['id'],
'inventarnummer' => $asset['inventarnummer'],
'bezeichnung' => $asset['bezeichnung']
]);
}
/**
* Check if warranty is expiring soon
*/
function is_warranty_expiring_soon(string $warrantyDate, int $days = 30): bool
{
if (empty($warrantyDate)) {
return false;
}
$warranty = strtotime($warrantyDate);
$now = time();
$expiring = strtotime("+{$days} days", $now);
return $warranty <= $expiring && $warranty > $now;
}
/**
* Calculate asset age in years
*/
function calculate_asset_age(string $purchaseDate): int
{
if (empty($purchaseDate)) {
return 0;
}
$purchase = new DateTime($purchaseDate);
$now = new DateTime();
$diff = $now->diff($purchase);
return $diff->y;
}
/**
* Get asset value depreciation
*/
function calculate_depreciation(float $purchasePrice, string $purchaseDate, float $depreciationRate = 0.1): float
{
$age = calculate_asset_age($purchaseDate);
$depreciation = $purchasePrice * $depreciationRate * $age;
return max(0, $purchasePrice - $depreciation);
}
/**
* Sanitize filename
*/
function sanitize_filename(string $filename): string
{
// Remove special characters
$filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $filename);
// Remove multiple underscores
$filename = preg_replace('/_+/', '_', $filename);
// Remove leading/trailing underscores
$filename = trim($filename, '_');
return $filename;
}
/**
* Get file extension from mime type
*/
function get_extension_from_mime(string $mimeType): string
{
$extensions = [
'application/pdf' => 'pdf',
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'application/msword' => 'doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
'application/vnd.ms-excel' => 'xls',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
'text/plain' => 'txt',
'text/csv' => 'csv'
];
return $extensions[$mimeType] ?? 'bin';
}
/**
* Check if string is valid JSON
*/
function is_valid_json(string $string): bool
{
json_decode($string);
return json_last_error() === JSON_ERROR_NONE;
}
/**
* Get current user
*/
function current_user(): ?array
{
$session = new \App\Core\Session();
return $session->getUser();
}
/**
* Get current user ID
*/
function current_user_id(): ?int
{
$session = new \App\Core\Session();
return $session->getUserId();
}
/**
* Check if user is logged in
*/
function is_logged_in(): bool
{
$session = new \App\Core\Session();
return $session->isLoggedIn();
}
/**
* Check if user is admin
*/
function is_admin(): bool
{
$session = new \App\Core\Session();
return $session->isAdmin();
}
/**
* Redirect to URL
*/
function redirect(string $url): void
{
header("Location: {$url}");
exit;
}
/**
* Get base URL
*/
function base_url(string $path = ''): string
{
$baseUrl = rtrim(APP_URL, '/');
$path = ltrim($path, '/');
return $baseUrl . '/' . $path;
}
/**
* Asset URL
*/
function asset_url(string $path): string
{
return base_url('assets/' . ltrim($path, '/'));
}
/**
* Storage URL
*/
function storage_url(string $path): string
{
return base_url('storage/' . ltrim($path, '/'));
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Middleware;
use App\Core\Request;
use App\Core\Response;
use App\Core\Session;
class AdminMiddleware
{
public function handle(Request $request, Response $response): void
{
$session = new Session();
// First check if user is logged in
if (!$session->isLoggedIn()) {
$session->flash('error', 'Bitte melden Sie sich an, um fortzufahren.');
$response->redirect('/login')->send();
}
// Check if user has admin role
if (!$session->isAdmin()) {
$session->flash('error', 'Sie haben keine Berechtigung für diese Aktion.');
$response->redirect('/dashboard')->send();
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Middleware;
use App\Core\Request;
use App\Core\Response;
use App\Core\Session;
class AuthMiddleware
{
public function handle(Request $request, Response $response): void
{
$session = new Session();
// Check if user is logged in
if (!$session->isLoggedIn()) {
$session->flash('error', 'Bitte melden Sie sich an, um fortzufahren.');
$response->redirect('/login')->send();
}
// Check if session is expired
if ($session->isExpired()) {
$session->logout();
$session->flash('error', 'Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.');
$response->redirect('/login')->send();
}
// Update last activity
$session->setLastActivity();
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Middleware;
use App\Core\Request;
use App\Core\Response;
use App\Core\Session;
class CsrfMiddleware
{
public function handle(Request $request, Response $response): void
{
$session = new Session();
// Skip CSRF check for GET requests
if ($request->getMethod() === 'GET') {
return;
}
// Get CSRF token from request
$token = $request->post('csrf_token') ?: $request->getHeader('X-CSRF-TOKEN');
if (!$token) {
$session->flash('error', 'CSRF-Token fehlt.');
$response->redirect('/dashboard')->send();
}
// Validate CSRF token
if (!$session->validateCsrfToken($token)) {
$session->flash('error', 'Ungültiger CSRF-Token.');
$response->redirect('/dashboard')->send();
}
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Models;
use App\Core\Database;
class PasswordReset
{
private Database $database;
public function __construct(Database $database)
{
$this->database = $database;
}
public function create(array $data): int
{
return $this->database->insert('password_resets', $data);
}
public function findByToken(string $token): ?array
{
return $this->database->fetch(
"SELECT * FROM password_resets WHERE token = :token",
['token' => $token]
);
}
public function delete(int $id): bool
{
return $this->database->delete('password_resets', 'id = :id', ['id' => $id]) > 0;
}
public function deleteExpired(): int
{
return $this->database->query(
"DELETE FROM password_resets WHERE expires_at < NOW()"
)->rowCount();
}
public function deleteByUserId(int $userId): int
{
return $this->database->delete('password_resets', 'user_id = :user_id', ['user_id' => $userId]);
}
}

204
app/Models/User.php Normal file
View File

@@ -0,0 +1,204 @@
<?php
namespace App\Models;
use App\Core\Database;
class User
{
private Database $database;
public function __construct(Database $database)
{
$this->database = $database;
}
public function find(int $id): ?array
{
return $this->database->fetch(
"SELECT * FROM users WHERE id = :id",
['id' => $id]
);
}
public function findByEmail(string $email): ?array
{
return $this->database->fetch(
"SELECT * FROM users WHERE email = :email",
['email' => $email]
);
}
public function findAll(array $filters = [], int $limit = null, int $offset = 0): array
{
$sql = "SELECT * FROM users WHERE 1=1";
$params = [];
if (!empty($filters['role'])) {
$sql .= " AND role = :role";
$params['role'] = $filters['role'];
}
if (isset($filters['active'])) {
$sql .= " AND active = :active";
$params['active'] = $filters['active'];
}
if (!empty($filters['search'])) {
$sql .= " AND (name LIKE :search OR email LIKE :search)";
$params['search'] = '%' . $filters['search'] . '%';
}
$sql .= " ORDER BY name ASC";
if ($limit) {
$sql .= " LIMIT :limit OFFSET :offset";
$params['limit'] = $limit;
$params['offset'] = $offset;
}
return $this->database->fetchAll($sql, $params);
}
public function create(array $data): int
{
$data['passhash'] = password_hash($data['password'], PASSWORD_ARGON2ID);
unset($data['password']);
$data['created_at'] = date('Y-m-d H:i:s');
$data['updated_at'] = date('Y-m-d H:i:s');
return $this->database->insert('users', $data);
}
public function update(int $id, array $data): bool
{
if (isset($data['password']) && !empty($data['password'])) {
$data['passhash'] = password_hash($data['password'], PASSWORD_ARGON2ID);
unset($data['password']);
}
$data['updated_at'] = date('Y-m-d H:i:s');
return $this->database->update('users', $data, 'id = :id', ['id' => $id]) > 0;
}
public function delete(int $id): bool
{
return $this->database->delete('users', 'id = :id', ['id' => $id]) > 0;
}
public function toggleStatus(int $id): bool
{
$user = $this->find($id);
if (!$user) {
return false;
}
$newStatus = !$user['active'];
return $this->update($id, ['active' => $newStatus]);
}
public function count(array $filters = []): int
{
$sql = "SELECT COUNT(*) as total FROM users WHERE 1=1";
$params = [];
if (!empty($filters['role'])) {
$sql .= " AND role = :role";
$params['role'] = $filters['role'];
}
if (isset($filters['active'])) {
$sql .= " AND active = :active";
$params['active'] = $filters['active'];
}
$result = $this->database->fetch($sql, $params);
return $result['total'] ?? 0;
}
public function getActiveUsers(): array
{
return $this->findAll(['active' => true]);
}
public function getUsersByRole(string $role): array
{
return $this->findAll(['role' => $role]);
}
public function searchUsers(string $search): array
{
return $this->findAll(['search' => $search]);
}
public function updateLastLogin(int $id): bool
{
return $this->update($id, ['last_login' => date('Y-m-d H:i:s')]);
}
public function changePassword(int $id, string $newPassword): bool
{
return $this->update($id, ['password' => $newPassword]);
}
public function getUsersWithStats(): array
{
$sql = "SELECT
u.*,
COUNT(DISTINCT a.id) as asset_count,
COUNT(DISTINCT al.id) as audit_count
FROM users u
LEFT JOIN assets a ON u.id = a.created_by
LEFT JOIN audit_log al ON u.id = al.user_id
GROUP BY u.id
ORDER BY u.name ASC";
return $this->database->fetchAll($sql);
}
public function getRecentActivity(int $userId, int $limit = 10): array
{
$sql = "SELECT al.*, a.inventarnummer, a.bezeichnung
FROM audit_log al
LEFT JOIN assets a ON al.table_name = 'assets' AND al.record_id = a.id
WHERE al.user_id = :user_id
ORDER BY al.created_at DESC
LIMIT :limit";
return $this->database->fetchAll($sql, [
'user_id' => $userId,
'limit' => $limit
]);
}
public function validateEmail(string $email, int $excludeId = null): bool
{
$sql = "SELECT COUNT(*) as total FROM users WHERE email = :email";
$params = ['email' => $email];
if ($excludeId) {
$sql .= " AND id != :exclude_id";
$params['exclude_id'] = $excludeId;
}
$result = $this->database->fetch($sql, $params);
return ($result['total'] ?? 0) === 0;
}
public function getRoles(): array
{
return [
'admin' => 'Administrator',
'auditor' => 'Auditor',
'employee' => 'Mitarbeiter'
];
}
public function getRoleName(string $role): string
{
$roles = $this->getRoles();
return $roles[$role] ?? $role;
}
}

View File

@@ -0,0 +1,200 @@
<?php
namespace App\Services;
use App\Core\Database;
use App\Core\Session;
class AuditService
{
private Database $database;
private Session $session;
public function __construct(Database $database, Session $session)
{
$this->database = $database;
$this->session = $session;
}
public function log(string $action, string $table, int $recordId, array $oldValue = null, array $newValue = null): void
{
$userId = $this->session->getUserId();
if (!$userId) {
return; // Skip logging if no user
}
$data = [
'user_id' => $userId,
'action' => $action,
'table_name' => $table,
'record_id' => $recordId,
'old_value' => $oldValue ? json_encode($oldValue) : null,
'new_value' => $newValue ? json_encode($newValue) : null,
'created_at' => date('Y-m-d H:i:s')
];
try {
$this->database->insert('audit_log', $data);
} catch (\Exception $e) {
error_log('Audit logging failed: ' . $e->getMessage());
}
}
public function getAuditLog(string $table = null, int $recordId = null, int $limit = 100): array
{
$sql = "SELECT al.*, u.name as user_name
FROM audit_log al
LEFT JOIN users u ON al.user_id = u.id
WHERE 1=1";
$params = [];
if ($table) {
$sql .= " AND al.table_name = :table";
$params['table'] = $table;
}
if ($recordId) {
$sql .= " AND al.record_id = :record_id";
$params['record_id'] = $recordId;
}
$sql .= " ORDER BY al.created_at DESC LIMIT :limit";
$params['limit'] = $limit;
return $this->database->fetchAll($sql, $params);
}
public function getAssetHistory(int $assetId): array
{
return $this->getAuditLog('assets', $assetId);
}
public function getUserActivity(int $userId, int $limit = 50): array
{
$sql = "SELECT al.*, u.name as user_name
FROM audit_log al
LEFT JOIN users u ON al.user_id = u.id
WHERE al.user_id = :user_id
ORDER BY al.created_at DESC
LIMIT :limit";
return $this->database->fetchAll($sql, [
'user_id' => $userId,
'limit' => $limit
]);
}
public function getRecentActivity(int $limit = 20): array
{
$sql = "SELECT al.*, u.name as user_name
FROM audit_log al
LEFT JOIN users u ON al.user_id = u.id
ORDER BY al.created_at DESC
LIMIT :limit";
return $this->database->fetchAll($sql, ['limit' => $limit]);
}
public function getActivityByDateRange(string $startDate, string $endDate): array
{
$sql = "SELECT al.*, u.name as user_name
FROM audit_log al
LEFT JOIN users u ON al.user_id = u.id
WHERE DATE(al.created_at) BETWEEN :start_date AND :end_date
ORDER BY al.created_at DESC";
return $this->database->fetchAll($sql, [
'start_date' => $startDate,
'end_date' => $endDate
]);
}
public function getActivityByAction(string $action, int $limit = 100): array
{
$sql = "SELECT al.*, u.name as user_name
FROM audit_log al
LEFT JOIN users u ON al.user_id = u.id
WHERE al.action = :action
ORDER BY al.created_at DESC
LIMIT :limit";
return $this->database->fetchAll($sql, [
'action' => $action,
'limit' => $limit
]);
}
public function getAssetChanges(int $assetId): array
{
$sql = "SELECT al.*, u.name as user_name
FROM audit_log al
LEFT JOIN users u ON al.user_id = u.id
WHERE al.table_name = 'assets'
AND al.record_id = :asset_id
AND al.action IN ('update', 'delete')
ORDER BY al.created_at DESC";
return $this->database->fetchAll($sql, ['asset_id' => $assetId]);
}
public function getInventoryActivity(int $inventoryId): array
{
$sql = "SELECT al.*, u.name as user_name
FROM audit_log al
LEFT JOIN users u ON al.user_id = u.id
WHERE al.table_name = 'inventories'
AND al.record_id = :inventory_id
ORDER BY al.created_at DESC";
return $this->database->fetchAll($sql, ['inventory_id' => $inventoryId]);
}
public function cleanupOldLogs(int $daysToKeep = 365): int
{
$sql = "DELETE FROM audit_log
WHERE created_at < DATE_SUB(NOW(), INTERVAL :days DAY)";
return $this->database->query($sql, ['days' => $daysToKeep])->rowCount();
}
public function getAuditSummary(): array
{
$sql = "SELECT
COUNT(*) as total_entries,
COUNT(DISTINCT user_id) as unique_users,
COUNT(DISTINCT table_name) as tables_affected,
MIN(created_at) as first_entry,
MAX(created_at) as last_entry
FROM audit_log";
return $this->database->fetch($sql) ?: [];
}
public function getTopUsers(int $limit = 10): array
{
$sql = "SELECT
u.name as user_name,
COUNT(*) as activity_count
FROM audit_log al
LEFT JOIN users u ON al.user_id = u.id
GROUP BY al.user_id, u.name
ORDER BY activity_count DESC
LIMIT :limit";
return $this->database->fetchAll($sql, ['limit' => $limit]);
}
public function getMostActiveTables(int $limit = 10): array
{
$sql = "SELECT
table_name,
COUNT(*) as activity_count
FROM audit_log
GROUP BY table_name
ORDER BY activity_count DESC
LIMIT :limit";
return $this->database->fetchAll($sql, ['limit' => $limit]);
}
}

42
app/Views/auth/login.php Normal file
View File

@@ -0,0 +1,42 @@
<div class="login-container">
<div class="login-box">
<div class="login-header">
<h2><?= $lang['login_title'] ?></h2>
<p>Asset Management System</p>
</div>
<form method="POST" action="/login" class="login-form">
<input type="hidden" name="csrf_token" value="<?= $csrfToken ?>">
<div class="form-group">
<label for="email"><?= $lang['login_email'] ?></label>
<input type="email" id="email" name="email" required class="form-control">
</div>
<div class="form-group">
<label for="password"><?= $lang['login_password'] ?></label>
<input type="password" id="password" name="password" required class="form-control">
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" name="remember" value="1">
<span class="checkmark"></span>
<?= $lang['login_remember'] ?>
</label>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary btn-block">
<?= $lang['login_submit'] ?>
</button>
</div>
</form>
<div class="login-footer">
<a href="/password/forgot" class="forgot-password">
<?= $lang['forgot_password'] ?>
</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="<?= $locale ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $lang['dashboard'] ?> - Asset Management System</title>
<!-- CSS -->
<link rel="stylesheet" href="/assets/css/style.css">
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/assets/img/favicon.ico">
<!-- Security Headers -->
<meta http-equiv="X-Content-Type-Options" content="nosniff">
<meta http-equiv="X-Frame-Options" content="SAMEORIGIN">
<meta http-equiv="X-XSS-Protection" content="1; mode=block">
</head>
<body>
<!-- Header -->
<header class="header">
<div class="container">
<div class="header-content">
<div class="logo">
<h1>Asset Management</h1>
</div>
<?php if ($user): ?>
<nav class="nav">
<ul class="nav-list">
<li><a href="/dashboard" class="nav-link"><?= $lang['dashboard'] ?></a></li>
<li><a href="/assets" class="nav-link"><?= $lang['assets'] ?></a></li>
<li><a href="/inventories" class="nav-link"><?= $lang['inventory'] ?></a></li>
<li><a href="/reports" class="nav-link"><?= $lang['reports'] ?></a></li>
<?php if ($user['role'] === 'admin'): ?>
<li class="nav-dropdown">
<a href="#" class="nav-link"><?= $lang['master_data'] ?></a>
<ul class="nav-dropdown-menu">
<li><a href="/categories"><?= $lang['categories_nav'] ?></a></li>
<li><a href="/locations"><?= $lang['locations_nav'] ?></a></li>
<li><a href="/departments"><?= $lang['departments_nav'] ?></a></li>
</ul>
</li>
<li><a href="/users" class="nav-link"><?= $lang['users'] ?></a></li>
<?php endif; ?>
</ul>
</nav>
<div class="user-menu">
<span class="user-name"><?= htmlspecialchars($user['name']) ?></span>
<div class="user-dropdown">
<a href="#" class="user-link"><?= $user['name'] ?></a>
<ul class="user-dropdown-menu">
<li><a href="/settings"><?= $lang['settings'] ?></a></li>
<li><a href="/logout"><?= $lang['logout'] ?></a></li>
</ul>
</div>
</div>
<?php endif; ?>
</div>
</div>
</header>
<!-- Main Content -->
<main class="main">
<div class="container">
<!-- Flash Messages -->
<?php if (!empty($flashMessages)): ?>
<div class="flash-messages">
<?php foreach ($flashMessages as $type => $message): ?>
<div class="alert alert-<?= $type ?>">
<?= htmlspecialchars($message) ?>
<button type="button" class="alert-close" onclick="this.parentElement.remove()">&times;</button>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Page Content -->
<div class="content">
<?= $content ?>
</div>
</div>
</main>
<!-- Footer -->
<footer class="footer">
<div class="container">
<p>&copy; <?= date('Y') ?> Asset Management System. Alle Rechte vorbehalten.</p>
</div>
</footer>
<!-- JavaScript -->
<script src="/assets/js/app.js"></script>
</body>
</html>