commit a707d036b84897d4fef78dcad58b677909845149 Author: root Date: Tue Aug 25 14:28:58 2026 +0200 🚀 Initial release: CorePlan v1.0 Community Edition diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f2509b3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# Wir nutzen ein schlankes, offizielles PHP 8.2 Image mit Apache +FROM php:8.2-apache + +# Aktiviere Apache Rewrite und SSL +RUN a2enmod rewrite ssl + +# ERLAUBE .htaccess OVERRIDES +RUN sed -i 's/AllowOverride None/AllowOverride All/g' /etc/apache2/apache2.conf + +# Installiere SQLite, PDO und OpenSSL +RUN apt-get update && apt-get install -y libsqlite3-dev openssl \ + && docker-php-ext-install pdo pdo_sqlite \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Generiere ein Self-Signed Zertifikat (Gebrandet auf Tessmann Digital) +# Die Pfade entsprechen genau denen, die Apache in der default-ssl.conf erwartet +RUN openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout /etc/ssl/private/ssl-cert-snakeoil.key \ + -out /etc/ssl/certs/ssl-cert-snakeoil.pem \ + -subj "/C=DE/ST=Brandenburg/L=Gruenheide/O=Tessmann Digital/CN=localhost" + +# Aktiviere den virtuellen SSL-Host von Apache +RUN a2ensite default-ssl + +# Setze das Arbeitsverzeichnis +WORKDIR /var/www/html \ No newline at end of file diff --git a/data/core.sqlite b/data/core.sqlite new file mode 100755 index 0000000..50c3a3f Binary files /dev/null and b/data/core.sqlite differ diff --git a/data/php_errors.log b/data/php_errors.log new file mode 100644 index 0000000..95edd16 --- /dev/null +++ b/data/php_errors.log @@ -0,0 +1,8 @@ +[25-Aug-2026 09:59:23 UTC] Exception: SQLSTATE[HY000]: General error: 1 no such column: closed_at in /var/www/html/projects.php on line 96 +[25-Aug-2026 09:59:29 UTC] Exception: SQLSTATE[HY000]: General error: 1 no such column: closed_at in /var/www/html/projects.php on line 96 +[25-Aug-2026 09:59:35 UTC] Exception: SQLSTATE[HY000]: General error: 1 no such column: closed_at in /var/www/html/projects.php on line 96 +[25-Aug-2026 10:01:24 UTC] Exception: SQLSTATE[HY000]: General error: 1 no such column: closed_at in /var/www/html/projects.php on line 96 +[25-Aug-2026 10:29:45 UTC] Error [2]: Cannot modify header information - headers already sent by (output started at /var/www/html/auth.php:1) in /var/www/html/router.php on line 6 +[25-Aug-2026 10:29:45 UTC] Exception: Cannot modify header information - headers already sent by (output started at /var/www/html/auth.php:1) in /var/www/html/router.php on line 6 +[25-Aug-2026 10:29:52 UTC] Error [2]: Cannot modify header information - headers already sent by (output started at /var/www/html/auth.php:1) in /var/www/html/router.php on line 6 +[25-Aug-2026 10:29:52 UTC] Exception: Cannot modify header information - headers already sent by (output started at /var/www/html/auth.php:1) in /var/www/html/router.php on line 6 diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..fcbd69d --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,13 @@ +version: '3.8' + +services: + coretemplate: + build: . + container_name: coreplan_app + ports: + # Außen 8443, intern 443 (HTTPS) + - "8443:443" + volumes: + - ./src:/var/www/html + - ./data:/var/www/data + restart: unless-stopped \ No newline at end of file diff --git a/src/.htaccess b/src/.htaccess new file mode 100644 index 0000000..307cde9 --- /dev/null +++ b/src/.htaccess @@ -0,0 +1,4 @@ +RewriteEngine On +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.*)$ index.php [QSA,L] \ No newline at end of file diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..a8c5903 --- /dev/null +++ b/src/app.html @@ -0,0 +1,437 @@ + + + + + + CorePlan + + + + +
+

CorePlan Login

+
Login fehlgeschlagen.
+
+
+
+ +
+
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/auth.php b/src/auth.php new file mode 100644 index 0000000..ac29743 --- /dev/null +++ b/src/auth.php @@ -0,0 +1,162 @@ +prepare("SELECT * FROM users WHERE username = ?"); + $stmt->execute([$username]); + $user = $stmt->fetch(); + + if ($user && password_verify($password, $user['password_hash'])) { + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + $_SESSION['role'] = $user['role']; + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$user['id'], $user['username'], 'User Login', $_SERVER['REMOTE_ADDR']]); + echo json_encode(['success' => true]); + } else { echo json_encode(['success' => false]); } + exit; +} + +function handleLogout() { + global $pdo; @session_start(); + if (isset($_SESSION['user_id'])) { $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'User Logout', 'System']); } + session_destroy(); echo json_encode(['success' => true]); exit; +} + +function handleGetLogs() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + $stmt = $pdo->query("SELECT * FROM audit_logs ORDER BY id DESC LIMIT 50"); + echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]); exit; +} + +function handleGetSettings() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + $stmt = $pdo->query("SELECT * FROM settings ORDER BY config_key ASC"); + echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]); exit; +} + +function handleSaveSettings() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + $key = trim($_POST['config_key'] ?? ''); $value = trim($_POST['config_value'] ?? ''); + if ($key === '') { echo json_encode(['success' => false, 'error' => 'Schlüssel darf nicht leer sein.']); exit; } + $pdo->prepare("REPLACE INTO settings (config_key, config_value) VALUES (?, ?)")->execute([$key, $value]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'Setting Update', "Key: $key"]); + echo json_encode(['success' => true]); exit; +} + +function handleDeleteSetting() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + $key = trim($_POST['config_key'] ?? ''); + $pdo->prepare("DELETE FROM settings WHERE config_key = ?")->execute([$key]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'Setting gelöscht', "Key: $key"]); + echo json_encode(['success' => true]); exit; +} + +function handleChangePassword() { + global $pdo; @session_start(); + $userId = $_SESSION['user_id'] ?? 0; if (!$userId) { http_response_code(401); exit; } + $oldPass = $_POST['old_password'] ?? ''; $newPass = $_POST['new_password'] ?? ''; + $stmt = $pdo->prepare("SELECT password_hash FROM users WHERE id = ?"); $stmt->execute([$userId]); $user = $stmt->fetch(); + if ($user && password_verify($oldPass, $user['password_hash'])) { + if (strlen($newPass) < 4) { echo json_encode(['success' => false, 'error' => 'Min 4 Zeichen.']); exit; } + $newHash = password_hash($newPass, PASSWORD_DEFAULT); + $pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?")->execute([$newHash, $userId]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$userId, $_SESSION['username'], 'Passwort geändert', 'Self-Service']); + echo json_encode(['success' => true]); + } else { echo json_encode(['success' => false, 'error' => 'Altes Passwort falsch.']); } + exit; +} + +function handleGetUsers() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + $stmt = $pdo->query("SELECT id, username, email, role FROM users ORDER BY id ASC"); + echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]); exit; +} + +function handleCreateUser() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + + $username = trim($_POST['username'] ?? ''); + $email = trim($_POST['email'] ?? ''); + $password = $_POST['password'] ?? ''; + $role = $_POST['role'] ?? 'user'; + if (!in_array($role, ['admin', 'pm', 'user'])) $role = 'user'; + + try { + $hash = password_hash($password, PASSWORD_DEFAULT); + $pdo->prepare("INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, ?)")->execute([$username, $email, $hash, $role]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'User erstellt', "User: $username ($role)"]); + echo json_encode(['success' => true]); + } catch (PDOException $e) { + echo json_encode(['success' => false, 'error' => 'Fehler (evtl. existiert der Name schon).']); + } + exit; +} + +// --- NEU: USER BEARBEITEN --- +function handleEditUser() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + + $id = $_POST['id'] ?? 0; + $username = trim($_POST['username'] ?? ''); + $email = trim($_POST['email'] ?? ''); + $role = $_POST['role'] ?? 'user'; + $password = $_POST['password'] ?? ''; // Optional + + if (!in_array($role, ['admin', 'pm', 'user'])) $role = 'user'; + + try { + if ($password !== '') { + $hash = password_hash($password, PASSWORD_DEFAULT); + $pdo->prepare("UPDATE users SET username = ?, email = ?, role = ?, password_hash = ? WHERE id = ?")->execute([$username, $email, $role, $hash, $id]); + } else { + $pdo->prepare("UPDATE users SET username = ?, email = ?, role = ? WHERE id = ?")->execute([$username, $email, $role, $id]); + } + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'User bearbeitet', "User ID: $id ($username)"]); + echo json_encode(['success' => true]); + } catch (PDOException $e) { + echo json_encode(['success' => false, 'error' => 'Fehler beim Speichern (evtl. Benutzername schon vergeben).']); + } + exit; +} + +function handleDeleteUser() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + $deleteId = $_POST['id'] ?? 0; + if ($deleteId == $_SESSION['user_id']) { echo json_encode(['success' => false, 'error' => 'Du kannst dich nicht selbst löschen.']); exit; } + $pdo->prepare("DELETE FROM users WHERE id = ?")->execute([$deleteId]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'User gelöscht', "User ID: $deleteId"]); + echo json_encode(['success' => true]); exit; +} + +function handleTestSmtp() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + + $stmt = $pdo->prepare("SELECT email FROM users WHERE id = ?"); $stmt->execute([$_SESSION['user_id']]); + $user = $stmt->fetch(); + if(!$user || empty($user['email'])) { echo json_encode(['success' => false, 'error' => 'Dein Admin-Account hat keine E-Mail-Adresse. Bitte bearbeite deinen Account unter "Benutzer".']); exit; } + + $res = sendSmtpMail($user['email'], "CorePlan SMTP Test", "Hallo!\n\nDein CorePlan SMTP-Server (SSL/TLS) funktioniert einwandfrei.\n\nBeste Grüße,\nCorePlan System"); + if($res === true) echo json_encode(['success' => true]); else echo json_encode(['success' => false, 'error' => $res]); + exit; +} + +function handleTestGotify() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + $res = sendGotify("Test Alarm", "Hallo Admin, Gotify Kommunikation funktioniert!"); + if($res === true) echo json_encode(['success' => true]); else echo json_encode(['success' => false, 'error' => $res]); + exit; +} \ No newline at end of file diff --git a/src/cron.php b/src/cron.php new file mode 100644 index 0000000..3b22bf1 --- /dev/null +++ b/src/cron.php @@ -0,0 +1,29 @@ +query("SELECT p.title, p.deadline, u.email, u.username FROM projects p JOIN users u ON p.assigned_to = u.id WHERE p.status = 'active' AND p.deadline = '$targetDate'"); +while($row = $stmt->fetch()) { + if(!empty($row['email'])) { + sendSmtpMail($row['email'], "Erinnerung: Projekt '{$row['title']}'", "Hallo {$row['username']},\n\ndas Projekt '{$row['title']}' ist in genau 7 Tagen fällig ({$row['deadline']}).\n\nBitte prüfe die offenen Tasks in CorePlan.\n\nDein CorePlan System"); + } + sendGotify("Projekt-Deadline", "Projekt '{$row['title']}' (Zugewiesen an: {$row['username']}) ist in 7 Tagen fällig."); +} + +// 2. Tasks (Fällig in 7 Tagen) +$stmt = $pdo->query("SELECT t.title, t.deadline, p.title as proj_title, u.email, u.username FROM tasks t JOIN projects p ON t.project_id = p.id JOIN users u ON t.assigned_to = u.id WHERE t.is_completed = 0 AND p.status = 'active' AND t.deadline = '$targetDate'"); +while($row = $stmt->fetch()) { + if(!empty($row['email'])) { + sendSmtpMail($row['email'], "Erinnerung: To-Do '{$row['title']}'", "Hallo {$row['username']},\n\ndie Aufgabe '{$row['title']}' im Projekt '{$row['proj_title']}' ist in 7 Tagen fällig ({$row['deadline']}).\n\nDein CorePlan System"); + } + // Gotify auch für Tasks, falls gewünscht. +} + +echo "Cronjob erfolgreich beendet.\n"; \ No newline at end of file diff --git a/src/database.php b/src/database.php new file mode 100644 index 0000000..a9691a3 --- /dev/null +++ b/src/database.php @@ -0,0 +1,42 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + + $pdo->exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT DEFAULT 'user')"); + // Auto-Migration Users + try { $pdo->exec("ALTER TABLE users ADD COLUMN email TEXT"); } catch (PDOException $e) {} + + $pdo->exec("CREATE TABLE IF NOT EXISTS audit_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, username TEXT, action TEXT, target TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP)"); + $pdo->exec("CREATE TABLE IF NOT EXISTS settings (config_key TEXT PRIMARY KEY, config_value TEXT NOT NULL)"); + + $pdo->exec("CREATE TABLE IF NOT EXISTS projects (id INTEGER PRIMARY KEY AUTOINCREMENT, client_id INTEGER DEFAULT 0, title TEXT NOT NULL, description TEXT, deadline DATETIME, status TEXT DEFAULT 'active', closed_at DATETIME, last_activity DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"); + + // Auto-Migration Projekte + try { $pdo->exec("ALTER TABLE projects ADD COLUMN closed_at DATETIME"); } catch (PDOException $e) {} + try { $pdo->exec("ALTER TABLE projects ADD COLUMN close_comment TEXT"); } catch (PDOException $e) {} + try { $pdo->exec("ALTER TABLE projects ADD COLUMN briefing TEXT"); } catch (PDOException $e) {} + try { $pdo->exec("ALTER TABLE projects ADD COLUMN assigned_to INTEGER"); } catch (PDOException $e) {} + + $pdo->exec("CREATE TABLE IF NOT EXISTS tasks (id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, title TEXT NOT NULL, is_completed INTEGER DEFAULT 0, completed_by INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP)"); + + // Auto-Migration Tasks + try { $pdo->exec("ALTER TABLE tasks ADD COLUMN deadline DATETIME"); } catch (PDOException $e) {} + try { $pdo->exec("ALTER TABLE tasks ADD COLUMN completed_at DATETIME"); } catch (PDOException $e) {} + try { $pdo->exec("ALTER TABLE tasks ADD COLUMN priority INTEGER DEFAULT 2"); } catch (PDOException $e) {} + try { $pdo->exec("ALTER TABLE tasks ADD COLUMN phase TEXT DEFAULT 'Standard'"); } catch (PDOException $e) {} + try { $pdo->exec("ALTER TABLE tasks ADD COLUMN completion_comment TEXT"); } catch (PDOException $e) {} + try { $pdo->exec("ALTER TABLE tasks ADD COLUMN assigned_to INTEGER"); } catch (PDOException $e) {} + + $stmt = $pdo->query("SELECT COUNT(*) FROM users"); + if ($stmt->fetchColumn() == 0) { + $hash = password_hash('admin', PASSWORD_DEFAULT); + // Admin ohne Mail initialisieren, kann später gelöscht/neu angelegt werden + $pdo->prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)")->execute(['admin', $hash, 'admin']); + } +} catch (PDOException $e) { + die(json_encode(['success' => false, 'error' => 'Datenbankfehler: ' . $e->getMessage()])); +} \ No newline at end of file diff --git a/src/index.php b/src/index.php new file mode 100644 index 0000000..954ecd8 --- /dev/null +++ b/src/index.php @@ -0,0 +1,36 @@ +getMessage() . " in " . $e->getFile() . " on line " . $e->getLine()); + $request = $_SERVER['REQUEST_URI'] ?? ''; + if (strpos($request, '/api/') === 0) { + http_response_code(500); + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'error' => 'Interner Systemfehler. Bitte Logs prüfen.']); + } else { + http_response_code(500); + echo "

500 - Systemfehler

"; + } + exit; +}); + +header('Content-Type: application/json'); + +require_once 'database.php'; +require_once 'mailer.php'; // NEU +require_once 'auth.php'; +require_once 'projects.php'; +require_once 'router.php'; \ No newline at end of file diff --git a/src/mailer.php b/src/mailer.php new file mode 100644 index 0000000..7a1d350 --- /dev/null +++ b/src/mailer.php @@ -0,0 +1,72 @@ +query("SELECT * FROM settings WHERE config_key IN ('SMTP_HOST', 'SMTP_PORT', 'SMTP_USER', 'SMTP_PASS', 'SMTP_FROM')"); + $s = []; while($r = $stmt->fetch()) $s[$r['config_key']] = $r['config_value']; + + $host = $s['SMTP_HOST'] ?? ''; + $port = $s['SMTP_PORT'] ?? 587; + $user = $s['SMTP_USER'] ?? ''; + $pass = $s['SMTP_PASS'] ?? ''; + $from = $s['SMTP_FROM'] ?? ''; + + if(!$host || !$from) return "SMTP nicht vollständig konfiguriert."; + + // SMTPS (Port 465) nutzt direkt SSL, Port 587 nutzt STARTTLS + $isSSL = ($port == 465); + $socketHost = $isSSL ? "ssl://$host" : $host; + + $socket = @fsockopen($socketHost, $port, $errno, $errstr, 10); + if(!$socket) return "Verbindung fehlgeschlagen: $errstr ($errno)"; + + $res = fread($socket, 1024); + fputs($socket, "EHLO $host\r\n"); $res = fread($socket, 1024); + + if (!$isSSL && $port != 25) { + fputs($socket, "STARTTLS\r\n"); $res = fread($socket, 1024); + stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); + fputs($socket, "EHLO $host\r\n"); $res = fread($socket, 1024); + } + + if($user && $pass) { + fputs($socket, "AUTH LOGIN\r\n"); $res = fread($socket, 1024); + fputs($socket, base64_encode($user)."\r\n"); $res = fread($socket, 1024); + fputs($socket, base64_encode($pass)."\r\n"); $res = fread($socket, 1024); + if(substr($res, 0, 3) !== '235') return "Auth fehlgeschlagen: $res"; + } + + fputs($socket, "MAIL FROM: <$from>\r\n"); fread($socket, 1024); + fputs($socket, "RCPT TO: <$to>\r\n"); fread($socket, 1024); + fputs($socket, "DATA\r\n"); fread($socket, 1024); + + $headers = "From: CorePlan <$from>\r\n"; + $headers .= "To: <$to>\r\n"; + $headers .= "Subject: =?UTF-8?B?".base64_encode($subject)."?=\r\n"; + $headers .= "MIME-Version: 1.0\r\n"; + $headers .= "Content-Type: text/plain; charset=UTF-8\r\n"; + + fputs($socket, $headers . "\r\n" . $message . "\r\n.\r\n"); + $res = fread($socket, 1024); + fputs($socket, "QUIT\r\n"); fclose($socket); + + if(substr($res, 0, 3) !== '250') return "Senden fehlgeschlagen: $res"; + return true; +} + +function sendGotify($title, $message) { + global $pdo; + $stmt = $pdo->query("SELECT * FROM settings WHERE config_key IN ('GOTIFY_URL', 'GOTIFY_TOKEN')"); + $s = []; while($r = $stmt->fetch()) $s[$r['config_key']] = $r['config_value']; + if(empty($s['GOTIFY_URL']) || empty($s['GOTIFY_TOKEN'])) return "Gotify nicht konfiguriert."; + + $url = rtrim($s['GOTIFY_URL'], '/') . '/message?token=' . $s['GOTIFY_TOKEN']; + $data = ['title' => $title, 'message' => $message, 'priority' => 5]; + $options = [ 'http' => [ 'header' => "Content-type: application/json\r\n", 'method' => 'POST', 'content' => json_encode($data) ] ]; + $context = stream_context_create($options); + $res = @file_get_contents($url, false, $context); + + if ($res === false) return "Verbindung zu Gotify fehlgeschlagen."; + return true; +} \ No newline at end of file diff --git a/src/projects.php b/src/projects.php new file mode 100644 index 0000000..fdfce14 --- /dev/null +++ b/src/projects.php @@ -0,0 +1,184 @@ +query("SELECT config_value FROM settings WHERE config_key = 'LICENSE_KEY'"); + $license = $stmt->fetchColumn(); + if (!$license) return 3; + $parts = explode('.', $license); + if (count($parts) !== 2) return 3; + $payload = json_decode(base64_decode($parts[0]), true); + $secret = 'TESSMANN_COREPLAN_SECURE_2026_MASTER_KEY'; + $expectedSig = hash_hmac('sha256', $parts[0], $secret); + if (!hash_equals($expectedSig, $parts[1])) return 3; + if (isset($payload['expires']) && strtotime($payload['expires']) < time()) return 3; + return (int)($payload['limit'] ?? 3); +} + +function isPMorAdmin() { return in_array($_SESSION['role'] ?? '', ['admin', 'pm']); } + +function handleGetProjects() { + global $pdo; @session_start(); + if (!isset($_SESSION['user_id'])) { http_response_code(401); exit; } + $stmt = $pdo->query(" + SELECT p.*, u.username as assigned_username, + (SELECT COUNT(*) FROM tasks WHERE project_id = p.id) as total_tasks, + (SELECT COUNT(*) FROM tasks WHERE project_id = p.id AND is_completed = 1) as completed_tasks + FROM projects p + LEFT JOIN users u ON p.assigned_to = u.id + ORDER BY CASE WHEN p.status = 'closed' THEN 1 WHEN p.status = 'canceled' THEN 2 ELSE 0 END, p.deadline ASC, p.id DESC + "); + echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]); exit; +} + +function handleCreateProject() { + global $pdo; @session_start(); + if (!isset($_SESSION['user_id'])) { http_response_code(401); exit; } + + $activeCount = $pdo->query("SELECT COUNT(*) FROM projects WHERE status = 'active'")->fetchColumn(); + $limit = getProjectLimit(); + if ($activeCount >= $limit) { + $tierName = $limit === 3 ? "Community Edition" : "Standard Edition"; + echo json_encode(['success' => false, 'error' => "Lizenzlimit der $tierName erreicht."]); exit; + } + + $title = trim($_POST['title'] ?? ''); $description = trim($_POST['description'] ?? ''); $briefing = trim($_POST['briefing'] ?? ''); $deadline = trim($_POST['deadline'] ?? ''); + if ($title === '') { echo json_encode(['success' => false, 'error' => 'Titel darf nicht leer sein.']); exit; } + if ($deadline === '') $deadline = null; + + $pdo->prepare("INSERT INTO projects (title, description, briefing, deadline) VALUES (?, ?, ?, ?)")->execute([$title, $description, $briefing, $deadline]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'Projekt erstellt', "Projekt: $title"]); + echo json_encode(['success' => true]); exit; +} + +function handleAssignProject() { + global $pdo; @session_start(); + if (!isPMorAdmin()) { echo json_encode(['success' => false, 'error' => 'Keine Berechtigung.']); exit; } + $id = $_POST['id'] ?? 0; + $pdo->prepare("UPDATE projects SET assigned_to = ?, last_activity = CURRENT_TIMESTAMP WHERE id = ?")->execute([$_SESSION['user_id'], $id]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'Projekt zugewiesen', "ID: $id"]); + echo json_encode(['success' => true]); exit; +} + +function handleExtendProject() { + global $pdo; @session_start(); + if (!isPMorAdmin()) { echo json_encode(['success' => false, 'error' => 'Keine Berechtigung.']); exit; } + $id = $_POST['id'] ?? 0; $deadline = trim($_POST['deadline'] ?? ''); if ($deadline === '') $deadline = null; + $pdo->prepare("UPDATE projects SET deadline = ?, last_activity = CURRENT_TIMESTAMP WHERE id = ? AND status = 'active'")->execute([$deadline, $id]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'Projekt verlängert', "ID: $id"]); + echo json_encode(['success' => true]); exit; +} + +function handleCloseProject() { + global $pdo; @session_start(); + if (!isPMorAdmin()) { echo json_encode(['success' => false, 'error' => 'Keine Berechtigung.']); exit; } + $id = $_POST['id'] ?? 0; $status = $_POST['status'] ?? 'closed'; $comment = trim($_POST['comment'] ?? ''); + if (!in_array($status, ['closed', 'canceled'])) $status = 'closed'; + $pdo->prepare("UPDATE projects SET status = ?, closed_at = CURRENT_TIMESTAMP, close_comment = ? WHERE id = ? AND status = 'active'")->execute([$status, $comment, $id]); + $actionLog = $status === 'canceled' ? 'Projekt abgebrochen' : 'Projekt abgeschlossen'; + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], $actionLog, "ID: $id"]); + echo json_encode(['success' => true]); exit; +} + +function handleDeleteProject() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { echo json_encode(['success' => false, 'error' => 'Nur Admins dürfen löschen.']); exit; } + $id = $_POST['id'] ?? 0; + $stmt = $pdo->prepare("SELECT status, closed_at FROM projects WHERE id = ?"); $stmt->execute([$id]); $proj = $stmt->fetch(); + if (!$proj) { echo json_encode(['success' => false, 'error' => 'Projekt nicht gefunden.']); exit; } + if ($proj['status'] === 'active') { echo json_encode(['success' => false, 'error' => 'Aktive Projekte können nicht gelöscht werden.']); exit; } + if ($proj['status'] === 'closed') { + $closedAt = strtotime($proj['closed_at']); $sixtyDaysAgo = strtotime('-60 days'); + if ($closedAt > $sixtyDaysAgo) { $daysLeft = ceil(($closedAt - $sixtyDaysAgo) / 86400); echo json_encode(['success' => false, 'error' => "Löschsperre aktiv. Erfolgreiche Projekte erst in $daysLeft Tag(en) löschbar."]); exit; } + } + $pdo->prepare("DELETE FROM tasks WHERE project_id = ?")->execute([$id]); + $pdo->prepare("DELETE FROM projects WHERE id = ?")->execute([$id]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'Projekt gelöscht', "ID: $id"]); + echo json_encode(['success' => true]); exit; +} + +function handleGetTasks() { + global $pdo; @session_start(); + if (!isset($_SESSION['user_id'])) { http_response_code(401); exit; } + $projectId = $_GET['project_id'] ?? 0; + $stmt = $pdo->prepare("SELECT t.*, u.username as assigned_username FROM tasks t LEFT JOIN users u ON t.assigned_to = u.id WHERE t.project_id = ? ORDER BY t.is_completed ASC, t.phase ASC, t.priority ASC, t.deadline ASC, t.id ASC"); + $stmt->execute([$projectId]); + echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]); exit; +} + +function handleCreateTask() { + global $pdo; @session_start(); + if (!isset($_SESSION['user_id'])) { http_response_code(401); exit; } + + $projectId = $_POST['project_id'] ?? 0; $title = trim($_POST['title'] ?? ''); $deadline = trim($_POST['deadline'] ?? ''); $phase = trim($_POST['phase'] ?? 'Standard'); $priority = (int)($_POST['priority'] ?? 2); $assignedTo = trim($_POST['assigned_to'] ?? ''); + if ($deadline === '') $deadline = null; if ($phase === '') $phase = 'Standard'; if ($assignedTo === '') $assignedTo = null; + + if ($title !== '') { + $pdo->prepare("INSERT INTO tasks (project_id, title, deadline, phase, priority, assigned_to) VALUES (?, ?, ?, ?, ?, ?)")->execute([$projectId, $title, $deadline, $phase, $priority, $assignedTo]); + $pdo->prepare("UPDATE projects SET last_activity = CURRENT_TIMESTAMP WHERE id = ?")->execute([$projectId]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'Task erstellt', "Proj-ID: $projectId | Task: $title"]); + } + echo json_encode(['success' => true]); exit; +} + +function handleEditTask() { + global $pdo; @session_start(); + if (!isset($_SESSION['user_id'])) { http_response_code(401); exit; } + + $taskId = $_POST['task_id'] ?? 0; $title = trim($_POST['title'] ?? ''); $deadline = trim($_POST['deadline'] ?? ''); $phase = trim($_POST['phase'] ?? 'Standard'); $priority = (int)($_POST['priority'] ?? 2); $assignedTo = trim($_POST['assigned_to'] ?? ''); + if ($deadline === '') $deadline = null; if ($phase === '') $phase = 'Standard'; if ($assignedTo === '') $assignedTo = null; + + if ($title !== '') { + $pdo->prepare("UPDATE tasks SET title = ?, deadline = ?, phase = ?, priority = ?, assigned_to = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?")->execute([$title, $deadline, $phase, $priority, $assignedTo, $taskId]); + $stmt = $pdo->prepare("SELECT project_id FROM tasks WHERE id = ?"); $stmt->execute([$taskId]); $t = $stmt->fetch(); + if ($t) $pdo->prepare("UPDATE projects SET last_activity = CURRENT_TIMESTAMP WHERE id = ?")->execute([$t['project_id']]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'Task bearbeitet', "Task ID: $taskId ($title)"]); + } + echo json_encode(['success' => true]); exit; +} + +function handleToggleTask() { + global $pdo; @session_start(); + if (!isset($_SESSION['user_id'])) { http_response_code(401); exit; } + $taskId = $_POST['task_id'] ?? 0; $projectId = $_POST['project_id'] ?? 0; $comment = trim($_POST['comment'] ?? ''); + $stmt = $pdo->prepare("SELECT is_completed, title FROM tasks WHERE id = ?"); $stmt->execute([$taskId]); $task = $stmt->fetch(); + if ($task) { + if ($task['is_completed'] == 0) { + $pdo->prepare("UPDATE tasks SET is_completed = 1, completed_by = ?, completed_at = CURRENT_TIMESTAMP, completion_comment = ? WHERE id = ?")->execute([$_SESSION['user_id'], $comment, $taskId]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'Task erledigt', "Task: " . $task['title']]); + } else { + $pdo->prepare("UPDATE tasks SET is_completed = 0, completed_by = NULL, completed_at = NULL, completion_comment = NULL WHERE id = ?")->execute([$taskId]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'Task reaktiviert', "Task: " . $task['title']]); + } + $pdo->prepare("UPDATE projects SET last_activity = CURRENT_TIMESTAMP WHERE id = ?")->execute([$projectId]); + } + echo json_encode(['success' => true]); exit; +} + +function handleDeleteTask() { + global $pdo; @session_start(); + if (!isset($_SESSION['user_id'])) { http_response_code(401); exit; } + $taskId = $_POST['task_id'] ?? 0; + $stmt = $pdo->prepare("SELECT project_id, title FROM tasks WHERE id = ?"); $stmt->execute([$taskId]); $task = $stmt->fetch(); + if ($task) { + $pdo->prepare("DELETE FROM tasks WHERE id = ?")->execute([$taskId]); + $pdo->prepare("UPDATE projects SET last_activity = CURRENT_TIMESTAMP WHERE id = ?")->execute([$task['project_id']]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'Task gelöscht', "Task: " . $task['title']]); + } + echo json_encode(['success' => true]); exit; +} + +function handleGetMyArea() { + global $pdo; @session_start(); + if (!isset($_SESSION['user_id'])) { http_response_code(401); exit; } + $userId = $_SESSION['user_id']; + + $stmtProj = $pdo->prepare("SELECT p.*, (SELECT COUNT(*) FROM tasks WHERE project_id = p.id) as total_tasks, (SELECT COUNT(*) FROM tasks WHERE project_id = p.id AND is_completed = 1) as completed_tasks FROM projects p WHERE assigned_to = ? AND status = 'active' ORDER BY deadline ASC"); + $stmtProj->execute([$userId]); + $myProjects = $stmtProj->fetchAll(); + + $stmtTasks = $pdo->prepare("SELECT t.*, p.title as project_title FROM tasks t JOIN projects p ON t.project_id = p.id WHERE t.assigned_to = ? AND t.is_completed = 0 AND p.status = 'active' ORDER BY t.priority ASC, t.deadline ASC"); + $stmtTasks->execute([$userId]); + $myTasks = $stmtTasks->fetchAll(); + + echo json_encode(['success' => true, 'projects' => $myProjects, 'tasks' => $myTasks]); exit; +} \ No newline at end of file diff --git a/src/router.php b/src/router.php new file mode 100644 index 0000000..13839b4 --- /dev/null +++ b/src/router.php @@ -0,0 +1,34 @@ + true, 'status' => 'online', 'system' => 'CorePlan']); +} elseif ($path === '/api/logs' && $method === 'GET') { handleGetLogs(); +} elseif ($path === '/api/settings' && $method === 'GET') { handleGetSettings(); +} elseif ($path === '/api/settings' && $method === 'POST') { handleSaveSettings(); +} elseif ($path === '/api/settings/test-smtp' && $method === 'POST') { handleTestSmtp(); +} elseif ($path === '/api/settings/test-gotify' && $method === 'POST') { handleTestGotify(); +} elseif ($path === '/api/settings/delete' && $method === 'POST') { handleDeleteSetting(); +} elseif ($path === '/api/password' && $method === 'POST') { handleChangePassword(); +} elseif ($path === '/api/users' && $method === 'GET') { handleGetUsers(); +} elseif ($path === '/api/users' && $method === 'POST') { handleCreateUser(); +} elseif ($path === '/api/users/edit' && $method === 'POST') { handleEditUser(); // NEU +} elseif ($path === '/api/users/delete' && $method === 'POST') { handleDeleteUser(); +// --- COREPLAN ENDPUNKTE --- +} elseif ($path === '/api/projects' && $method === 'GET') { handleGetProjects(); +} elseif ($path === '/api/projects' && $method === 'POST') { handleCreateProject(); +} elseif ($path === '/api/projects/close' && $method === 'POST') { handleCloseProject(); +} elseif ($path === '/api/projects/extend' && $method === 'POST') { handleExtendProject(); +} elseif ($path === '/api/projects/assign' && $method === 'POST') { handleAssignProject(); +} elseif ($path === '/api/projects/delete' && $method === 'POST') { handleDeleteProject(); +} elseif ($path === '/api/tasks' && $method === 'GET') { handleGetTasks(); +} elseif ($path === '/api/tasks/create' && $method === 'POST') { handleCreateTask(); +} elseif ($path === '/api/tasks/toggle' && $method === 'POST') { handleToggleTask(); +} elseif ($path === '/api/tasks/edit' && $method === 'POST') { handleEditTask(); +} elseif ($path === '/api/tasks/delete' && $method === 'POST') { handleDeleteTask(); +} elseif ($path === '/api/myarea' && $method === 'GET') { handleGetMyArea(); +} elseif ($path === '/api/me' && $method === 'GET') { + @session_start(); if (isset($_SESSION['user_id'])) { echo json_encode(['success' => true, 'username' => $_SESSION['username'], 'role' => $_SESSION['role'], 'user_id' => $_SESSION['user_id']]); } else { http_response_code(401); echo json_encode(['success' => false, 'error' => 'Not authenticated']); } exit; +} else { http_response_code(404); echo json_encode(['success' => false, 'error' => 'Endpoint not found']); } \ No newline at end of file