Files
mediakor2/admin_posts.php
2025-11-26 17:18:26 +00:00

611 lines
16 KiB
PHP

<?php
session_start();
/*
Admin Posts Panel for Mediakor
------------------------------
- Protect this file (admin password below, and ideally via IP/HTTP auth).
- Uses database from your Docker stack:
DB:
host: mariadb
name: appdb
user: appuser
pass: apppass
- On first successful connection it will ensure the "posts" table exists:
CREATE TABLE IF NOT EXISTS posts (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
meta VARCHAR(255) DEFAULT NULL,
body TEXT NOT NULL,
is_published TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
*/
// --- ADMIN CONFIG ---
// !! CHANGE THIS PASSWORD OR SET MK_ADMIN_PASSWORD IN ENV !!
$adminPassword = $_ENV['MK_ADMIN_PASSWORD'] ?? 'change_this_password';
// --- DB CONFIG (matches your docker-compose) ---
$dbHost = $_ENV['DB_HOST'] ?? 'mariadb';
$dbName = $_ENV['DB_NAME'] ?? 'appdb';
$dbUser = $_ENV['DB_USER'] ?? 'appuser';
$dbPass = $_ENV['DB_PASS'] ?? 'apppass';
$pdo = null;
$dbError = null;
$message = null;
// CSRF token
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(16));
}
$csrfToken = $_SESSION['csrf_token'];
function require_login() {
if (empty($_SESSION['mk_admin_logged_in'])) {
header('Location: admin_posts.php');
exit;
}
}
function h($v) {
return htmlspecialchars((string)$v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
/**
* Connect to appdb and ensure the posts table exists.
*/
function mk_get_pdo_and_bootstrap_admin(&$dbErrorOut = null) {
global $dbHost, $dbName, $dbUser, $dbPass;
try {
// Connect directly to the DB you set in MYSQL_DATABASE (appdb)
$dsn = "mysql:host={$dbHost};dbname={$dbName};charset=utf8mb4";
$pdo = new PDO($dsn, $dbUser, $dbPass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
// Ensure posts table exists
$pdo->exec("
CREATE TABLE IF NOT EXISTS posts (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
meta VARCHAR(255) DEFAULT NULL,
body TEXT NOT NULL,
is_published TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
return $pdo;
} catch (PDOException $e) {
$dbErrorOut = $e->getMessage();
return null;
}
}
// --- Handle login / logout ---
if (isset($_GET['logout'])) {
unset($_SESSION['mk_admin_logged_in']);
header('Location: admin_posts.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login_password'])) {
if (hash_equals($adminPassword, $_POST['login_password'])) {
$_SESSION['mk_admin_logged_in'] = true;
header('Location: admin_posts.php');
exit;
} else {
$message = 'Invalid admin password.';
}
}
$loggedIn = !empty($_SESSION['mk_admin_logged_in']);
// Connect once logged in and ensure table exists
if ($loggedIn) {
$pdo = mk_get_pdo_and_bootstrap_admin($dbError);
}
// --- Handle CRUD actions ---
if ($loggedIn && $pdo && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
if (!hash_equals($csrfToken, $_POST['csrf_token'] ?? '')) {
$message = 'Invalid CSRF token.';
} else {
$action = $_POST['action'];
if ($action === 'create') {
$title = trim($_POST['title'] ?? '');
$meta = trim($_POST['meta'] ?? '');
$body = trim($_POST['body'] ?? '');
$is_published = isset($_POST['is_published']) ? 1 : 0;
if ($title && $body) {
$stmt = $pdo->prepare("
INSERT INTO posts (title, meta, body, is_published)
VALUES (:title, :meta, :body, :is_published)
");
$stmt->execute([
':title' => $title,
':meta' => $meta ?: null,
':body' => $body,
':is_published' => $is_published,
]);
$message = 'Post created.';
} else {
$message = 'Title and body are required.';
}
}
if ($action === 'update') {
$id = (int)($_POST['id'] ?? 0);
$title = trim($_POST['title'] ?? '');
$meta = trim($_POST['meta'] ?? '');
$body = trim($_POST['body'] ?? '');
$is_published = isset($_POST['is_published']) ? 1 : 0;
if ($id > 0 && $title && $body) {
$stmt = $pdo->prepare("
UPDATE posts
SET title = :title,
meta = :meta,
body = :body,
is_published = :is_published
WHERE id = :id
LIMIT 1
");
$stmt->execute([
':id' => $id,
':title' => $title,
':meta' => $meta ?: null,
':body' => $body,
':is_published' => $is_published,
]);
$message = "Post #{$id} updated.";
} else {
$message = 'Title and body are required.';
}
}
}
}
// Handle delete via GET (with CSRF)
if ($loggedIn && $pdo && isset($_GET['delete'], $_GET['token'])) {
if (hash_equals($csrfToken, $_GET['token'])) {
$id = (int)$_GET['delete'];
if ($id > 0) {
$stmt = $pdo->prepare("DELETE FROM posts WHERE id = :id LIMIT 1");
$stmt->execute([':id' => $id]);
$message = "Post #{$id} deleted.";
}
} else {
$message = 'Invalid CSRF token for delete.';
}
}
// Fetch posts + maybe a single post to edit
$posts = [];
$editPost = null;
if ($loggedIn && $pdo) {
// List
$stmt = $pdo->query("
SELECT id, title, meta, is_published, created_at
FROM posts
ORDER BY created_at DESC, id DESC
");
$posts = $stmt->fetchAll();
// Edit
if (isset($_GET['edit'])) {
$id = (int)$_GET['edit'];
if ($id > 0) {
$stmt = $pdo->prepare("
SELECT id, title, meta, body, is_published
FROM posts
WHERE id = :id
LIMIT 1
");
$stmt->execute([':id' => $id]);
$editPost = $stmt->fetch();
}
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Mediakor — Admin Posts</title>
<style>
:root{
--bg:#050608;
--panel:#101218;
--panel-soft:#161923;
--accent:#ff8c32;
--accent-soft:rgba(255,140,50,0.18);
--accent-border:rgba(255,140,50,0.5);
--text:#f3f4f6;
--muted:#a1a1aa;
--divider:#23232e;
--shadow:0 0 40px rgba(0,0,0,0.65);
}
*{box-sizing:border-box;margin:0;padding:0}
body{
min-height:100vh;
background:
radial-gradient(800px 600px at 75% 0%, #17141f 0, transparent 60%),
radial-gradient(900px 700px at 0% 100%, #141019 0, transparent 60%),
linear-gradient(160deg,#050608,#050608 40%,#080910 100%);
color:var(--text);
font:500 14px/1.5 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
display:flex;
flex-direction:column;
align-items:center;
padding:24px 12px 40px;
}
.shell{
width:100%;
max-width:1040px;
background:linear-gradient(145deg,var(--panel),var(--panel-soft));
border-radius:18px;
border:1px solid var(--divider);
box-shadow:var(--shadow);
padding:18px 18px 20px;
position:relative;
overflow:hidden;
}
.shell::before{
content:"";
position:absolute;
inset:0;
background:radial-gradient(600px 220px at 105% -10%,rgba(255,140,50,.15),transparent 60%);
opacity:0.8;
pointer-events:none;
}
.shell-inner{position:relative;z-index:1;}
h1{
font-size:20px;
letter-spacing:0.18em;
text-transform:uppercase;
margin-bottom:4px;
}
.subtitle{
font-size:12px;
color:var(--muted);
margin-bottom:16px;
}
a{
color:var(--accent);
text-decoration:none;
}
a:hover{text-decoration:underline;}
.topbar{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:14px;
gap:8px;
flex-wrap:wrap;
}
.badge{
font-size:11px;
border-radius:999px;
padding:3px 9px;
border:1px solid var(--accent-border);
background:rgba(0,0,0,.4);
text-transform:uppercase;
letter-spacing:0.14em;
}
.message{
margin-bottom:10px;
font-size:13px;
padding:6px 10px;
border-radius:10px;
border:1px solid var(--accent-border);
background:rgba(0,0,0,.55);
}
.message.error{
border-color:#f97373;
color:#fecaca;
}
.grid{
display:grid;
grid-template-columns:1.1fr 1.1fr;
gap:16px;
}
@media (max-width:900px){
.grid{grid-template-columns:1fr;}
}
.panel{
border-radius:14px;
border:1px solid var(--divider);
background:rgba(5,6,10,.7);
padding:12px 12px 10px;
}
.panel h2{
font-size:14px;
text-transform:uppercase;
letter-spacing:0.16em;
margin-bottom:6px;
}
label{
display:block;
font-size:11px;
text-transform:uppercase;
letter-spacing:0.14em;
color:var(--muted);
margin:8px 0 3px;
}
input[type="text"],
input[type="password"],
textarea{
width:100%;
border-radius:10px;
border:1px solid #3f3f46;
background:#050608;
color:var(--text);
padding:7px 9px;
font-size:13px;
resize:vertical;
}
textarea{min-height:110px;}
.chk-row{
display:flex;
align-items:center;
gap:6px;
margin-top:6px;
font-size:12px;
color:var(--muted);
}
input[type="checkbox"]{
accent-color:var(--accent);
}
.btn-row{
margin-top:10px;
display:flex;
gap:8px;
align-items:center;
}
button{
border-radius:999px;
border:1px solid var(--accent-border);
background:linear-gradient(135deg,var(--accent-soft),rgba(0,0,0,.7));
color:var(--text);
padding:6px 14px;
font-size:12px;
text-transform:uppercase;
letter-spacing:0.16em;
cursor:pointer;
}
button.secondary{
border-color:#52525b;
background:rgba(0,0,0,.6);
}
button:hover{
filter:brightness(1.1);
}
table{
width:100%;
border-collapse:collapse;
font-size:12px;
margin-top:4px;
}
th, td{
padding:6px 4px;
border-bottom:1px solid #27272f;
}
th{
text-align:left;
text-transform:uppercase;
letter-spacing:0.14em;
color:var(--muted);
font-size:11px;
}
tr:last-child td{border-bottom:none;}
.pill{
padding:2px 7px;
border-radius:999px;
font-size:10px;
border:1px solid var(--accent-border);
background:rgba(0,0,0,.7);
text-transform:uppercase;
letter-spacing:0.14em;
}
.pill.off{
border-color:#52525b;
color:#a1a1aa;
}
.actions a{
margin-right:6px;
font-size:11px;
}
.login-card{
max-width:360px;
background:linear-gradient(145deg,var(--panel),var(--panel-soft));
border-radius:18px;
border:1px solid var(--divider);
box-shadow:var(--shadow);
padding:18px 18px 16px;
}
</style>
</head>
<body>
<?php if (!$loggedIn): ?>
<div class="login-card">
<h1>Mediakor Admin</h1>
<div class="subtitle">Command Feed • Login</div>
<?php if ($message): ?>
<div class="message error"><?php echo h($message); ?></div>
<?php endif; ?>
<form method="post">
<label for="login_password">Admin Password</label>
<input type="password" id="login_password" name="login_password" required />
<div class="btn-row" style="margin-top:12px;">
<button type="submit">Enter Console</button>
</div>
</form>
</div>
<?php else: ?>
<div class="shell">
<div class="shell-inner">
<div class="topbar">
<div>
<h1>Mediakor Admin</h1>
<div class="subtitle">
Manage posts that power the <strong>Command Feed</strong> on your homepage.
</div>
</div>
<div style="text-align:right;">
<div class="badge">Logged in as Operator</div>
<a href="index.php" style="font-size:12px;">&larr; View Site</a> ·
<a href="?logout=1" style="font-size:12px;">Logout</a>
</div>
</div>
<?php if ($message): ?>
<div class="message"><?php echo h($message); ?></div>
<?php endif; ?>
<?php if ($dbError): ?>
<div class="message error">
DB Error: <?php echo h($dbError); ?>
</div>
<?php endif; ?>
<?php if ($pdo): ?>
<div class="grid">
<!-- Create / Edit Panel -->
<div class="panel">
<h2><?php echo $editPost ? 'Edit Post #'.h($editPost['id']) : 'New Post'; ?></h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?php echo h($csrfToken); ?>">
<input type="hidden" name="action" value="<?php echo $editPost ? 'update' : 'create'; ?>">
<?php if ($editPost): ?>
<input type="hidden" name="id" value="<?php echo h($editPost['id']); ?>">
<?php endif; ?>
<label for="title">Title</label>
<input type="text" id="title" name="title"
value="<?php echo h($editPost['title'] ?? ''); ?>" required />
<label for="meta">Meta (optional)</label>
<input type="text" id="meta" name="meta"
placeholder="e.g. System Log • Today"
value="<?php echo h($editPost['meta'] ?? ''); ?>" />
<label for="body">Body</label>
<textarea id="body" name="body" required><?php echo h($editPost['body'] ?? ''); ?></textarea>
<div class="chk-row">
<input type="checkbox" id="is_published" name="is_published"
<?php echo !isset($editPost['is_published']) || $editPost['is_published'] ? 'checked' : ''; ?> />
<label for="is_published" style="margin:0;">Published</label>
</div>
<div class="btn-row">
<button type="submit"><?php echo $editPost ? 'Save Changes' : 'Create Post'; ?></button>
<?php if ($editPost): ?>
<a href="admin_posts.php" style="font-size:11px;">Cancel edit</a>
<?php endif; ?>
</div>
</form>
</div>
<!-- List Panel -->
<div class="panel">
<h2>Existing Posts</h2>
<?php if (empty($posts)): ?>
<p style="font-size:12px;color:var(--muted);margin-top:4px;">
No posts found yet. Create one using the form on the left.
</p>
<?php else: ?>
<table>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Meta</th>
<th>State</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($posts as $p): ?>
<tr>
<td><?php echo h($p['id']); ?></td>
<td><?php echo h($p['title']); ?></td>
<td><?php echo h($p['meta'] ?? ''); ?></td>
<td>
<?php if ($p['is_published']): ?>
<span class="pill">Published</span>
<?php else: ?>
<span class="pill off">Hidden</span>
<?php endif; ?>
</td>
<td><?php echo h($p['created_at']); ?></td>
<td class="actions">
<a href="?edit=<?php echo h($p['id']); ?>">Edit</a>
<a href="?delete=<?php echo h($p['id']); ?>&token=<?php echo h($csrfToken); ?>"
onclick="return confirm('Delete post #<?php echo h($p['id']); ?>?');">
Delete
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
</body>
</html>