1fef2571e8
email — денежный сервис; сумма вписывается в админке «Система» (Yandex360BalanceStore), светофор по порогам, кнопка «Открыть оплату»/«Пополнить» → admin.yandex.ru/products. Робот-скрейпер отклонён (SPA Яндекса враждебен ботам + автопополнение защищает баланс). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
125 lines
5.1 KiB
PHP
125 lines
5.1 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Http\Controllers\Api;
|
||
|
||
use App\Http\Controllers\Controller;
|
||
use App\Models\SaasAdminAuditLog;
|
||
use App\Models\SystemSetting;
|
||
use Illuminate\Http\JsonResponse;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
/**
|
||
* SaaS-admin → Система: edit-flow для system_settings.
|
||
*
|
||
* GET /api/admin/system-settings → список всех ключей.
|
||
* PUT /api/admin/system-settings/{key} → обновление с reason ≥30 chars.
|
||
*
|
||
* Каждое обновление пишет запись в `saas_admin_audit_log`
|
||
* (action='system_settings.update', target_type='system_setting',
|
||
* payload_before/after, reason, ip_address). Hash-chain trigger BEFORE INSERT
|
||
* заполняет log_hash автоматически (OPEN-И-15).
|
||
*
|
||
* Type-validation: int → ctype_digit, decimal → numeric, bool → in('true','false'),
|
||
* json → json_decode без ошибок. Иначе 422.
|
||
*
|
||
* NB: на MVP saas_admin_users auth не реализован — admin_user_id и ip_address
|
||
* принимаются параметрами. Production: middleware('auth:saas-admin') +
|
||
* $request->user()->id + $request->ip().
|
||
*/
|
||
class AdminSystemSettingsController extends Controller
|
||
{
|
||
/** Ключи-секреты: значение не отдаём в общий список (хранится зашифрованно). */
|
||
private const SENSITIVE_KEYS = ['supplier_webhook_secret'];
|
||
|
||
/** GET /api/admin/system-settings */
|
||
public function index(): JsonResponse
|
||
{
|
||
$settings = SystemSetting::orderBy('key')->get()->map(function (SystemSetting $s) {
|
||
if (in_array($s->key, self::SENSITIVE_KEYS, true)) {
|
||
$s->value = $s->value === '' ? '' : '••• (скрыто)';
|
||
}
|
||
|
||
return $s;
|
||
});
|
||
|
||
return response()->json(['settings' => $settings]);
|
||
}
|
||
|
||
/** PUT /api/admin/system-settings/{key} */
|
||
public function update(Request $request, string $key): JsonResponse
|
||
{
|
||
$newValue = (string) $request->input('value');
|
||
$reason = $request->string('reason')->toString();
|
||
$adminUserId = (int) $request->input('admin_user_id', 1); // TODO: $request->user()->id
|
||
|
||
if (mb_strlen($reason) < 30) {
|
||
return response()->json([
|
||
'message' => 'Основание (reason) должно быть не короче 30 символов.',
|
||
'errors' => ['reason' => ['Минимум 30 символов.']],
|
||
], 422);
|
||
}
|
||
|
||
$setting = SystemSetting::find($key);
|
||
if (! $setting) {
|
||
return response()->json(['message' => "Настройка «{$key}» не найдена."], 404);
|
||
}
|
||
|
||
if (! $this->isValidForType($newValue, $setting->type)) {
|
||
return response()->json([
|
||
'message' => "Значение не соответствует типу «{$setting->type}».",
|
||
'errors' => ['value' => ["Должно быть валидным {$setting->type}."]],
|
||
], 422);
|
||
}
|
||
|
||
$oldValue = $setting->value;
|
||
if ($oldValue === $newValue) {
|
||
return response()->json([
|
||
'message' => 'Значение не изменилось.',
|
||
'errors' => ['value' => ['Новое значение совпадает со старым.']],
|
||
], 422);
|
||
}
|
||
|
||
DB::transaction(function () use ($setting, $oldValue, $newValue, $reason, $adminUserId, $request) {
|
||
$setting->update([
|
||
'value' => $newValue,
|
||
'updated_at' => now(),
|
||
'updated_by' => $adminUserId,
|
||
]);
|
||
|
||
SaasAdminAuditLog::create([
|
||
'admin_user_id' => $adminUserId,
|
||
'action' => 'system_settings.update',
|
||
'target_type' => 'system_setting',
|
||
'target_id' => null, // PK — string key, не bigint
|
||
'payload_before' => ['key' => $setting->key, 'value' => $oldValue],
|
||
'payload_after' => ['key' => $setting->key, 'value' => $newValue],
|
||
'reason' => $reason,
|
||
'ip_address' => $request->ip() ?? '127.0.0.1',
|
||
'user_agent' => $request->userAgent(),
|
||
]);
|
||
});
|
||
|
||
return response()->json([
|
||
'key' => $setting->key,
|
||
'value' => $newValue,
|
||
'previous_value' => $oldValue,
|
||
'updated_at' => $setting->updated_at->toIso8601String(),
|
||
'message' => 'Настройка обновлена.',
|
||
]);
|
||
}
|
||
|
||
private function isValidForType(string $value, string $type): bool
|
||
{
|
||
return match ($type) {
|
||
'int' => ctype_digit($value) || (str_starts_with($value, '-') && ctype_digit(substr($value, 1))),
|
||
'decimal' => is_numeric($value),
|
||
'bool' => in_array($value, ['true', 'false', '1', '0'], true),
|
||
'json' => json_decode($value, true, 16, JSON_THROW_ON_ERROR) !== null || $value === 'null',
|
||
default => true, // 'string' и unknown типы — без валидации
|
||
};
|
||
}
|
||
}
|