79 lines
3.7 KiB
PHP
79 lines
3.7 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
require_once dirname(__DIR__, 4) . '/backend/JwtToken.php';
|
|
require_once dirname(__DIR__, 4) . '/backend/ApiAuth.php';
|
|
require_once dirname(__DIR__) . '/_bootstrap.php';
|
|
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
|
if (!in_array($method, ['GET', 'PATCH'], true)) {
|
|
header('Allow: GET, PATCH');
|
|
api_json(['error' => 'method_not_allowed'], 405);
|
|
}
|
|
$auth = ApiAuth::bearerClaims();
|
|
ApiAuth::requireRole($auth, ['owner', 'content_manager']);
|
|
|
|
try {
|
|
$db = Database::getInstance();
|
|
if ($method === 'GET') {
|
|
$result = $db->getConnection()->query('SELECT setting_key, setting_value, is_public, revision, updated_at FROM app_settings ORDER BY setting_key');
|
|
$settings = [];
|
|
while ($row = $result->fetch_assoc()) {
|
|
$settings[] = [
|
|
'key' => $row['setting_key'],
|
|
'value' => json_decode($row['setting_value'], true),
|
|
'is_public' => (bool) $row['is_public'],
|
|
'revision' => (int) $row['revision'],
|
|
'updated_at' => $row['updated_at'],
|
|
];
|
|
}
|
|
api_json(['settings' => $settings]);
|
|
}
|
|
|
|
$body = json_decode(file_get_contents('php://input') ?: '', true);
|
|
if (!is_array($body) || !is_array($body['settings'] ?? null) || count($body['settings']) > 100) {
|
|
api_json(['error' => 'invalid_settings_payload'], 400);
|
|
}
|
|
$connection = $db->getConnection();
|
|
$connection->begin_transaction();
|
|
$read = $db->prepare('SELECT setting_value, is_public, revision FROM app_settings WHERE setting_key = ? FOR UPDATE');
|
|
$write = $db->prepare('INSERT INTO app_settings (setting_key, setting_value, is_public, revision, updated_by) VALUES (?, ?, ?, 1, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value), is_public = VALUES(is_public), revision = revision + 1, updated_by = VALUES(updated_by)');
|
|
$audit = $db->prepare('INSERT INTO app_setting_audit (setting_key, previous_value, new_value, actor_user_id) VALUES (?, ?, ?, ?)');
|
|
foreach ($body['settings'] as $item) {
|
|
if (!is_array($item) || !is_string($item['key'] ?? null) || !preg_match('/^[a-z][a-z0-9_.-]{0,99}$/', $item['key']) || !array_key_exists('value', $item) || !is_bool($item['is_public'] ?? null)) {
|
|
$connection->rollback();
|
|
api_json(['error' => 'invalid_setting'], 400);
|
|
}
|
|
$key = $item['key'];
|
|
if (preg_match('/(secret|password|token|credential|private.?key|api.?key)/i', $key)) {
|
|
$connection->rollback();
|
|
api_json(['error' => 'secrets_must_use_server_environment'], 400);
|
|
}
|
|
$encodedValue = json_encode($item['value'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
if ($encodedValue === false || strlen($encodedValue) > 65535) {
|
|
$connection->rollback();
|
|
api_json(['error' => 'setting_value_too_large'], 400);
|
|
}
|
|
$read->bind_param('s', $key);
|
|
$read->execute();
|
|
$previous = $read->get_result()->fetch_assoc();
|
|
$isPublic = $item['is_public'] ? 1 : 0;
|
|
$actor = $auth['user_id'];
|
|
$write->bind_param('ssii', $key, $encodedValue, $isPublic, $actor);
|
|
$write->execute();
|
|
$previousValue = $previous['setting_value'] ?? null;
|
|
$audit->bind_param('sssi', $key, $previousValue, $encodedValue, $actor);
|
|
$audit->execute();
|
|
}
|
|
$read->close();
|
|
$write->close();
|
|
$audit->close();
|
|
$connection->commit();
|
|
api_json(['status' => 'updated']);
|
|
} catch (Throwable $exception) {
|
|
if (isset($connection) && $connection instanceof mysqli) {
|
|
try { $connection->rollback(); } catch (Throwable $ignored) {}
|
|
}
|
|
error_log('Admin settings update failed: ' . $exception->getMessage());
|
|
api_json(['error' => 'service_unavailable'], 503);
|
|
}
|