PluginProbe
The Innovative Form Builder – IvyForms / 0.7
The Innovative Form Builder – IvyForms v0.7
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.7, at frontend/src/stores/useSettingsStore.ts

572 lines 17.0 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 // Request deduplication - prevent multiple simultaneous calls
32 let loadPromise: Promise<boolean> | null = null
33
34 // Template favorites state
35 const favoriteTemplates = ref<string[]>([])
36
37 // Delete on uninstall state
38 const deleteOnUninstall = ref(false)
39
40 const { getLabel } = useLabels()
41 const { request } = useApiClient()
42
43 // Use unsaved changes store for state management
44 const unsavedChangesStore = useUnsavedChangesStore()
45
46 // Create complete entity manager for settings
47 const entityManager = unsavedChangesStore.createEntityManager('settingsBuilder', {
48 type: recaptchaType,
49 siteKey: recaptchaSiteKey,
50 secretKey: recaptchaSecretKey,
51 language: recaptchaLanguage,
52 wcagBackendCompliance: wcagBackendCompliance,
53 favoriteTemplates: favoriteTemplates,
54 deleteOnUninstall: deleteOnUninstall,
55 })
56
57 // Computed values
58 const recaptchaSettings = computed(() => ({
59 type: recaptchaType.value,
60 siteKey: recaptchaSiteKey.value,
61 secretKey: recaptchaSecretKey.value,
62 language: recaptchaLanguage.value,
63 }))
64
65 // Computed getters for all settings
66 const securitySettings = computed(() => allSettings.value?.security || null)
67 const generalSettings = computed(() => allSettings.value?.general || null)
68 const integrationsSettings = computed(() => allSettings.value?.integrations || null)
69
70 // Specific settings getters
71 const currentRecaptchaSettings = computed(() => allSettings.value?.security?.recaptcha || null)
72 // Future extensions can be added here:
73 // const currentHCaptchaSettings = computed(() => allSettings.value?.security?.hcaptcha || null)
74 // const currentTurnstileSettings = computed(() => allSettings.value?.security?.turnstile || null)
75
76 // Integration settings getters
77 const wpDataTablesIntegration = computed(
78 () => allSettings.value?.integrations?.wpdatatables || null,
79 )
80
81 const isRecaptchaConfigured = computed(() => {
82 // reCAPTCHA is configured only when both keys are present and valid
83 const siteKeyPresent = recaptchaSiteKey.value && recaptchaSiteKey.value.trim() !== ''
84 const secretKeyPresent = recaptchaSecretKey.value && recaptchaSecretKey.value.trim() !== ''
85
86 return (
87 siteKeyPresent &&
88 secretKeyPresent &&
89 validateRecaptchaKeyFormat(recaptchaSiteKey.value) &&
90 validateRecaptchaKeyFormat(recaptchaSecretKey.value)
91 )
92 })
93
94 const hasChanges = computed(() => {
95 return entityManager.hasUnsavedChanges().value
96 })
97
98 // Actions - following the same pattern as useFormBuilder
99 const setRecaptchaType = (type: RecaptchaType) => {
100 recaptchaType.value = type
101 }
102
103 const setRecaptchaSiteKey = (siteKey: string) => {
104 recaptchaSiteKey.value = siteKey
105 }
106
107 const setRecaptchaSecretKey = (secretKey: string) => {
108 recaptchaSecretKey.value = secretKey
109 }
110
111 const setRecaptchaLanguage = (language: string) => {
112 recaptchaLanguage.value = language
113 }
114
115 const setWcagBackendCompliance = (value: boolean) => {
116 wcagBackendCompliance.value = value
117 }
118
119 const setDeleteOnUninstall = (value: boolean) => {
120 deleteOnUninstall.value = value
121 }
122
123 const setRecaptchaSettings = (settings: RecaptchaSettings) => {
124 recaptchaType.value = settings.type
125 recaptchaSiteKey.value = settings.siteKey
126 recaptchaSecretKey.value = settings.secretKey
127 // Set defaults for new fields if not provided
128 recaptchaLanguage.value = settings.language || ''
129 }
130
131 // Load all settings for admin page - single API call for better performance
132 const loadAllSettings = async (): Promise<boolean> => {
133 // If there's already a request in progress, return it
134 if (loadPromise) {
135 return loadPromise
136 }
137
138 loadPromise = (async () => {
139 try {
140 isLoading.value = true
141 const { data, error, status } = await request('settings/all', {
142 method: 'GET',
143 headers: {
144 'Content-Type': 'application/json',
145 },
146 })
147
148 if (status === 200 && !error && data?.data?.data) {
149 allSettings.value = data.data.data
150
151 // Update individual reCAPTCHA refs for backward compatibility
152 const recaptcha = data.data.data.security?.recaptcha
153 if (recaptcha) {
154 recaptchaType.value = recaptcha.type || 'v2'
155 recaptchaSiteKey.value = recaptcha.siteKey || ''
156 recaptchaSecretKey.value = recaptcha.secretKey || ''
157 recaptchaLanguage.value = recaptcha.language || ''
158 }
159
160 // Update WCAG compliance setting
161 const general = data.data.data.general
162 if (general && general.wcagBackend !== undefined) {
163 wcagBackendCompliance.value = general.wcagBackend
164 }
165
166 // Update delete on uninstall setting
167 if (general && general.delete_on_uninstall !== undefined) {
168 deleteOnUninstall.value = general.delete_on_uninstall
169 }
170
171 // Update favorite templates
172 if (general && general.favoriteTemplates) {
173 favoriteTemplates.value = general.favoriteTemplates
174 }
175
176 // Reset unsaved changes after successful load
177 entityManager.resetUnsavedChanges()
178 return true
179 } else {
180 IvyMessage({
181 message: getLabel('failed_to_load_settings'),
182 type: 'error',
183 })
184 return false
185 }
186 } catch (error) {
187 IvyMessage({
188 message: getLabel('error_loading_settings'),
189 type: 'error',
190 })
191 console.error(getLabel('error_loading_settings'), error)
192 return false
193 } finally {
194 isLoading.value = false
195 loadPromise = null
196 }
197 })()
198
199 return loadPromise
200 }
201
202 // Generic settings loader - can be used for any category/option combination
203 const loadSetting = async (category: string, option: string) => {
204 try {
205 isLoading.value = true
206 const { data, error, status } = await request(`setting/${category}/${option}`, {
207 method: 'GET',
208 headers: {
209 'Content-Type': 'application/json',
210 },
211 })
212
213 if (status === 200 && !error && data?.data?.value) {
214 return data.data.value
215 } else {
216 IvyMessage({
217 message: getLabel('failed_to_load_settings'),
218 type: 'error',
219 })
220 return null
221 }
222 } catch (err) {
223 console.error(getLabel('failed_to_load_settings'), err)
224 IvyMessage({
225 message: getLabel('failed_to_load_settings'),
226 type: 'error',
227 })
228 return null
229 } finally {
230 isLoading.value = false
231 }
232 }
233
234 // Generic settings saver - can be used for any category/option combination
235 const saveSetting = async (
236 category: string,
237 option: string,
238 value: unknown,
239 showMessage: boolean = true,
240 ): Promise<boolean> => {
241 try {
242 isLoading.value = true
243 const { data, error, status } = await request('setting/update', {
244 method: 'POST',
245 headers: {
246 'Content-Type': 'application/json',
247 },
248 data: {
249 settingsCategory: category,
250 settingsOption: option,
251 settingsValue: value,
252 },
253 })
254
255 if (status === 200 && !error) {
256 if (showMessage) {
257 IvyMessage({
258 message: getLabel('settings_saved'),
259 type: 'success',
260 })
261 }
262 return true
263 } else {
264 if (showMessage) {
265 IvyMessage({
266 message: getLabel('failed_to_save_settings'),
267 type: 'error',
268 })
269 }
270 console.error(getLabel('failed_to_save_settings'), data?.message)
271 return false
272 }
273 } catch (error) {
274 if (showMessage) {
275 IvyMessage({
276 message: getLabel('error_saving_settings'),
277 type: 'error',
278 })
279 }
280 console.error(getLabel('error_saving_settings'), error)
281 return false
282 } finally {
283 isLoading.value = false
284 }
285 }
286
287 // Update specific setting in allSettings state (for real-time updates)
288 const updateSettingInState = (category: keyof AllSettings, option: string, value: unknown) => {
289 if (allSettings.value) {
290 // Create a deep copy to trigger reactivity
291 const updatedSettings = { ...allSettings.value }
292 if (updatedSettings[category]) {
293 ;(updatedSettings[category] as Record<string, unknown>)[option] = value
294 allSettings.value = updatedSettings
295 }
296 }
297 }
298
299 // Load reCAPTCHA settings - following the async pattern from useFormBuilder
300 const loadRecaptchaSettings = async () => {
301 const settings = await loadSetting('security', 'recaptcha')
302
303 if (settings) {
304 recaptchaType.value = settings.type || 'v2'
305 recaptchaSiteKey.value = settings.siteKey || ''
306 recaptchaSecretKey.value = settings.secretKey || ''
307 recaptchaLanguage.value = settings.language || ''
308
309 // Reset unsaved changes after successful load
310 entityManager.resetUnsavedChanges()
311 }
312 }
313
314 // Save reCAPTCHA settings - following the async pattern from useFormBuilder
315 const saveRecaptchaSettings = async (): Promise<boolean> => {
316 // Basic frontend validation - only validate non-empty keys
317 const siteKeyValue = recaptchaSiteKey.value.trim()
318 const secretKeyValue = recaptchaSecretKey.value.trim()
319
320 // Validate both keys at once if they are provided
321 if (
322 (siteKeyValue && !validateRecaptchaKeyFormat(siteKeyValue)) ||
323 (secretKeyValue && !validateRecaptchaKeyFormat(secretKeyValue))
324 ) {
325 const errorMessage =
326 siteKeyValue && !validateRecaptchaKeyFormat(siteKeyValue)
327 ? getLabel('site_key_invalid')
328 : getLabel('secret_key_invalid')
329
330 IvyMessage({
331 message: errorMessage,
332 type: 'error',
333 })
334 return false
335 }
336
337 const settingsValue = {
338 type: recaptchaType.value,
339 siteKey: recaptchaSiteKey.value,
340 secretKey: recaptchaSecretKey.value,
341 language: recaptchaLanguage.value,
342 }
343
344 const success = await saveSetting('security', 'recaptcha', settingsValue)
345
346 if (success) {
347 // Update the allSettings state with the new values
348 updateSettingInState('security', 'recaptcha', settingsValue)
349
350 // Reset unsaved changes after successful save
351 entityManager.resetUnsavedChanges()
352 }
353
354 return success
355 }
356
357 // Save general settings
358 const saveGeneralSettings = async (): Promise<boolean> => {
359 const wcagSuccess = await saveSetting(
360 'general',
361 'wcagBackend',
362 wcagBackendCompliance.value,
363 false,
364 )
365 const deleteSuccess = await saveSetting(
366 'general',
367 'delete_on_uninstall',
368 deleteOnUninstall.value,
369 )
370
371 const success = wcagSuccess && deleteSuccess
372
373 if (success) {
374 // Update the allSettings state with the new values
375 updateSettingInState('general', 'wcagBackend', wcagBackendCompliance.value)
376 updateSettingInState('general', 'delete_on_uninstall', deleteOnUninstall.value)
377
378 // Reset unsaved changes after successful save
379 entityManager.resetUnsavedChanges()
380 }
381
382 return success
383 }
384
385 // Basic reCAPTCHA key format validation
386 const validateRecaptchaKeyFormat = (key: string): boolean => {
387 // Allow empty keys (for clearing/resetting settings)
388 if (!key || key.trim() === '') {
389 return true
390 }
391
392 const trimmedKey = key.trim()
393
394 // reCAPTCHA keys are typically 40 characters long, but can vary
395 if (trimmedKey.length < 20 || trimmedKey.length > 100) {
396 return false
397 }
398
399 // Basic format validation - should contain alphanumeric characters and some special chars
400 return /^[A-Za-z0-9_-]+$/.test(trimmedKey)
401 }
402
403 // Reset changes - following the pattern from useFormBuilder
404 const resetChanges = () => {
405 entityManager.resetUnsavedChanges()
406 }
407
408 // Integration settings methods
409 /**
410 * Get integration settings dynamically by name
411 */
412 const getIntegrationSettings = (integrationName: string) => {
413 return allSettings.value?.integrations?.[integrationName] || null
414 }
415
416 /**
417 * Check if integration is enabled
418 */
419 const isIntegrationEnabled = (integrationName: string): boolean => {
420 return allSettings.value?.integrations?.[integrationName]?.enabled || false
421 }
422
423 /**
424 * Update integration enabled status (simple toggle)
425 */
426 const updateIntegrationEnabled = async (
427 integrationName: string,
428 enabled: boolean,
429 ): Promise<boolean> => {
430 const integrationValue = { enabled }
431 const success = await saveSetting('integrations', integrationName, integrationValue)
432
433 if (success) {
434 updateSettingInState('integrations', integrationName, integrationValue)
435 }
436
437 return success
438 }
439
440 /**
441 * Update integration settings (for complex integrations with multiple settings)
442 */
443 const updateIntegrationSettings = async (
444 integrationName: string,
445 settings: Record<string, unknown>,
446 ): Promise<boolean> => {
447 const success = await saveSetting('integrations', integrationName, settings)
448
449 if (success) {
450 updateSettingInState('integrations', integrationName, settings)
451 }
452
453 return success
454 }
455
456 /**
457 * Toggle a template as favorite
458 */
459 const toggleFavoriteTemplate = async (templateId: string): Promise<boolean> => {
460 const index = favoriteTemplates.value.indexOf(templateId)
461 if (index > -1) {
462 // Remove from favorites
463 favoriteTemplates.value = favoriteTemplates.value.filter((id) => id !== templateId)
464 } else {
465 // Add to favorites
466 favoriteTemplates.value = [...favoriteTemplates.value, templateId]
467 }
468 // Save to backend
469 const success = await saveSetting('general', 'favoriteTemplates', favoriteTemplates.value)
470 if (success) {
471 updateSettingInState('general', 'favoriteTemplates', favoriteTemplates.value)
472 }
473 return success
474 }
475
476 /**
477 * Check if a template is favorited
478 */
479 const isTemplateFavorited = (templateId: string): boolean => {
480 return favoriteTemplates.value.includes(templateId)
481 }
482
483 /**
484 * Load favorite templates
485 */
486 const loadFavoriteTemplates = async () => {
487 // If allSettings is already loaded, use it
488 if (allSettings.value?.general?.favoriteTemplates) {
489 favoriteTemplates.value = allSettings.value.general.favoriteTemplates as string[]
490 return
491 }
492
493 // Otherwise, load from individual endpoint
494 const favorites = await loadSetting('general', 'favoriteTemplates')
495 if (favorites && Array.isArray(favorites)) {
496 favoriteTemplates.value = favorites as string[]
497 } else if (favorites === null) {
498 // If it returns null, try loading all settings
499 await loadAllSettings()
500 if (allSettings.value?.general?.favoriteTemplates) {
501 favoriteTemplates.value = allSettings.value.general.favoriteTemplates as string[]
502 }
503 }
504 }
505
506 return {
507 // State
508 recaptchaType,
509 recaptchaSiteKey,
510 recaptchaSecretKey,
511 recaptchaLanguage,
512 wcagBackendCompliance,
513 favoriteTemplates,
514 deleteOnUninstall,
515 isLoading,
516
517 // All Settings State
518 allSettings,
519
520 // Computed
521 recaptchaSettings,
522 isRecaptchaConfigured,
523 hasChanges,
524 getCurrentState: entityManager.getCurrentState,
525
526 // All Settings Computed
527 securitySettings,
528 generalSettings,
529 integrationsSettings,
530 currentRecaptchaSettings,
531 wpDataTablesIntegration,
532 // Future extensions:
533 // currentHCaptchaSettings,
534 // currentTurnstileSettings,
535
536 // Actions
537 setRecaptchaType,
538 setRecaptchaSiteKey,
539 setRecaptchaSecretKey,
540 setRecaptchaLanguage,
541 setWcagBackendCompliance,
542 setDeleteOnUninstall,
543 setRecaptchaSettings,
544 loadRecaptchaSettings,
545 saveRecaptchaSettings,
546 saveGeneralSettings,
547 resetChanges,
548
549 // All Settings API
550 loadAllSettings,
551 updateSettingInState,
552
553 // Generic Settings API
554 loadSetting,
555 saveSetting,
556
557 // Integration Settings API
558 updateIntegrationEnabled,
559 getIntegrationSettings,
560 isIntegrationEnabled,
561 updateIntegrationSettings,
562
563 // Template Favorites API
564 toggleFavoriteTemplate,
565 isTemplateFavorited,
566 loadFavoriteTemplates,
567
568 // Validation utilities
569 validateRecaptchaKeyFormat,
570 }
571 })
572