39401-vm/admin-portal.php
2026-03-30 17:47:50 +00:00

290 lines
15 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/layout.php';
function admin_setting_url_looks_safe(string $value): bool
{
if ($value === '') {
return true;
}
$lower = strtolower($value);
return !str_contains($value, '<')
&& !str_contains($value, '>')
&& !str_starts_with($lower, 'javascript:')
&& !str_starts_with($lower, 'vbscript:');
}
$portalUrl = app_url('admin-portal.php');
if (isset($_GET['logout'])) {
unset($_SESSION['is_admin_logged_in']);
set_flash('success', 'Admin berhasil logout.');
header('Location: ' . $portalUrl);
exit;
}
$loginError = '';
$passwordErrors = [];
$brandingErrors = [];
$brandingForm = [
'brand_logo_url' => app_setting('brand_logo_url'),
'brand_favicon_url' => app_setting('brand_favicon_url'),
];
if (!is_admin_logged_in() && $_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'login') {
$username = trim((string) ($_POST['username'] ?? ''));
$password = (string) ($_POST['password'] ?? '');
if (verify_admin_login($username, $password)) {
$_SESSION['is_admin_logged_in'] = true;
set_flash('success', 'Login admin berhasil. Kamu bisa mengatur iklan, sandi, dan branding sekarang.');
header('Location: ' . $portalUrl);
exit;
}
$loginError = 'Username atau password admin tidak cocok.';
}
if (is_admin_logged_in() && $_SERVER['REQUEST_METHOD'] === 'POST') {
$action = (string) ($_POST['action'] ?? '');
if ($action === 'save_ads') {
save_setting('ads_head', trim((string) ($_POST['ads_head'] ?? '')));
save_setting('ads_body', trim((string) ($_POST['ads_body'] ?? '')));
set_flash('success', 'Kode iklan berhasil disimpan dan akan dimuat di semua halaman.');
header('Location: ' . $portalUrl);
exit;
}
if ($action === 'change_password') {
$currentPassword = (string) ($_POST['current_password'] ?? '');
$newPassword = (string) ($_POST['new_password'] ?? '');
$confirmPassword = (string) ($_POST['confirm_password'] ?? '');
if (!verify_admin_login(admin_username(), $currentPassword)) {
$passwordErrors['current_password'] = 'Password admin saat ini tidak cocok.';
}
if (strlen($newPassword) < 8) {
$passwordErrors['new_password'] = 'Password baru minimal 8 karakter.';
}
if ($confirmPassword === '' || $newPassword !== $confirmPassword) {
$passwordErrors['confirm_password'] = 'Konfirmasi password harus sama dengan password baru.';
}
if (!$passwordErrors) {
save_admin_password($newPassword);
set_flash('success', 'Password admin berhasil diubah. Password default tidak dipakai lagi.');
header('Location: ' . $portalUrl);
exit;
}
}
if ($action === 'save_branding') {
$brandingForm['brand_logo_url'] = trim((string) ($_POST['brand_logo_url'] ?? ''));
$brandingForm['brand_favicon_url'] = trim((string) ($_POST['brand_favicon_url'] ?? ''));
foreach ($brandingForm as $field => $value) {
if (strlen($value) > 500) {
$brandingErrors[$field] = 'URL terlalu panjang. Maksimal 500 karakter.';
continue;
}
if (!admin_setting_url_looks_safe($value)) {
$brandingErrors[$field] = 'Nilai tidak valid. Gunakan URL biasa atau path file publik.';
}
}
if (!$brandingErrors) {
save_setting('brand_logo_url', $brandingForm['brand_logo_url']);
save_setting('brand_favicon_url', $brandingForm['brand_favicon_url']);
set_flash('success', 'Branding website berhasil disimpan. Logo dan favicon baru langsung dipakai.');
header('Location: ' . $portalUrl);
exit;
}
}
}
$adsHead = app_setting('ads_head');
$adsBody = app_setting('ads_body');
$logoPreviewUrl = public_asset_url($brandingForm['brand_logo_url']);
$faviconPreviewUrl = public_asset_url($brandingForm['brand_favicon_url']);
render_page_start([
'title' => 'Admin portal tersembunyi',
'description' => 'Login admin untuk mengelola script iklan global, password admin, dan branding website.',
'page' => 'admin-portal',
'robots' => 'noindex, nofollow',
]);
render_flash(consume_flash());
?>
<section class="py-5">
<div class="container">
<div class="row justify-content-center">
<div class="col-xl-9 col-lg-10">
<?php if (!is_admin_logged_in()): ?>
<div class="surface-card admin-card mx-auto" style="max-width: 560px;">
<span class="eyebrow">Hidden admin</span>
<h1 class="section-title mt-2 mb-2">Login panel admin</h1>
<p class="text-secondary">Panel ini tetap tersembunyi via slug khusus. Setelah login, admin bisa mengelola script iklan global, mengganti sandi admin, dan mengatur logo/favikon website.</p>
<?php if ($loginError !== ''): ?>
<div class="alert alert-danger small" role="alert"><?= e($loginError) ?></div>
<?php endif; ?>
<form method="post" class="vstack gap-3">
<input type="hidden" name="action" value="login">
<div>
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" value="<?= e(admin_username()) ?>" autocomplete="username">
</div>
<div>
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" autocomplete="current-password">
</div>
<button type="submit" class="btn btn-dark">Masuk ke panel admin</button>
</form>
<?php if (!admin_has_custom_password()): ?>
<div class="small text-muted mt-3">Akses awal MVP ini: <strong><?= e(admin_username()) ?></strong> / <strong><?= e(admin_default_password_hint()) ?></strong>. Setelah masuk, segera ganti password admin di panel.</div>
<?php else: ?>
<div class="small text-muted mt-3">Password admin sudah memakai versi custom yang tersimpan di database, jadi hint default tidak lagi dipakai.</div>
<?php endif; ?>
</div>
<?php else: ?>
<div class="surface-card admin-card">
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 mb-4">
<div>
<span class="eyebrow">Admin portal</span>
<h1 class="section-title mt-2 mb-2">Pengaturan global website</h1>
<p class="text-secondary mb-0">Semua pengaturan di halaman ini berlaku global untuk semua halaman: script iklan, password admin, dan branding website.</p>
</div>
<div class="d-flex flex-wrap gap-2">
<span class="summary-chip"><?= $adsHead !== '' ? 'HEAD aktif' : 'HEAD kosong' ?></span>
<span class="summary-chip"><?= $adsBody !== '' ? 'BODY aktif' : 'BODY kosong' ?></span>
<span class="summary-chip"><?= admin_has_custom_password() ? 'Password custom aktif' : 'Masih password default' ?></span>
<a class="btn btn-outline-secondary btn-sm" href="<?= e($portalUrl) ?>?logout=1">Logout</a>
</div>
</div>
<div class="alert alert-warning small" role="alert">
Untuk shared hosting/cPanel, isi <strong>logo URL</strong> dan <strong>favicon URL</strong> dengan path publik seperti <code>assets/images/logo.png</code> atau URL penuh. Kalau app dipasang di subfolder, path relatif akan ikut menyesuaikan otomatis.
</div>
<div class="row g-4">
<div class="col-12">
<div class="surface-subsection admin-section-card">
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
<div>
<h2 class="h5 mb-1">Script iklan global</h2>
<p class="small text-secondary mb-0">Tempel kode JavaScript/HTML iklan yang akan dirender di semua halaman.</p>
</div>
<span class="badge badge-soft">Ads</span>
</div>
<form method="post" class="vstack gap-4">
<input type="hidden" name="action" value="save_ads">
<div>
<label for="ads_head" class="form-label fw-semibold">Slot HEAD</label>
<textarea class="form-control font-monospace" id="ads_head" name="ads_head" rows="7" placeholder="Contoh: &lt;script&gt;console.log('ads head')&lt;/script&gt;"><?= e($adsHead) ?></textarea>
<div class="small text-muted mt-2">Cocok untuk loader iklan, verifikasi, atau script yang memang harus masuk ke <code>&lt;head&gt;</code>.</div>
</div>
<div>
<label for="ads_body" class="form-label fw-semibold">Slot BODY</label>
<textarea class="form-control font-monospace" id="ads_body" name="ads_body" rows="7" placeholder="Contoh: &lt;script&gt;console.log('ads body')&lt;/script&gt;"><?= e($adsBody) ?></textarea>
<div class="small text-muted mt-2">Ideal untuk tag yang harus muncul setelah <code>&lt;body&gt;</code> terbuka.</div>
</div>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-dark">Simpan script iklan</button>
</div>
</form>
</div>
</div>
<div class="col-lg-6">
<div class="surface-subsection admin-section-card h-100">
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
<div>
<h2 class="h5 mb-1">Keamanan admin</h2>
<p class="small text-secondary mb-0">Ganti password admin agar akses slug tersembunyi ini lebih aman.</p>
</div>
<span class="badge badge-soft">Security</span>
</div>
<form method="post" class="vstack gap-3" novalidate>
<input type="hidden" name="action" value="change_password">
<div>
<label for="current_password" class="form-label">Password saat ini</label>
<input type="password" class="form-control <?= isset($passwordErrors['current_password']) ? 'is-invalid' : '' ?>" id="current_password" name="current_password" autocomplete="current-password">
<?php if (isset($passwordErrors['current_password'])): ?><div class="invalid-feedback"><?= e($passwordErrors['current_password']) ?></div><?php endif; ?>
</div>
<div>
<label for="new_password" class="form-label">Password baru</label>
<input type="password" class="form-control <?= isset($passwordErrors['new_password']) ? 'is-invalid' : '' ?>" id="new_password" name="new_password" autocomplete="new-password">
<?php if (isset($passwordErrors['new_password'])): ?><div class="invalid-feedback"><?= e($passwordErrors['new_password']) ?></div><?php endif; ?>
</div>
<div>
<label for="confirm_password" class="form-label">Konfirmasi password baru</label>
<input type="password" class="form-control <?= isset($passwordErrors['confirm_password']) ? 'is-invalid' : '' ?>" id="confirm_password" name="confirm_password" autocomplete="new-password">
<?php if (isset($passwordErrors['confirm_password'])): ?><div class="invalid-feedback"><?= e($passwordErrors['confirm_password']) ?></div><?php endif; ?>
</div>
<div class="small text-muted">Username admin tetap <strong><?= e(admin_username()) ?></strong>. Yang diubah di sini hanya password-nya.</div>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-dark">Ubah password admin</button>
</div>
</form>
</div>
</div>
<div class="col-lg-6">
<div class="surface-subsection admin-section-card h-100">
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
<div>
<h2 class="h5 mb-1">Logo & ikon website</h2>
<p class="small text-secondary mb-0">Atur logo yang muncul di navbar/footer dan favicon yang tampil di tab browser.</p>
</div>
<span class="badge badge-soft">Branding</span>
</div>
<form method="post" class="vstack gap-3" novalidate>
<input type="hidden" name="action" value="save_branding">
<div>
<label for="brand_logo_url" class="form-label">Logo URL / path publik</label>
<input type="text" class="form-control <?= isset($brandingErrors['brand_logo_url']) ? 'is-invalid' : '' ?>" id="brand_logo_url" name="brand_logo_url" value="<?= e($brandingForm['brand_logo_url']) ?>" placeholder="Contoh: assets/images/logo.png atau https://.../logo.svg">
<?php if (isset($brandingErrors['brand_logo_url'])): ?><div class="invalid-feedback"><?= e($brandingErrors['brand_logo_url']) ?></div><?php endif; ?>
</div>
<div>
<label for="brand_favicon_url" class="form-label">Favicon URL / path publik</label>
<input type="text" class="form-control <?= isset($brandingErrors['brand_favicon_url']) ? 'is-invalid' : '' ?>" id="brand_favicon_url" name="brand_favicon_url" value="<?= e($brandingForm['brand_favicon_url']) ?>" placeholder="Contoh: assets/images/favicon.ico atau https://.../favicon.png">
<?php if (isset($brandingErrors['brand_favicon_url'])): ?><div class="invalid-feedback"><?= e($brandingErrors['brand_favicon_url']) ?></div><?php endif; ?>
</div>
<div class="admin-branding-preview">
<div>
<div class="small text-uppercase text-muted mb-2">Preview logo</div>
<?php if ($logoPreviewUrl !== ''): ?>
<img class="admin-brand-preview" src="<?= e($logoPreviewUrl) ?>" alt="Preview logo website">
<?php else: ?>
<div class="small text-secondary">Belum ada logo custom. Saat ini website masih memakai inisial brand.</div>
<?php endif; ?>
</div>
<div>
<div class="small text-uppercase text-muted mb-2">Preview favicon</div>
<?php if ($faviconPreviewUrl !== ''): ?>
<img class="admin-favicon-preview" src="<?= e($faviconPreviewUrl) ?>" alt="Preview favicon website">
<?php else: ?>
<div class="small text-secondary">Belum ada favicon custom. Tab browser masih memakai icon default browser.</div>
<?php endif; ?>
</div>
</div>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-dark">Simpan branding</button>
</div>
</form>
</div>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
</section>
<?php render_page_end(); ?>