Files
boursecu_server/app/Services/SuperAdminProgramSettingsClient.php
T

291 lines
8.6 KiB
PHP
Raw Normal View History

<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
* خواندن تنظیمات پیشرفته از سوپرادمین (منبع حقیقت can_edit=0).
*
* Public اول (دامنه‌محور، بدون وابستگی به HMAC): GET {origin}/api/public/program-settings?domain=
* Full (با HMAC/Bearer): GET {origin}/api/remote/program-settings
*
* Public برای همین ۱۴ کلید مالکیت سوپرادمین کافی است؛ اگر remote اول باشد و replica عقب بماند،
* سوییچ‌هایی مثل enable_automation روی false کهنه می‌مانند.
*/
class SuperAdminProgramSettingsClient
{
public const REMOTE_PATH = '/api/remote/program-settings';
public const PUBLIC_PATH = '/api/public/program-settings';
public const RETRY_ROUNDS = 3;
public function fetchItems(): ?array
{
$public = $this->fetchPublic();
if ($public !== null) {
return $public;
}
if ($this->canAuth()) {
$items = $this->fetchRemoteFull();
if ($items !== null) {
return $items;
}
}
return null;
}
private function canAuth(): bool
{
return $this->sharedSecret() !== '' || $this->bearerToken() !== '';
}
/**
* @return array<int, array<string, mixed>>|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<int, array<string, mixed>>|null
*/
private function fetchPublic(): ?array
{
$domains = $this->domainCandidates();
if ($domains === []) {
return null;
}
foreach ($domains as $domain) {
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,
'domain' => $domain,
'error' => $e->getMessage(),
]);
}
}
}
return null;
}
/**
* سه دامنه × سه دور (مستر، بعد اسلیوها).
*
* @return iterable<int, string>
*/
private function attemptOrigins(): iterable
{
$origins = $this->origins();
for ($round = 1; $round <= self::RETRY_ROUNDS; $round++) {
foreach ($origins as $origin) {
yield $origin;
}
}
}
/**
* @return list<string>
*/
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<int, array<string, mixed>>|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<string, string>
*/
private function authHeaders(string $method, string $canonicalPath): array
{
$secret = $this->sharedSecret();
if ($secret !== '') {
$domain = $this->domainCandidates()[0] ?? '';
$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', ''));
}
/**
* دامنه‌های کاندید برای resolve نسخه در سوپرادمین.
*
* @return list<string>
*/
private function domainCandidates(): array
{
$candidates = [];
$override = trim((string) config('services.superadmin.sms_domain', ''));
if ($override !== '') {
$candidates[] = $this->normalizeHost($override);
}
$fromApp = parse_url((string) config('app.url'), PHP_URL_HOST);
if (is_string($fromApp) && $fromApp !== '') {
$candidates[] = $this->normalizeHost($fromApp);
}
try {
$request = request();
if ($request) {
$candidates[] = $this->normalizeHost((string) $request->getHost());
$forwarded = (string) $request->headers->get('X-Forwarded-Host', '');
if ($forwarded !== '') {
$candidates[] = $this->normalizeHost(explode(',', $forwarded)[0] ?? '');
}
}
} catch (\Throwable) {
// CLI / بدون request
}
$out = [];
foreach ($candidates as $host) {
if ($host === '' || in_array($host, $out, true)) {
continue;
}
if (in_array($host, ['localhost', '127.0.0.1', '::1'], true)) {
continue;
}
$out[] = $host;
}
return $out;
}
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;
}
}