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

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