import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { useApiClient } from '@/composables/useApiClient' import { useLabels } from '@/composables/useLabels' import { useUnsavedChangesStore } from '@/stores/useUnsavedChangesStore' import IvyMessage from '@/views/_components/message/ivyMessage.ts' import type { RecaptchaSettings } from '@/types/recaptcha/recaptcha-interface' import type { RecaptchaType } from '@/types/recaptcha/recaptcha-type' import type { AllSettings } from '@/types/settings' /** * Settings store for managing application settings * This store handles reCAPTCHA settings and other security/general settings */ export const useSettingsStore = defineStore('settingsStore', () => { // State using refs (composition API style like useFormBuilder) // All settings state const allSettings = ref(null) const isLoading = ref(false) // reCAPTCHA specific state (for backward compatibility) const recaptchaType = ref('v2') const recaptchaSiteKey = ref('') const recaptchaSecretKey = ref('') const recaptchaLanguage = ref('') // WCAG Compliance state const wcagBackendCompliance = ref(false) // Request deduplication - prevent multiple simultaneous calls let loadPromise: Promise | null = null // Template favorites state const favoriteTemplates = ref([]) // Delete on uninstall state const deleteOnUninstall = ref(false) const { getLabel } = useLabels() const { request } = useApiClient() // Use unsaved changes store for state management const unsavedChangesStore = useUnsavedChangesStore() // Create complete entity manager for settings const entityManager = unsavedChangesStore.createEntityManager('settingsBuilder', { type: recaptchaType, siteKey: recaptchaSiteKey, secretKey: recaptchaSecretKey, language: recaptchaLanguage, wcagBackendCompliance: wcagBackendCompliance, favoriteTemplates: favoriteTemplates, deleteOnUninstall: deleteOnUninstall, }) // Computed values const recaptchaSettings = computed(() => ({ type: recaptchaType.value, siteKey: recaptchaSiteKey.value, secretKey: recaptchaSecretKey.value, language: recaptchaLanguage.value, })) // Computed getters for all settings const securitySettings = computed(() => allSettings.value?.security || null) const generalSettings = computed(() => allSettings.value?.general || null) const integrationsSettings = computed(() => allSettings.value?.integrations || null) // Specific settings getters const currentRecaptchaSettings = computed(() => allSettings.value?.security?.recaptcha || null) // Future extensions can be added here: // const currentHCaptchaSettings = computed(() => allSettings.value?.security?.hcaptcha || null) // const currentTurnstileSettings = computed(() => allSettings.value?.security?.turnstile || null) // Integration settings getters const wpDataTablesIntegration = computed( () => allSettings.value?.integrations?.wpdatatables || null, ) const isRecaptchaConfigured = computed(() => { // reCAPTCHA is configured only when both keys are present and valid const siteKeyPresent = recaptchaSiteKey.value && recaptchaSiteKey.value.trim() !== '' const secretKeyPresent = recaptchaSecretKey.value && recaptchaSecretKey.value.trim() !== '' return ( siteKeyPresent && secretKeyPresent && validateRecaptchaKeyFormat(recaptchaSiteKey.value) && validateRecaptchaKeyFormat(recaptchaSecretKey.value) ) }) const hasChanges = computed(() => { return entityManager.hasUnsavedChanges().value }) // Actions - following the same pattern as useFormBuilder const setRecaptchaType = (type: RecaptchaType) => { recaptchaType.value = type } const setRecaptchaSiteKey = (siteKey: string) => { recaptchaSiteKey.value = siteKey } const setRecaptchaSecretKey = (secretKey: string) => { recaptchaSecretKey.value = secretKey } const setRecaptchaLanguage = (language: string) => { recaptchaLanguage.value = language } const setWcagBackendCompliance = (value: boolean) => { wcagBackendCompliance.value = value } const setDeleteOnUninstall = (value: boolean) => { deleteOnUninstall.value = value } const setRecaptchaSettings = (settings: RecaptchaSettings) => { recaptchaType.value = settings.type recaptchaSiteKey.value = settings.siteKey recaptchaSecretKey.value = settings.secretKey // Set defaults for new fields if not provided recaptchaLanguage.value = settings.language || '' } // Load all settings for admin page - single API call for better performance const loadAllSettings = async (): Promise => { // If there's already a request in progress, return it if (loadPromise) { return loadPromise } loadPromise = (async () => { try { isLoading.value = true const { data, error, status } = await request('settings/all', { method: 'GET', headers: { 'Content-Type': 'application/json', }, }) if (status === 200 && !error && data?.data?.data) { allSettings.value = data.data.data // Update individual reCAPTCHA refs for backward compatibility const recaptcha = data.data.data.security?.recaptcha if (recaptcha) { recaptchaType.value = recaptcha.type || 'v2' recaptchaSiteKey.value = recaptcha.siteKey || '' recaptchaSecretKey.value = recaptcha.secretKey || '' recaptchaLanguage.value = recaptcha.language || '' } // Update WCAG compliance setting const general = data.data.data.general if (general && general.wcagBackend !== undefined) { wcagBackendCompliance.value = general.wcagBackend } // Update delete on uninstall setting if (general && general.delete_on_uninstall !== undefined) { deleteOnUninstall.value = general.delete_on_uninstall } // Update favorite templates if (general && general.favoriteTemplates) { favoriteTemplates.value = general.favoriteTemplates } // Reset unsaved changes after successful load entityManager.resetUnsavedChanges() return true } else { IvyMessage({ message: getLabel('failed_to_load_settings'), type: 'error', }) return false } } catch (error) { IvyMessage({ message: getLabel('error_loading_settings'), type: 'error', }) console.error(getLabel('error_loading_settings'), error) return false } finally { isLoading.value = false loadPromise = null } })() return loadPromise } // Generic settings loader - can be used for any category/option combination const loadSetting = async (category: string, option: string) => { try { isLoading.value = true const { data, error, status } = await request(`setting/${category}/${option}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }) if (status === 200 && !error && data?.data?.value) { return data.data.value } else { IvyMessage({ message: getLabel('failed_to_load_settings'), type: 'error', }) return null } } catch (err) { console.error(getLabel('failed_to_load_settings'), err) IvyMessage({ message: getLabel('failed_to_load_settings'), type: 'error', }) return null } finally { isLoading.value = false } } // Generic settings saver - can be used for any category/option combination const saveSetting = async ( category: string, option: string, value: unknown, showMessage: boolean = true, ): Promise => { try { isLoading.value = true const { data, error, status } = await request('setting/update', { method: 'POST', headers: { 'Content-Type': 'application/json', }, data: { settingsCategory: category, settingsOption: option, settingsValue: value, }, }) if (status === 200 && !error) { if (showMessage) { IvyMessage({ message: getLabel('settings_saved'), type: 'success', }) } return true } else { if (showMessage) { IvyMessage({ message: getLabel('failed_to_save_settings'), type: 'error', }) } console.error(getLabel('failed_to_save_settings'), data?.message) return false } } catch (error) { if (showMessage) { IvyMessage({ message: getLabel('error_saving_settings'), type: 'error', }) } console.error(getLabel('error_saving_settings'), error) return false } finally { isLoading.value = false } } // Update specific setting in allSettings state (for real-time updates) const updateSettingInState = (category: keyof AllSettings, option: string, value: unknown) => { if (allSettings.value) { // Create a deep copy to trigger reactivity const updatedSettings = { ...allSettings.value } if (updatedSettings[category]) { ;(updatedSettings[category] as Record)[option] = value allSettings.value = updatedSettings } } } // Load reCAPTCHA settings - following the async pattern from useFormBuilder const loadRecaptchaSettings = async () => { const settings = await loadSetting('security', 'recaptcha') if (settings) { recaptchaType.value = settings.type || 'v2' recaptchaSiteKey.value = settings.siteKey || '' recaptchaSecretKey.value = settings.secretKey || '' recaptchaLanguage.value = settings.language || '' // Reset unsaved changes after successful load entityManager.resetUnsavedChanges() } } // Save reCAPTCHA settings - following the async pattern from useFormBuilder const saveRecaptchaSettings = async (): Promise => { // Basic frontend validation - only validate non-empty keys const siteKeyValue = recaptchaSiteKey.value.trim() const secretKeyValue = recaptchaSecretKey.value.trim() // Validate both keys at once if they are provided if ( (siteKeyValue && !validateRecaptchaKeyFormat(siteKeyValue)) || (secretKeyValue && !validateRecaptchaKeyFormat(secretKeyValue)) ) { const errorMessage = siteKeyValue && !validateRecaptchaKeyFormat(siteKeyValue) ? getLabel('site_key_invalid') : getLabel('secret_key_invalid') IvyMessage({ message: errorMessage, type: 'error', }) return false } const settingsValue = { type: recaptchaType.value, siteKey: recaptchaSiteKey.value, secretKey: recaptchaSecretKey.value, language: recaptchaLanguage.value, } const success = await saveSetting('security', 'recaptcha', settingsValue) if (success) { // Update the allSettings state with the new values updateSettingInState('security', 'recaptcha', settingsValue) // Reset unsaved changes after successful save entityManager.resetUnsavedChanges() } return success } // Save general settings const saveGeneralSettings = async (): Promise => { const wcagSuccess = await saveSetting( 'general', 'wcagBackend', wcagBackendCompliance.value, false, ) const deleteSuccess = await saveSetting( 'general', 'delete_on_uninstall', deleteOnUninstall.value, ) const success = wcagSuccess && deleteSuccess if (success) { // Update the allSettings state with the new values updateSettingInState('general', 'wcagBackend', wcagBackendCompliance.value) updateSettingInState('general', 'delete_on_uninstall', deleteOnUninstall.value) // Reset unsaved changes after successful save entityManager.resetUnsavedChanges() } return success } // Basic reCAPTCHA key format validation const validateRecaptchaKeyFormat = (key: string): boolean => { // Allow empty keys (for clearing/resetting settings) if (!key || key.trim() === '') { return true } const trimmedKey = key.trim() // reCAPTCHA keys are typically 40 characters long, but can vary if (trimmedKey.length < 20 || trimmedKey.length > 100) { return false } // Basic format validation - should contain alphanumeric characters and some special chars return /^[A-Za-z0-9_-]+$/.test(trimmedKey) } // Reset changes - following the pattern from useFormBuilder const resetChanges = () => { entityManager.resetUnsavedChanges() } // Integration settings methods /** * Get integration settings dynamically by name */ const getIntegrationSettings = (integrationName: string) => { return allSettings.value?.integrations?.[integrationName] || null } /** * Check if integration is enabled */ const isIntegrationEnabled = (integrationName: string): boolean => { return allSettings.value?.integrations?.[integrationName]?.enabled || false } /** * Update integration enabled status (simple toggle) */ const updateIntegrationEnabled = async ( integrationName: string, enabled: boolean, ): Promise => { const integrationValue = { enabled } const success = await saveSetting('integrations', integrationName, integrationValue) if (success) { updateSettingInState('integrations', integrationName, integrationValue) } return success } /** * Update integration settings (for complex integrations with multiple settings) */ const updateIntegrationSettings = async ( integrationName: string, settings: Record, ): Promise => { const success = await saveSetting('integrations', integrationName, settings) if (success) { updateSettingInState('integrations', integrationName, settings) } return success } /** * Toggle a template as favorite */ const toggleFavoriteTemplate = async (templateId: string): Promise => { const index = favoriteTemplates.value.indexOf(templateId) if (index > -1) { // Remove from favorites favoriteTemplates.value = favoriteTemplates.value.filter((id) => id !== templateId) } else { // Add to favorites favoriteTemplates.value = [...favoriteTemplates.value, templateId] } // Save to backend const success = await saveSetting('general', 'favoriteTemplates', favoriteTemplates.value) if (success) { updateSettingInState('general', 'favoriteTemplates', favoriteTemplates.value) } return success } /** * Check if a template is favorited */ const isTemplateFavorited = (templateId: string): boolean => { return favoriteTemplates.value.includes(templateId) } /** * Load favorite templates */ const loadFavoriteTemplates = async () => { // If allSettings is already loaded, use it if (allSettings.value?.general?.favoriteTemplates) { favoriteTemplates.value = allSettings.value.general.favoriteTemplates as string[] return } // Otherwise, load from individual endpoint const favorites = await loadSetting('general', 'favoriteTemplates') if (favorites && Array.isArray(favorites)) { favoriteTemplates.value = favorites as string[] } else if (favorites === null) { // If it returns null, try loading all settings await loadAllSettings() if (allSettings.value?.general?.favoriteTemplates) { favoriteTemplates.value = allSettings.value.general.favoriteTemplates as string[] } } } return { // State recaptchaType, recaptchaSiteKey, recaptchaSecretKey, recaptchaLanguage, wcagBackendCompliance, favoriteTemplates, deleteOnUninstall, isLoading, // All Settings State allSettings, // Computed recaptchaSettings, isRecaptchaConfigured, hasChanges, getCurrentState: entityManager.getCurrentState, // All Settings Computed securitySettings, generalSettings, integrationsSettings, currentRecaptchaSettings, wpDataTablesIntegration, // Future extensions: // currentHCaptchaSettings, // currentTurnstileSettings, // Actions setRecaptchaType, setRecaptchaSiteKey, setRecaptchaSecretKey, setRecaptchaLanguage, setWcagBackendCompliance, setDeleteOnUninstall, setRecaptchaSettings, loadRecaptchaSettings, saveRecaptchaSettings, saveGeneralSettings, resetChanges, // All Settings API loadAllSettings, updateSettingInState, // Generic Settings API loadSetting, saveSetting, // Integration Settings API updateIntegrationEnabled, getIntegrationSettings, isIntegrationEnabled, updateIntegrationSettings, // Template Favorites API toggleFavoriteTemplate, isTemplateFavorited, loadFavoriteTemplates, // Validation utilities validateRecaptchaKeyFormat, } })