@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 +1 -1
- package/src/composables/auth.ts +73 -10
- package/src/i18n/en-US/index.ts +2 -0
- package/src/i18n/fr-FR/index.ts +2 -0
package/package.json
CHANGED
package/src/composables/auth.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 ===
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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,
|
package/src/i18n/en-US/index.ts
CHANGED
|
@@ -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",
|
package/src/i18n/fr-FR/index.ts
CHANGED
|
@@ -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",
|