@sowell/quasar-app-extension-auth 1.5.1 → 1.6.1
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 +315 -65
- package/src/components/pages/WebResetPassword.vue +447 -72
- package/src/composables/resetPassword.ts +105 -26
- 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,109 +1,209 @@
|
|
|
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-
|
|
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
|
+
ref="confirmPasswordInput"
|
|
74
|
+
v-model="confirmPassword"
|
|
75
|
+
data-testid="confirm-password-input"
|
|
76
|
+
data-cy="reset-confirm-password"
|
|
77
|
+
:type="showConfirmPassword ? 'text' : 'password'"
|
|
78
|
+
:rules="validations['passwordConfirmationRules']"
|
|
79
|
+
lazy-rules
|
|
80
|
+
borderless
|
|
81
|
+
:placeholder="$t('auth.confirmPasswordPlaceholder')"
|
|
82
|
+
class="q-mt-sm"
|
|
83
|
+
:input-style="{ fontSize: '13px', fontWeight: '500' }">
|
|
84
|
+
<template #append>
|
|
85
|
+
<q-icon
|
|
86
|
+
:name="
|
|
87
|
+
showConfirmPassword
|
|
88
|
+
? icons.ionEyeOffOutline
|
|
89
|
+
: icons.ionEyeOutline
|
|
90
|
+
"
|
|
91
|
+
class="cursor-pointer"
|
|
92
|
+
@click="showConfirmPassword = !showConfirmPassword" />
|
|
93
|
+
</template>
|
|
94
|
+
</q-input>
|
|
65
95
|
</div>
|
|
96
|
+
|
|
97
|
+
<div class="password-requirements q-mt-md" data-testid="password-requirements">
|
|
98
|
+
<div
|
|
99
|
+
v-for="requirement in passwordRequirements"
|
|
100
|
+
:key="requirement.label"
|
|
101
|
+
class="requirement-item"
|
|
102
|
+
:class="{ 'requirement-valid': requirement.valid }">
|
|
103
|
+
<q-icon
|
|
104
|
+
:name="requirement.valid ? 'check_circle' : 'radio_button_unchecked'"
|
|
105
|
+
size="16px" />
|
|
106
|
+
<span class="q-ml-xs">{{ requirement.label }}</span>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<q-btn
|
|
111
|
+
data-testid="submit-btn"
|
|
112
|
+
data-cy="send-new-password"
|
|
113
|
+
color="primary"
|
|
114
|
+
unelevated
|
|
115
|
+
no-caps
|
|
116
|
+
text-color="white"
|
|
117
|
+
:loading="loading"
|
|
118
|
+
type="submit"
|
|
119
|
+
class="full-width q-mt-lg submit-button"
|
|
120
|
+
:disable="!canSubmit">
|
|
121
|
+
<q-icon name="lock_open" size="16px" class="q-mr-sm" />
|
|
122
|
+
{{ $t("common.saveNewPassword") }}
|
|
123
|
+
</q-btn>
|
|
66
124
|
</q-form>
|
|
67
|
-
</
|
|
68
|
-
<
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
125
|
+
</template>
|
|
126
|
+
<template v-else>
|
|
127
|
+
<div
|
|
128
|
+
class="column items-center header-section"
|
|
129
|
+
data-testid="password-updated-confirmation"
|
|
130
|
+
role="status"
|
|
131
|
+
aria-live="polite">
|
|
132
|
+
<div class="success-badge">
|
|
133
|
+
<q-icon name="check_circle" size="48px" color="primary" />
|
|
134
|
+
</div>
|
|
135
|
+
<div
|
|
136
|
+
ref="successTitleRef"
|
|
137
|
+
class="title text-center q-mt-md"
|
|
138
|
+
data-testid="password-updated-title"
|
|
139
|
+
tabindex="-1">
|
|
140
|
+
{{ $t("auth.passwordSavedTitle") }}
|
|
141
|
+
</div>
|
|
142
|
+
<div class="subtitle text-center q-mt-sm" data-testid="password-updated-message">
|
|
143
|
+
{{ $t("auth.passwordSavedMessage") }}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<q-btn
|
|
147
|
+
data-testid="go-to-login-btn"
|
|
148
|
+
data-cy="go-to-login"
|
|
149
|
+
color="primary"
|
|
150
|
+
unelevated
|
|
151
|
+
no-caps
|
|
152
|
+
text-color="white"
|
|
153
|
+
class="full-width q-mt-lg submit-button"
|
|
154
|
+
@click="goToLoginPage">
|
|
155
|
+
{{ $t("auth.goToLogin") }}
|
|
156
|
+
</q-btn>
|
|
157
|
+
</template>
|
|
75
158
|
</div>
|
|
76
159
|
</q-layout>
|
|
77
160
|
</template>
|
|
78
161
|
|
|
79
162
|
<script lang="ts">
|
|
80
|
-
import { defineComponent } from "vue"
|
|
81
|
-
import SowellMessage from "../dialogs/Message.vue"
|
|
163
|
+
import { defineComponent, nextTick, ref, watch } from "vue"
|
|
82
164
|
import { useResetPassword } from "composables/resetPassword"
|
|
83
165
|
|
|
84
166
|
export default defineComponent({
|
|
85
167
|
name: "ResetPassword",
|
|
86
|
-
components: { SowellMessage },
|
|
87
168
|
setup() {
|
|
88
169
|
const {
|
|
89
170
|
password,
|
|
171
|
+
confirmPassword,
|
|
90
172
|
loading,
|
|
91
173
|
passwordUpdated,
|
|
174
|
+
showPassword,
|
|
92
175
|
validations,
|
|
176
|
+
passwordRequirements,
|
|
93
177
|
LOCAL_LOGIN_ROUTE,
|
|
94
178
|
canSubmit,
|
|
179
|
+
confirmPasswordInput,
|
|
95
180
|
onSubmit,
|
|
96
181
|
goToLoginPage,
|
|
97
182
|
icons
|
|
98
183
|
} = useResetPassword()
|
|
99
184
|
|
|
185
|
+
const showConfirmPassword = ref(false)
|
|
186
|
+
const successTitleRef = ref<HTMLElement | null>(null)
|
|
187
|
+
|
|
188
|
+
watch(passwordUpdated, (updated) => {
|
|
189
|
+
if (updated) {
|
|
190
|
+
nextTick(() => successTitleRef.value?.focus())
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
|
|
100
194
|
return {
|
|
101
195
|
password,
|
|
196
|
+
confirmPassword,
|
|
102
197
|
loading,
|
|
103
198
|
canSubmit,
|
|
104
199
|
passwordUpdated,
|
|
200
|
+
showPassword,
|
|
201
|
+
showConfirmPassword,
|
|
202
|
+
successTitleRef,
|
|
105
203
|
LOCAL_LOGIN_ROUTE,
|
|
106
204
|
validations,
|
|
205
|
+
passwordRequirements,
|
|
206
|
+
confirmPasswordInput,
|
|
107
207
|
icons,
|
|
108
208
|
onSubmit,
|
|
109
209
|
goToLoginPage
|
|
@@ -113,12 +213,162 @@ export default defineComponent({
|
|
|
113
213
|
</script>
|
|
114
214
|
|
|
115
215
|
<style scoped>
|
|
116
|
-
.
|
|
117
|
-
height:
|
|
216
|
+
.reset-password-page {
|
|
217
|
+
min-height: 100vh;
|
|
118
218
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
219
|
+
|
|
220
|
+
.background-logo-container {
|
|
221
|
+
position: fixed;
|
|
222
|
+
top: -53px;
|
|
223
|
+
left: 0;
|
|
224
|
+
width: 100%;
|
|
225
|
+
height: 100vh;
|
|
226
|
+
display: flex;
|
|
227
|
+
justify-content: center;
|
|
228
|
+
align-items: center;
|
|
229
|
+
opacity: 0.25;
|
|
230
|
+
overflow: hidden;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.background-logo {
|
|
234
|
+
width: 100%;
|
|
235
|
+
height: 100%;
|
|
236
|
+
object-fit: cover;
|
|
237
|
+
max-width: 100%;
|
|
238
|
+
max-height: 100vh;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.top-bar {
|
|
242
|
+
z-index: 1;
|
|
243
|
+
max-width: 430px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.back-link {
|
|
247
|
+
cursor: pointer;
|
|
248
|
+
color: #00668a;
|
|
249
|
+
font-size: 14px;
|
|
250
|
+
font-weight: 600;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.reset-password-card {
|
|
254
|
+
z-index: 1;
|
|
255
|
+
max-width: 398px;
|
|
256
|
+
padding: 32px 24px;
|
|
257
|
+
margin-top: 24px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.reset-password-card--centered {
|
|
261
|
+
flex: 1;
|
|
262
|
+
display: flex;
|
|
263
|
+
flex-direction: column;
|
|
264
|
+
justify-content: center;
|
|
265
|
+
align-items: center;
|
|
266
|
+
margin-top: 0 !important;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.icon-badge {
|
|
270
|
+
width: 64px;
|
|
271
|
+
height: 64px;
|
|
272
|
+
border-radius: 9999px;
|
|
273
|
+
background: rgba(56, 189, 248, 0.1);
|
|
274
|
+
border: 1px solid rgba(56, 189, 248, 0.3);
|
|
275
|
+
display: flex;
|
|
276
|
+
align-items: center;
|
|
277
|
+
justify-content: center;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.success-badge {
|
|
281
|
+
width: 84px;
|
|
282
|
+
height: 84px;
|
|
283
|
+
border-radius: 9999px;
|
|
284
|
+
background: rgba(56, 189, 248, 0.1);
|
|
285
|
+
display: flex;
|
|
286
|
+
align-items: center;
|
|
287
|
+
justify-content: center;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.title {
|
|
291
|
+
font-size: 21px;
|
|
292
|
+
font-weight: 600;
|
|
293
|
+
color: #20627f;
|
|
294
|
+
line-height: 1.6;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.subtitle {
|
|
298
|
+
font-size: 14px;
|
|
299
|
+
font-weight: 400;
|
|
300
|
+
color: #3e484f;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.input-label {
|
|
304
|
+
font-size: 14px;
|
|
305
|
+
font-weight: 700;
|
|
306
|
+
color: #112240;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.reset-password-page :deep(.q-field__control-container) {
|
|
310
|
+
padding: 4px;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.reset-password-page :deep(div.q-field__control) {
|
|
314
|
+
border: 1px solid #bdc8d1;
|
|
315
|
+
border-radius: 8px;
|
|
316
|
+
height: 55px;
|
|
317
|
+
padding: 0 12px;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.reset-password-page :deep(.q-field--error .q-field__control) {
|
|
321
|
+
border-color: #ff8b87;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.reset-password-page :deep(.q-field--error .q-field__bottom) {
|
|
325
|
+
color: #ef476f;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.reset-password-page :deep(.q-field__bottom) {
|
|
329
|
+
padding: 4px 0px;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.password-requirements {
|
|
333
|
+
width: 100%;
|
|
334
|
+
background: #f2f4f6;
|
|
335
|
+
border-radius: 8px;
|
|
336
|
+
padding: 16px;
|
|
337
|
+
display: grid;
|
|
338
|
+
grid-template-columns: repeat(2, 1fr);
|
|
339
|
+
gap: 10px 16px;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.requirement-item {
|
|
343
|
+
display: flex;
|
|
344
|
+
align-items: center;
|
|
345
|
+
font-size: 12px;
|
|
346
|
+
font-weight: 500;
|
|
347
|
+
color: #3e484f;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.requirement-item .q-icon {
|
|
351
|
+
color: #bdc8d1;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.requirement-item.requirement-valid {
|
|
355
|
+
color: #06d6a0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.requirement-item.requirement-valid .q-icon {
|
|
359
|
+
color: #06d6a0;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.submit-button {
|
|
363
|
+
border-radius: 15px;
|
|
364
|
+
height: 50px;
|
|
365
|
+
font-size: 14px;
|
|
366
|
+
font-weight: 500;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
@media (max-width: 389px) {
|
|
370
|
+
.reset-password-card {
|
|
371
|
+
padding: 24px 16px;
|
|
122
372
|
}
|
|
123
373
|
}
|
|
124
374
|
</style>
|
|
@@ -1,111 +1,198 @@
|
|
|
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
|
+
ref="confirmPasswordInput"
|
|
48
|
+
v-model="confirmPassword"
|
|
49
|
+
data-testid="confirm-password-input"
|
|
50
|
+
data-cy="reset-confirm-password"
|
|
51
|
+
:type="showConfirmPassword ? 'text' : 'password'"
|
|
52
|
+
:rules="validations['passwordConfirmationRules']"
|
|
53
|
+
lazy-rules
|
|
54
|
+
:placeholder="$t('auth.confirmPassword')"
|
|
55
|
+
outlined
|
|
56
|
+
no-shadow
|
|
57
|
+
class="form-input">
|
|
58
|
+
<template #append>
|
|
59
|
+
<q-icon
|
|
60
|
+
:name="
|
|
61
|
+
showConfirmPassword
|
|
62
|
+
? icons.ionEyeOffOutline
|
|
63
|
+
: icons.ionEyeOutline
|
|
64
|
+
"
|
|
65
|
+
class="password-toggle"
|
|
66
|
+
@click="showConfirmPassword = !showConfirmPassword" />
|
|
67
|
+
</template>
|
|
68
|
+
</q-input>
|
|
29
69
|
</div>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
dense
|
|
40
|
-
class="full-width q-mb-md">
|
|
41
|
-
<template #append>
|
|
70
|
+
|
|
71
|
+
<div
|
|
72
|
+
class="requirements-checklist"
|
|
73
|
+
data-testid="password-requirements">
|
|
74
|
+
<div
|
|
75
|
+
v-for="requirement in passwordRequirements"
|
|
76
|
+
:key="requirement.label"
|
|
77
|
+
class="requirement-item"
|
|
78
|
+
:class="{ 'requirement-item--valid': requirement.valid }">
|
|
42
79
|
<q-icon
|
|
43
80
|
:name="
|
|
44
|
-
|
|
45
|
-
? icons.ionEyeOffOutline
|
|
46
|
-
: icons.ionEyeOutline
|
|
81
|
+
requirement.valid ? 'check_circle' : 'radio_button_unchecked'
|
|
47
82
|
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<div>
|
|
83
|
+
size="16px"
|
|
84
|
+
class="requirement-icon" />
|
|
85
|
+
<span class="requirement-label">{{ requirement.label }}</span>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
55
89
|
<q-btn
|
|
90
|
+
:label="$t('common.saveNewPassword')"
|
|
91
|
+
icon="lock"
|
|
92
|
+
no-caps
|
|
93
|
+
unelevated
|
|
56
94
|
data-testid="submit-btn"
|
|
57
|
-
|
|
58
|
-
color="primary"
|
|
95
|
+
data-cy="send-new-password"
|
|
59
96
|
:loading="loading"
|
|
60
97
|
type="submit"
|
|
61
|
-
|
|
62
|
-
class="
|
|
63
|
-
|
|
64
|
-
|
|
98
|
+
:disable="!canSubmit"
|
|
99
|
+
class="reset-button" />
|
|
100
|
+
</q-form>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="back-section">
|
|
104
|
+
<q-btn
|
|
105
|
+
flat
|
|
106
|
+
no-caps
|
|
107
|
+
class="back-button"
|
|
108
|
+
icon="arrow_back"
|
|
109
|
+
data-testid="back-btn"
|
|
110
|
+
@click="$router.push(LOCAL_LOGIN_ROUTE)">
|
|
111
|
+
<span class="back-text">{{ $t("auth.backToLoginFull") }}</span>
|
|
112
|
+
</q-btn>
|
|
113
|
+
</div>
|
|
114
|
+
</template>
|
|
115
|
+
|
|
116
|
+
<template v-else>
|
|
117
|
+
<div
|
|
118
|
+
class="success-content"
|
|
119
|
+
data-testid="password-updated-confirmation"
|
|
120
|
+
role="status"
|
|
121
|
+
aria-live="polite">
|
|
122
|
+
<div class="success-icon-outer">
|
|
123
|
+
<div class="success-icon-inner">
|
|
124
|
+
<q-icon name="check" size="24px" color="white" />
|
|
125
|
+
</div>
|
|
65
126
|
</div>
|
|
66
|
-
|
|
67
|
-
|
|
127
|
+
<div
|
|
128
|
+
ref="successTitleRef"
|
|
129
|
+
class="success-title"
|
|
130
|
+
data-testid="password-updated-title"
|
|
131
|
+
tabindex="-1">
|
|
132
|
+
{{ $t("auth.passwordSavedTitle") }}
|
|
133
|
+
</div>
|
|
134
|
+
<div class="success-subtitle" data-testid="password-updated-message">
|
|
135
|
+
{{ $t("auth.passwordSavedMessage") }}
|
|
136
|
+
</div>
|
|
137
|
+
<q-btn
|
|
138
|
+
unelevated
|
|
139
|
+
no-caps
|
|
140
|
+
class="connect-button"
|
|
141
|
+
:label="$t('auth.goToLogin')"
|
|
142
|
+
data-testid="go-to-login-btn"
|
|
143
|
+
@click="goToLoginPage" />
|
|
144
|
+
</div>
|
|
145
|
+
</template>
|
|
68
146
|
</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
147
|
</div>
|
|
78
148
|
</q-layout>
|
|
79
149
|
</template>
|
|
80
150
|
|
|
81
151
|
<script lang="ts">
|
|
82
|
-
import { defineComponent } from "vue"
|
|
83
|
-
import SwInfoDialog from "../dialogs/sw-info-dialog.vue"
|
|
152
|
+
import { defineComponent, nextTick, ref, watch } from "vue"
|
|
84
153
|
import { useResetPassword } from "composables/resetPassword"
|
|
85
154
|
|
|
86
155
|
export default defineComponent({
|
|
87
|
-
name: "
|
|
88
|
-
components: { SwInfoDialog },
|
|
156
|
+
name: "WebResetPassword",
|
|
89
157
|
setup() {
|
|
90
158
|
const {
|
|
91
159
|
password,
|
|
160
|
+
confirmPassword,
|
|
92
161
|
loading,
|
|
93
162
|
passwordUpdated,
|
|
94
163
|
showPassword,
|
|
95
164
|
validations,
|
|
165
|
+
passwordRequirements,
|
|
166
|
+
LOCAL_LOGIN_ROUTE,
|
|
96
167
|
canSubmit,
|
|
97
|
-
|
|
168
|
+
confirmPasswordInput,
|
|
98
169
|
onSubmit,
|
|
99
|
-
goToLoginPage
|
|
170
|
+
goToLoginPage,
|
|
171
|
+
icons
|
|
100
172
|
} = useResetPassword()
|
|
101
173
|
|
|
174
|
+
const showConfirmPassword = ref(false)
|
|
175
|
+
const successTitleRef = ref<HTMLElement | null>(null)
|
|
176
|
+
|
|
177
|
+
watch(passwordUpdated, (updated) => {
|
|
178
|
+
if (updated) {
|
|
179
|
+
nextTick(() => successTitleRef.value?.focus())
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
|
|
102
183
|
return {
|
|
103
184
|
password,
|
|
185
|
+
confirmPassword,
|
|
104
186
|
validations,
|
|
187
|
+
passwordRequirements,
|
|
105
188
|
loading,
|
|
106
189
|
canSubmit,
|
|
107
190
|
passwordUpdated,
|
|
108
191
|
showPassword,
|
|
192
|
+
showConfirmPassword,
|
|
193
|
+
successTitleRef,
|
|
194
|
+
LOCAL_LOGIN_ROUTE,
|
|
195
|
+
confirmPasswordInput,
|
|
109
196
|
icons,
|
|
110
197
|
goToLoginPage,
|
|
111
198
|
onSubmit
|
|
@@ -115,7 +202,295 @@ export default defineComponent({
|
|
|
115
202
|
</script>
|
|
116
203
|
|
|
117
204
|
<style scoped>
|
|
118
|
-
.
|
|
119
|
-
|
|
205
|
+
.web-reset-password-page {
|
|
206
|
+
background: linear-gradient(
|
|
207
|
+
224deg,
|
|
208
|
+
rgba(233, 236, 237, 1) 0%,
|
|
209
|
+
rgba(64, 196, 255, 1) 59%,
|
|
210
|
+
rgba(255, 255, 255, 1) 100%
|
|
211
|
+
);
|
|
212
|
+
min-height: 100vh;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.reset-container {
|
|
216
|
+
min-height: 100vh;
|
|
217
|
+
display: flex;
|
|
218
|
+
align-items: center;
|
|
219
|
+
justify-content: center;
|
|
220
|
+
padding: 20px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.reset-card {
|
|
224
|
+
background: #ffffff;
|
|
225
|
+
border-radius: 20px;
|
|
226
|
+
padding: 52px;
|
|
227
|
+
width: 100%;
|
|
228
|
+
max-width: 634px;
|
|
229
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
230
|
+
display: flex;
|
|
231
|
+
flex-direction: column;
|
|
232
|
+
gap: 40px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.logo-container {
|
|
236
|
+
display: flex;
|
|
237
|
+
justify-content: center;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.logo {
|
|
241
|
+
height: 53px;
|
|
242
|
+
width: auto;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.content-section {
|
|
246
|
+
display: flex;
|
|
247
|
+
flex-direction: column;
|
|
248
|
+
gap:29px
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.reset-form {
|
|
252
|
+
display: flex;
|
|
253
|
+
flex-direction: column;
|
|
254
|
+
gap: 29px;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.reset-heading {
|
|
258
|
+
display: flex;
|
|
259
|
+
flex-direction: column;
|
|
260
|
+
gap: 14px;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.reset-title {
|
|
264
|
+
font-family: "Poppins", sans-serif;
|
|
265
|
+
font-size: 27px;
|
|
266
|
+
font-weight: 600;
|
|
267
|
+
line-height: 40px;
|
|
268
|
+
letter-spacing: 0.1px;
|
|
269
|
+
color: #152f3b;
|
|
270
|
+
margin: 0;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.reset-subtitle {
|
|
274
|
+
font-size: 16px;
|
|
275
|
+
font-weight: 400;
|
|
276
|
+
line-height: normal;
|
|
277
|
+
color: #152f3b;
|
|
278
|
+
margin: 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.reset-inputs {
|
|
282
|
+
display: flex;
|
|
283
|
+
flex-direction: column;
|
|
284
|
+
gap: 18px;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.form-input {
|
|
288
|
+
width: 100%;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.form-input :deep(.q-field__control) {
|
|
292
|
+
height: 44px;
|
|
293
|
+
border-radius: 10px !important;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.form-input :deep(.q-field__native),
|
|
297
|
+
.form-input :deep(.q-field__input) {
|
|
298
|
+
font-size: 16px;
|
|
299
|
+
font-weight: 500;
|
|
300
|
+
line-height: 1.5em;
|
|
301
|
+
letter-spacing: -0.5px;
|
|
302
|
+
color: #103140;
|
|
303
|
+
padding: 0 16px;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.form-input :deep(.q-field__marginal) {
|
|
307
|
+
height: 44px;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.form-input :deep(input::placeholder) {
|
|
311
|
+
color: #8f9da3;
|
|
312
|
+
opacity: 1;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.form-input :deep(.q-field__bottom) {
|
|
316
|
+
font-size: 12px;
|
|
317
|
+
font-weight: 400;
|
|
318
|
+
line-height: 1.5em;
|
|
319
|
+
letter-spacing: -0.5px;
|
|
320
|
+
color: #ef476f;
|
|
321
|
+
padding-top: 4px;
|
|
322
|
+
padding-left: 0;
|
|
323
|
+
min-height: 20px;
|
|
324
|
+
}
|
|
325
|
+
:deep(.q-field--with-bottom) {
|
|
326
|
+
padding-bottom: 8px;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.password-toggle {
|
|
330
|
+
cursor: pointer;
|
|
331
|
+
color: #103140;
|
|
332
|
+
font-size: 24px;
|
|
333
|
+
padding-right: 6px;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.requirements-checklist {
|
|
337
|
+
background: #f2f4f6;
|
|
338
|
+
border-radius: 8px;
|
|
339
|
+
padding: 17px 16px 16px;
|
|
340
|
+
display: grid;
|
|
341
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
342
|
+
gap: 8px 16px;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.requirement-item {
|
|
346
|
+
display: flex;
|
|
347
|
+
align-items: center;
|
|
348
|
+
gap: 8px;
|
|
349
|
+
color: #3e484f;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.requirement-icon {
|
|
353
|
+
color: #bbcbd2;
|
|
354
|
+
flex-shrink: 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.requirement-item--valid {
|
|
358
|
+
color: #06d6a0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.requirement-item--valid .requirement-icon {
|
|
362
|
+
color: #06d6a0;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.requirement-label {
|
|
366
|
+
font-size: 12px;
|
|
367
|
+
font-weight: 500;
|
|
368
|
+
line-height: 14.4px;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.reset-button {
|
|
372
|
+
width: 100%;
|
|
373
|
+
height: 54px;
|
|
374
|
+
background: #40c4ff;
|
|
375
|
+
color: #ffffff;
|
|
376
|
+
font-size: 16px;
|
|
377
|
+
font-weight: 600;
|
|
378
|
+
line-height: 24px;
|
|
379
|
+
border-radius: 15px;
|
|
380
|
+
text-transform: none;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.reset-button:hover {
|
|
384
|
+
background: #35b0e6;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.reset-button :deep(.q-icon) {
|
|
388
|
+
font-size: 16px;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.back-section {
|
|
392
|
+
display: flex;
|
|
393
|
+
justify-content: center;
|
|
394
|
+
align-items: center;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.back-button {
|
|
398
|
+
display: flex;
|
|
399
|
+
align-items: center;
|
|
400
|
+
gap: 10px;
|
|
401
|
+
color: #103140;
|
|
402
|
+
font-size: 16px;
|
|
403
|
+
font-weight: 600;
|
|
404
|
+
line-height: 1.5em;
|
|
405
|
+
padding: 10px;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.back-button :deep(.q-icon) {
|
|
409
|
+
margin-right: 0;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.success-content {
|
|
413
|
+
display: flex;
|
|
414
|
+
flex-direction: column;
|
|
415
|
+
align-items: center;
|
|
416
|
+
gap: 24px;
|
|
417
|
+
text-align: center;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.success-icon-outer {
|
|
421
|
+
width: 96px;
|
|
422
|
+
height: 96px;
|
|
423
|
+
border-radius: 9999px;
|
|
424
|
+
background: rgba(6, 214, 160, 0.2);
|
|
425
|
+
display: flex;
|
|
426
|
+
align-items: center;
|
|
427
|
+
justify-content: center;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.success-icon-inner {
|
|
431
|
+
width: 40px;
|
|
432
|
+
height: 40px;
|
|
433
|
+
border-radius: 9999px;
|
|
434
|
+
background: #06d6a0;
|
|
435
|
+
display: flex;
|
|
436
|
+
align-items: center;
|
|
437
|
+
justify-content: center;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.success-title {
|
|
441
|
+
font-family: "Poppins", sans-serif;
|
|
442
|
+
font-size: 27px;
|
|
443
|
+
font-weight: 600;
|
|
444
|
+
line-height: 40px;
|
|
445
|
+
letter-spacing: 0.1px;
|
|
446
|
+
color: #152f3b;
|
|
447
|
+
margin: 0;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.success-subtitle {
|
|
451
|
+
font-size: 16px;
|
|
452
|
+
font-weight: 400;
|
|
453
|
+
line-height: normal;
|
|
454
|
+
letter-spacing: 0.1px;
|
|
455
|
+
color: #152f3b;
|
|
456
|
+
margin: 0;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.connect-button {
|
|
460
|
+
width: 100%;
|
|
461
|
+
height: 56px;
|
|
462
|
+
background: #40c4ff;
|
|
463
|
+
color: #ffffff;
|
|
464
|
+
font-size: 16px;
|
|
465
|
+
font-weight: 600;
|
|
466
|
+
line-height: 24px;
|
|
467
|
+
border-radius: 15px;
|
|
468
|
+
text-transform: none;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.connect-button:hover {
|
|
472
|
+
background: #35b0e6;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
@media screen and (max-width: 768px) {
|
|
476
|
+
.reset-card {
|
|
477
|
+
padding: 32px 24px;
|
|
478
|
+
max-width: 100%;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.logo {
|
|
482
|
+
width: 160px;
|
|
483
|
+
height: auto;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.reset-title,
|
|
487
|
+
.success-title {
|
|
488
|
+
font-size: 22px;
|
|
489
|
+
line-height: 1.3em;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.requirements-checklist {
|
|
493
|
+
grid-template-columns: 1fr;
|
|
494
|
+
}
|
|
120
495
|
}
|
|
121
496
|
</style>
|
|
@@ -1,7 +1,7 @@
|
|
|
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
|
-
import { computed, ref, Ref, onMounted } from "vue"
|
|
4
|
+
import { computed, ref, Ref, onMounted, watch } from "vue"
|
|
5
5
|
import {
|
|
6
6
|
ionCloseOutline,
|
|
7
7
|
ionEyeOutline,
|
|
@@ -19,18 +19,21 @@ 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
|
}
|
|
35
|
+
type ValidatableInput = { resetValidation: () => void }
|
|
36
|
+
const confirmPasswordInput = ref<ValidatableInput | null>(null)
|
|
34
37
|
|
|
35
38
|
const { patch, post } = $api()
|
|
36
39
|
const router = $router()
|
|
@@ -49,26 +52,86 @@ export function useResetPassword() {
|
|
|
49
52
|
|
|
50
53
|
const isOtp = computed(() => IS_OTP.toLowerCase() === "y")
|
|
51
54
|
|
|
55
|
+
const hasMinLength = computed(() => password.value.length >= 8)
|
|
56
|
+
const hasUppercase = computed(() => /[A-Z]/.test(password.value))
|
|
57
|
+
const hasLowercase = computed(() => /[a-z]/.test(password.value))
|
|
58
|
+
const hasNumber = computed(() => /[0-9]/.test(password.value))
|
|
59
|
+
const hasLetter = computed(() => /[a-zA-Z]/.test(password.value))
|
|
60
|
+
const hasSpecialChar = computed(() =>
|
|
61
|
+
/[^a-zA-Z0-9\s]/.test(password.value)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const isPasswordValid = computed(
|
|
65
|
+
() =>
|
|
66
|
+
hasMinLength.value &&
|
|
67
|
+
hasUppercase.value &&
|
|
68
|
+
hasLowercase.value &&
|
|
69
|
+
hasNumber.value &&
|
|
70
|
+
hasLetter.value &&
|
|
71
|
+
hasSpecialChar.value
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const passwordRequirements = computed(() => [
|
|
75
|
+
{ label: t("auth.passwordRequirement.minLength"), valid: hasMinLength.value },
|
|
76
|
+
{ label: t("auth.passwordRequirement.uppercase"), valid: hasUppercase.value },
|
|
77
|
+
{ label: t("auth.passwordRequirement.lowercase"), valid: hasLowercase.value },
|
|
78
|
+
{ label: t("auth.passwordRequirement.number"), valid: hasNumber.value },
|
|
79
|
+
{ label: t("auth.passwordRequirement.letter"), valid: hasLetter.value },
|
|
80
|
+
{
|
|
81
|
+
label: t("auth.passwordRequirement.specialChar"),
|
|
82
|
+
valid: hasSpecialChar.value
|
|
83
|
+
}
|
|
84
|
+
])
|
|
85
|
+
|
|
52
86
|
const canSubmit = computed(() => {
|
|
53
87
|
return (
|
|
54
88
|
(decodedToken.value !== null || userInfos?.uid) &&
|
|
55
|
-
|
|
89
|
+
isPasswordValid.value &&
|
|
90
|
+
confirmPassword.value === password.value
|
|
56
91
|
)
|
|
57
92
|
})
|
|
58
93
|
|
|
59
94
|
const validations = {
|
|
60
95
|
passwordRules: [
|
|
61
96
|
(val: string) => !!val || t("auth.passwordPresenceError"),
|
|
62
|
-
(
|
|
97
|
+
() => isPasswordValid.value || t("auth.passwordRequirementsError")
|
|
63
98
|
],
|
|
64
99
|
passwordConfirmationRules: [
|
|
65
|
-
(val: string) =>
|
|
66
|
-
val === password.value || t("auth.newPasswordConfirmationFailed"),
|
|
67
100
|
(val: string) => !!val || t("auth.passwordPresenceError"),
|
|
68
|
-
(val: string) =>
|
|
101
|
+
(val: string) =>
|
|
102
|
+
val === password.value || t("auth.newPasswordConfirmationFailed")
|
|
69
103
|
]
|
|
70
104
|
}
|
|
71
105
|
|
|
106
|
+
// Quasar's lazy-rules only re-validates the confirmation field on blur/submit,
|
|
107
|
+
// never while typing. Without this, correcting the field after an error was
|
|
108
|
+
// shown (e.g. clearing it, then retyping) leaves the previous, now-stale
|
|
109
|
+
// error message on screen until the field is blurred again.
|
|
110
|
+
watch([password, confirmPassword], () => {
|
|
111
|
+
confirmPasswordInput.value?.resetValidation()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const KNOWN_API_PASSWORD_ERRORS: Array<{ pattern: RegExp; key: string }> = [
|
|
115
|
+
{
|
|
116
|
+
pattern: /simple sequence of \d\+ characters/i,
|
|
117
|
+
key: "auth.passwordSequenceError"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
pattern: /\d+ or more sequential characters/i,
|
|
121
|
+
key: "auth.passwordSequenceError"
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
const translateApiErrorDetail = (detail?: string) => {
|
|
126
|
+
if (!detail) {
|
|
127
|
+
return undefined
|
|
128
|
+
}
|
|
129
|
+
const knownError = KNOWN_API_PASSWORD_ERRORS.find((entry) =>
|
|
130
|
+
entry.pattern.test(detail)
|
|
131
|
+
)
|
|
132
|
+
return knownError ? t(knownError.key) : undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
72
135
|
const decodeKey = (key: string) => {
|
|
73
136
|
try {
|
|
74
137
|
// remove backslash to avoid decoding error
|
|
@@ -129,24 +192,37 @@ export function useResetPassword() {
|
|
|
129
192
|
.then(() => {
|
|
130
193
|
loading.value = false
|
|
131
194
|
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
|
-
})
|
|
195
|
+
LocalStorage.remove("emailResetPassword")
|
|
196
|
+
LocalStorage.remove("userInfos")
|
|
149
197
|
})
|
|
198
|
+
.catch(
|
|
199
|
+
(error: {
|
|
200
|
+
response?: {
|
|
201
|
+
status?: number
|
|
202
|
+
data?: { errors?: Array<{ detail?: string; title?: string }> }
|
|
203
|
+
}
|
|
204
|
+
}) => {
|
|
205
|
+
loading.value = false
|
|
206
|
+
|
|
207
|
+
const apiErrorDetail =
|
|
208
|
+
error.response?.data?.errors?.[0]?.detail ||
|
|
209
|
+
error.response?.data?.errors?.[0]?.title
|
|
210
|
+
const apiErrorMessage = translateApiErrorDetail(apiErrorDetail)
|
|
211
|
+
|
|
212
|
+
notify({
|
|
213
|
+
message: apiErrorMessage || t("auth.passwordRecoveryFailed"),
|
|
214
|
+
color: "negative",
|
|
215
|
+
icon: ionAlertCircleOutline,
|
|
216
|
+
position: "top",
|
|
217
|
+
actions: [
|
|
218
|
+
{
|
|
219
|
+
color: "white",
|
|
220
|
+
icon: ionCloseOutline
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
)
|
|
150
226
|
}
|
|
151
227
|
|
|
152
228
|
const goToLoginPage = () => {
|
|
@@ -221,12 +297,15 @@ export function useResetPassword() {
|
|
|
221
297
|
|
|
222
298
|
return {
|
|
223
299
|
password,
|
|
300
|
+
confirmPassword,
|
|
224
301
|
loading,
|
|
225
302
|
passwordUpdated,
|
|
226
303
|
showPassword,
|
|
227
304
|
validations,
|
|
305
|
+
passwordRequirements,
|
|
228
306
|
canSubmit,
|
|
229
307
|
LOCAL_LOGIN_ROUTE,
|
|
308
|
+
confirmPasswordInput,
|
|
230
309
|
onSubmit,
|
|
231
310
|
goToLoginPage,
|
|
232
311
|
icons: {
|
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
|
}
|