diff --git a/app/Services/ConfigService.php b/app/Services/ConfigService.php index 5277437..4530186 100644 --- a/app/Services/ConfigService.php +++ b/app/Services/ConfigService.php @@ -803,7 +803,8 @@ class ConfigService /** * Hostname for WebOTP SMS (@domain #code) and other public links. - * Only from the current HTTP request — no settings, no .env. + * Prefer settings.app_url (دامنهٔ اپ کاربر) over the API request host — + * API often lives on a different subdomain (e.g. *.gholaktala.ir). */ public function resolvePublicHost(?\Illuminate\Http\Request $request = null): string { @@ -816,9 +817,14 @@ class ConfigService return ''; } - /** Full user-facing site URL, e.g. https://example.ir */ + /** Full user-facing site URL, e.g. https://example.ir — prefers settings.app_url */ public function resolvePublicSiteUrl(?\Illuminate\Http\Request $request = null): string { + $fromAppUrl = $this->normalizedAppUrl(); + if ($fromAppUrl !== '') { + return $fromAppUrl; + } + if ($request !== null) { $origin = rtrim(trim((string) $request->getSchemeAndHttpHost()), '/'); $host = $this->parseHostFromUrl($origin); @@ -854,6 +860,11 @@ class ConfigService { $candidates = []; + $appUrlHost = $this->parseHostFromUrl((string) $this->get('app_url', '')); + if ($appUrlHost !== '') { + $candidates[] = $appUrlHost; + } + if ($request !== null) { $candidates[] = trim((string) $request->getHost()); } @@ -865,6 +876,33 @@ class ConfigService return $candidates; } + /** settings.app_url as https://host… without trailing slash; empty if unusable */ + private function normalizedAppUrl(): string + { + $raw = trim((string) $this->get('app_url', '')); + if ($raw === '') { + return ''; + } + + if (! preg_match('#^https?://#i', $raw)) { + $raw = 'https://' . ltrim($raw, '/'); + } + + $host = $this->parseHostFromUrl($raw); + if (! $this->isUsableHost($host)) { + return ''; + } + + $scheme = parse_url($raw, PHP_URL_SCHEME) ?: 'https'; + $port = parse_url($raw, PHP_URL_PORT); + $origin = strtolower((string) $scheme) . '://' . $this->normalizeHost($host); + if ($port !== null && ! in_array((int) $port, [80, 443], true)) { + $origin .= ':' . $port; + } + + return $origin; + } + private function isUsableHost(string $host): bool { $host = strtolower(trim($host)); diff --git a/tests/Unit/ConfigServiceOtpHostTest.php b/tests/Unit/ConfigServiceOtpHostTest.php index fad741a..5dfb004 100644 --- a/tests/Unit/ConfigServiceOtpHostTest.php +++ b/tests/Unit/ConfigServiceOtpHostTest.php @@ -4,16 +4,38 @@ namespace Tests\Unit; use App\Services\ConfigService; use Illuminate\Http\Request; +use Mockery; use Tests\TestCase; class ConfigServiceOtpHostTest extends TestCase { + protected function tearDown(): void + { + Mockery::close(); + parent::tearDown(); + } + + /** @param mixed $appUrl */ + private function cfgWithAppUrl($appUrl): ConfigService + { + $cfg = Mockery::mock(ConfigService::class)->makePartial(); + $cfg->shouldReceive('get')->andReturnUsing(function (string $key, $default = null) use ($appUrl) { + if ($key === 'app_url') { + return $appUrl !== null && $appUrl !== '' ? $appUrl : $default; + } + + return $default; + }); + + return $cfg; + } + public function test_uses_request_host_without_settings(): void { $request = Request::create('https://boursemes.com/server/auth/login', 'POST'); $this->app->instance('request', $request); - $host = app(ConfigService::class)->resolvePublicHost($request); + $host = $this->cfgWithAppUrl(null)->resolvePublicHost($request); $this->assertSame('boursemes.com', $host); } @@ -22,14 +44,35 @@ class ConfigServiceOtpHostTest extends TestCase { $request = Request::create('https://boursemes.com/server/auth/login', 'POST'); - $url = app(ConfigService::class)->resolvePublicSiteUrl($request); + $url = $this->cfgWithAppUrl(null)->resolvePublicSiteUrl($request); $this->assertSame('https://boursemes.com', $url); } + public function test_prefers_app_url_over_api_request_host(): void + { + $request = Request::create('https://goldarvin.gholaktala.ir/server/customer/order', 'POST'); + $this->app->instance('request', $request); + + $cfg = $this->cfgWithAppUrl('https://my.goldarvin.ir'); + + $this->assertSame('my.goldarvin.ir', $cfg->resolvePublicHost($request)); + $this->assertSame('https://my.goldarvin.ir', $cfg->resolvePublicSiteUrl($request)); + } + + public function test_app_url_without_scheme_gets_https(): void + { + $request = Request::create('https://goldarvin.gholaktala.ir/server/auth/login', 'POST'); + + $this->assertSame( + 'https://my.goldarvin.ir', + $this->cfgWithAppUrl('my.goldarvin.ir')->resolvePublicSiteUrl($request) + ); + } + public function test_returns_empty_without_request(): void { - $host = app(ConfigService::class)->resolvePublicHost(); + $host = $this->cfgWithAppUrl(null)->resolvePublicHost(); $this->assertSame('', $host); } @@ -38,7 +81,7 @@ class ConfigServiceOtpHostTest extends TestCase { $request = Request::create('https://www.boursemes.com/server/auth/login', 'POST'); - $host = app(ConfigService::class)->resolvePublicHost($request); + $host = $this->cfgWithAppUrl(null)->resolvePublicHost($request); $this->assertSame('boursemes.com', $host); } @@ -48,7 +91,7 @@ class ConfigServiceOtpHostTest extends TestCase $request = Request::create('http://localhost/server/auth/login', 'POST'); $this->app->instance('request', $request); - $host = app(ConfigService::class)->resolvePublicHost($request); + $host = $this->cfgWithAppUrl(null)->resolvePublicHost($request); $this->assertSame('', $host); }