dev/patrick #1
@@ -4,3 +4,4 @@ POSTGRES_PORT=postgres_port
|
|||||||
POSTGRES_DB=postgres_database
|
POSTGRES_DB=postgres_database
|
||||||
POSTGRES_USER=postgres_user
|
POSTGRES_USER=postgres_user
|
||||||
POSTGRES_PASSWORD=
|
POSTGRES_PASSWORD=
|
||||||
|
ADMIN_PASSWORD=
|
||||||
281
public/admin.css
Normal file
281
public/admin.css
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
/* =====================================================================
|
||||||
|
Moderation Page Styles
|
||||||
|
===================================================================== */
|
||||||
|
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||||
|
background: #f4f5f7;
|
||||||
|
color: #1a1a2e;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Header
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
.admin-header {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
padding: 16px 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header h1 { font-size: 1.2rem; }
|
||||||
|
|
||||||
|
.admin-header a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
opacity: 0.8;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header a:hover { opacity: 1; }
|
||||||
|
|
||||||
|
.admin-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Container
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
.admin-container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 24px auto;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Statistics Cards
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .stat-number {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .stat-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #5a5a7a;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Section Headers
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
h2 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 2px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section { margin-bottom: 40px; }
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Contribution Rows
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
.contribution-row {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contribution-info { flex: 1; }
|
||||||
|
|
||||||
|
.contribution-info .title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contribution-info .meta {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #5a5a7a;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contribution-info .description {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #5a5a7a;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Badges
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-pending { background: #fff3cd; color: #856404; }
|
||||||
|
.badge-approved { background: #d4edda; color: #155724; }
|
||||||
|
.badge-rejected { background: #f8d7da; color: #721c24; }
|
||||||
|
.badge-point { background: #e3f2fd; color: #1565c0; }
|
||||||
|
.badge-line { background: #f3e5f5; color: #6a1b9a; }
|
||||||
|
.badge-polygon { background: #e8f5e9; color: #2e7d32; }
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Action Buttons
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-approve { background: #2e7d32; color: white; }
|
||||||
|
.btn-approve:hover { background: #1b5e20; }
|
||||||
|
|
||||||
|
.btn-reject { background: #c62828; color: white; }
|
||||||
|
.btn-reject:hover { background: #b71c1c; }
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Empty State
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Login Page
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
.login-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px;
|
||||||
|
max-width: 380px;
|
||||||
|
width: 90%;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box h1 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box p {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #5a5a7a;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box input[type="password"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 55, 109, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box button:hover { filter: brightness(1.15); }
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #c62828;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link a { color: #5a5a7a; }
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
Mobile Responsive
|
||||||
|
----------------------------------------------------------------- */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.contribution-row {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons form {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons .btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
254
public/admin.php
Normal file
254
public/admin.php
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
<?php
|
||||||
|
// =====================================================================
|
||||||
|
// Moderation Page
|
||||||
|
// Lists pending Contributions for Review. Moderators can approve
|
||||||
|
// or reject Contributions.
|
||||||
|
// ToDo: Extend with News Management, User Management, Analytics.
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
require_once __DIR__ . '/api/db.php';
|
||||||
|
require_once __DIR__ . '/api/auth.php';
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// Routing: Login, Logout, or Main Page
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
$page = $_GET['page'] ?? 'main';
|
||||||
|
|
||||||
|
// Handle Login Form Submission
|
||||||
|
if ($page === 'login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
if (admin_login($password)) {
|
||||||
|
header('Location: admin.php');
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$login_error = 'Falsches Passwort.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Logout
|
||||||
|
if ($page === 'logout') {
|
||||||
|
admin_logout();
|
||||||
|
header('Location: admin.php?page=login');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// Load Municipality for Theming (needed for both Login and Main Page)
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
$pdo = get_db();
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM municipalities WHERE slug = :slug");
|
||||||
|
$stmt->execute([':slug' => 'lohne']);
|
||||||
|
$municipality = $stmt->fetch();
|
||||||
|
|
||||||
|
// Show Login Page if not authenticated
|
||||||
|
if ($page === 'login' || !is_admin()) {
|
||||||
|
show_login_page($municipality, $login_error ?? null);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// Handle Moderation Actions (Approve / Reject)
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['mod_action'])) {
|
||||||
|
$contribution_id = $_POST['contribution_id'] ?? '';
|
||||||
|
$mod_action = $_POST['mod_action'];
|
||||||
|
|
||||||
|
if ($contribution_id && in_array($mod_action, ['approved', 'rejected'])) {
|
||||||
|
$stmt = $pdo->prepare("UPDATE contributions SET status = :status WHERE contribution_id = :id");
|
||||||
|
$stmt->execute([':status' => $mod_action, ':id' => $contribution_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirects to prevent Form Resubmission on Refresh
|
||||||
|
header('Location: admin.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// Load Contributions Data
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
|
||||||
|
// Pending Contributions
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT contribution_id, title, category, description, author_name, geom_type, status, created_at
|
||||||
|
FROM contributions
|
||||||
|
WHERE municipality_id = :mid AND status = 'pending'
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
");
|
||||||
|
$stmt->execute([':mid' => $municipality['municipality_id']]);
|
||||||
|
$pending = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Recently moderated Contributions
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT contribution_id, title, category, description, author_name, geom_type, status, created_at, updated_at
|
||||||
|
FROM contributions
|
||||||
|
WHERE municipality_id = :mid AND status IN ('approved', 'rejected')
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
LIMIT 20
|
||||||
|
");
|
||||||
|
$stmt->execute([':mid' => $municipality['municipality_id']]);
|
||||||
|
$moderated = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT status, COUNT(*) as count
|
||||||
|
FROM contributions
|
||||||
|
WHERE municipality_id = :mid
|
||||||
|
GROUP BY status
|
||||||
|
");
|
||||||
|
$stmt->execute([':mid' => $municipality['municipality_id']]);
|
||||||
|
$stats_rows = $stmt->fetchAll();
|
||||||
|
$stats = [];
|
||||||
|
foreach ($stats_rows as $row) {
|
||||||
|
$stats[$row['status']] = $row['count'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// Render Main Page
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Moderation — <?= htmlspecialchars($municipality['name']) ?></title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="admin.css">
|
||||||
|
<style>:root { --color-primary: <?= htmlspecialchars($municipality['primary_color']) ?>; }</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="admin-header">
|
||||||
|
<h1><i class="fa-solid fa-shield-halved"></i> Moderation — <?= htmlspecialchars($municipality['name']) ?></h1>
|
||||||
|
<div class="admin-nav">
|
||||||
|
<a href="index.php"><i class="fa-solid fa-map"></i> Zur Karte</a>
|
||||||
|
<a href="admin.php?page=logout"><i class="fa-solid fa-right-from-bracket"></i> Abmelden</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-container">
|
||||||
|
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-number"><?= ($stats['pending'] ?? 0) ?></div>
|
||||||
|
<div class="stat-label">Ausstehend</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-number"><?= ($stats['approved'] ?? 0) ?></div>
|
||||||
|
<div class="stat-label">Freigegeben</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-number"><?= ($stats['rejected'] ?? 0) ?></div>
|
||||||
|
<div class="stat-label">Abgelehnt</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-number"><?= array_sum(array_column($stats_rows, 'count')) ?></div>
|
||||||
|
<div class="stat-label">Gesamt</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2><i class="fa-solid fa-clock"></i> Ausstehende Beiträge (<?= count($pending) ?>)</h2>
|
||||||
|
|
||||||
|
<?php if (empty($pending)): ?>
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="fa-solid fa-check-circle" style="font-size:2rem;margin-bottom:8px;display:block;"></i>
|
||||||
|
Keine ausstehenden Beiträge.
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($pending as $item): ?>
|
||||||
|
<div class="contribution-row">
|
||||||
|
<div class="contribution-info">
|
||||||
|
<div class="title"><?= htmlspecialchars($item['title']) ?></div>
|
||||||
|
<div class="meta">
|
||||||
|
<span class="badge badge-<?= $item['geom_type'] ?>"><?= $item['geom_type'] ?></span>
|
||||||
|
<span class="badge badge-pending">ausstehend</span>
|
||||||
|
· <?= htmlspecialchars($item['category']) ?>
|
||||||
|
· <?= htmlspecialchars($item['author_name']) ?>
|
||||||
|
· <?= date('d.m.Y H:i', strtotime($item['created_at'])) ?>
|
||||||
|
</div>
|
||||||
|
<?php if ($item['description']): ?>
|
||||||
|
<div class="description"><?= htmlspecialchars($item['description']) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="contribution_id" value="<?= $item['contribution_id'] ?>">
|
||||||
|
<input type="hidden" name="mod_action" value="approved">
|
||||||
|
<button type="submit" class="btn btn-approve"><i class="fa-solid fa-check"></i> Freigeben</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="contribution_id" value="<?= $item['contribution_id'] ?>">
|
||||||
|
<input type="hidden" name="mod_action" value="rejected">
|
||||||
|
<button type="submit" class="btn btn-reject"><i class="fa-solid fa-xmark"></i> Ablehnen</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2><i class="fa-solid fa-history"></i> Kürzlich moderiert</h2>
|
||||||
|
|
||||||
|
<?php if (empty($moderated)): ?>
|
||||||
|
<div class="empty-state">Noch keine moderierten Beiträge.</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($moderated as $item): ?>
|
||||||
|
<div class="contribution-row">
|
||||||
|
<div class="contribution-info">
|
||||||
|
<div class="title"><?= htmlspecialchars($item['title']) ?></div>
|
||||||
|
<div class="meta">
|
||||||
|
<span class="badge badge-<?= $item['geom_type'] ?>"><?= $item['geom_type'] ?></span>
|
||||||
|
<span class="badge badge-<?= $item['status'] ?>"><?= $item['status'] === 'approved' ? 'freigegeben' : 'abgelehnt' ?></span>
|
||||||
|
· <?= htmlspecialchars($item['category']) ?>
|
||||||
|
· <?= htmlspecialchars($item['author_name']) ?>
|
||||||
|
· <?= date('d.m.Y H:i', strtotime($item['created_at'])) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// Login Page
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
function show_login_page($municipality, $error = null) {
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Moderation — Anmeldung</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="admin.css">
|
||||||
|
<style>:root { --color-primary: <?= htmlspecialchars($municipality['primary_color']) ?>; }</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-wrapper">
|
||||||
|
<div class="login-box">
|
||||||
|
<h1><i class="fa-solid fa-shield-halved"></i> Moderation</h1>
|
||||||
|
<p>Bitte geben Sie das Moderationspasswort ein.</p>
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="error"><?= htmlspecialchars($error) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form method="POST" action="admin.php?page=login">
|
||||||
|
<input type="password" name="password" placeholder="Passwort" autofocus>
|
||||||
|
<button type="submit"><i class="fa-solid fa-right-to-bracket"></i> Anmelden</button>
|
||||||
|
</form>
|
||||||
|
<div class="back-link"><a href="index.php">← Zurück zur Karte</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
41
public/api/auth.php
Normal file
41
public/api/auth.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
// =====================================================================
|
||||||
|
// Admin Authentication Helper
|
||||||
|
// Provides simple Password-based Session Authentication for the
|
||||||
|
// Moderation Page. Uses ADMIN_PASSWORD from .env File.
|
||||||
|
// ToDo: Replace with full User Authentication in Phase 3-3.
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
// Reads Admin Password from Environment
|
||||||
|
function get_admin_password() {
|
||||||
|
return getenv('ADMIN_PASSWORD');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if current Session is authenticated as Admin
|
||||||
|
function is_admin() {
|
||||||
|
return isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticates with Password, returns true on Success
|
||||||
|
function admin_login($password) {
|
||||||
|
$correct = get_admin_password();
|
||||||
|
if ($correct && $password === $correct) {
|
||||||
|
$_SESSION['is_admin'] = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logs out Admin Session
|
||||||
|
function admin_logout() {
|
||||||
|
$_SESSION['is_admin'] = false;
|
||||||
|
session_destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirects to Login if not authenticated
|
||||||
|
function require_admin() {
|
||||||
|
if (!is_admin()) {
|
||||||
|
header('Location: admin.php?page=login');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,9 +67,16 @@ function handle_read($input) {
|
|||||||
// Builds SQL Query with Placeholders for prepared Statement
|
// Builds SQL Query with Placeholders for prepared Statement
|
||||||
$sql = "SELECT *, ST_AsGeoJSON(geom) AS geojson
|
$sql = "SELECT *, ST_AsGeoJSON(geom) AS geojson
|
||||||
FROM contributions
|
FROM contributions
|
||||||
WHERE municipality_id = :mid AND status = 'approved'";
|
WHERE municipality_id = :mid";
|
||||||
$params = [':mid' => $municipality_id];
|
$params = [':mid' => $municipality_id];
|
||||||
|
|
||||||
|
// Optional: Filters by Status (Default: only approved)
|
||||||
|
$status = $input['status'] ?? 'approved';
|
||||||
|
if ($status !== 'all') {
|
||||||
|
$sql .= " AND status = :status";
|
||||||
|
$params[':status'] = $status;
|
||||||
|
}
|
||||||
|
|
||||||
// Optional: Filters by Category
|
// Optional: Filters by Category
|
||||||
if (!empty($input['category'])) {
|
if (!empty($input['category'])) {
|
||||||
$sql .= " AND category = :cat";
|
$sql .= " AND category = :cat";
|
||||||
|
|||||||
@@ -99,10 +99,13 @@ if (!$municipality) {
|
|||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<!-- Admin Login Button -->
|
||||||
|
<a href="admin.php" class="nav-btn nav-btn-admin" title="Moderationsbereich">
|
||||||
|
<i class="fa-solid fa-lock"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
<!-- Mobile Hamburger Menu -->
|
<!-- Mobile Hamburger Menu -->
|
||||||
<button class="header-menu-toggle" onclick="toggleMobileNav()">
|
<button class="header-menu-toggle" onclick="toggleMobileNav()">
|
||||||
<i class="fa-solid fa-bars"></i>
|
|
||||||
</button>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
/* Municipality Colors */
|
/* Municipality Colors */
|
||||||
--color-primary: #00376D;
|
--color-primary: #00376D;
|
||||||
--color-primary-light: #00376D22;
|
--color-primary-light: #00376D22;
|
||||||
--color-primary-dark: #00376D;
|
|
||||||
|
|
||||||
/* Neutral Colors */
|
/* Neutral Colors */
|
||||||
--color-bg: #f4f5f7;
|
--color-bg: #f4f5f7;
|
||||||
@@ -133,6 +132,19 @@ html, body {
|
|||||||
background: rgba(255, 255, 255, 0.25);
|
background: rgba(255, 255, 255, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-btn-admin {
|
||||||
|
text-decoration: none;
|
||||||
|
margin-left: var(--space-sm);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
opacity: 0.6;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn-admin:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.header-menu-toggle {
|
.header-menu-toggle {
|
||||||
display: none;
|
display: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user