290 lines
15 KiB
PHP
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: <script>console.log('ads head')</script>"><?= e($adsHead) ?></textarea>
|
|
<div class="small text-muted mt-2">Cocok untuk loader iklan, verifikasi, atau script yang memang harus masuk ke <code><head></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: <script>console.log('ads body')</script>"><?= e($adsBody) ?></textarea>
|
|
<div class="small text-muted mt-2">Ideal untuk tag yang harus muncul setelah <code><body></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(); ?>
|