@sowell/quasar-app-extension-auth 1.7.1 → 1.8.0

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.7.1",
3
+ "version": "1.8.0",
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",
@@ -1,4 +1,4 @@
1
- import { ref, onMounted, inject } from "vue"
1
+ import { ref, computed, onMounted, onUnmounted, inject } from "vue"
2
2
  import isEmail from "validator/es/lib/isEmail"
3
3
  import { useI18n } from "vue-i18n"
4
4
  import {
@@ -27,6 +27,11 @@ interface HeadersInterface {
27
27
  }
28
28
  }
29
29
 
30
+ // Duree de verrouillage si l'API ne renvoie pas d'en-tete Retry-After exploitable
31
+ const LOCKOUT_FALLBACK_SECONDS = 60
32
+ // Persiste l'echeance : un rechargement de page ne doit pas degriser le bouton
33
+ const LOCKOUT_STORAGE_KEY = "loginLockoutUntil"
34
+
30
35
  export function useAuth() {
31
36
  const authState = useAuthState()
32
37
  const msalState = useMsalState()
@@ -74,10 +79,45 @@ export function useAuth() {
74
79
  const errorEmailMessage = ref("")
75
80
  const errorPasswordMessage = ref("")
76
81
  const apiErrorMessage = ref("")
82
+
83
+ // Verrouillage du bouton de connexion apres trop de tentatives (HTTP 429).
84
+ // L'echeance est persistee : ni le changement d'email, ni un rechargement de
85
+ // page ne degrisent le bouton, seule l'expiration du delai le fait.
86
+ const lockedUntil = ref(0)
87
+ const isLockedOut = computed(() => lockedUntil.value > 0)
88
+ let unlockTimer: ReturnType<typeof setTimeout> | undefined
89
+
90
+ const releaseLock = () => {
91
+ lockedUntil.value = 0
92
+ localStorage.remove(LOCKOUT_STORAGE_KEY)
93
+ if (apiErrorMessage.value === t("auth.tooManyAttempts")) {
94
+ apiErrorMessage.value = ""
95
+ }
96
+ }
97
+
98
+ const lockUntil = (until: number) => {
99
+ clearTimeout(unlockTimer)
100
+ lockedUntil.value = until
101
+ localStorage.set(LOCKOUT_STORAGE_KEY, until)
102
+ unlockTimer = setTimeout(releaseLock, Math.max(until - Date.now(), 0))
103
+ }
104
+
105
+ // L'API indique le delai d'attente en secondes via Retry-After
106
+ const retryAfterSeconds = (err: { response?: { headers?: Record<string, string> } }) => {
107
+ const parsed = Number(err.response?.headers?.["retry-after"])
108
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : LOCKOUT_FALLBACK_SECONDS
109
+ }
110
+
111
+ // Pendant le verrouillage le message doit rester affiche : sinon la saisie
112
+ // d'un autre identifiant laisse un bouton grise sans aucune explication
113
+ const clearApiError = () => {
114
+ if (!isLockedOut.value) apiErrorMessage.value = ""
115
+ }
116
+
77
117
  const validations = ref({
78
118
  email: [
79
119
  (val: string) => {
80
- apiErrorMessage.value = ""
120
+ clearApiError()
81
121
  errorEmailMessage.value = !!val ? "" : t("auth.emailPresenceError")
82
122
  return !!val || t("auth.emailPresenceError")
83
123
  },
@@ -90,7 +130,7 @@ export function useAuth() {
90
130
  ],
91
131
  password: [
92
132
  (val: string) => {
93
- apiErrorMessage.value = ""
133
+ clearApiError()
94
134
  errorPasswordMessage.value = !!val
95
135
  ? ""
96
136
  : t("auth.passwordPresenceError")
@@ -122,8 +162,13 @@ export function useAuth() {
122
162
  }
123
163
 
124
164
  const onSubmit = async (provider = "SoWell") => {
165
+ // Le bouton est grise, mais la touche Entree et l'auto-login le contournent
166
+ if (isLockedOut.value) {
167
+ apiErrorMessage.value = t("auth.tooManyAttempts")
168
+ return
169
+ }
125
170
  apiErrorMessage.value = ""
126
-
171
+
127
172
  if (provider === "SoWell") {
128
173
  loading.value = true
129
174
  }
@@ -181,7 +226,10 @@ export function useAuth() {
181
226
 
182
227
  if (err && typeof err === "object" && "response" in err) {
183
228
  const axiosError = err as any
184
- if (axiosError.response?.status === 401) {
229
+ if (axiosError.response?.status === 429) {
230
+ lockUntil(Date.now() + retryAfterSeconds(axiosError) * 1000)
231
+ apiErrorMessage.value = t("auth.tooManyAttempts")
232
+ } else if (axiosError.response?.status === 401) {
185
233
  apiErrorMessage.value = t("auth.loginFailed")
186
234
  } else if (axiosError.response?.status === 403) {
187
235
  apiErrorMessage.value = t("auth.403")
@@ -197,11 +245,14 @@ export function useAuth() {
197
245
  }
198
246
 
199
247
  const isSubmitBtnDisabled = () => {
200
- return !(
201
- email.value &&
202
- isEmail(email.value) &&
203
- password.value &&
204
- password.value.length > 5
248
+ return (
249
+ isLockedOut.value ||
250
+ !(
251
+ email.value &&
252
+ isEmail(email.value) &&
253
+ password.value &&
254
+ password.value.length > 5
255
+ )
205
256
  )
206
257
  }
207
258
 
@@ -244,6 +295,15 @@ export function useAuth() {
244
295
  }
245
296
 
246
297
  onMounted(async () => {
298
+ // Reprise d'un verrou en cours apres rechargement
299
+ const storedUntil = Number(localStorage.getItem(LOCKOUT_STORAGE_KEY))
300
+ if (Number.isFinite(storedUntil) && storedUntil > Date.now()) {
301
+ lockUntil(storedUntil)
302
+ apiErrorMessage.value = t("auth.tooManyAttempts")
303
+ } else {
304
+ localStorage.remove(LOCKOUT_STORAGE_KEY)
305
+ }
306
+
247
307
  await deviceInfo()
248
308
  if (authState.raw) {
249
309
  replace(LOCAL_SUCCESS_REDIRECTION_ROUTE)
@@ -254,11 +314,14 @@ export function useAuth() {
254
314
  tryAutoLogin()
255
315
  })
256
316
 
317
+ onUnmounted(() => clearTimeout(unlockTimer))
318
+
257
319
  return {
258
320
  email,
259
321
  password,
260
322
  loading,
261
323
  msalLoading,
324
+ isLockedOut,
262
325
  showPassword,
263
326
  validations,
264
327
  errorEmailMessage,
@@ -12,6 +12,8 @@ export default {
12
12
  passwordPresenceError: "The password must be provided",
13
13
  passwordLengthError: "The password length be at least 6",
14
14
  loginFailed: "Incorrect credentials",
15
+ tooManyAttempts:
16
+ "Too many incorrect login attempts. Please wait 1 minute before trying again.",
15
17
  emailNotRecognize: "Login failed, please check your credentials",
16
18
  emailSent: "Email sent, check your inbox",
17
19
  tapEmail: "Please enter your email",
@@ -12,6 +12,8 @@ export default {
12
12
  passwordPresenceError: "Le mot de passe doit être fourni",
13
13
  passwordLengthError: "La longueur du mot de passe doit être au minimum 6",
14
14
  loginFailed: "Identifiants incorrectes",
15
+ tooManyAttempts:
16
+ "Plusieurs tentatives de connexion incorrectes. Veuillez attendre 1 minute avant de réessayer.",
15
17
  emailNotRecognize: "L'adresse email fournie n'est pas reconnue",
16
18
  emailSent: "Email envoyé, vérifier votre boite email",
17
19
  tapEmail: "Entrer votre email",