@sowell/quasar-app-extension-auth 1.8.0 → 1.8.2
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
CHANGED
|
@@ -128,7 +128,9 @@
|
|
|
128
128
|
</div>
|
|
129
129
|
<div class="channel-text">
|
|
130
130
|
<div class="channel-label">{{ $t("auth.channelEmailLabel") }}</div>
|
|
131
|
-
<div class="channel-description"
|
|
131
|
+
<div class="channel-description" data-testid="channel-email-hint">
|
|
132
|
+
{{ channelHint("email") }}
|
|
133
|
+
</div>
|
|
132
134
|
</div>
|
|
133
135
|
<div
|
|
134
136
|
class="channel-radio"
|
|
@@ -149,7 +151,9 @@
|
|
|
149
151
|
</div>
|
|
150
152
|
<div class="channel-text">
|
|
151
153
|
<div class="channel-label">{{ $t("auth.channelSmsLabel") }}</div>
|
|
152
|
-
<div class="channel-description"
|
|
154
|
+
<div class="channel-description" data-testid="channel-sms-hint">
|
|
155
|
+
{{ channelHint("sms") }}
|
|
156
|
+
</div>
|
|
153
157
|
</div>
|
|
154
158
|
<div
|
|
155
159
|
class="channel-radio"
|
|
@@ -197,12 +201,15 @@
|
|
|
197
201
|
<div class="subtitle text-center q-mt-sm">
|
|
198
202
|
{{ $t("auth.magicLinkSentDescription") }}
|
|
199
203
|
</div>
|
|
200
|
-
<div
|
|
201
|
-
|
|
204
|
+
<div
|
|
205
|
+
class="subtitle-secondary text-center q-mt-md"
|
|
206
|
+
data-testid="magic-link-check-message">
|
|
207
|
+
{{ checkChannelMessage }}
|
|
202
208
|
</div>
|
|
203
209
|
</div>
|
|
204
210
|
|
|
205
211
|
<q-btn
|
|
212
|
+
v-if="!isSmsChannel"
|
|
206
213
|
data-testid="open-mailbox-btn"
|
|
207
214
|
data-cy="open-mailbox"
|
|
208
215
|
color="primary"
|
|
@@ -236,7 +243,9 @@
|
|
|
236
243
|
<div class="banner-icon-badge">
|
|
237
244
|
<q-icon :name="icons.ionInformationCircleOutline" size="12px" color="white" />
|
|
238
245
|
</div>
|
|
239
|
-
<span class="q-ml-sm"
|
|
246
|
+
<span class="q-ml-sm" data-testid="magic-link-warning">{{
|
|
247
|
+
magicLinkWarningMessage
|
|
248
|
+
}}</span>
|
|
240
249
|
</div>
|
|
241
250
|
</template>
|
|
242
251
|
</div>
|
|
@@ -244,7 +253,8 @@
|
|
|
244
253
|
</template>
|
|
245
254
|
|
|
246
255
|
<script lang="ts">
|
|
247
|
-
import { defineComponent } from "vue"
|
|
256
|
+
import { computed, defineComponent } from "vue"
|
|
257
|
+
import { useI18n } from "vue-i18n"
|
|
248
258
|
import {
|
|
249
259
|
ionWarningOutline,
|
|
250
260
|
ionInformationCircleOutline
|
|
@@ -254,11 +264,14 @@ import { useMagicLink } from "composables/useMagicLink"
|
|
|
254
264
|
export default defineComponent({
|
|
255
265
|
name: "FirstLogin",
|
|
256
266
|
setup() {
|
|
267
|
+
const { t, te } = useI18n()
|
|
268
|
+
|
|
257
269
|
const {
|
|
258
270
|
email,
|
|
259
271
|
emailError,
|
|
260
272
|
apiErrorMessage,
|
|
261
273
|
channels,
|
|
274
|
+
channelTargets,
|
|
262
275
|
selectedChannel,
|
|
263
276
|
step,
|
|
264
277
|
isLoading,
|
|
@@ -280,11 +293,41 @@ export default defineComponent({
|
|
|
280
293
|
ionInformationCircleOutline
|
|
281
294
|
}
|
|
282
295
|
|
|
296
|
+
const translateOrFallback = (key: string, fallbackKey: string): string =>
|
|
297
|
+
te(key) ? t(key) : t(fallbackKey)
|
|
298
|
+
|
|
299
|
+
const channelHint = (channel: "email" | "sms"): string => {
|
|
300
|
+
const target = channelTargets.value[channel]
|
|
301
|
+
if (target) return target
|
|
302
|
+
|
|
303
|
+
return channel === "email"
|
|
304
|
+
? maskedEmail.value
|
|
305
|
+
: t("auth.channelSmsDescription")
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const isSmsChannel = computed(() => selectedChannel.value === "sms")
|
|
309
|
+
|
|
310
|
+
const checkChannelMessage = computed(() =>
|
|
311
|
+
isSmsChannel.value
|
|
312
|
+
? translateOrFallback(
|
|
313
|
+
"auth.magicLinkCheckSmsMessage",
|
|
314
|
+
"auth.magicLinkCheckInboxMessage"
|
|
315
|
+
)
|
|
316
|
+
: t("auth.magicLinkCheckInboxMessage")
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
const magicLinkWarningMessage = computed(() =>
|
|
320
|
+
isSmsChannel.value
|
|
321
|
+
? translateOrFallback("auth.magicLinkWarningSms", "auth.magicLinkWarning")
|
|
322
|
+
: t("auth.magicLinkWarning")
|
|
323
|
+
)
|
|
324
|
+
|
|
283
325
|
return {
|
|
284
326
|
email,
|
|
285
327
|
emailError,
|
|
286
328
|
apiErrorMessage,
|
|
287
329
|
channels,
|
|
330
|
+
channelTargets,
|
|
288
331
|
selectedChannel,
|
|
289
332
|
step,
|
|
290
333
|
isLoading,
|
|
@@ -294,6 +337,10 @@ export default defineComponent({
|
|
|
294
337
|
isActivateContext,
|
|
295
338
|
maskedEmail,
|
|
296
339
|
icons,
|
|
340
|
+
channelHint,
|
|
341
|
+
isSmsChannel,
|
|
342
|
+
checkChannelMessage,
|
|
343
|
+
magicLinkWarningMessage,
|
|
297
344
|
requestChannels,
|
|
298
345
|
sendMagicLink,
|
|
299
346
|
resend,
|
|
@@ -8,6 +8,36 @@ import { $api, $prompts, $router } from "../injects"
|
|
|
8
8
|
type Channel = "email" | "sms"
|
|
9
9
|
type Step = "email" | "channel" | "sent"
|
|
10
10
|
|
|
11
|
+
interface ChannelEntry {
|
|
12
|
+
channel: string
|
|
13
|
+
target?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ChannelTargets = Partial<Record<Channel, string>>
|
|
17
|
+
|
|
18
|
+
const AVAILABLE_CHANNELS: Channel[] = ["email", "sms"]
|
|
19
|
+
|
|
20
|
+
const isChannel = (value: string): value is Channel =>
|
|
21
|
+
(AVAILABLE_CHANNELS as string[]).includes(value)
|
|
22
|
+
|
|
23
|
+
const parseChannels = (
|
|
24
|
+
entries: Array<string | ChannelEntry> | undefined
|
|
25
|
+
): { channels: Channel[]; targets: ChannelTargets } => {
|
|
26
|
+
const channels: Channel[] = []
|
|
27
|
+
const targets: ChannelTargets = {}
|
|
28
|
+
|
|
29
|
+
for (const entry of entries || []) {
|
|
30
|
+
const name = typeof entry === "string" ? entry : entry?.channel
|
|
31
|
+
if (!name || !isChannel(name)) continue
|
|
32
|
+
|
|
33
|
+
channels.push(name)
|
|
34
|
+
const target = typeof entry === "string" ? undefined : entry?.target
|
|
35
|
+
if (target) targets[name] = target
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { channels, targets }
|
|
39
|
+
}
|
|
40
|
+
|
|
11
41
|
interface ApiErrorPayload {
|
|
12
42
|
errors?: Array<{ detail?: string; title?: string }>
|
|
13
43
|
}
|
|
@@ -29,6 +59,7 @@ export function useMagicLink() {
|
|
|
29
59
|
const emailError = ref("")
|
|
30
60
|
const apiErrorMessage = ref("")
|
|
31
61
|
const channels = ref<Channel[]>([])
|
|
62
|
+
const channelTargets = ref<ChannelTargets>({})
|
|
32
63
|
const selectedChannel = ref<Channel>("email")
|
|
33
64
|
const step = ref<Step>("email")
|
|
34
65
|
const isLoading = ref(false)
|
|
@@ -90,7 +121,9 @@ export function useMagicLink() {
|
|
|
90
121
|
const { data } = await api.post(AUTH_SERVER_REQUEST_MAGICLINK_ROUTE, {
|
|
91
122
|
auth: { email: email.value.trim() }
|
|
92
123
|
})
|
|
93
|
-
channels
|
|
124
|
+
const { channels: available, targets } = parseChannels(data?.channels)
|
|
125
|
+
channels.value = available.length ? available : ["email"]
|
|
126
|
+
channelTargets.value = targets
|
|
94
127
|
selectedChannel.value = "email"
|
|
95
128
|
step.value = "channel"
|
|
96
129
|
} catch (error) {
|
|
@@ -189,6 +222,7 @@ export function useMagicLink() {
|
|
|
189
222
|
emailError,
|
|
190
223
|
apiErrorMessage,
|
|
191
224
|
channels,
|
|
225
|
+
channelTargets,
|
|
192
226
|
selectedChannel,
|
|
193
227
|
step,
|
|
194
228
|
isLoading,
|
|
@@ -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/en-US/index.ts
CHANGED
|
@@ -49,7 +49,10 @@ export default {
|
|
|
49
49
|
back: "Back",
|
|
50
50
|
backToLoginFull: "Back to login",
|
|
51
51
|
continueWith: "Or continue with",
|
|
52
|
-
emailOrPasswordIncorrect: "Email or password incorrect"
|
|
52
|
+
emailOrPasswordIncorrect: "Email or password incorrect",
|
|
53
|
+
magicLinkCheckSmsMessage: "Check your text messages",
|
|
54
|
+
magicLinkWarningSms:
|
|
55
|
+
"If you do not receive anything within 2 minutes, check your mobile network or contact support."
|
|
53
56
|
},
|
|
54
57
|
common: {
|
|
55
58
|
register: "Save",
|
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:
|
|
@@ -52,7 +52,10 @@ export default {
|
|
|
52
52
|
back: "Retour",
|
|
53
53
|
backToLoginFull: "Retour à la connexion",
|
|
54
54
|
continueWith: "Ou continuer avec",
|
|
55
|
-
emailOrPasswordIncorrect: "Email ou mot de passe incorrect"
|
|
55
|
+
emailOrPasswordIncorrect: "Email ou mot de passe incorrect",
|
|
56
|
+
magicLinkCheckSmsMessage: "Vérifiez vos messages",
|
|
57
|
+
magicLinkWarningSms:
|
|
58
|
+
"Si vous ne recevez rien d'ici 2 minutes, vérifiez votre réseau mobile ou contactez le support."
|
|
56
59
|
},
|
|
57
60
|
common: {
|
|
58
61
|
register: "Enregistrer",
|