@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sowell/quasar-app-extension-auth",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
4
4
  "description": "A Quasar app extension to manage Authentication",
5
5
  "main": "src/index.js",
6
6
  "repository": "https://github.com/sowell-ai/quasar.extension.auth",
@@ -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">{{ maskedEmail }}</div>
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">{{ $t("auth.channelSmsDescription") }}</div>
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 class="subtitle-secondary text-center q-mt-md">
201
- {{ $t("auth.magicLinkCheckInboxMessage") }}
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">{{ $t("auth.magicLinkWarning") }}</span>
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.value = data?.channels?.length ? data.channels : ["email"]
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, never a string - the requirements checklist below
95
- // the fields already explains exactly what's missing (green/gray/red), so the
96
- // field itself only needs to turn red, not repeat the message underneath.
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) => val === password.value || false
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]
@@ -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",
@@ -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 mot de passes que vous avez saisi ne correspondent pas",
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",