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

397 lines 11.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 { 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
58 // Specific settings getters
59 const currentRecaptchaSettings = computed(() => allSettings.value?.security?.recaptcha || null)
60 // Future extensions can be added here:
61 // const currentHCaptchaSettings = computed(() => allSettings.value?.security?.hcaptcha || null)
62 // const currentTurnstileSettings = computed(() => allSettings.value?.security?.turnstile || null)
63
64 const isRecaptchaConfigured = computed(() => {
65 // reCAPTCHA is configured only when both keys are present and valid
66 const siteKeyPresent = recaptchaSiteKey.value && recaptchaSiteKey.value.trim() !== ''
67 const secretKeyPresent = recaptchaSecretKey.value && recaptchaSecretKey.value.trim() !== ''
68
69 return (
70 siteKeyPresent &&
71 secretKeyPresent &&
72 validateRecaptchaKeyFormat(recaptchaSiteKey.value) &&
73 validateRecaptchaKeyFormat(recaptchaSecretKey.value)
74 )
75 })
76
77 const hasChanges = computed(() => {
78 return entityManager.hasUnsavedChanges().value
79 })
80
81 // Actions - following the same pattern as useFormBuilder
82 const setRecaptchaType = (type: RecaptchaType) => {
83 recaptchaType.value = type
84 }
85
86 const setRecaptchaSiteKey = (siteKey: string) => {
87 recaptchaSiteKey.value = siteKey
88 }
89
90 const setRecaptchaSecretKey = (secretKey: string) => {
91 recaptchaSecretKey.value = secretKey
92 }
93
94 const setRecaptchaLanguage = (language: string) => {
95 recaptchaLanguage.value = language
96 }
97
98 const setWcagBackendCompliance = (value: boolean) => {
99 wcagBackendCompliance.value = value
100 }
101
102 const setRecaptchaSettings = (settings: RecaptchaSettings) => {
103 recaptchaType.value = settings.type
104 recaptchaSiteKey.value = settings.siteKey
105 recaptchaSecretKey.value = settings.secretKey
106 // Set defaults for new fields if not provided
107 recaptchaLanguage.value = settings.language || ''
108 }
109
110 // Load all settings for admin page - single API call for better performance
111 const loadAllSettings = async (): Promise<boolean> => {
112 try {
113 isLoading.value = true
114 const { data, error, status } = await request('settings/all', {
115 method: 'GET',
116 headers: {
117 'Content-Type': 'application/json',
118 },
119 })
120
121 if (status === 200 && !error && data?.data?.data) {
122 allSettings.value = data.data.data
123
124 // Update individual reCAPTCHA refs for backward compatibility
125 const recaptcha = data.data.data.security?.recaptcha
126 if (recaptcha) {
127 recaptchaType.value = recaptcha.type || 'v2'
128 recaptchaSiteKey.value = recaptcha.siteKey || ''
129 recaptchaSecretKey.value = recaptcha.secretKey || ''
130 recaptchaLanguage.value = recaptcha.language || ''
131 }
132
133 // Update WCAG compliance setting
134 const general = data.data.data.general
135 if (general && general.wcagBackend !== undefined) {
136 wcagBackendCompliance.value = general.wcagBackend
137 }
138
139 // Reset unsaved changes after successful load
140 entityManager.resetUnsavedChanges()
141 return true
142 } else {
143 IvyMessage({
144 message: getLabel('failed_to_load_settings'),
145 type: 'error',
146 })
147 return false
148 }
149 } catch (error) {
150 IvyMessage({
151 message: getLabel('error_loading_settings'),
152 type: 'error',
153 })
154 console.error(getLabel('error_loading_settings'), error)
155 return false
156 } finally {
157 isLoading.value = false
158 }
159 }
160
161 // Generic settings loader - can be used for any category/option combination
162 const loadSetting = async (category: string, option: string) => {
163 try {
164 isLoading.value = true
165 const { data, error, status } = await request(`setting/${category}/${option}`, {
166 method: 'GET',
167 headers: {
168 'Content-Type': 'application/json',
169 },
170 })
171
172 if (status === 200 && !error && data?.data?.value) {
173 return data.data.value
174 } else {
175 IvyMessage({
176 message: getLabel('failed_to_load_settings'),
177 type: 'error',
178 })
179 return null
180 }
181 } catch (err) {
182 console.error(getLabel('failed_to_load_settings'), err)
183 IvyMessage({
184 message: getLabel('failed_to_load_settings'),
185 type: 'error',
186 })
187 return null
188 } finally {
189 isLoading.value = false
190 }
191 }
192
193 // Generic settings saver - can be used for any category/option combination
194 const saveSetting = async (
195 category: string,
196 option: string,
197 value: unknown,
198 ): Promise<boolean> => {
199 try {
200 isLoading.value = true
201 const { data, error, status } = await request('setting/update', {
202 method: 'POST',
203 headers: {
204 'Content-Type': 'application/json',
205 },
206 data: {
207 settingsCategory: category,
208 settingsOption: option,
209 settingsValue: value,
210 },
211 })
212
213 if (status === 200 && !error) {
214 IvyMessage({
215 message: getLabel('settings_saved'),
216 type: 'success',
217 })
218 return true
219 } else {
220 IvyMessage({
221 message: getLabel('failed_to_save_settings'),
222 type: 'error',
223 })
224 console.error(getLabel('failed_to_save_settings'), data?.message)
225 return false
226 }
227 } catch (error) {
228 IvyMessage({
229 message: getLabel('error_saving_settings'),
230 type: 'error',
231 })
232 console.error(getLabel('error_saving_settings'), error)
233 return false
234 } finally {
235 isLoading.value = false
236 }
237 }
238
239 // Update specific setting in allSettings state (for real-time updates)
240 const updateSettingInState = (category: keyof AllSettings, option: string, value: unknown) => {
241 if (allSettings.value) {
242 // Create a deep copy to trigger reactivity
243 const updatedSettings = { ...allSettings.value }
244 if (updatedSettings[category]) {
245 ;(updatedSettings[category] as Record<string, unknown>)[option] = value
246 allSettings.value = updatedSettings
247 }
248 }
249 }
250
251 // Load reCAPTCHA settings - following the async pattern from useFormBuilder
252 const loadRecaptchaSettings = async () => {
253 const settings = await loadSetting('security', 'recaptcha')
254
255 if (settings) {
256 recaptchaType.value = settings.type || 'v2'
257 recaptchaSiteKey.value = settings.siteKey || ''
258 recaptchaSecretKey.value = settings.secretKey || ''
259 recaptchaLanguage.value = settings.language || ''
260
261 // Reset unsaved changes after successful load
262 entityManager.resetUnsavedChanges()
263 }
264 }
265
266 // Save reCAPTCHA settings - following the async pattern from useFormBuilder
267 const saveRecaptchaSettings = async (): Promise<boolean> => {
268 // Basic frontend validation - only validate non-empty keys
269 const siteKeyValue = recaptchaSiteKey.value.trim()
270 const secretKeyValue = recaptchaSecretKey.value.trim()
271
272 // Validate both keys at once if they are provided
273 if (
274 (siteKeyValue && !validateRecaptchaKeyFormat(siteKeyValue)) ||
275 (secretKeyValue && !validateRecaptchaKeyFormat(secretKeyValue))
276 ) {
277 const errorMessage =
278 siteKeyValue && !validateRecaptchaKeyFormat(siteKeyValue)
279 ? getLabel('site_key_invalid')
280 : getLabel('secret_key_invalid')
281
282 IvyMessage({
283 message: errorMessage,
284 type: 'error',
285 })
286 return false
287 }
288
289 const settingsValue = {
290 type: recaptchaType.value,
291 siteKey: recaptchaSiteKey.value,
292 secretKey: recaptchaSecretKey.value,
293 language: recaptchaLanguage.value,
294 }
295
296 const success = await saveSetting('security', 'recaptcha', settingsValue)
297
298 if (success) {
299 // Update the allSettings state with the new values
300 updateSettingInState('security', 'recaptcha', settingsValue)
301
302 // Reset unsaved changes after successful save
303 entityManager.resetUnsavedChanges()
304 }
305
306 return success
307 }
308
309 // Save general settings
310 const saveGeneralSettings = async (): Promise<boolean> => {
311 const success = await saveSetting('general', 'wcagBackend', wcagBackendCompliance.value)
312
313 if (success) {
314 // Update the allSettings state with the new values
315 updateSettingInState('general', 'wcagBackend', wcagBackendCompliance.value)
316
317 // Reset unsaved changes after successful save
318 entityManager.resetUnsavedChanges()
319 }
320
321 return success
322 }
323
324 // Basic reCAPTCHA key format validation
325 const validateRecaptchaKeyFormat = (key: string): boolean => {
326 // Allow empty keys (for clearing/resetting settings)
327 if (!key || key.trim() === '') {
328 return true
329 }
330
331 const trimmedKey = key.trim()
332
333 // reCAPTCHA keys are typically 40 characters long, but can vary
334 if (trimmedKey.length < 20 || trimmedKey.length > 100) {
335 return false
336 }
337
338 // Basic format validation - should contain alphanumeric characters and some special chars
339 return /^[A-Za-z0-9_-]+$/.test(trimmedKey)
340 }
341
342 // Reset changes - following the pattern from useFormBuilder
343 const resetChanges = () => {
344 entityManager.resetUnsavedChanges()
345 }
346
347 return {
348 // State
349 recaptchaType,
350 recaptchaSiteKey,
351 recaptchaSecretKey,
352 recaptchaLanguage,
353 wcagBackendCompliance,
354 isLoading,
355
356 // All Settings State
357 allSettings,
358
359 // Computed
360 recaptchaSettings,
361 isRecaptchaConfigured,
362 hasChanges,
363 getCurrentState: entityManager.getCurrentState,
364
365 // All Settings Computed
366 securitySettings,
367 generalSettings,
368 currentRecaptchaSettings,
369 // Future extensions:
370 // currentHCaptchaSettings,
371 // currentTurnstileSettings,
372
373 // Actions
374 setRecaptchaType,
375 setRecaptchaSiteKey,
376 setRecaptchaSecretKey,
377 setRecaptchaLanguage,
378 setWcagBackendCompliance,
379 setRecaptchaSettings,
380 loadRecaptchaSettings,
381 saveRecaptchaSettings,
382 saveGeneralSettings,
383 resetChanges,
384
385 // All Settings API
386 loadAllSettings,
387 updateSettingInState,
388
389 // Generic Settings API
390 loadSetting,
391 saveSetting,
392
393 // Validation utilities
394 validateRecaptchaKeyFormat,
395 }
396 })
397