Version 1.5
This commit is contained in:
909
httpdocs/app/bootstrap.php
Normal file
909
httpdocs/app/bootstrap.php
Normal file
@@ -0,0 +1,909 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 0,
|
||||
'path' => '/',
|
||||
'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
}
|
||||
|
||||
session_start();
|
||||
|
||||
$localConfig = [];
|
||||
$localConfigFile = dirname(__DIR__, 2) . '/config.php';
|
||||
if (is_file($localConfigFile)) {
|
||||
$loaded = require $localConfigFile;
|
||||
if (is_array($loaded)) {
|
||||
$localConfig = $loaded;
|
||||
}
|
||||
}
|
||||
|
||||
$config = [
|
||||
'app_name' => 'TC Ingelfingen Arbeitszeiterfassung',
|
||||
'app_version' => (string)($localConfig['app_version'] ?? '1.0.0'),
|
||||
'author' => (string)($localConfig['author'] ?? 'OpenCode'),
|
||||
'copyright' => (string)($localConfig['copyright'] ?? 'Copyright 2026 TC-Ingelfingen'),
|
||||
'hours_target' => (int)($localConfig['hours_target'] ?? (getenv('HOURS_TARGET') ?: 12)),
|
||||
'hourly_rate_eur' => (float)($localConfig['hourly_rate_eur'] ?? (getenv('HOURLY_RATE_EUR') ?: 10)),
|
||||
'dashboard_info' => (string)($localConfig['dashboard_info'] ?? ''),
|
||||
];
|
||||
|
||||
$smtpConfig = [
|
||||
'host' => (string)($localConfig['smtp']['host'] ?? ''),
|
||||
'port' => (int)($localConfig['smtp']['port'] ?? 587),
|
||||
'username' => (string)($localConfig['smtp']['username'] ?? ''),
|
||||
'password' => (string)($localConfig['smtp']['password'] ?? ''),
|
||||
'encryption' => (string)($localConfig['smtp']['encryption'] ?? 'tls'),
|
||||
'from_email' => (string)($localConfig['smtp']['from_email'] ?? ''),
|
||||
'from_name' => (string)($localConfig['smtp']['from_name'] ?? $config['app_name']),
|
||||
];
|
||||
|
||||
$dbConfig = [
|
||||
'host' => (string)($localConfig['db']['host'] ?? (getenv('DB_HOST') ?: '')),
|
||||
'name' => (string)($localConfig['db']['name'] ?? (getenv('DB_NAME') ?: '')),
|
||||
'user' => (string)($localConfig['db']['user'] ?? (getenv('DB_USER') ?: '')),
|
||||
'pass' => (string)($localConfig['db']['pass'] ?? (getenv('DB_PASS') ?: '')),
|
||||
];
|
||||
|
||||
$pdo = null;
|
||||
if ($dbConfig['host'] !== '' && $dbConfig['name'] !== '' && $dbConfig['user'] !== '') {
|
||||
try {
|
||||
$dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $dbConfig['host'], $dbConfig['name']);
|
||||
$pdo = new PDO($dsn, $dbConfig['user'], $dbConfig['pass'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||
} catch (Throwable $e) {
|
||||
$pdo = null;
|
||||
}
|
||||
}
|
||||
|
||||
function money(float $value): string
|
||||
{
|
||||
return number_format($value, 2, ',', '.') . ' EUR';
|
||||
}
|
||||
|
||||
function formatDateTime(string $value): string
|
||||
{
|
||||
$timestamp = strtotime($value);
|
||||
if ($timestamp === false) {
|
||||
return $value;
|
||||
}
|
||||
return date('d.m.Y H:i', $timestamp);
|
||||
}
|
||||
|
||||
function renderSimpleMarkdown(string $text): string
|
||||
{
|
||||
$escaped = htmlspecialchars(trim($text), ENT_QUOTES, 'UTF-8');
|
||||
if ($escaped === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$escaped = preg_replace('/\*\*(.+?)\*\*/s', '<strong>$1</strong>', $escaped);
|
||||
$escaped = preg_replace('/\*(.+?)\*/s', '<em>$1</em>', $escaped);
|
||||
$blocks = preg_split('/\R{2,}/', $escaped) ?: [];
|
||||
$html = [];
|
||||
foreach ($blocks as $block) {
|
||||
$block = trim($block);
|
||||
if ($block === '') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^(?:- .*(?:\R|$))+$/', $block)) {
|
||||
$items = preg_split('/\R/', $block) ?: [];
|
||||
$listItems = [];
|
||||
foreach ($items as $item) {
|
||||
$item = trim($item);
|
||||
if (str_starts_with($item, '- ')) {
|
||||
$listItems[] = '<li>' . substr($item, 2) . '</li>';
|
||||
}
|
||||
}
|
||||
if ($listItems) {
|
||||
$html[] = '<ul>' . implode('', $listItems) . '</ul>';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$html[] = '<p>' . nl2br($block) . '</p>';
|
||||
}
|
||||
return implode('', $html);
|
||||
}
|
||||
|
||||
function appUrl(): string
|
||||
{
|
||||
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||
return $scheme . '://' . $host;
|
||||
}
|
||||
|
||||
function sendPasswordResetMail(array $smtpConfig, string $toEmail, string $resetUrl): bool
|
||||
{
|
||||
if (($smtpConfig['from_email'] ?? '') === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$subject = 'Passwort zuruecksetzen';
|
||||
$message = "Hallo,\n\n" .
|
||||
"bitte nutzen Sie den folgenden Link, um Ihr Passwort zurueckzusetzen:\n\n" .
|
||||
$resetUrl . "\n\n" .
|
||||
"Der Link ist 60 Minuten gueltig.\n";
|
||||
$headers = [
|
||||
'From: ' . ($smtpConfig['from_name'] !== '' ? $smtpConfig['from_name'] . ' <' . $smtpConfig['from_email'] . '>' : $smtpConfig['from_email']),
|
||||
'Content-Type: text/plain; charset=UTF-8',
|
||||
];
|
||||
|
||||
return mail($toEmail, $subject, $message, implode("\r\n", $headers));
|
||||
}
|
||||
|
||||
function pageTitle(string $page): string
|
||||
{
|
||||
return match ($page) {
|
||||
'login' => 'Anmeldung',
|
||||
'reset_password' => 'Passwort zuruecksetzen',
|
||||
'profile' => 'Profil',
|
||||
'members' => 'Mitglieder',
|
||||
'booking' => 'Buchung',
|
||||
'admin' => 'Vereinskonfiguration',
|
||||
'administration' => 'Administration',
|
||||
default => 'Dashboard',
|
||||
};
|
||||
}
|
||||
|
||||
function currentUser(): ?array
|
||||
{
|
||||
return $_SESSION['user'] ?? null;
|
||||
}
|
||||
|
||||
function hasRole(array $user, array $roles): bool
|
||||
{
|
||||
return in_array($user['role'], $roles, true);
|
||||
}
|
||||
|
||||
function canCreateMembers(array $user): bool
|
||||
{
|
||||
return hasRole($user, ['editor', 'admin']);
|
||||
}
|
||||
|
||||
function canManageUsers(array $user): bool
|
||||
{
|
||||
return $user['role'] === 'admin';
|
||||
}
|
||||
|
||||
function canManageMembers(array $user): bool
|
||||
{
|
||||
return hasRole($user, ['editor', 'admin']);
|
||||
}
|
||||
|
||||
function displayName(array $user): string
|
||||
{
|
||||
return trim(((string)($user['firstname'] ?? '')) . ' ' . ((string)($user['lastname'] ?? '')));
|
||||
}
|
||||
|
||||
function roleLabel(string $role): string
|
||||
{
|
||||
return match ($role) {
|
||||
'member' => 'Mitglied',
|
||||
'editor' => 'Bearbeiter',
|
||||
'admin' => 'Admin',
|
||||
default => $role,
|
||||
};
|
||||
}
|
||||
|
||||
function isInstalled(?PDO $pdo): bool
|
||||
{
|
||||
if (!$pdo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return (int)$pdo->query('SELECT COUNT(*) FROM users')->fetchColumn() > 0;
|
||||
} catch (Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function initSchema(PDO $pdo): void
|
||||
{
|
||||
$pdo->exec('CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
firstname VARCHAR(100) NOT NULL,
|
||||
lastname VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(190) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role ENUM("member", "editor", "admin") NOT NULL DEFAULT "member",
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
|
||||
|
||||
$columns = $pdo->query('SHOW COLUMNS FROM users')->fetchAll(PDO::FETCH_COLUMN);
|
||||
if (!in_array('firstname', $columns, true)) {
|
||||
$pdo->exec('ALTER TABLE users ADD COLUMN firstname VARCHAR(100) NOT NULL DEFAULT "" AFTER id');
|
||||
}
|
||||
if (!in_array('lastname', $columns, true)) {
|
||||
$pdo->exec('ALTER TABLE users ADD COLUMN lastname VARCHAR(100) NOT NULL DEFAULT "" AFTER firstname');
|
||||
}
|
||||
if (in_array('name', $columns, true)) {
|
||||
$stmt = $pdo->query('SELECT id, name, firstname, lastname FROM users');
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
if (trim((string)$row['firstname']) !== '' || trim((string)$row['lastname']) !== '') {
|
||||
continue;
|
||||
}
|
||||
$parts = preg_split('/\s+/', trim((string)$row['name'])) ?: [];
|
||||
$lastname = array_pop($parts) ?: '';
|
||||
$firstname = trim(implode(' ', $parts));
|
||||
$update = $pdo->prepare('UPDATE users SET firstname = ?, lastname = ? WHERE id = ?');
|
||||
$update->execute([$firstname, $lastname, (int)$row['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->exec('CREATE TABLE IF NOT EXISTS work_logs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
member_id INT NOT NULL,
|
||||
actor_id INT NOT NULL,
|
||||
hours DECIMAL(5,2) NOT NULL,
|
||||
note VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX (member_id),
|
||||
INDEX (actor_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
|
||||
|
||||
$pdo->exec('CREATE TABLE IF NOT EXISTS settings (
|
||||
setting_key VARCHAR(100) PRIMARY KEY,
|
||||
setting_value TEXT NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
|
||||
|
||||
$pdo->exec('CREATE TABLE IF NOT EXISTS password_resets (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
token_hash VARCHAR(255) NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX (user_id),
|
||||
INDEX (expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
|
||||
|
||||
$pdo->exec('CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
actor_id INT NOT NULL,
|
||||
action VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX (actor_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
|
||||
|
||||
$settingColumns = $pdo->query('SHOW COLUMNS FROM settings')->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($settingColumns as $settingColumn) {
|
||||
if (($settingColumn['Field'] ?? '') === 'setting_value' && stripos((string)($settingColumn['Type'] ?? ''), 'text') === false) {
|
||||
$pdo->exec('ALTER TABLE settings MODIFY setting_value TEXT NOT NULL');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($pdo) {
|
||||
initSchema($pdo);
|
||||
|
||||
$hoursTarget = $pdo->query('SELECT setting_value FROM settings WHERE setting_key = "hours_target" LIMIT 1')->fetchColumn();
|
||||
$hourlyRate = $pdo->query('SELECT setting_value FROM settings WHERE setting_key = "hourly_rate_eur" LIMIT 1')->fetchColumn();
|
||||
$dashboardInfo = $pdo->query('SELECT setting_value FROM settings WHERE setting_key = "dashboard_info" LIMIT 1')->fetchColumn();
|
||||
|
||||
if ($hoursTarget !== false) {
|
||||
$config['hours_target'] = (int)$hoursTarget;
|
||||
}
|
||||
if ($hourlyRate !== false) {
|
||||
$config['hourly_rate_eur'] = (float)$hourlyRate;
|
||||
}
|
||||
if ($dashboardInfo !== false) {
|
||||
$config['dashboard_info'] = (string)$dashboardInfo;
|
||||
}
|
||||
}
|
||||
|
||||
function fetchUsers(?PDO $pdo): array
|
||||
{
|
||||
if (!$pdo) {
|
||||
return [
|
||||
['id' => 1, 'firstname' => 'Max', 'lastname' => 'Mustermann', 'name' => 'Max Mustermann', 'email' => 'max@example.com', 'role' => 'admin', 'hours_worked' => 8.5],
|
||||
['id' => 2, 'firstname' => 'Lisa', 'lastname' => 'Beispiel', 'name' => 'Lisa Beispiel', 'email' => 'lisa@example.com', 'role' => 'editor', 'hours_worked' => 12.0],
|
||||
['id' => 3, 'firstname' => 'Tom', 'lastname' => 'Mitglied', 'name' => 'Tom Mitglied', 'email' => 'tom@example.com', 'role' => 'member', 'hours_worked' => 6.0],
|
||||
];
|
||||
}
|
||||
|
||||
return $pdo->query('SELECT u.id, u.firstname, u.lastname, CONCAT(u.firstname, " ", u.lastname) AS name, u.email, u.role, COALESCE(SUM(w.hours), 0) AS hours_worked FROM users u LEFT JOIN work_logs w ON w.member_id = u.id GROUP BY u.id, u.firstname, u.lastname, u.email, u.role ORDER BY u.lastname, u.firstname')->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
function fetchUsersPage(?PDO $pdo, int $page = 1, int $perPage = 50): array
|
||||
{
|
||||
$page = max(1, $page);
|
||||
$perPage = max(1, $perPage);
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
if (!$pdo) {
|
||||
$rows = fetchUsers($pdo);
|
||||
return [
|
||||
'entries' => array_slice($rows, $offset, $perPage),
|
||||
'total' => count($rows),
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'total_pages' => max(1, (int)ceil(count($rows) / $perPage)),
|
||||
];
|
||||
}
|
||||
|
||||
$total = (int)$pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
|
||||
$stmt = $pdo->prepare('SELECT u.id, u.firstname, u.lastname, CONCAT(u.firstname, " ", u.lastname) AS name, u.email, u.role, COALESCE(SUM(w.hours), 0) AS hours_worked FROM users u LEFT JOIN work_logs w ON w.member_id = u.id GROUP BY u.id, u.firstname, u.lastname, u.email, u.role ORDER BY u.lastname, u.firstname LIMIT ? OFFSET ?');
|
||||
$stmt->bindValue(1, $perPage, PDO::PARAM_INT);
|
||||
$stmt->bindValue(2, $offset, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
return [
|
||||
'entries' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'total_pages' => max(1, (int)ceil($total / $perPage)),
|
||||
];
|
||||
}
|
||||
|
||||
function fetchLogs(?PDO $pdo, int $page = 1, int $perPage = 50): array
|
||||
{
|
||||
$page = max(1, $page);
|
||||
$perPage = max(1, $perPage);
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
if (!$pdo) {
|
||||
$rows = [
|
||||
['ts' => '2026-05-19 14:00', 'actor' => 'Admin', 'action' => 'Lisa Beispiel als Bearbeiter angelegt'],
|
||||
['ts' => '2026-05-18 09:30', 'actor' => 'Lisa Beispiel', 'action' => '2.0 Stunden für Tom Mitglied gebucht'],
|
||||
['ts' => '2026-05-17 18:15', 'actor' => 'Admin', 'action' => 'Stundensatz auf 10 EUR gesetzt'],
|
||||
];
|
||||
return [
|
||||
'entries' => array_slice($rows, $offset, $perPage),
|
||||
'total' => count($rows),
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'total_pages' => max(1, (int)ceil(count($rows) / $perPage)),
|
||||
];
|
||||
}
|
||||
|
||||
$countSql = 'SELECT COUNT(*) FROM ((SELECT w.id FROM work_logs w) UNION ALL (SELECT l.id FROM audit_logs l)) combined_logs';
|
||||
$total = (int)$pdo->query($countSql)->fetchColumn();
|
||||
|
||||
$stmt = $pdo->prepare('(SELECT w.created_at AS ts, CONCAT(a.firstname, " ", a.lastname) AS actor, CONCAT(w.hours, " Stunden für ", m.firstname, " ", m.lastname, " gebucht: ", w.note) AS action FROM work_logs w JOIN users a ON a.id = w.actor_id JOIN users m ON m.id = w.member_id) UNION ALL (SELECT l.created_at AS ts, CONCAT(a.firstname, " ", a.lastname) AS actor, l.action FROM audit_logs l JOIN users a ON a.id = l.actor_id) ORDER BY ts DESC LIMIT ? OFFSET ?');
|
||||
$stmt->bindValue(1, $perPage, PDO::PARAM_INT);
|
||||
$stmt->bindValue(2, $offset, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
return [
|
||||
'entries' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'total_pages' => max(1, (int)ceil($total / $perPage)),
|
||||
];
|
||||
}
|
||||
|
||||
function addAuditLog(?PDO $pdo, int $actorId, string $action): void
|
||||
{
|
||||
if (!$pdo || $actorId <= 0 || trim($action) === '') {
|
||||
return;
|
||||
}
|
||||
$stmt = $pdo->prepare('INSERT INTO audit_logs (actor_id, action) VALUES (?, ?)');
|
||||
$stmt->execute([$actorId, $action]);
|
||||
}
|
||||
|
||||
function sqlValue(PDO $pdo, mixed $value): string
|
||||
{
|
||||
if ($value === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
return $pdo->quote((string)$value);
|
||||
}
|
||||
|
||||
function fetchBookings(?PDO $pdo, array $user): array
|
||||
{
|
||||
if (!$pdo) {
|
||||
$rows = [
|
||||
['id' => 1, 'ts' => '2026-05-19 14:00', 'member_id' => 3, 'member' => 'Tom Mitglied', 'actor' => 'Lisa Beispiel', 'hours' => 2.0, 'note' => 'Fruehjahrsputz'],
|
||||
['id' => 2, 'ts' => '2026-05-18 09:30', 'member_id' => 2, 'member' => 'Lisa Beispiel', 'actor' => 'Max Mustermann', 'hours' => 1.5, 'note' => 'Platzpflege'],
|
||||
['id' => 3, 'ts' => '2026-05-17 18:15', 'member_id' => 3, 'member' => 'Tom Mitglied', 'actor' => 'Max Mustermann', 'hours' => 2.5, 'note' => 'Turnierhilfe'],
|
||||
];
|
||||
if (($user['role'] ?? '') === 'member') {
|
||||
return array_values(array_filter($rows, static fn (array $row): bool => (int)$row['member_id'] === (int)($user['id'] ?? 0)));
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
if (($user['role'] ?? '') === 'member') {
|
||||
$stmt = $pdo->prepare('SELECT w.id, w.created_at AS ts, w.member_id, CONCAT(m.firstname, " ", m.lastname) AS member, CONCAT(a.firstname, " ", a.lastname) AS actor, w.hours, w.note FROM work_logs w JOIN users a ON a.id = w.actor_id JOIN users m ON m.id = w.member_id WHERE w.member_id = ? ORDER BY w.created_at DESC');
|
||||
$stmt->execute([(int)($user['id'] ?? 0)]);
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
return $pdo->query('SELECT w.id, w.created_at AS ts, w.member_id, CONCAT(m.firstname, " ", m.lastname) AS member, CONCAT(a.firstname, " ", a.lastname) AS actor, w.hours, w.note FROM work_logs w JOIN users a ON a.id = w.actor_id JOIN users m ON m.id = w.member_id ORDER BY w.created_at DESC')->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
function fetchBookingsPage(?PDO $pdo, array $user, int $page = 1, int $perPage = 50): array
|
||||
{
|
||||
$page = max(1, $page);
|
||||
$perPage = max(1, $perPage);
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
if (!$pdo) {
|
||||
$rows = fetchBookings($pdo, $user);
|
||||
return [
|
||||
'entries' => array_slice($rows, $offset, $perPage),
|
||||
'total' => count($rows),
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'total_pages' => max(1, (int)ceil(count($rows) / $perPage)),
|
||||
];
|
||||
}
|
||||
|
||||
if (($user['role'] ?? '') === 'member') {
|
||||
$countStmt = $pdo->prepare('SELECT COUNT(*) FROM work_logs WHERE member_id = ?');
|
||||
$countStmt->execute([(int)($user['id'] ?? 0)]);
|
||||
$total = (int)$countStmt->fetchColumn();
|
||||
|
||||
$stmt = $pdo->prepare('SELECT w.id, w.created_at AS ts, w.member_id, CONCAT(m.firstname, " ", m.lastname) AS member, CONCAT(a.firstname, " ", a.lastname) AS actor, w.hours, w.note FROM work_logs w JOIN users a ON a.id = w.actor_id JOIN users m ON m.id = w.member_id WHERE w.member_id = ? ORDER BY w.created_at DESC LIMIT ? OFFSET ?');
|
||||
$stmt->bindValue(1, (int)($user['id'] ?? 0), PDO::PARAM_INT);
|
||||
$stmt->bindValue(2, $perPage, PDO::PARAM_INT);
|
||||
$stmt->bindValue(3, $offset, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
} else {
|
||||
$total = (int)$pdo->query('SELECT COUNT(*) FROM work_logs')->fetchColumn();
|
||||
$stmt = $pdo->prepare('SELECT w.id, w.created_at AS ts, w.member_id, CONCAT(m.firstname, " ", m.lastname) AS member, CONCAT(a.firstname, " ", a.lastname) AS actor, w.hours, w.note FROM work_logs w JOIN users a ON a.id = w.actor_id JOIN users m ON m.id = w.member_id ORDER BY w.created_at DESC LIMIT ? OFFSET ?');
|
||||
$stmt->bindValue(1, $perPage, PDO::PARAM_INT);
|
||||
$stmt->bindValue(2, $offset, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
return [
|
||||
'entries' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'total_pages' => max(1, (int)ceil($total / $perPage)),
|
||||
];
|
||||
}
|
||||
|
||||
function findUserByEmail(?PDO $pdo, string $email): ?array
|
||||
{
|
||||
if (!$pdo) {
|
||||
$demoUsers = [
|
||||
['id' => 1, 'firstname' => 'Max', 'lastname' => 'Mustermann', 'name' => 'Max Mustermann', 'email' => 'max@example.com', 'password_hash' => password_hash('admin123', PASSWORD_DEFAULT), 'role' => 'admin'],
|
||||
['id' => 2, 'firstname' => 'Lisa', 'lastname' => 'Beispiel', 'name' => 'Lisa Beispiel', 'email' => 'lisa@example.com', 'password_hash' => password_hash('editor123', PASSWORD_DEFAULT), 'role' => 'editor'],
|
||||
['id' => 3, 'firstname' => 'Tom', 'lastname' => 'Mitglied', 'name' => 'Tom Mitglied', 'email' => 'tom@example.com', 'password_hash' => password_hash('member123', PASSWORD_DEFAULT), 'role' => 'member'],
|
||||
];
|
||||
foreach ($demoUsers as $demoUser) {
|
||||
if (strcasecmp($demoUser['email'], $email) === 0) {
|
||||
return $demoUser;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ? LIMIT 1');
|
||||
$stmt->execute([$email]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return $user ?: null;
|
||||
}
|
||||
|
||||
$error = null;
|
||||
$notice = null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
$user = currentUser();
|
||||
|
||||
if ($action === 'login') {
|
||||
$found = findUserByEmail($pdo, trim((string)($_POST['email'] ?? '')));
|
||||
if ($found && password_verify((string)($_POST['password'] ?? ''), $found['password_hash'])) {
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['user'] = ['id' => (int)$found['id'], 'firstname' => $found['firstname'] ?? '', 'lastname' => $found['lastname'] ?? '', 'name' => displayName($found), 'email' => $found['email'], 'role' => $found['role']];
|
||||
header('Location: /?page=dashboard');
|
||||
exit;
|
||||
}
|
||||
$error = 'Login fehlgeschlagen.';
|
||||
}
|
||||
|
||||
if ($action === 'request_password_reset') {
|
||||
$email = trim((string)($_POST['email'] ?? ''));
|
||||
if (!$pdo) {
|
||||
$error = 'Passwort-Reset ist ohne Datenbank nicht verfuegbar.';
|
||||
} elseif (($smtpConfig['from_email'] ?? '') === '') {
|
||||
$error = 'SMTP/Absender ist noch nicht konfiguriert.';
|
||||
} else {
|
||||
$found = findUserByEmail($pdo, $email);
|
||||
if ($found) {
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$tokenHash = password_hash($token, PASSWORD_DEFAULT);
|
||||
$expiresAt = date('Y-m-d H:i:s', time() + 3600);
|
||||
$stmt = $pdo->prepare('DELETE FROM password_resets WHERE user_id = ?');
|
||||
$stmt->execute([(int)$found['id']]);
|
||||
$stmt = $pdo->prepare('INSERT INTO password_resets (user_id, token_hash, expires_at) VALUES (?, ?, ?)');
|
||||
$stmt->execute([(int)$found['id'], $tokenHash, $expiresAt]);
|
||||
$resetUrl = appUrl() . '/?page=reset_password&token=' . urlencode($token);
|
||||
sendPasswordResetMail($smtpConfig, (string)$found['email'], $resetUrl);
|
||||
}
|
||||
$notice = 'Wenn die E-Mail-Adresse vorhanden ist, wurde ein Link zum Zuruecksetzen versendet.';
|
||||
}
|
||||
}
|
||||
|
||||
if ($action === 'reset_password') {
|
||||
$token = trim((string)($_POST['token'] ?? ''));
|
||||
$newPassword = (string)($_POST['new_password'] ?? '');
|
||||
$confirmPassword = (string)($_POST['confirm_password'] ?? '');
|
||||
if (!$pdo) {
|
||||
$error = 'Passwort-Reset ist ohne Datenbank nicht verfuegbar.';
|
||||
} elseif ($newPassword === '' || strlen($newPassword) < 6) {
|
||||
$error = 'Das neue Passwort muss mindestens 6 Zeichen lang sein.';
|
||||
} elseif ($newPassword !== $confirmPassword) {
|
||||
$error = 'Die neuen Passwoerter stimmen nicht ueberein.';
|
||||
} else {
|
||||
$stmt = $pdo->query('SELECT id, user_id, token_hash, expires_at FROM password_resets ORDER BY id DESC');
|
||||
$resetRow = null;
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $candidate) {
|
||||
if (strtotime((string)$candidate['expires_at']) < time()) {
|
||||
continue;
|
||||
}
|
||||
if (password_verify($token, (string)$candidate['token_hash'])) {
|
||||
$resetRow = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$resetRow) {
|
||||
$error = 'Der Reset-Link ist ungueltig oder abgelaufen.';
|
||||
} else {
|
||||
$stmt = $pdo->prepare('UPDATE users SET password_hash = ? WHERE id = ?');
|
||||
$stmt->execute([password_hash($newPassword, PASSWORD_DEFAULT), (int)$resetRow['user_id']]);
|
||||
$stmt = $pdo->prepare('DELETE FROM password_resets WHERE user_id = ?');
|
||||
$stmt->execute([(int)$resetRow['user_id']]);
|
||||
addAuditLog($pdo, (int)$resetRow['user_id'], 'Passwort über Reset-Link neu gesetzt.');
|
||||
$_SESSION['login_notice'] = 'Passwort erfolgreich zurueckgesetzt. Bitte anmelden.';
|
||||
header('Location: /?page=login');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($user && $action === 'create_member' && canCreateMembers($user) && $pdo) {
|
||||
$firstname = trim((string)$_POST['firstname']);
|
||||
$lastname = trim((string)$_POST['lastname']);
|
||||
$email = trim((string)$_POST['email']);
|
||||
$role = trim((string)($_POST['role'] ?? 'member'));
|
||||
if (!in_array($role, ['member', 'editor', 'admin'], true)) {
|
||||
$role = 'member';
|
||||
}
|
||||
if (!canManageUsers($user)) {
|
||||
$role = 'member';
|
||||
}
|
||||
$stmt = $pdo->prepare('INSERT INTO users (firstname, lastname, email, password_hash, role) VALUES (?, ?, ?, ?, ?)');
|
||||
$stmt->execute([$firstname, $lastname, $email, password_hash('mitglied123', PASSWORD_DEFAULT), $role]);
|
||||
addAuditLog($pdo, (int)$user['id'], trim($firstname . ' ' . $lastname) . ' (' . $email . ') als ' . roleLabel($role) . ' angelegt.');
|
||||
$notice = $role === 'admin' ? 'Benutzer als Admin angelegt.' : ($role === 'editor' ? 'Benutzer als Bearbeiter angelegt.' : 'Mitglied angelegt.');
|
||||
}
|
||||
|
||||
if ($user && $action === 'update_user_role' && canManageUsers($user) && $pdo) {
|
||||
$userId = (int)($_POST['user_id'] ?? 0);
|
||||
$role = trim((string)($_POST['role'] ?? 'member'));
|
||||
if (in_array($role, ['member', 'editor', 'admin'], true)) {
|
||||
$stmt = $pdo->prepare('SELECT firstname, lastname FROM users WHERE id = ? LIMIT 1');
|
||||
$stmt->execute([$userId]);
|
||||
$targetUser = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||
$stmt = $pdo->prepare('UPDATE users SET role = ? WHERE id = ?');
|
||||
$stmt->execute([$role, $userId]);
|
||||
if ($targetUser) {
|
||||
addAuditLog($pdo, (int)$user['id'], trim(displayName($targetUser)) . ' Rolle auf ' . roleLabel($role) . ' gesetzt.');
|
||||
}
|
||||
$notice = 'Rolle gespeichert.';
|
||||
}
|
||||
}
|
||||
|
||||
if ($user && $action === 'reset_user_password' && canManageMembers($user) && $pdo) {
|
||||
$userId = (int)($_POST['user_id'] ?? 0);
|
||||
$stmt = $pdo->prepare('SELECT firstname, lastname FROM users WHERE id = ? LIMIT 1');
|
||||
$stmt->execute([$userId]);
|
||||
$targetUser = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||
$stmt = $pdo->prepare('UPDATE users SET password_hash = ? WHERE id = ? AND role <> "admin"');
|
||||
$stmt->execute([password_hash('mitglied123', PASSWORD_DEFAULT), $userId]);
|
||||
$targetName = $targetUser ? displayName($targetUser) : 'Der Benutzer';
|
||||
addAuditLog($pdo, (int)$user['id'], trim($targetName) . ' Passwort zurückgesetzt.');
|
||||
$notice = trim($targetName) . ' wurde auf das Passwort mitglied123 zurueckgesetzt.';
|
||||
}
|
||||
|
||||
if ($user && $action === 'delete_user' && canManageMembers($user) && $pdo) {
|
||||
$userId = (int)($_POST['user_id'] ?? 0);
|
||||
$stmt = $pdo->prepare('SELECT firstname, lastname, email FROM users WHERE id = ? LIMIT 1');
|
||||
$stmt->execute([$userId]);
|
||||
$targetUser = $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||
$stmt = $pdo->prepare('DELETE FROM work_logs WHERE member_id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$stmt = $pdo->prepare('DELETE FROM users WHERE id = ? AND role <> "admin"');
|
||||
$stmt->execute([$userId]);
|
||||
if ($targetUser) {
|
||||
addAuditLog($pdo, (int)$user['id'], trim(displayName($targetUser)) . ' (' . (string)$targetUser['email'] . ') gelöscht.');
|
||||
}
|
||||
$notice = 'Benutzer geloescht.';
|
||||
}
|
||||
|
||||
if ($user && $action === 'book_hours' && hasRole($user, ['editor', 'admin']) && $pdo) {
|
||||
$stmt = $pdo->prepare('INSERT INTO work_logs (member_id, actor_id, hours, note) VALUES (?, ?, ?, ?)');
|
||||
$stmt->execute([(int)$_POST['member_id'], (int)$user['id'], (float)$_POST['hours'], trim((string)$_POST['note'])]);
|
||||
$notice = 'Stunden gebucht.';
|
||||
}
|
||||
|
||||
if ($user && $action === 'delete_booking' && ($user['role'] ?? '') === 'admin' && $pdo) {
|
||||
$bookingId = (int)($_POST['booking_id'] ?? 0);
|
||||
$stmt = $pdo->prepare('DELETE FROM work_logs WHERE id = ?');
|
||||
$stmt->execute([$bookingId]);
|
||||
$notice = 'Stundenbuchung geloescht.';
|
||||
}
|
||||
|
||||
if ($user && $action === 'update_profile') {
|
||||
$firstname = trim((string)($_POST['firstname'] ?? ''));
|
||||
$lastname = trim((string)($_POST['lastname'] ?? ''));
|
||||
|
||||
if ($firstname === '' || $lastname === '') {
|
||||
$error = 'Vorname und Nachname sind erforderlich.';
|
||||
} elseif ($pdo) {
|
||||
$stmt = $pdo->prepare('UPDATE users SET firstname = ?, lastname = ? WHERE id = ?');
|
||||
$stmt->execute([$firstname, $lastname, (int)$user['id']]);
|
||||
$_SESSION['user']['firstname'] = $firstname;
|
||||
$_SESSION['user']['lastname'] = $lastname;
|
||||
$_SESSION['user']['name'] = trim($firstname . ' ' . $lastname);
|
||||
$notice = 'Profil gespeichert.';
|
||||
} else {
|
||||
$_SESSION['user']['firstname'] = $firstname;
|
||||
$_SESSION['user']['lastname'] = $lastname;
|
||||
$_SESSION['user']['name'] = trim($firstname . ' ' . $lastname);
|
||||
$notice = 'Profil im Demo-Modus nur fuer diese Sitzung aktualisiert.';
|
||||
}
|
||||
}
|
||||
|
||||
if ($user && $action === 'change_password') {
|
||||
$currentPassword = (string)($_POST['current_password'] ?? '');
|
||||
$newPassword = (string)($_POST['new_password'] ?? '');
|
||||
$confirmPassword = (string)($_POST['confirm_password'] ?? '');
|
||||
|
||||
if ($newPassword === '' || strlen($newPassword) < 6) {
|
||||
$error = 'Das neue Passwort muss mindestens 6 Zeichen lang sein.';
|
||||
} elseif ($newPassword !== $confirmPassword) {
|
||||
$error = 'Die neuen Passwoerter stimmen nicht ueberein.';
|
||||
} elseif ($pdo) {
|
||||
$stmt = $pdo->prepare('SELECT password_hash FROM users WHERE id = ? LIMIT 1');
|
||||
$stmt->execute([(int)$user['id']]);
|
||||
$passwordHash = $stmt->fetchColumn();
|
||||
if (!$passwordHash || !password_verify($currentPassword, (string)$passwordHash)) {
|
||||
$error = 'Das aktuelle Passwort ist nicht korrekt.';
|
||||
} else {
|
||||
$stmt = $pdo->prepare('UPDATE users SET password_hash = ? WHERE id = ?');
|
||||
$stmt->execute([password_hash($newPassword, PASSWORD_DEFAULT), (int)$user['id']]);
|
||||
addAuditLog($pdo, (int)$user['id'], 'Eigenes Passwort geändert.');
|
||||
$notice = 'Passwort geaendert.';
|
||||
}
|
||||
} else {
|
||||
$demoUser = findUserByEmail($pdo, (string)$user['email']);
|
||||
if (!$demoUser || !password_verify($currentPassword, (string)$demoUser['password_hash'])) {
|
||||
$error = 'Das aktuelle Passwort ist nicht korrekt.';
|
||||
} else {
|
||||
$notice = 'Im Demo-Modus kann das Passwort nicht dauerhaft gespeichert werden.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($user && $action === 'update_settings' && hasRole($user, ['editor', 'admin'])) {
|
||||
$previousHoursTarget = (int)$config['hours_target'];
|
||||
$previousHourlyRate = (float)$config['hourly_rate_eur'];
|
||||
$previousDashboardInfo = trim((string)$config['dashboard_info']);
|
||||
$hoursTarget = max(0, (int)($_POST['hours_target'] ?? $config['hours_target']));
|
||||
$hourlyRate = max(0, (float)($_POST['hourly_rate_eur'] ?? $config['hourly_rate_eur']));
|
||||
$dashboardInfo = trim((string)($_POST['dashboard_info'] ?? $config['dashboard_info']));
|
||||
if ($pdo) {
|
||||
$stmt = $pdo->prepare('INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)');
|
||||
$stmt->execute(['hours_target', (string)$hoursTarget]);
|
||||
$stmt->execute(['hourly_rate_eur', (string)$hourlyRate]);
|
||||
$stmt->execute(['dashboard_info', $dashboardInfo]);
|
||||
$config['hours_target'] = $hoursTarget;
|
||||
$config['hourly_rate_eur'] = $hourlyRate;
|
||||
$config['dashboard_info'] = $dashboardInfo;
|
||||
if ($previousHoursTarget !== $hoursTarget || abs($previousHourlyRate - $hourlyRate) > 0.0001) {
|
||||
addAuditLog($pdo, (int)$user['id'], 'Vereinswerte gespeichert. Pflichtstunden: ' . $hoursTarget . ', Stundenwert: ' . number_format($hourlyRate, 2, ',', '.') . ' EUR.');
|
||||
}
|
||||
if ($previousDashboardInfo !== $dashboardInfo) {
|
||||
addAuditLog($pdo, (int)$user['id'], 'Informationskasten im Dashboard aktualisiert.');
|
||||
}
|
||||
$notice = 'Vereinswerte gespeichert.';
|
||||
} else {
|
||||
$error = 'Vereinswerte koennen ohne Datenbank nicht gespeichert werden.';
|
||||
}
|
||||
}
|
||||
|
||||
if ($user && $action === 'reset_all_hours' && $user['role'] === 'admin') {
|
||||
if ($pdo) {
|
||||
$pdo->exec('DELETE FROM work_logs');
|
||||
addAuditLog($pdo, (int)$user['id'], 'Alle Arbeitsstunden auf 0 zurückgesetzt.');
|
||||
$notice = 'Alle Arbeitsstunden wurden auf 0 zurueckgesetzt.';
|
||||
} else {
|
||||
$error = 'Arbeitsstunden koennen ohne Datenbank nicht zurueckgesetzt werden.';
|
||||
}
|
||||
}
|
||||
|
||||
if ($user && $action === 'export_csv' && hasRole($user, ['editor', 'admin'])) {
|
||||
if ($pdo) {
|
||||
addAuditLog($pdo, (int)$user['id'], 'Benutzerdaten als CSV exportiert.');
|
||||
$filename = 'arbeitsstunden-export-' . date('Y-m-d-His') . '.csv';
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
$output = fopen('php://output', 'wb');
|
||||
if ($output !== false) {
|
||||
fwrite($output, "\xEF\xBB\xBF");
|
||||
$stmt = $pdo->query('SELECT u.id, u.firstname, u.lastname, u.email, u.role, COALESCE(SUM(w.hours), 0) AS hours_worked, u.created_at FROM users u LEFT JOIN work_logs w ON w.member_id = u.id GROUP BY u.id, u.firstname, u.lastname, u.email, u.role, u.created_at ORDER BY u.lastname, u.firstname');
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if ($rows) {
|
||||
fputcsv($output, array_keys($rows[0]));
|
||||
foreach ($rows as $row) {
|
||||
fputcsv($output, $row);
|
||||
}
|
||||
}
|
||||
fclose($output);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
$error = 'CSV-Export ist ohne Datenbank nicht verfuegbar.';
|
||||
}
|
||||
|
||||
if ($user && $action === 'export_bookings_csv' && hasRole($user, ['editor', 'admin'])) {
|
||||
if ($pdo) {
|
||||
addAuditLog($pdo, (int)$user['id'], 'Stundenbuchungen als CSV exportiert.');
|
||||
$filename = 'stundenbuchungen-export-' . date('Y-m-d-His') . '.csv';
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
$output = fopen('php://output', 'wb');
|
||||
if ($output !== false) {
|
||||
fwrite($output, "\xEF\xBB\xBF");
|
||||
$stmt = $pdo->query('SELECT w.id, CONCAT(m.lastname, ", ", m.firstname) AS member, CONCAT(a.lastname, ", ", a.firstname) AS actor, w.hours, w.note, w.created_at FROM work_logs w JOIN users a ON a.id = w.actor_id JOIN users m ON m.id = w.member_id ORDER BY w.created_at DESC');
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
if ($rows) {
|
||||
fputcsv($output, array_keys($rows[0]));
|
||||
foreach ($rows as $row) {
|
||||
fputcsv($output, $row);
|
||||
}
|
||||
}
|
||||
fclose($output);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
$error = 'CSV-Export ist ohne Datenbank nicht verfuegbar.';
|
||||
}
|
||||
|
||||
if ($user && $action === 'export_sql_dump' && $user['role'] === 'admin') {
|
||||
if ($pdo) {
|
||||
addAuditLog($pdo, (int)$user['id'], 'SQL-Dump exportiert.');
|
||||
$filename = 'arbeitsstunden-backup-' . date('Y-m-d-His') . '.sql';
|
||||
header('Content-Type: application/sql; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
$output = fopen('php://output', 'wb');
|
||||
if ($output !== false) {
|
||||
fwrite($output, "-- Arbeitsstunden SQL Dump\n");
|
||||
fwrite($output, '-- Erstellt am ' . date('Y-m-d H:i:s') . "\n\n");
|
||||
$tables = ['users', 'work_logs', 'settings', 'audit_logs', 'password_resets'];
|
||||
foreach ($tables as $table) {
|
||||
$createStmt = $pdo->query('SHOW CREATE TABLE ' . $table)->fetch(PDO::FETCH_ASSOC);
|
||||
$createSql = $createStmt['Create Table'] ?? '';
|
||||
fwrite($output, 'DROP TABLE IF EXISTS `' . $table . '`;' . "\n");
|
||||
fwrite($output, $createSql . ';' . "\n\n");
|
||||
$rows = $pdo->query('SELECT * FROM ' . $table)->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($rows as $row) {
|
||||
$columns = array_map(static fn (string $column): string => '`' . $column . '`', array_keys($row));
|
||||
$values = array_map(static fn ($value): string => sqlValue($pdo, $value), array_values($row));
|
||||
fwrite($output, 'INSERT INTO `' . $table . '` (' . implode(', ', $columns) . ') VALUES (' . implode(', ', $values) . ');' . "\n");
|
||||
}
|
||||
fwrite($output, "\n");
|
||||
}
|
||||
fclose($output);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
$error = 'SQL-Dump ist ohne Datenbank nicht verfuegbar.';
|
||||
}
|
||||
|
||||
if ($user && $action === 'import_sql_dump' && $user['role'] === 'admin') {
|
||||
if (!$pdo) {
|
||||
$error = 'SQL-Wiederherstellung ist ohne Datenbank nicht verfuegbar.';
|
||||
} elseif (!isset($_FILES['sql_file']) || (int)($_FILES['sql_file']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||||
$error = 'Bitte eine gueltige SQL-Datei auswaehlen.';
|
||||
} else {
|
||||
$sql = file_get_contents((string)$_FILES['sql_file']['tmp_name']);
|
||||
if ($sql === false || trim($sql) === '') {
|
||||
$error = 'Die SQL-Datei konnte nicht gelesen werden.';
|
||||
} else {
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
$pdo->exec('SET FOREIGN_KEY_CHECKS=0');
|
||||
$statements = preg_split('/;\s*(?:\R|$)/', $sql) ?: [];
|
||||
foreach ($statements as $statement) {
|
||||
$statement = trim($statement);
|
||||
if ($statement === '' || str_starts_with($statement, '--')) {
|
||||
continue;
|
||||
}
|
||||
$pdo->exec($statement);
|
||||
}
|
||||
$pdo->exec('SET FOREIGN_KEY_CHECKS=1');
|
||||
$pdo->commit();
|
||||
addAuditLog($pdo, (int)$user['id'], 'SQL-Dump importiert und Datenbank wiederhergestellt.');
|
||||
$notice = 'SQL-Dump erfolgreich importiert.';
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
$pdo->exec('SET FOREIGN_KEY_CHECKS=1');
|
||||
$error = 'Die SQL-Datei konnte nicht importiert werden.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($user && $action === 'import_csv' && $user['role'] === 'admin') {
|
||||
if (!$pdo) {
|
||||
$error = 'CSV-Import ist ohne Datenbank nicht verfuegbar.';
|
||||
} elseif (!isset($_FILES['csv_file']) || (int)($_FILES['csv_file']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||||
$error = 'Bitte eine gueltige CSV-Datei auswaehlen.';
|
||||
} else {
|
||||
$handle = fopen((string)$_FILES['csv_file']['tmp_name'], 'rb');
|
||||
if ($handle === false) {
|
||||
$error = 'Die CSV-Datei konnte nicht gelesen werden.';
|
||||
} else {
|
||||
$header = fgetcsv($handle);
|
||||
$requiredColumns = ['firstname', 'lastname', 'email', 'role', 'hours_worked'];
|
||||
if (!$header || array_diff($requiredColumns, $header)) {
|
||||
$error = 'Die CSV-Datei hat nicht das erwartete Format.';
|
||||
} else {
|
||||
$columns = array_flip($header);
|
||||
$created = 0;
|
||||
$updated = 0;
|
||||
while (($row = fgetcsv($handle)) !== false) {
|
||||
$firstname = trim((string)($row[$columns['firstname']] ?? ''));
|
||||
$lastname = trim((string)($row[$columns['lastname']] ?? ''));
|
||||
$email = trim((string)($row[$columns['email']] ?? ''));
|
||||
$role = trim((string)($row[$columns['role']] ?? 'member'));
|
||||
$hoursWorked = (float)str_replace(',', '.', trim((string)($row[$columns['hours_worked']] ?? '0')));
|
||||
if ($firstname === '' || $lastname === '' || $email === '') {
|
||||
continue;
|
||||
}
|
||||
if (!in_array($role, ['member', 'editor', 'admin'], true)) {
|
||||
$role = 'member';
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('SELECT id FROM users WHERE email = ? LIMIT 1');
|
||||
$stmt->execute([$email]);
|
||||
$existingId = (int)($stmt->fetchColumn() ?: 0);
|
||||
|
||||
if ($existingId > 0) {
|
||||
$stmt = $pdo->prepare('UPDATE users SET firstname = ?, lastname = ?, role = ? WHERE id = ?');
|
||||
$stmt->execute([$firstname, $lastname, $role, $existingId]);
|
||||
$userId = $existingId;
|
||||
$updated++;
|
||||
} else {
|
||||
$stmt = $pdo->prepare('INSERT INTO users (firstname, lastname, email, password_hash, role) VALUES (?, ?, ?, ?, ?)');
|
||||
$stmt->execute([$firstname, $lastname, $email, password_hash('mitglied123', PASSWORD_DEFAULT), $role]);
|
||||
$userId = (int)$pdo->lastInsertId();
|
||||
$created++;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('DELETE FROM work_logs WHERE member_id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
if ($hoursWorked > 0) {
|
||||
$stmt = $pdo->prepare('INSERT INTO work_logs (member_id, actor_id, hours, note) VALUES (?, ?, ?, ?)');
|
||||
$stmt->execute([$userId, (int)$user['id'], $hoursWorked, 'CSV-Import']);
|
||||
}
|
||||
}
|
||||
addAuditLog($pdo, (int)$user['id'], $created . ' Benutzer importiert, ' . $updated . ' Benutzer per CSV aktualisiert.');
|
||||
$notice = $created . ' Benutzer importiert, ' . $updated . ' Benutzer aktualisiert.';
|
||||
}
|
||||
fclose($handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($user && $action === 'logout') {
|
||||
session_destroy();
|
||||
header('Location: /?page=login');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user