feat(auth): implement user management, invite system, granular module permissions, and admin tools module
This commit is contained in:
parent
1aaf656612
commit
2d42df53dd
18 changed files with 1182 additions and 116 deletions
|
|
@ -75,6 +75,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['password_to_hash']))
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="position: fixed; top: 16px; left: 16px; z-index: 200; display: flex; gap: 16px; font-family: var(--retro-font); font-size: 0.65rem;">
|
||||
<a href="/admin" style="color: var(--primary-color); text-decoration: none; padding: 6px 10px; background: rgba(0,0,0,0.6); border: 1px solid var(--primary-color);">< /ADMIN</a>
|
||||
<a href="/admin/users" style="color: var(--primary-color); text-decoration: none; padding: 6px 10px; background: rgba(0,0,0,0.6); border: 1px solid var(--primary-color);">BENUTZER</a>
|
||||
<a href="/" style="color: var(--secondary-color); text-decoration: none; padding: 6px 10px; background: rgba(0,0,0,0.6); border: 1px solid var(--secondary-color);">HOME</a>
|
||||
</div>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1>ADMIN TOOLS</h1>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ APP_NAME=philippurbschat.de
|
|||
APP_ENV=production
|
||||
BASE_URL=https://philippurbschat.de/
|
||||
|
||||
# Datenbank (Optional - leer lassen, wenn keine DB benötigt wird)
|
||||
# Datenbank (Standard: mysql. Für lokale Entwicklung ohne MySQL kann DB_CONNECTION=sqlite genutzt werden)
|
||||
# DB_CONNECTION=mysql
|
||||
DB_HOST=localhost
|
||||
DB_NAME=your_db_name
|
||||
DB_USER=your_db_user
|
||||
|
|
|
|||
|
|
@ -24,15 +24,25 @@ class AdminController extends Controller {
|
|||
'title' => 'Einstellungen'
|
||||
]);
|
||||
}
|
||||
// User List
|
||||
// User & Invite Management
|
||||
public function users() {
|
||||
$userModel = $this->model('User');
|
||||
$invitationModel = $this->model('Invitation');
|
||||
|
||||
$users = $userModel->getAll();
|
||||
$invitations = $invitationModel->getAll();
|
||||
$allModules = ModuleService::getAll();
|
||||
$activeTab = $_GET['tab'] ?? 'users';
|
||||
|
||||
$this->view('admin/users', [
|
||||
'title' => 'Benutzerverwaltung',
|
||||
'users' => $users
|
||||
'title' => 'Benutzer- & Zugangsverwaltung',
|
||||
'users' => $users,
|
||||
'invitations' => $invitations,
|
||||
'allModules' => $allModules,
|
||||
'activeTab' => $activeTab
|
||||
]);
|
||||
}
|
||||
|
||||
// User Form
|
||||
public function user_form($id = null) {
|
||||
$user = null;
|
||||
|
|
@ -41,41 +51,75 @@ class AdminController extends Controller {
|
|||
$user = $userModel->getById($id);
|
||||
if (!$user) {
|
||||
Flash::set('Nutzer nicht gefunden.', 'error');
|
||||
header('Location: ' . Config::get('BASE_URL') . 'admin/users');
|
||||
header('Location: ' . Config::get('BASE_URL', '/') . 'admin/users');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
$this->view('admin/user_form', [
|
||||
'title' => $id ? 'Benutzer bearbeiten' : 'Neuen Benutzer anlegen',
|
||||
'user' => $user
|
||||
'user' => $user,
|
||||
'allModules' => ModuleService::getAll()
|
||||
]);
|
||||
}
|
||||
|
||||
// Save User
|
||||
public function user_save() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ' . Config::get('BASE_URL') . 'admin/users');
|
||||
header('Location: ' . Config::get('BASE_URL', '/') . 'admin/users');
|
||||
exit;
|
||||
}
|
||||
try {
|
||||
Security::checkCsrf($_POST['csrf_token']);
|
||||
$id = !empty($_POST['id']) ? $_POST['id'] : null;
|
||||
Security::checkCsrf($_POST['csrf_token'] ?? '');
|
||||
$id = !empty($_POST['id']) ? (int)$_POST['id'] : null;
|
||||
$userModel = $this->model('User');
|
||||
|
||||
if (empty($_POST['name']) || empty($_POST['email'])) {
|
||||
throw new Exception("Name und E-Mail sind Pflichtfelder.");
|
||||
}
|
||||
$userModel->save($_POST, $id);
|
||||
|
||||
// Projects from checkboxes
|
||||
$projects = isset($_POST['projects']) && is_array($_POST['projects']) ? $_POST['projects'] : [];
|
||||
$postData = $_POST;
|
||||
$postData['projects'] = $projects;
|
||||
|
||||
$userModel->save($postData, $id);
|
||||
Flash::set('Nutzer erfolgreich gespeichert!', 'success');
|
||||
} catch (Exception $e) {
|
||||
Flash::set($e->getMessage(), 'error');
|
||||
}
|
||||
header('Location: ' . Config::get('BASE_URL') . 'admin/users');
|
||||
header('Location: ' . Config::get('BASE_URL', '/') . 'admin/users');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Quick toggle user status (Active / Inactive)
|
||||
public function user_toggle_status($id) {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
try {
|
||||
Security::checkCsrf($_POST['csrf_token'] ?? '');
|
||||
$userModel = $this->model('User');
|
||||
$user = $userModel->getById($id);
|
||||
if (!$user) {
|
||||
throw new Exception("Nutzer nicht gefunden.");
|
||||
}
|
||||
|
||||
$newStatus = ($user['status'] === 'Active') ? 'Inactive' : 'Active';
|
||||
$user['status'] = $newStatus;
|
||||
$userModel->save($user, $id);
|
||||
|
||||
Flash::set('Status für ' . htmlspecialchars($user['name']) . ' geändert auf: ' . $newStatus, 'success');
|
||||
} catch (Exception $e) {
|
||||
Flash::set('Fehler: ' . $e->getMessage(), 'error');
|
||||
}
|
||||
}
|
||||
header('Location: ' . Config::get('BASE_URL', '/') . 'admin/users');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Delete User
|
||||
public function user_delete($id) {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
try {
|
||||
Security::checkCsrf($_POST['csrf_token']);
|
||||
Security::checkCsrf($_POST['csrf_token'] ?? '');
|
||||
$userModel = $this->model('User');
|
||||
$userModel->delete($id);
|
||||
Flash::set('Nutzer wurde gelöscht.', 'success');
|
||||
|
|
@ -83,7 +127,57 @@ class AdminController extends Controller {
|
|||
Flash::set('Fehler beim Löschen: ' . $e->getMessage(), 'error');
|
||||
}
|
||||
}
|
||||
header('Location: ' . Config::get('BASE_URL') . 'admin/users');
|
||||
header('Location: ' . Config::get('BASE_URL', '/') . 'admin/users');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Create Invitation Link
|
||||
public function invite_create() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ' . Config::get('BASE_URL', '/') . 'admin/users?tab=invites');
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
Security::checkCsrf($_POST['csrf_token'] ?? '');
|
||||
$invitationModel = $this->model('Invitation');
|
||||
|
||||
$assignedProjects = isset($_POST['projects']) && is_array($_POST['projects']) ? $_POST['projects'] : [];
|
||||
$note = trim($_POST['note'] ?? '');
|
||||
$maxUses = !empty($_POST['max_uses']) ? max(1, (int)$_POST['max_uses']) : 1;
|
||||
|
||||
$expiresAt = null;
|
||||
if (!empty($_POST['days_valid']) && (int)$_POST['days_valid'] > 0) {
|
||||
$days = (int)$_POST['days_valid'];
|
||||
$expiresAt = (new DateTime("+{$days} days"))->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
$token = $invitationModel->create($assignedProjects, $note ?: null, $maxUses, $expiresAt);
|
||||
$baseUrl = rtrim(Config::get('BASE_URL', 'https://philippurbschat.de/'), '/');
|
||||
$inviteUrl = $baseUrl . '/register?token=' . $token;
|
||||
|
||||
Flash::set('Einladungslink erfolgreich erstellt: ' . $inviteUrl, 'success');
|
||||
} catch (Exception $e) {
|
||||
Flash::set('Fehler beim Erstellen des Einladungslinks: ' . $e->getMessage(), 'error');
|
||||
}
|
||||
|
||||
header('Location: ' . Config::get('BASE_URL', '/') . 'admin/users?tab=invites');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Delete Invitation Link
|
||||
public function invite_delete($id) {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
try {
|
||||
Security::checkCsrf($_POST['csrf_token'] ?? '');
|
||||
$invitationModel = $this->model('Invitation');
|
||||
$invitationModel->delete((int)$id);
|
||||
Flash::set('Einladungslink wurde gelöscht.', 'success');
|
||||
} catch (Exception $e) {
|
||||
Flash::set('Fehler beim Löschen: ' . $e->getMessage(), 'error');
|
||||
}
|
||||
}
|
||||
header('Location: ' . Config::get('BASE_URL', '/') . 'admin/users?tab=invites');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
131
home/app/controllers/RegisterController.php
Normal file
131
home/app/controllers/RegisterController.php
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
require_once __DIR__ . '/../../core/Controller.php';
|
||||
|
||||
class RegisterController extends Controller {
|
||||
public function index($urlToken = null) {
|
||||
$token = trim($_GET['token'] ?? $urlToken ?? '');
|
||||
|
||||
if (empty($token)) {
|
||||
$this->view('register', [
|
||||
'title' => 'Zugriff beschränkt',
|
||||
'valid' => false,
|
||||
'errorMessage' => 'Die Registrierung auf dieser Plattform ist nur über einen persönlichen Einladungslink möglich.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$invitationModel = $this->model('Invitation');
|
||||
$invitation = $invitationModel->getByToken($token);
|
||||
|
||||
if (!$invitation) {
|
||||
$this->view('register', [
|
||||
'title' => 'Ungültiger Einladungslink',
|
||||
'valid' => false,
|
||||
'errorMessage' => 'Dieser Einladungslink ist leider ungültig, abgelaufen oder wurde bereits verwendet.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get modules assigned to this invitation
|
||||
$assignedModules = [];
|
||||
foreach ($invitation['assigned_projects'] as $slug) {
|
||||
$mod = ModuleService::getBySlug($slug);
|
||||
if ($mod) {
|
||||
$assignedModules[] = $mod;
|
||||
}
|
||||
}
|
||||
|
||||
$this->view('register', [
|
||||
'title' => 'Account Registrierung',
|
||||
'valid' => true,
|
||||
'token' => $token,
|
||||
'note' => $invitation['note'] ?? null,
|
||||
'assignedModules' => $assignedModules
|
||||
]);
|
||||
}
|
||||
|
||||
public function submit() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ' . Config::get('BASE_URL', '/'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$token = trim($_POST['token'] ?? '');
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$email = trim(strtolower($_POST['email'] ?? ''));
|
||||
$password = $_POST['password'] ?? '';
|
||||
$passwordConfirm = $_POST['password_confirm'] ?? '';
|
||||
|
||||
$redirectUrl = Config::get('BASE_URL', '/') . 'register?token=' . urlencode($token);
|
||||
|
||||
try {
|
||||
Security::checkCsrf($_POST['csrf_token'] ?? '');
|
||||
|
||||
if (empty($token)) {
|
||||
throw new Exception('Einladungs-Token fehlt.');
|
||||
}
|
||||
|
||||
$invitationModel = $this->model('Invitation');
|
||||
$invitation = $invitationModel->getByToken($token);
|
||||
if (!$invitation) {
|
||||
throw new Exception('Der Einladungslink ist ungültig oder abgelaufen.');
|
||||
}
|
||||
|
||||
if (empty($name)) {
|
||||
throw new Exception('Bitte gib deinen Namen ein.');
|
||||
}
|
||||
|
||||
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new Exception('Bitte gib eine gültige E-Mail-Adresse ein.');
|
||||
}
|
||||
|
||||
if (strlen($password) < 8) {
|
||||
throw new Exception('Das Passwort muss mindestens 8 Zeichen lang sein.');
|
||||
}
|
||||
|
||||
if ($password !== $passwordConfirm) {
|
||||
throw new Exception('Die eingegebenen Passwörter stimmen nicht überein.');
|
||||
}
|
||||
|
||||
// Check if user email already exists
|
||||
$userModel = $this->model('User');
|
||||
$db = new Database();
|
||||
$db->query("SELECT id FROM home_users WHERE email = :email");
|
||||
$db->bind(':email', $email);
|
||||
if ($db->single()) {
|
||||
throw new Exception('Diese E-Mail-Adresse ist bereits registriert. Bitte melde dich an.');
|
||||
}
|
||||
|
||||
// Save new user
|
||||
$userData = [
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
'projects' => implode(',', $invitation['assigned_projects'] ?? []),
|
||||
'is_admin' => 0,
|
||||
'status' => 'Active'
|
||||
];
|
||||
|
||||
$userModel->save($userData);
|
||||
|
||||
// Record token usage
|
||||
$invitationModel->recordUse($token);
|
||||
|
||||
// Auto-login user
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['user_email'] = $email;
|
||||
$_SESSION['user_projects'] = $invitation['assigned_projects'] ?? [];
|
||||
$_SESSION['is_admin'] = false;
|
||||
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||
|
||||
Flash::set('Willkommen, ' . htmlspecialchars($name) . '! Dein Account wurde erfolgreich eingerichtet.', 'success');
|
||||
header('Location: ' . Config::get('BASE_URL', '/'));
|
||||
exit;
|
||||
|
||||
} catch (Exception $e) {
|
||||
Flash::set($e->getMessage(), 'error');
|
||||
header('Location: ' . $redirectUrl);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,8 @@ class SeoController extends Controller {
|
|||
$txt .= "Disallow: /admin/settings\n";
|
||||
$txt .= "Disallow: /login\n";
|
||||
$txt .= "Disallow: /login/\n";
|
||||
$txt .= "Disallow: /register\n";
|
||||
$txt .= "Disallow: /register/\n";
|
||||
$txt .= "Disallow: /core/\n";
|
||||
$txt .= "Disallow: /app/\n";
|
||||
$txt .= "Disallow: /views/\n";
|
||||
|
|
|
|||
85
home/app/models/Invitation.php
Normal file
85
home/app/models/Invitation.php
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
class Invitation {
|
||||
private $db;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = new Database();
|
||||
}
|
||||
|
||||
public function create(array $assignedProjects = [], ?string $note = null, int $maxUses = 1, ?string $expiresAt = null): string {
|
||||
$token = bin2hex(random_bytes(24)); // 48-char secure hex token
|
||||
$projectsJson = json_encode(array_values(array_filter($assignedProjects)));
|
||||
|
||||
$sql = "INSERT INTO home_invitations (token, assigned_projects, max_uses, uses_count, expires_at, note)
|
||||
VALUES (:token, :assigned_projects, :max_uses, 0, :expires_at, :note)";
|
||||
$this->db->query($sql);
|
||||
$this->db->bind(':token', $token);
|
||||
$this->db->bind(':assigned_projects', $projectsJson);
|
||||
$this->db->bind(':max_uses', $maxUses);
|
||||
$this->db->bind(':expires_at', $expiresAt);
|
||||
$this->db->bind(':note', $note);
|
||||
$this->db->execute();
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function getByToken(string $token): ?array {
|
||||
$sql = "SELECT * FROM home_invitations WHERE token = :token";
|
||||
$this->db->query($sql);
|
||||
$this->db->bind(':token', $token);
|
||||
$invitation = $this->db->single();
|
||||
|
||||
if (!$invitation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check uses count
|
||||
if ($invitation['max_uses'] > 0 && $invitation['uses_count'] >= $invitation['max_uses']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if (!empty($invitation['expires_at'])) {
|
||||
$now = new DateTime();
|
||||
$expires = new DateTime($invitation['expires_at']);
|
||||
if ($now > $expires) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$invitation['assigned_projects'] = json_decode($invitation['assigned_projects'] ?? '[]', true) ?? [];
|
||||
return $invitation;
|
||||
}
|
||||
|
||||
public function recordUse(string $token): bool {
|
||||
$sql = "UPDATE home_invitations SET uses_count = uses_count + 1 WHERE token = :token";
|
||||
$this->db->query($sql);
|
||||
$this->db->bind(':token', $token);
|
||||
return $this->db->execute();
|
||||
}
|
||||
|
||||
public function getAll(): array {
|
||||
$sql = "SELECT * FROM home_invitations ORDER BY id DESC";
|
||||
$this->db->query($sql);
|
||||
$invitations = $this->db->resultSet();
|
||||
|
||||
foreach ($invitations as &$inv) {
|
||||
$inv['assigned_projects'] = json_decode($inv['assigned_projects'] ?? '[]', true) ?? [];
|
||||
$isExpired = false;
|
||||
if (!empty($inv['expires_at'])) {
|
||||
$isExpired = (new DateTime()) > (new DateTime($inv['expires_at']));
|
||||
}
|
||||
$isDepleted = ($inv['max_uses'] > 0 && $inv['uses_count'] >= $inv['max_uses']);
|
||||
$inv['is_valid'] = (!$isExpired && !$isDepleted);
|
||||
}
|
||||
|
||||
return $invitations;
|
||||
}
|
||||
|
||||
public function delete(int $id): bool {
|
||||
$sql = "DELETE FROM home_invitations WHERE id = :id";
|
||||
$this->db->query($sql);
|
||||
$this->db->bind(':id', $id);
|
||||
return $this->db->execute();
|
||||
}
|
||||
}
|
||||
|
|
@ -36,7 +36,13 @@ class User {
|
|||
}
|
||||
// Persistence
|
||||
public function save($data, $id = null) {
|
||||
$projectsJson = json_encode(array_map('trim', explode(',', $data['projects'])));
|
||||
if (is_array($data['projects'] ?? null)) {
|
||||
$projectsList = array_values(array_filter($data['projects']));
|
||||
} else {
|
||||
$projectsStr = $data['projects'] ?? '';
|
||||
$projectsList = !empty($projectsStr) ? array_values(array_filter(array_map('trim', explode(',', $projectsStr)))) : [];
|
||||
}
|
||||
$projectsJson = json_encode($projectsList);
|
||||
if ($id) {
|
||||
$sql = "UPDATE home_users SET name = :name, email = :email, projects = :projects, is_admin = :is_admin, status = :status ";
|
||||
if (!empty($data['password'])) {
|
||||
|
|
@ -52,7 +58,7 @@ class User {
|
|||
$this->db->bind(':name', $data['name']);
|
||||
$this->db->bind(':email', $data['email']);
|
||||
$this->db->bind(':projects', $projectsJson);
|
||||
$this->db->bind(':is_admin', isset($data['is_admin']) ? 1 : 0);
|
||||
$this->db->bind(':is_admin', !empty($data['is_admin']) ? 1 : 0);
|
||||
$this->db->bind(':status', $data['status']);
|
||||
if (!$id || !empty($data['password'])) {
|
||||
$this->db->bind(':password', password_hash($data['password'], PASSWORD_DEFAULT));
|
||||
|
|
|
|||
56
home/app/services/MigrationService.php
Normal file
56
home/app/services/MigrationService.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
class MigrationService {
|
||||
public static function run(Database $db) {
|
||||
$driver = $db->getDriver();
|
||||
|
||||
if ($driver === 'sqlite') {
|
||||
$db->query("CREATE TABLE IF NOT EXISTS home_users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(191) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
projects TEXT NULL,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'Active',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)");
|
||||
$db->execute();
|
||||
|
||||
$db->query("CREATE TABLE IF NOT EXISTS home_invitations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
token VARCHAR(64) NOT NULL UNIQUE,
|
||||
assigned_projects TEXT NULL,
|
||||
max_uses INTEGER NOT NULL DEFAULT 1,
|
||||
uses_count INTEGER NOT NULL DEFAULT 0,
|
||||
expires_at DATETIME NULL,
|
||||
note VARCHAR(255) NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)");
|
||||
$db->execute();
|
||||
} else {
|
||||
$db->query("CREATE TABLE IF NOT EXISTS home_users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(191) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
projects TEXT NULL,
|
||||
is_admin TINYINT(1) NOT NULL DEFAULT 0,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'Active',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
|
||||
$db->execute();
|
||||
|
||||
$db->query("CREATE TABLE IF NOT EXISTS home_invitations (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
token VARCHAR(64) NOT NULL UNIQUE,
|
||||
assigned_projects TEXT NULL,
|
||||
max_uses INT NOT NULL DEFAULT 1,
|
||||
uses_count INT NOT NULL DEFAULT 0,
|
||||
expires_at DATETIME NULL,
|
||||
note VARCHAR(255) NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci");
|
||||
$db->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
83
home/app/services/ModuleService.php
Normal file
83
home/app/services/ModuleService.php
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
class ModuleService {
|
||||
private static $modules = [
|
||||
'dinos' => [
|
||||
'slug' => 'dinos',
|
||||
'name' => 'Dino Generator',
|
||||
'icon' => '🦖',
|
||||
'badge' => 'AI Art',
|
||||
'description' => 'KI Dino-Bilderstellung & Google Photos Sync',
|
||||
'path' => '/dinos',
|
||||
'adminOnly' => false
|
||||
],
|
||||
'plants' => [
|
||||
'slug' => 'plants',
|
||||
'name' => 'Plant Tracker',
|
||||
'icon' => '🪴',
|
||||
'badge' => 'AI Vision',
|
||||
'description' => 'Pflanzen-Tracking, Pflege & KI-Erkennung',
|
||||
'path' => '/plants',
|
||||
'adminOnly' => false
|
||||
],
|
||||
'storymachine' => [
|
||||
'slug' => 'storymachine',
|
||||
'name' => 'Story Machine',
|
||||
'icon' => '📖',
|
||||
'badge' => 'Creative',
|
||||
'description' => 'Interaktive Kinder-Geschichtenmaschine',
|
||||
'path' => '/storymachine',
|
||||
'adminOnly' => false
|
||||
],
|
||||
'cooles-feature' => [
|
||||
'slug' => 'cooles-feature',
|
||||
'name' => 'Web Audio Synth',
|
||||
'icon' => '🎛️',
|
||||
'badge' => 'Audio Lab',
|
||||
'description' => 'Interaktive Web Audio API Synthese',
|
||||
'path' => '/cooles-feature',
|
||||
'adminOnly' => false
|
||||
],
|
||||
'admin-tools' => [
|
||||
'slug' => 'admin-tools',
|
||||
'name' => 'Admin Tools',
|
||||
'icon' => '🛠️',
|
||||
'badge' => 'System',
|
||||
'description' => 'Hash Generator, Diagnostik & Systemwerkzeuge',
|
||||
'path' => '/admin-tools',
|
||||
'adminOnly' => true
|
||||
],
|
||||
'test' => [
|
||||
'slug' => 'test',
|
||||
'name' => 'Test Environment',
|
||||
'icon' => '🧪',
|
||||
'badge' => 'Sandbox',
|
||||
'description' => 'Test- und Entwicklungsumgebung',
|
||||
'path' => '/test',
|
||||
'adminOnly' => false
|
||||
]
|
||||
];
|
||||
|
||||
public static function getAll(): array {
|
||||
return self::$modules;
|
||||
}
|
||||
|
||||
public static function getBySlug(string $slug): ?array {
|
||||
return self::$modules[$slug] ?? null;
|
||||
}
|
||||
|
||||
public static function getAccessibleForUser(array $userProjects, bool $isAdmin = false): array {
|
||||
$accessible = [];
|
||||
foreach (self::$modules as $slug => $module) {
|
||||
if ($isAdmin) {
|
||||
$accessible[$slug] = $module;
|
||||
} elseif (in_array($slug, $userProjects, true) && !$module['adminOnly']) {
|
||||
$accessible[$slug] = $module;
|
||||
}
|
||||
}
|
||||
return $accessible;
|
||||
}
|
||||
|
||||
public static function isValidModule(string $slug): bool {
|
||||
return isset(self::$modules[$slug]);
|
||||
}
|
||||
}
|
||||
92
home/bin/admin.php
Normal file
92
home/bin/admin.php
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
// CLI Tool: php home/bin/admin.php
|
||||
if (php_sapi_name() !== 'cli') {
|
||||
die("This tool can only be run via CLI.\n");
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../app/init.php';
|
||||
|
||||
$action = $argv[1] ?? 'help';
|
||||
|
||||
switch ($action) {
|
||||
case 'set-admin':
|
||||
$email = trim(strtolower($argv[2] ?? 'hi@philippurbschat.de'));
|
||||
$password = $argv[3] ?? null;
|
||||
$name = $argv[4] ?? 'Philipp';
|
||||
|
||||
$db = new Database();
|
||||
$db->query("SELECT * FROM home_users WHERE email = :email");
|
||||
$db->bind(':email', $email);
|
||||
$existing = $db->single();
|
||||
|
||||
$userModel = new User();
|
||||
|
||||
if ($existing) {
|
||||
$data = [
|
||||
'name' => $existing['name'] ?: $name,
|
||||
'email' => $email,
|
||||
'is_admin' => 1,
|
||||
'status' => 'Active',
|
||||
'projects' => ModuleService::getAll() ? array_keys(ModuleService::getAll()) : ['dinos','plants','storymachine','cooles-feature','admin-tools','test']
|
||||
];
|
||||
if (!empty($password)) {
|
||||
$data['password'] = $password;
|
||||
}
|
||||
$userModel->save($data, $existing['id']);
|
||||
echo "✓ Admin-Rechte für {$email} erfolgreich aktualisiert (ID: {$existing['id']})!\n";
|
||||
} else {
|
||||
if (empty($password)) {
|
||||
$password = bin2hex(random_bytes(6));
|
||||
echo "Hinweis: Zufälliges Initial-Passwort generiert: {$password}\n";
|
||||
}
|
||||
$data = [
|
||||
'name' => $name,
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
'is_admin' => 1,
|
||||
'status' => 'Active',
|
||||
'projects' => array_keys(ModuleService::getAll())
|
||||
];
|
||||
$userModel->save($data);
|
||||
echo "✓ Neuer Admin-Benutzer {$email} erfolgreich angelegt!\n";
|
||||
}
|
||||
break;
|
||||
|
||||
case 'list-users':
|
||||
$userModel = new User();
|
||||
$users = $userModel->getAll();
|
||||
echo sprintf("%-4s | %-15s | %-28s | %-6s | %-8s | %s\n", "ID", "Name", "E-Mail", "Admin", "Status", "Module");
|
||||
echo str_repeat("-", 80) . "\n";
|
||||
foreach ($users as $u) {
|
||||
echo sprintf(
|
||||
"%-4s | %-15s | %-28s | %-6s | %-8s | %s\n",
|
||||
$u['id'],
|
||||
substr($u['name'], 0, 15),
|
||||
substr($u['email'], 0, 28),
|
||||
$u['is_admin'] ? 'YES' : 'no',
|
||||
$u['status'],
|
||||
implode(',', $u['projects'] ?? [])
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'create-invite':
|
||||
$note = $argv[2] ?? 'CLI-Generated';
|
||||
$projects = isset($argv[3]) ? explode(',', $argv[3]) : array_keys(ModuleService::getAll());
|
||||
$invModel = new Invitation();
|
||||
$token = $invModel->create($projects, $note, 1, null);
|
||||
$baseUrl = rtrim(Config::get('BASE_URL', 'https://philippurbschat.de/'), '/');
|
||||
echo "✓ Einladungslink generiert:\n";
|
||||
echo " URL: {$baseUrl}/register?token={$token}\n";
|
||||
echo " Module: " . implode(', ', $projects) . "\n";
|
||||
break;
|
||||
|
||||
case 'help':
|
||||
default:
|
||||
echo "PhilCore CLI Admin Tool\n";
|
||||
echo "Verwendung:\n";
|
||||
echo " php home/bin/admin.php set-admin [email] [password] [name]\n";
|
||||
echo " php home/bin/admin.php list-users\n";
|
||||
echo " php home/bin/admin.php create-invite [notiz] [projekte,kommasepariert]\n";
|
||||
break;
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ class Controller {
|
|||
$viewFile = __DIR__ . '/../views/' . $view . '.php';
|
||||
|
||||
if (file_exists($viewFile)) {
|
||||
require_once $viewFile;
|
||||
require $viewFile;
|
||||
} else {
|
||||
throw new Exception("View '{$view}' existiert nicht unter: {$viewFile}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,96 @@
|
|||
<?php
|
||||
class Database {
|
||||
private static $sharedDbh = null;
|
||||
private static $sharedDriver = 'mysql';
|
||||
private $dbh;
|
||||
private $stmt;
|
||||
private $error;
|
||||
private $driver = 'mysql';
|
||||
private static $migrated = false;
|
||||
|
||||
public function __construct() {
|
||||
if (self::$sharedDbh !== null) {
|
||||
$this->dbh = self::$sharedDbh;
|
||||
$this->driver = self::$sharedDriver;
|
||||
return;
|
||||
}
|
||||
|
||||
$connectionType = Config::get('DB_CONNECTION', 'mysql');
|
||||
$host = Config::get('DB_HOST', 'localhost');
|
||||
$user = Config::get('DB_USER', 'root');
|
||||
$pass = Config::get('DB_PASS', '');
|
||||
$dbname = Config::get('DB_NAME', '');
|
||||
|
||||
// Wenn gar keine DB konfiguriert ist, direkt Exception werfen
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
|
||||
];
|
||||
|
||||
// Explicit SQLite configuration or empty dbname in non-production
|
||||
if ($connectionType === 'sqlite' || (empty($dbname) && Config::get('APP_ENV') !== 'production')) {
|
||||
$this->connectSqlite($options);
|
||||
self::$sharedDbh = $this->dbh;
|
||||
self::$sharedDriver = $this->driver;
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($dbname)) {
|
||||
throw new Exception("Keine Datenbank in .env konfiguriert.");
|
||||
}
|
||||
|
||||
$dsn = 'mysql:host=' . $host . ';dbname=' . $dbname . ';charset=utf8mb4';
|
||||
|
||||
$options = [
|
||||
PDO::ATTR_PERSISTENT => true,
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
|
||||
];
|
||||
|
||||
try {
|
||||
$this->dbh = new PDO($dsn, $user, $pass, $options);
|
||||
$this->driver = 'mysql';
|
||||
self::$sharedDbh = $this->dbh;
|
||||
self::$sharedDriver = $this->driver;
|
||||
} catch (PDOException $e) {
|
||||
$this->error = $e->getMessage();
|
||||
// Exception werfen statt sterben!
|
||||
throw new Exception("Datenbank-Verbindungsfehler.");
|
||||
// In local development (macOS / Darwin) or when explicitly configured, fall back to SQLite
|
||||
if (PHP_OS_FAMILY === 'Darwin' || Config::get('APP_ENV') !== 'production' || $connectionType === 'sqlite') {
|
||||
$this->connectSqlite($options);
|
||||
self::$sharedDbh = $this->dbh;
|
||||
self::$sharedDriver = $this->driver;
|
||||
} else {
|
||||
throw new Exception("Datenbank-Verbindungsfehler: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->ensureSchema();
|
||||
}
|
||||
|
||||
private function connectSqlite(array $options): void {
|
||||
$this->driver = 'sqlite';
|
||||
$dataDir = __DIR__ . '/../data';
|
||||
if (!is_dir($dataDir)) {
|
||||
mkdir($dataDir, 0755, true);
|
||||
}
|
||||
$sqlitePath = $dataDir . '/database.sqlite';
|
||||
$this->dbh = new PDO('sqlite:' . $sqlitePath, null, null, $options);
|
||||
$this->dbh->exec('PRAGMA busy_timeout = 5000;');
|
||||
$this->dbh->exec('PRAGMA journal_mode = WAL;');
|
||||
$this->dbh->exec('PRAGMA foreign_keys = ON;');
|
||||
$this->ensureSchema();
|
||||
}
|
||||
|
||||
private function ensureSchema(): void {
|
||||
if (!self::$migrated) {
|
||||
self::$migrated = true;
|
||||
if (class_exists('MigrationService')) {
|
||||
MigrationService::run($this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getDriver(): string {
|
||||
return $this->driver;
|
||||
}
|
||||
|
||||
public function lastInsertId(): string {
|
||||
return $this->dbh->lastInsertId();
|
||||
}
|
||||
|
||||
public function query($sql) {
|
||||
$this->stmt = $this->dbh->prepare($sql);
|
||||
}
|
||||
|
|
@ -54,12 +113,16 @@ class Database {
|
|||
|
||||
public function resultSet() {
|
||||
$this->execute();
|
||||
return $this->stmt->fetchAll();
|
||||
$result = $this->stmt->fetchAll();
|
||||
$this->stmt->closeCursor();
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function single() {
|
||||
$this->execute();
|
||||
return $this->stmt->fetch();
|
||||
$result = $this->stmt->fetch();
|
||||
$this->stmt->closeCursor();
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function rowCount() {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,50 +1,127 @@
|
|||
<?php require_once __DIR__ . '/../inc/admin_header.php'; ?>
|
||||
<div class="max-w-4xl w-full mx-auto mt-8 px-6">
|
||||
<?php
|
||||
$baseUrl = rtrim(Config::get('BASE_URL', 'https://philippurbschat.de/'), '/');
|
||||
$userProjects = $user['projects'] ?? [];
|
||||
$modules = $allModules ?? ModuleService::getAll();
|
||||
?>
|
||||
<div class="max-w-4xl w-full mx-auto mt-6 px-4 sm:px-6">
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold text-slate-100 font-mono tracking-tight">
|
||||
<h1 class="text-2xl font-bold text-slate-100 font-mono tracking-tight flex items-center gap-2">
|
||||
<span class="text-emerald-500">></span> <?= htmlspecialchars($title) ?>
|
||||
</h1>
|
||||
<a href="<?= Config::get('BASE_URL') ?>admin/users" class="text-slate-500 hover:text-emerald-400 font-mono text-sm transition-colors">< Zurück zur Liste</a>
|
||||
<a href="<?= $baseUrl ?>/admin/users" class="text-slate-500 hover:text-emerald-400 font-mono text-xs sm:text-sm transition-colors">
|
||||
< Zurück zur Benutzerliste
|
||||
</a>
|
||||
</div>
|
||||
<div class="bg-slate-900/80 border border-slate-700/50 rounded-xl shadow-2xl p-6 sm:p-8 backdrop-blur-sm relative overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-linear-to-r from-transparent via-emerald-500/40 to-transparent"></div>
|
||||
<form action="<?= Config::get('BASE_URL') ?>admin/user_save" method="POST" class="space-y-6 relative z-10">
|
||||
|
||||
<div class="bg-slate-900/80 border border-slate-700/50 rounded-2xl shadow-2xl p-6 sm:p-8 backdrop-blur-sm relative overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-emerald-500/40 to-transparent"></div>
|
||||
|
||||
<form action="<?= $baseUrl ?>/admin/user_save" method="POST" class="space-y-6 relative z-10">
|
||||
<input type="hidden" name="csrf_token" value="<?= Security::csrf() ?>">
|
||||
<?php if($user): ?><input type="hidden" name="id" value="<?= $user['id'] ?>"><?php endif; ?>
|
||||
<?php if ($user): ?>
|
||||
<input type="hidden" name="id" value="<?= $user['id'] ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- User Credentials -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-slate-400 font-mono text-xs uppercase tracking-wider mb-2">Name</label>
|
||||
<input type="text" name="name" value="<?= htmlspecialchars($user['name'] ?? '') ?>" required class="w-full bg-slate-950/80 border border-slate-700/80 rounded-lg p-3 text-slate-200 text-sm focus:outline-none focus:border-emerald-500/50 transition-colors">
|
||||
<input type="text" name="name" value="<?= htmlspecialchars($user['name'] ?? '') ?>" required placeholder="z.B. Philipp" class="w-full bg-slate-950/80 border border-slate-700/80 rounded-xl p-3 text-slate-200 text-sm focus:outline-none focus:border-emerald-500 transition-colors font-mono">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-slate-400 font-mono text-xs uppercase tracking-wider mb-2">E-Mail (Login)</label>
|
||||
<input type="email" name="email" value="<?= htmlspecialchars($user['email'] ?? '') ?>" required class="w-full bg-slate-950/80 border border-slate-700/80 rounded-lg p-3 text-slate-200 text-sm focus:outline-none focus:border-emerald-500/50 transition-colors">
|
||||
<input type="email" name="email" value="<?= htmlspecialchars($user['email'] ?? '') ?>" required placeholder="user@beispiel.de" class="w-full bg-slate-950/80 border border-slate-700/80 rounded-xl p-3 text-slate-200 text-sm focus:outline-none focus:border-emerald-500 transition-colors font-mono">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-slate-400 font-mono text-xs uppercase tracking-wider mb-2">Passwort <?= $user ? '<span class="text-[10px] text-slate-600">(Leer lassen für keine Änderung)</span>' : '' ?></label>
|
||||
<input type="password" name="password" <?= $user ? '' : 'required' ?> class="w-full bg-slate-950/80 border border-slate-700/80 rounded-lg p-3 text-slate-200 text-sm focus:outline-none focus:border-emerald-500/50 transition-colors">
|
||||
<label class="block text-slate-400 font-mono text-xs uppercase tracking-wider mb-2">
|
||||
Passwort <?= $user ? '<span class="text-[10px] text-slate-500">(Leer lassen für keine Änderung)</span>' : '' ?>
|
||||
</label>
|
||||
<input type="password" name="password" <?= $user ? '' : 'required' ?> minlength="8" placeholder="Mindestens 8 Zeichen" class="w-full bg-slate-950/80 border border-slate-700/80 rounded-xl p-3 text-slate-200 text-sm focus:outline-none focus:border-emerald-500 transition-colors font-mono">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-slate-400 font-mono text-xs uppercase tracking-wider mb-2">Status</label>
|
||||
<select name="status" class="w-full bg-slate-950/80 border border-slate-700/80 rounded-lg p-3 text-slate-200 text-sm focus:outline-none focus:border-emerald-500/50 transition-colors">
|
||||
<option value="Active" <?= ($user['status'] ?? '') === 'Active' ? 'selected' : '' ?>>Active</option>
|
||||
<option value="Inactive" <?= ($user['status'] ?? '') === 'Inactive' ? 'selected' : '' ?>>Inactive</option>
|
||||
<select name="status" class="w-full bg-slate-950/80 border border-slate-700/80 rounded-xl p-3 text-slate-200 text-sm focus:outline-none focus:border-emerald-500 transition-colors font-mono">
|
||||
<option value="Active" <?= ($user['status'] ?? 'Active') === 'Active' ? 'selected' : '' ?>>Active (Aktiviert)</option>
|
||||
<option value="Inactive" <?= ($user['status'] ?? '') === 'Inactive' ? 'selected' : '' ?>>Inactive (Gesperrt)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Admin Checkbox -->
|
||||
<div class="flex items-center gap-3 bg-slate-950/60 p-4 rounded-xl border border-slate-800">
|
||||
<input type="checkbox" name="is_admin" id="is_admin" value="1" <?= (!empty($user['is_admin'])) ? 'checked' : '' ?> class="w-4 h-4 accent-fuchsia-500 bg-slate-900 border-slate-700 rounded cursor-pointer" onchange="toggleAdminNotice(this)">
|
||||
<div>
|
||||
<label class="block text-slate-400 font-mono text-xs uppercase tracking-wider mb-2">Projekte (Kommasepariert)</label>
|
||||
<input type="text" name="projects" placeholder="z.B. plants, storymachine, dinos" value="<?= htmlspecialchars(implode(', ', $user['projects'] ?? [])) ?>" class="w-full bg-slate-950/80 border border-slate-700/80 rounded-lg p-3 text-slate-200 text-sm focus:outline-none focus:border-emerald-500/50 transition-colors">
|
||||
<p class="text-[10px] text-slate-500 font-mono mt-1">Verzeichnisse im Root für Zugriffsberechtigung.</p>
|
||||
<label for="is_admin" class="text-slate-200 font-mono text-sm font-bold cursor-pointer">
|
||||
Benutzer ist Administrator (Vollzugriff)
|
||||
</label>
|
||||
<p class="text-[11px] text-slate-500 font-mono">Admins haben automatisch Zugriff auf alle Module, Einstellungen und den Admin-Bereich.</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 bg-slate-950/50 p-4 rounded-lg border border-slate-800">
|
||||
<input type="checkbox" name="is_admin" id="is_admin" value="1" <?= (!empty($user['is_admin'])) ? 'checked' : '' ?> class="w-4 h-4 accent-emerald-500 bg-slate-900 border-slate-700 rounded">
|
||||
<label for="is_admin" class="text-slate-300 font-mono text-sm cursor-pointer">Benutzer ist Administrator (Vollzugriff)</label>
|
||||
</div>
|
||||
<div class="pt-4 border-t border-slate-800">
|
||||
<button type="submit" class="bg-emerald-500/20 border border-emerald-500/50 text-emerald-400 hover:bg-emerald-500/40 hover:text-emerald-300 font-bold font-mono uppercase tracking-wider py-3 px-6 rounded-lg transition-all duration-300">💾 Speichern</button>
|
||||
|
||||
<!-- Module Permission Matrix -->
|
||||
<div>
|
||||
<div class="flex flex-wrap items-center justify-between mb-3 gap-2">
|
||||
<div>
|
||||
<label class="block text-slate-300 font-mono text-xs uppercase tracking-wider font-bold">
|
||||
Modul-Berechtigungen
|
||||
</label>
|
||||
<p class="text-[11px] text-slate-500 font-mono">Wähle aus, auf welche Projekte dieser Benutzer Zugriff hat.</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" onclick="selectAllModules(true)" class="text-[11px] font-mono text-emerald-400 hover:text-emerald-300 px-2 py-1 bg-emerald-500/10 rounded border border-emerald-500/20 transition-colors cursor-pointer">Alle auswählen</button>
|
||||
<button type="button" onclick="selectAllModules(false)" class="text-[11px] font-mono text-slate-400 hover:text-slate-300 px-2 py-1 bg-slate-800 rounded border border-slate-700 transition-colors cursor-pointer">Keine</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<?php foreach ($modules as $slug => $mod):
|
||||
$isChecked = in_array($slug, $userProjects, true);
|
||||
?>
|
||||
<label class="flex items-start gap-3 p-3.5 bg-slate-950/70 border border-slate-800 hover:border-emerald-500/50 rounded-xl cursor-pointer transition-all group">
|
||||
<input type="checkbox" name="projects[]" value="<?= htmlspecialchars($slug) ?>" <?= $isChecked ? 'checked' : '' ?> class="module-checkbox mt-1 w-4 h-4 accent-emerald-500">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between gap-1 mb-1">
|
||||
<span class="text-sm font-bold text-slate-200 font-mono flex items-center gap-1.5 truncate">
|
||||
<span><?= $mod['icon'] ?></span>
|
||||
<span><?= htmlspecialchars($mod['name']) ?></span>
|
||||
</span>
|
||||
<span class="text-[10px] font-mono px-1.5 py-0.5 rounded bg-slate-900 border border-slate-800 text-slate-400 shrink-0">
|
||||
<?= htmlspecialchars($mod['badge'] ?? $slug) ?>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-[11px] text-slate-500 font-mono line-clamp-2"><?= htmlspecialchars($mod['description']) ?></p>
|
||||
</div>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Buttons -->
|
||||
<div class="pt-4 border-t border-slate-800 flex items-center justify-between">
|
||||
<a href="<?= $baseUrl ?>/admin/users" class="text-slate-500 hover:text-slate-300 font-mono text-xs uppercase">
|
||||
Abbrechen
|
||||
</a>
|
||||
<button type="submit" class="bg-emerald-500/20 border border-emerald-500/50 text-emerald-400 hover:bg-emerald-500/30 hover:text-emerald-300 font-bold font-mono uppercase tracking-wider py-3 px-6 rounded-xl transition-all duration-300 shadow-[0_0_15px_rgba(16,185,129,0.15)] cursor-pointer">
|
||||
💾 Benutzer speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function selectAllModules(check) {
|
||||
document.querySelectorAll('.module-checkbox').forEach(function(cb) {
|
||||
cb.checked = check;
|
||||
});
|
||||
}
|
||||
function toggleAdminNotice(adminCheckbox) {
|
||||
// If admin is checked, all checkboxes are accessible anyway
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/../inc/admin_footer.php'; ?>
|
||||
|
|
@ -1,61 +1,241 @@
|
|||
<?php require_once __DIR__ . '/../inc/admin_header.php'; ?>
|
||||
<div class="max-w-6xl w-full mx-auto flex flex-col md:flex-row gap-4 sm:gap-6 p-4 sm:p-6">
|
||||
<?php
|
||||
$baseUrl = rtrim(Config::get('BASE_URL', 'https://philippurbschat.de/'), '/');
|
||||
$currentTab = $activeTab ?? ($_GET['tab'] ?? 'users');
|
||||
?>
|
||||
<div class="max-w-7xl w-full mx-auto flex flex-col md:flex-row gap-4 sm:gap-6 p-2 sm:p-4">
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="w-full md:w-64 bg-slate-900/80 backdrop-blur-sm border border-slate-700/50 rounded-xl shadow-lg p-4 h-fit shrink-0">
|
||||
<div class="flex items-center gap-2 mb-6 border-b border-slate-700/50 pb-3">
|
||||
<span class="text-xl">⚙️</span>
|
||||
<h2 class="text-emerald-400 font-mono font-bold text-lg tracking-tight">System_Admin</h2>
|
||||
</div>
|
||||
<nav class="flex flex-col gap-2 font-mono text-sm">
|
||||
<a href="<?= Config::get('BASE_URL', '/') ?>admin" class="px-3 py-2 hover:bg-slate-800 text-slate-400 hover:text-slate-200 rounded-md transition-all">> Dashboard</a>
|
||||
<a href="<?= Config::get('BASE_URL', '/') ?>admin/users" class="px-3 py-2 bg-emerald-500/10 text-emerald-400 rounded-md border border-emerald-500/20 transition-all">> Benutzer</a>
|
||||
<a href="<?= Config::get('BASE_URL', '/') ?>admin/settings" class="px-3 py-2 hover:bg-slate-800 text-slate-400 hover:text-slate-200 rounded-md transition-all">> Einstellungen</a>
|
||||
<a href="<?= $baseUrl ?>/admin" class="px-3 py-2 hover:bg-slate-800 text-slate-400 hover:text-slate-200 rounded-md transition-all">> Dashboard</a>
|
||||
<a href="<?= $baseUrl ?>/admin/users?tab=users" class="px-3 py-2 <?= $currentTab === 'users' ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'hover:bg-slate-800 text-slate-400 hover:text-slate-200' ?> rounded-md transition-all">> Benutzer (<?= count($users ?? []) ?>)</a>
|
||||
<a href="<?= $baseUrl ?>/admin/users?tab=invites" class="px-3 py-2 <?= $currentTab === 'invites' ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'hover:bg-slate-800 text-slate-400 hover:text-slate-200' ?> rounded-md transition-all">> Einladungslinks (<?= count($invitations ?? []) ?>)</a>
|
||||
<a href="<?= $baseUrl ?>/admin-tools" class="px-3 py-2 hover:bg-slate-800 text-cyan-400 hover:text-cyan-300 rounded-md transition-all">> Admin-Tools</a>
|
||||
<a href="<?= $baseUrl ?>/admin/settings" class="px-3 py-2 hover:bg-slate-800 text-slate-400 hover:text-slate-200 rounded-md transition-all">> Einstellungen</a>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="flex-1 min-w-0 bg-slate-900/80 backdrop-blur-sm border border-slate-700/50 rounded-xl shadow-2xl overflow-hidden flex flex-col">
|
||||
<div class="bg-slate-800/80 px-4 py-3 border-b border-slate-700/50 flex items-center justify-between">
|
||||
<!-- Top Toolbar -->
|
||||
<div class="bg-slate-800/80 px-4 py-3 border-b border-slate-700/50 flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-3 h-3 rounded-full bg-rose-500 hidden sm:block"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-amber-500 hidden sm:block"></div>
|
||||
<div class="w-3 h-3 rounded-full bg-emerald-500 hidden sm:block"></div>
|
||||
<div class="sm:ml-2 font-mono text-xs text-slate-400">philcore-users.sh</div>
|
||||
<div class="sm:ml-2 font-mono text-xs text-slate-400">auth-matrix.sh</div>
|
||||
</div>
|
||||
<a href="<?= Config::get('BASE_URL') ?>admin/user_form" class="text-xs font-mono bg-emerald-500/20 text-emerald-400 border border-emerald-500/50 px-3 py-1 rounded hover:bg-emerald-500/40 transition-colors whitespace-nowrap">+ NEW_USER</a>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="<?= $baseUrl ?>/admin/users?tab=users" class="text-xs font-mono px-3 py-1.5 rounded-lg border transition-all <?= $currentTab === 'users' ? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/50' : 'bg-slate-900/50 text-slate-400 border-slate-700 hover:text-slate-200' ?>">
|
||||
👥 Benutzer
|
||||
</a>
|
||||
<a href="<?= $baseUrl ?>/admin/users?tab=invites" class="text-xs font-mono px-3 py-1.5 rounded-lg border transition-all <?= $currentTab === 'invites' ? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/50' : 'bg-slate-900/50 text-slate-400 border-slate-700 hover:text-slate-200' ?>">
|
||||
✉️ Einladungslinks
|
||||
</a>
|
||||
<a href="<?= $baseUrl ?>/admin/user_form" class="text-xs font-mono bg-emerald-500/20 text-emerald-400 border border-emerald-500/50 px-3 py-1.5 rounded-lg hover:bg-emerald-500/40 transition-colors whitespace-nowrap">
|
||||
+ NEW_USER
|
||||
</a>
|
||||
</div>
|
||||
<div class="p-4 sm:p-8 flex-1">
|
||||
<header class="mb-6 sm:mb-8 border-b border-slate-800 pb-4 sm:pb-6">
|
||||
<h1 class="text-2xl sm:text-3xl font-extrabold text-transparent bg-clip-text bg-linear-to-r from-emerald-400 to-cyan-400 tracking-tight mb-2">> <?= htmlspecialchars($title) ?>_</h1>
|
||||
<p class="text-slate-400 font-mono text-xs sm:text-sm">Registrierte System-Accounts.</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 sm:p-6 flex-1">
|
||||
<?php if ($currentTab === 'invites'): ?>
|
||||
<!-- ================= TAB: EINLADUNGSLINKS ================= -->
|
||||
<header class="mb-6 border-b border-slate-800 pb-4">
|
||||
<h1 class="text-xl sm:text-2xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-emerald-400 to-cyan-400 font-mono tracking-tight mb-1">
|
||||
> Einladungs-System_
|
||||
</h1>
|
||||
<p class="text-slate-400 font-mono text-xs sm:text-sm">
|
||||
Erstelle geheime Registrierungs-Links mit vorkonfigurierten Modul-Zugriffsrechten. Nicht öffentlich & vor Suchmaschinen geschützt.
|
||||
</p>
|
||||
</header>
|
||||
<div class="overflow-x-auto border border-slate-800/80 rounded-lg -mx-4 sm:mx-0">
|
||||
<table class="w-full text-left font-mono text-xs sm:text-sm text-slate-400">
|
||||
|
||||
<!-- Generator Card -->
|
||||
<div class="bg-slate-950/70 border border-emerald-500/30 rounded-xl p-5 mb-8">
|
||||
<h2 class="text-sm font-bold font-mono text-emerald-400 uppercase tracking-wider mb-4 flex items-center gap-2">
|
||||
<span>⚡</span> Neuen Einladungslink generieren
|
||||
</h2>
|
||||
<form action="<?= $baseUrl ?>/admin/invite_create" method="POST" class="space-y-4">
|
||||
<input type="hidden" name="csrf_token" value="<?= Security::csrf() ?>">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-slate-400 font-mono text-xs uppercase mb-1.5">Notiz / Empfänger</label>
|
||||
<input type="text" name="note" placeholder="z.B. Für Peter Müller" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2.5 text-slate-200 text-xs font-mono focus:outline-none focus:border-emerald-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-slate-400 font-mono text-xs uppercase mb-1.5">Max. Nutzungen</label>
|
||||
<input type="number" name="max_uses" value="1" min="1" max="100" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2.5 text-slate-200 text-xs font-mono focus:outline-none focus:border-emerald-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-slate-400 font-mono text-xs uppercase mb-1.5">Gültigkeit in Tagen (optional)</label>
|
||||
<input type="number" name="days_valid" placeholder="z.B. 7 (leer = unbegrenzt)" min="1" max="365" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-2.5 text-slate-200 text-xs font-mono focus:outline-none focus:border-emerald-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-slate-400 font-mono text-xs uppercase mb-2">Automatisch freigeschaltete Module:</label>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-2">
|
||||
<?php foreach ($allModules as $slug => $mod): ?>
|
||||
<label class="flex items-center gap-2 p-2 bg-slate-900 border border-slate-800 hover:border-emerald-500/40 rounded-lg cursor-pointer transition-colors text-xs font-mono">
|
||||
<input type="checkbox" name="projects[]" value="<?= htmlspecialchars($slug) ?>" class="accent-emerald-500">
|
||||
<span><?= $mod['icon'] ?></span>
|
||||
<span class="text-slate-300 truncate"><?= htmlspecialchars($mod['name']) ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-2">
|
||||
<button type="submit" class="bg-emerald-500/20 border border-emerald-500/50 hover:bg-emerald-500/30 text-emerald-400 font-mono text-xs uppercase tracking-wider py-2.5 px-5 rounded-lg transition-all font-bold">
|
||||
Link generieren & Speichern →
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Invites Table -->
|
||||
<h3 class="text-xs font-mono uppercase text-slate-400 tracking-wider mb-3">Bestehende Einladungslinks</h3>
|
||||
<div class="overflow-x-auto border border-slate-800/80 rounded-lg">
|
||||
<table class="w-full text-left font-mono text-xs text-slate-400">
|
||||
<thead class="bg-slate-950/80 text-emerald-400 uppercase text-[10px]">
|
||||
<tr>
|
||||
<th class="px-3 py-2.5 border-b border-slate-800">Token & Link</th>
|
||||
<th class="px-3 py-2.5 border-b border-slate-800">Notiz</th>
|
||||
<th class="px-3 py-2.5 border-b border-slate-800">Vordefinierte Module</th>
|
||||
<th class="px-3 py-2.5 border-b border-slate-800">Nutzungen</th>
|
||||
<th class="px-3 py-2.5 border-b border-slate-800">Gültig bis</th>
|
||||
<th class="px-3 py-2.5 border-b border-slate-800">Status</th>
|
||||
<th class="px-3 py-2.5 border-b border-slate-800 text-right">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800/50 bg-slate-900/50">
|
||||
<?php if (empty($invitations)): ?>
|
||||
<tr><td colspan="7" class="px-4 py-4 text-center text-slate-500">Noch keine Einladungslinks erstellt.</td></tr>
|
||||
<?php else: foreach ($invitations as $inv):
|
||||
$fullUrl = $baseUrl . '/register?token=' . $inv['token'];
|
||||
?>
|
||||
<tr class="hover:bg-slate-800/40 transition-colors">
|
||||
<td class="px-3 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-slate-300 font-mono text-[11px] select-all"><?= substr($inv['token'], 0, 10) ?>...</span>
|
||||
<button type="button" onclick="copyInviteLink('<?= $fullUrl ?>', this)" class="px-2 py-0.5 bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 rounded text-[10px] transition-colors cursor-pointer" title="Kopieren">
|
||||
📋 Kopieren
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-3 text-slate-300"><?= htmlspecialchars($inv['note'] ?: '-') ?></td>
|
||||
<td class="px-3 py-3">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<?php if (empty($inv['assigned_projects'])): ?>
|
||||
<span class="text-slate-500">-</span>
|
||||
<?php else: foreach ($inv['assigned_projects'] as $slug):
|
||||
$mod = $allModules[$slug] ?? null;
|
||||
?>
|
||||
<span class="inline-flex items-center gap-1 px-1.5 py-0.5 bg-slate-950 border border-slate-800 rounded text-[10px] text-slate-300">
|
||||
<?= $mod['icon'] ?? '📦' ?> <?= htmlspecialchars($mod['name'] ?? $slug) ?>
|
||||
</span>
|
||||
<?php endforeach; endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-3 text-slate-300 font-mono"><?= $inv['uses_count'] ?> / <?= $inv['max_uses'] > 0 ? $inv['max_uses'] : '∞' ?></td>
|
||||
<td class="px-3 py-3 text-slate-400"><?= $inv['expires_at'] ? date('d.m.Y H:i', strtotime($inv['expires_at'])) : 'Unbegrenzt' ?></td>
|
||||
<td class="px-3 py-3">
|
||||
<?php if ($inv['is_valid']): ?>
|
||||
<span class="px-2 py-0.5 bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 rounded text-[10px]">Aktiv</span>
|
||||
<?php else: ?>
|
||||
<span class="px-2 py-0.5 bg-rose-500/10 text-rose-400 border border-rose-500/20 rounded text-[10px]">Inaktiv</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-3 py-3 text-right">
|
||||
<form action="<?= $baseUrl ?>/admin/invite_delete/<?= $inv['id'] ?>" method="POST" class="inline" onsubmit="return confirm('Diesen Einladungslink wirklich löschen?');">
|
||||
<input type="hidden" name="csrf_token" value="<?= Security::csrf() ?>">
|
||||
<button type="submit" class="text-rose-400 hover:text-rose-300 text-xs">Löschen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
<!-- ================= TAB: BENUTZERVERWALTUNG ================= -->
|
||||
<header class="mb-6 border-b border-slate-800 pb-4">
|
||||
<h1 class="text-xl sm:text-2xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-emerald-400 to-cyan-400 font-mono tracking-tight mb-1">
|
||||
> Benutzerverwaltung_
|
||||
</h1>
|
||||
<p class="text-slate-400 font-mono text-xs sm:text-sm">
|
||||
Registrierte Benutzer und individuelle Modulfreigaben.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="overflow-x-auto border border-slate-800/80 rounded-lg">
|
||||
<table class="w-full text-left font-mono text-xs text-slate-400">
|
||||
<thead class="bg-slate-950/80 text-emerald-400 text-[10px] sm:text-xs uppercase">
|
||||
<tr>
|
||||
<th class="px-3 sm:px-4 py-3 border-b border-slate-800/80 whitespace-nowrap">ID</th>
|
||||
<th class="px-3 sm:px-4 py-3 border-b border-slate-800/80 whitespace-nowrap md:whitespace-normal">Name</th>
|
||||
<th class="px-3 sm:px-4 py-3 border-b border-slate-800/80 whitespace-nowrap md:whitespace-normal">Email</th>
|
||||
<th class="px-3 sm:px-4 py-3 border-b border-slate-800/80 whitespace-nowrap md:whitespace-normal">Rolle</th>
|
||||
<th class="px-3 sm:px-4 py-3 border-b border-slate-800/80 whitespace-nowrap md:whitespace-normal">Projekte</th>
|
||||
<th class="px-3 sm:px-4 py-3 border-b border-slate-800/80 whitespace-nowrap">Status</th>
|
||||
<th class="px-3 sm:px-4 py-3 border-b border-slate-800/80 text-right whitespace-nowrap">Action</th>
|
||||
<th class="px-3 sm:px-4 py-3 border-b border-slate-800">ID</th>
|
||||
<th class="px-3 sm:px-4 py-3 border-b border-slate-800">Name</th>
|
||||
<th class="px-3 sm:px-4 py-3 border-b border-slate-800">Email</th>
|
||||
<th class="px-3 sm:px-4 py-3 border-b border-slate-800">Rolle</th>
|
||||
<th class="px-3 sm:px-4 py-3 border-b border-slate-800">Freigeschaltete Module</th>
|
||||
<th class="px-3 sm:px-4 py-3 border-b border-slate-800">Status</th>
|
||||
<th class="px-3 sm:px-4 py-3 border-b border-slate-800 text-right">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800/50 bg-slate-900/50">
|
||||
<?php if (empty($users)): ?>
|
||||
<tr><td colspan="7" class="px-4 py-4 text-center">Keine Benutzer gefunden.</td></tr>
|
||||
<tr><td colspan="7" class="px-4 py-4 text-center text-slate-500">Keine Benutzer vorhanden.</td></tr>
|
||||
<?php else: foreach ($users as $u): ?>
|
||||
<tr class="hover:bg-slate-800/50 transition-colors">
|
||||
<td class="px-3 sm:px-4 py-3 whitespace-nowrap">#<?= $u['id'] ?></td>
|
||||
<td class="px-3 sm:px-4 py-3 text-slate-200 text-xs sm:text-sm whitespace-nowrap md:whitespace-normal"><?= htmlspecialchars($u['name']) ?></td>
|
||||
<td class="px-3 sm:px-4 py-3 text-[10px] sm:text-xs text-slate-400 whitespace-nowrap md:whitespace-normal break-all"><?= htmlspecialchars($u['email']) ?></td>
|
||||
<td class="px-3 sm:px-4 py-3 whitespace-nowrap"><?= $u['is_admin'] ? '<span class="text-fuchsia-400">Admin</span>' : 'User' ?></td>
|
||||
<td class="px-3 sm:px-4 py-3 text-[10px] sm:text-xs text-slate-500 min-w-120px max-w-180px md:max-w-none truncate md:whitespace-normal md:wrap-break-words"><?php $proj = $u['projects'] ?? []; echo empty($proj) ? '-' : htmlspecialchars(implode(', ', $proj)); ?></td>
|
||||
<td class="px-3 sm:px-4 py-3 whitespace-nowrap"><span class="<?= $u['status'] === 'Active' ? 'text-emerald-500' : 'text-rose-500' ?>"><?= htmlspecialchars($u['status']) ?></span></td>
|
||||
<td class="px-3 sm:px-4 py-3 text-right whitespace-nowrap">
|
||||
<div class="flex justify-end gap-2 sm:gap-3">
|
||||
<a href="<?= Config::get('BASE_URL') ?>admin/user_form/<?= $u['id'] ?>" class="text-cyan-400 hover:text-cyan-300">Edit</a>
|
||||
<form action="<?= Config::get('BASE_URL') ?>admin/user_delete/<?= $u['id'] ?>" method="POST" class="inline" onsubmit="return confirm('Wirklich löschen?');">
|
||||
<tr class="hover:bg-slate-800/40 transition-colors">
|
||||
<td class="px-3 sm:px-4 py-3">#<?= $u['id'] ?></td>
|
||||
<td class="px-3 sm:px-4 py-3 text-slate-200 font-bold"><?= htmlspecialchars($u['name']) ?></td>
|
||||
<td class="px-3 sm:px-4 py-3 text-slate-400 break-all"><?= htmlspecialchars($u['email']) ?></td>
|
||||
<td class="px-3 sm:px-4 py-3">
|
||||
<?php if (!empty($u['is_admin'])): ?>
|
||||
<span class="px-2 py-0.5 bg-fuchsia-500/10 text-fuchsia-400 border border-fuchsia-500/30 rounded text-[10px] font-bold">Admin</span>
|
||||
<?php else: ?>
|
||||
<span class="text-slate-500">User</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="px-3 sm:px-4 py-3">
|
||||
<div class="flex flex-wrap gap-1 max-w-xs">
|
||||
<?php
|
||||
$proj = $u['projects'] ?? [];
|
||||
if (!empty($u['is_admin'])): ?>
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-fuchsia-950/60 border border-fuchsia-700/40 rounded text-[10px] text-fuchsia-300">
|
||||
★ Alle Module (Vollzugriff)
|
||||
</span>
|
||||
<?php elseif (empty($proj)): ?>
|
||||
<span class="text-slate-600 italic">Keine Module</span>
|
||||
<?php else: foreach ($proj as $slug):
|
||||
$mod = $allModules[$slug] ?? null;
|
||||
?>
|
||||
<span class="inline-flex items-center gap-1 px-1.5 py-0.5 bg-slate-950 border border-slate-800 rounded text-[10px] text-slate-300">
|
||||
<?= $mod['icon'] ?? '📦' ?> <?= htmlspecialchars($mod['name'] ?? $slug) ?>
|
||||
</span>
|
||||
<?php endforeach; endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 sm:px-4 py-3">
|
||||
<form action="<?= $baseUrl ?>/admin/user_toggle_status/<?= $u['id'] ?>" method="POST" class="inline">
|
||||
<input type="hidden" name="csrf_token" value="<?= Security::csrf() ?>">
|
||||
<button type="submit" class="text-rose-400 hover:text-rose-300">Del</button>
|
||||
<button type="submit" class="px-2.5 py-1 rounded text-[11px] font-mono border transition-all cursor-pointer <?= $u['status'] === 'Active' ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/30 hover:bg-emerald-500/20' : 'bg-rose-500/10 text-rose-400 border-rose-500/30 hover:bg-rose-500/20' ?>" title="Klicken zum Umschalten">
|
||||
<?= $u['status'] === 'Active' ? '● Aktiv' : '○ Inaktiv' ?>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td class="px-3 sm:px-4 py-3 text-right whitespace-nowrap">
|
||||
<div class="flex justify-end items-center gap-3">
|
||||
<a href="<?= $baseUrl ?>/admin/user_form/<?= $u['id'] ?>" class="text-cyan-400 hover:text-cyan-300">Bearbeiten</a>
|
||||
<form action="<?= $baseUrl ?>/admin/user_delete/<?= $u['id'] ?>" method="POST" class="inline" onsubmit="return confirm('Benutzer <?= htmlspecialchars(addslashes($u['name'])) ?> wirklich löschen?');">
|
||||
<input type="hidden" name="csrf_token" value="<?= Security::csrf() ?>">
|
||||
<button type="submit" class="text-rose-400 hover:text-rose-300">Löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -64,7 +244,48 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyInviteLink(url, btn) {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(url).then(showSuccess, fallback);
|
||||
} else {
|
||||
fallback();
|
||||
}
|
||||
|
||||
function fallback() {
|
||||
var textArea = document.createElement("textarea");
|
||||
textArea.value = url;
|
||||
textArea.style.position = "fixed";
|
||||
textArea.style.left = "-999999px";
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
showSuccess();
|
||||
} catch (err) {
|
||||
alert('Link konnte nicht kopiert werden: ' + url);
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
function showSuccess() {
|
||||
var originalText = btn.innerHTML;
|
||||
btn.innerHTML = '✓ Kopiert!';
|
||||
btn.classList.remove('bg-emerald-500/10', 'text-emerald-400');
|
||||
btn.classList.add('bg-emerald-500', 'text-slate-950', 'font-bold');
|
||||
setTimeout(function() {
|
||||
btn.innerHTML = originalText;
|
||||
btn.classList.remove('bg-emerald-500', 'text-slate-950', 'font-bold');
|
||||
btn.classList.add('bg-emerald-500/10', 'text-emerald-400');
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/../inc/admin_footer.php'; ?>
|
||||
|
|
@ -38,22 +38,45 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 sm:gap-10 mb-16">
|
||||
<?php if (isset($_SESSION['user_email'])): ?>
|
||||
<?php if (isset($_SESSION['user_email'])):
|
||||
$isAdmin = !empty($_SESSION['is_admin']);
|
||||
$userProjects = $_SESSION['user_projects'] ?? [];
|
||||
$accessibleModules = ModuleService::getAccessibleForUser($userProjects, $isAdmin);
|
||||
?>
|
||||
<div class="p-5 sm:p-8 bg-slate-900/80 border border-emerald-500/20 rounded-3xl sm:rounded-[2.5rem] backdrop-blur-xl shadow-[0_0_30px_rgba(16,185,129,0.15)] relative group overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-linear-to-r from-transparent via-emerald-500/40 to-transparent"></div>
|
||||
<div class="flex justify-between items-start mb-6">
|
||||
<h2 class="text-slate-300 text-xs font-bold uppercase tracking-[0.2em] font-mono">Projects</h2>
|
||||
<div>
|
||||
<h2 class="text-slate-300 text-xs font-bold uppercase tracking-[0.2em] font-mono">Module & Projekte</h2>
|
||||
<p class="text-[11px] font-mono text-slate-400 mt-0.5">
|
||||
<?= htmlspecialchars($_SESSION['user_email']) ?>
|
||||
<?php if ($isAdmin): ?>
|
||||
<span class="ml-1 text-[10px] px-1.5 py-0.5 bg-fuchsia-500/10 text-fuchsia-400 border border-fuchsia-500/30 rounded">ADMIN</span>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
<a href="<?= Config::get('BASE_URL', '/') ?>login/logout" class="text-xs font-mono text-rose-400 hover:text-rose-300 transition-colors uppercase tracking-wider">Logout</a>
|
||||
</div>
|
||||
<p class="text-slate-400 text-sm mb-6 sm:mb-8 font-mono"><span class="text-emerald-500 animate-pulse">/</span> access_granted</p>
|
||||
<div class="space-y-4 relative z-10">
|
||||
<?php $projects = $_SESSION['user_projects'] ?? [];
|
||||
if (empty($projects)): ?>
|
||||
<p class="text-slate-400 font-mono text-sm">Keine aktiven Projekte gefunden.</p>
|
||||
<?php else: foreach ($projects as $project): ?>
|
||||
<a href="<?= Config::get('BASE_URL', '/') ?><?= htmlspecialchars($project) ?>" class="flex items-center justify-between w-full bg-slate-950/80 border border-slate-700/60 hover:border-emerald-500/50 rounded-2xl p-4 text-slate-200 text-sm transition-all shadow-lg hover:shadow-emerald-500/20 group/btn">
|
||||
<span class="font-mono font-bold uppercase tracking-wider"><?= htmlspecialchars($project) ?></span>
|
||||
<span class="text-slate-500 group-hover/btn:text-emerald-400 group-hover/btn:translate-x-1 transition-all">→</span>
|
||||
<p class="text-slate-400 text-xs mb-4 font-mono"><span class="text-emerald-500 animate-pulse">/</span> access_granted – Freigeschaltete Systeme:</p>
|
||||
<div class="space-y-3 relative z-10">
|
||||
<?php if (empty($accessibleModules)): ?>
|
||||
<div class="p-4 bg-slate-950/60 border border-slate-800 rounded-2xl text-center">
|
||||
<p class="text-slate-400 font-mono text-xs">Aktuell sind keine Module für deinen Account freigeschaltet.</p>
|
||||
<p class="text-slate-600 font-mono text-[10px] mt-1">Wende dich an Philipp, um Zugriff auf Tools zu erhalten.</p>
|
||||
</div>
|
||||
<?php else: foreach ($accessibleModules as $mod): ?>
|
||||
<a href="<?= Config::get('BASE_URL', '/') ?><?= ltrim($mod['path'], '/') ?>" class="flex items-center justify-between w-full bg-slate-950/80 border border-slate-700/60 hover:border-emerald-500/50 rounded-2xl p-3.5 sm:p-4 text-slate-200 text-sm transition-all shadow-lg hover:shadow-emerald-500/20 group/btn">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<span class="text-2xl shrink-0"><?= $mod['icon'] ?></span>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-mono font-bold tracking-wider text-slate-100 group-hover/btn:text-emerald-400 transition-colors"><?= htmlspecialchars($mod['name']) ?></span>
|
||||
<span class="text-[10px] font-mono px-1.5 py-0.5 rounded bg-slate-900 border border-slate-800 text-slate-400 shrink-0"><?= htmlspecialchars($mod['badge']) ?></span>
|
||||
</div>
|
||||
<p class="text-[11px] text-slate-500 font-mono truncate"><?= htmlspecialchars($mod['description']) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-slate-500 group-hover/btn:text-emerald-400 group-hover/btn:translate-x-1 transition-all shrink-0 ml-2">→</span>
|
||||
</a>
|
||||
<?php endforeach; endif; ?>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,20 +5,26 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#020617">
|
||||
<title>Admin | <?= htmlspecialchars(Config::get('APP_NAME', 'PhilCore')) ?></title>
|
||||
<link rel="stylesheet" href="/css/app.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="shortcut icon" href="/favicon.ico">
|
||||
</head>
|
||||
<body class="bg-[#0b1120] text-slate-300 min-h-screen bg-grid-admin font-sans">
|
||||
<body class="bg-[#0b1120] text-slate-300 min-h-screen bg-grid-admin font-sans selection-pink">
|
||||
<header class="p-4 border-b border-slate-800 bg-slate-900/50 backdrop-blur-md sticky top-0 z-50">
|
||||
<div class="container mx-auto flex justify-between items-center">
|
||||
<div class="flex items-center gap-2 text-emerald-400 font-bold">
|
||||
<a href="<?= Config::get('BASE_URL', '/') ?>admin" class="flex items-center gap-2 hover:text-emerald-300 transition-colors">
|
||||
👾 <span class="tracking-tight"><?= htmlspecialchars(Config::get('APP_NAME', 'PhilCore')) ?></span>
|
||||
<span class="text-slate-600 ml-2 text-xs font-mono">/ Admin</span>
|
||||
<span class="text-slate-600 ml-1 text-xs font-mono">/ Admin</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 text-xs font-mono">
|
||||
<a href="<?= Config::get('BASE_URL', '/') ?>admin-tools" class="text-cyan-400 hover:text-cyan-300 transition-colors flex items-center gap-1">🛠️ Admin-Tools</a>
|
||||
<a href="<?= Config::get('BASE_URL', '/') ?>" class="text-slate-400 hover:text-emerald-400 transition-colors">← Zurück zur Website</a>
|
||||
</div>
|
||||
<a href="<?= Config::get('BASE_URL', '/') ?>" class="text-xs font-mono text-slate-500 hover:text-emerald-400 transition-colors">Home</a>
|
||||
</div>
|
||||
</header>
|
||||
<?php Flash::display(); ?>
|
||||
|
|
|
|||
121
home/views/register.php
Normal file
121
home/views/register.php
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
$baseUrl = rtrim(Config::get('BASE_URL', 'https://philippurbschat.de/'), '/');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<meta name="theme-color" content="#020617">
|
||||
<title><?= htmlspecialchars($title ?? 'Registrierung') ?> | Philipp Urbschat</title>
|
||||
<link rel="stylesheet" href="/css/app.css">
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
</head>
|
||||
<body class="bg-[#0b1120] text-slate-300 min-h-screen flex flex-col justify-center items-center p-4 sm:p-6 font-sans relative overflow-x-hidden selection-pink">
|
||||
<div class="crt-overlay pointer-events-none"></div>
|
||||
|
||||
<div class="max-w-md w-full my-8 relative z-10">
|
||||
<!-- Logo / Brand Header -->
|
||||
<div class="text-center mb-8">
|
||||
<a href="<?= $baseUrl ?>" class="inline-flex items-center gap-2 text-slate-200 hover:text-emerald-400 font-mono text-sm uppercase tracking-widest transition-colors">
|
||||
<span class="text-emerald-400 font-bold">👾</span> philippurbschat.de
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php Flash::display(); ?>
|
||||
|
||||
<?php if (!empty($valid)): ?>
|
||||
<!-- Valid Invitation Card -->
|
||||
<div class="bg-slate-900/90 border border-emerald-500/30 rounded-3xl p-6 sm:p-8 backdrop-blur-xl shadow-[0_0_40px_rgba(16,185,129,0.12)] relative overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-emerald-500 to-transparent"></div>
|
||||
|
||||
<div class="mb-6">
|
||||
<span class="inline-block px-3 py-1 bg-emerald-500/10 border border-emerald-500/30 rounded-full text-emerald-400 font-mono text-xs uppercase tracking-wider mb-3">
|
||||
✓ Einladung bestätigt
|
||||
</span>
|
||||
<h1 class="text-2xl font-black text-slate-100 font-mono tracking-tight">Account erstellen</h1>
|
||||
<p class="text-slate-400 text-sm mt-1">Vergib dein persönliches Passwort, um deinen Zugang zu aktivieren.</p>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($note)): ?>
|
||||
<div class="bg-slate-950/70 border border-slate-800 rounded-xl p-3 mb-5 font-mono text-xs text-slate-400">
|
||||
<span class="text-emerald-400">Hinweis:</span> <?= htmlspecialchars($note) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($assignedModules)): ?>
|
||||
<div class="mb-6">
|
||||
<label class="block text-slate-400 font-mono text-[11px] uppercase tracking-wider mb-2">Für dich freigeschaltete Module:</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<?php foreach ($assignedModules as $mod): ?>
|
||||
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-slate-950/80 border border-slate-700/80 rounded-xl text-xs font-mono text-slate-200">
|
||||
<span><?= $mod['icon'] ?></span>
|
||||
<span><?= htmlspecialchars($mod['name']) ?></span>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form action="<?= $baseUrl ?>/register/submit" method="POST" class="space-y-4">
|
||||
<input type="hidden" name="csrf_token" value="<?= Security::csrf() ?>">
|
||||
<input type="hidden" name="token" value="<?= htmlspecialchars($token) ?>">
|
||||
|
||||
<div>
|
||||
<label class="block text-slate-400 font-mono text-xs uppercase tracking-wider mb-1.5">Dein Name</label>
|
||||
<input type="text" name="name" required placeholder="z.B. Alex" autocomplete="name" class="w-full bg-slate-950/80 border border-slate-700/80 rounded-xl p-3.5 text-slate-200 text-sm focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all placeholder:text-slate-600 font-mono">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-slate-400 font-mono text-xs uppercase tracking-wider mb-1.5">E-Mail-Adresse</label>
|
||||
<input type="email" name="email" required placeholder="name@beispiel.de" autocomplete="email" class="w-full bg-slate-950/80 border border-slate-700/80 rounded-xl p-3.5 text-slate-200 text-sm focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all placeholder:text-slate-600 font-mono">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-slate-400 font-mono text-xs uppercase tracking-wider mb-1.5">Passwort</label>
|
||||
<input type="password" name="password" required minlength="8" placeholder="Mindestens 8 Zeichen" autocomplete="new-password" class="w-full bg-slate-950/80 border border-slate-700/80 rounded-xl p-3.5 text-slate-200 text-sm focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all placeholder:text-slate-600 font-mono">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-slate-400 font-mono text-xs uppercase tracking-wider mb-1.5">Passwort wiederholen</label>
|
||||
<input type="password" name="password_confirm" required minlength="8" placeholder="Passwort bestätigen" autocomplete="new-password" class="w-full bg-slate-950/80 border border-slate-700/80 rounded-xl p-3.5 text-slate-200 text-sm focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-all placeholder:text-slate-600 font-mono">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full cursor-pointer bg-emerald-500/20 border border-emerald-500/50 hover:bg-emerald-500/30 text-emerald-400 hover:text-emerald-300 font-bold font-mono uppercase tracking-wider py-4 rounded-xl transition-all duration-300 transform active:scale-95 shadow-[0_0_20px_rgba(16,185,129,0.15)] mt-6">
|
||||
Account freischalten →
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 pt-4 border-t border-slate-800/80 text-center">
|
||||
<span class="text-xs font-mono text-slate-500">Bereits registriert?</span>
|
||||
<a href="<?= $baseUrl ?>" class="text-xs font-mono text-emerald-400 hover:underline ml-1">Zum Login</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<!-- Invalid / Restricted Access Card -->
|
||||
<div class="bg-slate-900/90 border border-rose-500/30 rounded-3xl p-6 sm:p-8 backdrop-blur-xl shadow-[0_0_40px_rgba(244,63,94,0.15)] relative overflow-hidden text-center">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-rose-500 to-transparent"></div>
|
||||
|
||||
<div class="w-14 h-14 bg-rose-500/10 border border-rose-500/30 rounded-2xl flex items-center justify-center mx-auto mb-4 text-2xl">
|
||||
🔒
|
||||
</div>
|
||||
|
||||
<h1 class="text-2xl font-black text-slate-100 font-mono tracking-tight mb-3">Zugriff beschränkt</h1>
|
||||
<p class="text-slate-400 text-sm font-mono mb-6 leading-relaxed">
|
||||
<?= htmlspecialchars($errorMessage ?? 'Registrierung nur mit gültigem Einladungslink möglich.') ?>
|
||||
</p>
|
||||
|
||||
<div class="pt-4 border-t border-slate-800">
|
||||
<a href="<?= $baseUrl ?>" class="inline-flex items-center gap-2 px-5 py-2.5 bg-slate-800/80 hover:bg-slate-700/80 text-slate-200 rounded-xl text-xs font-mono uppercase tracking-wider transition-colors">
|
||||
← Zur Startseite
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in a new issue