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

759 lines 22.9 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 IvyMessage from '@/views/_components/message/ivyMessage.ts'
6 import type { RecaptchaSettings } from '@/types/recaptcha/recaptcha-interface'
7 import type { RecaptchaType } from '@/types/recaptcha/recaptcha-type'
8 import type { TurnstileSettings } from '@/types/turnstile/turnstile-interface'
9 import type { TurnstileTheme } from '@/types/turnstile/turnstile-type'
10 import type { HCaptchaSettings } from '@/types/hcaptcha/hcaptcha-interface'
11 import type { HCaptchaType } from '@/types/hcaptcha/hcaptcha-type'
12 import type { AllSettings } from '@/types/settings'
13
14 /**
15 * Settings store for managing application settings
16 * This store handles reCAPTCHA settings and other security/general settings
17 */
18 export const useSettingsStore = defineStore('settingsStore', () => {
19 // State using refs (composition API style like useFormBuilder)
20
21 // All settings state
22 const allSettings = ref<AllSettings | null>(null)
23 const isLoading = ref(false)
24
25 // reCAPTCHA specific state (for backward compatibility)
26 const recaptchaType = ref<RecaptchaType>('v2')
27 const recaptchaSiteKey = ref('')
28 const recaptchaSecretKey = ref('')
29 const recaptchaLanguage = ref('')
30
31 // Turnstile specific state
32 const turnstileSiteKey = ref('')
33 const turnstileSecretKey = ref('')
34 const turnstileTheme = ref<TurnstileTheme>('auto')
35
36 // hCaptcha specific state
37 const hcaptchaType = ref<HCaptchaType>('checkbox')
38 const hcaptchaSiteKey = ref('')
39 const hcaptchaSecretKey = ref('')
40
41 // WCAG Compliance state
42 const wcagBackendCompliance = ref(false)
43
44 // Request deduplication - prevent multiple simultaneous calls
45 let loadPromise: Promise<boolean> | null = null
46
47 // Template favorites state
48 const favoriteTemplates = ref<string[]>([])
49
50 // Delete on uninstall state
51 const deleteOnUninstall = ref(false)
52
53 const { getLabel } = useLabels()
54 const { request } = useApiClient()
55
56 // Computed values
57 const recaptchaSettings = computed(() => ({
58 type: recaptchaType.value,
59 siteKey: recaptchaSiteKey.value,
60 secretKey: recaptchaSecretKey.value,
61 language: recaptchaLanguage.value,
62 }))
63
64 const turnstileSettings = computed(() => ({
65 siteKey: turnstileSiteKey.value,
66 secretKey: turnstileSecretKey.value,
67 theme: turnstileTheme.value,
68 }))
69
70 const hcaptchaSettings = computed(() => ({
71 type: hcaptchaType.value,
72 siteKey: hcaptchaSiteKey.value,
73 secretKey: hcaptchaSecretKey.value,
74 }))
75
76 // Computed getters for all settings
77 const securitySettings = computed(() => allSettings.value?.security || null)
78 const generalSettings = computed(() => allSettings.value?.general || null)
79 const integrationsSettings = computed(() => allSettings.value?.integrations || null)
80
81 // Specific settings getters
82 const currentRecaptchaSettings = computed(() => allSettings.value?.security?.recaptcha || null)
83 const currentTurnstileSettings = computed(() => allSettings.value?.security?.turnstile || null)
84 const currentHCaptchaSettings = computed(() => allSettings.value?.security?.hcaptcha || null)
85
86 // Integration settings getters
87 const wpDataTablesIntegration = computed(
88 () => allSettings.value?.integrations?.wpdatatables || null,
89 )
90
91 const isRecaptchaConfigured = computed(() => {
92 // reCAPTCHA is configured only when both keys are present and valid
93 const siteKeyPresent = recaptchaSiteKey.value && recaptchaSiteKey.value.trim() !== ''
94 const secretKeyPresent = recaptchaSecretKey.value && recaptchaSecretKey.value.trim() !== ''
95
96 return (
97 siteKeyPresent &&
98 secretKeyPresent &&
99 validateCaptchaKeyFormat(recaptchaSiteKey.value) &&
100 validateCaptchaKeyFormat(recaptchaSecretKey.value)
101 )
102 })
103
104 const isTurnstileConfigured = computed(() => {
105 // Turnstile is configured only when both keys are present and valid
106 const siteKeyPresent = turnstileSiteKey.value && turnstileSiteKey.value.trim() !== ''
107 const secretKeyPresent = turnstileSecretKey.value && turnstileSecretKey.value.trim() !== ''
108
109 return (
110 siteKeyPresent &&
111 secretKeyPresent &&
112 validateCaptchaKeyFormat(turnstileSiteKey.value) && // Turnstile uses similar format
113 validateCaptchaKeyFormat(turnstileSecretKey.value)
114 )
115 })
116
117 const isHCaptchaConfigured = computed(() => {
118 // hCaptcha is configured only when both keys are present and valid
119 const siteKeyPresent = hcaptchaSiteKey.value && hcaptchaSiteKey.value.trim() !== ''
120 const secretKeyPresent = hcaptchaSecretKey.value && hcaptchaSecretKey.value.trim() !== ''
121
122 return (
123 siteKeyPresent &&
124 secretKeyPresent &&
125 validateCaptchaKeyFormat(hcaptchaSiteKey.value) && // hCaptcha uses similar format
126 validateCaptchaKeyFormat(hcaptchaSecretKey.value)
127 )
128 })
129
130 // Actions - following the same pattern as useFormBuilder
131 const setRecaptchaType = (type: RecaptchaType) => {
132 recaptchaType.value = type
133 }
134
135 const setRecaptchaSiteKey = (siteKey: string) => {
136 recaptchaSiteKey.value = siteKey
137 }
138
139 const setRecaptchaSecretKey = (secretKey: string) => {
140 recaptchaSecretKey.value = secretKey
141 }
142
143 const setRecaptchaLanguage = (language: string) => {
144 recaptchaLanguage.value = language
145 }
146
147 const setTurnstileSiteKey = (siteKey: string) => {
148 turnstileSiteKey.value = siteKey
149 }
150
151 const setTurnstileSecretKey = (secretKey: string) => {
152 turnstileSecretKey.value = secretKey
153 }
154
155 const setTurnstileTheme = (theme: TurnstileTheme) => {
156 turnstileTheme.value = theme
157 }
158
159 const setHCaptchaType = (type: HCaptchaType) => {
160 hcaptchaType.value = type
161 }
162
163 const setHCaptchaSiteKey = (siteKey: string) => {
164 hcaptchaSiteKey.value = siteKey
165 }
166
167 const setHCaptchaSecretKey = (secretKey: string) => {
168 hcaptchaSecretKey.value = secretKey
169 }
170
171 const setWcagBackendCompliance = (value: boolean) => {
172 wcagBackendCompliance.value = value
173 }
174
175 const setDeleteOnUninstall = (value: boolean) => {
176 deleteOnUninstall.value = value
177 }
178
179 const setRecaptchaSettings = (settings: RecaptchaSettings) => {
180 recaptchaType.value = settings.type
181 recaptchaSiteKey.value = settings.siteKey
182 recaptchaSecretKey.value = settings.secretKey
183 // Set defaults for new fields if not provided
184 recaptchaLanguage.value = settings.language || ''
185 }
186
187 const setTurnstileSettings = (settings: TurnstileSettings) => {
188 turnstileSiteKey.value = settings.siteKey
189 turnstileSecretKey.value = settings.secretKey
190 // Set defaults for new fields if not provided
191 turnstileTheme.value = settings.theme || 'auto'
192 }
193
194 const setHCaptchaSettings = (settings: HCaptchaSettings) => {
195 hcaptchaType.value = settings.type
196 hcaptchaSiteKey.value = settings.siteKey
197 hcaptchaSecretKey.value = settings.secretKey
198 }
199
200 // Load all settings for admin page - single API call for better performance
201 const loadAllSettings = async (): Promise<boolean> => {
202 // If there's already a request in progress, return it
203 if (loadPromise) {
204 return loadPromise
205 }
206
207 loadPromise = (async () => {
208 try {
209 isLoading.value = true
210 const { data, error, status } = await request('settings/all', {
211 method: 'GET',
212 headers: {
213 'Content-Type': 'application/json',
214 },
215 })
216
217 if (status === 200 && !error && data?.data?.data) {
218 allSettings.value = data.data.data
219
220 // Update individual reCAPTCHA refs for backward compatibility
221 const recaptcha = data.data.data.security?.recaptcha
222 if (recaptcha) {
223 recaptchaType.value = recaptcha.type || 'v2'
224 recaptchaSiteKey.value = recaptcha.siteKey || ''
225 recaptchaSecretKey.value = recaptcha.secretKey || ''
226 recaptchaLanguage.value = recaptcha.language || ''
227 }
228
229 // Update individual Turnstile refs
230 const turnstile = data.data.data.security?.turnstile
231 if (turnstile) {
232 turnstileSiteKey.value = turnstile.siteKey || ''
233 turnstileSecretKey.value = turnstile.secretKey || ''
234 turnstileTheme.value = turnstile.theme || 'auto'
235 }
236
237 // Update individual hCaptcha refs
238 const hcaptcha = data.data.data.security?.hcaptcha
239 if (hcaptcha) {
240 hcaptchaType.value = hcaptcha.type || 'checkbox'
241 hcaptchaSiteKey.value = hcaptcha.siteKey || ''
242 hcaptchaSecretKey.value = hcaptcha.secretKey || ''
243 }
244
245 // Update WCAG compliance setting
246 const general = data.data.data.general
247 if (general && general.wcagBackend !== undefined) {
248 wcagBackendCompliance.value = general.wcagBackend
249 }
250
251 // Update delete on uninstall setting
252 if (general && general.delete_on_uninstall !== undefined) {
253 deleteOnUninstall.value = general.delete_on_uninstall
254 }
255
256 // Update favorite templates
257 if (general && general.favoriteTemplates) {
258 favoriteTemplates.value = general.favoriteTemplates
259 }
260
261 return true
262 } else {
263 IvyMessage({
264 message: getLabel('failed_to_load_settings'),
265 type: 'error',
266 })
267 return false
268 }
269 } catch (error) {
270 IvyMessage({
271 message: getLabel('error_loading_settings'),
272 type: 'error',
273 })
274 console.error(getLabel('error_loading_settings'), error)
275 return false
276 } finally {
277 isLoading.value = false
278 loadPromise = null
279 }
280 })()
281
282 return loadPromise
283 }
284
285 // Generic settings loader - can be used for any category/option combination
286 const loadSetting = async (category: string, option: string) => {
287 try {
288 isLoading.value = true
289 const { data, error, status } = await request(`setting/${category}/${option}`, {
290 method: 'GET',
291 headers: {
292 'Content-Type': 'application/json',
293 },
294 })
295
296 if (status === 200 && !error && data?.data?.value) {
297 return data.data.value
298 } else {
299 IvyMessage({
300 message: getLabel('failed_to_load_settings'),
301 type: 'error',
302 })
303 return null
304 }
305 } catch (err) {
306 console.error(getLabel('failed_to_load_settings'), err)
307 IvyMessage({
308 message: getLabel('failed_to_load_settings'),
309 type: 'error',
310 })
311 return null
312 } finally {
313 isLoading.value = false
314 }
315 }
316
317 // Generic settings saver - can be used for any category/option combination
318 const saveSetting = async (
319 category: string,
320 option: string,
321 value: unknown,
322 showMessage: boolean = true,
323 ): Promise<boolean> => {
324 try {
325 isLoading.value = true
326 const { data, error, status } = await request('setting/update', {
327 method: 'POST',
328 headers: {
329 'Content-Type': 'application/json',
330 },
331 data: {
332 settingsCategory: category,
333 settingsOption: option,
334 settingsValue: value,
335 },
336 })
337
338 if (status === 200 && !error) {
339 if (showMessage) {
340 IvyMessage({
341 message: getLabel('settings_saved'),
342 type: 'success',
343 })
344 }
345 return true
346 } else {
347 if (showMessage) {
348 IvyMessage({
349 message: getLabel('failed_to_save_settings'),
350 type: 'error',
351 })
352 }
353 console.error(getLabel('failed_to_save_settings'), data?.message)
354 return false
355 }
356 } catch (error) {
357 if (showMessage) {
358 IvyMessage({
359 message: getLabel('error_saving_settings'),
360 type: 'error',
361 })
362 }
363 console.error(getLabel('error_saving_settings'), error)
364 return false
365 } finally {
366 isLoading.value = false
367 }
368 }
369
370 // Update specific setting in allSettings state (for real-time updates)
371 const updateSettingInState = (category: keyof AllSettings, option: string, value: unknown) => {
372 if (allSettings.value) {
373 // Create a deep copy to trigger reactivity
374 const updatedSettings = { ...allSettings.value }
375 if (updatedSettings[category]) {
376 ;(updatedSettings[category] as Record<string, unknown>)[option] = value
377 allSettings.value = updatedSettings
378 }
379 }
380 }
381
382 // Load reCAPTCHA settings - following the async pattern from useFormBuilder
383 const loadRecaptchaSettings = async () => {
384 const settings = await loadSetting('security', 'recaptcha')
385
386 if (settings) {
387 recaptchaType.value = settings.type || 'v2'
388 recaptchaSiteKey.value = settings.siteKey || ''
389 recaptchaSecretKey.value = settings.secretKey || ''
390 recaptchaLanguage.value = settings.language || ''
391 }
392 }
393
394 // Save reCAPTCHA settings - following the async pattern from useFormBuilder
395 const saveRecaptchaSettings = async (): Promise<boolean> => {
396 // Basic frontend validation - only validate non-empty keys
397 const siteKeyValue = recaptchaSiteKey.value.trim()
398 const secretKeyValue = recaptchaSecretKey.value.trim()
399
400 // Validate both keys at once if they are provided
401 if (
402 (siteKeyValue && !validateCaptchaKeyFormat(siteKeyValue)) ||
403 (secretKeyValue && !validateCaptchaKeyFormat(secretKeyValue))
404 ) {
405 const errorMessage =
406 siteKeyValue && !validateCaptchaKeyFormat(siteKeyValue)
407 ? getLabel('site_key_invalid')
408 : getLabel('secret_key_invalid')
409
410 IvyMessage({
411 message: errorMessage,
412 type: 'error',
413 })
414 return false
415 }
416
417 const settingsValue = {
418 type: recaptchaType.value,
419 siteKey: recaptchaSiteKey.value,
420 secretKey: recaptchaSecretKey.value,
421 language: recaptchaLanguage.value,
422 }
423
424 const success = await saveSetting('security', 'recaptcha', settingsValue)
425
426 if (success) {
427 // Update the allSettings state with the new values
428 updateSettingInState('security', 'recaptcha', settingsValue)
429 }
430
431 return success
432 }
433
434 // Load Turnstile settings - following the async pattern from useFormBuilder
435 const loadTurnstileSettings = async () => {
436 const settings = await loadSetting('security', 'turnstile')
437
438 if (settings) {
439 turnstileSiteKey.value = settings.siteKey || ''
440 turnstileSecretKey.value = settings.secretKey || ''
441 turnstileTheme.value = settings.theme || 'auto'
442 }
443 }
444
445 // Load hCaptcha settings - following the async pattern from useFormBuilder
446 const loadHCaptchaSettings = async () => {
447 const settings = await loadSetting('security', 'hcaptcha')
448
449 if (settings) {
450 hcaptchaType.value = settings.type || 'checkbox'
451 hcaptchaSiteKey.value = settings.siteKey || ''
452 hcaptchaSecretKey.value = settings.secretKey || ''
453 }
454 }
455
456 // Save Turnstile settings - following the async pattern from useFormBuilder
457 const saveTurnstileSettings = async (): Promise<boolean> => {
458 // Basic frontend validation - only validate non-empty keys
459 const siteKeyValue = turnstileSiteKey.value.trim()
460 const secretKeyValue = turnstileSecretKey.value.trim()
461
462 // Validate both keys at once if they are provided
463 if (
464 (siteKeyValue && !validateCaptchaKeyFormat(siteKeyValue)) ||
465 (secretKeyValue && !validateCaptchaKeyFormat(secretKeyValue))
466 ) {
467 const errorMessage =
468 siteKeyValue && !validateCaptchaKeyFormat(siteKeyValue)
469 ? getLabel('site_key_invalid')
470 : getLabel('secret_key_invalid')
471
472 IvyMessage({
473 message: errorMessage,
474 type: 'error',
475 })
476 return false
477 }
478
479 const settingsValue = {
480 siteKey: turnstileSiteKey.value,
481 secretKey: turnstileSecretKey.value,
482 theme: turnstileTheme.value,
483 }
484
485 const success = await saveSetting('security', 'turnstile', settingsValue)
486
487 if (success) {
488 // Update the allSettings state with the new values
489 updateSettingInState('security', 'turnstile', settingsValue)
490 }
491
492 return success
493 }
494
495 // Save hCaptcha settings - following the async pattern from useFormBuilder
496 const saveHCaptchaSettings = async (): Promise<boolean> => {
497 // Basic frontend validation - only validate non-empty keys
498 const siteKeyValue = hcaptchaSiteKey.value.trim()
499 const secretKeyValue = hcaptchaSecretKey.value.trim()
500
501 // Validate both keys at once if they are provided
502 if (
503 (siteKeyValue && !validateCaptchaKeyFormat(siteKeyValue)) ||
504 (secretKeyValue && !validateCaptchaKeyFormat(secretKeyValue))
505 ) {
506 const errorMessage =
507 siteKeyValue && !validateCaptchaKeyFormat(siteKeyValue)
508 ? getLabel('site_key_invalid')
509 : getLabel('secret_key_invalid')
510
511 IvyMessage({
512 message: errorMessage,
513 type: 'error',
514 })
515 return false
516 }
517
518 const settingsValue = {
519 type: hcaptchaType.value,
520 siteKey: hcaptchaSiteKey.value,
521 secretKey: hcaptchaSecretKey.value,
522 }
523
524 const success = await saveSetting('security', 'hcaptcha', settingsValue)
525
526 if (success) {
527 // Update the allSettings state with the new values
528 updateSettingInState('security', 'hcaptcha', settingsValue)
529 }
530
531 return success
532 }
533
534 // Save general settings
535 const saveGeneralSettings = async (): Promise<boolean> => {
536 const wcagSuccess = await saveSetting(
537 'general',
538 'wcagBackend',
539 wcagBackendCompliance.value,
540 false,
541 )
542 const deleteSuccess = await saveSetting(
543 'general',
544 'delete_on_uninstall',
545 deleteOnUninstall.value,
546 )
547
548 const success = wcagSuccess && deleteSuccess
549
550 if (success) {
551 // Update the allSettings state with the new values
552 updateSettingInState('general', 'wcagBackend', wcagBackendCompliance.value)
553 updateSettingInState('general', 'delete_on_uninstall', deleteOnUninstall.value)
554 }
555
556 return success
557 }
558
559 // Basic captcha key format validation
560 const validateCaptchaKeyFormat = (key: string): boolean => {
561 // Allow empty keys (for clearing/resetting settings)
562 if (!key || key.trim() === '') {
563 return true
564 }
565
566 const trimmedKey = key.trim()
567
568 // captcha keys are typically 40 characters long, but can vary
569 if (trimmedKey.length < 20 || trimmedKey.length > 100) {
570 return false
571 }
572
573 // Basic format validation - should contain alphanumeric characters and some special chars
574 return /^[A-Za-z0-9_-]+$/.test(trimmedKey)
575 }
576
577 // Integration settings methods
578 /**
579 * Get integration settings dynamically by name
580 */
581 const getIntegrationSettings = (integrationName: string) => {
582 return allSettings.value?.integrations?.[integrationName] || null
583 }
584
585 /**
586 * Check if integration is enabled
587 */
588 const isIntegrationEnabled = (integrationName: string): boolean => {
589 return allSettings.value?.integrations?.[integrationName]?.enabled || false
590 }
591
592 /**
593 * Update integration enabled status (simple toggle)
594 */
595 const updateIntegrationEnabled = async (
596 integrationName: string,
597 enabled: boolean,
598 ): Promise<boolean> => {
599 const integrationValue = { enabled }
600 const success = await saveSetting('integrations', integrationName, integrationValue)
601
602 if (success) {
603 updateSettingInState('integrations', integrationName, integrationValue)
604 }
605
606 return success
607 }
608
609 /**
610 * Update integration settings (for complex integrations with multiple settings)
611 */
612 const updateIntegrationSettings = async (
613 integrationName: string,
614 settings: Record<string, unknown>,
615 ): Promise<boolean> => {
616 const success = await saveSetting('integrations', integrationName, settings)
617
618 if (success) {
619 updateSettingInState('integrations', integrationName, settings)
620 }
621
622 return success
623 }
624
625 /**
626 * Toggle a template as favorite
627 */
628 const toggleFavoriteTemplate = async (templateId: string): Promise<boolean> => {
629 const index = favoriteTemplates.value.indexOf(templateId)
630 if (index > -1) {
631 // Remove from favorites
632 favoriteTemplates.value = favoriteTemplates.value.filter((id) => id !== templateId)
633 } else {
634 // Add to favorites
635 favoriteTemplates.value = [...favoriteTemplates.value, templateId]
636 }
637 // Save to backend
638 const success = await saveSetting('general', 'favoriteTemplates', favoriteTemplates.value)
639 if (success) {
640 updateSettingInState('general', 'favoriteTemplates', favoriteTemplates.value)
641 }
642 return success
643 }
644
645 /**
646 * Check if a template is favorited
647 */
648 const isTemplateFavorited = (templateId: string): boolean => {
649 return favoriteTemplates.value.includes(templateId)
650 }
651
652 /**
653 * Load favorite templates
654 */
655 const loadFavoriteTemplates = async () => {
656 // If allSettings is already loaded, use it
657 if (allSettings.value?.general?.favoriteTemplates) {
658 favoriteTemplates.value = allSettings.value.general.favoriteTemplates as string[]
659 return
660 }
661
662 // Otherwise, load from individual endpoint
663 const favorites = await loadSetting('general', 'favoriteTemplates')
664 if (favorites && Array.isArray(favorites)) {
665 favoriteTemplates.value = favorites as string[]
666 } else if (favorites === null) {
667 // If it returns null, try loading all settings
668 await loadAllSettings()
669 if (allSettings.value?.general?.favoriteTemplates) {
670 favoriteTemplates.value = allSettings.value.general.favoriteTemplates as string[]
671 }
672 }
673 }
674
675 return {
676 // State
677 recaptchaType,
678 recaptchaSiteKey,
679 recaptchaSecretKey,
680 recaptchaLanguage,
681 turnstileSiteKey,
682 turnstileSecretKey,
683 turnstileTheme,
684 hcaptchaType,
685 hcaptchaSiteKey,
686 hcaptchaSecretKey,
687 wcagBackendCompliance,
688 favoriteTemplates,
689 deleteOnUninstall,
690 isLoading,
691
692 // All Settings State
693 allSettings,
694
695 // Computed
696 recaptchaSettings,
697 turnstileSettings,
698 hcaptchaSettings,
699 isRecaptchaConfigured,
700 isTurnstileConfigured,
701 isHCaptchaConfigured,
702
703 // All Settings Computed
704 securitySettings,
705 generalSettings,
706 integrationsSettings,
707 currentRecaptchaSettings,
708 currentTurnstileSettings,
709 currentHCaptchaSettings,
710 wpDataTablesIntegration,
711
712 // Actions
713 setRecaptchaType,
714 setRecaptchaSiteKey,
715 setRecaptchaSecretKey,
716 setRecaptchaLanguage,
717 setTurnstileSiteKey,
718 setTurnstileSecretKey,
719 setTurnstileTheme,
720 setHCaptchaType,
721 setHCaptchaSiteKey,
722 setHCaptchaSecretKey,
723 setWcagBackendCompliance,
724 setDeleteOnUninstall,
725 setRecaptchaSettings,
726 setTurnstileSettings,
727 setHCaptchaSettings,
728 loadRecaptchaSettings,
729 saveRecaptchaSettings,
730 loadTurnstileSettings,
731 saveTurnstileSettings,
732 loadHCaptchaSettings,
733 saveHCaptchaSettings,
734 saveGeneralSettings,
735
736 // All Settings API
737 loadAllSettings,
738 updateSettingInState,
739
740 // Generic Settings API
741 loadSetting,
742 saveSetting,
743
744 // Integration Settings API
745 updateIntegrationEnabled,
746 getIntegrationSettings,
747 isIntegrationEnabled,
748 updateIntegrationSettings,
749
750 // Template Favorites API
751 toggleFavoriteTemplate,
752 isTemplateFavorited,
753 loadFavoriteTemplates,
754
755 // Validation utilities
756 validateCaptchaKeyFormat,
757 }
758 })
759