PluginProbe
The Innovative Form Builder – IvyForms / 0.5
The Innovative Form Builder – IvyForms v0.5
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / stores / useSettingsStore.ts

useSettingsStore.ts in The Innovative Form Builder – IvyForms 0.5, at frontend/src/stores/useSettingsStore.ts

459 lines 13.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { defineStore } from 'pinia'
2 import { ref, computed } from 'vue'
3 import { useApiClient } from '@/composables/useApiClient'
4 import { useLabels } from '@/composables/useLabels'
5 import { useUnsavedChangesStore } from '@/stores/useUnsavedChangesStore'
6 import IvyMessage from '@/views/_components/message/ivyMessage.ts'
7 import type { RecaptchaSettings } from '@/types/recaptcha/recaptcha-interface'
8 import type { RecaptchaType } from '@/types/recaptcha/recaptcha-type'
9 import type { AllSettings } from '@/types/settings'
10
11 /**
12 * Settings store for managing application settings
13 * This store handles reCAPTCHA settings and other security/general settings
14 */
15 export const useSettingsStore = defineStore('settingsStore', () => {
16 // State using refs (composition API style like useFormBuilder)
17
18 // All settings state
19 const allSettings = ref<AllSettings | null>(null)
20 const isLoading = ref(false)
21
22 // reCAPTCHA specific state (for backward compatibility)
23 const recaptchaType = ref<RecaptchaType>('v2')
24 const recaptchaSiteKey = ref('')
25 const recaptchaSecretKey = ref('')
26 const recaptchaLanguage = ref('')
27
28 // WCAG Compliance state
29 const wcagBackendCompliance = ref(false)
30
31 const { getLabel } = useLabels()
32 const { request } = useApiClient()
33
34 // Use unsaved changes store for state management
35 const unsavedChangesStore = useUnsavedChangesStore()
36
37 // Create complete entity manager for settings
38 const entityManager = unsavedChangesStore.createEntityManager('settingsBuilder', {
39 type: recaptchaType,
40 siteKey: recaptchaSiteKey,
41 secretKey: recaptchaSecretKey,
42 language: recaptchaLanguage,
43 wcagBackendCompliance: wcagBackendCompliance,
44 })
45
46 // Computed values
47 const recaptchaSettings = computed(() => ({
48 type: recaptchaType.value,
49 siteKey: recaptchaSiteKey.value,
50 secretKey: recaptchaSecretKey.value,
51 language: recaptchaLanguage.value,
52 }))
53
54 // Computed getters for all settings
55 const securitySettings = computed(() => allSettings.value?.security || null)
56 const generalSettings = computed(() => allSettings.value?.general || null)
57 const integrationsSettings = computed(() => allSettings.value?.integrations || null)
58
59 // Specific settings getters
60 const currentRecaptchaSettings = computed(() => allSettings.value?.security?.recaptcha || null)
61 // Future extensions can be added here:
62 // const currentHCaptchaSettings = computed(() => allSettings.value?.security?.hcaptcha || null)
63 // const currentTurnstileSettings = computed(() => allSettings.value?.security?.turnstile || null)
64
65 // Integration settings getters
66 const wpDataTablesIntegration = computed(
67 () => allSettings.value?.integrations?.wpdatatables || null,
68 )
69
70 const isRecaptchaConfigured = computed(() => {
71 // reCAPTCHA is configured only when both keys are present and valid
72 const siteKeyPresent = recaptchaSiteKey.value && recaptchaSiteKey.value.trim() !== ''
73 const secretKeyPresent = recaptchaSecretKey.value && recaptchaSecretKey.value.trim() !== ''
74
75 return (
76 siteKeyPresent &&
77 secretKeyPresent &&
78 validateRecaptchaKeyFormat(recaptchaSiteKey.value) &&
79 validateRecaptchaKeyFormat(recaptchaSecretKey.value)
80 )
81 })
82
83 const hasChanges = computed(() => {
84 return entityManager.hasUnsavedChanges().value
85 })
86
87 // Actions - following the same pattern as useFormBuilder
88 const setRecaptchaType = (type: RecaptchaType) => {
89 recaptchaType.value = type
90 }
91
92 const setRecaptchaSiteKey = (siteKey: string) => {
93 recaptchaSiteKey.value = siteKey
94 }
95
96 const setRecaptchaSecretKey = (secretKey: string) => {
97 recaptchaSecretKey.value = secretKey
98 }
99
100 const setRecaptchaLanguage = (language: string) => {
101 recaptchaLanguage.value = language
102 }
103
104 const setWcagBackendCompliance = (value: boolean) => {
105 wcagBackendCompliance.value = value
106 }
107
108 const setRecaptchaSettings = (settings: RecaptchaSettings) => {
109 recaptchaType.value = settings.type
110 recaptchaSiteKey.value = settings.siteKey
111 recaptchaSecretKey.value = settings.secretKey
112 // Set defaults for new fields if not provided
113 recaptchaLanguage.value = settings.language || ''
114 }
115
116 // Load all settings for admin page - single API call for better performance
117 const loadAllSettings = async (): Promise<boolean> => {
118 try {
119 isLoading.value = true
120 const { data, error, status } = await request('settings/all', {
121 method: 'GET',
122 headers: {
123 'Content-Type': 'application/json',
124 },
125 })
126
127 if (status === 200 && !error && data?.data?.data) {
128 allSettings.value = data.data.data
129
130 // Update individual reCAPTCHA refs for backward compatibility
131 const recaptcha = data.data.data.security?.recaptcha
132 if (recaptcha) {
133 recaptchaType.value = recaptcha.type || 'v2'
134 recaptchaSiteKey.value = recaptcha.siteKey || ''
135 recaptchaSecretKey.value = recaptcha.secretKey || ''
136 recaptchaLanguage.value = recaptcha.language || ''
137 }
138
139 // Update WCAG compliance setting
140 const general = data.data.data.general
141 if (general && general.wcagBackend !== undefined) {
142 wcagBackendCompliance.value = general.wcagBackend
143 }
144
145 // Reset unsaved changes after successful load
146 entityManager.resetUnsavedChanges()
147 return true
148 } else {
149 IvyMessage({
150 message: getLabel('failed_to_load_settings'),
151 type: 'error',
152 })
153 return false
154 }
155 } catch (error) {
156 IvyMessage({
157 message: getLabel('error_loading_settings'),
158 type: 'error',
159 })
160 console.error(getLabel('error_loading_settings'), error)
161 return false
162 } finally {
163 isLoading.value = false
164 }
165 }
166
167 // Generic settings loader - can be used for any category/option combination
168 const loadSetting = async (category: string, option: string) => {
169 try {
170 isLoading.value = true
171 const { data, error, status } = await request(`setting/${category}/${option}`, {
172 method: 'GET',
173 headers: {
174 'Content-Type': 'application/json',
175 },
176 })
177
178 if (status === 200 && !error && data?.data?.value) {
179 return data.data.value
180 } else {
181 IvyMessage({
182 message: getLabel('failed_to_load_settings'),
183 type: 'error',
184 })
185 return null
186 }
187 } catch (err) {
188 console.error(getLabel('failed_to_load_settings'), err)
189 IvyMessage({
190 message: getLabel('failed_to_load_settings'),
191 type: 'error',
192 })
193 return null
194 } finally {
195 isLoading.value = false
196 }
197 }
198
199 // Generic settings saver - can be used for any category/option combination
200 const saveSetting = async (
201 category: string,
202 option: string,
203 value: unknown,
204 ): Promise<boolean> => {
205 try {
206 isLoading.value = true
207 const { data, error, status } = await request('setting/update', {
208 method: 'POST',
209 headers: {
210 'Content-Type': 'application/json',
211 },
212 data: {
213 settingsCategory: category,
214 settingsOption: option,
215 settingsValue: value,
216 },
217 })
218
219 if (status === 200 && !error) {
220 IvyMessage({
221 message: getLabel('settings_saved'),
222 type: 'success',
223 })
224 return true
225 } else {
226 IvyMessage({
227 message: getLabel('failed_to_save_settings'),
228 type: 'error',
229 })
230 console.error(getLabel('failed_to_save_settings'), data?.message)
231 return false
232 }
233 } catch (error) {
234 IvyMessage({
235 message: getLabel('error_saving_settings'),
236 type: 'error',
237 })
238 console.error(getLabel('error_saving_settings'), error)
239 return false
240 } finally {
241 isLoading.value = false
242 }
243 }
244
245 // Update specific setting in allSettings state (for real-time updates)
246 const updateSettingInState = (category: keyof AllSettings, option: string, value: unknown) => {
247 if (allSettings.value) {
248 // Create a deep copy to trigger reactivity
249 const updatedSettings = { ...allSettings.value }
250 if (updatedSettings[category]) {
251 ;(updatedSettings[category] as Record<string, unknown>)[option] = value
252 allSettings.value = updatedSettings
253 }
254 }
255 }
256
257 // Load reCAPTCHA settings - following the async pattern from useFormBuilder
258 const loadRecaptchaSettings = async () => {
259 const settings = await loadSetting('security', 'recaptcha')
260
261 if (settings) {
262 recaptchaType.value = settings.type || 'v2'
263 recaptchaSiteKey.value = settings.siteKey || ''
264 recaptchaSecretKey.value = settings.secretKey || ''
265 recaptchaLanguage.value = settings.language || ''
266
267 // Reset unsaved changes after successful load
268 entityManager.resetUnsavedChanges()
269 }
270 }
271
272 // Save reCAPTCHA settings - following the async pattern from useFormBuilder
273 const saveRecaptchaSettings = async (): Promise<boolean> => {
274 // Basic frontend validation - only validate non-empty keys
275 const siteKeyValue = recaptchaSiteKey.value.trim()
276 const secretKeyValue = recaptchaSecretKey.value.trim()
277
278 // Validate both keys at once if they are provided
279 if (
280 (siteKeyValue && !validateRecaptchaKeyFormat(siteKeyValue)) ||
281 (secretKeyValue && !validateRecaptchaKeyFormat(secretKeyValue))
282 ) {
283 const errorMessage =
284 siteKeyValue && !validateRecaptchaKeyFormat(siteKeyValue)
285 ? getLabel('site_key_invalid')
286 : getLabel('secret_key_invalid')
287
288 IvyMessage({
289 message: errorMessage,
290 type: 'error',
291 })
292 return false
293 }
294
295 const settingsValue = {
296 type: recaptchaType.value,
297 siteKey: recaptchaSiteKey.value,
298 secretKey: recaptchaSecretKey.value,
299 language: recaptchaLanguage.value,
300 }
301
302 const success = await saveSetting('security', 'recaptcha', settingsValue)
303
304 if (success) {
305 // Update the allSettings state with the new values
306 updateSettingInState('security', 'recaptcha', settingsValue)
307
308 // Reset unsaved changes after successful save
309 entityManager.resetUnsavedChanges()
310 }
311
312 return success
313 }
314
315 // Save general settings
316 const saveGeneralSettings = async (): Promise<boolean> => {
317 const success = await saveSetting('general', 'wcagBackend', wcagBackendCompliance.value)
318
319 if (success) {
320 // Update the allSettings state with the new values
321 updateSettingInState('general', 'wcagBackend', wcagBackendCompliance.value)
322
323 // Reset unsaved changes after successful save
324 entityManager.resetUnsavedChanges()
325 }
326
327 return success
328 }
329
330 // Basic reCAPTCHA key format validation
331 const validateRecaptchaKeyFormat = (key: string): boolean => {
332 // Allow empty keys (for clearing/resetting settings)
333 if (!key || key.trim() === '') {
334 return true
335 }
336
337 const trimmedKey = key.trim()
338
339 // reCAPTCHA keys are typically 40 characters long, but can vary
340 if (trimmedKey.length < 20 || trimmedKey.length > 100) {
341 return false
342 }
343
344 // Basic format validation - should contain alphanumeric characters and some special chars
345 return /^[A-Za-z0-9_-]+$/.test(trimmedKey)
346 }
347
348 // Reset changes - following the pattern from useFormBuilder
349 const resetChanges = () => {
350 entityManager.resetUnsavedChanges()
351 }
352
353 // Integration settings methods
354 /**
355 * Get integration settings dynamically by name
356 */
357 const getIntegrationSettings = (integrationName: string) => {
358 return allSettings.value?.integrations?.[integrationName] || null
359 }
360
361 /**
362 * Check if integration is enabled
363 */
364 const isIntegrationEnabled = (integrationName: string): boolean => {
365 return allSettings.value?.integrations?.[integrationName]?.enabled || false
366 }
367
368 /**
369 * Update integration enabled status (simple toggle)
370 */
371 const updateIntegrationEnabled = async (
372 integrationName: string,
373 enabled: boolean,
374 ): Promise<boolean> => {
375 const integrationValue = { enabled }
376 const success = await saveSetting('integrations', integrationName, integrationValue)
377
378 if (success) {
379 updateSettingInState('integrations', integrationName, integrationValue)
380 }
381
382 return success
383 }
384
385 /**
386 * Update integration settings (for complex integrations with multiple settings)
387 */
388 const updateIntegrationSettings = async (
389 integrationName: string,
390 settings: Record<string, unknown>,
391 ): Promise<boolean> => {
392 const success = await saveSetting('integrations', integrationName, settings)
393
394 if (success) {
395 updateSettingInState('integrations', integrationName, settings)
396 }
397
398 return success
399 }
400
401 return {
402 // State
403 recaptchaType,
404 recaptchaSiteKey,
405 recaptchaSecretKey,
406 recaptchaLanguage,
407 wcagBackendCompliance,
408 isLoading,
409
410 // All Settings State
411 allSettings,
412
413 // Computed
414 recaptchaSettings,
415 isRecaptchaConfigured,
416 hasChanges,
417 getCurrentState: entityManager.getCurrentState,
418
419 // All Settings Computed
420 securitySettings,
421 generalSettings,
422 integrationsSettings,
423 currentRecaptchaSettings,
424 wpDataTablesIntegration,
425 // Future extensions:
426 // currentHCaptchaSettings,
427 // currentTurnstileSettings,
428
429 // Actions
430 setRecaptchaType,
431 setRecaptchaSiteKey,
432 setRecaptchaSecretKey,
433 setRecaptchaLanguage,
434 setWcagBackendCompliance,
435 setRecaptchaSettings,
436 loadRecaptchaSettings,
437 saveRecaptchaSettings,
438 saveGeneralSettings,
439 resetChanges,
440
441 // All Settings API
442 loadAllSettings,
443 updateSettingInState,
444
445 // Generic Settings API
446 loadSetting,
447 saveSetting,
448
449 // Integration Settings API
450 updateIntegrationEnabled,
451 getIntegrationSettings,
452 isIntegrationEnabled,
453 updateIntegrationSettings,
454
455 // Validation utilities
456 validateRecaptchaKeyFormat,
457 }
458 })
459