PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.21
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.21
1.7.0 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 All 50 releases
fluent-cart / app / Modules / StorageDrivers / S3 / VueTemplates / SettingsPage.vue

SettingsPage.vue in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.3.21, at app/Modules/StorageDrivers/S3/VueTemplates/SettingsPage.vue

1,161 lines 50.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <template>
2 <div class="space-y-6">
3 <div v-if="fetching" class="fct-card fct-card-border overflow-hidden mb-0">
4 <div class="fct-card-body">
5 <el-skeleton animated :rows="6" />
6 </div>
7 </div>
8
9 <template v-else>
10 <div v-if="showDashboard" class="space-y-6">
11 <div class="fct-storage-activated-card">
12 <div class="activated-card-header">
13 <div class="activated-card-content">
14 <img
15 v-if="payload.logo"
16 :class="{ 'dark:hidden': payload.dark_logo }"
17 :src="payload.logo"
18 :alt="payload.driver_label"
19 />
20 <img
21 v-if="payload.dark_logo"
22 :src="payload.dark_logo"
23 :alt="payload.driver_label"
24 class="hidden dark:block"
25 />
26
27 <div
28 v-if="!payload.logo && !payload.dark_logo"
29 class="bucket-name-logo"
30 >
31 S3
32 </div>
33
34 <div class="activated-card-content-inner">
35 <div class="activated-card-bucket-name">
36 <h2 class="title">
37 {{ payload.driver_label }}
38 </h2>
39
40 <span :class="['badge small', form.is_active === 'yes' ? 'success' : 'info']">
41 {{ form.is_active === 'yes' ? translate('Active') : translate('Inactive') }}
42 </span>
43
44 <span class="badge small info">
45 {{ connectionMethodLabel }}
46 </span>
47 </div>
48
49 <p class="activated-card-bucket-desc">
50 {{ payload.description }}
51 </p>
52
53 <!-- <div class="mt-2 flex flex-wrap items-center gap-2 text-sm text-system-mid dark:text-gray-300 hidden">
54 <a
55 v-if="bucketConsoleUrl"
56 :href="bucketConsoleUrl"
57 target="_blank"
58 rel="noopener noreferrer"
59 class="font-medium text-primary-500 no-underline hover:underline dark:text-gray-50"
60 >
61 {{ form.bucket }}
62 </a>
63 <span v-else class="font-medium text-system-light dark:text-gray-400">
64 {{ translate('No bucket selected') }}
65 </span>
66 <span v-if="form.region" class="text-system-light dark:text-gray-400">/</span>
67 <span v-if="form.region">{{ formattedRegion }}</span>
68 </div> -->
69
70 <!-- btn group -->
71 <div class="fct-btn-group mt-3">
72 <el-button size="small" :loading="checking_connection" @click="checkConnection">
73 {{ translate('Check again') }}
74 </el-button>
75
76 <el-button size="small" @click="enableEditMode">
77 {{ translate('Edit Config') }}
78 </el-button>
79
80 <el-popconfirm
81 :title="translate('Are you sure you want to reset all settings? This cannot be undone.')"
82 :confirm-button-text="translate('Yes, Reset')"
83 :cancel-button-text="translate('No')"
84 confirm-button-type="danger"
85 @confirm="resetSettings"
86 >
87 <template #reference>
88 <el-button size="small" plain type="danger">
89 {{ translate('Reset') }}
90 </el-button>
91 </template>
92 </el-popconfirm>
93 </div>
94 </div>
95 </div>
96
97 <div class="shrink-0">
98 <el-switch
99 v-model="form.is_active"
100 active-value="yes"
101 inactive-value="no"
102 :loading="saving"
103 @change="handleSwitchChange"
104 />
105 </div>
106 </div>
107
108 <div class="activated-card-body">
109 <ul class="fct-activated-bucket-list-info">
110 <li>
111 <div class="info-title">
112 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">>
113 <path d="M4.66659 4.66667H11.1666C12.571 4.66667 13.2733 4.66667 13.7777 5.00373C13.9961 5.14964 14.1836 5.33715 14.3295 5.55553C14.6666 6.05997 14.6666 6.7622 14.6666 8.16667C14.6666 10.5074 14.6666 11.6778 14.1048 12.5186C13.8616 12.8825 13.5491 13.195 13.1852 13.4382C12.3444 14 11.174 14 8.83325 14H7.99992C4.85722 14 3.28587 14 2.30956 13.0237C1.33325 12.0474 1.33325 10.476 1.33325 7.33333V5.29618C1.33325 4.0852 1.33325 3.47971 1.5868 3.02538C1.76753 2.70151 2.03476 2.43428 2.35863 2.25354C2.81296 2 3.41845 2 4.62943 2C5.40527 2 5.79318 2 6.13276 2.12734C6.90807 2.41808 7.22778 3.12238 7.57763 3.82208L7.99992 4.66667" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
114 </svg>
115 {{ translate('Bucket') }}
116 </div>
117 <div class="info-value">
118 <a
119 v-if="bucketConsoleUrl"
120 :href="bucketConsoleUrl"
121 target="_blank"
122 rel="noopener noreferrer"
123 >
124 {{ form.bucket }}
125 </a>
126 <span v-else>
127 {{ translate('No bucket selected') }}
128 </span>
129 </div>
130 </li>
131
132 <li>
133 <div class="info-title">
134 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">>
135 <g clip-path="url(#clip0_22443_64899)">
136 <circle cx="7.99992" cy="7.9987" r="6.66667" stroke="currentColor" stroke-width="1.5"/>
137 <ellipse cx="7.99992" cy="7.9987" rx="2.66667" ry="6.66667" stroke="currentColor" stroke-width="1.5"/>
138 <path d="M1.33325 8H14.6666" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
139 </g>
140 <defs>
141 <clipPath id="clip0_22443_64899">
142 <rect width="16" height="16" fill="white"/>
143 </clipPath>
144 </defs>
145 </svg>
146 {{ translate('Region') }}
147 </div>
148 <div class="info-value">
149 {{ form.region ? formattedRegion : translate('Not selected') }}
150 </div>
151 </li>
152
153 <li>
154 <div class="info-title">
155 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">>
156 <g clip-path="url(#clip0_22443_64906)">
157 <path d="M4.66659 1.33203C2.82564 1.33203 1.33325 2.82442 1.33325 4.66536C1.33325 5.89917 2.00358 6.97641 2.99992 7.55276V11.8941C2.99992 12.4391 2.99992 12.7116 3.10141 12.9567C3.20291 13.2017 3.39559 13.3944 3.78097 13.7797L4.66659 14.6654L6.07205 13.2599C6.13687 13.1951 6.1693 13.1627 6.19618 13.1275C6.26679 13.0351 6.31203 12.9259 6.3274 12.8107C6.33325 12.7668 6.33325 12.721 6.33325 12.6293C6.33325 12.5551 6.33325 12.518 6.32932 12.482C6.31901 12.3874 6.28861 12.2962 6.24015 12.2144C6.22167 12.1833 6.19941 12.1536 6.15489 12.0942L5.33325 10.9987L5.79992 10.3765C6.06424 10.024 6.19641 9.84782 6.26483 9.64256C6.33325 9.43729 6.33325 9.21702 6.33325 8.77648V7.55276C7.32959 6.97641 7.99992 5.89917 7.99992 4.66536C7.99992 2.82442 6.50753 1.33203 4.66659 1.33203Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
158 <path d="M4.66675 4.66797H4.67274" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
159 <path d="M8.66675 9.33203H12.6667C13.288 9.33203 13.5986 9.33203 13.8437 9.43353C14.1704 9.56885 14.4299 9.82842 14.5653 10.1551C14.6667 10.4001 14.6667 10.7108 14.6667 11.332C14.6667 11.9533 14.6667 12.2639 14.5653 12.5089C14.4299 12.8356 14.1704 13.0952 13.8437 13.2305C13.5986 13.332 13.288 13.332 12.6667 13.332H8.66675" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
160 <path d="M10 3.33203H12.6667C13.2879 3.33203 13.5985 3.33203 13.8436 3.43353C14.1703 3.56885 14.4298 3.82842 14.5652 4.15512C14.6667 4.40015 14.6667 4.71078 14.6667 5.33203C14.6667 5.95329 14.6667 6.26391 14.5652 6.50894C14.4298 6.83565 14.1703 7.09521 13.8436 7.23054C13.5985 7.33203 13.2879 7.33203 12.6667 7.33203H10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
161 </g>
162 <defs>
163 <clipPath id="clip0_22443_64906">
164 <rect width="16" height="16" fill="white"/>
165 </clipPath>
166 </defs></svg>
167 {{ translate('Authentication') }}
168 </div>
169 <div class="info-value">
170 {{ connectionMethodLabel }}
171 </div>
172 </li>
173
174 <li>
175 <div class="info-title">
176 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">>
177 <g clip-path="url(#clip0_22443_64941)">
178 <path d="M7.99888 1.33203C5.9937 1.33203 4.69354 2.67803 3.15589 3.16863C2.53067 3.36812 2.21806 3.46786 2.09155 3.60846C1.96504 3.74907 1.92799 3.95453 1.8539 4.36545C1.06104 8.76269 2.79401 12.828 6.92694 14.4103C7.371 14.5804 7.59303 14.6654 8.00107 14.6654C8.40911 14.6654 8.63112 14.5804 9.07515 14.4103C13.2078 12.828 14.9392 8.76268 14.1461 4.36545C14.0719 3.95446 14.0349 3.74896 13.9083 3.60836C13.7818 3.46775 13.4692 3.36806 12.844 3.16869C11.3058 2.67813 10.0041 1.33203 7.99888 1.33203Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
179 <path d="M6 8.66667C6 8.66667 6.66667 8.66667 7.33333 10C7.33333 10 9.45098 6.66667 11.3333 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
180 </g>
181 <defs>
182 <clipPath id="clip0_22443_64941">
183 <rect width="16" height="16" fill="white"/>
184 </clipPath>
185 </defs></svg>
186 {{ translate('Security posture') }}
187 </div>
188 <div class="info-value">
189 {{ form.block_public_access === 'yes' ? translate('Public access blocked') : translate('Public access open') }},
190 {{ form.object_ownership === 'yes' ? translate('ownership enforced') : translate('ownership not enforced') }}
191
192 <span :class="['badge small', securityRecommendationLabel === translate('Recommended') ? 'success' : 'warning']">
193 {{ securityRecommendationLabel }}
194 </span>
195 </div>
196 </li>
197 </ul>
198 </div>
199
200
201 </div>
202 </div>
203
204 <div v-else class="space-y-6">
205 <el-alert
206 v-if="showMissingDefineCredentialsWarning"
207 :title="missingDefineCredentialsMessage"
208 type="warning"
209 show-icon
210 :closable="false"
211 />
212
213 <div class="fct-card">
214 <div class="fct-card-header border-bottom">
215 <StepIndicator
216 :steps="visibleSteps"
217 :current-step="stepperActive + 1"
218 @step-click="handleStepClick"
219 />
220 </div>
221
222 <div class="fct-card-body">
223 <div v-show="currentStep === 1" class="fct-step-content-box space-y-4">
224 <h4 class="fct-step-content-box-heading">
225 {{ translate('Verify Access Keys') }}
226 </h4>
227 <ul class="fct-coverage-selector">
228 <li
229 :class="{ active: form.auth_method === 'define' }"
230 @click="!form.defined_in_wp_config && (form.auth_method = 'define')"
231 >
232 <div class="w-full">
233 <div class="fct-coverage-selector-content-wrap">
234 <div class="fct-coverage-selector-content">
235 <span class="fct-coverage-selector-title">
236 {{ translate('Define access keys in wp-config.php') }}
237 <span class="opacity-50">{{ translate('(Recommended)') }}</span>
238 </span>
239 <span class="fct-coverage-selector-desc">
240 {{ form.defined_in_wp_config
241 ? translate('You have already defined your credentials in wp-config.php.')
242 : translate('Recommended. Keeps your credentials secure by defining them in your configuration file.') }}
243 </span>
244 </div>
245 <div class="fct-coverage-selector-dot-wrap shrink-0">
246 <span class="fct-coverage-selector-dot"></span>
247 </div>
248 </div>
249
250 <Animation :visible="form.auth_method === 'define'" accordion :duration="220">
251 <div class="mt-4">
252 <div class="space-y-4">
253 <div class="config-snippet">
254 <pre class="m-0 overflow-x-auto text-xs text-system-dark dark:text-gray-50">{{ wpConfigSnippet }}</pre>
255 </div>
256
257 <div class="flex justify-start">
258 <el-button @click.stop="copyCode">{{ translate('Copy Snippet') }}</el-button>
259 </div>
260 </div>
261 </div>
262 </Animation>
263 </div>
264 </li>
265
266 <li
267 :class="{ active: form.auth_method === 'db', 'opacity-60 cursor-not-allowed': form.defined_in_wp_config }"
268 @click="!form.defined_in_wp_config && (form.auth_method = 'db')"
269 >
270 <div class="w-full">
271 <div class="fct-coverage-selector-content-wrap">
272 <div class="fct-coverage-selector-content">
273 <span class="fct-coverage-selector-title">{{ translate('Store credentials in the database') }}</span>
274 <span class="fct-coverage-selector-desc">
275 {{ translate('Store credentials in the database. Less secure than other methods.') }}
276 </span>
277 <span v-if="form.defined_in_wp_config" class="mt-2 block text-sm text-system-mid dark:text-gray-300">
278 {{ translate('The database credential option is unavailable while your access keys are defined in wp-config.php.') }}
279 </span>
280 </div>
281 <div class="fct-coverage-selector-dot-wrap shrink-0">
282 <span class="fct-coverage-selector-dot"></span>
283 </div>
284 </div>
285
286 <Animation :visible="form.auth_method === 'db'" accordion :duration="220">
287 <div class="mt-4">
288 <div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
289 <div>
290 <label class="mb-2 block text-sm font-medium text-system-dark dark:text-gray-50">{{ translate('Access Key') }}</label>
291 <el-input v-model="form.access_key" :placeholder="translate('Enter access key')" @click.stop />
292 </div>
293
294 <div>
295 <label class="mb-2 block text-sm font-medium text-system-dark dark:text-gray-50">{{ translate('Secret Key') }}</label>
296 <el-input v-model="form.secret_key" show-password :placeholder="translate('Enter secret key')" @click.stop />
297 </div>
298 </div>
299 </div>
300 </Animation>
301 </div>
302 </li>
303 </ul>
304 </div>
305
306 <div v-show="currentStep === 2" class="fct-step-content-box space-y-4">
307 <h4 class="fct-step-content-box-heading">
308 {{ translate('Choose where files live') }}
309 </h4>
310
311 <ul class="fct-coverage-selector">
312 <li :class="{ active: bucketAction === 'existing' }" @click="bucketAction = 'existing'">
313 <div class="w-full">
314 <div class="fct-coverage-selector-content-wrap">
315 <div class="fct-coverage-selector-content">
316 <span class="fct-coverage-selector-title">{{ translate('Connect an existing bucket') }}</span>
317 <span class="fct-coverage-selector-desc">{{ translate('Choose a bucket you already manage in this AWS account.') }}</span>
318 </div>
319 <div class="fct-coverage-selector-dot-wrap shrink-0">
320 <span class="fct-coverage-selector-dot"></span>
321 </div>
322 </div>
323
324 <Animation :visible="bucketAction === 'existing'" accordion :duration="220">
325 <div class="mt-4">
326 <div class="space-y-4">
327 <div class="space-y-2">
328 <span class="block text-sm font-medium text-system-dark dark:text-gray-50">{{ translate('Bucket name') }}</span>
329 <div class="fct-coverage-bucket-name-selectors">
330 <label @click.stop>
331 <el-radio v-model="bucketInputMode" value="enter" @click.stop />
332 <span>{{ translate('Enter bucket name') }}</span>
333 </label>
334
335 <label @click.stop>
336 <el-radio v-model="bucketInputMode" value="select" @click.stop />
337 <span>{{ translate('Select from existing') }}</span>
338 </label>
339
340 </div>
341 </div>
342
343 <div v-if="bucketInputMode === 'enter'">
344 <el-input v-model="form.bucket" :placeholder="translate('Enter bucket name')" @click.stop />
345 </div>
346
347 <div v-else class="fct-bucket-list-wrap">
348 <div class="fct-bucket-list-search">
349 <el-input
350 v-model="bucketSearchQuery"
351 clearable
352 :placeholder="translate('Search buckets')"
353 @input="handleBucketSearch"
354 @clear="handleBucketSearch"
355 @click.stop
356 />
357 </div>
358
359 <div class="fct-bucket-list-header">
360 <span class="selected-bucket-count">
361 {{ bucketOptions.length }} {{ translate(bucketOptions.length === 1 ? 'bucket' : 'buckets') }}
362 </span>
363 <el-button text class="px-0" :loading="bucketLoading" @click.stop="refreshBucketList">
364 {{ translate('Reload bucket list') }}
365 </el-button>
366 </div>
367
368 <div v-if="bucketLoading" class="px-4 py-5 text-sm text-system-mid dark:text-gray-300">
369 {{ translate('Loading buckets...') }}
370 </div>
371
372 <div v-else-if="!bucketOptions.length" class="px-4 py-5 text-sm text-system-mid dark:text-gray-300">
373 {{ translate('No buckets found or credentials are invalid.') }}
374 </div>
375
376 <div v-else-if="!filteredBucketOptions.length" class="px-4 py-5 text-sm text-system-mid dark:text-gray-300">
377 {{ translate('No buckets match your search.') }}
378 </div>
379
380 <div v-else :class="bucketListClasses" class="fct-bucket-list">
381 <button
382 v-for="item in filteredBucketOptions"
383 :key="item.value"
384 type="button"
385 :class="form.bucket === item.value ? 'is-bucket-selected' : ''"
386 @click.stop="selectBucket(item)"
387 >
388 <span class="min-w-0 flex-1">
389 <span class="bucket-name">
390 <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">>
391 <path d="M4.75 10.375C4.75 10.6097 5.09575 11.0185 5.8975 11.4197C6.9355 11.9387 8.40775 12.25 10 12.25C11.5923 12.25 13.0645 11.9387 14.1025 11.4197C14.9043 11.0185 15.25 10.6097 15.25 10.375V8.74675C14.0125 9.51175 12.1203 10 10 10C7.87975 10 5.9875 9.511 4.75 8.74675V10.375ZM15.25 12.4968C14.0125 13.2618 12.1203 13.75 10 13.75C7.87975 13.75 5.9875 13.261 4.75 12.4968V14.125C4.75 14.3597 5.09575 14.7685 5.8975 15.1697C6.9355 15.6887 8.40775 16 10 16C11.5923 16 13.0645 15.6887 14.1025 15.1697C14.9043 14.7685 15.25 14.3597 15.25 14.125V12.4968ZM3.25 14.125V6.625C3.25 4.76125 6.2725 3.25 10 3.25C13.7275 3.25 16.75 4.76125 16.75 6.625V14.125C16.75 15.9887 13.7275 17.5 10 17.5C6.2725 17.5 3.25 15.9887 3.25 14.125ZM10 8.5C11.5923 8.5 13.0645 8.18875 14.1025 7.66975C14.9043 7.2685 15.25 6.85975 15.25 6.625C15.25 6.39025 14.9043 5.9815 14.1025 5.58025C13.0645 5.06125 11.5923 4.75 10 4.75C8.40775 4.75 6.9355 5.06125 5.8975 5.58025C5.09575 5.9815 4.75 6.39025 4.75 6.625C4.75 6.85975 5.09575 7.2685 5.8975 7.66975C6.9355 8.18875 8.40775 8.5 10 8.5Z" fill="currentColor"/>
392 </svg>
393
394 {{ item.label }}
395 </span>
396
397 <span v-if="item.region" class="bucket-region">
398 {{ item.region }}
399 </span>
400 </span>
401
402 <span v-if="form.bucket === item.value" class="selected-icon">
403 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">>
404 <path d="M7 14C3.1339 14 0 10.8661 0 7C0 3.1339 3.1339 0 7 0C10.8661 0 14 3.1339 14 7C14 10.8661 10.8661 14 7 14ZM6.3021 9.8L11.2511 4.8503L10.2613 3.8605L6.3021 7.8204L4.3218 5.8401L3.332 6.8299L6.3021 9.8Z" fill="currentColor"/>
405 </svg>
406 </span>
407 </button>
408 </div>
409 </div>
410 </div>
411 </div>
412 </Animation>
413 </div>
414 </li>
415
416 <li :class="{ active: bucketAction === 'new' }" @click="bucketAction = 'new'">
417 <div class="w-full">
418 <div class="fct-coverage-selector-content-wrap">
419 <div class="fct-coverage-selector-content">
420 <span class="fct-coverage-selector-title">{{ translate('Create a new bucket') }}</span>
421 <span class="fct-coverage-selector-desc">{{ translate('Create and select a fresh bucket using the current credentials.') }}</span>
422 </div>
423 <div class="fct-coverage-selector-dot-wrap shrink-0">
424 <span class="fct-coverage-selector-dot"></span>
425 </div>
426 </div>
427
428 <Animation :visible="bucketAction === 'new'" accordion :duration="220">
429 <div class="mt-4">
430 <div class="space-y-4">
431 <div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
432 <div class="lg:col-span-2">
433 <label class="mb-2 block text-sm font-medium text-system-dark dark:text-gray-50">{{ translate('Bucket Name') }}</label>
434 <el-input v-model="newBucketName" :placeholder="translate('Enter bucket name')" @click.stop />
435 <p v-if="createBucketAttempted && !newBucketName" class="mt-1 text-xs text-red-500">
436 {{ translate('Bucket name not entered.') }}
437 </p>
438 </div>
439
440 <div>
441 <label class="mb-2 block text-sm font-medium text-system-dark dark:text-gray-50">{{ translate('Region') }}</label>
442 <el-select v-model="newBucketRegion" class="w-full" :placeholder="translate('Select Region')" @click.stop>
443 <el-option v-for="region in awsRegions" :key="region.value" :label="region.label" :value="region.value" />
444 </el-select>
445 <p v-if="bucketStepAttempted && !hasSelectedRegion(newBucketRegion)" class="mt-1 text-xs text-red-500">
446 {{ translate('Region is required.') }}
447 </p>
448 </div>
449 </div>
450 </div>
451 </div>
452 </Animation>
453 </div>
454 </li>
455 </ul>
456 </div>
457
458 <div v-show="currentStep === 3" class="fct-step-content-box space-y-4">
459 <h4 class="fct-step-content-box-heading">
460 {{ translate('Review protection settings') }}
461 </h4>
462 <div class="space-y-4">
463 <div class="config-snippet config-snippet-white">
464 <div class="flex items-start justify-between gap-4">
465 <div class="min-w-0">
466 <div class="flex flex-wrap items-center gap-2">
467 <span class="text-sm font-medium text-system-dark dark:text-gray-50">{{ translate('Block All Public Access') }}</span>
468 <span :class="['badge small', form.block_public_access === 'yes' ? 'success' : 'warning']">
469 {{ form.block_public_access === 'yes' ? translate('Recommended') : translate('Review') }}
470 </span>
471 </div>
472 <p class="m-0 mt-2 text-sm text-system-mid dark:text-gray-300">
473 {{ translate('Recommended for private asset delivery.') }}
474 </p>
475 </div>
476
477 <el-switch v-model="form.block_public_access" active-value="yes" inactive-value="no" />
478 </div>
479 </div>
480
481 <div class="config-snippet config-snippet-white">
482 <div class="flex items-start justify-between gap-4">
483 <div class="min-w-0">
484 <div class="flex flex-wrap items-center gap-2">
485 <span class="text-sm font-medium text-system-dark dark:text-gray-50">{{ translate('Object Ownership') }}</span>
486 <span :class="['badge small', form.object_ownership === 'yes' ? 'success' : 'warning']">
487 {{ form.object_ownership === 'yes' ? translate('Recommended') : translate('Review') }}
488 </span>
489 </div>
490 <p class="m-0 mt-2 text-sm text-system-mid dark:text-gray-300">
491 {{ translate('Ensure uploaded files stay under the expected ownership model.') }}
492 </p>
493 </div>
494
495 <el-switch v-model="form.object_ownership" active-value="yes" inactive-value="no" />
496 </div>
497 </div>
498 </div>
499 </div>
500 </div>
501 </div>
502
503 <div class="flex items-center justify-end gap-3">
504 <div class="fct-btn-group">
505 <el-button v-if="editMode" @click="cancelEdit">
506 {{ translate('Cancel') }}
507 </el-button>
508
509 <el-button v-if="!editMode && currentStep === 2" @click="currentStep = 1">
510 {{ translate('Back') }}
511 </el-button>
512
513 <el-button v-if="currentStep === 3" @click="currentStep = 2">
514 {{ translate('Back') }}
515 </el-button>
516
517 <el-button v-if="currentStep === 1" type="primary" :loading="saving" @click="saveAndContinue">
518 {{ translate('Save & Continue') }}
519 </el-button>
520
521 <el-button
522 v-if="currentStep === 2"
523 type="primary"
524 :loading="saving"
525 @click="advanceFromBucketStep"
526 >
527 {{ bucketAction === 'new' ? translate('Create Bucket & Continue') : translate('Next') }}
528 </el-button>
529
530 <el-button v-if="currentStep === 3" type="primary" :loading="saving" @click="saveSecuritySettings">
531 {{ translate('Save Settings') }}
532 </el-button>
533 </div>
534 </div>
535 </div>
536 </template>
537 </div>
538 </template>
539
540 <script>
541 export default {
542 components: {
543 Animation,
544 StepIndicator
545 },
546 props: {
547 payload: {
548 type: Object,
549 required: true
550 }
551 },
552 data() {
553 return {
554 saving: false,
555 fetching: false,
556 checking_connection: false,
557 bucketLoading: false,
558 editMode: false,
559 wizardMode: true,
560 currentStep: 1,
561 bucketAction: 'existing',
562 bucketInputMode: 'enter',
563 bucketSearchQuery: '',
564 newBucketName: '',
565 newBucketRegion: 'us-east-1',
566 bucketStepAttempted: false,
567 createBucketAttempted: false,
568 bucketOptions: [],
569 connectionError: null,
570 form: this.makeForm(this.payload.settings || {}),
571 awsRegions: [
572 { label: 'US East (N. Virginia)', value: 'us-east-1' },
573 { label: 'US East (Ohio)', value: 'us-east-2' },
574 { label: 'US West (N. California)', value: 'us-west-1' },
575 { label: 'US West (Oregon)', value: 'us-west-2' },
576 { label: 'Canada (Central)', value: 'ca-central-1' },
577 { label: 'Canada West (Calgary)', value: 'ca-west-1' },
578 { label: 'Africa (Cape Town)', value: 'af-south-1' },
579 { label: 'Asia Pacific (Hong Kong)', value: 'ap-east-1' },
580 { label: 'Asia Pacific (Mumbai)', value: 'ap-south-1' },
581 { label: 'Asia Pacific (Hyderabad)', value: 'ap-south-2' },
582 { label: 'Asia Pacific (Tokyo)', value: 'ap-northeast-1' },
583 { label: 'Asia Pacific (Seoul)', value: 'ap-northeast-2' },
584 { label: 'Asia Pacific (Osaka)', value: 'ap-northeast-3' },
585 { label: 'Asia Pacific (Singapore)', value: 'ap-southeast-1' },
586 { label: 'Asia Pacific (Sydney)', value: 'ap-southeast-2' },
587 { label: 'Asia Pacific (Jakarta)', value: 'ap-southeast-3' },
588 { label: 'Asia Pacific (Melbourne)', value: 'ap-southeast-4' },
589 { label: 'Asia Pacific (Malaysia)', value: 'ap-southeast-5' },
590 { label: 'Asia Pacific (Thailand)', value: 'ap-southeast-7' },
591 { label: 'China (Beijing)', value: 'cn-north-1' },
592 { label: 'China (Ningxia)', value: 'cn-northwest-1' },
593 { label: 'EU (Frankfurt)', value: 'eu-central-1' },
594 { label: 'EU (Zurich)', value: 'eu-central-2' },
595 { label: 'EU (Ireland)', value: 'eu-west-1' },
596 { label: 'EU (London)', value: 'eu-west-2' },
597 { label: 'EU (Paris)', value: 'eu-west-3' },
598 { label: 'EU (Milan)', value: 'eu-south-1' },
599 { label: 'EU (Spain)', value: 'eu-south-2' },
600 { label: 'EU (Stockholm)', value: 'eu-north-1' },
601 { label: 'Israel (Tel Aviv)', value: 'il-central-1' },
602 { label: 'Middle East (Bahrain)', value: 'me-south-1' },
603 { label: 'Middle East (UAE)', value: 'me-central-1' },
604 { label: 'Mexico (Central)', value: 'mx-central-1' },
605 { label: 'South America (São Paulo)', value: 'sa-east-1' }
606 ]
607 };
608 },
609 computed: {
610 showDashboard() {
611 return !this.wizardMode && !this.editMode;
612 },
613 stepperActive() {
614 const currentIndex = this.visibleSteps.findIndex((step) => step.number === this.currentStep);
615 return currentIndex >= 0 ? currentIndex : 0;
616 },
617 showMissingDefineCredentialsWarning() {
618 return this.currentStep === 1 && this.form.define_credentials_missing;
619 },
620 missingDefineCredentialsMessage() {
621 return this.form.define_credentials_missing_message || this.translate('S3 was configured previously, but the wp-config.php credentials are missing now. Add them again to continue.');
622 },
623 formattedRegion() {
624 const region = this.awsRegions.find((item) => item.value === this.form.region);
625 return region ? region.label : this.form.region;
626 },
627 visibleSteps() {
628 const steps = [
629 {
630 number: 1,
631 label: this.translate('Credential'),
632 description: this.translate('Verify access keys')
633 },
634 {
635 number: 2,
636 label: this.translate('Bucket'),
637 description: this.translate('Choose where files live')
638 },
639 {
640 number: 3,
641 label: this.translate('Security'),
642 description: this.translate('Review protection settings')
643 }
644 ];
645
646 return this.editMode ? steps.filter((step) => step.number > 1) : steps;
647 },
648 connectionMethodLabel() {
649 if (this.form.auth_method === 'define') {
650 return this.translate('Defined mode');
651 }
652
653 return this.translate('Database mode');
654 },
655 securityRecommendationLabel() {
656 return this.form.block_public_access === 'yes' && this.form.object_ownership === 'yes'
657 ? this.translate('Recommended')
658 : this.translate('Needs review');
659 },
660 bucketConsoleUrl() {
661 if (!this.form.bucket) {
662 return '';
663 }
664
665 return `https://console.aws.amazon.com/s3/buckets/${this.form.bucket}/?region=${this.form.region || 'us-east-1'}`;
666 },
667 filteredBucketOptions() {
668 const query = (this.bucketSearchQuery || '').trim().toLowerCase();
669
670 if (!query) {
671 return this.bucketOptions;
672 }
673
674 return this.bucketOptions.filter((item) => {
675 return [item.label, item.value, item.region].some((value) => {
676 return String(value || '').toLowerCase().includes(query);
677 });
678 });
679 },
680 bucketListClasses() {
681 const classes = [];
682
683 if (this.filteredBucketOptions.length > 5) {
684 classes.push('max-h-[16rem]', 'overflow-y-auto');
685 }
686
687 return classes;
688 },
689 wpConfigSnippet() {
690 return `define( 'FCT_S3_ACCESS_KEY', '********************' );
691 define( 'FCT_S3_SECRET_KEY', '****************************************' );`;
692 }
693 },
694 watch: {
695 payload: {
696 deep: true,
697 handler(newPayload) {
698 this.applyServerSettings(newPayload.settings || {});
699 }
700 },
701 newBucketName(val) {
702 const formatted = (val || '').toLowerCase().replace(/[^a-z0-9.-]/g, '-').replace(/-+/g, '-');
703 if (formatted !== val) {
704 this.newBucketName = formatted;
705 }
706 },
707 bucketAction(newValue) {
708 if (newValue === 'existing') {
709 this.bucketSearchQuery = '';
710 this.ensureBucketOptionsLoaded();
711 }
712 },
713 bucketInputMode() {
714 this.form.bucket = '';
715 },
716 currentStep(newValue) {
717 if (newValue === 2 && this.bucketAction === 'existing') {
718 this.ensureBucketOptionsLoaded();
719 }
720 }
721 },
722 mounted() {
723 this.applyServerSettings(this.payload.settings || {});
724 },
725 methods: {
726 translate,
727 isStepAvailable(step) {
728 return this.editMode || step < this.currentStep || (this.form.bucket && step <= 3);
729 },
730 defaults() {
731 return {
732 is_active: 'no',
733 auth_method: 'define',
734 access_key: '',
735 secret_key: '',
736 bucket: '',
737 region: 'us-east-1',
738 buckets: [],
739 defined_in_wp_config: false,
740 define_credentials_missing: false,
741 define_credentials_missing_message: '',
742 block_public_access: 'yes',
743 object_ownership: 'yes',
744 connection_error: ''
745 };
746 },
747 normalizeSettings(settings = {}) {
748 const normalized = {
749 ...this.defaults(),
750 ...settings
751 };
752
753 normalized.defined_in_wp_config = !!normalized.defined_in_wp_config;
754 normalized.define_credentials_missing = !!normalized.define_credentials_missing;
755 normalized.block_public_access = normalized.block_public_access === true || normalized.block_public_access === 'yes' ? 'yes' : 'no';
756 normalized.object_ownership = normalized.object_ownership === true || normalized.object_ownership === 'yes' ? 'yes' : 'no';
757
758 return normalized;
759 },
760 makeForm(settings) {
761 return this.normalizeSettings(settings);
762 },
763 applyServerSettings(settings = {}, options = {}) {
764 const { preserveWizardState = false } = options;
765 const normalized = this.normalizeSettings(settings);
766
767 if (!Object.prototype.hasOwnProperty.call(settings, 'access_key') && this.form?.access_key) {
768 normalized.access_key = this.form.access_key;
769 }
770
771 if (!Object.prototype.hasOwnProperty.call(settings, 'secret_key') && this.form?.secret_key) {
772 normalized.secret_key = this.form.secret_key;
773 }
774
775 this.form = {
776 ...this.form,
777 ...normalized
778 };
779
780 if (this.form.define_credentials_missing) {
781 this.connectionError = null;
782 this.bucketOptions = [];
783 this.wizardMode = true;
784 this.currentStep = 1;
785 return;
786 }
787
788 this.connectionError = settings.connection_error || null;
789
790 if (preserveWizardState) {
791 return;
792 }
793
794 this.wizardMode = !this.form.bucket;
795 this.currentStep = this.form.bucket ? 2 : 1;
796 },
797 mergeResponseSettings(response, options = {}) {
798 const serverSettings = response?.data?.data || response?.data || response?.settings || {};
799 this.applyServerSettings(serverSettings, options);
800 return serverSettings;
801 },
802 copyCode() {
803 if (!navigator?.clipboard?.writeText) {
804 Notify.error(this.translate('Clipboard API is not available.'));
805 return;
806 }
807
808 navigator.clipboard.writeText(this.wpConfigSnippet).then(() => {
809 Notify.success(this.translate('Code copied to clipboard!'));
810 }).catch(() => {
811 Notify.error(this.translate('Failed to copy code.'));
812 });
813 },
814 getDriver() {
815 return this.payload.driver || 's3';
816 },
817 getSaveEndpoint() {
818 return this.payload.save_endpoint || 'settings/storage-drivers';
819 },
820 getCreateBucketEndpoint() {
821 return this.payload.create_bucket_endpoint || 'settings/storage-drivers/create-bucket';
822 },
823 getBucketListEndpoint() {
824 return this.payload.bucket_list_endpoint || 'settings/storage-drivers/bucket-list';
825 },
826 getVerifyEndpoint() {
827 return this.payload.connection_verify_endpoint || 'settings/storage-drivers/verify-info';
828 },
829 getResetEndpoint() {
830 return this.payload.reset_endpoint || 'settings/storage-drivers/reset';
831 },
832 enableEditMode() {
833 this.editMode = true;
834 this.bucketAction = 'existing';
835 this.bucketInputMode = 'enter';
836 this.currentStep = this.form.bucket ? 2 : 1;
837 this.bucketSearchQuery = '';
838 this.connectionError = null;
839 },
840 cancelEdit() {
841 this.editMode = false;
842 this.refreshSettings();
843 },
844 goToStep(step) {
845 if (this.editMode || step < this.currentStep || (this.form.bucket && step <= 3)) {
846 this.currentStep = step;
847 }
848 },
849 handleStepClick(localIndex) {
850 const step = this.visibleSteps[localIndex - 1];
851 if (step) {
852 this.goToStep(step.number);
853 }
854 },
855 refreshSettings(showLoading = false) {
856 if (showLoading) {
857 this.fetching = true;
858 }
859
860 return Rest.get('settings/storage-drivers/' + this.getDriver()).then((response) => {
861 this.applyServerSettings(response.settings || {});
862 }).finally(() => {
863 if (showLoading) {
864 this.fetching = false;
865 }
866 });
867 },
868 checkConnection() {
869 this.checking_connection = true;
870
871 Rest.post(this.getVerifyEndpoint(), {
872 driver: this.getDriver(),
873 settings: this.buildSettingsPayload()
874 }).then((response) => {
875 this.connectionError = null;
876 Notify.success(response?.message || this.translate('Connection verified successfully.'));
877 }).catch((errors) => {
878 const message = errors?.data?.message || this.translate('Connection failed. Please check settings.');
879 this.connectionError = message;
880 Notify.error(message);
881 }).finally(() => {
882 this.checking_connection = false;
883 });
884 },
885 handleSwitchChange() {
886 if (this.form.is_active === 'yes') {
887 this.activateDriver().catch(() => {
888 this.form.is_active = 'no';
889 });
890 } else {
891 this.deactivateDriver().catch(() => {
892 this.form.is_active = 'yes';
893 });
894 }
895 },
896 activateDriver() {
897 this.saving = true;
898 return Rest.post(this.getSaveEndpoint(), {
899 driver: this.getDriver(),
900 settings: {
901 ...this.form,
902 is_active: 'yes'
903 }
904 }).then((response) => {
905 this.mergeResponseSettings(response);
906 Notify.success(this.translate('S3 activated successfully.'));
907 }).catch((errors) => {
908 const message = errors?.data?.message || this.translate('Failed to activate driver.');
909 Notify.error(message);
910 throw errors;
911 }).finally(() => {
912 this.saving = false;
913 });
914 },
915 deactivateDriver() {
916 this.saving = true;
917 return Rest.post(this.getSaveEndpoint(), {
918 driver: this.getDriver(),
919 settings: {
920 ...this.form,
921 is_active: 'no',
922 preserve_settings: true
923 }
924 }).then(() => {
925 Notify.success(this.translate('S3 deactivated successfully.'));
926 }).catch((errors) => {
927 const message = errors?.data?.message || this.translate('Failed to deactivate driver.');
928 Notify.error(message);
929 throw errors;
930 }).finally(() => {
931 this.saving = false;
932 });
933 },
934 resetSettings() {
935 this.saving = true;
936 Rest.post(this.getResetEndpoint(), {
937 driver: this.getDriver()
938 }).then((response) => {
939 this.applyServerSettings(response?.data?.data || {});
940 this.form = this.makeForm(response?.data?.data || {});
941 this.currentStep = 1;
942 this.editMode = false;
943 this.wizardMode = true;
944 this.bucketOptions = [];
945 Notify.success(this.translate('S3 settings reset successfully.'));
946 }).catch((errors) => {
947 const message = errors?.data?.message || this.translate('Failed to reset settings.');
948 Notify.error(message);
949 }).finally(() => {
950 this.saving = false;
951 });
952 },
953 fetchBuckets(query) {
954 this.bucketLoading = true;
955 Rest.post(this.getBucketListEndpoint(), {
956 driver: this.getDriver(),
957 query: query,
958 settings: this.form
959 }).then((response) => {
960 this.bucketOptions = response.options || [];
961 }).catch((errors) => {
962 this.bucketOptions = [];
963
964 const message = errors?.data?.message || this.translate('Failed to load buckets.');
965 Notify.error(message);
966 }).finally(() => {
967 this.bucketLoading = false;
968 });
969 },
970 ensureBucketOptionsLoaded() {
971 if (!this.bucketOptions.length && !this.bucketLoading) {
972 this.fetchBuckets('');
973 }
974 },
975 refreshBucketList() {
976 this.fetchBuckets('');
977 },
978 handleBucketSearch() {
979 this.bucketSearchQuery = (this.bucketSearchQuery || '').trim();
980 },
981 hasSelectedRegion(region) {
982 return !!String(region || '').trim();
983 },
984 resetCreateBucketState(options = {}) {
985 const { clearName = false } = options;
986
987 this.bucketStepAttempted = false;
988 this.createBucketAttempted = false;
989
990 if (clearName) {
991 this.newBucketName = '';
992 }
993 },
994 buildSettingsPayload(extra = {}) {
995 const settings = {
996 ...this.form,
997 ...extra
998 };
999
1000 delete settings.create_new_bucket;
1001 delete settings.new_bucket_name;
1002 delete settings.new_bucket_region;
1003
1004 return settings;
1005 },
1006 selectBucket(item) {
1007 this.form.bucket = item.value;
1008 if (item.region) {
1009 this.form.region = item.region;
1010 }
1011 },
1012 advanceFromBucketStep() {
1013 this.bucketStepAttempted = true;
1014
1015 if (this.bucketAction === 'new') {
1016 if (!this.hasSelectedRegion(this.newBucketRegion)) {
1017 Notify.error(this.translate('Please select a region before continuing.'));
1018 return;
1019 }
1020 this.createNewBucket();
1021 return;
1022 }
1023
1024 this.saveBucketSettings();
1025 },
1026 saveAndContinue() {
1027 this.saving = true;
1028 Rest.post(this.getSaveEndpoint(), {
1029 driver: this.getDriver(),
1030 settings: this.buildSettingsPayload({
1031 is_active: 'no',
1032 preserve_settings: true,
1033 verify_credentials: true,
1034 verify_only_credentials: true
1035 })
1036 }).then((response) => {
1037 this.mergeResponseSettings(response);
1038 this.currentStep = 2;
1039 this.bucketOptions = [];
1040 this.bucketSearchQuery = '';
1041 this.ensureBucketOptionsLoaded();
1042 Notify.success(this.translate('Credentials saved. Proceeding to bucket selection.'));
1043 }).catch((errors) => {
1044 const message = errors?.data?.message || this.translate('Failed to save credentials.');
1045 if (errors.status_code === '422' && Notify.validationErrors) {
1046 Notify.validationErrors(errors);
1047 } else {
1048 Notify.error(message);
1049 }
1050 }).finally(() => {
1051 this.saving = false;
1052 });
1053 },
1054 createNewBucket() {
1055 this.createBucketAttempted = true;
1056
1057 if (!this.newBucketName) {
1058 return;
1059 }
1060
1061 this.saving = true;
1062 Rest.post(this.getCreateBucketEndpoint(), {
1063 driver: this.getDriver(),
1064 settings: {
1065 ...this.buildSettingsPayload(),
1066 create_new_bucket: 'yes',
1067 new_bucket_name: this.newBucketName,
1068 new_bucket_region: this.newBucketRegion
1069 }
1070 }).then((response) => {
1071 const bucketData = response?.data?.data || response?.data || {};
1072 this.form.bucket = bucketData.bucket || this.newBucketName;
1073 this.form.region = bucketData.region || this.newBucketRegion;
1074 this.form.connection_error = '';
1075
1076 if (bucketData.block_public_access) {
1077 this.form.block_public_access = bucketData.block_public_access;
1078 }
1079
1080 if (bucketData.object_ownership) {
1081 this.form.object_ownership = bucketData.object_ownership;
1082 }
1083
1084 this.bucketAction = 'existing';
1085 this.bucketSearchQuery = '';
1086 this.resetCreateBucketState({
1087 clearName: true
1088 });
1089 this.currentStep = 3;
1090 Notify.success(this.translate('Bucket created and selected successfully.'));
1091 }).catch((errors) => {
1092 const message = errors?.data?.message || this.translate('Failed to create bucket.');
1093 if (errors.status_code === '422' && Notify.validationErrors) {
1094 Notify.validationErrors(errors);
1095 } else {
1096 Notify.error(message);
1097 }
1098 }).finally(() => {
1099 this.saving = false;
1100 });
1101 },
1102 saveBucketSettings() {
1103 this.form.bucket = (this.form.bucket || '').trim();
1104
1105 if (!this.form.bucket) {
1106 Notify.error(this.translate('Please select or enter a bucket before continuing.'));
1107 return;
1108 }
1109
1110 this.saving = true;
1111 Rest.post(this.getSaveEndpoint(), {
1112 driver: this.getDriver(),
1113 settings: this.buildSettingsPayload({
1114 is_active: 'no',
1115 preserve_settings: true,
1116 check_bucket_exists: 'yes'
1117 })
1118 }).then((response) => {
1119 this.mergeResponseSettings(response, {
1120 preserveWizardState: true
1121 });
1122 this.resetCreateBucketState();
1123 this.currentStep = 3;
1124 Notify.success(this.translate('Bucket settings saved.'));
1125 }).catch((errors) => {
1126 const message = errors?.data?.message || this.translate('Failed to save bucket settings.');
1127 if (errors.status_code === '422' && Notify.validationErrors) {
1128 Notify.validationErrors(errors);
1129 } else {
1130 Notify.error(message);
1131 }
1132 }).finally(() => {
1133 this.saving = false;
1134 });
1135 },
1136 saveSecuritySettings() {
1137 this.saving = true;
1138 Rest.post(this.getSaveEndpoint(), {
1139 driver: this.getDriver(),
1140 settings: this.buildSettingsPayload({
1141 is_active: 'yes'
1142 })
1143 }).then((response) => {
1144 this.mergeResponseSettings(response);
1145 this.wizardMode = false;
1146 this.editMode = false;
1147 this.resetCreateBucketState({
1148 clearName: true
1149 });
1150 Notify.success(this.translate('S3 settings saved successfully.'));
1151 }).catch((errors) => {
1152 const message = errors?.data?.message || this.translate('Failed to save security settings.');
1153 Notify.error(message);
1154 }).finally(() => {
1155 this.saving = false;
1156 });
1157 }
1158 }
1159 };
1160 </script>
1161