fix support issue

This commit is contained in:
2026-08-05 12:56:52 +03:30
parent bfb8b91133
commit f42ae09d14
5 changed files with 131 additions and 11 deletions
@@ -21,13 +21,21 @@ class SupportChatNotifyController extends Controller
public function notify(Request $request) public function notify(Request $request)
{ {
$secret = (string) self::Config('chat_webhook_secret'); $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)) { if ($secret === '' || ! hash_equals($secret, $incoming)) {
Log::warning('support chat notify unauthorized', [
'has_secret' => $secret !== '',
'has_header' => $incoming !== '',
]);
return response()->json(['message' => 'unauthorized'], 401); return response()->json(['message' => 'unauthorized'], 401);
} }
if (! self::isSmsNotifEnabled('sms_notif_support_admin')) { 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']); return response()->json(['ok' => true, 'skipped' => 'disabled']);
} }
@@ -37,6 +45,7 @@ class SupportChatNotifyController extends Controller
} }
if (Cache::has(self::FOCUS_CACHE_PREFIX.$roomId)) { 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']); return response()->json(['ok' => true, 'skipped' => 'admin_focused']);
} }
@@ -52,14 +61,33 @@ class SupportChatNotifyController extends Controller
$name = 'مشتری'; $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 { try {
User::where('send_sms', 1)->get()->each(function ($admin) use ($message) { $admins->each(function ($admin) use ($message) {
if (! empty($admin->mobile)) { if (! empty($admin->mobile)) {
$this->send($admin->mobile, $message); $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) { } catch (\Throwable $e) {
Log::warning('support chat SMS notify failed', [ Log::warning('support chat SMS notify failed', [
'room_id' => $roomId, 'room_id' => $roomId,
@@ -109,10 +137,12 @@ class SupportChatNotifyController extends Controller
*/ */
public function ensureWebhook() public function ensureWebhook()
{ {
$ok = app(ChatWebhookService::class)->ensureRegistered(); $service = app(ChatWebhookService::class);
$ok = $service->ensureRegistered();
return response()->json([ return response()->json([
'ok' => $ok, 'ok' => $ok,
'webhook_url' => $service->webhookUrl(),
'message' => $ok ? 'webhook registered' : 'webhook register skipped or failed', 'message' => $ok ? 'webhook registered' : 'webhook register skipped or failed',
], $ok ? 200 : 422); ], $ok ? 200 : 422);
} }
+2
View File
@@ -80,6 +80,8 @@ class Setting extends Model
'pargar_username', 'pargar_username',
'zarin_api', 'zarin_api',
'zarin_token', 'zarin_token',
'chat_webhook_secret',
'chat_webhook_base_url',
]; ];
if (in_array($key, $exact, true)) { if (in_array($key, $exact, true)) {
return true; return true;
+81 -5
View File
@@ -25,7 +25,15 @@ class ChatWebhookService
return false; 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 { try {
$response = Http::timeout(8)->post('https://'.$host.'/api/projects/set-webhook', [ $response = Http::timeout(8)->post('https://'.$host.'/api/projects/set-webhook', [
@@ -34,10 +42,27 @@ class ChatWebhookService
'webhook_secret' => $secret, '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) { } catch (\Throwable $e) {
Log::warning('chat webhook register failed', [ Log::warning('chat webhook register failed', [
'host' => $host, 'host' => $host,
'webhook_url' => $webhookUrl,
'error' => $e->getMessage(), '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 public function ensureSecret(): string
{ {
$existing = trim((string) Controller::Config('chat_webhook_secret')); $existing = $this->sanitizeSecret((string) Controller::Config('chat_webhook_secret'));
if ($existing !== '') { if ($existing !== '') {
return $existing; return $existing;
} }
@@ -56,11 +111,18 @@ class ChatWebhookService
$row = Setting::where('key', 'chat_webhook_secret')->first(); $row = Setting::where('key', 'chat_webhook_secret')->first();
if ($row) { if ($row) {
if (trim((string) $row->value) === '') { $current = $this->sanitizeSecret((string) $row->value);
if ($current === '') {
$row->value = $secret; $row->value = $secret;
$row->save(); $row->save();
} else { } else {
return (string) $row->value; // مقدار ذخیره‌شده کاراکتر نامعتبر هدر داشت — با نسخهٔ تمیز جایگزین کن
if ($current !== trim((string) $row->value)) {
$row->value = $current;
$row->save();
}
return $current;
} }
} else { } else {
Setting::create([ Setting::create([
@@ -76,4 +138,18 @@ class ChatWebhookService
return $secret; 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);
}
} }
+8 -1
View File
@@ -499,6 +499,11 @@ SELECT 'chat_webhook_secret', '', 'رمز وب‌هوک چت', 0, 1,
'رمز مشترک بین لاراول و سرور چت برای اعلان پیام پشتیبانی. فقط سرور؛ در صورت خالی بودن به‌صورت خودکار ساخته می‌شود.', 'text' 'رمز مشترک بین لاراول و سرور چت برای اعلان پیام پشتیبانی. فقط سرور؛ در صورت خالی بودن به‌صورت خودکار ساخته می‌شود.', 'text'
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'chat_webhook_secret' LIMIT 1); 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`) INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`)
SELECT 'gift_for_referrer', '0.005', 'پاداش معرف', 1, 1, NULL SELECT 'gift_for_referrer', '0.005', 'پاداش معرف', 1, 1, NULL
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'gift_for_referrer' LIMIT 1); 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` = 'روشن: وقتی اعتبار کیف پول پنل پیامک صفر یا منفی شود، هشدار به سوپرادمین و ادمین‌های دارای «ارسال پیامک» می‌رود. خاموش: این هشدار نمی‌رود.' WHERE `key` = 'sms_notif_wallet_alert';
UPDATE `settings` SET `description` = 'روشن: وقتی مشتری اولین پیام خوانده‌نشده را در چت پشتیبانی می‌فرستد و ادمین همان اتاق را باز و visible ندارد، پیامک به ادمین‌های دارای «ارسال پیامک» می‌رود. خاموش: اعلان پیامک چت نمی‌رود.' WHERE `key` = 'sms_notif_support_admin'; UPDATE `settings` SET `description` = 'روشن: وقتی مشتری اولین پیام خوانده‌نشده را در چت پشتیبانی می‌فرستد و ادمین همان اتاق را باز و visible ندارد، پیامک به ادمین‌های دارای «ارسال پیامک» می‌رود. خاموش: اعلان پیامک چت نمی‌رود.' WHERE `key` = 'sms_notif_support_admin';
UPDATE `settings` SET `description` = 'رمز مشترک بین لاراول و سرور چت برای اعلان پیام پشتیبانی. فقط سرور؛ در صورت خالی بودن به‌صورت خودکار ساخته می‌شود.' WHERE `key` = 'chat_webhook_secret'; 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 `description` = 'روشن: در لیست مشتریان دکمه ارسال پیامک تکی نمایش داده می‌شود و ادمین می‌تواند برای هر مشتری متن دلخواه بفرستد. خاموش: دکمه مخفی است. فقط از صفحه دیپلوی تغییر می‌کند.' WHERE `key` = 'customer_direct_sms';
UPDATE `settings` SET `title_fa` = 'پیامک تکی به مشتری از لیست' WHERE `key` = 'customer_direct_sms'; UPDATE `settings` SET `title_fa` = 'پیامک تکی به مشتری از لیست' WHERE `key` = 'customer_direct_sms';
UPDATE `settings` SET `description` = 'روشن: سرویس احراز هویت جیبیت (تطبیق کد ملی/موبایل و کارت بانکی) فعال است. خاموش: هیچ فراخوانی به جیبیت انجام نمی‌شود و احراز هویت خودکار کار نمی‌کند. فقط از صفحه دیپلوی قابل تغییر است.' WHERE `key` = 'jibit_enabled'; 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', 'up_price', 'down_price',
'wss_enabled', 'wss_enabled',
'zaarbox_api_url', '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) -- قابل ویرایش در پنل (can_edit = 1)
@@ -454,6 +454,11 @@ SELECT 'chat_webhook_secret', '', 'رمز وب‌هوک چت', 0, 1,
'رمز مشترک بین لاراول و سرور چت برای اعلان پیام پشتیبانی. فقط سرور؛ در صورت خالی بودن به‌صورت خودکار ساخته می‌شود.', 'text' 'رمز مشترک بین لاراول و سرور چت برای اعلان پیام پشتیبانی. فقط سرور؛ در صورت خالی بودن به‌صورت خودکار ساخته می‌شود.', 'text'
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'chat_webhook_secret' LIMIT 1); 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`) INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`)
SELECT 'gift_for_referrer', '0.005', 'پاداش معرف', 1, 1, NULL SELECT 'gift_for_referrer', '0.005', 'پاداش معرف', 1, 1, NULL
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'gift_for_referrer' LIMIT 1); 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' '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 ( UPDATE `settings` SET `can_edit` = 1 WHERE `key` IN (
'name_fa', 'name_fa',