@sowell/quasar-app-extension-auth 1.8.1 → 1.8.3
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/assets/Sowell-plus-logo.svg +31 -0
- package/src/components/pages/AdminAuth.vue +41 -2
- package/src/components/pages/Auth.vue +41 -2
- package/src/components/pages/WebAuth.vue +41 -2
- package/src/components/pages/WebFirstLogin.vue +864 -0
- package/src/components/pages/WebSetPassword.vue +580 -0
- package/src/composables/useMagicLink.ts +15 -2
- package/src/composables/useValidateMagicLink.ts +14 -5
- package/src/i18n/fr-FR/index.ts +1 -1
- package/src/injects/prompts.ts +1 -0
- package/src/prompts.js +8 -0
- package/src/routes/index.ts +15 -2
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="reset-container">
|
|
3
|
+
<div class="reset-card">
|
|
4
|
+
<div class="logo-container">
|
|
5
|
+
<img
|
|
6
|
+
src="../../assets/Sowell-plus-logo.svg"
|
|
7
|
+
alt="SoWell+"
|
|
8
|
+
class="logo" />
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<!-- Expired / already used link (401) -->
|
|
12
|
+
<template v-if="linkExpired">
|
|
13
|
+
<div class="success-content" data-testid="magic-link-expired">
|
|
14
|
+
<div class="error-icon-outer">
|
|
15
|
+
<q-icon
|
|
16
|
+
:name="icons.ionCloseCircleOutline"
|
|
17
|
+
size="40px"
|
|
18
|
+
style="color: #ef476f" />
|
|
19
|
+
</div>
|
|
20
|
+
<div class="success-subtitle" data-testid="message-dialog">
|
|
21
|
+
{{ $t("auth.error.magicLinkExpired") }}
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="back-section">
|
|
26
|
+
<q-btn
|
|
27
|
+
flat
|
|
28
|
+
no-caps
|
|
29
|
+
class="back-button"
|
|
30
|
+
:icon="icons.ionArrowBackOutline"
|
|
31
|
+
data-testid="back-to-first-login-link"
|
|
32
|
+
@click="goToFirstLogin">
|
|
33
|
+
<span class="back-text">{{ $t("auth.backToFirstLogin") }}</span>
|
|
34
|
+
</q-btn>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<!-- Password saved -->
|
|
39
|
+
<template v-else-if="passwordUpdated">
|
|
40
|
+
<div class="success-content" data-testid="feedback-dialog">
|
|
41
|
+
<div class="success-icon-outer">
|
|
42
|
+
<div class="success-icon-inner">
|
|
43
|
+
<q-icon :name="icons.ionCheckmark" size="24px" color="white" />
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="success-title" data-testid="header-dialog">
|
|
47
|
+
{{ $t("auth.passwordSavedTitle") }}
|
|
48
|
+
</div>
|
|
49
|
+
<div class="success-subtitle" data-testid="message-dialog">
|
|
50
|
+
{{ $t("auth.magicLinkPasswordChangedSubtitle") }}
|
|
51
|
+
</div>
|
|
52
|
+
<q-btn
|
|
53
|
+
unelevated
|
|
54
|
+
no-caps
|
|
55
|
+
class="connect-button"
|
|
56
|
+
:label="$t('auth.goToLogin')"
|
|
57
|
+
data-testid="close-btn"
|
|
58
|
+
@click="goToLoginPage" />
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<!-- Set password form -->
|
|
63
|
+
<template v-else>
|
|
64
|
+
<q-form
|
|
65
|
+
class="password-reset-form"
|
|
66
|
+
data-testid="set-password-form"
|
|
67
|
+
@submit.prevent="onSubmit"
|
|
68
|
+
@validation-error="handleInvalidSubmit">
|
|
69
|
+
<div class="reset-body">
|
|
70
|
+
<div class="reset-heading">
|
|
71
|
+
<h1 class="reset-title">{{ $t("auth.setPasswordTitle") }}</h1>
|
|
72
|
+
<p class="reset-subtitle">
|
|
73
|
+
{{ $t("auth.setPasswordDescription") }}
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="reset-inputs">
|
|
78
|
+
<q-input
|
|
79
|
+
v-model="password"
|
|
80
|
+
:type="showPassword ? 'text' : 'password'"
|
|
81
|
+
:placeholder="$t('auth.webNewPasswordPlaceholder')"
|
|
82
|
+
:rules="validations.passwordRules"
|
|
83
|
+
hide-bottom-space
|
|
84
|
+
lazy-rules
|
|
85
|
+
data-testid="password-input"
|
|
86
|
+
data-cy="new-password"
|
|
87
|
+
outlined
|
|
88
|
+
no-shadow
|
|
89
|
+
class="form-input">
|
|
90
|
+
<template #append>
|
|
91
|
+
<q-icon
|
|
92
|
+
:name="
|
|
93
|
+
showPassword ? icons.ionEyeOffOutline : icons.ionEyeOutline
|
|
94
|
+
"
|
|
95
|
+
class="password-toggle"
|
|
96
|
+
@click="showPassword = !showPassword" />
|
|
97
|
+
</template>
|
|
98
|
+
</q-input>
|
|
99
|
+
|
|
100
|
+
<q-input
|
|
101
|
+
ref="confirmPasswordInput"
|
|
102
|
+
v-model="confirmPassword"
|
|
103
|
+
:type="showConfirmPassword ? 'text' : 'password'"
|
|
104
|
+
:placeholder="$t('auth.confirmPassword')"
|
|
105
|
+
:rules="validations.passwordConfirmationRules"
|
|
106
|
+
lazy-rules
|
|
107
|
+
data-testid="password-confirmation-input"
|
|
108
|
+
data-cy="confirm-password"
|
|
109
|
+
outlined
|
|
110
|
+
no-shadow
|
|
111
|
+
class="form-input">
|
|
112
|
+
<template #append>
|
|
113
|
+
<q-icon
|
|
114
|
+
:name="
|
|
115
|
+
showConfirmPassword
|
|
116
|
+
? icons.ionEyeOffOutline
|
|
117
|
+
: icons.ionEyeOutline
|
|
118
|
+
"
|
|
119
|
+
class="password-toggle"
|
|
120
|
+
@click="showConfirmPassword = !showConfirmPassword" />
|
|
121
|
+
</template>
|
|
122
|
+
</q-input>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div class="requirements-checklist" data-testid="password-requirements">
|
|
126
|
+
<div
|
|
127
|
+
v-for="requirement in passwordRequirements"
|
|
128
|
+
:key="requirement.label"
|
|
129
|
+
class="requirement-item"
|
|
130
|
+
:class="{
|
|
131
|
+
'requirement-item--valid': requirement.valid,
|
|
132
|
+
'requirement-item--invalid': requirement.invalid
|
|
133
|
+
}">
|
|
134
|
+
<q-icon
|
|
135
|
+
:name="
|
|
136
|
+
requirement.valid
|
|
137
|
+
? icons.ionCheckmarkCircle
|
|
138
|
+
: icons.ionEllipseOutline
|
|
139
|
+
"
|
|
140
|
+
size="16px"
|
|
141
|
+
class="requirement-icon" />
|
|
142
|
+
<span class="requirement-label">{{ requirement.label }}</span>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<!-- Deliberately NOT `disable`: the button stays clickable so an
|
|
147
|
+
invalid submit can turn the unmet rules red instead of leaving
|
|
148
|
+
the user with a dead button and no explanation. -->
|
|
149
|
+
<q-btn
|
|
150
|
+
:label="$t('common.saveNewPassword')"
|
|
151
|
+
:icon="icons.ionLockClosedOutline"
|
|
152
|
+
no-caps
|
|
153
|
+
unelevated
|
|
154
|
+
type="submit"
|
|
155
|
+
:loading="loading"
|
|
156
|
+
data-testid="submit-btn"
|
|
157
|
+
data-cy="submit-new-password"
|
|
158
|
+
class="reset-button"
|
|
159
|
+
:class="{ 'reset-button--inactive': !canSubmit }" />
|
|
160
|
+
|
|
161
|
+
<p v-if="serverError" class="server-error" data-testid="server-error">
|
|
162
|
+
{{ serverError }}
|
|
163
|
+
</p>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div class="back-section">
|
|
167
|
+
<q-btn
|
|
168
|
+
flat
|
|
169
|
+
no-caps
|
|
170
|
+
class="back-button"
|
|
171
|
+
:icon="icons.ionArrowBackOutline"
|
|
172
|
+
data-testid="back-to-login-link"
|
|
173
|
+
@click="goToLoginPage">
|
|
174
|
+
<span class="back-text">{{ $t("auth.backToLoginFull") }}</span>
|
|
175
|
+
</q-btn>
|
|
176
|
+
</div>
|
|
177
|
+
</q-form>
|
|
178
|
+
</template>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
</template>
|
|
182
|
+
|
|
183
|
+
<script lang="ts">
|
|
184
|
+
import { defineComponent, computed, ref, onMounted } from "vue"
|
|
185
|
+
import {
|
|
186
|
+
ionArrowBackOutline,
|
|
187
|
+
ionCheckmark,
|
|
188
|
+
ionCheckmarkCircle,
|
|
189
|
+
ionCloseCircleOutline,
|
|
190
|
+
ionEllipseOutline
|
|
191
|
+
} from "@quasar/extras/ionicons-v5"
|
|
192
|
+
|
|
193
|
+
import { useValidateMagicLink } from "composables/useValidateMagicLink"
|
|
194
|
+
import { $prompts, $router } from "../../injects"
|
|
195
|
+
|
|
196
|
+
export default defineComponent({
|
|
197
|
+
name: "WebSetPassword",
|
|
198
|
+
setup() {
|
|
199
|
+
const router = $router()
|
|
200
|
+
const { LOCAL_LOGIN_ROUTE } = $prompts()
|
|
201
|
+
const $route = router.currentRoute.value
|
|
202
|
+
|
|
203
|
+
const magicToken = computed(() =>
|
|
204
|
+
(($route.query.magic as string) || "").trim()
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
onMounted(() => {
|
|
208
|
+
if (!magicToken.value) {
|
|
209
|
+
router.replace(LOCAL_LOGIN_ROUTE)
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const {
|
|
214
|
+
password,
|
|
215
|
+
confirmPassword,
|
|
216
|
+
loading,
|
|
217
|
+
passwordUpdated,
|
|
218
|
+
linkExpired,
|
|
219
|
+
serverError,
|
|
220
|
+
showPassword,
|
|
221
|
+
confirmPasswordInput,
|
|
222
|
+
validations,
|
|
223
|
+
passwordRequirements,
|
|
224
|
+
canSubmit,
|
|
225
|
+
onSubmit,
|
|
226
|
+
handleInvalidSubmit,
|
|
227
|
+
goToLoginPage,
|
|
228
|
+
goToFirstLogin,
|
|
229
|
+
icons: composableIcons
|
|
230
|
+
} = useValidateMagicLink(magicToken)
|
|
231
|
+
|
|
232
|
+
// The composable exposes a single visibility flag (mobile UI shows one field
|
|
233
|
+
// at a time); the web form shows both fields, so the confirmation gets its own.
|
|
234
|
+
const showConfirmPassword = ref(false)
|
|
235
|
+
|
|
236
|
+
const icons = {
|
|
237
|
+
...composableIcons,
|
|
238
|
+
ionArrowBackOutline,
|
|
239
|
+
ionCheckmark,
|
|
240
|
+
ionCheckmarkCircle,
|
|
241
|
+
ionCloseCircleOutline,
|
|
242
|
+
ionEllipseOutline
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
password,
|
|
247
|
+
confirmPassword,
|
|
248
|
+
loading,
|
|
249
|
+
passwordUpdated,
|
|
250
|
+
linkExpired,
|
|
251
|
+
serverError,
|
|
252
|
+
showPassword,
|
|
253
|
+
showConfirmPassword,
|
|
254
|
+
confirmPasswordInput,
|
|
255
|
+
validations,
|
|
256
|
+
passwordRequirements,
|
|
257
|
+
canSubmit,
|
|
258
|
+
icons,
|
|
259
|
+
onSubmit,
|
|
260
|
+
handleInvalidSubmit,
|
|
261
|
+
goToLoginPage,
|
|
262
|
+
goToFirstLogin
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
</script>
|
|
267
|
+
|
|
268
|
+
<style scoped>
|
|
269
|
+
.reset-container {
|
|
270
|
+
min-height: 100vh;
|
|
271
|
+
display: flex;
|
|
272
|
+
align-items: center;
|
|
273
|
+
justify-content: center;
|
|
274
|
+
padding: 20px;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.reset-card {
|
|
278
|
+
background: #ffffff;
|
|
279
|
+
border-radius: 20px;
|
|
280
|
+
padding: 52px;
|
|
281
|
+
width: 100%;
|
|
282
|
+
max-width: 634px;
|
|
283
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
284
|
+
display: flex;
|
|
285
|
+
flex-direction: column;
|
|
286
|
+
gap: 40px;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.logo-container {
|
|
290
|
+
display: flex;
|
|
291
|
+
justify-content: center;
|
|
292
|
+
position: relative;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.logo {
|
|
296
|
+
height: 53px;
|
|
297
|
+
width: auto;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.password-reset-form {
|
|
301
|
+
display: flex;
|
|
302
|
+
flex-direction: column;
|
|
303
|
+
gap: 40px;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.reset-body {
|
|
307
|
+
display: flex;
|
|
308
|
+
flex-direction: column;
|
|
309
|
+
gap: 29px;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.reset-heading {
|
|
313
|
+
display: flex;
|
|
314
|
+
flex-direction: column;
|
|
315
|
+
gap: 14px;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.reset-title {
|
|
319
|
+
font-size: 27px;
|
|
320
|
+
font-weight: 600;
|
|
321
|
+
line-height: 40px;
|
|
322
|
+
letter-spacing: 0.1px;
|
|
323
|
+
color: #152f3b;
|
|
324
|
+
margin: 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.reset-subtitle {
|
|
328
|
+
font-size: 16px;
|
|
329
|
+
font-weight: 400;
|
|
330
|
+
line-height: normal;
|
|
331
|
+
color: #152f3b;
|
|
332
|
+
margin: 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.reset-inputs {
|
|
336
|
+
display: flex;
|
|
337
|
+
flex-direction: column;
|
|
338
|
+
gap: 18px;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.form-input {
|
|
342
|
+
width: 100%;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.form-input :deep(.q-field__control) {
|
|
346
|
+
height: 44px;
|
|
347
|
+
border-radius: 10px !important;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.form-input :deep(.q-field__native),
|
|
351
|
+
.form-input :deep(.q-field__input) {
|
|
352
|
+
font-size: 16px;
|
|
353
|
+
font-weight: 500;
|
|
354
|
+
line-height: 1.5em;
|
|
355
|
+
letter-spacing: -0.5px;
|
|
356
|
+
color: #103140;
|
|
357
|
+
padding: 0 16px;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.form-input :deep(.q-field__marginal) {
|
|
361
|
+
height: 44px;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.form-input :deep(input::placeholder) {
|
|
365
|
+
color: #8f9da3;
|
|
366
|
+
opacity: 1;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.form-input :deep(.q-field__bottom) {
|
|
370
|
+
font-size: 14px;
|
|
371
|
+
font-weight: 400;
|
|
372
|
+
line-height: 1.5em;
|
|
373
|
+
letter-spacing: -0.5px;
|
|
374
|
+
color: #ef476f;
|
|
375
|
+
padding-top: 6px;
|
|
376
|
+
padding-left: 0;
|
|
377
|
+
min-height: 20px;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.password-toggle {
|
|
381
|
+
cursor: pointer;
|
|
382
|
+
color: #103140;
|
|
383
|
+
font-size: 24px;
|
|
384
|
+
padding-right: 6px;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.requirements-checklist {
|
|
388
|
+
background: #f2f4f6;
|
|
389
|
+
border-radius: 8px;
|
|
390
|
+
padding: 17px 16px 16px;
|
|
391
|
+
display: grid;
|
|
392
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
393
|
+
gap: 8px 16px;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.requirement-item {
|
|
397
|
+
display: flex;
|
|
398
|
+
align-items: center;
|
|
399
|
+
gap: 8px;
|
|
400
|
+
color: #3e484f;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.requirement-icon {
|
|
404
|
+
color: #bbcbd2;
|
|
405
|
+
flex-shrink: 0;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.requirement-item--valid {
|
|
409
|
+
color: #06d6a0;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.requirement-item--valid .requirement-icon {
|
|
413
|
+
color: #06d6a0;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.requirement-item--invalid {
|
|
417
|
+
color: #ef476f;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.requirement-item--invalid .requirement-icon {
|
|
421
|
+
color: #ef476f;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.requirement-label {
|
|
425
|
+
font-size: 12px;
|
|
426
|
+
font-weight: 500;
|
|
427
|
+
line-height: 14.4px;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.reset-button {
|
|
431
|
+
width: 100%;
|
|
432
|
+
height: 54px;
|
|
433
|
+
background: #40c4ff;
|
|
434
|
+
color: #ffffff;
|
|
435
|
+
font-size: 16px;
|
|
436
|
+
font-weight: 600;
|
|
437
|
+
line-height: 24px;
|
|
438
|
+
border-radius: 15px;
|
|
439
|
+
text-transform: none;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.reset-button:hover {
|
|
443
|
+
background: #35b0e6;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.reset-button--inactive {
|
|
447
|
+
opacity: 0.5;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.reset-button :deep(.q-icon) {
|
|
451
|
+
font-size: 16px;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.server-error {
|
|
455
|
+
font-size: 14px;
|
|
456
|
+
font-weight: 400;
|
|
457
|
+
color: #ef476f;
|
|
458
|
+
margin: 12px 0 0 0;
|
|
459
|
+
text-align: center;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.back-section {
|
|
463
|
+
display: flex;
|
|
464
|
+
justify-content: center;
|
|
465
|
+
align-items: center;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.back-button {
|
|
469
|
+
color: #103140;
|
|
470
|
+
font-size: 16px;
|
|
471
|
+
font-weight: 600;
|
|
472
|
+
line-height: 1.5em;
|
|
473
|
+
padding: 10px;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.back-button :deep(.q-btn__content) {
|
|
477
|
+
display: flex;
|
|
478
|
+
align-items: center;
|
|
479
|
+
justify-content: center;
|
|
480
|
+
gap: 10px;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.back-button :deep(.q-icon) {
|
|
484
|
+
margin-right: 0;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.success-content {
|
|
488
|
+
display: flex;
|
|
489
|
+
flex-direction: column;
|
|
490
|
+
align-items: center;
|
|
491
|
+
gap: 24px;
|
|
492
|
+
text-align: center;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.success-icon-outer {
|
|
496
|
+
width: 96px;
|
|
497
|
+
height: 96px;
|
|
498
|
+
border-radius: 9999px;
|
|
499
|
+
background: rgba(6, 214, 160, 0.2);
|
|
500
|
+
display: flex;
|
|
501
|
+
align-items: center;
|
|
502
|
+
justify-content: center;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.success-icon-inner {
|
|
506
|
+
width: 40px;
|
|
507
|
+
height: 40px;
|
|
508
|
+
border-radius: 9999px;
|
|
509
|
+
background: #06d6a0;
|
|
510
|
+
display: flex;
|
|
511
|
+
align-items: center;
|
|
512
|
+
justify-content: center;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.error-icon-outer {
|
|
516
|
+
width: 96px;
|
|
517
|
+
height: 96px;
|
|
518
|
+
border-radius: 9999px;
|
|
519
|
+
background: rgba(239, 71, 111, 0.15);
|
|
520
|
+
display: flex;
|
|
521
|
+
align-items: center;
|
|
522
|
+
justify-content: center;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.success-title {
|
|
526
|
+
font-size: 27px;
|
|
527
|
+
font-weight: 600;
|
|
528
|
+
line-height: 40px;
|
|
529
|
+
letter-spacing: 0.1px;
|
|
530
|
+
color: #152f3b;
|
|
531
|
+
margin: 0;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.success-subtitle {
|
|
535
|
+
font-size: 16px;
|
|
536
|
+
font-weight: 400;
|
|
537
|
+
line-height: normal;
|
|
538
|
+
letter-spacing: 0.1px;
|
|
539
|
+
color: #152f3b;
|
|
540
|
+
margin: 0;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.connect-button {
|
|
544
|
+
width: 100%;
|
|
545
|
+
height: 56px;
|
|
546
|
+
background: #40c4ff;
|
|
547
|
+
color: #ffffff;
|
|
548
|
+
font-size: 16px;
|
|
549
|
+
font-weight: 600;
|
|
550
|
+
line-height: 24px;
|
|
551
|
+
border-radius: 15px;
|
|
552
|
+
text-transform: none;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.connect-button:hover {
|
|
556
|
+
background: #35b0e6;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
@media screen and (max-width: 768px) {
|
|
560
|
+
.reset-card {
|
|
561
|
+
padding: 32px 24px;
|
|
562
|
+
max-width: 100%;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.logo {
|
|
566
|
+
width: 160px;
|
|
567
|
+
height: auto;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.reset-title,
|
|
571
|
+
.success-title {
|
|
572
|
+
font-size: 22px;
|
|
573
|
+
line-height: 1.3em;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.requirements-checklist {
|
|
577
|
+
grid-template-columns: 1fr;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
</style>
|
|
@@ -52,9 +52,19 @@ export function useMagicLink() {
|
|
|
52
52
|
AUTH_SERVER_REQUEST_MAGICLINK_ROUTE,
|
|
53
53
|
LOCAL_SET_PASSWORD_ROUTE,
|
|
54
54
|
LOCAL_LOGIN_ROUTE,
|
|
55
|
-
APP_SERVER_BASE_URL
|
|
55
|
+
APP_SERVER_BASE_URL,
|
|
56
|
+
MAGICLINK_CHANNELS
|
|
56
57
|
} = $prompts()
|
|
57
58
|
|
|
59
|
+
// Some backends advertise SMS for users who happen to have a phone number on
|
|
60
|
+
// file even when the product doesn't send SMS at all (the V1 API is in that
|
|
61
|
+
// case). MAGICLINK_CHANNELS lets an app restrict what it offers; left empty,
|
|
62
|
+
// whatever the API returns is shown.
|
|
63
|
+
const allowedChannels = (MAGICLINK_CHANNELS || "")
|
|
64
|
+
.split(",")
|
|
65
|
+
.map((channel: string) => channel.trim().toLowerCase())
|
|
66
|
+
.filter((channel: string): channel is Channel => isChannel(channel))
|
|
67
|
+
|
|
58
68
|
const email = ref("")
|
|
59
69
|
const emailError = ref("")
|
|
60
70
|
const apiErrorMessage = ref("")
|
|
@@ -122,7 +132,10 @@ export function useMagicLink() {
|
|
|
122
132
|
auth: { email: email.value.trim() }
|
|
123
133
|
})
|
|
124
134
|
const { channels: available, targets } = parseChannels(data?.channels)
|
|
125
|
-
|
|
135
|
+
const offered = allowedChannels.length
|
|
136
|
+
? available.filter((channel) => allowedChannels.includes(channel))
|
|
137
|
+
: available
|
|
138
|
+
channels.value = offered.length ? offered : ["email"]
|
|
126
139
|
channelTargets.value = targets
|
|
127
140
|
selectedChannel.value = "email"
|
|
128
141
|
step.value = "channel"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, ref, Ref } from "vue"
|
|
1
|
+
import { computed, ref, watch, Ref } from "vue"
|
|
2
2
|
import { useI18n } from "vue-i18n"
|
|
3
3
|
import { AxiosError } from "axios"
|
|
4
4
|
import {
|
|
@@ -91,9 +91,10 @@ export function useValidateMagicLink(magicToken: Ref<string>) {
|
|
|
91
91
|
confirmPassword.value !== ""
|
|
92
92
|
)
|
|
93
93
|
|
|
94
|
-
// NOTE: rules return a boolean
|
|
95
|
-
// the fields already explains exactly what's missing (green/gray/red), so
|
|
96
|
-
// field itself only needs to turn red
|
|
94
|
+
// NOTE: rules return a boolean rather than a string - the requirements checklist
|
|
95
|
+
// below the fields already explains exactly what's missing (green/gray/red), so
|
|
96
|
+
// the field itself only needs to turn red. The only exception is the "passwords
|
|
97
|
+
// don't match" case: no checklist item covers it, so that one needs its message.
|
|
97
98
|
const validations = {
|
|
98
99
|
passwordRules: [
|
|
99
100
|
(val: string) => !!val || false,
|
|
@@ -101,10 +102,18 @@ export function useValidateMagicLink(magicToken: Ref<string>) {
|
|
|
101
102
|
],
|
|
102
103
|
passwordConfirmationRules: [
|
|
103
104
|
(val: string) => !!val || false,
|
|
104
|
-
(val: string) =>
|
|
105
|
+
(val: string) =>
|
|
106
|
+
val === password.value || t("auth.newPasswordConfirmationFailed")
|
|
105
107
|
]
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
// Quasar's lazy-rules only re-validates the confirmation field on blur/submit,
|
|
111
|
+
// never while typing. Without this, the "passwords don't match" message stays
|
|
112
|
+
// on screen while the user is already fixing the field.
|
|
113
|
+
watch([password, confirmPassword], () => {
|
|
114
|
+
confirmPasswordInput.value?.resetValidation()
|
|
115
|
+
})
|
|
116
|
+
|
|
108
117
|
const readApiError = (error: unknown): string | undefined => {
|
|
109
118
|
const axiosError = error as AxiosError<ApiErrorPayload>
|
|
110
119
|
const apiError = axiosError.response?.data?.errors?.[0]
|
package/src/i18n/fr-FR/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ export default {
|
|
|
27
27
|
confirmPassword: "Confirmer le mot de passe",
|
|
28
28
|
confirmPasswordPlaceholder: "Répéter le mot de passe",
|
|
29
29
|
newPasswordConfirmationFailed:
|
|
30
|
-
"Les
|
|
30
|
+
"Les mots de passe que vous avez saisi ne correspondent pas",
|
|
31
31
|
passwordRequirementsError:
|
|
32
32
|
"Le mot de passe ne respecte pas les critères requis",
|
|
33
33
|
passwordSequenceError:
|
package/src/injects/prompts.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface PromptsInterface {
|
|
|
24
24
|
LOCAL_SET_PASSWORD_ROUTE: string
|
|
25
25
|
AUTH_SERVER_REQUEST_MAGICLINK_ROUTE: string
|
|
26
26
|
AUTH_SERVER_VALIDATE_MAGICLINK_ROUTE: string
|
|
27
|
+
MAGICLINK_CHANNELS?: string
|
|
27
28
|
}
|
|
28
29
|
export const $prompts = (): PromptsInterface => {
|
|
29
30
|
const $prompts = inject<Record<"@sowell/auth", PromptsInterface>>("$prompts")
|
package/src/prompts.js
CHANGED
|
@@ -168,5 +168,13 @@ module.exports = function () {
|
|
|
168
168
|
message: "API endpoint to validate a magic link and set the password (without host)",
|
|
169
169
|
default: "/validate-magiclink",
|
|
170
170
|
},
|
|
171
|
+
{
|
|
172
|
+
name: "MAGICLINK_CHANNELS",
|
|
173
|
+
type: "input",
|
|
174
|
+
required: false,
|
|
175
|
+
message:
|
|
176
|
+
"Magic-link channels offered to the user, comma separated (empty = whatever the API returns)",
|
|
177
|
+
default: "",
|
|
178
|
+
},
|
|
171
179
|
];
|
|
172
180
|
};
|