where('is_runtime_enforced', true) ->pluck('key'); $settings = Setting::whereIn('key', $runtimeKeys)->get() ->map(function (Setting $setting) { $meta = SettingsCatalog::metaFor($setting->key) ?? []; if ($setting->is_secret) { $setting->value = $setting->value ? '********' : ''; } return array_merge($setting->toArray(), [ 'label' => $meta['label'] ?? $setting->key, 'hint' => $meta['hint'] ?? $setting->description, 'used_by' => $meta['used_by'] ?? [], 'coming_soon' => $meta['coming_soon'] ?? ! $setting->is_runtime_enforced, ]); }) ->groupBy('group'); return response()->json($settings); } public function update(Request $request): JsonResponse { Gate::authorize('update', Setting::class); SettingsCatalog::ensureDefaults(); $request->validate([ 'settings' => 'required|array', 'settings.*.key' => 'required|string', 'settings.*.value' => 'nullable', 'settings.*.group' => 'nullable|string', 'settings.*.type' => 'nullable|string', ]); foreach ($request->settings as $setting) { $existing = Setting::where('key', $setting['key'])->first(); $this->validateSettingValue($setting, $existing); $oldValue = $existing?->value; $newValue = ($existing?->is_secret && ($setting['value'] ?? '') === '********') ? $oldValue : $this->normalizeValue($setting['value'] ?? '', $setting['type'] ?? $existing?->type ?? 'string'); $meta = SettingsCatalog::metaFor($setting['key']); Setting::updateOrCreate( ['key' => $setting['key']], [ 'value' => $newValue, 'group' => $meta['group'] ?? $setting['group'] ?? $existing?->group ?? 'general', 'type' => $meta['type'] ?? $setting['type'] ?? $existing?->type ?? 'string', 'default_value' => $meta['default_value'] ?? $existing?->default_value, 'allowed_values' => $meta['allowed_values'] ?? $existing?->allowed_values, 'is_secret' => $meta['is_secret'] ?? $existing?->is_secret ?? false, 'is_public' => $meta['is_public'] ?? $existing?->is_public ?? false, 'is_runtime_enforced' => $meta['is_runtime_enforced'] ?? $existing?->is_runtime_enforced ?? true, 'description' => $meta['hint'] ?? $existing?->description, ] ); if ($this->isSensitiveChange($setting['key'], $meta ?? null)) { $old = ($existing?->is_secret ?? false) ? '[secret]' : $oldValue; ActivityLogger::log('sensitive_setting_changed', "Setting {$setting['key']} changed from {$old}"); } } ActivityLogger::log('settings_updated', 'System settings updated'); return response()->json(['message' => 'تنظیمات ذخیره شد']); } public function public(): JsonResponse { SettingsCatalog::ensureDefaults(); $publicRuntimeKeys = collect(SettingsCatalog::definitions()) ->where('is_runtime_enforced', true) ->where('is_public', true) ->pluck('key'); $settings = Setting::whereIn('key', $publicRuntimeKeys)->get() ->filter(fn (Setting $setting) => ! $setting->is_secret) ->mapWithKeys(fn (Setting $setting) => [$setting->key => $setting->effectiveValue()]); return response()->json($settings); } public function testVoip(): JsonResponse { Gate::authorize('viewAny', Setting::class); $provider = Setting::where('key', 'voip_provider')->value('value') ?: 'none'; $missing = []; if ($provider === 'ami') { foreach (['voip_ami_host', 'voip_ami_port', 'voip_ami_username', 'voip_ami_secret', 'voip_ami_channel_technology', 'voip_ami_context'] as $key) { if (! Setting::where('key', $key)->value('value')) { $missing[] = $key; } } } if ($provider === 'api') { foreach (['voip_api_base_url', 'voip_api_token'] as $key) { if (! Setting::where('key', $key)->value('value')) { $missing[] = $key; } } } if ($provider === 'socket') { foreach (['voip_socket_host', 'voip_socket_port'] as $key) { if (! Setting::where('key', $key)->value('value')) { $missing[] = $key; } } } ActivityLogger::log('voip_connection_tested', "VoIP provider {$provider} tested"); if ($missing === []) { $result = $this->voipManager->testConnection(); return response()->json([ 'ok' => (bool) ($result['ok'] ?? false), 'provider' => $provider, 'message' => $result['message'] ?? 'نتیجه تست اتصال مشخص نیست.', 'missing' => $result['missing'] ?? [], ]); } return response()->json([ 'ok' => ! $missing, 'provider' => $provider, 'message' => $missing ? 'تنظیمات اتصال کامل نیست.' : 'تنظیمات اتصال معتبر است.', 'missing' => $missing, ]); } public function testVoipCall(Request $request): JsonResponse { Gate::authorize('update', Setting::class); $validated = $request->validate([ 'phone' => ['required', 'string', 'max:30', 'regex:/^[0-9+*#]+$/'], 'extension' => ['required', 'string', 'max:20', 'regex:/^[0-9*#]+$/'], ]); $result = $this->voipManager->initiateCall($validated['phone'], $validated['extension']); ActivityLogger::log('voip_test_call_requested', 'A real VoIP test call was requested', null, null, [ 'success' => (bool) ($result['success'] ?? false), 'status' => $result['status'] ?? null, ]); return response()->json([ 'ok' => (bool) ($result['success'] ?? false), 'message' => $result['message'] ?? 'نتیجه تماس آزمایشی مشخص نیست.', 'provider_call_id' => $result['provider_call_id'] ?? null, 'status' => $result['status'] ?? null, ], ($result['success'] ?? false) ? 200 : 422); } private function validateSettingValue(array $payload, ?Setting $existing): void { $type = $payload['type'] ?? $existing?->type ?? 'string'; $value = $payload['value'] ?? ''; $rules = $existing?->validation_rules ?: match ($type) { 'boolean' => ['in:true,false,1,0'], 'integer' => ['integer'], default => ['nullable', 'string'], }; if ($type === 'boolean') { $rules = ['in:true,false,1,0']; } if ($existing?->allowed_values) { $rules[] = 'in:'.implode(',', $existing->allowed_values); } Validator::make(['value' => $value], ['value' => $rules])->validate(); } private function normalizeValue(mixed $value, string $type): string { return match ($type) { 'boolean' => filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false', 'integer' => (string) (int) $value, default => (string) $value, }; } private function isSensitiveChange(string $key, ?array $meta): bool { return ($meta['is_secret'] ?? false) || in_array($meta['group'] ?? '', ['security', 'import_export', 'voip'], true) || str_contains($key, 'password') || str_contains($key, 'token') || str_contains($key, 'export'); } }