Files
boursecu_server/app/Http/Controllers/Admin/SettingsController.php
T
mohmdsdgandCursor b21b136711 fix(settings): honor last-price switch and overlay only SuperAdmin access flags
Stored DB prices were treated as live quotes, and SuperAdmin overlay overwrote tenant keys like trading hours. Keep overlay to the 14 access flags and hide fallback prices when the panel says so.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 03:01:07 +03:30

428 lines
15 KiB
PHP

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Setting;
use App\Services\ConfigService;
use App\Services\PriceChannelBroadcastService;
use App\Services\SuperAdminProgramSettingsOverlay;
use Illuminate\Http\Request;
class SettingsController extends Controller
{
private const DEPLOY_ADMIN_MOBILE_SUFFIX = '9128081392';
private const NEW_SETTING_NOTIFY_MOBILE = '09134469985';
/**
* کلیدهایی که پنل ادمین باید بتواند ویرایش کند حتی اگر در DB هنوز can_edit=0 باشد
* (بنرها / سایت‌های ری‌گیری).
*/
private const PANEL_MANAGED_KEYS = [
'tracking_systems',
];
public function index(Request $request)
{
$query = Setting::query()
->select(Setting::adminSelectColumns())
->orderBy('key');
if ($request->boolean('system_only')) {
$local = $query->get();
return response()->json($this->composeSystemSettings($local));
}
if ($request->boolean('editable_only')) {
$local = $query->get();
return response()->json($this->composeEditableSettings($local));
}
$local = $query->get();
return response()->json($this->composeGetSettings($local));
}
/**
* اپ کاربر (khanegi_user / theme / ghatreh) — فقط ردیف‌های show_to_user=1، بدون credential.
* پنل ادمین از admin/get_settings استفاده می‌کند.
*/
public function publicIndex()
{
if (! Setting::hasShowToUserColumn()) {
return response()->json([]);
}
$local = Setting::query()
->select(Setting::adminSelectColumns())
->where('show_to_user', 1)
->orderBy('key')
->get();
return response()->json(Setting::forClientApi($this->composeGetSettings($local)));
}
/**
* جدول settings همین نسخه — بدون overlay سوپرادمین.
* سوپرادمین برای ایمپورت از این مسیر can_edit=0 را برمی‌دارد.
*/
public function localTable()
{
$rows = Setting::query()
->select(Setting::adminSelectColumns())
->orderBy('key')
->get();
return response()->json($rows);
}
/**
* پاک کردن کش overlay و خواندن دوباره از سوپرادمین
* (همهٔ کلیدهای can_edit=0 مالک سوپرادمین — دسترسی مشتری به قابلیت‌ها).
*/
public function refreshSuperAdminOverlay()
{
$result = app(SuperAdminProgramSettingsOverlay::class)->refreshFromSuperAdmin();
if (! ($result['ok'] ?? false)) {
return response()->json([
'message' => 'ارتباط با سوپرادمین برقرار نشد؛ کش پاک شد و در درخواست بعدی دوباره تلاش می‌شود',
'ok' => false,
'count' => 0,
'items' => [],
], 502);
}
$count = (int) ($result['count'] ?? 0);
return response()->json([
'message' => "همگام شد ({$count} کلید دسترسی از سوپرادمین)",
'ok' => true,
'count' => $count,
'items' => $result['items'] ?? [],
'source' => $result['source'] ?? 'superadmin',
]);
}
public function edit(Request $request)
{
$key = trim((string) $request->key);
$isDeployAdmin = $this->isDeployAdminUser();
$isSystemDeploy = $request->boolean('system_deploy');
$setting = Setting::where('key', $key)->first();
$isNew = !$setting;
if (SuperAdminProgramSettingsOverlay::isSuperAdminOwned($key)) {
return response()->json([
'message' => 'این تنظیم از سوپرادمین کنترل می‌شود و در پنل خانگی قابل ویرایش نیست',
], 403);
}
if ($isNew) {
if ($isSystemDeploy && !$isDeployAdmin) {
return response()->json([
'message' => 'افزودن تنظیم پیشرفته فقط برای ادمین دیپلوی مجاز است',
], 403);
}
$setting = new Setting();
$setting->key = $key;
$setting->can_edit = $isSystemDeploy ? 0 : 1;
} elseif (
SuperAdminProgramSettingsOverlay::isRuntime($key)
&& ! $this->isPanelManagedKey($key)
) {
return response()->json([
'message' => 'این مقدار توسط سیستم به‌روز می‌شود و از پنل قابل ویرایش نیست',
], 403);
} elseif (!$setting->can_edit && !$isDeployAdmin && !$this->isPanelManagedKey($key)) {
return response()->json([
'message' => 'این تنظیم پیشرفته است و فقط ادمین دیپلوی می‌تواند آن را تغییر دهد',
], 403);
}
$previousValue = $setting->exists ? (string) ($setting->value ?? '') : null;
$setting->value = $request->value;
$setting->description = $request->description;
if ($request->has('is_active')) {
$setting->is_active = filter_var($request->is_active, FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
} elseif ($isNew) {
$setting->is_active = 1;
}
$setting->title_fa = $request->title_fa ?? '';
if (Setting::hasTypeColumn()) {
$setting->type = $request->type ?? '';
}
$setting->save();
if (
$request->key === 'price_channel_message_text'
&& $previousValue !== (string) ($request->value ?? '')
) {
dispatch(function () {
app(PriceChannelBroadcastService::class)->broadcastAfterTemplateChange();
})->afterResponse();
}
if (app(ConfigService::class)->isTradingHoursSettingKey($key)) {
try {
app(ConfigService::class)->notifyTradingHoursChanged();
} catch (\Throwable $e) {
report($e);
}
}
if ($isNew) {
$projectName = Setting::where('key', 'name_fa')->value('value') ?: 'پروژه';
$settingTitle = trim($request->title_fa ?? '') ?: $request->key;
$this->Send(
self::NEW_SETTING_NOTIFY_MOBILE,
"تنظیم جدید: {$settingTitle}\nپروژه: {$projectName}",
false
);
}
return response()->json($setting);
}
private function normalizeAdminMobile(?string $mobile): string
{
$digits = preg_replace('/\D+/', '', (string) $mobile) ?? '';
if (str_starts_with($digits, '98') && strlen($digits) >= 12) {
$digits = substr($digits, 2);
}
if (strlen($digits) === 11 && str_starts_with($digits, '0')) {
$digits = substr($digits, 1);
}
return $digits;
}
private function isDeployAdminUser(): bool
{
$user = auth('user')->user() ?? auth()->user();
if (!$user) {
return false;
}
return $this->normalizeAdminMobile($user->mobile ?? '') === self::DEPLOY_ADMIN_MOBILE_SUFFIX;
}
private function isPanelManagedKey(string $key): bool
{
return in_array($key, self::PANEL_MANAGED_KEYS, true)
|| app(ConfigService::class)->isTradingHoursSettingKey($key);
}
/**
* تنظیمات پیشرفته: ۱۴ دسترسی از سوپرادمین + بقیهٔ can_edit=0 از DB لوکال.
*/
private function composeSystemSettings($localRows)
{
$overlay = app(SuperAdminProgramSettingsOverlay::class);
$saMap = $overlay->catalogMap();
$byKey = [];
$localByKey = [];
foreach ($localRows as $row) {
$localByKey[(string) $row->key] = $row;
}
if ($saMap !== null) {
foreach ($saMap as $key => $item) {
if (! is_array($item)) {
continue;
}
if (! SuperAdminProgramSettingsOverlay::isSuperAdminOwned((string) $key)) {
continue;
}
$value = $item['value'] ?? null;
if ($value !== null && $value !== '') {
$byKey[(string) $key] = $this->overlayRowToSetting($item, $localByKey[(string) $key] ?? null);
}
}
foreach ($localRows as $row) {
$key = (string) $row->key;
if (isset($byKey[$key])) {
continue;
}
if (
SuperAdminProgramSettingsOverlay::isRuntime($key)
|| SuperAdminProgramSettingsOverlay::isLocalOwned($key)
) {
continue;
}
if (! $this->isEditableCanEdit($row->can_edit ?? null)) {
$byKey[$key] = $row;
}
}
} else {
foreach ($localRows as $row) {
if (! $this->isEditableCanEdit($row->can_edit ?? null)) {
$byKey[(string) $row->key] = $row;
}
}
}
foreach ($localRows as $row) {
$key = (string) $row->key;
if (
SuperAdminProgramSettingsOverlay::isLocalOwned($key)
|| SuperAdminProgramSettingsOverlay::isRuntime($key)
) {
$byKey[$key] = $this->normalizeTenantOwnedRow($row);
}
}
return collect(array_values($byKey))->sortBy(fn ($row) => (string) ($row->key ?? ''))->values();
}
/**
* تنظیمات عادی پنل — can_edit=1 در DB + کلیدهای runtime (کد tenant می‌نویسد).
*/
private function composeEditableSettings($localRows)
{
$rows = collect($localRows)->filter(function ($row) {
$key = (string) ($row->key ?? '');
return $this->isEditableCanEdit($row->can_edit ?? null)
|| SuperAdminProgramSettingsOverlay::isRuntime($key)
|| SuperAdminProgramSettingsOverlay::isLocalOwned($key);
});
return $rows
->map(fn ($row) => $this->normalizeTenantOwnedRow($row))
->sortBy(fn ($row) => (string) ($row->key ?? ''))
->values();
}
/**
* get_settings ادمین (و پایهٔ publicIndex قبل از فیلتر show_to_user):
* can_edit=1 (+ runtime + local-owned) از DB لوکال
* فقط ۱۴ دسترسی سوپرادمین از overlay؛ بقیهٔ can_edit=0 از کپی لوکال
* اگر سوپرادمین در دسترس نباشد کل جدول لوکال برمی‌گردد.
*/
private function composeGetSettings($localRows)
{
$overlay = app(SuperAdminProgramSettingsOverlay::class);
$saMap = $overlay->catalogMap();
if ($saMap === null) {
return $localRows;
}
$localByKey = [];
foreach ($localRows as $row) {
$localByKey[(string) $row->key] = $row;
}
$byKey = [];
foreach ($localRows as $row) {
$key = (string) $row->key;
if (
$this->isEditableCanEdit($row->can_edit ?? null)
|| SuperAdminProgramSettingsOverlay::isRuntime($key)
|| SuperAdminProgramSettingsOverlay::isLocalOwned($key)
) {
$byKey[$key] = $this->normalizeTenantOwnedRow($row);
}
}
foreach ($saMap as $key => $item) {
if (! is_array($item)) {
continue;
}
$key = (string) $key;
if (! SuperAdminProgramSettingsOverlay::isSuperAdminOwned($key)) {
continue;
}
$value = $item['value'] ?? null;
if ($value !== null && $value !== '') {
$byKey[$key] = $this->overlayRowToSetting($item, $localByKey[$key] ?? null);
}
}
foreach ($localRows as $row) {
$key = (string) $row->key;
if (isset($byKey[$key])) {
continue;
}
if (
SuperAdminProgramSettingsOverlay::isRuntime($key)
|| SuperAdminProgramSettingsOverlay::isLocalOwned($key)
) {
$byKey[$key] = $this->normalizeTenantOwnedRow($row);
continue;
}
if (! $this->isEditableCanEdit($row->can_edit ?? null)) {
$byKey[$key] = $row;
}
}
return collect(array_values($byKey))->sortBy(fn ($row) => (string) ($row->key ?? ''))->values();
}
private function isEditableCanEdit(mixed $canEdit): bool
{
return $canEdit === 1 || $canEdit === true || $canEdit === '1';
}
private function mergeOverlayIntoLocal($rows)
{
return $this->composeGetSettings($rows);
}
/**
* @param array<int, array<string, mixed>> $overlay
*/
private function overlayToRows(array $overlay)
{
return collect($overlay)
->filter(fn ($item) => is_array($item) && trim((string) ($item['key'] ?? '')) !== '')
->map(fn ($item) => $this->overlayRowToSetting($item))
->values();
}
private function overlayRowToSetting(array $item, ?Setting $local = null): Setting
{
$setting = new Setting();
$setting->id = isset($item['id']) && is_numeric($item['id']) ? (int) $item['id'] : null;
$setting->key = (string) ($item['key'] ?? '');
$setting->value = $item['value'] ?? '';
$setting->title_fa = $item['title_fa'] ?? '';
$setting->description = $item['description'] ?? '';
$setting->type = $item['type'] ?? '';
$setting->is_active = $item['is_active'] ?? 1;
$setting->can_edit = 0;
if ($local) {
if (Setting::hasMenuColumn()) {
$setting->menu = $local->menu;
}
if (Setting::hasShowToUserColumn()) {
$setting->show_to_user = $local->show_to_user;
}
}
return $setting;
}
/** کلیدهای tenant-only — can_edit=1 در پاسخ API حتی اگر DB قدیمی 0 باشد */
private function normalizeTenantOwnedRow(Setting $row): Setting
{
$key = (string) ($row->key ?? '');
if (
SuperAdminProgramSettingsOverlay::isRuntime($key)
|| SuperAdminProgramSettingsOverlay::isLocalOwned($key)
) {
$row->can_edit = 1;
}
return $row;
}
}