| Current Path : /home/seto/888mirror.pm/admin/ |
| Current File : /home/seto/888mirror.pm/admin/settings.php |
<?php
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/helpers.php';
auth_check();
$msg = [];
$err = [];
// OpenAI настройки
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_openai']) && auth_can('openai_key')) {
$key = trim($_POST['openai_key'] ?? '');
$model = trim($_POST['openai_model'] ?? 'gpt-4o');
setting_set('openai_key', $key);
setting_set('openai_model', $model);
$msg['openai'] = 'OpenAI настройки сохранены.';
}
// Название сайта
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_site'])) {
$site_name = trim($_POST['site_name'] ?? '');
if ($site_name) {
setting_set('site_name', $site_name);
$msg['site'] = 'Название сайта сохранено.';
} else {
$err['site'] = 'Название не может быть пустым.';
}
}
// Excluded IPs
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_excluded_ips'])) {
$raw_ips = $_POST['excluded_ips'] ?? '';
$lines = array_filter(array_map('trim', explode("\n", str_replace("\r", "\n", $raw_ips))));
$valid = array_filter($lines, function($entry) {
if (str_contains($entry, '/')) {
[$ip, $bits] = explode('/', $entry, 2);
$is_ipv6 = str_contains($ip, ':');
$max_bits = $is_ipv6 ? 128 : 32;
return filter_var($ip, FILTER_VALIDATE_IP) && ctype_digit($bits) && (int)$bits >= 0 && (int)$bits <= $max_bits;
}
return (bool)filter_var($entry, FILTER_VALIDATE_IP);
});
setting_set('excluded_ips', implode("\n", $valid));
$msg['ips'] = 'Сохранено. Записей в списке: ' . count($valid) . '.';
}
// Смена пароля
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['change_password'])) {
$cur = $_POST['current_password'] ?? '';
$new = $_POST['new_password'] ?? '';
$conf = $_POST['confirm_password'] ?? '';
$user = db()->prepare("SELECT id, password_hash FROM users WHERE id = ?");
$user->execute([$_SESSION['user_id']]);
$user = $user->fetch();
if (!$cur || !$new || !$conf) {
$err['pwd'] = 'Заполните все поля.';
} elseif (!password_verify($cur, $user['password_hash'])) {
$err['pwd'] = 'Текущий пароль неверный.';
} elseif (strlen($new) < 6) {
$err['pwd'] = 'Новый пароль должен быть не менее 6 символов.';
} elseif ($new !== $conf) {
$err['pwd'] = 'Пароли не совпадают.';
} else {
$hash = password_hash($new, PASSWORD_DEFAULT);
db()->prepare("UPDATE users SET password_hash = ? WHERE id = ?")->execute([$hash, $user['id']]);
$msg['pwd'] = 'Пароль успешно изменён.';
}
}
$openai_key = setting('openai_key');
$openai_model = setting('openai_model') ?: 'gpt-4o';
$site_name = setting('site_name') ?: '888mirror.pm';
$excluded_ips = setting('excluded_ips');
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Настройки — Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<?= admin_nav('settings') ?>
<div class="container" style="max-width:680px">
<h5 class="mb-4">Настройки</h5>
<!-- OpenAI -->
<?php if (auth_can('openai_key')): ?>
<div class="card shadow-sm mb-4">
<div class="card-header fw-semibold">🤖 OpenAI</div>
<div class="card-body">
<?php if (!empty($msg['openai'])): ?>
<div class="alert alert-success py-2"><?= h($msg['openai']) ?></div>
<?php endif ?>
<?php if (!$openai_key): ?>
<div class="alert alert-warning py-2">⚠ API ключ не задан — генерация контента недоступна.</div>
<?php endif ?>
<form method="post">
<div class="mb-3">
<label class="form-label">API Key</label>
<input type="text" name="openai_key" class="form-control font-monospace"
value="<?= h($openai_key) ?>" placeholder="sk-...">
<div class="form-text">Получить на <a href="https://platform.openai.com/api-keys" target="_blank">platform.openai.com</a></div>
</div>
<div class="mb-3">
<label class="form-label">Модель</label>
<select name="openai_model" class="form-select">
<?php foreach (['gpt-4o' => 'gpt-4o (умная)', 'gpt-4o-mini' => 'gpt-4o-mini (дешевле)', 'gpt-4-turbo' => 'gpt-4-turbo'] as $val => $label): ?>
<option value="<?= $val ?>" <?= $openai_model === $val ? 'selected' : '' ?>><?= $label ?></option>
<?php endforeach ?>
</select>
</div>
<button type="submit" name="save_openai" class="btn btn-primary btn-sm">Сохранить</button>
</form>
</div>
</div>
<?php endif ?>
<!-- Название сайта -->
<div class="card shadow-sm mb-4">
<div class="card-header fw-semibold">🌐 Название сайта</div>
<div class="card-body">
<?php if (!empty($msg['site'])): ?>
<div class="alert alert-success py-2"><?= h($msg['site']) ?></div>
<?php endif ?>
<?php if (!empty($err['site'])): ?>
<div class="alert alert-danger py-2"><?= h($err['site']) ?></div>
<?php endif ?>
<form method="post">
<div class="mb-3">
<label class="form-label">Название</label>
<input type="text" name="site_name" class="form-control"
value="<?= h($site_name) ?>" placeholder="888mirror.pm">
<div class="form-text">Используется в title и footer публичной страницы.</div>
</div>
<button type="submit" name="save_site" class="btn btn-primary btn-sm">Сохранить</button>
</form>
</div>
</div>
<!-- Excluded IPs -->
<div class="card shadow-sm mb-4">
<div class="card-header fw-semibold">🚫 Исключённые IP-адреса</div>
<div class="card-body">
<?php if (!empty($msg['ips'])): ?>
<div class="alert alert-success py-2"><?= h($msg['ips']) ?></div>
<?php endif ?>
<form method="post">
<div class="mb-3">
<label class="form-label">Список IP <span class="text-muted fw-normal">(один адрес на строку)</span></label>
<textarea name="excluded_ips" class="form-control font-monospace" rows="5"
placeholder="192.168.1.1 10.0.0.0/8 2a01:4f9:3090:2163::2 2a01:4f9:3090:2163::/64"><?= h($excluded_ips) ?></textarea>
<div class="form-text">Показы и клики с этих IP не будут записываться в статистику. Поддерживаются IPv4, IPv6 и CIDR-диапазоны (например <code>2a01:4f9::/32</code>).</div>
</div>
<button type="submit" name="save_excluded_ips" class="btn btn-primary btn-sm">Сохранить</button>
<?php
$ip_count = count(array_filter(array_map('trim', explode("\n", $excluded_ips))));
if ($ip_count > 0): ?>
<span class="text-muted small ms-2"><?= $ip_count ?> запис<?= $ip_count >= 5 ? 'ей' : ($ip_count >= 2 ? 'и' : 'ь') ?> в списке</span>
<?php endif ?>
</form>
</div>
</div>
<!-- Внешний вид -->
<div class="card shadow-sm mb-4">
<div class="card-header fw-semibold">🎨 Внешний вид админки</div>
<div class="card-body">
<p class="text-muted small mb-3">Тема применяется только в вашем браузере (localStorage).</p>
<div class="row g-2" id="theme-picker">
<?php
$themes = [
'default' => ['Default', 'Стандартный Bootstrap', '#0d6efd', '#fff'],
'flatly' => ['Flatly', 'Плоский, современный стиль', '#2c3e50', '#18bc9c'],
'darkly' => ['Darkly', 'Тёмная версия Flatly', '#222', '#375a7f'],
'cyborg' => ['Cyborg', 'Тёмная sci-fi тема', '#060606', '#2a9fd6'],
'minty' => ['Minty', 'Свежая, мятно-зелёная', '#fff', '#78c2ad'],
];
foreach ($themes as $key => [$name, $desc, $bg, $accent]): ?>
<div class="col-6 col-md-4">
<button type="button"
class="btn w-100 p-0 border-2 rounded-3 overflow-hidden theme-btn"
data-theme="<?= $key ?>"
style="border-color: transparent">
<div style="background:<?= $bg ?>;height:40px;display:flex;align-items:center;justify-content:center;gap:6px">
<div style="width:12px;height:12px;border-radius:50%;background:<?= $accent ?>"></div>
<span style="color:<?= $bg === '#fff' ? '#333' : '#fff' ?>;font-size:13px;font-weight:600"><?= $name ?></span>
</div>
<div class="px-2 py-1 bg-body-tertiary">
<small class="text-muted"><?= $desc ?></small>
</div>
</button>
</div>
<?php endforeach ?>
</div>
</div>
</div>
<!-- Смена пароля -->
<div class="card shadow-sm mb-4">
<div class="card-header fw-semibold">🔒 Смена пароля</div>
<div class="card-body">
<?php if (!empty($msg['pwd'])): ?>
<div class="alert alert-success py-2"><?= h($msg['pwd']) ?></div>
<?php endif ?>
<?php if (!empty($err['pwd'])): ?>
<div class="alert alert-danger py-2"><?= h($err['pwd']) ?></div>
<?php endif ?>
<form method="post" autocomplete="off">
<div class="mb-2">
<label class="form-label">Текущий пароль</label>
<input type="password" name="current_password" class="form-control" required>
</div>
<div class="mb-2">
<label class="form-label">Новый пароль</label>
<input type="password" name="new_password" class="form-control" required minlength="6">
</div>
<div class="mb-3">
<label class="form-label">Повторите новый пароль</label>
<input type="password" name="confirm_password" class="form-control" required minlength="6">
</div>
<button type="submit" name="change_password" class="btn btn-warning btn-sm">Сменить пароль</button>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
(function(){
var THEMES = {
'default': 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css',
'flatly': 'https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/flatly/bootstrap.min.css',
'darkly': 'https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/darkly/bootstrap.min.css',
'cyborg': 'https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/cyborg/bootstrap.min.css',
'minty': 'https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/minty/bootstrap.min.css',
};
var current = localStorage.getItem('admin_theme') || 'default';
function applyActive() {
document.querySelectorAll('.theme-btn').forEach(function(btn) {
var t = btn.dataset.theme;
btn.style.borderColor = (t === current) ? '#0d6efd' : 'transparent';
btn.style.boxShadow = (t === current) ? '0 0 0 3px rgba(13,110,253,.35)' : '';
});
}
function applyTheme(t) {
current = t;
localStorage.setItem('admin_theme', t);
var link = document.querySelector('link[rel="stylesheet"][href*="bootstrap"]');
if (link) {
document.documentElement.style.opacity = '0';
link.href = THEMES[t];
link.addEventListener('load', function() {
document.documentElement.style.opacity = '';
}, {once: true});
setTimeout(function(){ document.documentElement.style.opacity = ''; }, 400);
}
applyActive();
}
document.querySelectorAll('.theme-btn').forEach(function(btn) {
btn.addEventListener('click', function() { applyTheme(btn.dataset.theme); });
});
applyActive();
})();
</script>
</body>
</html>