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:
209
app/Controllers/AuthController.php
Normal file
209
app/Controllers/AuthController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
236
app/Controllers/BaseController.php
Normal file
236
app/Controllers/BaseController.php
Normal 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, '-');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user