Files
portal/app/app/Http/Controllers/Api/AdminSystemSettingsController.php
T
Дмитрий 1fef2571e8
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
feat(y360): баланс почты Яндекс 360 — ручной ввод + кнопка Пополнить
email — денежный сервис; сумма вписывается в админке «Система» (Yandex360BalanceStore),
светофор по порогам, кнопка «Открыть оплату»/«Пополнить» → admin.yandex.ru/products.
Робот-скрейпер отклонён (SPA Яндекса враждебен ботам + автопополнение защищает баланс).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 13:34:13 +03:00

125 lines
5.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 типы — без валидации
};
}
}