diff --git a/app/Console/Commands/DeploySettingsConstCommand.php b/app/Console/Commands/DeploySettingsConstCommand.php index fe9e84a..774fb3f 100644 --- a/app/Console/Commands/DeploySettingsConstCommand.php +++ b/app/Console/Commands/DeploySettingsConstCommand.php @@ -65,6 +65,14 @@ class DeploySettingsConstCommand extends Command return self::FAILURE; } + $extraPath = database_path('sql/settings_menu_and_show_to_user.sql'); + if (is_file($extraPath)) { + $extraSql = file_get_contents($extraPath); + if ($extraSql !== false && trim($extraSql) !== '') { + $sql .= "\n".$extraSql; + } + } + foreach ($databases as $db) { $db = trim((string) $db); if ($db === '') { diff --git a/app/Http/Controllers/Admin/SettingsController.php b/app/Http/Controllers/Admin/SettingsController.php index 3e90edd..2f20ba1 100644 --- a/app/Http/Controllers/Admin/SettingsController.php +++ b/app/Http/Controllers/Admin/SettingsController.php @@ -31,22 +31,37 @@ class SettingsController extends Controller if ($request->boolean('system_only')) { $local = $query->get(); - return response()->json($this->authorizeSettingsPayload( - $this->composeSystemSettings($local) - )); + return response()->json($this->composeSystemSettings($local)); } if ($request->boolean('editable_only')) { $local = $query->get(); - return response()->json($this->authorizeSettingsPayload( - $this->composeEditableSettings($local) - )); + return response()->json($this->composeEditableSettings($local)); } $local = $query->get(); - return response()->json($this->authorizeSettingsPayload($this->composeGetSettings($local))); + return response()->json($this->composeGetSettings($local)); + } + + /** + * اپ کاربر (khanegi_user / theme / ghatreh) — فقط ردیف‌های show_to_user=1، بدون credential. + * پنل ادمین از admin/get_settings استفاده می‌کند. + */ + public function publicIndex() + { + if (! Setting::hasShowToUserColumn()) { + return response()->json([]); + } + + $local = Setting::query() + ->select(Setting::adminSelectColumns()) + ->where('show_to_user', 1) + ->orderBy('key') + ->get(); + + return response()->json(Setting::forClientApi($this->composeGetSettings($local))); } /** @@ -60,7 +75,7 @@ class SettingsController extends Controller ->orderBy('key') ->get(); - return response()->json($this->authorizeSettingsPayload($rows)); + return response()->json($rows); } public function edit(Request $request) @@ -174,6 +189,10 @@ class SettingsController extends Controller $saMap = $overlay->catalogMap(); $byKey = []; + $localByKey = []; + foreach ($localRows as $row) { + $localByKey[(string) $row->key] = $row; + } if ($saMap !== null) { foreach ($saMap as $key => $item) { @@ -188,7 +207,7 @@ class SettingsController extends Controller } $value = $item['value'] ?? null; if ($value !== null && $value !== '') { - $byKey[(string) $key] = $this->overlayRowToSetting($item); + $byKey[(string) $key] = $this->overlayRowToSetting($item, $localByKey[(string) $key] ?? null); } } foreach ($localRows as $row) { @@ -246,7 +265,7 @@ class SettingsController extends Controller } /** - * get_settings اپ/ادمین: + * get_settings ادمین (و پایهٔ publicIndex قبل از فیلتر show_to_user): * can_edit=1 (+ runtime) از DB لوکال * can_edit=0 از سوپرادمین؛ اگر کلید در SA نبود یا value خالی بود → کپی لوکال * اگر سوپرادمین در دسترس نباشد کل جدول لوکال برمی‌گردد. @@ -259,6 +278,11 @@ class SettingsController extends Controller return $localRows; } + $localByKey = []; + foreach ($localRows as $row) { + $localByKey[(string) $row->key] = $row; + } + $byKey = []; foreach ($localRows as $row) { @@ -285,7 +309,7 @@ class SettingsController extends Controller } $value = $item['value'] ?? null; if ($value !== null && $value !== '') { - $byKey[$key] = $this->overlayRowToSetting($item); + $byKey[$key] = $this->overlayRowToSetting($item, $localByKey[$key] ?? null); } } @@ -331,7 +355,7 @@ class SettingsController extends Controller ->values(); } - private function overlayRowToSetting(array $item): Setting + private function overlayRowToSetting(array $item, ?Setting $local = null): Setting { $setting = new Setting(); $setting->id = isset($item['id']) && is_numeric($item['id']) ? (int) $item['id'] : null; @@ -342,6 +366,14 @@ class SettingsController extends Controller $setting->type = $item['type'] ?? ''; $setting->is_active = $item['is_active'] ?? 1; $setting->can_edit = 0; + if ($local) { + if (Setting::hasMenuColumn()) { + $setting->menu = $local->menu; + } + if (Setting::hasShowToUserColumn()) { + $setting->show_to_user = $local->show_to_user; + } + } return $setting; } @@ -359,20 +391,4 @@ class SettingsController extends Controller return $row; } - - /** - * اپ کاربر (بدون auth ادمین) هرگز secret/credential نمی‌بیند. - * پنل ادمین با auth:user همه کلیدها را می‌بیند. - * - * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Collection|iterable $rows - * @return \Illuminate\Support\Collection - */ - private function authorizeSettingsPayload($rows) - { - if (auth('user')->check()) { - return collect($rows)->values(); - } - - return Setting::forPublicApi($rows); - } } diff --git a/app/Models/Setting.php b/app/Models/Setting.php index 0f16568..7052400 100644 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -14,21 +14,35 @@ class Setting extends Model { use HasFactory,Selector; - private static ?bool $typeColumnExists = null; + /** @var array */ + private static array $columnExists = []; + + public static function hasSettingsColumn(string $column): bool + { + if (! isset(static::$columnExists[$column])) { + try { + static::$columnExists[$column] = Schema::hasColumn((new static)->getTable(), $column); + } catch (\Throwable) { + static::$columnExists[$column] = false; + } + } + + return static::$columnExists[$column]; + } public static function hasTypeColumn(): bool { - if (static::$typeColumnExists !== null) { - return static::$typeColumnExists; - } + return static::hasSettingsColumn('type'); + } - try { - static::$typeColumnExists = Schema::hasColumn((new static)->getTable(), 'type'); - } catch (\Throwable) { - static::$typeColumnExists = false; - } + public static function hasMenuColumn(): bool + { + return static::hasSettingsColumn('menu'); + } - return static::$typeColumnExists; + public static function hasShowToUserColumn(): bool + { + return static::hasSettingsColumn('show_to_user'); } /** @return array */ @@ -38,6 +52,12 @@ class Setting extends Model if (static::hasTypeColumn()) { $cols[] = 'type'; } + if (static::hasMenuColumn()) { + $cols[] = 'menu'; + } + if (static::hasShowToUserColumn()) { + $cols[] = 'show_to_user'; + } return $cols; } @@ -90,16 +110,52 @@ class Setting extends Model return (bool) preg_match('/(^|_)(secret|password|token|api_key|apikey|private)(_|$)/', $key); } + public static function isShownToUserApp($row): bool + { + $key = (string) (is_array($row) ? ($row['key'] ?? '') : ($row->key ?? '')); + if ($key === '' || static::isSecretOrServerOnlyKey($key)) { + return false; + } + + $flag = is_array($row) ? ($row['show_to_user'] ?? 0) : ($row->show_to_user ?? 0); + + return $flag === true || $flag === 1 || $flag === '1'; + } + /** - * فیلتر تنظیمات برای پاسخ عمومی (اپ کاربر). + * فیلتر تنظیمات برای پاسخ عمومی (اپ کاربر) — ستون show_to_user، بدون secret. * * @param \Illuminate\Support\Collection|iterable $settings * @return \Illuminate\Support\Collection */ public static function forPublicApi($settings) + { + return static::forClientApi($settings); + } + + /** + * @param \Illuminate\Support\Collection|iterable $settings + * @return \Illuminate\Support\Collection + */ + public static function forClientApi($settings) { return collect($settings) - ->filter(fn ($row) => ! static::isSecretOrServerOnlyKey((string) (is_array($row) ? ($row['key'] ?? '') : ($row->key ?? '')))) + ->filter(fn ($row) => static::isShownToUserApp($row)) + ->map(function ($row) { + if (is_array($row)) { + return [ + 'key' => (string) ($row['key'] ?? ''), + 'value' => $row['value'] ?? '', + 'is_active' => $row['is_active'] ?? 1, + ]; + } + + return [ + 'key' => (string) ($row->key ?? ''), + 'value' => $row->value ?? '', + 'is_active' => $row->is_active ?? 1, + ]; + }) ->values(); } @@ -112,9 +168,12 @@ class Setting extends Model 'value_json', 'type', 'can_edit', + 'menu', + 'show_to_user', ]; protected $casts = [ 'value_json'=>'array', 'is_active'=>'boolean', + 'show_to_user'=>'boolean', ]; } diff --git a/database/migrations/2026_08_24_000001_add_menu_and_show_to_user_to_settings.php b/database/migrations/2026_08_24_000001_add_menu_and_show_to_user_to_settings.php new file mode 100644 index 0000000..d2b53f2 --- /dev/null +++ b/database/migrations/2026_08_24_000001_add_menu_and_show_to_user_to_settings.php @@ -0,0 +1,46 @@ +string('menu', 64)->nullable()->after('can_edit'); + } + if (! Schema::hasColumn('settings', 'show_to_user')) { + $table->boolean('show_to_user')->default(false)->after('menu'); + } + }); + + $path = database_path('sql/settings_menu_and_show_to_user.sql'); + if (is_file($path)) { + DB::unprepared((string) file_get_contents($path)); + } + } + + public function down(): void + { + if (! Schema::hasTable('settings')) { + return; + } + + Schema::table('settings', function (Blueprint $table) { + if (Schema::hasColumn('settings', 'show_to_user')) { + $table->dropColumn('show_to_user'); + } + if (Schema::hasColumn('settings', 'menu')) { + $table->dropColumn('menu'); + } + }); + } +}; diff --git a/database/schema/mysql-schema.sql.reference b/database/schema/mysql-schema.sql.reference index aa4a617..87f0915 100644 --- a/database/schema/mysql-schema.sql.reference +++ b/database/schema/mysql-schema.sql.reference @@ -708,6 +708,8 @@ CREATE TABLE `settings` ( `deleted_at` timestamp NULL DEFAULT NULL, `is_active` tinyint DEFAULT NULL, `type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `menu` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `show_to_user` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`) USING BTREE ) ENGINE=MyISAM DEFAULT CHARSET=latin1; diff --git a/database/sql/deploy_settings_const.sql b/database/sql/deploy_settings_const.sql index bd817af..5a80b94 100644 --- a/database/sql/deploy_settings_const.sql +++ b/database/sql/deploy_settings_const.sql @@ -15,7 +15,8 @@ -- اصول: -- • چند بار قابل اجرا (idempotent) -- • value / is_active ردیف‌های موجود هرگز دست نخورده — فقط INSERT برای کلید جدید --- • can_edit / title_fa / description / type هر deploy از این فایل به‌روز می‌شوند +-- • can_edit / title_fa / description / type / menu / show_to_user هر deploy از این فایل به‌روز می‌شوند +-- • menu و show_to_user: database/sql/settings_menu_and_show_to_user.sql (settings:deploy و migrate اضافه می‌کنند) -- • کلیدهای can_edit=0 (سوپرادمین) در این فایل کامنت شده‌اند — مالکیت: SuperAdminProgramSettingsOverlay -- • schema (جدول/ستون) فقط با migrate — نه این فایل -- @@ -2435,6 +2436,9 @@ COMMIT; SELECT 'deploy_settings_const.sql finished' AS status; +-- menu / show_to_user: php artisan settings:deploy و migrate فایل +-- database/sql/settings_menu_and_show_to_user.sql را بعد از این اجرا می‌کنند. + -- بررسی fee_deduct_from_gold (desc_preview باید با «مشتری دقیقاً» شروع شود): SELECT `key`, `title_fa`, LEFT(`description`, 60) AS desc_preview, `can_edit`, `type`, `value` FROM `settings` diff --git a/database/sql/settings_menu_and_show_to_user.sql b/database/sql/settings_menu_and_show_to_user.sql new file mode 100644 index 0000000..68efc61 --- /dev/null +++ b/database/sql/settings_menu_and_show_to_user.sql @@ -0,0 +1,142 @@ +-- menu + show_to_user روی settings (متادیتا؛ value دست نخورده) +-- از migrate و php artisan settings:deploy اجرا می‌شود. + +SET @db = DATABASE(); + +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'settings' AND COLUMN_NAME = 'menu') = 0, + 'ALTER TABLE `settings` ADD COLUMN `menu` VARCHAR(64) NULL DEFAULT NULL', + 'SELECT 1' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'settings' AND COLUMN_NAME = 'show_to_user') = 0, + 'ALTER TABLE `settings` ADD COLUMN `show_to_user` TINYINT(1) NOT NULL DEFAULT 0', + 'SELECT 1' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +UPDATE `settings` SET `menu` = CASE + WHEN `key` IN ( + 'close_price', 'startDay', 'endDay', 'show_last_price', + 'show_home_price_when_closed', 'show_gold_ounce_price', 'trading_hours_by_day' + ) THEN 'trading_hours' + WHEN `key` = 'tracking_systems' THEN 'capture_sites' + WHEN `key` LIKE 'chat_%' THEN 'support_chat' + WHEN `key` LIKE 'deposit_method_%' + OR `key` IN ( + 'deposit_account_holder', 'deposit_card_number', 'deposit_account_number', + 'deposit_sheba', 'deposit_bank_name' + ) THEN 'deposit_methods' + WHEN `key` LIKE 'support_messenger%' + OR `key` IN ('deposit_sms_support_number', 'deposit_support_message_text') THEN 'support' + WHEN `key` LIKE 'zarin_%' THEN 'accounting_zarin' + WHEN `key` LIKE 'tahesab_%' OR `key` = 'send_to_tahesab' THEN 'accounting_tahesab' + WHEN `key` LIKE 'pargar_%' OR `key` = 'send_to_pargar' THEN 'accounting_pargar' + WHEN `key` = 'accounting_provider' THEN 'accounting_provider' + WHEN `key` LIKE 'payping_%' THEN 'payping' + WHEN `key` LIKE 'external_payment_%' OR `key` = 'wallet_payment_provider' THEN 'deposit_methods' + WHEN `key` LIKE 'montzar_%' OR `key` = 'display_montzar' THEN 'montzar' + WHEN `key` LIKE 'sms_notif_%' + OR `key` IN ('charge_notif', 'register_referral_sms_to_referrer') THEN 'notifications' + WHEN `key` LIKE 'price_channel_%' OR `key` = 'change_mesghal' THEN 'price_channel' + WHEN `key` LIKE 'telegram_bot%' OR `key` LIKE 'bale_bot%' OR `key` LIKE 'rubika_bot%' THEN 'bots' + WHEN `key` LIKE 'referral_%' + OR `key` IN ('invite_link_text', 'gift_for', 'register_referral', 'referral_bonus_amount') THEN 'referral' + WHEN `key` LIKE 'text_%' OR `key` IN ('enamad_url', 'about_software') THEN 'content' + WHEN `key` LIKE 'min_%' OR `key` LIKE 'minimum_%' OR `key` LIKE 'balance_report_exclude%' THEN 'limits' + WHEN `key` LIKE 'zaarbox_%' OR `key` IN ('price_provider', 'main_product') THEN 'price_source' + WHEN `key` LIKE 'node_%' OR `key` IN ('montify_name', 'external_order_api_enabled') THEN 'store' + WHEN `key` LIKE 'chart_%' THEN 'chart' + WHEN `key` IN ('wss_enabled', 'sse_path', 'wss_path', 'profit_gold_api_enabled') THEN 'infra' + WHEN `key` LIKE 'fee_%' + OR `key` IN ('buy_fee', 'sell_fee', 'up_price', 'down_price', 'price_extreme_date') + OR `key` LIKE '%charge_percentage%' THEN 'fees' + WHEN `key` LIKE 'product_%' + OR `key` IN ('main_wallet_products', 'require_zero_main_gold_before_other_buy') + OR `key` LIKE 'profit_gold%' THEN 'features' + WHEN `key` IN ( + 'banner_enabled', 'tracking_systems_enabled', 'jibit_verification', 'register_jibit_verify', + 'show_product_details', 'show_product_trade_status', 'show_product_card_meta', + 'products_site_link_enabled', 'products_site_link_url', 'products_site_link_text', 'has_chat', + 'enable_conditional_order', 'enable_product_funds', 'has_page_price_trigger', 'has_page_notification', + 'has_page_gift_card', 'enable_customer_level_profit', 'jibit_enabled', 'customer_direct_sms', + 'has_page_earn_money', 'group_sms', 'customer_special_pricing', 'permision__enabled', 'show_to_users_chat' + ) THEN 'customer_features' + WHEN `key` LIKE 'logo_%' + OR `key` IN ( + 'name_fa', 'name_en', 'app_url', 'admin_url', 'apk_download_link', 'bace_url', 'server_url', + 'title_pwa', 'title_fav', 'metal_type', 'main_metal', 'unit', 'open_register', 'auto_active_customer', + 'store_status', 'key', 'is_store', 'sms_panel' + ) THEN 'general' + ELSE 'misc' +END; + +UPDATE `settings` SET `show_to_user` = 0; + +UPDATE `settings` SET `show_to_user` = 1 WHERE `key` IN ( + 'apk_download_link', + 'app_url', + 'banner_enabled', + 'chat_api', + 'chat_download_url', + 'chat_upload_url', + 'close_price', + 'deposit_account_holder', + 'deposit_account_number', + 'deposit_bank_name', + 'deposit_card_number', + 'deposit_method_card_to_card', + 'deposit_method_gateway', + 'deposit_method_identified', + 'deposit_method_shetab', + 'deposit_sheba', + 'deposit_sms_support_number', + 'deposit_support_message_text', + 'enable_conditional_order', + 'enable_product_funds', + 'endDay', + 'fee_conditional_order', + 'fee_deduct_from_gold', + 'fee_service_charge_percentage', + 'has_chat', + 'has_page_gift_card', + 'has_page_notification', + 'has_page_price_trigger', + 'invite_link_text', + 'main_product', + 'main_wallet_products', + 'metal_type', + 'min_deposit', + 'min_withdraw', + 'open_register', + 'permision__enabled', + 'products_site_link_enabled', + 'products_site_link_text', + 'products_site_link_url', + 'referral_id', + 'referral_show', + 'show_gold_ounce_price', + 'show_home_price_when_closed', + 'show_product_card_meta', + 'show_product_details', + 'show_product_trade_status', + 'show_to_users_chat', + 'sse_path', + 'startDay', + 'support_messenger', + 'support_messenger_id', + 'support_messenger_text', + 'support_messengers', + 'text_about', + 'text_rule', + 'tracking_systems', + 'tracking_systems_enabled', + 'unit', + 'wss_enabled', + 'wss_path', + 'zaarbox_api_url' +); diff --git a/docs/WALLET_ONLINE_PAYMENT.md b/docs/WALLET_ONLINE_PAYMENT.md index 5fbd475..f388454 100644 --- a/docs/WALLET_ONLINE_PAYMENT.md +++ b/docs/WALLET_ONLINE_PAYMENT.md @@ -59,9 +59,9 @@ Montify → payment_url → درگاه بانکی ### ۳.۲. فیلتر `get_settings` -در `App\Models\Setting::isSecretOrServerOnlyKey()` و `SettingsController::authorizeSettingsPayload()`: +در `App\Models\Setting::isSecretOrServerOnlyKey()` و `Setting::forClientApi()`: -- درخواست عمومی (`api/get_settings` بدون auth ادمین) → کلیدهای محرمانه حذف می‌شوند. +- درخواست عمومی (`api/get_settings` بدون auth) → فقط ردیف‌های `show_to_user=1`؛ credential حذف می‌شود. - پنل ادمین (`admin/get_settings` با `auth:user`) → همه کلیدها قابل مشاهده/ویرایش طبق `can_edit`. کلیدهای مرتبط با پرداخت که به اپ عمومی نمی‌روند شامل: diff --git a/routes/api.php b/routes/api.php index 7de9e4a..101bde7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -53,7 +53,7 @@ Route::post('price_triggers/table', [PriceTriggerController::class, 'table']); Route::get('/city/state',[CityController::class, 'stateList']); Route::get('/city/all',[CityController::class, 'cityListAll']); Route::get('/city/{state_id}',[CityController::class, 'cityList']); -Route::get('get_settings',[SettingsController::class, 'index']); +Route::get('get_settings', [SettingsController::class, 'publicIndex']); // وب‌هوک سرور چت → اعلان SMS اولین پیام خوانده‌نشده پشتیبانی Route::post('/chat/support-message-notify', [SupportChatNotifyController::class, 'notify']);