611 lines
16 KiB
PHP
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;">← 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>
|