From f42ae09d1474ecec39c1c35525c80ebc36a8e409 Mon Sep 17 00:00:00 2001 From: adonis Date: Wed, 5 Aug 2026 12:56:52 +0330 Subject: [PATCH] fix support issue --- .../Chat/SupportChatNotifyController.php | 38 +++++++- app/Models/Setting.php | 2 + app/Services/ChatWebhookService.php | 86 +++++++++++++++++-- database/sql/deploy_settings_const.sql | 9 +- ...ngs_const_preserve_value_sync_can_edit.sql | 7 +- 5 files changed, 131 insertions(+), 11 deletions(-) diff --git a/app/Http/Controllers/Chat/SupportChatNotifyController.php b/app/Http/Controllers/Chat/SupportChatNotifyController.php index e253512..d3f981f 100644 --- a/app/Http/Controllers/Chat/SupportChatNotifyController.php +++ b/app/Http/Controllers/Chat/SupportChatNotifyController.php @@ -21,13 +21,21 @@ class SupportChatNotifyController extends Controller public function notify(Request $request) { $secret = (string) self::Config('chat_webhook_secret'); - $incoming = (string) $request->header('X-Chat-Webhook-Secret', ''); + $secret = app(ChatWebhookService::class)->sanitizeSecret($secret); + $incoming = app(ChatWebhookService::class)->sanitizeSecret( + (string) $request->header('X-Chat-Webhook-Secret', '') + ); if ($secret === '' || ! hash_equals($secret, $incoming)) { + Log::warning('support chat notify unauthorized', [ + 'has_secret' => $secret !== '', + 'has_header' => $incoming !== '', + ]); return response()->json(['message' => 'unauthorized'], 401); } if (! self::isSmsNotifEnabled('sms_notif_support_admin')) { + Log::info('support chat notify skipped: sms_notif_support_admin disabled'); return response()->json(['ok' => true, 'skipped' => 'disabled']); } @@ -37,6 +45,7 @@ class SupportChatNotifyController extends Controller } if (Cache::has(self::FOCUS_CACHE_PREFIX.$roomId)) { + Log::info('support chat notify skipped: admin focused', ['room_id' => $roomId]); return response()->json(['ok' => true, 'skipped' => 'admin_focused']); } @@ -52,14 +61,33 @@ class SupportChatNotifyController extends Controller $name = 'مشتری'; } - $message = 'شما از '.$name.' پیام دارید'; + $companyName = trim((string) self::Config('name_fa')); + if ($companyName === '') { + $companyName = trim((string) self::Config('name')); + } + if ($companyName === '') { + $companyName = 'پشتیبانی'; + } + + $message = 'شما از '.$name." پیام دارید\n".$companyName; + + $admins = User::where('send_sms', 1)->get(); + if ($admins->isEmpty()) { + Log::warning('support chat notify: no admins with send_sms=1', ['room_id' => $roomId]); + } try { - User::where('send_sms', 1)->get()->each(function ($admin) use ($message) { + $admins->each(function ($admin) use ($message) { if (! empty($admin->mobile)) { $this->send($admin->mobile, $message); } }); + Log::info('support chat notify SMS sent', [ + 'room_id' => $roomId, + 'name' => $name, + 'company' => $companyName, + 'admin_count' => $admins->count(), + ]); } catch (\Throwable $e) { Log::warning('support chat SMS notify failed', [ 'room_id' => $roomId, @@ -109,10 +137,12 @@ class SupportChatNotifyController extends Controller */ public function ensureWebhook() { - $ok = app(ChatWebhookService::class)->ensureRegistered(); + $service = app(ChatWebhookService::class); + $ok = $service->ensureRegistered(); return response()->json([ 'ok' => $ok, + 'webhook_url' => $service->webhookUrl(), 'message' => $ok ? 'webhook registered' : 'webhook register skipped or failed', ], $ok ? 200 : 422); } diff --git a/app/Models/Setting.php b/app/Models/Setting.php index cf15038..0f16568 100644 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -80,6 +80,8 @@ class Setting extends Model 'pargar_username', 'zarin_api', 'zarin_token', + 'chat_webhook_secret', + 'chat_webhook_base_url', ]; if (in_array($key, $exact, true)) { return true; diff --git a/app/Services/ChatWebhookService.php b/app/Services/ChatWebhookService.php index 6e6799d..e1d1e17 100644 --- a/app/Services/ChatWebhookService.php +++ b/app/Services/ChatWebhookService.php @@ -25,7 +25,15 @@ class ChatWebhookService return false; } - $webhookUrl = rtrim((string) config('app.url'), '/').'/api/chat/support-message-notify'; + $webhookUrl = $this->webhookUrl(); + if ($this->isUnreachableWebhookUrl($webhookUrl)) { + Log::warning('chat webhook register refused: unreachable/local URL', [ + 'webhook_url' => $webhookUrl, + 'hint' => 'Set chat_webhook_base_url to public Laravel URL (e.g. https://niazitala.ir/server) or fix APP_URL', + ]); + + return false; + } try { $response = Http::timeout(8)->post('https://'.$host.'/api/projects/set-webhook', [ @@ -34,10 +42,27 @@ class ChatWebhookService 'webhook_secret' => $secret, ]); - return $response->successful(); + if (! $response->successful()) { + Log::warning('chat webhook register non-2xx', [ + 'host' => $host, + 'webhook_url' => $webhookUrl, + 'status' => $response->status(), + 'body' => mb_substr($response->body(), 0, 300), + ]); + + return false; + } + + Log::info('chat webhook registered', [ + 'host' => $host, + 'webhook_url' => $webhookUrl, + ]); + + return true; } catch (\Throwable $e) { Log::warning('chat webhook register failed', [ 'host' => $host, + 'webhook_url' => $webhookUrl, 'error' => $e->getMessage(), ]); @@ -45,9 +70,39 @@ class ChatWebhookService } } + /** + * آدرس عمومی وب‌هوک — باید به لاراول بخورد نه فرانت SPA. + * اولویت: chat_webhook_base_url (مثلاً https://niazitala.ir/server) سپس APP_URL. + */ + public function webhookUrl(): string + { + $base = trim((string) Controller::Config('chat_webhook_base_url')); + if ($base === '' || $base === '0' || strtolower($base) === 'false') { + $base = rtrim((string) config('app.url'), '/'); + } else { + $base = rtrim($base, '/'); + } + + return $base.'/api/chat/support-message-notify'; + } + + public function isUnreachableWebhookUrl(string $url): bool + { + $host = parse_url($url, PHP_URL_HOST); + if (! is_string($host) || $host === '') { + return true; + } + + $host = strtolower($host); + + return in_array($host, ['localhost', '127.0.0.1', '::1', '0.0.0.0'], true) + || str_ends_with($host, '.local') + || str_ends_with($host, '.internal'); + } + public function ensureSecret(): string { - $existing = trim((string) Controller::Config('chat_webhook_secret')); + $existing = $this->sanitizeSecret((string) Controller::Config('chat_webhook_secret')); if ($existing !== '') { return $existing; } @@ -56,11 +111,18 @@ class ChatWebhookService $row = Setting::where('key', 'chat_webhook_secret')->first(); if ($row) { - if (trim((string) $row->value) === '') { + $current = $this->sanitizeSecret((string) $row->value); + if ($current === '') { $row->value = $secret; $row->save(); } else { - return (string) $row->value; + // مقدار ذخیره‌شده کاراکتر نامعتبر هدر داشت — با نسخهٔ تمیز جایگزین کن + if ($current !== trim((string) $row->value)) { + $row->value = $current; + $row->save(); + } + + return $current; } } else { Setting::create([ @@ -76,4 +138,18 @@ class ChatWebhookService return $secret; } + + /** فقط ASCII قابل‌استفاده در هدر HTTP (بدون فاصله/خط جدید/یونیکد). */ + public function sanitizeSecret(string $secret): string + { + $secret = trim($secret); + if ($secret === '') { + return ''; + } + + // حذف کاراکترهای غیرقابل‌قبول برای header value + $clean = preg_replace('/[^\x21-\x7E]/', '', $secret) ?? ''; + + return trim($clean); + } } diff --git a/database/sql/deploy_settings_const.sql b/database/sql/deploy_settings_const.sql index 6b08693..3088c02 100644 --- a/database/sql/deploy_settings_const.sql +++ b/database/sql/deploy_settings_const.sql @@ -499,6 +499,11 @@ SELECT 'chat_webhook_secret', '', 'رمز وب‌هوک چت', 0, 1, 'رمز مشترک بین لاراول و سرور چت برای اعلان پیام پشتیبانی. فقط سرور؛ در صورت خالی بودن به‌صورت خودکار ساخته می‌شود.', 'text' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'chat_webhook_secret' LIMIT 1); +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'chat_webhook_base_url', '', 'آدرس پایه وب‌هوک چت', 0, 1, + 'اگر APP_URL فرانت است (مثلاً https://niazitala.ir) اینجا آدرس لاراول را بگذار (مثلاً https://niazitala.ir/server). خالی = همان APP_URL.', 'text' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'chat_webhook_base_url' LIMIT 1); + INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`) SELECT 'gift_for_referrer', '0.005', 'پاداش معرف', 1, 1, NULL WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'gift_for_referrer' LIMIT 1); @@ -1589,6 +1594,8 @@ UPDATE `settings` SET `description` = 'روشن: وقتی هشدار قیمت م UPDATE `settings` SET `description` = 'روشن: وقتی اعتبار کیف پول پنل پیامک صفر یا منفی شود، هشدار به سوپرادمین و ادمین‌های دارای «ارسال پیامک» می‌رود. خاموش: این هشدار نمی‌رود.' WHERE `key` = 'sms_notif_wallet_alert'; UPDATE `settings` SET `description` = 'روشن: وقتی مشتری اولین پیام خوانده‌نشده را در چت پشتیبانی می‌فرستد و ادمین همان اتاق را باز و visible ندارد، پیامک به ادمین‌های دارای «ارسال پیامک» می‌رود. خاموش: اعلان پیامک چت نمی‌رود.' WHERE `key` = 'sms_notif_support_admin'; UPDATE `settings` SET `description` = 'رمز مشترک بین لاراول و سرور چت برای اعلان پیام پشتیبانی. فقط سرور؛ در صورت خالی بودن به‌صورت خودکار ساخته می‌شود.' WHERE `key` = 'chat_webhook_secret'; +UPDATE `settings` SET `description` = 'اگر APP_URL فرانت است (مثلاً https://niazitala.ir) اینجا آدرس لاراول را بگذار (مثلاً https://niazitala.ir/server). خالی = همان APP_URL.' WHERE `key` = 'chat_webhook_base_url'; +UPDATE `settings` SET `title_fa` = 'آدرس پایه وب‌هوک چت' WHERE `key` = 'chat_webhook_base_url'; UPDATE `settings` SET `description` = 'روشن: در لیست مشتریان دکمه ارسال پیامک تکی نمایش داده می‌شود و ادمین می‌تواند برای هر مشتری متن دلخواه بفرستد. خاموش: دکمه مخفی است. فقط از صفحه دیپلوی تغییر می‌کند.' WHERE `key` = 'customer_direct_sms'; UPDATE `settings` SET `title_fa` = 'پیامک تکی به مشتری از لیست' WHERE `key` = 'customer_direct_sms'; UPDATE `settings` SET `description` = 'روشن: سرویس احراز هویت جیبیت (تطبیق کد ملی/موبایل و کارت بانکی) فعال است. خاموش: هیچ فراخوانی به جیبیت انجام نمی‌شود و احراز هویت خودکار کار نمی‌کند. فقط از صفحه دیپلوی قابل تغییر است.' WHERE `key` = 'jibit_enabled'; @@ -1883,7 +1890,7 @@ UPDATE `settings` SET `can_edit` = 0 WHERE `key` IN ( 'up_price', 'down_price', 'wss_enabled', 'zaarbox_api_url', - 'show_to_users_chat', 'chat_api', 'chat_api_key', 'chat_upload_url', 'chat_download_url', 'chat_webhook_secret' + 'show_to_users_chat', 'chat_api', 'chat_api_key', 'chat_upload_url', 'chat_download_url', 'chat_webhook_secret', 'chat_webhook_base_url' ); -- قابل ویرایش در پنل (can_edit = 1) diff --git a/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql b/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql index 56d43d5..3e45fd4 100644 --- a/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql +++ b/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql @@ -454,6 +454,11 @@ SELECT 'chat_webhook_secret', '', 'رمز وب‌هوک چت', 0, 1, 'رمز مشترک بین لاراول و سرور چت برای اعلان پیام پشتیبانی. فقط سرور؛ در صورت خالی بودن به‌صورت خودکار ساخته می‌شود.', 'text' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'chat_webhook_secret' LIMIT 1); +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'chat_webhook_base_url', '', 'آدرس پایه وب‌هوک چت', 0, 1, + 'اگر APP_URL فرانت است (مثلاً https://niazitala.ir) اینجا آدرس لاراول را بگذار (مثلاً https://niazitala.ir/server). خالی = همان APP_URL.', 'text' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'chat_webhook_base_url' LIMIT 1); + INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`) SELECT 'gift_for_referrer', '0.005', 'پاداش معرف', 1, 1, NULL WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'gift_for_referrer' LIMIT 1); @@ -1467,7 +1472,7 @@ UPDATE `settings` SET `can_edit` = 0 WHERE `key` IN ( 'zaarbox_api_url' ); -UPDATE `settings` SET `can_edit` = 0 WHERE `key` = 'chat_webhook_secret'; +UPDATE `settings` SET `can_edit` = 0 WHERE `key` IN ('chat_webhook_secret', 'chat_webhook_base_url'); UPDATE `settings` SET `can_edit` = 1 WHERE `key` IN ( 'name_fa',