diff --git a/app/Http/Controllers/Admin/SettingsController.php b/app/Http/Controllers/Admin/SettingsController.php index 14096b0..da09635 100644 --- a/app/Http/Controllers/Admin/SettingsController.php +++ b/app/Http/Controllers/Admin/SettingsController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Models\Setting; use App\Services\PriceChannelBroadcastService; +use App\Services\SuperAdminProgramSettingsOverlay; use Illuminate\Http\Request; class SettingsController extends Controller @@ -19,8 +20,6 @@ class SettingsController extends Controller */ private const PANEL_MANAGED_KEYS = [ 'tracking_systems', - 'tracking_systems_enabled', - 'banner_enabled', ]; public function index(Request $request) @@ -30,6 +29,11 @@ class SettingsController extends Controller ->orderBy('key'); if ($request->boolean('system_only')) { + $overlay = app(SuperAdminProgramSettingsOverlay::class)->items(); + if ($overlay) { + return response()->json($this->authorizeSettingsPayload($this->overlayToRows($overlay))); + } + $rows = $query ->where(function ($q) { $q->where('can_edit', 0)->orWhere('can_edit', false); @@ -48,26 +52,30 @@ class SettingsController extends Controller return response()->json($this->authorizeSettingsPayload($query->where('can_edit', 1)->get())); } - return response()->json($this->authorizeSettingsPayload($query->get())); + return response()->json($this->authorizeSettingsPayload( + $this->mergeOverlayIntoLocal($query->get()) + )); } public function edit(Request $request) { - $isDeployAdmin = $this->isDeployAdminUser(); - $isSystemDeploy = $request->boolean('system_deploy'); $setting = Setting::where('key', $request->key)->first(); $isNew = !$setting; if ($isNew) { - if ($isSystemDeploy && !$isDeployAdmin) { - return response()->json(['message' => 'Unauthorized'], 403); + if ($request->boolean('system_deploy')) { + return response()->json([ + 'message' => 'تنظیمات پیشرفته فقط از سوپرادمین اضافه می‌شوند', + ], 403); } $setting = new Setting(); $setting->key = $request->key; - $setting->can_edit = $isSystemDeploy ? 0 : 1; - } elseif (!$setting->can_edit && !$isDeployAdmin && !$this->isPanelManagedKey((string) $request->key)) { - return response()->json(['message' => 'Forbidden'], 403); + $setting->can_edit = 1; + } elseif (!$setting->can_edit && !$this->isPanelManagedKey((string) $request->key)) { + return response()->json([ + 'message' => 'این تنظیم از سوپرادمین کنترل می‌شود و در پنل خانگی قابل ویرایش نیست', + ], 403); } $previousValue = $setting->exists ? (string) ($setting->value ?? '') : null; @@ -94,7 +102,7 @@ class SettingsController extends Controller })->afterResponse(); } - if ($isNew && $isSystemDeploy) { + if ($isNew) { $projectName = Setting::where('key', 'name_fa')->value('value') ?: 'پروژه'; $settingTitle = trim($request->title_fa ?? '') ?: $request->key; $this->Send( @@ -135,6 +143,71 @@ class SettingsController extends Controller return in_array($key, self::PANEL_MANAGED_KEYS, true); } + private function mergeOverlayIntoLocal($rows) + { + $overlay = app(SuperAdminProgramSettingsOverlay::class)->items(); + if (! $overlay) { + return $rows; + } + + $byKey = []; + foreach ($rows as $row) { + $byKey[(string) $row->key] = $row; + } + + foreach ($overlay as $item) { + $key = trim((string) ($item['key'] ?? '')); + if ($key === '' || SuperAdminProgramSettingsOverlay::isLocalOwned($key)) { + continue; + } + if (isset($byKey[$key])) { + $byKey[$key]->value = $item['value'] ?? $byKey[$key]->value; + if (array_key_exists('title_fa', $item) && $item['title_fa'] !== null && $item['title_fa'] !== '') { + $byKey[$key]->title_fa = $item['title_fa']; + } + if (array_key_exists('description', $item) && $item['description'] !== null) { + $byKey[$key]->description = $item['description']; + } + if (array_key_exists('type', $item) && $item['type'] !== null && $item['type'] !== '') { + $byKey[$key]->type = $item['type']; + } + if (array_key_exists('is_active', $item)) { + $byKey[$key]->is_active = $item['is_active']; + } + } else { + $byKey[$key] = $this->overlayRowToSetting($item); + } + } + + return collect(array_values($byKey)); + } + + /** + * @param array> $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 + { + $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; + + return $setting; + } + /** * اپ کاربر (بدون auth ادمین) هرگز secret/credential نمی‌بیند. * پنل ادمین با auth:user همه کلیدها را می‌بیند. diff --git a/app/Services/ConfigService.php b/app/Services/ConfigService.php index 95e5c89..d226ea4 100644 --- a/app/Services/ConfigService.php +++ b/app/Services/ConfigService.php @@ -24,6 +24,11 @@ class ConfigService */ public function get(string $key, $default = null) { + $fromSa = $this->valueFromSuperAdmin($key, $default); + if ($fromSa !== '__sa_miss__') { + return $fromSa; + } + try { $row = Setting::where('key', $key)->first(); @@ -39,6 +44,40 @@ class ConfigService return $fromFile !== null ? $fromFile : $default; } + /** + * مقدار سوپرادمین اگر کلید در overlay باشد؛ وگرنه سنتنل miss. + * + * @return mixed + */ + private function valueFromSuperAdmin(string $key, $default) + { + try { + $row = app(SuperAdminProgramSettingsOverlay::class)->row($key); + } catch (\Throwable $e) { + return '__sa_miss__'; + } + + if ($row === null) { + return '__sa_miss__'; + } + + $active = $row['is_active'] ?? 1; + if ($active === 0 || $active === false || $active === '0') { + $fromFile = config("price_providers.{$key}"); + + return $fromFile !== null ? $fromFile : $default; + } + + $value = $row['value'] ?? null; + if ($value === null || $value === '') { + $fromFile = config("price_providers.{$key}"); + + return $fromFile !== null ? $fromFile : $default; + } + + return $value; + } + public function getString(string $key, string $default = ''): string { $value = $this->get($key, $default); diff --git a/app/Services/SuperAdminProgramSettingsClient.php b/app/Services/SuperAdminProgramSettingsClient.php new file mode 100644 index 0000000..eb66b34 --- /dev/null +++ b/app/Services/SuperAdminProgramSettingsClient.php @@ -0,0 +1,245 @@ +canAuth()) { + $items = $this->fetchRemoteFull(); + if ($items !== null) { + return $items; + } + } + + return $this->fetchPublic(); + } + + private function canAuth(): bool + { + return $this->sharedSecret() !== '' || $this->bearerToken() !== ''; + } + + /** + * @return array>|null + */ + private function fetchRemoteFull(): ?array + { + foreach ($this->attemptOrigins() as $origin) { + try { + $response = Http::acceptJson() + ->withHeaders(array_merge($this->authHeaders('GET', self::REMOTE_PATH), [ + 'Cache-Control' => 'no-store', + ])) + ->timeout(6) + ->get($origin.self::REMOTE_PATH); + + if (! $response->successful() || ! $this->isJsonResponse($response)) { + Log::warning('SuperAdmin program-settings remote failed', [ + 'origin' => $origin, + 'status' => $response->status(), + ]); + + continue; + } + + $items = $this->extractItems($response->json()); + if ($items !== null) { + return $items; + } + } catch (\Throwable $e) { + Log::warning('SuperAdmin program-settings remote exception', [ + 'origin' => $origin, + 'error' => $e->getMessage(), + ]); + } + } + + return null; + } + + /** + * @return array>|null + */ + private function fetchPublic(): ?array + { + $domain = $this->domain(); + if ($domain === '') { + return null; + } + + foreach ($this->attemptOrigins() as $origin) { + try { + $response = Http::acceptJson() + ->withHeaders(['Cache-Control' => 'no-store']) + ->timeout(6) + ->get($origin.self::PUBLIC_PATH, ['domain' => $domain]); + + if (! $response->successful() || ! $this->isJsonResponse($response)) { + continue; + } + + $items = $this->extractItems($response->json()); + if ($items !== null) { + return $items; + } + } catch (\Throwable $e) { + Log::warning('SuperAdmin public program-settings exception', [ + 'origin' => $origin, + 'error' => $e->getMessage(), + ]); + } + } + + return null; + } + + /** + * سه دامنه × سه دور (مستر، بعد اسلیوها). + * + * @return iterable + */ + private function attemptOrigins(): iterable + { + $origins = $this->origins(); + for ($round = 1; $round <= self::RETRY_ROUNDS; $round++) { + foreach ($origins as $origin) { + yield $origin; + } + } + } + + /** + * @return list + */ + private function origins(): array + { + $defaults = [ + 'https://adm.montzar.ir', + 'https://adm.montech.ir', + 'https://adm.montest.ir', + ]; + + $extra = []; + $explicit = trim((string) config('services.superadmin.origin', '')); + if ($explicit !== '') { + $extra[] = rtrim($explicit, '/'); + } + + $sms = trim((string) config('services.superadmin.sms_base_url', '')); + if ($sms !== '') { + $parsed = parse_url($sms); + if (! empty($parsed['scheme']) && ! empty($parsed['host'])) { + $port = isset($parsed['port']) ? ':'.$parsed['port'] : ''; + $extra[] = $parsed['scheme'].'://'.$parsed['host'].$port; + } + } + + return array_values(array_unique(array_merge($extra, $defaults))); + } + + private function isJsonResponse($response): bool + { + $contentType = strtolower((string) $response->header('Content-Type')); + + return str_contains($contentType, 'json'); + } + + /** + * @param mixed $data + * @return array>|null + */ + private function extractItems($data): ?array + { + if (! is_array($data)) { + return null; + } + if (array_is_list($data)) { + return $data; + } + foreach (['settings', 'items', 'data'] as $key) { + if (isset($data[$key]) && is_array($data[$key])) { + return $data[$key]; + } + } + + return null; + } + + /** + * @return array + */ + private function authHeaders(string $method, string $canonicalPath): array + { + $secret = $this->sharedSecret(); + if ($secret !== '') { + $domain = $this->domain(); + $timestamp = (string) time(); + $payload = $timestamp.'.'.strtoupper($method).'.'.$canonicalPath.'.'.$domain; + $signature = hash_hmac('sha256', $payload, $secret); + + return [ + 'X-Abshode-Domain' => $domain, + 'X-Abshode-Timestamp' => $timestamp, + 'X-Abshode-Signature' => $signature, + ]; + } + + $token = $this->bearerToken(); + if ($token !== '') { + return [ + 'Authorization' => 'Bearer '.$token, + ]; + } + + return []; + } + + private function sharedSecret(): string + { + return trim((string) config('services.superadmin.sms_shared_secret', '')); + } + + private function bearerToken(): string + { + return trim((string) config('services.superadmin.remote_token', '')); + } + + private function domain(): string + { + $override = trim((string) config('services.superadmin.sms_domain', '')); + if ($override !== '') { + return $this->normalizeHost($override); + } + + $fromApp = parse_url((string) config('app.url'), PHP_URL_HOST); + + return $this->normalizeHost(is_string($fromApp) ? $fromApp : ''); + } + + private function normalizeHost(string $value): string + { + $host = strtolower(trim($value)); + $host = preg_replace('#^https?://#', '', $host) ?? $host; + $host = explode('/', $host)[0] ?? $host; + $host = explode(':', $host)[0] ?? $host; + + return $host; + } +} diff --git a/app/Services/SuperAdminProgramSettingsOverlay.php b/app/Services/SuperAdminProgramSettingsOverlay.php new file mode 100644 index 0000000..375b8c0 --- /dev/null +++ b/app/Services/SuperAdminProgramSettingsOverlay.php @@ -0,0 +1,107 @@ +>|null */ + private static ?array $memo = null; + + private static bool $loaded = false; + + /** + * @return array>|null null = سوپرادمین در دسترس نبود + */ + public function map(): ?array + { + if (self::$loaded) { + return self::$memo; + } + + self::$loaded = true; + + $items = app(SuperAdminProgramSettingsClient::class)->fetchItems(); + if ($items === null) { + self::$memo = null; + + return null; + } + + $map = []; + foreach ($items as $row) { + if (! is_array($row)) { + continue; + } + $key = trim((string) ($row['key'] ?? '')); + if ($key === '' || in_array($key, self::LOCAL_OWNED_KEYS, true) || in_array($key, self::RUNTIME_KEYS, true)) { + continue; + } + $map[$key] = $row; + } + + self::$memo = $map; + + return $map; + } + + /** + * @return array> + */ + public function items(): array + { + $map = $this->map(); + if ($map === null || $map === []) { + return []; + } + + return array_values($map); + } + + /** + * @return array|null + */ + public function row(string $key): ?array + { + $map = $this->map(); + if ($map === null) { + return null; + } + $key = trim($key); + if ($key === '' || ! array_key_exists($key, $map)) { + return null; + } + + return $map[$key]; + } + + public static function isLocalOwned(string $key): bool + { + return in_array($key, self::LOCAL_OWNED_KEYS, true); + } +} diff --git a/config/services.php b/config/services.php index 7da9b96..573eb8d 100644 --- a/config/services.php +++ b/config/services.php @@ -50,6 +50,8 @@ return [ 'sms_shared_secret' => env('SUPERADMIN_SMS_SHARED_SECRET'), 'sms_domain' => env('SUPERADMIN_SMS_DOMAIN'), 'remote_token' => env('SUPERADMIN_REMOTE_TOKEN'), + /** Origin سوپرادمین برای /api/public/program-settings و /api/remote/program-settings */ + 'origin' => env('SUPERADMIN_ORIGIN'), ], ];