@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,864 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="login-container">
|
|
3
|
+
<div class="login-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
|
+
<div class="login-content">
|
|
12
|
+
<!-- Step: forbidden (403) - takes precedence over `step`, the backend can
|
|
13
|
+
reject on either magiclink call (with or without channel). -->
|
|
14
|
+
<template v-if="restricted">
|
|
15
|
+
<div class="forbidden-content" data-testid="magic-link-forbidden">
|
|
16
|
+
<div class="warning-icon-outer">
|
|
17
|
+
<svg width="32" height="30" viewBox="0 0 32 30" fill="none">
|
|
18
|
+
<path
|
|
19
|
+
d="M31.33 26.0414L18.0152 1.31484C17.0714 -0.438281 14.5574 -0.438281 13.6128 1.31484L0.298773 26.0414C0.0938612 26.422 -0.00887221 26.8492 0.000600544 27.2814C0.0100733 27.7135 0.131429 28.1358 0.352821 28.507C0.574214 28.8783 0.888079 29.1858 1.26378 29.3995C1.63948 29.6133 2.06418 29.7259 2.49643 29.7266H29.1285C29.561 29.7266 29.9862 29.6144 30.3625 29.401C30.7387 29.1875 31.0531 28.8801 31.275 28.5087C31.4968 28.1374 31.6185 27.7148 31.6282 27.2823C31.6378 26.8499 31.5351 26.4223 31.33 26.0414V26.0414ZM15.8144 25.8984C15.5054 25.8984 15.2033 25.8068 14.9463 25.6351C14.6894 25.4634 14.4891 25.2194 14.3708 24.9339C14.2526 24.6484 14.2216 24.3342 14.2819 24.0311C14.3422 23.728 14.491 23.4496 14.7095 23.2311C14.9281 23.0126 15.2065 22.8638 15.5096 22.8035C15.8127 22.7432 16.1268 22.7741 16.4123 22.8924C16.6978 23.0106 16.9419 23.2109 17.1136 23.4679C17.2853 23.7248 17.3769 24.0269 17.3769 24.3359C17.3769 24.5411 17.3365 24.7443 17.258 24.9339C17.1794 25.1235 17.0643 25.2957 16.9193 25.4408C16.7742 25.5859 16.6019 25.701 16.4123 25.7795C16.2228 25.858 16.0196 25.8984 15.8144 25.8984ZM17.5113 10.1836L17.0628 19.7148C17.0628 20.0464 16.9311 20.3643 16.6967 20.5987C16.4623 20.8331 16.1444 20.9648 15.8128 20.9648C15.4813 20.9648 15.1634 20.8331 14.929 20.5987C14.6945 20.3643 14.5628 20.0464 14.5628 19.7148L14.1144 10.1875C14.1043 9.95983 14.1402 9.73247 14.2198 9.51896C14.2995 9.30545 14.4213 9.11017 14.5781 8.94475C14.7348 8.77933 14.9233 8.64716 15.1322 8.55612C15.3411 8.46508 15.5662 8.41703 15.7941 8.41484H15.8105C16.0399 8.41473 16.267 8.46109 16.478 8.55114C16.689 8.6412 16.8796 8.77307 17.0383 8.9388C17.1969 9.10454 17.3204 9.3007 17.4011 9.51545C17.4819 9.73019 17.5183 9.95908 17.5081 10.1883L17.5113 10.1836Z"
|
|
20
|
+
fill="#FFBD3D" />
|
|
21
|
+
</svg>
|
|
22
|
+
</div>
|
|
23
|
+
<h1 class="forbidden-title" data-testid="forbidden-title">
|
|
24
|
+
{{ $t("auth.restrictedTitle") }}
|
|
25
|
+
</h1>
|
|
26
|
+
<p class="forbidden-message" data-testid="forbidden-message">
|
|
27
|
+
{{ forbiddenMessageParts.before
|
|
28
|
+
}}<strong>{{ forbiddenMessageParts.bold }}</strong
|
|
29
|
+
>{{ forbiddenMessageParts.after }}
|
|
30
|
+
</p>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<a
|
|
34
|
+
href="#"
|
|
35
|
+
class="back-link back-link--center back-link--lg"
|
|
36
|
+
data-testid="back-to-login-link"
|
|
37
|
+
@click.prevent="goBackToLogin">
|
|
38
|
+
<q-icon :name="icons.ionArrowBackOutline" size="24px" />
|
|
39
|
+
{{ $t("auth.backToLoginFull") }}
|
|
40
|
+
</a>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<!-- Step: email -->
|
|
44
|
+
<template v-else-if="step === 'email'">
|
|
45
|
+
<div class="step-header" data-testid="first-login-email-step">
|
|
46
|
+
<h1 class="step-title" data-testid="first-login-title">
|
|
47
|
+
{{ stepTitle }}
|
|
48
|
+
</h1>
|
|
49
|
+
<p class="step-subtitle" data-testid="first-login-description">
|
|
50
|
+
{{ stepDescription }}
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<q-form
|
|
55
|
+
class="login-form"
|
|
56
|
+
data-testid="first-login-email-form"
|
|
57
|
+
@submit.prevent="requestChannels">
|
|
58
|
+
<div class="form-group">
|
|
59
|
+
<q-input
|
|
60
|
+
v-model.trim="email"
|
|
61
|
+
type="email"
|
|
62
|
+
:placeholder="$t('auth.emailPlaceholder')"
|
|
63
|
+
:error="!!emailError"
|
|
64
|
+
:error-message="emailError"
|
|
65
|
+
data-testid="first-login-email-input"
|
|
66
|
+
data-cy="first-login-email"
|
|
67
|
+
outlined
|
|
68
|
+
no-shadow
|
|
69
|
+
class="form-input" />
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<q-btn
|
|
73
|
+
:label="$t('auth.continueButton')"
|
|
74
|
+
no-caps
|
|
75
|
+
unelevated
|
|
76
|
+
type="submit"
|
|
77
|
+
:loading="isLoading"
|
|
78
|
+
:disable="!isEmailValid || isLoading"
|
|
79
|
+
data-testid="first-login-continue-btn"
|
|
80
|
+
data-cy="first-login-continue"
|
|
81
|
+
class="primary-button" />
|
|
82
|
+
</q-form>
|
|
83
|
+
|
|
84
|
+
<div
|
|
85
|
+
v-if="apiErrorMessage"
|
|
86
|
+
class="api-error"
|
|
87
|
+
data-testid="first-login-api-error">
|
|
88
|
+
{{ apiErrorMessage }}
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div class="or-divider">
|
|
92
|
+
<div class="or-divider-line"></div>
|
|
93
|
+
<span class="or-divider-text">{{ $t("common.or") }}</span>
|
|
94
|
+
<div class="or-divider-line"></div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<a
|
|
98
|
+
href="#"
|
|
99
|
+
class="back-link back-link--center back-link--lg"
|
|
100
|
+
data-testid="back-to-login-link"
|
|
101
|
+
@click.prevent="goBackToLogin">
|
|
102
|
+
<q-icon :name="icons.ionArrowBackOutline" size="24px" />
|
|
103
|
+
{{ $t("auth.backToLoginFull") }}
|
|
104
|
+
</a>
|
|
105
|
+
</template>
|
|
106
|
+
|
|
107
|
+
<!-- Step: channel -->
|
|
108
|
+
<template v-else-if="step === 'channel'">
|
|
109
|
+
<div class="step-header" data-testid="first-login-channel-step">
|
|
110
|
+
<h1 class="step-title" data-testid="channel-title">
|
|
111
|
+
{{ $t("auth.channelTitle") }}
|
|
112
|
+
</h1>
|
|
113
|
+
<p class="step-subtitle" data-testid="channel-subtitle">
|
|
114
|
+
{{ $t("auth.channelSubtitle") }}
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div class="channel-list" data-testid="channel-list">
|
|
119
|
+
<div
|
|
120
|
+
v-for="option in channelOptions"
|
|
121
|
+
:key="option.value"
|
|
122
|
+
class="channel-option"
|
|
123
|
+
:class="{
|
|
124
|
+
'channel-option--selected': selectedChannel === option.value
|
|
125
|
+
}"
|
|
126
|
+
:data-testid="`channel-option-${option.value}`"
|
|
127
|
+
@click="selectChannel(option.value)">
|
|
128
|
+
<div class="channel-icon-box">
|
|
129
|
+
<svg
|
|
130
|
+
v-if="option.value === 'email'"
|
|
131
|
+
width="20"
|
|
132
|
+
height="16"
|
|
133
|
+
viewBox="0 0 20 16"
|
|
134
|
+
fill="none">
|
|
135
|
+
<path
|
|
136
|
+
d="M2 16C1.45 16 0.979167 15.8042 0.5875 15.4125C0.195833 15.0208 0 14.55 0 14V2C0 1.45 0.195833 0.979167 0.5875 0.5875C0.979167 0.195833 1.45 0 2 0H18C18.55 0 19.0208 0.195833 19.4125 0.5875C19.8042 0.979167 20 1.45 20 2V14C20 14.55 19.8042 15.0208 19.4125 15.4125C19.0208 15.8042 18.55 16 18 16H2V16M10 9L2 4V14V14V14H18V14V14V4L10 9V9M10 7L18 2H2L10 7V7M2 4V2V2V4V14V14V14V14V14V14V4V4"
|
|
137
|
+
fill="#40C4FF" />
|
|
138
|
+
</svg>
|
|
139
|
+
<svg
|
|
140
|
+
v-else
|
|
141
|
+
width="20"
|
|
142
|
+
height="20"
|
|
143
|
+
viewBox="0 0 20 20"
|
|
144
|
+
fill="none">
|
|
145
|
+
<path
|
|
146
|
+
d="M6 9C6.28333 9 6.52083 8.90417 6.7125 8.7125C6.90417 8.52083 7 8.28333 7 8C7 7.71667 6.90417 7.47917 6.7125 7.2875C6.52083 7.09583 6.28333 7 6 7C5.71667 7 5.47917 7.09583 5.2875 7.2875C5.09583 7.47917 5 7.71667 5 8C5 8.28333 5.09583 8.52083 5.2875 8.7125C5.47917 8.90417 5.71667 9 6 9V9M10 9C10.2833 9 10.5208 8.90417 10.7125 8.7125C10.9042 8.52083 11 8.28333 11 8C11 7.71667 10.9042 7.47917 10.7125 7.2875C10.5208 7.09583 10.2833 7 10 7C9.71667 7 9.47917 7.09583 9.2875 7.2875C9.09583 7.47917 9 7.71667 9 8C9 8.28333 9.09583 8.52083 9.2875 8.7125C9.47917 8.90417 9.71667 9 10 9V9M14 9C14.2833 9 14.5208 8.90417 14.7125 8.7125C14.9042 8.52083 15 8.28333 15 8C15 7.71667 14.9042 7.47917 14.7125 7.2875C14.5208 7.09583 14.2833 7 14 7C13.7167 7 13.4792 7.09583 13.2875 7.2875C13.0958 7.47917 13 7.71667 13 8C13 8.28333 13.0958 8.52083 13.2875 8.7125C13.4792 8.90417 13.7167 9 14 9V9M0 20V2C0 1.45 0.195833 0.979167 0.5875 0.5875C0.979167 0.195833 1.45 0 2 0H18C18.55 0 19.0208 0.195833 19.4125 0.5875C19.8042 0.979167 20 1.45 20 2V14C20 14.55 19.8042 15.0208 19.4125 15.4125C19.0208 15.8042 18.55 16 18 16H4L0 20V20M3.15 14H18V14V14V2V2V2H2V2V2V15.125L3.15 14V14M2 14V14V2V2V2V2V2V2V14V14V14V14V14"
|
|
147
|
+
fill="#00668A" />
|
|
148
|
+
</svg>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="channel-texts">
|
|
151
|
+
<div class="channel-label" data-testid="channel-option-label">
|
|
152
|
+
{{ option.label }}
|
|
153
|
+
</div>
|
|
154
|
+
<div
|
|
155
|
+
class="channel-description"
|
|
156
|
+
data-testid="channel-option-description">
|
|
157
|
+
{{ option.description }}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<div
|
|
161
|
+
class="channel-radio"
|
|
162
|
+
:class="{
|
|
163
|
+
'channel-radio--selected': selectedChannel === option.value
|
|
164
|
+
}">
|
|
165
|
+
<div
|
|
166
|
+
v-if="selectedChannel === option.value"
|
|
167
|
+
class="channel-radio-dot" />
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<q-btn
|
|
173
|
+
:label="$t('auth.sendMagicLink')"
|
|
174
|
+
no-caps
|
|
175
|
+
unelevated
|
|
176
|
+
:loading="isLoading"
|
|
177
|
+
:disable="isLoading"
|
|
178
|
+
data-testid="send-magic-link-btn"
|
|
179
|
+
data-cy="send-magic-link"
|
|
180
|
+
class="primary-button"
|
|
181
|
+
@click="sendMagicLink(selectedChannel)" />
|
|
182
|
+
|
|
183
|
+
<div
|
|
184
|
+
v-if="apiErrorMessage"
|
|
185
|
+
class="api-error"
|
|
186
|
+
data-testid="first-login-api-error">
|
|
187
|
+
{{ apiErrorMessage }}
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<a
|
|
191
|
+
href="#"
|
|
192
|
+
class="back-link back-link--center back-link--lg"
|
|
193
|
+
data-testid="back-to-login-link"
|
|
194
|
+
@click.prevent="goBackToLogin">
|
|
195
|
+
<q-icon :name="icons.ionArrowBackOutline" size="24px" />
|
|
196
|
+
{{ $t("auth.backToLoginFull") }}
|
|
197
|
+
</a>
|
|
198
|
+
</template>
|
|
199
|
+
|
|
200
|
+
<!-- Step: sent -->
|
|
201
|
+
<template v-else>
|
|
202
|
+
<div class="sent-content" data-testid="magic-link-sent">
|
|
203
|
+
<div class="success-icon-outer">
|
|
204
|
+
<q-icon
|
|
205
|
+
:name="icons.ionCheckmarkCircle"
|
|
206
|
+
size="40px"
|
|
207
|
+
style="color: #06d6a0" />
|
|
208
|
+
</div>
|
|
209
|
+
<h1 class="sent-title" data-testid="sent-title">
|
|
210
|
+
{{ $t("auth.magicLinkSentTitle") }}
|
|
211
|
+
</h1>
|
|
212
|
+
<p class="sent-description" data-testid="sent-description">
|
|
213
|
+
{{ $t("auth.magicLinkSentDescription") }}
|
|
214
|
+
</p>
|
|
215
|
+
|
|
216
|
+
<div
|
|
217
|
+
class="info-banner"
|
|
218
|
+
:data-testid="
|
|
219
|
+
isSmsChannel ? 'check-sms-banner' : 'check-inbox-banner'
|
|
220
|
+
">
|
|
221
|
+
<q-icon
|
|
222
|
+
v-if="isSmsChannel"
|
|
223
|
+
:name="icons.ionChatbubbleEllipsesOutline"
|
|
224
|
+
size="13px"
|
|
225
|
+
color="primary" />
|
|
226
|
+
<svg v-else width="13" height="11" viewBox="0 0 13 11" fill="none">
|
|
227
|
+
<path
|
|
228
|
+
d="M8.1375 10.5L5.65833 8.02083L6.475 7.20417L8.1375 8.86667L11.4333 5.57083L12.25 6.3875L8.1375 10.5ZM5.83333 4.08333L10.5 1.16667H1.16667L5.83333 4.08333ZM5.83333 5.25L1.16667 2.33333V8.16667H4.17083L5.3375 9.33333H1.16667C0.845833 9.33333 0.571181 9.2191 0.342708 8.99063C0.114236 8.76215 0 8.4875 0 8.16667V1.16667C0 0.845833 0.114236 0.571181 0.342708 0.342708C0.571181 0.114236 0.845833 0 1.16667 0H10.5C10.8208 0 11.0955 0.114236 11.324 0.342708C11.5524 0.571181 11.6667 0.845833 11.6667 1.16667V3.70417L10.5 4.87083V2.33333L5.83333 5.25Z"
|
|
229
|
+
fill="#00668A" />
|
|
230
|
+
</svg>
|
|
231
|
+
<span data-testid="magic-link-check-message">{{
|
|
232
|
+
checkChannelMessage
|
|
233
|
+
}}</span>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<q-btn
|
|
237
|
+
no-caps
|
|
238
|
+
unelevated
|
|
239
|
+
class="resend-button"
|
|
240
|
+
:disable="resendCountdown > 0 || isLoading"
|
|
241
|
+
data-testid="resend-magic-link-btn"
|
|
242
|
+
data-cy="resend-magic-link"
|
|
243
|
+
@click="resend">
|
|
244
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
|
245
|
+
<path
|
|
246
|
+
d="M6.75 13.5C5.025 13.5 3.52187 12.9281 2.24062 11.7844C0.959375 10.6406 0.225 9.2125 0.0375 7.5H1.575C1.75 8.8 2.32812 9.875 3.30938 10.725C4.29063 11.575 5.4375 12 6.75 12C8.2125 12 9.45312 11.4906 10.4719 10.4719C11.4906 9.45312 12 8.2125 12 6.75C12 5.2875 11.4906 4.04688 10.4719 3.02813C9.45312 2.00938 8.2125 1.5 6.75 1.5C5.8875 1.5 5.08125 1.7 4.33125 2.1C3.58125 2.5 2.95 3.05 2.4375 3.75H4.5V5.25H0V0.75H1.5V2.5125C2.1375 1.7125 2.91562 1.09375 3.83437 0.65625C4.75312 0.21875 5.725 0 6.75 0C7.6875 0 8.56562 0.178125 9.38437 0.534375C10.2031 0.890625 10.9156 1.37188 11.5219 1.97812C12.1281 2.58437 12.6094 3.29688 12.9656 4.11562C13.3219 4.93437 13.5 5.8125 13.5 6.75C13.5 7.6875 13.3219 8.56562 12.9656 9.38437C12.6094 10.2031 12.1281 10.9156 11.5219 11.5219C10.9156 12.1281 10.2031 12.6094 9.38437 12.9656C8.56562 13.3219 7.6875 13.5 6.75 13.5ZM8.85 9.9L6 7.05V3H7.5V6.45L9.9 8.85L8.85 9.9Z"
|
|
247
|
+
fill="white" />
|
|
248
|
+
</svg>
|
|
249
|
+
<span>{{ resendLabel }}</span>
|
|
250
|
+
</q-btn>
|
|
251
|
+
|
|
252
|
+
<div class="warning-banner" data-testid="magic-link-warning">
|
|
253
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
254
|
+
<path
|
|
255
|
+
d="M9 15H11V9H9V15V15M10 7C10.2833 7 10.5208 6.90417 10.7125 6.7125C10.9042 6.52083 11 6.28333 11 6C11 5.71667 10.9042 5.47917 10.7125 5.2875C10.5208 5.09583 10.2833 5 10 5C9.71667 5 9.47917 5.09583 9.2875 5.2875C9.09583 5.47917 9 5.71667 9 6C9 6.28333 9.09583 6.52083 9.2875 6.7125C9.47917 6.90417 9.71667 7 10 7V7M10 20C8.61667 20 7.31667 19.7375 6.1 19.2125C4.88333 18.6875 3.825 17.975 2.925 17.075C2.025 16.175 1.3125 15.1167 0.7875 13.9C0.2625 12.6833 0 11.3833 0 10C0 8.61667 0.2625 7.31667 0.7875 6.1C1.3125 4.88333 2.025 3.825 2.925 2.925C3.825 2.025 4.88333 1.3125 6.1 0.7875C7.31667 0.2625 8.61667 0 10 0C11.3833 0 12.6833 0.2625 13.9 0.7875C15.1167 1.3125 16.175 2.025 17.075 2.925C17.975 3.825 18.6875 4.88333 19.2125 6.1C19.7375 7.31667 20 8.61667 20 10C20 11.3833 19.7375 12.6833 19.2125 13.9C18.6875 15.1167 17.975 16.175 17.075 17.075C16.175 17.975 15.1167 18.6875 13.9 19.2125C12.6833 19.7375 11.3833 20 10 20V20M10 18C12.2333 18 14.125 17.225 15.675 15.675C17.225 14.125 18 12.2333 18 10C18 7.76667 17.225 5.875 15.675 4.325C14.125 2.775 12.2333 2 10 2C7.76667 2 5.875 2.775 4.325 4.325C2.775 5.875 2 7.76667 2 10C2 12.2333 2.775 14.125 4.325 15.675C5.875 17.225 7.76667 18 10 18V18M10 10V10V10V10V10V10V10V10V10V10"
|
|
256
|
+
fill="#FFCE6E" />
|
|
257
|
+
</svg>
|
|
258
|
+
<span data-testid="magic-link-warning-label">{{
|
|
259
|
+
magicLinkWarningMessage
|
|
260
|
+
}}</span>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<div
|
|
264
|
+
v-if="apiErrorMessage"
|
|
265
|
+
class="api-error"
|
|
266
|
+
data-testid="first-login-api-error">
|
|
267
|
+
{{ apiErrorMessage }}
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<a
|
|
272
|
+
href="#"
|
|
273
|
+
class="back-link back-link--center"
|
|
274
|
+
data-testid="back-to-login-link"
|
|
275
|
+
@click.prevent="goBackToLogin">
|
|
276
|
+
<q-icon :name="icons.ionArrowBackOutline" size="20px" />
|
|
277
|
+
{{ $t("auth.backToLoginFull") }}
|
|
278
|
+
</a>
|
|
279
|
+
</template>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
</template>
|
|
284
|
+
|
|
285
|
+
<script lang="ts">
|
|
286
|
+
import { defineComponent, computed } from "vue"
|
|
287
|
+
import { useI18n } from "vue-i18n"
|
|
288
|
+
import isEmail from "validator/es/lib/isEmail"
|
|
289
|
+
import {
|
|
290
|
+
ionArrowBackOutline,
|
|
291
|
+
ionCheckmarkCircle,
|
|
292
|
+
ionChatbubbleEllipsesOutline
|
|
293
|
+
} from "@quasar/extras/ionicons-v5"
|
|
294
|
+
|
|
295
|
+
import { useMagicLink } from "composables/useMagicLink"
|
|
296
|
+
|
|
297
|
+
type Channel = "email" | "sms"
|
|
298
|
+
|
|
299
|
+
interface ChannelOption {
|
|
300
|
+
value: Channel
|
|
301
|
+
label: string
|
|
302
|
+
description: string
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
interface ForbiddenMessageParts {
|
|
306
|
+
before: string
|
|
307
|
+
bold: string
|
|
308
|
+
after: string
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// The 403 copy names Microsoft explicitly; the word is bolded wherever it lands
|
|
312
|
+
// in the sentence, so the message stays a single i18n string (or the API detail).
|
|
313
|
+
const FORBIDDEN_MESSAGE_BOLD_KEYWORD = "Microsoft"
|
|
314
|
+
|
|
315
|
+
export default defineComponent({
|
|
316
|
+
name: "WebFirstLogin",
|
|
317
|
+
setup() {
|
|
318
|
+
const { t, te } = useI18n()
|
|
319
|
+
|
|
320
|
+
const {
|
|
321
|
+
email,
|
|
322
|
+
emailError,
|
|
323
|
+
apiErrorMessage,
|
|
324
|
+
channels,
|
|
325
|
+
channelTargets,
|
|
326
|
+
selectedChannel,
|
|
327
|
+
step,
|
|
328
|
+
isLoading,
|
|
329
|
+
resendCountdown,
|
|
330
|
+
restricted,
|
|
331
|
+
restrictedMessage,
|
|
332
|
+
isActivateContext,
|
|
333
|
+
maskedEmail,
|
|
334
|
+
requestChannels,
|
|
335
|
+
sendMagicLink,
|
|
336
|
+
resend,
|
|
337
|
+
selectChannel,
|
|
338
|
+
goBackToLogin
|
|
339
|
+
} = useMagicLink()
|
|
340
|
+
|
|
341
|
+
const icons = {
|
|
342
|
+
ionArrowBackOutline,
|
|
343
|
+
ionCheckmarkCircle,
|
|
344
|
+
ionChatbubbleEllipsesOutline
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const translateOrFallback = (key: string, fallbackKey: string): string =>
|
|
348
|
+
te(key) ? t(key) : t(fallbackKey)
|
|
349
|
+
|
|
350
|
+
const stepTitle = computed(() =>
|
|
351
|
+
isActivateContext
|
|
352
|
+
? t("auth.firstConnectionTitle")
|
|
353
|
+
: t("auth.forgotPasswordTitle")
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
const stepDescription = computed(() =>
|
|
357
|
+
isActivateContext
|
|
358
|
+
? t("auth.firstConnectionDescription")
|
|
359
|
+
: t("auth.forgotPasswordDescription")
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
// The composable only fills emailError on submit; the Continue button needs
|
|
363
|
+
// to know about validity while typing, hence this local check.
|
|
364
|
+
const isEmailValid = computed(() => isEmail(email.value.trim()))
|
|
365
|
+
|
|
366
|
+
const channelDescription = (channel: Channel): string => {
|
|
367
|
+
const target = channelTargets.value[channel]
|
|
368
|
+
if (target) return target
|
|
369
|
+
|
|
370
|
+
return channel === "email"
|
|
371
|
+
? maskedEmail.value
|
|
372
|
+
: t("auth.channelSmsDescription")
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const channelOptions = computed<ChannelOption[]>(() =>
|
|
376
|
+
channels.value.map((channel) => ({
|
|
377
|
+
value: channel,
|
|
378
|
+
label:
|
|
379
|
+
channel === "email"
|
|
380
|
+
? t("auth.channelEmailLabel")
|
|
381
|
+
: t("auth.channelSmsLabel"),
|
|
382
|
+
description: channelDescription(channel)
|
|
383
|
+
}))
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
const isSmsChannel = computed(() => selectedChannel.value === "sms")
|
|
387
|
+
|
|
388
|
+
const checkChannelMessage = computed(() =>
|
|
389
|
+
isSmsChannel.value
|
|
390
|
+
? translateOrFallback(
|
|
391
|
+
"auth.magicLinkCheckSmsMessage",
|
|
392
|
+
"auth.magicLinkCheckInboxMessage"
|
|
393
|
+
)
|
|
394
|
+
: t("auth.magicLinkCheckInboxMessage")
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
const magicLinkWarningMessage = computed(() =>
|
|
398
|
+
isSmsChannel.value
|
|
399
|
+
? translateOrFallback(
|
|
400
|
+
"auth.magicLinkWarningSms",
|
|
401
|
+
"auth.magicLinkWarning"
|
|
402
|
+
)
|
|
403
|
+
: t("auth.magicLinkWarning")
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
const resendLabel = computed(() =>
|
|
407
|
+
resendCountdown.value > 0
|
|
408
|
+
? t("auth.resendMagicLinkCountdown", { seconds: resendCountdown.value })
|
|
409
|
+
: t("auth.resendMagicLink")
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
const forbiddenMessageParts = computed<ForbiddenMessageParts>(() => {
|
|
413
|
+
const message = restrictedMessage.value || t("auth.restrictedDescription")
|
|
414
|
+
const keywordIndex = message.indexOf(FORBIDDEN_MESSAGE_BOLD_KEYWORD)
|
|
415
|
+
if (keywordIndex === -1) {
|
|
416
|
+
return { before: message, bold: "", after: "" }
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
before: message.slice(0, keywordIndex),
|
|
420
|
+
bold: FORBIDDEN_MESSAGE_BOLD_KEYWORD,
|
|
421
|
+
after: message.slice(
|
|
422
|
+
keywordIndex + FORBIDDEN_MESSAGE_BOLD_KEYWORD.length
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
email,
|
|
429
|
+
emailError,
|
|
430
|
+
apiErrorMessage,
|
|
431
|
+
selectedChannel,
|
|
432
|
+
step,
|
|
433
|
+
isLoading,
|
|
434
|
+
resendCountdown,
|
|
435
|
+
restricted,
|
|
436
|
+
icons,
|
|
437
|
+
stepTitle,
|
|
438
|
+
stepDescription,
|
|
439
|
+
isEmailValid,
|
|
440
|
+
channelOptions,
|
|
441
|
+
isSmsChannel,
|
|
442
|
+
checkChannelMessage,
|
|
443
|
+
magicLinkWarningMessage,
|
|
444
|
+
resendLabel,
|
|
445
|
+
forbiddenMessageParts,
|
|
446
|
+
requestChannels,
|
|
447
|
+
sendMagicLink,
|
|
448
|
+
resend,
|
|
449
|
+
selectChannel,
|
|
450
|
+
goBackToLogin
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
})
|
|
454
|
+
</script>
|
|
455
|
+
|
|
456
|
+
<style scoped>
|
|
457
|
+
.login-container {
|
|
458
|
+
min-height: 100vh;
|
|
459
|
+
display: flex;
|
|
460
|
+
align-items: center;
|
|
461
|
+
justify-content: center;
|
|
462
|
+
padding: 20px;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.login-card {
|
|
466
|
+
background: #ffffff;
|
|
467
|
+
border-radius: 20px;
|
|
468
|
+
padding: 49px 108px;
|
|
469
|
+
width: 100%;
|
|
470
|
+
max-width: 668px;
|
|
471
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
472
|
+
display: flex;
|
|
473
|
+
flex-direction: column;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.login-content {
|
|
477
|
+
flex: 1;
|
|
478
|
+
display: flex;
|
|
479
|
+
flex-direction: column;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.logo-container {
|
|
483
|
+
display: flex;
|
|
484
|
+
justify-content: center;
|
|
485
|
+
margin-bottom: 49px;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.logo {
|
|
489
|
+
height: 53px;
|
|
490
|
+
width: auto;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.step-header {
|
|
494
|
+
margin-bottom: 29px;
|
|
495
|
+
text-align: left;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.step-title {
|
|
499
|
+
font-size: 24px;
|
|
500
|
+
font-weight: 600;
|
|
501
|
+
line-height: 1.18em;
|
|
502
|
+
letter-spacing: -0.5px;
|
|
503
|
+
color: #152f3b;
|
|
504
|
+
margin: 0 0 18px 0;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.step-subtitle {
|
|
508
|
+
font-size: 16px;
|
|
509
|
+
font-weight: 400;
|
|
510
|
+
line-height: 1.5em;
|
|
511
|
+
color: #152f3b;
|
|
512
|
+
margin: 0;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.login-form {
|
|
516
|
+
display: flex;
|
|
517
|
+
flex-direction: column;
|
|
518
|
+
gap: 16px;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.form-group {
|
|
522
|
+
display: flex;
|
|
523
|
+
flex-direction: column;
|
|
524
|
+
gap: 9px;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.form-input {
|
|
528
|
+
width: 100%;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.form-input :deep(.q-field__control) {
|
|
532
|
+
height: 65px;
|
|
533
|
+
border-radius: 15px !important;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.form-input :deep(.q-field__native) {
|
|
537
|
+
font-size: 16px;
|
|
538
|
+
font-weight: 500;
|
|
539
|
+
line-height: 1.5em;
|
|
540
|
+
letter-spacing: -0.5px;
|
|
541
|
+
color: #103140;
|
|
542
|
+
padding: 0 16px;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.form-input :deep(.q-field__bottom) {
|
|
546
|
+
font-size: 14px;
|
|
547
|
+
font-weight: 400;
|
|
548
|
+
line-height: 1.5em;
|
|
549
|
+
color: #ef476f;
|
|
550
|
+
padding-top: 9px;
|
|
551
|
+
padding-left: 0;
|
|
552
|
+
min-height: 24px;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.primary-button {
|
|
556
|
+
width: 100%;
|
|
557
|
+
height: 65px;
|
|
558
|
+
background: #40c4ff;
|
|
559
|
+
color: #ffffff;
|
|
560
|
+
font-size: 18px;
|
|
561
|
+
font-weight: 600;
|
|
562
|
+
line-height: 1.33em;
|
|
563
|
+
letter-spacing: -0.5px;
|
|
564
|
+
border-radius: 15px;
|
|
565
|
+
text-transform: none;
|
|
566
|
+
margin-top: 8px;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.primary-button:hover {
|
|
570
|
+
background: #35b0e6;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.api-error {
|
|
574
|
+
margin-top: 16px;
|
|
575
|
+
font-size: 14px;
|
|
576
|
+
line-height: 1.5em;
|
|
577
|
+
color: #ef476f;
|
|
578
|
+
text-align: center;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.channel-list {
|
|
582
|
+
display: flex;
|
|
583
|
+
flex-direction: column;
|
|
584
|
+
gap: 14px;
|
|
585
|
+
margin-bottom: 22px;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.channel-option {
|
|
589
|
+
display: flex;
|
|
590
|
+
align-items: center;
|
|
591
|
+
gap: 14px;
|
|
592
|
+
border: 1px solid #bdc8d1;
|
|
593
|
+
border-radius: 15px;
|
|
594
|
+
padding: 16px;
|
|
595
|
+
cursor: pointer;
|
|
596
|
+
transition: all 0.15s ease;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.channel-option--selected {
|
|
600
|
+
border-color: #40c4ff;
|
|
601
|
+
background: #f3fcff;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.channel-icon-box {
|
|
605
|
+
flex-shrink: 0;
|
|
606
|
+
width: 48px;
|
|
607
|
+
height: 48px;
|
|
608
|
+
border-radius: 8px;
|
|
609
|
+
background: #cff0ff;
|
|
610
|
+
display: flex;
|
|
611
|
+
align-items: center;
|
|
612
|
+
justify-content: center;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.channel-texts {
|
|
616
|
+
flex: 1;
|
|
617
|
+
min-width: 0;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.channel-label {
|
|
621
|
+
font-size: 16px;
|
|
622
|
+
font-weight: 600;
|
|
623
|
+
color: #103140;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.channel-description {
|
|
627
|
+
font-size: 14px;
|
|
628
|
+
font-weight: 400;
|
|
629
|
+
color: #607680;
|
|
630
|
+
margin-top: 2px;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.channel-radio {
|
|
634
|
+
flex-shrink: 0;
|
|
635
|
+
width: 20px;
|
|
636
|
+
height: 20px;
|
|
637
|
+
border-radius: 9999px;
|
|
638
|
+
border: 1.5px solid #bdc8d1;
|
|
639
|
+
display: flex;
|
|
640
|
+
align-items: center;
|
|
641
|
+
justify-content: center;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.channel-radio--selected {
|
|
645
|
+
border-color: #40c4ff;
|
|
646
|
+
background: #40c4ff;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.channel-radio-dot {
|
|
650
|
+
width: 10px;
|
|
651
|
+
height: 10px;
|
|
652
|
+
border-radius: 9999px;
|
|
653
|
+
background: #ffffff;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.back-link {
|
|
657
|
+
color: #001d3d;
|
|
658
|
+
text-decoration: none;
|
|
659
|
+
font-size: 15px;
|
|
660
|
+
line-height: 1.5em;
|
|
661
|
+
cursor: pointer;
|
|
662
|
+
transition: color 0.2s;
|
|
663
|
+
display: flex;
|
|
664
|
+
align-items: center;
|
|
665
|
+
gap: 8px;
|
|
666
|
+
margin-top: 15px;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.back-link:hover {
|
|
670
|
+
color: #35b0e6;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.back-link--center {
|
|
674
|
+
justify-content: center;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.back-link--lg {
|
|
678
|
+
font-size: 16px;
|
|
679
|
+
font-weight: 600;
|
|
680
|
+
margin-top: 40px;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.or-divider {
|
|
684
|
+
display: flex;
|
|
685
|
+
align-items: center;
|
|
686
|
+
width: 100%;
|
|
687
|
+
margin-top: 29px;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.or-divider-line {
|
|
691
|
+
flex: 1;
|
|
692
|
+
height: 1px;
|
|
693
|
+
background: #bdc8d1;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
.or-divider-text {
|
|
697
|
+
padding: 0 16px;
|
|
698
|
+
font-size: 12px;
|
|
699
|
+
font-weight: 500;
|
|
700
|
+
color: #6e7980;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.sent-content {
|
|
704
|
+
display: flex;
|
|
705
|
+
flex-direction: column;
|
|
706
|
+
align-items: center;
|
|
707
|
+
gap: 16px;
|
|
708
|
+
text-align: center;
|
|
709
|
+
flex: 1;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
.success-icon-outer {
|
|
713
|
+
width: 96px;
|
|
714
|
+
height: 96px;
|
|
715
|
+
border-radius: 9999px;
|
|
716
|
+
background: rgba(6, 214, 160, 0.2);
|
|
717
|
+
display: flex;
|
|
718
|
+
align-items: center;
|
|
719
|
+
justify-content: center;
|
|
720
|
+
margin-bottom: 8px;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.sent-title {
|
|
724
|
+
font-size: 24px;
|
|
725
|
+
font-weight: 600;
|
|
726
|
+
line-height: 1.18em;
|
|
727
|
+
letter-spacing: -0.5px;
|
|
728
|
+
color: #152f3b;
|
|
729
|
+
margin: 0;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.sent-description {
|
|
733
|
+
font-size: 16px;
|
|
734
|
+
font-weight: 400;
|
|
735
|
+
line-height: 1.5em;
|
|
736
|
+
color: #152f3b;
|
|
737
|
+
margin: 0;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
.info-banner {
|
|
741
|
+
width: 100%;
|
|
742
|
+
height: 56px;
|
|
743
|
+
display: flex;
|
|
744
|
+
align-items: center;
|
|
745
|
+
justify-content: center;
|
|
746
|
+
gap: 3px;
|
|
747
|
+
background: #f2f4f6;
|
|
748
|
+
border: 1px solid rgba(189, 200, 209, 0.3);
|
|
749
|
+
border-radius: 15px;
|
|
750
|
+
padding: 0 41px;
|
|
751
|
+
font-size: 14px;
|
|
752
|
+
font-weight: 600;
|
|
753
|
+
color: #3e484f;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.resend-button {
|
|
757
|
+
width: 100%;
|
|
758
|
+
height: 56px;
|
|
759
|
+
background: #40c4ff;
|
|
760
|
+
color: #ffffff;
|
|
761
|
+
font-size: 16px;
|
|
762
|
+
font-weight: 600;
|
|
763
|
+
border-radius: 15px;
|
|
764
|
+
text-transform: none;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/* q-btn wraps the default slot in .q-btn__content: a gap set on the button
|
|
768
|
+
itself would never reach the icon/label pair inside it. */
|
|
769
|
+
.resend-button :deep(.q-btn__content) {
|
|
770
|
+
display: flex;
|
|
771
|
+
align-items: center;
|
|
772
|
+
justify-content: center;
|
|
773
|
+
gap: 10px;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.resend-button:hover {
|
|
777
|
+
background: #35b0e6;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.warning-banner {
|
|
781
|
+
width: 100%;
|
|
782
|
+
display: flex;
|
|
783
|
+
align-items: flex-start;
|
|
784
|
+
gap: 10px;
|
|
785
|
+
background: rgba(255, 189, 61, 0.15);
|
|
786
|
+
border-left: 4px solid #ffce6e;
|
|
787
|
+
border-top-right-radius: 8px;
|
|
788
|
+
border-bottom-right-radius: 8px;
|
|
789
|
+
padding: 14px 16px;
|
|
790
|
+
font-size: 13px;
|
|
791
|
+
font-weight: 400;
|
|
792
|
+
line-height: 1.4em;
|
|
793
|
+
color: #607680;
|
|
794
|
+
text-align: left;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
.warning-banner svg {
|
|
798
|
+
flex-shrink: 0;
|
|
799
|
+
margin-top: 1px;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.forbidden-content {
|
|
803
|
+
display: flex;
|
|
804
|
+
flex-direction: column;
|
|
805
|
+
align-items: center;
|
|
806
|
+
gap: 16px;
|
|
807
|
+
text-align: center;
|
|
808
|
+
flex: 1;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.warning-icon-outer {
|
|
812
|
+
width: 96px;
|
|
813
|
+
height: 96px;
|
|
814
|
+
border-radius: 9999px;
|
|
815
|
+
background: rgba(255, 189, 61, 0.27);
|
|
816
|
+
display: flex;
|
|
817
|
+
align-items: center;
|
|
818
|
+
justify-content: center;
|
|
819
|
+
margin-bottom: 8px;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.forbidden-title {
|
|
823
|
+
font-size: 27px;
|
|
824
|
+
font-weight: 600;
|
|
825
|
+
line-height: 1.18em;
|
|
826
|
+
letter-spacing: -0.5px;
|
|
827
|
+
color: #152f3b;
|
|
828
|
+
margin: 0;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
.forbidden-message {
|
|
832
|
+
font-size: 16px;
|
|
833
|
+
font-weight: 400;
|
|
834
|
+
line-height: 1.5em;
|
|
835
|
+
color: #152f3b;
|
|
836
|
+
margin: 0;
|
|
837
|
+
text-align: center;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
@media screen and (max-width: 768px) {
|
|
841
|
+
.login-card {
|
|
842
|
+
padding: 32px 24px;
|
|
843
|
+
max-width: 100%;
|
|
844
|
+
min-height: auto;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.logo {
|
|
848
|
+
width: 200px;
|
|
849
|
+
height: auto;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
.step-title {
|
|
853
|
+
font-size: 20px;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
.form-input :deep(.q-field__control) {
|
|
857
|
+
height: 56px;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.primary-button {
|
|
861
|
+
height: 56px;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
</style>
|