@sowell/quasar-app-extension-auth 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/pages/AuthReporter.vue +7 -7
- package/src/components/pages/ResetPassword.vue +312 -65
- package/src/components/pages/WebResetPassword.vue +444 -72
- package/src/composables/resetPassword.ts +93 -25
- package/src/i18n/en-US/index.ts +25 -2
- package/src/i18n/fr-FR/index.ts +26 -2
package/package.json
CHANGED
|
@@ -359,14 +359,14 @@ export default defineComponent({
|
|
|
359
359
|
z-index: 1;
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
.login-page .q-field__control-container {
|
|
362
|
+
.login-page :deep(.q-field__control-container) {
|
|
363
363
|
padding: 4px;
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
-
.login-page div.q-field__control {
|
|
367
|
-
padding: 0;
|
|
366
|
+
.login-page :deep(div.q-field__control) {
|
|
368
367
|
border: 1px solid #d5d7da;
|
|
369
368
|
height: 55px;
|
|
369
|
+
padding: 0;
|
|
370
370
|
}
|
|
371
371
|
|
|
372
372
|
.q-field--with-bottom {
|
|
@@ -420,19 +420,19 @@ export default defineComponent({
|
|
|
420
420
|
gap: 6px;
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
-
.login-page .q-field--error .q-field__bottom {
|
|
423
|
+
.login-page :deep(.q-field--error .q-field__bottom) {
|
|
424
424
|
color: #ef476f;
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
-
.login-page .q-field__bottom {
|
|
427
|
+
.login-page :deep(.q-field__bottom) {
|
|
428
428
|
padding: 4px 0px;
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
-
.login-page .q-field--error .q-field__append .q-icon:not(.cursor-pointer) {
|
|
431
|
+
.login-page :deep(.q-field--error .q-field__append .q-icon:not(.cursor-pointer)) {
|
|
432
432
|
display: none;
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
-
.input-error .q-field__control {
|
|
435
|
+
.input-error :deep(.q-field__control) {
|
|
436
436
|
border: 1px solid #ff8b87 !important;
|
|
437
437
|
}
|
|
438
438
|
|
|
@@ -1,95 +1,178 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<q-layout
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
2
|
+
<q-layout
|
|
3
|
+
class="column justify-start items-center q-pb-lg bg-white reset-password-page"
|
|
4
|
+
style="max-width: 100vw; overflow-x: hidden">
|
|
5
|
+
<div class="background-logo-container">
|
|
6
|
+
<img
|
|
7
|
+
src="../../assets/Background-logo.svg"
|
|
8
|
+
class="background-logo"
|
|
9
|
+
alt="Background logo" />
|
|
10
|
+
</div>
|
|
11
|
+
<div class="row justify-between items-center full-width q-px-lg q-pt-lg top-bar">
|
|
12
|
+
<div
|
|
13
|
+
class="row items-center back-link"
|
|
14
|
+
data-testid="back-btn"
|
|
15
|
+
@click="$router.push(LOCAL_LOGIN_ROUTE)">
|
|
16
|
+
<q-icon name="arrow_back" size="16px" />
|
|
17
|
+
<span class="q-ml-xs">{{ $t("auth.back") }}</span>
|
|
18
|
+
</div>
|
|
19
|
+
<img src="../../assets/sowell-min.svg" style="width: auto; height: 33px" />
|
|
20
|
+
</div>
|
|
21
|
+
<div
|
|
22
|
+
class="full-width reset-password-card q-mt-lg"
|
|
23
|
+
:class="{ 'reset-password-card--centered': passwordUpdated }">
|
|
24
|
+
<template v-if="!passwordUpdated">
|
|
25
|
+
<div class="column items-center header-section">
|
|
26
|
+
<div class="icon-badge">
|
|
27
|
+
<q-icon name="verified_user" size="28px" color="primary" />
|
|
28
|
+
</div>
|
|
29
|
+
<div class="title text-center q-mt-md">
|
|
30
|
+
{{ $t("auth.passwordRecovery") }}
|
|
31
|
+
</div>
|
|
32
|
+
<div class="subtitle text-center q-mt-sm">
|
|
33
|
+
{{ $t("auth.recommendPassword") }}
|
|
34
|
+
</div>
|
|
26
35
|
</div>
|
|
27
36
|
<q-form
|
|
28
|
-
class="column
|
|
29
|
-
@submit="onSubmit"
|
|
37
|
+
class="column q-mt-lg"
|
|
30
38
|
data-testid="reset-password-form"
|
|
31
|
-
|
|
32
|
-
<div>
|
|
33
|
-
<div class="
|
|
39
|
+
@submit="onSubmit">
|
|
40
|
+
<div class="input-container">
|
|
41
|
+
<div class="input-label">
|
|
34
42
|
{{ $t("auth.newPassword") }}
|
|
35
43
|
</div>
|
|
36
44
|
<q-input
|
|
37
|
-
data-testid="password-input"
|
|
38
45
|
id="password"
|
|
39
46
|
v-model="password"
|
|
47
|
+
data-testid="password-input"
|
|
40
48
|
data-cy="reset-password"
|
|
49
|
+
:type="showPassword ? 'text' : 'password'"
|
|
41
50
|
:rules="validations['passwordRules']"
|
|
51
|
+
lazy-rules
|
|
42
52
|
borderless
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
:placeholder="$t('auth.newPasswordPlaceholder')"
|
|
54
|
+
class="q-mt-sm"
|
|
55
|
+
:input-style="{ fontSize: '13px', fontWeight: '500' }">
|
|
56
|
+
<template #append>
|
|
57
|
+
<q-icon
|
|
58
|
+
:name="
|
|
59
|
+
showPassword ? icons.ionEyeOffOutline : icons.ionEyeOutline
|
|
60
|
+
"
|
|
61
|
+
class="cursor-pointer"
|
|
62
|
+
@click="showPassword = !showPassword" />
|
|
49
63
|
</template>
|
|
50
64
|
</q-input>
|
|
51
65
|
</div>
|
|
52
|
-
|
|
53
|
-
<div>
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
data-cy="
|
|
62
|
-
|
|
63
|
-
:
|
|
64
|
-
|
|
66
|
+
|
|
67
|
+
<div class="input-container q-mt-sm">
|
|
68
|
+
<div class="input-label">
|
|
69
|
+
{{ $t("auth.confirmPassword") }}
|
|
70
|
+
</div>
|
|
71
|
+
<q-input
|
|
72
|
+
id="confirmPassword"
|
|
73
|
+
v-model="confirmPassword"
|
|
74
|
+
data-testid="confirm-password-input"
|
|
75
|
+
data-cy="reset-confirm-password"
|
|
76
|
+
:type="showConfirmPassword ? 'text' : 'password'"
|
|
77
|
+
:rules="validations['passwordConfirmationRules']"
|
|
78
|
+
lazy-rules
|
|
79
|
+
borderless
|
|
80
|
+
:placeholder="$t('auth.confirmPasswordPlaceholder')"
|
|
81
|
+
class="q-mt-sm"
|
|
82
|
+
:input-style="{ fontSize: '13px', fontWeight: '500' }">
|
|
83
|
+
<template #append>
|
|
84
|
+
<q-icon
|
|
85
|
+
:name="
|
|
86
|
+
showConfirmPassword
|
|
87
|
+
? icons.ionEyeOffOutline
|
|
88
|
+
: icons.ionEyeOutline
|
|
89
|
+
"
|
|
90
|
+
class="cursor-pointer"
|
|
91
|
+
@click="showConfirmPassword = !showConfirmPassword" />
|
|
92
|
+
</template>
|
|
93
|
+
</q-input>
|
|
65
94
|
</div>
|
|
95
|
+
|
|
96
|
+
<div class="password-requirements q-mt-md" data-testid="password-requirements">
|
|
97
|
+
<div
|
|
98
|
+
v-for="requirement in passwordRequirements"
|
|
99
|
+
:key="requirement.label"
|
|
100
|
+
class="requirement-item"
|
|
101
|
+
:class="{ 'requirement-valid': requirement.valid }">
|
|
102
|
+
<q-icon
|
|
103
|
+
:name="requirement.valid ? 'check_circle' : 'radio_button_unchecked'"
|
|
104
|
+
size="16px" />
|
|
105
|
+
<span class="q-ml-xs">{{ requirement.label }}</span>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<q-btn
|
|
110
|
+
data-testid="submit-btn"
|
|
111
|
+
data-cy="send-new-password"
|
|
112
|
+
color="primary"
|
|
113
|
+
unelevated
|
|
114
|
+
no-caps
|
|
115
|
+
text-color="white"
|
|
116
|
+
:loading="loading"
|
|
117
|
+
type="submit"
|
|
118
|
+
class="full-width q-mt-lg submit-button"
|
|
119
|
+
:disable="!canSubmit">
|
|
120
|
+
<q-icon name="lock_open" size="16px" class="q-mr-sm" />
|
|
121
|
+
{{ $t("common.saveNewPassword") }}
|
|
122
|
+
</q-btn>
|
|
66
123
|
</q-form>
|
|
67
|
-
</
|
|
68
|
-
<
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
124
|
+
</template>
|
|
125
|
+
<template v-else>
|
|
126
|
+
<div
|
|
127
|
+
class="column items-center header-section"
|
|
128
|
+
data-testid="password-updated-confirmation"
|
|
129
|
+
role="status"
|
|
130
|
+
aria-live="polite">
|
|
131
|
+
<div class="success-badge">
|
|
132
|
+
<q-icon name="check_circle" size="48px" color="primary" />
|
|
133
|
+
</div>
|
|
134
|
+
<div
|
|
135
|
+
ref="successTitleRef"
|
|
136
|
+
class="title text-center q-mt-md"
|
|
137
|
+
data-testid="password-updated-title"
|
|
138
|
+
tabindex="-1">
|
|
139
|
+
{{ $t("auth.passwordSavedTitle") }}
|
|
140
|
+
</div>
|
|
141
|
+
<div class="subtitle text-center q-mt-sm" data-testid="password-updated-message">
|
|
142
|
+
{{ $t("auth.passwordSavedMessage") }}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
<q-btn
|
|
146
|
+
data-testid="go-to-login-btn"
|
|
147
|
+
data-cy="go-to-login"
|
|
148
|
+
color="primary"
|
|
149
|
+
unelevated
|
|
150
|
+
no-caps
|
|
151
|
+
text-color="white"
|
|
152
|
+
class="full-width q-mt-lg submit-button"
|
|
153
|
+
@click="goToLoginPage">
|
|
154
|
+
{{ $t("auth.goToLogin") }}
|
|
155
|
+
</q-btn>
|
|
156
|
+
</template>
|
|
75
157
|
</div>
|
|
76
158
|
</q-layout>
|
|
77
159
|
</template>
|
|
78
160
|
|
|
79
161
|
<script lang="ts">
|
|
80
|
-
import { defineComponent } from "vue"
|
|
81
|
-
import SowellMessage from "../dialogs/Message.vue"
|
|
162
|
+
import { defineComponent, nextTick, ref, watch } from "vue"
|
|
82
163
|
import { useResetPassword } from "composables/resetPassword"
|
|
83
164
|
|
|
84
165
|
export default defineComponent({
|
|
85
166
|
name: "ResetPassword",
|
|
86
|
-
components: { SowellMessage },
|
|
87
167
|
setup() {
|
|
88
168
|
const {
|
|
89
169
|
password,
|
|
170
|
+
confirmPassword,
|
|
90
171
|
loading,
|
|
91
172
|
passwordUpdated,
|
|
173
|
+
showPassword,
|
|
92
174
|
validations,
|
|
175
|
+
passwordRequirements,
|
|
93
176
|
LOCAL_LOGIN_ROUTE,
|
|
94
177
|
canSubmit,
|
|
95
178
|
onSubmit,
|
|
@@ -97,13 +180,27 @@ export default defineComponent({
|
|
|
97
180
|
icons
|
|
98
181
|
} = useResetPassword()
|
|
99
182
|
|
|
183
|
+
const showConfirmPassword = ref(false)
|
|
184
|
+
const successTitleRef = ref<HTMLElement | null>(null)
|
|
185
|
+
|
|
186
|
+
watch(passwordUpdated, (updated) => {
|
|
187
|
+
if (updated) {
|
|
188
|
+
nextTick(() => successTitleRef.value?.focus())
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
100
192
|
return {
|
|
101
193
|
password,
|
|
194
|
+
confirmPassword,
|
|
102
195
|
loading,
|
|
103
196
|
canSubmit,
|
|
104
197
|
passwordUpdated,
|
|
198
|
+
showPassword,
|
|
199
|
+
showConfirmPassword,
|
|
200
|
+
successTitleRef,
|
|
105
201
|
LOCAL_LOGIN_ROUTE,
|
|
106
202
|
validations,
|
|
203
|
+
passwordRequirements,
|
|
107
204
|
icons,
|
|
108
205
|
onSubmit,
|
|
109
206
|
goToLoginPage
|
|
@@ -113,12 +210,162 @@ export default defineComponent({
|
|
|
113
210
|
</script>
|
|
114
211
|
|
|
115
212
|
<style scoped>
|
|
116
|
-
.
|
|
117
|
-
height:
|
|
213
|
+
.reset-password-page {
|
|
214
|
+
min-height: 100vh;
|
|
118
215
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
216
|
+
|
|
217
|
+
.background-logo-container {
|
|
218
|
+
position: fixed;
|
|
219
|
+
top: -53px;
|
|
220
|
+
left: 0;
|
|
221
|
+
width: 100%;
|
|
222
|
+
height: 100vh;
|
|
223
|
+
display: flex;
|
|
224
|
+
justify-content: center;
|
|
225
|
+
align-items: center;
|
|
226
|
+
opacity: 0.25;
|
|
227
|
+
overflow: hidden;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.background-logo {
|
|
231
|
+
width: 100%;
|
|
232
|
+
height: 100%;
|
|
233
|
+
object-fit: cover;
|
|
234
|
+
max-width: 100%;
|
|
235
|
+
max-height: 100vh;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.top-bar {
|
|
239
|
+
z-index: 1;
|
|
240
|
+
max-width: 430px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.back-link {
|
|
244
|
+
cursor: pointer;
|
|
245
|
+
color: #00668a;
|
|
246
|
+
font-size: 14px;
|
|
247
|
+
font-weight: 600;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.reset-password-card {
|
|
251
|
+
z-index: 1;
|
|
252
|
+
max-width: 398px;
|
|
253
|
+
padding: 32px 24px;
|
|
254
|
+
margin-top: 24px;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.reset-password-card--centered {
|
|
258
|
+
flex: 1;
|
|
259
|
+
display: flex;
|
|
260
|
+
flex-direction: column;
|
|
261
|
+
justify-content: center;
|
|
262
|
+
align-items: center;
|
|
263
|
+
margin-top: 0 !important;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.icon-badge {
|
|
267
|
+
width: 64px;
|
|
268
|
+
height: 64px;
|
|
269
|
+
border-radius: 9999px;
|
|
270
|
+
background: rgba(56, 189, 248, 0.1);
|
|
271
|
+
border: 1px solid rgba(56, 189, 248, 0.3);
|
|
272
|
+
display: flex;
|
|
273
|
+
align-items: center;
|
|
274
|
+
justify-content: center;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.success-badge {
|
|
278
|
+
width: 84px;
|
|
279
|
+
height: 84px;
|
|
280
|
+
border-radius: 9999px;
|
|
281
|
+
background: rgba(56, 189, 248, 0.1);
|
|
282
|
+
display: flex;
|
|
283
|
+
align-items: center;
|
|
284
|
+
justify-content: center;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.title {
|
|
288
|
+
font-size: 21px;
|
|
289
|
+
font-weight: 600;
|
|
290
|
+
color: #20627f;
|
|
291
|
+
line-height: 1.6;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.subtitle {
|
|
295
|
+
font-size: 14px;
|
|
296
|
+
font-weight: 400;
|
|
297
|
+
color: #3e484f;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.input-label {
|
|
301
|
+
font-size: 14px;
|
|
302
|
+
font-weight: 700;
|
|
303
|
+
color: #112240;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.reset-password-page :deep(.q-field__control-container) {
|
|
307
|
+
padding: 4px;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.reset-password-page :deep(div.q-field__control) {
|
|
311
|
+
border: 1px solid #bdc8d1;
|
|
312
|
+
border-radius: 8px;
|
|
313
|
+
height: 55px;
|
|
314
|
+
padding: 0 12px;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.reset-password-page :deep(.q-field--error .q-field__control) {
|
|
318
|
+
border-color: #ff8b87;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.reset-password-page :deep(.q-field--error .q-field__bottom) {
|
|
322
|
+
color: #ef476f;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.reset-password-page :deep(.q-field__bottom) {
|
|
326
|
+
padding: 4px 0px;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.password-requirements {
|
|
330
|
+
width: 100%;
|
|
331
|
+
background: #f2f4f6;
|
|
332
|
+
border-radius: 8px;
|
|
333
|
+
padding: 16px;
|
|
334
|
+
display: grid;
|
|
335
|
+
grid-template-columns: repeat(2, 1fr);
|
|
336
|
+
gap: 10px 16px;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.requirement-item {
|
|
340
|
+
display: flex;
|
|
341
|
+
align-items: center;
|
|
342
|
+
font-size: 12px;
|
|
343
|
+
font-weight: 500;
|
|
344
|
+
color: #3e484f;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.requirement-item .q-icon {
|
|
348
|
+
color: #bdc8d1;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.requirement-item.requirement-valid {
|
|
352
|
+
color: #06d6a0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.requirement-item.requirement-valid .q-icon {
|
|
356
|
+
color: #06d6a0;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.submit-button {
|
|
360
|
+
border-radius: 15px;
|
|
361
|
+
height: 50px;
|
|
362
|
+
font-size: 14px;
|
|
363
|
+
font-weight: 500;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
@media (max-width: 389px) {
|
|
367
|
+
.reset-password-card {
|
|
368
|
+
padding: 24px 16px;
|
|
122
369
|
}
|
|
123
370
|
}
|
|
124
371
|
</style>
|
|
@@ -1,111 +1,195 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<q-layout class="
|
|
3
|
-
<div>
|
|
4
|
-
<div>
|
|
5
|
-
<div class="
|
|
6
|
-
<img
|
|
7
|
-
src="../../assets/Sowell-logo.svg"
|
|
8
|
-
class="q-mb-sm q-mr-md"
|
|
9
|
-
style="width: 190px" />
|
|
2
|
+
<q-layout class="web-reset-password-page">
|
|
3
|
+
<div class="reset-container">
|
|
4
|
+
<div class="reset-card">
|
|
5
|
+
<div class="logo-container">
|
|
6
|
+
<img src="../../assets/Sowell-logo.svg" alt="SoWell" class="logo" />
|
|
10
7
|
</div>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
8
|
+
|
|
9
|
+
<template v-if="!passwordUpdated">
|
|
10
|
+
<div class="content-section">
|
|
11
|
+
<div class="reset-heading">
|
|
12
|
+
<h1 class="reset-title">{{ $t("auth.passwordRecovery") }}</h1>
|
|
13
|
+
<p class="reset-subtitle">{{ $t("auth.recommendPassword") }}</p>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<q-form
|
|
17
|
+
class="reset-form"
|
|
18
|
+
data-testid="reset-password-form"
|
|
19
|
+
@submit="onSubmit">
|
|
20
|
+
<div class="reset-inputs">
|
|
21
|
+
<q-input
|
|
22
|
+
id="password"
|
|
23
|
+
v-model="password"
|
|
24
|
+
data-testid="password-input"
|
|
25
|
+
data-cy="reset-password"
|
|
26
|
+
:type="showPassword ? 'text' : 'password'"
|
|
27
|
+
:rules="validations['passwordRules']"
|
|
28
|
+
lazy-rules
|
|
29
|
+
:placeholder="$t('auth.webNewPasswordPlaceholder')"
|
|
30
|
+
outlined
|
|
31
|
+
no-shadow
|
|
32
|
+
class="form-input">
|
|
33
|
+
<template #append>
|
|
34
|
+
<q-icon
|
|
35
|
+
:name="
|
|
36
|
+
showPassword
|
|
37
|
+
? icons.ionEyeOffOutline
|
|
38
|
+
: icons.ionEyeOutline
|
|
39
|
+
"
|
|
40
|
+
class="password-toggle"
|
|
41
|
+
@click="showPassword = !showPassword" />
|
|
42
|
+
</template>
|
|
43
|
+
</q-input>
|
|
44
|
+
|
|
45
|
+
<q-input
|
|
46
|
+
id="confirmPassword"
|
|
47
|
+
v-model="confirmPassword"
|
|
48
|
+
data-testid="confirm-password-input"
|
|
49
|
+
data-cy="reset-confirm-password"
|
|
50
|
+
:type="showConfirmPassword ? 'text' : 'password'"
|
|
51
|
+
:rules="validations['passwordConfirmationRules']"
|
|
52
|
+
lazy-rules
|
|
53
|
+
:placeholder="$t('auth.confirmPassword')"
|
|
54
|
+
outlined
|
|
55
|
+
no-shadow
|
|
56
|
+
class="form-input">
|
|
57
|
+
<template #append>
|
|
58
|
+
<q-icon
|
|
59
|
+
:name="
|
|
60
|
+
showConfirmPassword
|
|
61
|
+
? icons.ionEyeOffOutline
|
|
62
|
+
: icons.ionEyeOutline
|
|
63
|
+
"
|
|
64
|
+
class="password-toggle"
|
|
65
|
+
@click="showConfirmPassword = !showConfirmPassword" />
|
|
66
|
+
</template>
|
|
67
|
+
</q-input>
|
|
29
68
|
</div>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
dense
|
|
40
|
-
class="full-width q-mb-md">
|
|
41
|
-
<template #append>
|
|
69
|
+
|
|
70
|
+
<div
|
|
71
|
+
class="requirements-checklist"
|
|
72
|
+
data-testid="password-requirements">
|
|
73
|
+
<div
|
|
74
|
+
v-for="requirement in passwordRequirements"
|
|
75
|
+
:key="requirement.label"
|
|
76
|
+
class="requirement-item"
|
|
77
|
+
:class="{ 'requirement-item--valid': requirement.valid }">
|
|
42
78
|
<q-icon
|
|
43
79
|
:name="
|
|
44
|
-
|
|
45
|
-
? icons.ionEyeOffOutline
|
|
46
|
-
: icons.ionEyeOutline
|
|
80
|
+
requirement.valid ? 'check_circle' : 'radio_button_unchecked'
|
|
47
81
|
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<div>
|
|
82
|
+
size="16px"
|
|
83
|
+
class="requirement-icon" />
|
|
84
|
+
<span class="requirement-label">{{ requirement.label }}</span>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
55
88
|
<q-btn
|
|
89
|
+
:label="$t('common.saveNewPassword')"
|
|
90
|
+
icon="lock"
|
|
91
|
+
no-caps
|
|
92
|
+
unelevated
|
|
56
93
|
data-testid="submit-btn"
|
|
57
|
-
|
|
58
|
-
color="primary"
|
|
94
|
+
data-cy="send-new-password"
|
|
59
95
|
:loading="loading"
|
|
60
96
|
type="submit"
|
|
61
|
-
|
|
62
|
-
class="
|
|
63
|
-
|
|
64
|
-
|
|
97
|
+
:disable="!canSubmit"
|
|
98
|
+
class="reset-button" />
|
|
99
|
+
</q-form>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div class="back-section">
|
|
103
|
+
<q-btn
|
|
104
|
+
flat
|
|
105
|
+
no-caps
|
|
106
|
+
class="back-button"
|
|
107
|
+
icon="arrow_back"
|
|
108
|
+
data-testid="back-btn"
|
|
109
|
+
@click="$router.push(LOCAL_LOGIN_ROUTE)">
|
|
110
|
+
<span class="back-text">{{ $t("auth.backToLoginFull") }}</span>
|
|
111
|
+
</q-btn>
|
|
112
|
+
</div>
|
|
113
|
+
</template>
|
|
114
|
+
|
|
115
|
+
<template v-else>
|
|
116
|
+
<div
|
|
117
|
+
class="success-content"
|
|
118
|
+
data-testid="password-updated-confirmation"
|
|
119
|
+
role="status"
|
|
120
|
+
aria-live="polite">
|
|
121
|
+
<div class="success-icon-outer">
|
|
122
|
+
<div class="success-icon-inner">
|
|
123
|
+
<q-icon name="check" size="24px" color="white" />
|
|
124
|
+
</div>
|
|
65
125
|
</div>
|
|
66
|
-
|
|
67
|
-
|
|
126
|
+
<div
|
|
127
|
+
ref="successTitleRef"
|
|
128
|
+
class="success-title"
|
|
129
|
+
data-testid="password-updated-title"
|
|
130
|
+
tabindex="-1">
|
|
131
|
+
{{ $t("auth.passwordSavedTitle") }}
|
|
132
|
+
</div>
|
|
133
|
+
<div class="success-subtitle" data-testid="password-updated-message">
|
|
134
|
+
{{ $t("auth.passwordSavedMessage") }}
|
|
135
|
+
</div>
|
|
136
|
+
<q-btn
|
|
137
|
+
unelevated
|
|
138
|
+
no-caps
|
|
139
|
+
class="connect-button"
|
|
140
|
+
:label="$t('auth.goToLogin')"
|
|
141
|
+
data-testid="go-to-login-btn"
|
|
142
|
+
@click="goToLoginPage" />
|
|
143
|
+
</div>
|
|
144
|
+
</template>
|
|
68
145
|
</div>
|
|
69
|
-
<q-dialog v-model="passwordUpdated" persistent>
|
|
70
|
-
<sw-info-dialog
|
|
71
|
-
:title="$t('auth.passwordChanged')"
|
|
72
|
-
:content="$t('auth.passwordUpdated')"
|
|
73
|
-
:image="'passwordUpdated'"
|
|
74
|
-
:passwordUpdated="passwordUpdated"
|
|
75
|
-
@goToLoginPage="goToLoginPage" />
|
|
76
|
-
</q-dialog>
|
|
77
146
|
</div>
|
|
78
147
|
</q-layout>
|
|
79
148
|
</template>
|
|
80
149
|
|
|
81
150
|
<script lang="ts">
|
|
82
|
-
import { defineComponent } from "vue"
|
|
83
|
-
import SwInfoDialog from "../dialogs/sw-info-dialog.vue"
|
|
151
|
+
import { defineComponent, nextTick, ref, watch } from "vue"
|
|
84
152
|
import { useResetPassword } from "composables/resetPassword"
|
|
85
153
|
|
|
86
154
|
export default defineComponent({
|
|
87
|
-
name: "
|
|
88
|
-
components: { SwInfoDialog },
|
|
155
|
+
name: "WebResetPassword",
|
|
89
156
|
setup() {
|
|
90
157
|
const {
|
|
91
158
|
password,
|
|
159
|
+
confirmPassword,
|
|
92
160
|
loading,
|
|
93
161
|
passwordUpdated,
|
|
94
162
|
showPassword,
|
|
95
163
|
validations,
|
|
164
|
+
passwordRequirements,
|
|
165
|
+
LOCAL_LOGIN_ROUTE,
|
|
96
166
|
canSubmit,
|
|
97
|
-
icons,
|
|
98
167
|
onSubmit,
|
|
99
|
-
goToLoginPage
|
|
168
|
+
goToLoginPage,
|
|
169
|
+
icons
|
|
100
170
|
} = useResetPassword()
|
|
101
171
|
|
|
172
|
+
const showConfirmPassword = ref(false)
|
|
173
|
+
const successTitleRef = ref<HTMLElement | null>(null)
|
|
174
|
+
|
|
175
|
+
watch(passwordUpdated, (updated) => {
|
|
176
|
+
if (updated) {
|
|
177
|
+
nextTick(() => successTitleRef.value?.focus())
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
102
181
|
return {
|
|
103
182
|
password,
|
|
183
|
+
confirmPassword,
|
|
104
184
|
validations,
|
|
185
|
+
passwordRequirements,
|
|
105
186
|
loading,
|
|
106
187
|
canSubmit,
|
|
107
188
|
passwordUpdated,
|
|
108
189
|
showPassword,
|
|
190
|
+
showConfirmPassword,
|
|
191
|
+
successTitleRef,
|
|
192
|
+
LOCAL_LOGIN_ROUTE,
|
|
109
193
|
icons,
|
|
110
194
|
goToLoginPage,
|
|
111
195
|
onSubmit
|
|
@@ -115,7 +199,295 @@ export default defineComponent({
|
|
|
115
199
|
</script>
|
|
116
200
|
|
|
117
201
|
<style scoped>
|
|
118
|
-
.
|
|
119
|
-
|
|
202
|
+
.web-reset-password-page {
|
|
203
|
+
background: linear-gradient(
|
|
204
|
+
224deg,
|
|
205
|
+
rgba(233, 236, 237, 1) 0%,
|
|
206
|
+
rgba(64, 196, 255, 1) 59%,
|
|
207
|
+
rgba(255, 255, 255, 1) 100%
|
|
208
|
+
);
|
|
209
|
+
min-height: 100vh;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.reset-container {
|
|
213
|
+
min-height: 100vh;
|
|
214
|
+
display: flex;
|
|
215
|
+
align-items: center;
|
|
216
|
+
justify-content: center;
|
|
217
|
+
padding: 20px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.reset-card {
|
|
221
|
+
background: #ffffff;
|
|
222
|
+
border-radius: 20px;
|
|
223
|
+
padding: 52px;
|
|
224
|
+
width: 100%;
|
|
225
|
+
max-width: 634px;
|
|
226
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
227
|
+
display: flex;
|
|
228
|
+
flex-direction: column;
|
|
229
|
+
gap: 40px;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.logo-container {
|
|
233
|
+
display: flex;
|
|
234
|
+
justify-content: center;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.logo {
|
|
238
|
+
height: 53px;
|
|
239
|
+
width: auto;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.content-section {
|
|
243
|
+
display: flex;
|
|
244
|
+
flex-direction: column;
|
|
245
|
+
gap:29px
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.reset-form {
|
|
249
|
+
display: flex;
|
|
250
|
+
flex-direction: column;
|
|
251
|
+
gap: 29px;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.reset-heading {
|
|
255
|
+
display: flex;
|
|
256
|
+
flex-direction: column;
|
|
257
|
+
gap: 14px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.reset-title {
|
|
261
|
+
font-family: "Poppins", sans-serif;
|
|
262
|
+
font-size: 27px;
|
|
263
|
+
font-weight: 600;
|
|
264
|
+
line-height: 40px;
|
|
265
|
+
letter-spacing: 0.1px;
|
|
266
|
+
color: #152f3b;
|
|
267
|
+
margin: 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.reset-subtitle {
|
|
271
|
+
font-size: 16px;
|
|
272
|
+
font-weight: 400;
|
|
273
|
+
line-height: normal;
|
|
274
|
+
color: #152f3b;
|
|
275
|
+
margin: 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.reset-inputs {
|
|
279
|
+
display: flex;
|
|
280
|
+
flex-direction: column;
|
|
281
|
+
gap: 18px;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.form-input {
|
|
285
|
+
width: 100%;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.form-input :deep(.q-field__control) {
|
|
289
|
+
height: 44px;
|
|
290
|
+
border-radius: 10px !important;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.form-input :deep(.q-field__native),
|
|
294
|
+
.form-input :deep(.q-field__input) {
|
|
295
|
+
font-size: 16px;
|
|
296
|
+
font-weight: 500;
|
|
297
|
+
line-height: 1.5em;
|
|
298
|
+
letter-spacing: -0.5px;
|
|
299
|
+
color: #103140;
|
|
300
|
+
padding: 0 16px;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.form-input :deep(.q-field__marginal) {
|
|
304
|
+
height: 44px;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.form-input :deep(input::placeholder) {
|
|
308
|
+
color: #8f9da3;
|
|
309
|
+
opacity: 1;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.form-input :deep(.q-field__bottom) {
|
|
313
|
+
font-size: 12px;
|
|
314
|
+
font-weight: 400;
|
|
315
|
+
line-height: 1.5em;
|
|
316
|
+
letter-spacing: -0.5px;
|
|
317
|
+
color: #ef476f;
|
|
318
|
+
padding-top: 4px;
|
|
319
|
+
padding-left: 0;
|
|
320
|
+
min-height: 20px;
|
|
321
|
+
}
|
|
322
|
+
:deep(.q-field--with-bottom) {
|
|
323
|
+
padding-bottom: 8px;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.password-toggle {
|
|
327
|
+
cursor: pointer;
|
|
328
|
+
color: #103140;
|
|
329
|
+
font-size: 24px;
|
|
330
|
+
padding-right: 6px;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.requirements-checklist {
|
|
334
|
+
background: #f2f4f6;
|
|
335
|
+
border-radius: 8px;
|
|
336
|
+
padding: 17px 16px 16px;
|
|
337
|
+
display: grid;
|
|
338
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
339
|
+
gap: 8px 16px;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.requirement-item {
|
|
343
|
+
display: flex;
|
|
344
|
+
align-items: center;
|
|
345
|
+
gap: 8px;
|
|
346
|
+
color: #3e484f;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.requirement-icon {
|
|
350
|
+
color: #bbcbd2;
|
|
351
|
+
flex-shrink: 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.requirement-item--valid {
|
|
355
|
+
color: #06d6a0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.requirement-item--valid .requirement-icon {
|
|
359
|
+
color: #06d6a0;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.requirement-label {
|
|
363
|
+
font-size: 12px;
|
|
364
|
+
font-weight: 500;
|
|
365
|
+
line-height: 14.4px;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.reset-button {
|
|
369
|
+
width: 100%;
|
|
370
|
+
height: 54px;
|
|
371
|
+
background: #40c4ff;
|
|
372
|
+
color: #ffffff;
|
|
373
|
+
font-size: 16px;
|
|
374
|
+
font-weight: 600;
|
|
375
|
+
line-height: 24px;
|
|
376
|
+
border-radius: 15px;
|
|
377
|
+
text-transform: none;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.reset-button:hover {
|
|
381
|
+
background: #35b0e6;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.reset-button :deep(.q-icon) {
|
|
385
|
+
font-size: 16px;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.back-section {
|
|
389
|
+
display: flex;
|
|
390
|
+
justify-content: center;
|
|
391
|
+
align-items: center;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.back-button {
|
|
395
|
+
display: flex;
|
|
396
|
+
align-items: center;
|
|
397
|
+
gap: 10px;
|
|
398
|
+
color: #103140;
|
|
399
|
+
font-size: 16px;
|
|
400
|
+
font-weight: 600;
|
|
401
|
+
line-height: 1.5em;
|
|
402
|
+
padding: 10px;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.back-button :deep(.q-icon) {
|
|
406
|
+
margin-right: 0;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.success-content {
|
|
410
|
+
display: flex;
|
|
411
|
+
flex-direction: column;
|
|
412
|
+
align-items: center;
|
|
413
|
+
gap: 24px;
|
|
414
|
+
text-align: center;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.success-icon-outer {
|
|
418
|
+
width: 96px;
|
|
419
|
+
height: 96px;
|
|
420
|
+
border-radius: 9999px;
|
|
421
|
+
background: rgba(6, 214, 160, 0.2);
|
|
422
|
+
display: flex;
|
|
423
|
+
align-items: center;
|
|
424
|
+
justify-content: center;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.success-icon-inner {
|
|
428
|
+
width: 40px;
|
|
429
|
+
height: 40px;
|
|
430
|
+
border-radius: 9999px;
|
|
431
|
+
background: #06d6a0;
|
|
432
|
+
display: flex;
|
|
433
|
+
align-items: center;
|
|
434
|
+
justify-content: center;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.success-title {
|
|
438
|
+
font-family: "Poppins", sans-serif;
|
|
439
|
+
font-size: 27px;
|
|
440
|
+
font-weight: 600;
|
|
441
|
+
line-height: 40px;
|
|
442
|
+
letter-spacing: 0.1px;
|
|
443
|
+
color: #152f3b;
|
|
444
|
+
margin: 0;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.success-subtitle {
|
|
448
|
+
font-size: 16px;
|
|
449
|
+
font-weight: 400;
|
|
450
|
+
line-height: normal;
|
|
451
|
+
letter-spacing: 0.1px;
|
|
452
|
+
color: #152f3b;
|
|
453
|
+
margin: 0;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.connect-button {
|
|
457
|
+
width: 100%;
|
|
458
|
+
height: 56px;
|
|
459
|
+
background: #40c4ff;
|
|
460
|
+
color: #ffffff;
|
|
461
|
+
font-size: 16px;
|
|
462
|
+
font-weight: 600;
|
|
463
|
+
line-height: 24px;
|
|
464
|
+
border-radius: 15px;
|
|
465
|
+
text-transform: none;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.connect-button:hover {
|
|
469
|
+
background: #35b0e6;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
@media screen and (max-width: 768px) {
|
|
473
|
+
.reset-card {
|
|
474
|
+
padding: 32px 24px;
|
|
475
|
+
max-width: 100%;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.logo {
|
|
479
|
+
width: 160px;
|
|
480
|
+
height: auto;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.reset-title,
|
|
484
|
+
.success-title {
|
|
485
|
+
font-size: 22px;
|
|
486
|
+
line-height: 1.3em;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.requirements-checklist {
|
|
490
|
+
grid-template-columns: 1fr;
|
|
491
|
+
}
|
|
120
492
|
}
|
|
121
493
|
</style>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useQuasar } from "quasar"
|
|
1
|
+
import { useQuasar, LocalStorage } from "quasar"
|
|
2
2
|
import { useI18n } from "vue-i18n"
|
|
3
3
|
import jwt from "jwt-decode"
|
|
4
4
|
import { computed, ref, Ref, onMounted } from "vue"
|
|
@@ -19,15 +19,16 @@ interface DecodedToken {
|
|
|
19
19
|
token: string
|
|
20
20
|
}
|
|
21
21
|
export function useResetPassword() {
|
|
22
|
-
const {
|
|
22
|
+
const { notify } = useQuasar()
|
|
23
23
|
const { t } = useI18n()
|
|
24
24
|
|
|
25
25
|
const password = ref("")
|
|
26
|
+
const confirmPassword = ref("")
|
|
26
27
|
const loading = ref(false)
|
|
27
28
|
const passwordUpdated = ref(false)
|
|
28
29
|
const showPassword = ref(false)
|
|
29
30
|
const tempToken = ref("")
|
|
30
|
-
const userInfos = JSON.parse(
|
|
31
|
+
const userInfos = JSON.parse(LocalStorage.getItem("userInfos") || "{}") as {
|
|
31
32
|
uid: string
|
|
32
33
|
token: string
|
|
33
34
|
}
|
|
@@ -49,26 +50,78 @@ export function useResetPassword() {
|
|
|
49
50
|
|
|
50
51
|
const isOtp = computed(() => IS_OTP.toLowerCase() === "y")
|
|
51
52
|
|
|
53
|
+
const hasMinLength = computed(() => password.value.length >= 8)
|
|
54
|
+
const hasUppercase = computed(() => /[A-Z]/.test(password.value))
|
|
55
|
+
const hasLowercase = computed(() => /[a-z]/.test(password.value))
|
|
56
|
+
const hasNumber = computed(() => /[0-9]/.test(password.value))
|
|
57
|
+
const hasLetter = computed(() => /[a-zA-Z]/.test(password.value))
|
|
58
|
+
const hasSpecialChar = computed(() =>
|
|
59
|
+
/[^a-zA-Z0-9\s]/.test(password.value)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const isPasswordValid = computed(
|
|
63
|
+
() =>
|
|
64
|
+
hasMinLength.value &&
|
|
65
|
+
hasUppercase.value &&
|
|
66
|
+
hasLowercase.value &&
|
|
67
|
+
hasNumber.value &&
|
|
68
|
+
hasLetter.value &&
|
|
69
|
+
hasSpecialChar.value
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const passwordRequirements = computed(() => [
|
|
73
|
+
{ label: t("auth.passwordRequirement.minLength"), valid: hasMinLength.value },
|
|
74
|
+
{ label: t("auth.passwordRequirement.uppercase"), valid: hasUppercase.value },
|
|
75
|
+
{ label: t("auth.passwordRequirement.lowercase"), valid: hasLowercase.value },
|
|
76
|
+
{ label: t("auth.passwordRequirement.number"), valid: hasNumber.value },
|
|
77
|
+
{ label: t("auth.passwordRequirement.letter"), valid: hasLetter.value },
|
|
78
|
+
{
|
|
79
|
+
label: t("auth.passwordRequirement.specialChar"),
|
|
80
|
+
valid: hasSpecialChar.value
|
|
81
|
+
}
|
|
82
|
+
])
|
|
83
|
+
|
|
52
84
|
const canSubmit = computed(() => {
|
|
53
85
|
return (
|
|
54
86
|
(decodedToken.value !== null || userInfos?.uid) &&
|
|
55
|
-
|
|
87
|
+
isPasswordValid.value &&
|
|
88
|
+
confirmPassword.value === password.value
|
|
56
89
|
)
|
|
57
90
|
})
|
|
58
91
|
|
|
59
92
|
const validations = {
|
|
60
93
|
passwordRules: [
|
|
61
94
|
(val: string) => !!val || t("auth.passwordPresenceError"),
|
|
62
|
-
(
|
|
95
|
+
() => isPasswordValid.value || t("auth.passwordRequirementsError")
|
|
63
96
|
],
|
|
64
97
|
passwordConfirmationRules: [
|
|
65
|
-
(val: string) =>
|
|
66
|
-
val === password.value || t("auth.newPasswordConfirmationFailed"),
|
|
67
98
|
(val: string) => !!val || t("auth.passwordPresenceError"),
|
|
68
|
-
(val: string) =>
|
|
99
|
+
(val: string) =>
|
|
100
|
+
val === password.value || t("auth.newPasswordConfirmationFailed")
|
|
69
101
|
]
|
|
70
102
|
}
|
|
71
103
|
|
|
104
|
+
const KNOWN_API_PASSWORD_ERRORS: Array<{ pattern: RegExp; key: string }> = [
|
|
105
|
+
{
|
|
106
|
+
pattern: /simple sequence of \d+ characters/i,
|
|
107
|
+
key: "auth.passwordSequenceError"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
pattern: /\d+ or more sequential characters/i,
|
|
111
|
+
key: "auth.passwordSequenceError"
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
const translateApiErrorDetail = (detail?: string) => {
|
|
116
|
+
if (!detail) {
|
|
117
|
+
return undefined
|
|
118
|
+
}
|
|
119
|
+
const knownError = KNOWN_API_PASSWORD_ERRORS.find((entry) =>
|
|
120
|
+
entry.pattern.test(detail)
|
|
121
|
+
)
|
|
122
|
+
return knownError ? t(knownError.key) : undefined
|
|
123
|
+
}
|
|
124
|
+
|
|
72
125
|
const decodeKey = (key: string) => {
|
|
73
126
|
try {
|
|
74
127
|
// remove backslash to avoid decoding error
|
|
@@ -129,24 +182,37 @@ export function useResetPassword() {
|
|
|
129
182
|
.then(() => {
|
|
130
183
|
loading.value = false
|
|
131
184
|
passwordUpdated.value = true
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
})
|
|
135
|
-
.catch(() => {
|
|
136
|
-
loading.value = false
|
|
137
|
-
notify({
|
|
138
|
-
message: t("auth.passwordRecoveryFailed"),
|
|
139
|
-
color: "negative",
|
|
140
|
-
icon: ionAlertCircleOutline,
|
|
141
|
-
position: "top",
|
|
142
|
-
actions: [
|
|
143
|
-
{
|
|
144
|
-
color: "white",
|
|
145
|
-
icon: ionCloseOutline
|
|
146
|
-
}
|
|
147
|
-
]
|
|
148
|
-
})
|
|
185
|
+
LocalStorage.remove("emailResetPassword")
|
|
186
|
+
LocalStorage.remove("userInfos")
|
|
149
187
|
})
|
|
188
|
+
.catch(
|
|
189
|
+
(error: {
|
|
190
|
+
response?: {
|
|
191
|
+
status?: number
|
|
192
|
+
data?: { errors?: Array<{ detail?: string; title?: string }> }
|
|
193
|
+
}
|
|
194
|
+
}) => {
|
|
195
|
+
loading.value = false
|
|
196
|
+
|
|
197
|
+
const apiErrorDetail =
|
|
198
|
+
error.response?.data?.errors?.[0]?.detail ||
|
|
199
|
+
error.response?.data?.errors?.[0]?.title
|
|
200
|
+
const apiErrorMessage = translateApiErrorDetail(apiErrorDetail)
|
|
201
|
+
|
|
202
|
+
notify({
|
|
203
|
+
message: apiErrorMessage || t("auth.passwordRecoveryFailed"),
|
|
204
|
+
color: "negative",
|
|
205
|
+
icon: ionAlertCircleOutline,
|
|
206
|
+
position: "top",
|
|
207
|
+
actions: [
|
|
208
|
+
{
|
|
209
|
+
color: "white",
|
|
210
|
+
icon: ionCloseOutline
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
)
|
|
150
216
|
}
|
|
151
217
|
|
|
152
218
|
const goToLoginPage = () => {
|
|
@@ -221,10 +287,12 @@ export function useResetPassword() {
|
|
|
221
287
|
|
|
222
288
|
return {
|
|
223
289
|
password,
|
|
290
|
+
confirmPassword,
|
|
224
291
|
loading,
|
|
225
292
|
passwordUpdated,
|
|
226
293
|
showPassword,
|
|
227
294
|
validations,
|
|
295
|
+
passwordRequirements,
|
|
228
296
|
canSubmit,
|
|
229
297
|
LOCAL_LOGIN_ROUTE,
|
|
230
298
|
onSubmit,
|
package/src/i18n/en-US/index.ts
CHANGED
|
@@ -17,13 +17,35 @@ export default {
|
|
|
17
17
|
tapEmail: "Please enter your email",
|
|
18
18
|
404: "No user found with the provided credentials",
|
|
19
19
|
403: "Insufficient rights, your account does not allow access to this interface",
|
|
20
|
-
passwordRecovery: "Password
|
|
21
|
-
|
|
20
|
+
passwordRecovery: "Password reset",
|
|
21
|
+
recommendPassword: "Please set a new password",
|
|
22
|
+
newPassword: "Password",
|
|
23
|
+
newPasswordPlaceholder: "Minimum 8 characters",
|
|
24
|
+
webNewPasswordPlaceholder: "Password (Minimum 8 characters)",
|
|
25
|
+
confirmPassword: "Confirm password",
|
|
26
|
+
confirmPasswordPlaceholder: "Repeat your password",
|
|
22
27
|
newPasswordConfirmationFailed: "The passwords you typed don't match",
|
|
28
|
+
passwordRequirementsError: "The password does not meet the requirements",
|
|
29
|
+
passwordSequenceError:
|
|
30
|
+
"Password must not contain a simple sequence of 4+ characters (e.g. 1234, abcd, azerty, qwerty).",
|
|
31
|
+
passwordRequirement: {
|
|
32
|
+
minLength: "8 characters minimum",
|
|
33
|
+
uppercase: "1 uppercase letter",
|
|
34
|
+
lowercase: "1 lowercase letter",
|
|
35
|
+
number: "1 number",
|
|
36
|
+
letter: "1 letter",
|
|
37
|
+
specialChar: "1 special character"
|
|
38
|
+
},
|
|
23
39
|
passwordRecoverySuccess: "Your password has been updated",
|
|
24
40
|
passwordRecoveryFailed: "An error occured while updating your password",
|
|
25
41
|
passwordRecoveryExpiredLink: "The link expired",
|
|
26
42
|
passwordRecoveryInvalidLink: "the link is invalid",
|
|
43
|
+
passwordSavedTitle: "Password saved!",
|
|
44
|
+
passwordSavedMessage:
|
|
45
|
+
"You can now access all of your SoWell+ services.",
|
|
46
|
+
goToLogin: "Log in",
|
|
47
|
+
back: "Back",
|
|
48
|
+
backToLoginFull: "Back to login",
|
|
27
49
|
continueWith: "Or continue with",
|
|
28
50
|
emailOrPasswordIncorrect: "Email or password incorrect"
|
|
29
51
|
},
|
|
@@ -32,6 +54,7 @@ export default {
|
|
|
32
54
|
send: "Send",
|
|
33
55
|
cancel: "Cancel",
|
|
34
56
|
or: "or",
|
|
57
|
+
saveNewPassword: "Save my new password",
|
|
35
58
|
privacyPolicy: "Privacy policy"
|
|
36
59
|
}
|
|
37
60
|
}
|
package/src/i18n/fr-FR/index.ts
CHANGED
|
@@ -17,15 +17,38 @@ export default {
|
|
|
17
17
|
tapEmail: "Entrer votre email",
|
|
18
18
|
404: "Aucun utilisateur trouvé avec les identifiants donnés",
|
|
19
19
|
403: "Droits insuffisants, votre compte ne permet pas d'acceder à cette interface",
|
|
20
|
-
passwordRecovery: "
|
|
21
|
-
|
|
20
|
+
passwordRecovery: "Réinitialisation de votre mot de passe",
|
|
21
|
+
recommendPassword: "Veuillez configurer un nouveau mot de passe",
|
|
22
|
+
newPassword: "Mot de passe",
|
|
23
|
+
newPasswordPlaceholder: "Minimum 8 caractères",
|
|
24
|
+
webNewPasswordPlaceholder: "Mot de passe (Minimum 8 caractères)",
|
|
25
|
+
confirmPassword: "Confirmer le mot de passe",
|
|
26
|
+
confirmPasswordPlaceholder: "Répéter le mot de passe",
|
|
22
27
|
newPasswordConfirmationFailed:
|
|
23
28
|
"Les mot de passes que vous avez saisi ne correspondent pas",
|
|
29
|
+
passwordRequirementsError:
|
|
30
|
+
"Le mot de passe ne respecte pas les critères requis",
|
|
31
|
+
passwordSequenceError:
|
|
32
|
+
"Le mot de passe ne doit pas contenir de séquence simple de 4 caractères ou plus (ex. 1234, abcd, azerty, qwerty).",
|
|
33
|
+
passwordRequirement: {
|
|
34
|
+
minLength: "8 caractères minimum",
|
|
35
|
+
uppercase: "1 majuscule",
|
|
36
|
+
lowercase: "1 minuscule",
|
|
37
|
+
number: "1 chiffre",
|
|
38
|
+
letter: "1 lettre",
|
|
39
|
+
specialChar: "1 Caractère spécial"
|
|
40
|
+
},
|
|
24
41
|
passwordRecoverySuccess: "Votre mot de passe a été mis à jour",
|
|
25
42
|
passwordRecoveryFailed:
|
|
26
43
|
"Une erreur s'est produite lors de la mise à jour de votre mot de passe",
|
|
27
44
|
passwordRecoveryExpiredLink: "Lien de récupération expiré",
|
|
28
45
|
passwordRecoveryInvalidLink: "Lien de récupération invalide",
|
|
46
|
+
passwordSavedTitle: "Mot de passe enregistré !",
|
|
47
|
+
passwordSavedMessage:
|
|
48
|
+
"Vous pouvez maintenant accéder à l'intégralité de vos services SoWell+.",
|
|
49
|
+
goToLogin: "Me connecter",
|
|
50
|
+
back: "Retour",
|
|
51
|
+
backToLoginFull: "Retour à la connexion",
|
|
29
52
|
continueWith: "Ou continuer avec",
|
|
30
53
|
emailOrPasswordIncorrect: "Email ou mot de passe incorrect"
|
|
31
54
|
},
|
|
@@ -34,6 +57,7 @@ export default {
|
|
|
34
57
|
send: "Envoyer",
|
|
35
58
|
cancel: "Annuler",
|
|
36
59
|
or: "ou",
|
|
60
|
+
saveNewPassword: "Enregistrer mon nouveau mot de passe",
|
|
37
61
|
privacyPolicy: "Politique de confidentialité"
|
|
38
62
|
}
|
|
39
63
|
}
|