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

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