adata-ui 2.0.28 → 2.0.29

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.
@@ -1,34 +1,30 @@
1
1
  <script setup lang="ts">
2
- import TwoFactor from '#adata-ui/components/modals/two-factor/two-factor.vue'
3
2
  import { useAuthStore } from '#adata-ui/stores/auth.store'
4
3
  import { navigateToLocalizedPage } from '#adata-ui/utils/localizedNavigation'
5
- import { useToggle } from '@vueuse/shared'
6
4
  import * as z from 'zod'
7
5
 
6
+ const { $toast } = useNuxtApp()
8
7
  const { myLayer, commonAuth } = useAppConfig()
9
- const authApiURL = commonAuth.authApiURL
10
- const [isResendModal, toggleResendModal] = useToggle()
11
- const isConfirmationEmailModal = ref(false)
12
- const isTwoFactorOpen = ref(false)
13
- const isLoadingOtp = ref(false)
14
- const { $api, $toast } = useNuxtApp()
15
- const showOtpError = ref(false)
16
8
  const { t, locale } = useI18n()
17
- const submitted = ref(false)
18
- const accessToken = ref(null)
19
- const { loginModal, registrationModal, recoveryModal, confirmAccountOtpModal } = useIdModals()
20
9
 
21
10
  const authStore = useAuthStore()
22
11
  const { intermediateState } = storeToRefs(authStore)
23
12
 
13
+ const { loginModal, registrationModal, recoveryModal, confirmAccountOtpModal, twoFactorModal } = useIdModals()
14
+
15
+ const authApiURL = commonAuth.authApiURL
16
+
17
+ const submitted = ref(false)
18
+ const accessToken = ref(null)
19
+
24
20
  export interface ILoginForm {
25
21
  username: string
26
22
  password: string
27
23
  }
28
24
 
29
25
  const loginSchema = z.object({
30
- username: z.string().nonempty(t('register.form.errors.required')).email(t('register.form.errors.email')),
31
- password: z.string().nonempty(t('register.form.errors.required')),
26
+ username: z.string().nonempty(t('error.required')).email(t('error.email')),
27
+ password: z.string().nonempty(t('error.required')),
32
28
  })
33
29
 
34
30
  const validation = computed(() => {
@@ -78,34 +74,38 @@ async function submit() {
78
74
  }),
79
75
  })
80
76
  const { data, message } = await login.json().catch(() => ({}))
77
+
78
+ intermediateState.value.email = form.username
79
+ intermediateState.value.password = form.password
80
+
81
81
  if (data) accessToken.value = data?.access_token
82
82
  if (login.status > 202) {
83
83
  if (login.status === 422) {
84
84
  $toast.error(t('error.validation'))
85
85
  }
86
- else {
87
- intermediateState.value.email = form.username
88
- intermediateState.value.password = form.password
86
+ if (login.status === 206) {
87
+ confirmAccountOtpModal.value = true
89
88
 
90
- await fetch(`${authApiURL}/email/resend-otp`, {
89
+ $fetch(`${authApiURL}/email/resend-otp`, {
91
90
  method: 'GET',
92
91
  credentials: 'include',
93
92
  headers: {
94
- 'Content-Type': 'application/json',
95
- 'lang': locale.value,
93
+ lang: locale.value,
96
94
  },
97
- body: JSON.stringify({
95
+ params: {
98
96
  email: intermediateState.value.email,
99
- }),
97
+ },
100
98
  }).catch(() => {})
101
-
102
- confirmAccountOtpModal.value = true
99
+ }
100
+ else {
101
+ $toast.error(message)
103
102
  }
104
103
  }
105
104
  else {
106
105
  rememberMe.value ? savePassAndLogin(form) : clearAuthCookie()
107
106
  if (data.is_2fa_enabled) {
108
- isTwoFactorOpen.value = true
107
+ loginModal.value = false
108
+ twoFactorModal.value = true
109
109
  }
110
110
  else if (data.email_is_verified) {
111
111
  const response = await fetch(`${authApiURL}/access/cookie`, {
@@ -120,15 +120,19 @@ async function submit() {
120
120
  if (cookiesData) {
121
121
  const { access_token, expire_in } = cookiesData
122
122
  const hostname = location.hostname.split('.').reverse()
123
- document.cookie = `accessToken=${access_token}; max-age=${expire_in}; domain=.${hostname[1]}.${hostname[0]}; path=/`
123
+
124
+ useCookie('accessToken', {
125
+ maxAge: expire_in,
126
+ domain: `.${hostname[1]}.${hostname[0]}`,
127
+ path: '/',
128
+ secure: true,
129
+ }).value = access_token
124
130
  }
125
131
  $toast.success(t('login.successfully'))
126
132
  loginModal.value = false
127
133
  window.location.reload()
128
134
  }
129
135
  else {
130
- intermediateState.value.email = form.username
131
- intermediateState.value.password = form.password
132
136
  confirmAccountOtpModal.value = true
133
137
 
134
138
  $fetch(`${authApiURL}/email/resend-otp`, {
@@ -137,7 +141,7 @@ async function submit() {
137
141
  headers: {
138
142
  lang: locale.value,
139
143
  },
140
- body: {
144
+ params: {
141
145
  email: intermediateState.value.email,
142
146
  },
143
147
  }).catch(() => {})
@@ -154,13 +158,6 @@ function authWithSocial(social: string) {
154
158
  document.location.replace(`https://auth.${mode}.kz/api/login/social?source=${social}`)
155
159
  }
156
160
 
157
- async function onResend() {
158
- const { data, success, message } = await $api.auth.emailResend()
159
- if (success) {
160
- $toast.success(message)
161
- }
162
- }
163
-
164
161
  function onRegister() {
165
162
  loginModal.value = false
166
163
  registrationModal.value = true
@@ -179,64 +176,8 @@ onMounted(() => {
179
176
  rememberMe.value = !!useCookie('username').value && !!useCookie('password').value
180
177
  })
181
178
 
182
- function confirmationEmailResend() {
183
- isConfirmationEmailModal.value = false
184
- toggleResendModal()
185
- }
186
- async function handleConfirmOtp(otpCode: string) {
187
- isLoadingOtp.value = true
188
- const login = await fetch(`${authApiURL}/login`, {
189
- method: 'POST',
190
- credentials: 'include',
191
- headers: {
192
- 'Content-Type': 'application/json',
193
- 'lang': locale.value,
194
- },
195
- body: JSON.stringify({
196
- 'username': intermediateState.value.email.trim(),
197
- 'password': intermediateState.value.password.toString(),
198
- '2fa_code': otpCode,
199
- }),
200
- })
201
- const { data, message } = await login.json().catch(() => ({}))
202
-
203
- if (login.status > 202) {
204
- if (login.status === 403) {
205
- showOtpError.value = true
206
- isLoadingOtp.value = false
207
- }
208
- }
209
- else {
210
- const response = await fetch(`${authApiURL}/access/cookie`, {
211
- method: 'GET',
212
- credentials: 'include',
213
- headers: {
214
- Authorization: `Bearer ${accessToken.value}`,
215
- lang: locale.value,
216
- },
217
- })
218
- const { data: cookiesData } = await response.json()
219
- if (cookiesData?.access_token) {
220
- const { access_token, expire_in } = cookiesData
221
- const hostname = location.hostname.split('.').reverse()
222
- document.cookie = `accessToken=${access_token}; max-age=${expire_in}; domain=.${hostname[1]}.${hostname[0]}; path=/`
223
- }
224
- $toast.success(t('login.successfully'))
225
- loginModal.value = false
226
- window.location.reload()
227
-
228
- isTwoFactorOpen.value = false
229
- }
230
- isLoadingOtp.value = false
231
- }
232
-
233
179
  function handleEnter(e: KeyboardEvent) {
234
- if (
235
- e.key === 'Enter'
236
- && !isTwoFactorOpen.value
237
- && !isConfirmationEmailModal.value
238
- && !isResendModal.value
239
- ) {
180
+ if (e.key === 'Enter') {
240
181
  submit()
241
182
  }
242
183
  }
@@ -258,21 +199,21 @@ onBeforeUnmount(() => {
258
199
  <template>
259
200
  <div class="flex flex-col gap-5">
260
201
  <h1 class="heading-02 text-center">
261
- {{ $t('login.form.title') }}
202
+ {{ t('modals.id.login.title') }}
262
203
  </h1>
263
204
  <p class="body-400 text-center">
264
- {{ $t('login.form.subtitle') }}
205
+ {{ t('modals.id.login.subtitle') }}
265
206
  </p>
266
207
  <div class="flex flex-col gap-4">
267
208
  <a-input-standard
268
209
  v-model="form.username"
269
210
  type="email"
270
- :label="$t('login.form.labels.email')"
211
+ :label="t('modals.id.login.labels.email')"
271
212
  :error="getError('username')"
272
213
  />
273
214
  <a-input-password
274
215
  v-model="form.password"
275
- :label="$t('login.form.labels.password')"
216
+ :label="t('modals.id.login.labels.password')"
276
217
  :error="getError('password')"
277
218
  />
278
219
  <div class="flex items-center justify-between">
@@ -285,14 +226,14 @@ onBeforeUnmount(() => {
285
226
  for="remember_me"
286
227
  class="cursor-pointer"
287
228
  >{{
288
- $t('login.form.remember_me')
229
+ t('modals.id.login.remember_me')
289
230
  }}</label>
290
231
  </div>
291
232
  <button
292
233
  class="link-s-400"
293
234
  @click="onForgotPassword"
294
235
  >
295
- {{ $t('login.form.forget_password') }}
236
+ {{ t('modals.id.login.forget_password') }}
296
237
  </button>
297
238
  </div>
298
239
  </div>
@@ -325,10 +266,10 @@ onBeforeUnmount(() => {
325
266
  type="submit"
326
267
  @click="submit"
327
268
  >
328
- {{ $t('actions.login') }}
269
+ {{ t('actions.login') }}
329
270
  </a-button>
330
271
  <p class="body-400 text-center">
331
- {{ $t('login.form.first_time') }}
272
+ {{ t('modals.id.login.first_time') }}
332
273
  </p>
333
274
 
334
275
  <a-button
@@ -337,7 +278,7 @@ onBeforeUnmount(() => {
337
278
  class="w-full"
338
279
  @click="onRegister"
339
280
  >
340
- {{ $t('actions.register') }}
281
+ {{ t('actions.register') }}
341
282
  </a-button>
342
283
 
343
284
  <a-button
@@ -346,40 +287,16 @@ onBeforeUnmount(() => {
346
287
  class="w-full"
347
288
  @click="toTariffs"
348
289
  >
349
- {{ $t('actions.toTariffs') }}
290
+ {{ t('actions.toTariffs') }}
350
291
  </a-button>
351
292
 
352
293
  <a-alert
353
294
  class="max-w-screen-sm !text-[10px]"
354
295
  size="xs"
355
296
  >
356
- {{ $t('info.userAgreement') }}
297
+ {{ t('info.userAgreement') }}
357
298
  </a-alert>
358
299
  </div>
359
-
360
- <!-- <a-modal v-model="isResendModal"> -->
361
- <!-- <resend -->
362
- <!-- v-if="isResendModal" -->
363
- <!-- @close="isResendModal = false" -->
364
- <!-- @resend="onResend" -->
365
- <!-- /> -->
366
- <!-- </a-modal> -->
367
-
368
- <!-- <a-modal v-model="isConfirmationEmailModal"> -->
369
- <!-- <a-confirmation-email -->
370
- <!-- v-if="isConfirmationEmailModal" -->
371
- <!-- @close="isConfirmationEmailModal = false" -->
372
- <!-- @resend="confirmationEmailResend" -->
373
- <!-- /> -->
374
- <!-- </a-modal> -->
375
-
376
- <two-factor
377
- v-model="isTwoFactorOpen"
378
- v-model:error="showOtpError"
379
- :loading="isLoadingOtp"
380
- @confirm="handleConfirmOtp"
381
- @close="isTwoFactorOpen = false"
382
- />
383
300
  </template>
384
301
 
385
302
  <style scoped></style>
@@ -7,6 +7,7 @@ import IdPasswordSuccessfulModal from '#adata-ui/components/modals/id/IdPassword
7
7
  import IdRecoveryModal from '#adata-ui/components/modals/id/IdRecoveryModal.vue'
8
8
  import IdRegistrationModal from '#adata-ui/components/modals/id/IdRegistrationModal.vue'
9
9
  import IdResetPasswordOtpModal from '#adata-ui/components/modals/id/IdResetPasswordOtpModal.vue'
10
+ import IdTwoFactorModal from '#adata-ui/components/modals/id/IdTwoFactorModal.vue'
10
11
 
11
12
  const {
12
13
  loginModal,
@@ -25,11 +26,14 @@ const {
25
26
  <a-modal v-model="loginModal">
26
27
  <id-login-modal />
27
28
  </a-modal>
29
+ <a-modal v-model="twoFactorModal">
30
+ <id-two-factor-modal />
31
+ </a-modal>
28
32
 
29
33
  <a-modal v-model="registrationModal">
30
34
  <id-registration-modal v-if="registrationModal" />
31
35
  </a-modal>
32
- <a-modal v-model="confirmAccountOtpModal">
36
+ <a-modal v-model="confirmAccountOtpModal" prevent-close>
33
37
  <id-confirm-account-otp-modal v-if="confirmAccountOtpModal" />
34
38
  </a-modal>
35
39
  <a-modal v-model="confirmSuccessfulModal">
@@ -21,16 +21,18 @@ const loading = ref(false)
21
21
  const resetSchema = z.object({
22
22
  password: z
23
23
  .string()
24
- .min(8, t('register.form.errors.low_security', { length: 8 }))
25
- .regex(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/, t('register.form.errors.low_security')),
24
+ .nonempty(t('error.required'))
25
+ .min(8, t('modals.id.register.errors.low_security', { length: 8 }))
26
+ .regex(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/, t('modals.id.register.errors.low_security')),
26
27
  password_confirmation: z
27
28
  .string()
28
- .min(8, t('register.form.errors.low_security', { length: 8 }))
29
- .regex(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/, t('register.form.errors.low_security')),
29
+ .nonempty(t('error.required'))
30
+ .min(8, t('modals.id.register.errors.low_security', { length: 8 }))
31
+ .regex(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/, t('modals.id.register.errors.low_security')),
30
32
  }).refine(
31
33
  data => data.password === data.password_confirmation,
32
34
  {
33
- message: t('register.form.errors.sameAs'),
35
+ message: t('modals.id.register.errors.sameAs'),
34
36
  path: ['password_confirmation'],
35
37
  },
36
38
  )
@@ -34,21 +34,21 @@ const agreement = ref(false)
34
34
  const loading = ref(false)
35
35
 
36
36
  const registerSchema = z.object({
37
- email: z.string().nonempty(t('register.form.errors.required')).email(t('register.form.errors.email')),
37
+ email: z.string().nonempty(t('error.required')).email(t('error.email')),
38
38
  password: z
39
39
  .string()
40
- .nonempty(t('register.form.errors.required'))
41
- .min(8, t('register.form.errors.low_security', { length: 8 }))
42
- .regex(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/, t('register.form.errors.low_security')),
40
+ .nonempty(t('error.required'))
41
+ .min(8, t('modals.id.register.errors.low_security', { length: 8 }))
42
+ .regex(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/, t('modals.id.register.errors.low_security')),
43
43
  password_confirmation: z
44
44
  .string()
45
- .nonempty(t('register.form.errors.required'))
46
- .min(8, t('register.form.errors.low_security', { length: 8 }))
47
- .regex(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/, t('register.form.errors.low_security')),
45
+ .nonempty(t('error.required'))
46
+ .min(8, t('modals.id.register.errors.low_security', { length: 8 }))
47
+ .regex(/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/, t('modals.id.register.errors.low_security')),
48
48
  }).refine(
49
49
  data => data.password === data.password_confirmation,
50
50
  {
51
- message: t('register.form.errors.sameAs'),
51
+ message: t('modals.id.register.errors.sameAs'),
52
52
  path: ['password_confirmation'],
53
53
  },
54
54
  )
@@ -152,31 +152,31 @@ onBeforeUnmount(() => {
152
152
  @submit.prevent="onSubmit"
153
153
  >
154
154
  <h2 class="text-center text-2xl font-bold">
155
- {{ t('register.form.title') }}
155
+ {{ t('modals.id.register.title') }}
156
156
  </h2>
157
157
  <p class="text-center text-sm">
158
- {{ t('register.form.subtitle') }}
158
+ {{ t('modals.id.register.subtitle') }}
159
159
  </p>
160
160
  <a-input-standard
161
161
  v-model="form.email"
162
- :label="t('register.form.labels.email')"
162
+ :label="t('modals.id.register.labels.email')"
163
163
  :error="getError('email')"
164
164
  type="email"
165
165
  />
166
166
 
167
167
  <a-input-password
168
168
  v-model="form.password"
169
- :label="t('register.form.labels.password')"
169
+ :label="t('modals.id.register.labels.password')"
170
170
  :error="getError('password')"
171
171
  />
172
172
 
173
173
  <a-input-password
174
174
  v-model="form.password_confirmation"
175
- :label="t('register.form.labels.password_confirmation')"
175
+ :label="t('modals.id.register.labels.password_confirmation')"
176
176
  :error="getError('password_confirmation')"
177
177
  />
178
178
  <a-alert color="blue">
179
- {{ t('register.form.alert') }}
179
+ {{ t('modals.id.register.alert') }}
180
180
  <template #icon>
181
181
  <a-icon-info-circle />
182
182
  </template>
@@ -185,13 +185,13 @@ onBeforeUnmount(() => {
185
185
  v-model="agreement"
186
186
  side="right"
187
187
  >
188
- <i18n-t keypath="register.form.agreement.text">
188
+ <i18n-t keypath="modals.id.register.agreement.text">
189
189
  <template #link>
190
190
  <nuxt-link-locale
191
191
  class="text-blue-700"
192
192
  @click="getUrl"
193
193
  >
194
- {{ t('register.form.agreement.link') }}
194
+ {{ t('modals.id.register.agreement.link') }}
195
195
  </nuxt-link-locale>
196
196
  </template>
197
197
  </i18n-t>
@@ -200,17 +200,17 @@ onBeforeUnmount(() => {
200
200
  :disabled="!agreement"
201
201
  :loading="loading"
202
202
  >
203
- {{ t('register.form.continue') }}
203
+ {{ t('modals.id.register.continue') }}
204
204
  </a-button>
205
205
  <p class="text-center text-sm">
206
- {{ t('register.form.haveAcc') }}
206
+ {{ t('modals.id.register.haveAcc') }}
207
207
  </p>
208
208
  <a-button
209
209
  type="button"
210
210
  view="outline"
211
211
  @click="onLogin"
212
212
  >
213
- {{ t('register.form.enter') }}
213
+ {{ t('modals.id.register.enter') }}
214
214
  </a-button>
215
215
  </form>
216
216
  </template>
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import OtpInput from '#adata-ui/components/modals/two-factor/otp-input.vue'
2
+ import OtpInput from '#adata-ui/components/modals/id/otp-input.vue'
3
3
  import { useAuthStore } from '#adata-ui/stores/auth.store'
4
4
 
5
5
  const { $toast } = useNuxtApp()
@@ -17,6 +17,7 @@ const otp = ref(['', '', '', '', '', ''])
17
17
  const otpFormatted = computed(() => {
18
18
  return otp.value.join('')
19
19
  })
20
+ const attempts = ref(5)
20
21
  const showError = ref(false)
21
22
  const isLoading = ref(false)
22
23
 
@@ -43,6 +44,12 @@ async function onConfirm() {
43
44
  newPasswordModal.value = true
44
45
  }
45
46
  catch (error) {
47
+ if (error.data?.retry_after) {
48
+ attempts.value = 0
49
+ }
50
+ else {
51
+ attempts.value--
52
+ }
46
53
  showError.value = true
47
54
  $toast.error(error.data.message)
48
55
  }
@@ -72,6 +79,7 @@ async function onResend() {
72
79
 
73
80
  function onClose() {
74
81
  otp.value = ['', '', '', '', '', '']
82
+ attempts.value = 5
75
83
  showError.value = false
76
84
  resetPasswordOtpModal.value = false
77
85
  }
@@ -85,6 +93,7 @@ function handleEnter(e: KeyboardEvent) {
85
93
  const timer = ref(60)
86
94
 
87
95
  function runTimer() {
96
+ timer.value = 60
88
97
  const intervalId = setInterval(() => {
89
98
  if (!timer.value) clearInterval(intervalId)
90
99
  return timer.value--
@@ -116,7 +125,7 @@ onBeforeUnmount(() => {
116
125
  </p>
117
126
 
118
127
  <div v-if="timer > 0" class="text-2xl font-bold">
119
- {{ timer }} {{ t('register.modal.seconds') }}
128
+ {{ timer }} {{ t('modals.id.resetPasswordOtp.seconds') }}
120
129
  </div>
121
130
  <button
122
131
  v-else
@@ -127,7 +136,11 @@ onBeforeUnmount(() => {
127
136
  </button>
128
137
  </div>
129
138
 
130
- <otp-input v-model="otp" v-model:error="showError" />
139
+ <otp-input
140
+ v-model="otp"
141
+ v-model:error="showError"
142
+ :attempts="attempts"
143
+ />
131
144
 
132
145
  <div class="flex w-full gap-2">
133
146
  <a-button
@@ -140,7 +153,7 @@ onBeforeUnmount(() => {
140
153
  <a-button
141
154
  block
142
155
  :loading="isLoading"
143
- :disabled="otpFormatted.length < 6"
156
+ :disabled="otpFormatted.length < 6 || attempts === 0"
144
157
  @click="onConfirm"
145
158
  >
146
159
  {{ t('actions.confirm') }}
@@ -0,0 +1,127 @@
1
+ <script setup lang="ts">
2
+ import OtpInput from '#adata-ui/components/modals/id/otp-input.vue'
3
+ import { useAuthStore } from '#adata-ui/stores/auth.store'
4
+
5
+ const { $toast } = useNuxtApp()
6
+ const { t, locale } = useI18n()
7
+ const { commonAuth } = useAppConfig()
8
+
9
+ const authStore = useAuthStore()
10
+ const { intermediateState } = storeToRefs(authStore)
11
+
12
+ const { twoFactorModal } = useIdModals()
13
+
14
+ const authApiURL = commonAuth.authApiURL
15
+
16
+ const otp = ref(['', '', '', '', '', ''])
17
+ const otpFormatted = computed(() => {
18
+ return otp.value.join('')
19
+ })
20
+ const showError = ref(false)
21
+ const isLoading = ref(false)
22
+
23
+ async function onConfirm() {
24
+ isLoading.value = true
25
+ const login = await fetch(`${authApiURL}/login`, {
26
+ method: 'POST',
27
+ credentials: 'include',
28
+ headers: {
29
+ 'Content-Type': 'application/json',
30
+ 'lang': locale.value,
31
+ },
32
+ body: JSON.stringify({
33
+ 'username': intermediateState.value.email,
34
+ 'password': intermediateState.value.password,
35
+ '2fa_code': otpFormatted.value,
36
+ }),
37
+ })
38
+ const { data, message } = await login.json().catch(() => ({}))
39
+
40
+ if (login.status > 202) {
41
+ if (login.status === 403) {
42
+ showError.value = true
43
+ isLoading.value = false
44
+ }
45
+ }
46
+ else {
47
+ const response = await fetch(`${authApiURL}/access/cookie`, {
48
+ method: 'GET',
49
+ credentials: 'include',
50
+ headers: {
51
+ lang: locale.value,
52
+ },
53
+ })
54
+ const { data: cookiesData } = await response.json()
55
+ if (cookiesData?.access_token) {
56
+ const { access_token, expire_in } = cookiesData
57
+ const hostname = location.hostname.split('.').reverse()
58
+
59
+ useCookie('accessToken', {
60
+ maxAge: expire_in,
61
+ domain: `.${hostname[1]}.${hostname[0]}`,
62
+ path: '/',
63
+ secure: true,
64
+ }).value = access_token
65
+ }
66
+ $toast.success(t('login.successfully'))
67
+ twoFactorModal.value = false
68
+ window.location.reload()
69
+ }
70
+ isLoading.value = false
71
+ }
72
+
73
+ function onClose() {
74
+ otp.value = ['', '', '', '', '', '']
75
+ showError.value = false
76
+ twoFactorModal.value = false
77
+ }
78
+
79
+ function handleEnter(e: KeyboardEvent) {
80
+ if (e.key === 'Enter') {
81
+ onConfirm()
82
+ }
83
+ }
84
+
85
+ onMounted(() => {
86
+ document.addEventListener('keyup', handleEnter)
87
+ })
88
+
89
+ onBeforeUnmount(() => {
90
+ document.removeEventListener('keyup', handleEnter)
91
+ })
92
+ </script>
93
+
94
+ <template>
95
+ <div class="flex flex-col items-center gap-4">
96
+ <p class="heading-02 text-center">
97
+ {{ t('modals.id.twoFactor.title') }}
98
+ </p>
99
+ <a-icon-hand-with-phone-light class="size-32 dark:hidden" />
100
+ <a-icon-hand-with-phone-dark class="hidden size-32 dark:block" />
101
+ <otp-input v-model="otp" v-model:error="showError" />
102
+ <p class="body-400 text-center">
103
+ {{ t('modals.id.twoFactor.code') }}
104
+ </p>
105
+ <div class="flex w-full gap-2">
106
+ <a-button
107
+ class="w-full"
108
+ view="outline"
109
+ @click="onClose"
110
+ >
111
+ {{ t('actions.close') }}
112
+ </a-button>
113
+ <a-button
114
+ :loading="isLoading"
115
+ :disabled="otpFormatted.length < 6"
116
+ class="w-full"
117
+ @click="onConfirm"
118
+ >
119
+ {{ t('actions.confirm') }}
120
+ </a-button>
121
+ </div>
122
+ </div>
123
+ </template>
124
+
125
+ <style scoped>
126
+
127
+ </style>
@@ -1,4 +1,8 @@
1
1
  <script lang="ts" setup>
2
+ defineProps<{
3
+ attempts?: number
4
+ }>()
5
+
2
6
  const length = 6 // Длина OTP
3
7
  const showError = defineModel('error')
4
8
  const otp = defineModel()
@@ -133,6 +137,10 @@ function moveFocus(index: number, to: string) {
133
137
  >
134
138
  {{ t('error.otp') }}
135
139
  </a-alert>
140
+
141
+ <p v-show="showError" class="mt-1 text-xs text-red-500 dark:text-red-400">
142
+ {{ t('modals.id.resetPasswordOtp.attempts', { n: attempts }) }}
143
+ </p>
136
144
  </div>
137
145
  </template>
138
146
 
@@ -1 +1 @@
1
- # breadcrumb, tabs
1
+ # breadcrumb, tabs