This commit is contained in:
@@ -43,10 +43,10 @@ return [
|
||||
'icon' => '🎒',
|
||||
],
|
||||
[
|
||||
'slug' => 'freizeitideen',
|
||||
'title' => 'Freizeitideen',
|
||||
'description' => 'Ideen für Ausflüge, Beschäftigung, schlechtes Wetter und spontane freie Nachmittage.',
|
||||
'icon' => '🌤️',
|
||||
'slug' => 'sonstiges',
|
||||
'title' => 'Sonstiges',
|
||||
'description' => 'Alles, was thematisch dazugehört, aber in keinen der anderen Bereiche sauber hineinpasst.',
|
||||
'icon' => '🧩',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -30,6 +30,28 @@ $allowNoKidsChecked = $editEvent ? ((int)$editEvent['allow_kids'] === 0) : false
|
||||
|
||||
<div class="container dash-section">
|
||||
<div class="dash-grid">
|
||||
<?php if (!empty($communityIsSiteAdmin)): ?>
|
||||
<div class="card dash-card">
|
||||
<div class="badge">Admin</div>
|
||||
<h3>Community-Migration</h3>
|
||||
<?php if ($communityMigrationStatus): ?>
|
||||
<ul class="dash-list">
|
||||
<li>Status: <?= !empty($communityMigrationStatus['complete']) ? 'Vollständig eingerichtet' : 'Migration noch offen' ?></li>
|
||||
<li>Fehlende Bausteine: <?= !empty($communityMigrationStatus['missing']) ? htmlspecialchars(implode(', ', $communityMigrationStatus['missing']), ENT_QUOTES) : 'Keine' ?></li>
|
||||
</ul>
|
||||
<?php else: ?>
|
||||
<p class="muted small">Migrationsstatus konnte nicht ermittelt werden.</p>
|
||||
<?php endif; ?>
|
||||
<form method="post" style="margin-top:12px;">
|
||||
<input type="hidden" name="action" value="community_migrate">
|
||||
<button class="btn" type="submit">Community-Migration ausführen</button>
|
||||
</form>
|
||||
<p class="muted small" style="margin-top:10px;">
|
||||
Führt die für Rollen, Bewerbungen, Meldungen, Bewertungen und Forenstruktur benötigten Datenbank-Erweiterungen aus.
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card dash-card">
|
||||
<div class="badge">Community</div>
|
||||
<h3>Dein Community-Status</h3>
|
||||
|
||||
102
public/page/staging-users.php
Normal file
102
public/page/staging-users.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
if (!defined('APP_ENV') || APP_ENV !== 'staging') {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
$app = app();
|
||||
$pdo = $app->pdo();
|
||||
$users = [];
|
||||
$error = '';
|
||||
|
||||
try {
|
||||
if (!$pdo) {
|
||||
throw new RuntimeException('Keine Datenbankverbindung verfügbar.');
|
||||
}
|
||||
|
||||
$hasRolesTable = false;
|
||||
try {
|
||||
$stmt = $pdo->query("SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'user_roles' LIMIT 1");
|
||||
$hasRolesTable = (bool)$stmt->fetchColumn();
|
||||
} catch (Throwable) {
|
||||
$hasRolesTable = false;
|
||||
}
|
||||
|
||||
$sql = '
|
||||
SELECT u.id,
|
||||
u.email,
|
||||
u.status,
|
||||
u.created_at,
|
||||
COALESCE(p.display_name, "") AS display_name
|
||||
FROM users u
|
||||
LEFT JOIN user_profiles p ON p.user_id = u.id
|
||||
ORDER BY u.id ASC
|
||||
';
|
||||
$stmt = $pdo->query($sql);
|
||||
$users = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||
|
||||
if ($hasRolesTable && $users) {
|
||||
$roleStmt = $pdo->query('SELECT user_id, role FROM user_roles ORDER BY user_id ASC, role ASC');
|
||||
$rolesByUser = [];
|
||||
foreach ($roleStmt->fetchAll(PDO::FETCH_ASSOC) ?: [] as $row) {
|
||||
$rolesByUser[(int)$row['user_id']][] = (string)$row['role'];
|
||||
}
|
||||
foreach ($users as &$user) {
|
||||
$user['roles'] = $rolesByUser[(int)$user['id']] ?? [];
|
||||
}
|
||||
unset($user);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
?>
|
||||
<main class="section">
|
||||
<div class="container">
|
||||
<div class="section__intro">
|
||||
<h1>Staging: Registrierte Benutzer</h1>
|
||||
</div>
|
||||
|
||||
<div class="card dash-card">
|
||||
<?php if ($error !== ''): ?>
|
||||
<div class="toast-bar" style="border-color:#f87171; color:#991b1b; margin-bottom:12px;">
|
||||
Fehler: <?= htmlspecialchars($error, ENT_QUOTES) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<p class="muted small">Diese Seite ist nur im Staging verfügbar.</p>
|
||||
|
||||
<?php if (!$users): ?>
|
||||
<p class="muted">Keine Benutzer gefunden.</p>
|
||||
<?php else: ?>
|
||||
<div style="overflow:auto; margin-top:14px;">
|
||||
<table style="width:100%; border-collapse:collapse;">
|
||||
<thead>
|
||||
<tr style="text-align:left; border-bottom:1px solid var(--color-border);">
|
||||
<th style="padding:10px 8px;">ID</th>
|
||||
<th style="padding:10px 8px;">Anzeigename</th>
|
||||
<th style="padding:10px 8px;">E-Mail</th>
|
||||
<th style="padding:10px 8px;">Status</th>
|
||||
<th style="padding:10px 8px;">Registriert</th>
|
||||
<th style="padding:10px 8px;">Rollen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<tr style="border-bottom:1px solid var(--color-border);">
|
||||
<td style="padding:10px 8px; white-space:nowrap;"><?= (int)$user['id'] ?></td>
|
||||
<td style="padding:10px 8px;"><?= htmlspecialchars((string)$user['display_name'], ENT_QUOTES) ?></td>
|
||||
<td style="padding:10px 8px;"><?= htmlspecialchars((string)$user['email'], ENT_QUOTES) ?></td>
|
||||
<td style="padding:10px 8px;"><?= htmlspecialchars((string)$user['status'], ENT_QUOTES) ?></td>
|
||||
<td style="padding:10px 8px; white-space:nowrap;"><?= htmlspecialchars((string)$user['created_at'], ENT_QUOTES) ?></td>
|
||||
<td style="padding:10px 8px;"><?= htmlspecialchars(implode(', ', $user['roles'] ?? []), ENT_QUOTES) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -157,6 +157,7 @@ final class AccountPages
|
||||
$communityConfig = file_exists($communityCfg) ? require $communityCfg : [];
|
||||
$community = $pdo ? new Community($pdo, $communityConfig) : null;
|
||||
$communityAccess = $pdo ? new CommunityAccess($pdo, $communityConfig) : null;
|
||||
$communityMigration = $pdo ? new CommunityMigration($pdo) : null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
@@ -296,6 +297,12 @@ final class AccountPages
|
||||
}
|
||||
$communityAccess->submitApplication($userId, (string)($_POST['motivation'] ?? ''));
|
||||
$info = 'Deine Bewerbung wurde eingereicht.';
|
||||
} elseif ($action === 'community_migrate') {
|
||||
if (!$communityAccess || !$communityMigration || !$communityAccess->canManageApplications($userId)) {
|
||||
throw new \RuntimeException('Keine Berechtigung für die Community-Migration.');
|
||||
}
|
||||
$communityMigration->apply();
|
||||
$info = 'Die Community-Migration wurde ausgeführt.';
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$error = $e->getMessage();
|
||||
@@ -377,6 +384,8 @@ final class AccountPages
|
||||
'reply_blocked' => false,
|
||||
'reason' => null,
|
||||
];
|
||||
$communityIsSiteAdmin = $communityAccess ? $communityAccess->canManageApplications($userId) : false;
|
||||
$communityMigrationStatus = ($communityMigration && $communityIsSiteAdmin) ? $communityMigration->status() : null;
|
||||
|
||||
return compact(
|
||||
'flash',
|
||||
@@ -392,7 +401,9 @@ final class AccountPages
|
||||
'communityRoles',
|
||||
'communityApplication',
|
||||
'communityCanApply',
|
||||
'communityRestrictions'
|
||||
'communityRestrictions',
|
||||
'communityIsSiteAdmin',
|
||||
'communityMigrationStatus'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -140,16 +140,12 @@ final class Community
|
||||
$params[$ph2] = '%' . $tok . '%';
|
||||
$i++;
|
||||
}
|
||||
$defaultBoard = $this->getBoardBySlug('wochenendideen');
|
||||
$boardJoin = '';
|
||||
$boardSelect = "NULL AS board_slug, NULL AS board_title, NULL AS category_slug, NULL AS category_title";
|
||||
$boardWhere = '';
|
||||
if ($this->hasColumn('forum_threads', 'board_id') && $this->hasTable('forum_boards') && $this->hasTable('forum_categories')) {
|
||||
$boardJoin = ' LEFT JOIN forum_boards fb ON fb.id = ft.board_id LEFT JOIN forum_categories fc ON fc.id = fb.category_id ';
|
||||
$boardSelect = 'fb.slug AS board_slug, fb.title AS board_title, fc.slug AS category_slug, fc.title AS category_title';
|
||||
if ($boardSlug !== null && $boardSlug !== '') {
|
||||
$boardWhere = ' AND fb.slug = :boardSlug';
|
||||
$params[':boardSlug'] = $boardSlug;
|
||||
}
|
||||
}
|
||||
|
||||
$where = $conditions ? ('AND ' . implode(' AND ', $conditions)) : '';
|
||||
@@ -182,7 +178,7 @@ final class Community
|
||||
JOIN users u ON u.id = ft.user_id
|
||||
LEFT JOIN user_profiles p ON p.user_id = u.id
|
||||
$boardJoin
|
||||
WHERE 1=1 $where $boardWhere
|
||||
WHERE 1=1 $where
|
||||
ORDER BY ft.created_at DESC
|
||||
LIMIT :lim";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
@@ -191,7 +187,12 @@ final class Community
|
||||
}
|
||||
$stmt->bindValue(':lim', $limit, \PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
|
||||
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC) ?: [];
|
||||
$rows = array_map(fn(array $row) => $this->applyFallbackBoard($row, $defaultBoard), $rows);
|
||||
if ($boardSlug !== null && $boardSlug !== '') {
|
||||
$rows = array_values(array_filter($rows, fn(array $row): bool => (string)($row['board_slug'] ?? '') === $boardSlug));
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function listThreads(int $limit = 50, ?string $boardSlug = null): array
|
||||
@@ -210,7 +211,10 @@ final class Community
|
||||
$stmt = $this->pdo->prepare("SELECT $select FROM forum_threads ft LEFT JOIN user_profiles p ON p.user_id = ft.user_id $join WHERE ft.id = :id");
|
||||
$stmt->execute([':id' => $id]);
|
||||
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
return $row ?: null;
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
return $this->applyFallbackBoard($row, $this->getBoardBySlug('wochenendideen'));
|
||||
}
|
||||
|
||||
public function listPosts(int $threadId): array
|
||||
@@ -477,6 +481,19 @@ final class Community
|
||||
}
|
||||
}
|
||||
|
||||
private function applyFallbackBoard(array $row, ?array $defaultBoard): array
|
||||
{
|
||||
if (!empty($row['board_slug']) || !$defaultBoard) {
|
||||
return $row;
|
||||
}
|
||||
|
||||
$row['board_slug'] = $defaultBoard['slug'] ?? null;
|
||||
$row['board_title'] = $defaultBoard['title'] ?? null;
|
||||
$row['category_slug'] = $defaultBoard['category_slug'] ?? null;
|
||||
$row['category_title'] = $defaultBoard['category_title'] ?? null;
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function hasTable(string $table): bool
|
||||
{
|
||||
if (array_key_exists($table, $this->tableCache)) {
|
||||
|
||||
154
src/App/CommunityMigration.php
Normal file
154
src/App/CommunityMigration.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
final class CommunityMigration
|
||||
{
|
||||
public function __construct(private \PDO $pdo)
|
||||
{
|
||||
}
|
||||
|
||||
public function status(): array
|
||||
{
|
||||
$checks = [
|
||||
'forum_categories' => $this->hasTable('forum_categories'),
|
||||
'forum_boards' => $this->hasTable('forum_boards'),
|
||||
'forum_post_feedback' => $this->hasTable('forum_post_feedback'),
|
||||
'forum_reports' => $this->hasTable('forum_reports'),
|
||||
'user_roles' => $this->hasTable('user_roles'),
|
||||
'community_admin_applications' => $this->hasTable('community_admin_applications'),
|
||||
'community_user_restrictions' => $this->hasTable('community_user_restrictions'),
|
||||
'forum_threads.board_id' => $this->hasColumn('forum_threads', 'board_id'),
|
||||
'forum_posts.highlighted_by' => $this->hasColumn('forum_posts', 'highlighted_by'),
|
||||
'forum_posts.highlighted_at' => $this->hasColumn('forum_posts', 'highlighted_at'),
|
||||
];
|
||||
|
||||
$missing = array_keys(array_filter($checks, static fn(bool $ok): bool => !$ok));
|
||||
|
||||
return [
|
||||
'complete' => $missing === [],
|
||||
'checks' => $checks,
|
||||
'missing' => $missing,
|
||||
];
|
||||
}
|
||||
|
||||
public function apply(): array
|
||||
{
|
||||
$statements = [
|
||||
'CREATE TABLE IF NOT EXISTS forum_categories (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
slug VARCHAR(120) NOT NULL UNIQUE,
|
||||
title VARCHAR(180) NOT NULL,
|
||||
sort_order SMALLINT UNSIGNED NOT NULL DEFAULT 0
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||||
|
||||
'CREATE TABLE IF NOT EXISTS forum_boards (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
category_id BIGINT UNSIGNED NOT NULL,
|
||||
slug VARCHAR(120) NOT NULL UNIQUE,
|
||||
title VARCHAR(180) NOT NULL,
|
||||
description TEXT NULL,
|
||||
icon VARCHAR(32) NULL,
|
||||
sort_order SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
INDEX idx_fb_category (category_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||||
|
||||
'ALTER TABLE forum_threads
|
||||
ADD COLUMN IF NOT EXISTS board_id BIGINT UNSIGNED NULL,
|
||||
ADD INDEX IF NOT EXISTS idx_ft_board (board_id)',
|
||||
|
||||
'ALTER TABLE forum_posts
|
||||
ADD COLUMN IF NOT EXISTS highlighted_by BIGINT UNSIGNED NULL,
|
||||
ADD COLUMN IF NOT EXISTS highlighted_at DATETIME NULL',
|
||||
|
||||
'CREATE TABLE IF NOT EXISTS forum_post_feedback (
|
||||
post_id BIGINT UNSIGNED NOT NULL,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
value TINYINT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (post_id, user_id),
|
||||
INDEX idx_fpf_value (value)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||||
|
||||
'CREATE TABLE IF NOT EXISTS forum_reports (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
reporter_user_id BIGINT UNSIGNED NOT NULL,
|
||||
target_type ENUM("thread","post") NOT NULL,
|
||||
target_id BIGINT UNSIGNED NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
status ENUM("open","resolved","dismissed") NOT NULL DEFAULT "open",
|
||||
moderator_user_id BIGINT UNSIGNED NULL,
|
||||
moderator_note TEXT NULL,
|
||||
resolved_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_fr_status (status),
|
||||
INDEX idx_fr_target (target_type, target_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||||
|
||||
'CREATE TABLE IF NOT EXISTS user_roles (
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
role ENUM("forum_admin","site_admin","owner") NOT NULL,
|
||||
assigned_by BIGINT UNSIGNED NULL,
|
||||
assigned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (user_id, role)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||||
|
||||
'CREATE TABLE IF NOT EXISTS community_admin_applications (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
motivation TEXT NOT NULL,
|
||||
status ENUM("open","approved","rejected") NOT NULL DEFAULT "open",
|
||||
decision_reason TEXT NULL,
|
||||
decided_by BIGINT UNSIGNED NULL,
|
||||
decided_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_caa_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||||
|
||||
'CREATE TABLE IF NOT EXISTS community_user_restrictions (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
restriction_type ENUM("thread_create_blocked","reply_blocked") NOT NULL,
|
||||
reason TEXT NULL,
|
||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_by BIGINT UNSIGNED NOT NULL,
|
||||
expires_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_cur_user_active (user_id, active)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||||
];
|
||||
|
||||
foreach ($statements as $sql) {
|
||||
$this->pdo->exec($sql);
|
||||
}
|
||||
|
||||
// Initial owner/site-admin bootstrap for user 1 if roles table exists.
|
||||
if ($this->hasTable('user_roles')) {
|
||||
$stmt = $this->pdo->prepare('INSERT IGNORE INTO user_roles (user_id, role, assigned_by) VALUES (1, "owner", 1)');
|
||||
$stmt->execute();
|
||||
$stmt = $this->pdo->prepare('INSERT IGNORE INTO user_roles (user_id, role, assigned_by) VALUES (1, "site_admin", 1)');
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
return $this->status();
|
||||
}
|
||||
|
||||
private function hasTable(string $table): bool
|
||||
{
|
||||
$stmt = $this->pdo->prepare('SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = :table LIMIT 1');
|
||||
$stmt->execute(['table' => $table]);
|
||||
return (bool)$stmt->fetchColumn();
|
||||
}
|
||||
|
||||
private function hasColumn(string $table, string $column): bool
|
||||
{
|
||||
$stmt = $this->pdo->prepare('SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = :table AND column_name = :column LIMIT 1');
|
||||
$stmt->execute([
|
||||
'table' => $table,
|
||||
'column' => $column,
|
||||
]);
|
||||
return (bool)$stmt->fetchColumn();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user