Admin: move deploy flow into settings and use secrets file

This commit is contained in:
Alexander Andreev 2026-02-21 15:50:39 +03:00
parent 27691ba396
commit bc48e6959d
11 changed files with 305 additions and 136 deletions

2
.gitignore vendored
View File

@ -15,8 +15,8 @@ Thumbs.db
.vscode/ .vscode/
# Local secrets # Local secrets
deploy-config.php
config.php config.php
secrets.php
# Logs/temp # Logs/temp
*.log *.log

View File

@ -42,16 +42,16 @@ photo.andr33v.ru/
├─ admin.php # админка по токену ├─ admin.php # админка по токену
├─ index-mysql.php # alias -> index.php ├─ index-mysql.php # alias -> index.php
├─ admin-mysql.php # alias -> admin.php ├─ admin-mysql.php # alias -> admin.php
├─ deploy.php # webhook деплоя
├─ style.css # базовые стили ├─ style.css # базовые стили
├─ favicon.svg ├─ favicon.svg
├─ config.php.example # шаблон конфига БД ├─ config.php.example # шаблон конфига БД и деплоя
├─ deploy-config.php.example # шаблон токена/настроек деплоя ├─ secrets.php.example # шаблон секретов админки
├─ lib/ ├─ lib/
│ ├─ db.php # PDO + загрузка config.php │ ├─ db.php # PDO + загрузка config.php
│ ├─ db_gallery.php # доступ к данным галереи │ ├─ db_gallery.php # доступ к данным галереи
│ ├─ thumbs.php # генерация/чтение/удаление превью │ ├─ thumbs.php # генерация/чтение/удаление превью
│ ├─ admin_http.php # JSON-ответы админки │ ├─ admin_http.php # JSON-ответы админки
│ ├─ admin_deploy.php # проверка обновлений и запуск деплоя
│ ├─ admin_get_actions.php # GET-экшены админки │ ├─ admin_get_actions.php # GET-экшены админки
│ ├─ admin_post_actions.php # POST-экшены админки │ ├─ admin_post_actions.php # POST-экшены админки
│ └─ admin_helpers.php # helper-функции админки │ └─ admin_helpers.php # helper-функции админки
@ -87,36 +87,49 @@ cp config.php.example config.php
2. Заполни доступы к БД в `config.php`. 2. Заполни доступы к БД в `config.php`.
3. Прогон миграций: 3. Создай `secrets.php`:
```bash
cp secrets.php.example secrets.php
```
4. Заполни минимум `admin_token` в `secrets.php`.
5. Прогон миграций:
```bash ```bash
php scripts/migrate.php php scripts/migrate.php
``` ```
4. Локальный запуск: 6. Локальный запуск:
```bash ```bash
php -S 127.0.0.1:8080 php -S 127.0.0.1:8080
``` ```
5. Открой: 7. Открой:
- `http://127.0.0.1:8080` — публичная часть, - `http://127.0.0.1:8080` — публичная часть,
- `http://127.0.0.1:8080/admin.php?token=<SECRET>` — админка (после настройки `deploy-config.php`). - `http://127.0.0.1:8080/admin.php?token=<SECRET>` — админка (токен из `secrets.php`).
## Настройка админки ## Настройка админки
Админка использует `token` из `deploy-config.php`. Админка использует секреты из `secrets.php`.
Создай файл: Создай файл:
```bash ```bash
cp deploy-config.php.example deploy-config.php cp secrets.php.example secrets.php
``` ```
Минимально заполни: Минимально заполни:
- `token` — длинный случайный секрет. - `admin_token` — длинный случайный секрет.
Опционально можно ограничить доступ:
- `basic_auth_user` / `basic_auth_pass` — HTTP Basic слой поверх админки,
- `allowed_admin_ips` — белый список IP (строгое совпадение строк).
В админке доступны разделы: В админке доступны разделы:
@ -144,20 +157,22 @@ php scripts/generate_thumbs.php
## Деплой ## Деплой
Webhook: Деплой запускается из админки (вкладка `Настройки`):
- `deploy.php?token=<SECRET>` - кнопка `Проверить обновления` делает `git fetch` и сравнивает `HEAD` с `origin/<branch>`,
- если локальная ветка отстает и не расходится (`behind > 0`, `ahead = 0`) — показывается кнопка `Обновить проект`.
Скрипт `scripts/deploy.sh`: Скрипт `scripts/deploy.sh`:
1. делает `git fetch --all --prune`, 1. делает `git fetch --all --prune`,
2. переключает код на `origin/<branch>` через `git reset --hard`, 2. переключает код на `origin/<branch>` через `git reset --hard`,
3. сохраняет runtime-папки (`photos`, `thumbs`, `data`). 3. запускает миграции `php scripts/migrate.php`,
4. сохраняет runtime-папки (`photos`, `thumbs`, `data`).
Важно: деплой-скрипт перетирает рабочие изменения в репозитории на сервере. Важно: деплой-скрипт перетирает рабочие изменения в репозитории на сервере.
## Примечания ## Примечания
- Проект принудительно редиректит на HTTPS и non-www через `.htaccess`. - Проект принудительно редиректит на HTTPS и non-www через `.htaccess`.
- Для production рекомендуется ограничить webhook по IP и/или Basic Auth (`deploy-config.php`). - Для production рекомендуется включить IP whitelist и/или Basic Auth в `secrets.php`.
- Если `config.php` отсутствует, приложение корректно падает с ошибкой подключения к БД. - Если `config.php` отсутствует, приложение корректно падает с ошибкой подключения к БД.

View File

@ -6,19 +6,29 @@ require_once __DIR__ . '/lib/db_gallery.php';
require_once __DIR__ . '/lib/thumbs.php'; require_once __DIR__ . '/lib/thumbs.php';
require_once __DIR__ . '/lib/admin_http.php'; require_once __DIR__ . '/lib/admin_http.php';
require_once __DIR__ . '/lib/admin_helpers.php'; require_once __DIR__ . '/lib/admin_helpers.php';
require_once __DIR__ . '/lib/admin_deploy.php';
require_once __DIR__ . '/lib/admin_get_actions.php'; require_once __DIR__ . '/lib/admin_get_actions.php';
require_once __DIR__ . '/lib/admin_post_actions.php'; require_once __DIR__ . '/lib/admin_post_actions.php';
const MAX_UPLOAD_BYTES = 3 * 1024 * 1024; const MAX_UPLOAD_BYTES = 3 * 1024 * 1024;
$configPath = __DIR__ . '/deploy-config.php'; try {
if (!is_file($configPath)) { $config = appConfig();
$secrets = appSecrets();
} catch (Throwable $e) {
http_response_code(500); http_response_code(500);
exit('deploy-config.php not found'); exit($e->getMessage());
} }
$config = require $configPath;
$basicUser = (string)($config['basic_auth_user'] ?? ''); $allowedAdminIps = array_values(array_filter(array_map(static fn($ip): string => trim((string)$ip), (array)($secrets['allowed_admin_ips'] ?? [])), static fn(string $ip): bool => $ip !== ''));
$basicPass = (string)($config['basic_auth_pass'] ?? ''); $clientIp = (string)($_SERVER['REMOTE_ADDR'] ?? '');
if ($allowedAdminIps !== [] && !in_array($clientIp, $allowedAdminIps, true)) {
http_response_code(403);
exit('Forbidden');
}
$basicUser = (string)($secrets['basic_auth_user'] ?? '');
$basicPass = (string)($secrets['basic_auth_pass'] ?? '');
if ($basicUser !== '' || $basicPass !== '') { if ($basicUser !== '' || $basicPass !== '') {
$authUser = (string)($_SERVER['PHP_AUTH_USER'] ?? ''); $authUser = (string)($_SERVER['PHP_AUTH_USER'] ?? '');
@ -30,13 +40,27 @@ if ($basicUser !== '' || $basicPass !== '') {
} }
} }
$tokenExpected = (string)($config['token'] ?? ''); $tokenExpected = (string)($secrets['admin_token'] ?? '');
$tokenIncoming = (string)($_REQUEST['token'] ?? ''); $tokenIncoming = (string)($_REQUEST['token'] ?? '');
if ($tokenExpected === '' || !hash_equals($tokenExpected, $tokenIncoming)) { if ($tokenExpected === '' || !hash_equals($tokenExpected, $tokenIncoming)) {
http_response_code(403); http_response_code(403);
exit('Forbidden'); exit('Forbidden');
} }
$deployConfig = (array)($config['deploy'] ?? []);
$deployBranch = trim((string)($deployConfig['branch'] ?? 'main'));
if ($deployBranch === '') {
$deployBranch = 'main';
}
$deployScript = trim((string)($deployConfig['script'] ?? (__DIR__ . '/scripts/deploy.sh')));
if ($deployScript !== '' && !str_starts_with($deployScript, '/')) {
$deployScript = __DIR__ . '/' . ltrim($deployScript, '/');
}
$deployPhpBin = trim((string)($deployConfig['php_bin'] ?? 'php'));
if ($deployPhpBin === '') {
$deployPhpBin = 'php';
}
$requestAction = (string)($_REQUEST['action'] ?? ''); $requestAction = (string)($_REQUEST['action'] ?? '');
if ($_SERVER['REQUEST_METHOD'] === 'GET') { if ($_SERVER['REQUEST_METHOD'] === 'GET') {
adminHandleGetAction($requestAction); adminHandleGetAction($requestAction);
@ -44,6 +68,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$message = ''; $message = '';
$errors = []; $errors = [];
$deployStatus = null;
$deployOutput = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = (string)($_POST['action'] ?? ''); $action = (string)($_POST['action'] ?? '');
@ -51,8 +77,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|| strtolower((string)($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '')) === 'xmlhttprequest'; || strtolower((string)($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '')) === 'xmlhttprequest';
try { try {
$result = adminHandlePostAction($action, $isAjax, __DIR__); $result = adminHandlePostAction($action, $isAjax, __DIR__, [
'branch' => $deployBranch,
'script' => $deployScript,
'php_bin' => $deployPhpBin,
]);
$message = (string)($result['message'] ?? ''); $message = (string)($result['message'] ?? '');
$deployStatus = $result['deploy_status'] ?? null;
$deployOutput = (string)($result['deploy_output'] ?? '');
if (isset($result['errors']) && is_array($result['errors']) && $result['errors'] !== []) { if (isset($result['errors']) && is_array($result['errors']) && $result['errors'] !== []) {
$errors = array_merge($errors, $result['errors']); $errors = array_merge($errors, $result['errors']);
} }
@ -134,6 +166,8 @@ function assetUrl(string $path): string { $f=__DIR__ . '/' . ltrim($path,'/'); $
.sec a{display:block;padding:8px 10px;border-radius:8px;text-decoration:none;color:#111} .sec a{display:block;padding:8px 10px;border-radius:8px;text-decoration:none;color:#111}
.sec a.active{background:#eef4ff;color:#1f6feb} .sec a.active{background:#eef4ff;color:#1f6feb}
.small{font-size:12px;color:#667085} .small{font-size:12px;color:#667085}
.deploy-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
.deploy-output{margin-top:10px;padding:10px;border:1px solid #e5e7eb;border-radius:8px;background:#f8fafc;font-size:12px;white-space:pre-wrap;line-height:1.4;max-height:220px;overflow:auto}
.inline-form{margin:0} .inline-form{margin:0}
.after-slot{display:flex;flex-direction:column;align-items:flex-start;gap:6px} .after-slot{display:flex;flex-direction:column;align-items:flex-start;gap:6px}
.preview-actions{display:flex;gap:6px;margin-top:6px;flex-wrap:nowrap} .preview-actions{display:flex;gap:6px;margin-top:6px;flex-wrap:nowrap}
@ -242,6 +276,43 @@ function assetUrl(string $path): string { $f=__DIR__ . '/' . ltrim($path,'/'); $
</p> </p>
<button class="btn" type="submit">Сохранить настройки</button> <button class="btn" type="submit">Сохранить настройки</button>
</form> </form>
<hr style="border:none;border-top:1px solid #eee;margin:12px 0">
<h4 style="margin:0 0 8px">Обновление проекта</h4>
<p class="small" style="margin:0">Ветка: <strong><?= h($deployBranch) ?></strong></p>
<?php if (is_array($deployStatus)): ?>
<?php $deployState = (string)($deployStatus['state'] ?? ''); ?>
<?php $deployStateMessage = $deployState === 'update_available'
? 'Доступна новая версия.'
: ($deployState === 'up_to_date'
? 'Установлена актуальная версия.'
: ($deployState === 'local_ahead'
? 'Локальная ветка опережает origin. Автообновление отключено.'
: 'Ветка расходится с origin. Нужна ручная синхронизация.')); ?>
<div class="<?= in_array($deployState, ['local_ahead', 'diverged'], true) ? 'err' : 'ok' ?>" style="margin-top:8px">
<?= h($deployStateMessage) ?><br>
<span class="small">Локально: <?= h((string)($deployStatus['local_ref'] ?? '-')) ?> · origin/<?= h($deployBranch) ?>: <?= h((string)($deployStatus['remote_ref'] ?? '-')) ?> · behind: <?= (int)($deployStatus['behind'] ?? 0) ?> · ahead: <?= (int)($deployStatus['ahead'] ?? 0) ?></span>
</div>
<?php endif; ?>
<div class="deploy-actions">
<form method="post" action="?token=<?= urlencode($tokenIncoming) ?>&mode=welcome">
<input type="hidden" name="action" value="check_updates"><input type="hidden" name="token" value="<?= h($tokenIncoming) ?>">
<button class="btn btn-secondary" type="submit">Проверить обновления</button>
</form>
<?php if (is_array($deployStatus) && !empty($deployStatus['can_deploy'])): ?>
<form method="post" action="?token=<?= urlencode($tokenIncoming) ?>&mode=welcome" onsubmit="return confirm('Обновить код из origin/<?= h($deployBranch) ?> и запустить миграции?')">
<input type="hidden" name="action" value="deploy_updates"><input type="hidden" name="token" value="<?= h($tokenIncoming) ?>">
<button class="btn" type="submit">Обновить проект</button>
</form>
<?php endif; ?>
</div>
<?php if ($deployOutput !== ''): ?>
<pre class="deploy-output"><?= h($deployOutput) ?></pre>
<?php endif; ?>
</section> </section>
<?php endif; ?> <?php endif; ?>

View File

@ -8,4 +8,9 @@ return [
'pass' => 'change_me', 'pass' => 'change_me',
'charset' => 'utf8mb4', 'charset' => 'utf8mb4',
], ],
'deploy' => [
'branch' => 'main',
'script' => __DIR__ . '/scripts/deploy.sh',
'php_bin' => 'php',
],
]; ];

View File

@ -1,25 +0,0 @@
<?php
/**
* Copy this file to deploy-config.php and fill secrets.
* deploy-config.php is ignored by git.
*/
return [
// REQUIRED: long random token (40+ chars)
'token' => 'CHANGE_ME_TO_LONG_RANDOM_TOKEN',
// Optional: HTTP Basic auth layer
'basic_auth_user' => '',
'basic_auth_pass' => '',
// Optional: allow only these client IPs (empty = allow all)
'allowed_ips' => [
// '1.2.3.4',
],
// Deploy options
'branch' => 'main',
'deploy_script' => __DIR__ . '/scripts/deploy.sh',
// Where to write webhook logs
'log_file' => __DIR__ . '/data/deploy-webhook.log',
];

View File

@ -1,86 +0,0 @@
<?php
declare(strict_types=1);
$configPath = __DIR__ . '/deploy-config.php';
if (!is_file($configPath)) {
http_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
echo "deploy-config.php not found. Create it from deploy-config.php.example\n";
exit;
}
/** @var array<string,mixed> $config */
$config = require $configPath;
$tokenExpected = (string)($config['token'] ?? '');
$allowedIps = (array)($config['allowed_ips'] ?? []);
$basicUser = (string)($config['basic_auth_user'] ?? '');
$basicPass = (string)($config['basic_auth_pass'] ?? '');
$branch = (string)($config['branch'] ?? 'main');
$deployScript = (string)($config['deploy_script'] ?? (__DIR__ . '/scripts/deploy.sh'));
$logFile = (string)($config['log_file'] ?? (__DIR__ . '/data/deploy-webhook.log'));
header('Content-Type: text/plain; charset=utf-8');
if ($basicUser !== '' || $basicPass !== '') {
$authUser = $_SERVER['PHP_AUTH_USER'] ?? '';
$authPass = $_SERVER['PHP_AUTH_PW'] ?? '';
if (!hash_equals($basicUser, (string)$authUser) || !hash_equals($basicPass, (string)$authPass)) {
header('WWW-Authenticate: Basic realm="Deploy"');
http_response_code(401);
echo "Unauthorized\n";
exit;
}
}
$clientIp = (string)($_SERVER['REMOTE_ADDR'] ?? 'unknown');
if ($allowedIps !== [] && !in_array($clientIp, $allowedIps, true)) {
http_response_code(403);
echo "Forbidden: IP not allowed\n";
logLine($logFile, "DENY ip={$clientIp} reason=ip_not_allowed");
exit;
}
$tokenIncoming = (string)($_REQUEST['token'] ?? '');
if ($tokenExpected === '' || !hash_equals($tokenExpected, $tokenIncoming)) {
http_response_code(403);
echo "Forbidden: invalid token\n";
logLine($logFile, "DENY ip={$clientIp} reason=bad_token");
exit;
}
if (!is_file($deployScript)) {
http_response_code(500);
echo "Deploy script not found\n";
logLine($logFile, "ERROR ip={$clientIp} reason=script_missing path={$deployScript}");
exit;
}
$cmd = 'BRANCH=' . escapeshellarg($branch) . ' bash ' . escapeshellarg($deployScript) . ' 2>&1';
exec($cmd, $output, $code);
$preview = implode("\n", array_slice($output, -30));
logLine(
$logFile,
"RUN ip={$clientIp} code={$code} branch={$branch} output=" . str_replace(["\n", "\r"], ['\\n', ''], $preview)
);
if ($code !== 0) {
http_response_code(500);
echo "Deploy failed\n\n";
echo $preview . "\n";
exit;
}
echo "OK: deploy completed\n\n";
echo $preview . "\n";
function logLine(string $path, string $line): void
{
$dir = dirname($path);
if (!is_dir($dir)) {
@mkdir($dir, 0775, true);
}
$ts = date('Y-m-d H:i:s');
@file_put_contents($path, "[{$ts}] {$line}\n", FILE_APPEND);
}

97
lib/admin_deploy.php Normal file
View File

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
function adminCheckForUpdates(string $projectRoot, string $branch): array
{
if (!is_dir($projectRoot . '/.git')) {
throw new RuntimeException('Репозиторий не найден: .git отсутствует');
}
$fetch = adminRunShellCommand('git fetch origin ' . escapeshellarg($branch) . ' --prune', $projectRoot);
if ($fetch['code'] !== 0) {
throw new RuntimeException('Не удалось обновить данные из origin: ' . adminTailOutput($fetch['output']));
}
$local = adminRunShellCommand('git rev-parse --short=12 HEAD', $projectRoot);
$remote = adminRunShellCommand('git rev-parse --short=12 origin/' . escapeshellarg($branch), $projectRoot);
$behindRaw = adminRunShellCommand('git rev-list --count HEAD..origin/' . escapeshellarg($branch), $projectRoot);
$aheadRaw = adminRunShellCommand('git rev-list --count origin/' . escapeshellarg($branch) . '..HEAD', $projectRoot);
if ($local['code'] !== 0 || $remote['code'] !== 0 || $behindRaw['code'] !== 0 || $aheadRaw['code'] !== 0) {
throw new RuntimeException('Не удалось определить состояние ветки');
}
$behind = (int)trim($behindRaw['output']);
$ahead = (int)trim($aheadRaw['output']);
$state = 'up_to_date';
if ($behind > 0 && $ahead === 0) {
$state = 'update_available';
} elseif ($ahead > 0 && $behind === 0) {
$state = 'local_ahead';
} elseif ($ahead > 0 && $behind > 0) {
$state = 'diverged';
}
return [
'state' => $state,
'branch' => $branch,
'local_ref' => trim($local['output']),
'remote_ref' => trim($remote['output']),
'behind' => $behind,
'ahead' => $ahead,
'can_deploy' => $state === 'update_available',
];
}
function adminRunDeployScript(string $projectRoot, string $branch, string $scriptPath, string $phpBin): array
{
if (!is_file($scriptPath)) {
throw new RuntimeException('Скрипт деплоя не найден: ' . $scriptPath);
}
$run = adminRunShellCommand('bash ' . escapeshellarg($scriptPath), $projectRoot, [
'BRANCH' => $branch,
'PHP_BIN' => $phpBin,
]);
return [
'ok' => $run['code'] === 0,
'code' => $run['code'],
'output' => adminTailOutput($run['output']),
];
}
function adminRunShellCommand(string $command, string $cwd, array $env = []): array
{
$envPrefix = '';
foreach ($env as $key => $value) {
if (!preg_match('/^[A-Z_][A-Z0-9_]*$/', (string)$key)) {
continue;
}
$envPrefix .= $key . '=' . escapeshellarg((string)$value) . ' ';
}
$fullCommand = 'cd ' . escapeshellarg($cwd) . ' && ' . $envPrefix . $command . ' 2>&1';
$output = [];
$code = 0;
exec($fullCommand, $output, $code);
return ['code' => $code, 'output' => implode("\n", $output)];
}
function adminTailOutput(string $output, int $maxLines = 80): string
{
$output = trim($output);
if ($output === '') {
return '';
}
$lines = preg_split('/\r\n|\r|\n/', $output);
if (!is_array($lines)) {
return $output;
}
return implode("\n", array_slice($lines, -$maxLines));
}

View File

@ -2,10 +2,12 @@
declare(strict_types=1); declare(strict_types=1);
function adminHandlePostAction(string $action, bool $isAjax, string $projectRoot): array function adminHandlePostAction(string $action, bool $isAjax, string $projectRoot, array $deployOptions = []): array
{ {
$message = ''; $message = '';
$errors = []; $errors = [];
$deployStatus = null;
$deployOutput = '';
switch ($action) { switch ($action) {
case 'create_section': { case 'create_section': {
@ -141,6 +143,56 @@ function adminHandlePostAction(string $action, bool $isAjax, string $projectRoot
break; break;
} }
case 'check_updates': {
$branch = (string)($deployOptions['branch'] ?? 'main');
$deployStatus = adminCheckForUpdates($projectRoot, $branch);
$state = (string)($deployStatus['state'] ?? '');
if ($state === 'update_available') {
$message = 'Найдена новая версия. Можно обновиться.';
} elseif ($state === 'up_to_date') {
$message = 'Обновлений нет: установлена актуальная версия.';
} elseif ($state === 'local_ahead') {
$message = 'Локальная ветка опережает origin. Автообновление отключено.';
} else {
$message = 'Ветка расходится с origin. Нужна ручная синхронизация.';
}
break;
}
case 'deploy_updates': {
$branch = (string)($deployOptions['branch'] ?? 'main');
$scriptPath = (string)($deployOptions['script'] ?? ($projectRoot . '/scripts/deploy.sh'));
$phpBin = (string)($deployOptions['php_bin'] ?? 'php');
$deployStatus = adminCheckForUpdates($projectRoot, $branch);
if (!(bool)($deployStatus['can_deploy'] ?? false)) {
$state = (string)($deployStatus['state'] ?? '');
if ($state === 'up_to_date') {
$message = 'Обновление не требуется: уже актуальная версия.';
break;
}
if ($state === 'local_ahead') {
throw new RuntimeException('Локальная ветка опережает origin. Автообновление отключено.');
}
if ($state === 'diverged') {
throw new RuntimeException('Ветка расходится с origin. Выполни ручную синхронизацию.');
}
throw new RuntimeException('Нельзя применить обновление в текущем состоянии ветки.');
}
$deployResult = adminRunDeployScript($projectRoot, $branch, $scriptPath, $phpBin);
$deployOutput = (string)($deployResult['output'] ?? '');
if (!(bool)($deployResult['ok'] ?? false)) {
throw new RuntimeException('Деплой завершился с ошибкой: ' . ($deployOutput !== '' ? $deployOutput : ('код ' . (int)($deployResult['code'] ?? 1))));
}
$deployStatus = adminCheckForUpdates($projectRoot, $branch);
$message = 'Обновление выполнено.';
break;
}
case 'upload_before_bulk': { case 'upload_before_bulk': {
$sectionId = (int)($_POST['section_id'] ?? 0); $sectionId = (int)($_POST['section_id'] ?? 0);
if ($sectionId < 1 || !sectionById($sectionId)) { if ($sectionId < 1 || !sectionById($sectionId)) {
@ -392,5 +444,10 @@ function adminHandlePostAction(string $action, bool $isAjax, string $projectRoot
} }
} }
return ['message' => $message, 'errors' => $errors]; return [
'message' => $message,
'errors' => $errors,
'deploy_status' => $deployStatus,
'deploy_output' => $deployOutput,
];
} }

View File

@ -22,6 +22,26 @@ function appConfig(): array
return $cfg; return $cfg;
} }
function appSecrets(): array
{
static $secrets = null;
if ($secrets !== null) {
return $secrets;
}
$path = __DIR__ . '/../secrets.php';
if (!is_file($path)) {
throw new RuntimeException('secrets.php not found. Copy secrets.php.example');
}
$secrets = require $path;
if (!is_array($secrets)) {
throw new RuntimeException('Invalid secrets.php format');
}
return $secrets;
}
function db(): PDO function db(): PDO
{ {
static $pdo = null; static $pdo = null;

View File

@ -7,9 +7,11 @@ set -euo pipefail
# Optional env: # Optional env:
# APP_DIR=/home/USER/www/photo-gallery # APP_DIR=/home/USER/www/photo-gallery
# BRANCH=main # BRANCH=main
# PHP_BIN=php
APP_DIR="${APP_DIR:-$(cd "$(dirname "$0")/.." && pwd)}" APP_DIR="${APP_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
BRANCH="${BRANCH:-main}" BRANCH="${BRANCH:-main}"
PHP_BIN="${PHP_BIN:-php}"
cd "$APP_DIR" cd "$APP_DIR"
@ -42,6 +44,9 @@ fi
git fetch --all --prune git fetch --all --prune
git reset --hard "origin/$BRANCH" git reset --hard "origin/$BRANCH"
# Run DB migrations required by current code
"$PHP_BIN" scripts/migrate.php
# Make sure runtime files exist # Make sure runtime files exist
[ -f data/last_indexed.txt ] || echo "0" > data/last_indexed.txt [ -f data/last_indexed.txt ] || echo "0" > data/last_indexed.txt

10
secrets.php.example Normal file
View File

@ -0,0 +1,10 @@
<?php
return [
'admin_token' => 'CHANGE_ME_TO_LONG_RANDOM_TOKEN',
'basic_auth_user' => '',
'basic_auth_pass' => '',
'allowed_admin_ips' => [
// '1.2.3.4',
],
];