@adminforth/two-factors-auth 5.0.0 → 6.0.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/build.log CHANGED
@@ -21,5 +21,5 @@ custom/tsconfig.json
21
21
  custom/use2faApi.ts
22
22
  custom/utils.js
23
23
 
24
- sent 306,875 bytes received 324 bytes 614,398.00 bytes/sec
25
- total size is 305,624 speedup is 0.99
24
+ sent 301,180 bytes received 324 bytes 603,008.00 bytes/sec
25
+ total size is 299,946 speedup is 0.99
@@ -5,7 +5,7 @@
5
5
  class="af-login-with-passkey-btn w-full gap-2"
6
6
  @click="handleLoginWithPasskey"
7
7
  >
8
- <IconFingerprintOutline class="w-5 h-5 text-lightPrimary dark:text-darkPrimary" />
8
+ <IconFingerprint class="w-5 h-5 text-lightPrimary dark:text-darkPrimary" />
9
9
  {{ $t('Continue with Passkey') }}
10
10
  </Button>
11
11
 
@@ -14,7 +14,7 @@
14
14
 
15
15
 
16
16
  <script setup lang="ts">
17
- import { IconFingerprintOutline } from '@iconify-prerendered/vue-flowbite';
17
+ import { IconFingerprint } from '@iconify-prerendered/vue-lucide';
18
18
  import { getPasskey } from "./utils.js";
19
19
  import { callAdminForthApi } from '@/utils';
20
20
  import { useUserStore } from '@/stores/user';
@@ -9,45 +9,45 @@
9
9
  closable
10
10
  @close="onCancel"
11
11
  >
12
- <pre v-if="dialogDetails" class="af-2fa-details max-h-60 overflow-auto rounded-lg border border-lightDialogBreakLine bg-gray-50 p-3 text-xs text-gray-800 dark:border-darkDialogBreakLine dark:bg-gray-800 dark:text-gray-100">{{ dialogDetails }}</pre>
12
+ <pre v-if="dialogDetails" class="af-2fa-details max-h-60 overflow-auto rounded-lg border border-lightDialogBreakLine bg-lightSecondaryDarken p-3 text-xs text-lightInputText dark:border-darkDialogBreakLine dark:bg-darkSecondaryLighten dark:text-darkInputText">{{ dialogDetails }}</pre>
13
13
 
14
14
  <div v-if="confirmItems.length" class="af-2fa-actions">
15
- <p class="mb-2 text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">{{ $t('You are approving') }}</p>
16
- <ul class="af-2fa-actions-list max-h-60 overflow-auto divide-y divide-lightDialogBreakLine rounded-lg border border-lightDialogBreakLine bg-gray-50 dark:divide-darkDialogBreakLine dark:border-darkDialogBreakLine dark:bg-gray-800">
15
+ <p class="mb-2 text-xs font-medium uppercase tracking-wider text-lightInputIconColor dark:text-darkInputIconColor">{{ $t('You are approving') }}</p>
16
+ <ul class="af-2fa-actions-list max-h-60 overflow-auto divide-y divide-lightDialogBreakLine rounded-lg border border-lightDialogBreakLine bg-lightSecondaryDarken dark:divide-darkDialogBreakLine dark:border-darkDialogBreakLine dark:bg-darkSecondaryLighten">
17
17
  <li v-for="(item, i) in confirmItems" :key="i" class="flex gap-3 px-3.5 py-3">
18
18
  <span aria-hidden="true" class="mt-2 w-1.5 h-1.5 shrink-0 rounded-full bg-lightPrimary dark:bg-darkPrimary"></span>
19
- <span class="min-w-0 text-base leading-6 break-words text-gray-800 dark:text-gray-100">{{ item }}</span>
19
+ <span class="min-w-0 text-base leading-6 break-words text-lightInputText dark:text-darkInputText">{{ item }}</span>
20
20
  </li>
21
21
  </ul>
22
22
  </div>
23
23
 
24
24
  <TwoFaCodeInput v-if="modalMode === 'totp'" ref="codeInput" :invalid="Boolean(codeError)" @complete="sendConfirmationResult" />
25
25
  <TwoFaHint v-else-if="modalMode === 'passkey'" class="af-2fa-passkey-hint">
26
- <template #icon><IconFingerprintOutline class="w-6 h-6" /></template>
26
+ <template #icon><IconFingerprint class="w-5 h-5" /></template>
27
27
  {{ $t('Your browser will ask for Touch ID, Windows Hello or a security key.') }}
28
28
  </TwoFaHint>
29
- <p v-else role="status" class="af-2fa-grace-notice flex items-start gap-2.5 rounded-lg border border-green-200 bg-green-50 px-3.5 py-3 text-sm leading-5 text-green-800 dark:border-green-500/30 dark:bg-green-500/10 dark:text-green-300">
30
- <IconCheckCircleOutline class="w-5 h-5 shrink-0" />
29
+ <p v-else role="status" class="af-2fa-grace-notice flex items-start gap-2.5 rounded-lg border border-lightSuccessBorder bg-lightSuccessSoft px-3.5 py-3 text-sm leading-5 text-lightSuccess dark:border-darkSuccessBorder dark:bg-darkSuccessSoft dark:text-darkSuccess">
30
+ <IconCircleCheck class="w-5 h-5 shrink-0" />
31
31
  {{ $t('You verified recently, so no code is needed.') }}
32
32
  </p>
33
33
 
34
34
  <template #footer>
35
35
  <TwoFaLinkButton v-if="modalMode === 'totp' && doesUserHavePasskeys" class="af-2fa-switch-to-passkey" @click="modalMode = 'passkey'">
36
- <IconFingerprintOutline class="w-4 h-4" />
36
+ <IconFingerprint class="w-4 h-4" />
37
37
  {{ $t('Use passkey instead') }}
38
38
  </TwoFaLinkButton>
39
39
  <TwoFaLinkButton v-else-if="modalMode === 'passkey'" class="af-2fa-switch-to-totp" @click="modalMode = 'totp'">
40
- <IconMobilePhoneOutline class="w-4 h-4" />
40
+ <IconSmartphone class="w-4 h-4" />
41
41
  {{ $t('Use code instead') }}
42
42
  </TwoFaLinkButton>
43
43
  <div class="af-2fa-buttons ms-auto flex gap-2 whitespace-nowrap">
44
44
  <Button variant="secondary" class="af-2fa-cancel-btn" @click="onCancel">{{ $t('Cancel') }}</Button>
45
45
  <Button v-if="modalMode === 'passkey'" @click="usePasskeyButtonClick" :disabled="isFetchingPasskey" :loader="isFetchingPasskey" class="af-2fa-passkey-btn gap-2">
46
- <IconFingerprintOutline class="w-4 h-4" />
46
+ <IconFingerprint class="w-4 h-4" />
47
47
  {{ $t('Use passkey') }}
48
48
  </Button>
49
49
  <Button v-else-if="modalMode === 'grace'" class="af-2fa-approve-btn gap-2" @click="approveInGracePeriod">
50
- <IconCheckOutline class="w-4 h-4" />
50
+ <IconCheck class="w-4 h-4" />
51
51
  {{ approveLabel }}
52
52
  </Button>
53
53
  </div>
@@ -63,12 +63,7 @@
63
63
  import { useI18n } from 'vue-i18n';
64
64
  import { callAdminForthApi } from '@/utils';
65
65
  import { Link, Button } from '@/afcl';
66
- import {
67
- IconCheckCircleOutline,
68
- IconCheckOutline,
69
- IconFingerprintOutline,
70
- IconMobilePhoneOutline,
71
- } from '@iconify-prerendered/vue-flowbite';
66
+ import { IconCheck, IconCircleCheck, IconFingerprint, IconSmartphone } from '@iconify-prerendered/vue-lucide';
72
67
  import { getPasskey } from './utils.js'
73
68
  import TwoFaCard from './TwoFaCard.vue';
74
69
  import TwoFaCodeInput from './TwoFaCodeInput.vue';
@@ -1,34 +1,44 @@
1
1
  <template>
2
- <div class="af-2fa-card relative flex flex-col w-full max-w-md overflow-hidden rounded-xl border border-lightDialogBreakLine bg-lightDialogBackgorund shadow-2xl dark:border-darkDialogBreakLine dark:bg-darkDialogBackgorund">
2
+ <div class="af-2fa-card relative flex flex-col w-full max-w-md overflow-hidden rounded-xl border border-lightDialogBreakLine bg-lightDialogBackgorund shadow-popover dark:border-darkDialogBreakLine dark:bg-darkDialogBackgorund dark:shadow-darkPopover">
3
3
  <div class="af-2fa-header flex items-start gap-3.5 border-b border-lightDialogBreakLine py-5 pl-6 dark:border-darkDialogBreakLine" :class="closable ? 'pr-4' : 'pr-6'">
4
4
  <div class="af-2fa-icon-wrap flex w-10 h-10 shrink-0 items-center justify-center rounded-lg bg-lightPrimary/10 text-lightPrimary dark:bg-darkPrimary/20 dark:text-darkPrimary">
5
- <IconShieldCheckOutline class="af-2fa-shield-icon w-6 h-6" />
5
+ <IconShieldCheck class="af-2fa-shield-icon w-5 h-5" />
6
6
  </div>
7
7
  <div class="af-2fa-title-wrap flex-1 min-w-0">
8
8
  <p class="af-2fa-title break-words text-lg leading-6 font-semibold text-lightDialogHeaderText dark:text-darkDialogHeaderText">{{ title }}</p>
9
- <p class="af-2fa-subtitle mt-0.5 text-sm leading-5 text-gray-500 dark:text-gray-400">{{ subtitle }}</p>
9
+ <p class="af-2fa-subtitle mt-0.5 text-sm leading-5 text-lightInputIconColor dark:text-darkInputIconColor">{{ subtitle }}</p>
10
10
  </div>
11
- <button v-if="closable" type="button" :aria-label="$t('Cancel')" class="af-2fa-close-btn -mt-1 inline-flex w-9 h-9 shrink-0 items-center justify-center rounded-lg text-lightDialogCloseButton hover:bg-lightDialogCloseButtonHoverBackground hover:text-lightDialogCloseButtonHover dark:text-darkDialogCloseButton dark:hover:bg-darkDialogCloseButtonHoverBackground dark:hover:text-darkDialogCloseButtonHover" @click="emit('close')">
12
- <IconCloseOutline class="w-5 h-5" />
13
- </button>
11
+ <Button
12
+ v-if="closable"
13
+ type="button"
14
+ variant="ghost"
15
+ size="sm"
16
+ iconOnly
17
+ :aria-label="$t('Cancel')"
18
+ class="af-2fa-close-btn -mt-0.5 text-lightDialogCloseButton hover:text-lightDialogCloseButtonHover hover:bg-lightDialogCloseButtonHoverBackground dark:text-darkDialogCloseButton dark:hover:text-darkDialogCloseButtonHover dark:hover:bg-darkDialogCloseButtonHoverBackground"
19
+ @click="emit('close')"
20
+ >
21
+ <IconX aria-hidden="true" />
22
+ </Button>
14
23
  </div>
15
24
 
16
25
  <div class="af-2fa-body flex flex-col gap-5 px-6 pt-5 pb-6">
17
26
  <slot />
18
- <p v-if="error" role="alert" class="af-2fa-code-error -mt-2.5 flex items-center gap-1.5 text-sm text-red-600 dark:text-red-400">
19
- <IconExclamationCircleOutline class="w-4 h-4 shrink-0" />
27
+ <p v-if="error" role="alert" class="af-2fa-code-error -mt-2.5 flex items-center gap-1.5 text-sm text-lightDanger dark:text-darkDanger">
28
+ <IconCircleAlert class="w-4 h-4 shrink-0" />
20
29
  {{ error }}
21
30
  </p>
22
31
  </div>
23
32
 
24
- <div v-if="$slots.footer" class="af-2fa-footer flex flex-wrap items-center gap-x-3 gap-y-2 border-t border-lightDialogBreakLine bg-gray-50 py-3 pl-6 pr-5 dark:border-darkDialogBreakLine dark:bg-gray-800/50">
33
+ <div v-if="$slots.footer" class="af-2fa-footer flex flex-wrap items-center gap-x-3 gap-y-2 border-t border-lightDialogBreakLine bg-lightFormHeading py-3 pl-6 pr-5 dark:border-darkDialogBreakLine dark:bg-darkFormHeading">
25
34
  <slot name="footer" />
26
35
  </div>
27
36
  </div>
28
37
  </template>
29
38
 
30
39
  <script setup lang="ts">
31
- import { IconCloseOutline, IconExclamationCircleOutline, IconShieldCheckOutline } from '@iconify-prerendered/vue-flowbite';
40
+ import { IconCircleAlert, IconShieldCheck, IconX } from '@iconify-prerendered/vue-lucide';
41
+ import { Button } from '@/afcl';
32
42
 
33
43
  defineProps<{
34
44
  title: string;
@@ -2,29 +2,31 @@
2
2
  <div class="af-2fa-code">
3
3
  <div :id="LABEL_ID">
4
4
  <slot name="label">
5
- <p class="mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">{{ $t('Authenticator code') }}</p>
5
+ <p class="mb-2 text-sm font-medium text-lightInputText dark:text-darkInputText">{{ $t('Authenticator code') }}</p>
6
6
  </slot>
7
7
  </div>
8
- <div ref="otpRoot" class="af-2fa-otp-root text-gray-300 dark:text-gray-500">
9
- <v-otp-input
10
- ref="otpInput"
11
- :input-classes="inputClasses"
12
- :num-inputs="CODE_LENGTH"
13
- inputType="number"
14
- inputmode="numeric"
15
- :should-auto-focus="true"
16
- :should-focus-order="true"
17
- @on-complete="emit('complete', $event)"
8
+ <div class="af-2fa-otp-root flex justify-center">
9
+ <CodeInput
10
+ ref="codeInput"
11
+ id="mfaCode"
12
+ name="mfaCode"
13
+ :aria-labelledby="LABEL_ID"
14
+ :length="CODE_LENGTH"
15
+ :groupSize="3"
16
+ :invalid="invalid"
17
+ autofocus
18
+ keepFocus
19
+ @complete="emit('complete', $event)"
18
20
  />
19
21
  </div>
20
22
  </div>
21
23
  </template>
22
24
 
23
25
  <script setup lang="ts">
24
- import VOtpInput from 'vue3-otp-input';
25
- import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
26
+ import { ref } from 'vue';
27
+ import { CodeInput } from '@/afcl';
26
28
 
27
- const props = defineProps<{
29
+ defineProps<{
28
30
  invalid?: boolean;
29
31
  }>();
30
32
 
@@ -34,98 +36,10 @@ const emit = defineEmits<{
34
36
 
35
37
  const CODE_LENGTH = 6;
36
38
  const LABEL_ID = 'mfaCode-label';
37
- const INPUT_CLASS = 'otp-input';
38
39
 
39
- const otpRoot = ref<HTMLElement | null>(null);
40
- const otpInput = ref<{ clearInput(): void; fillInput(value: string): void } | null>(null);
41
-
42
- const inputClasses = computed(() => [
43
- `${INPUT_CLASS} w-12 h-14 p-0 rounded-lg border-2 bg-gray-50 text-center text-2xl font-medium leading-none tabular-nums text-gray-900 outline-none focus:ring-4 dark:bg-gray-800 dark:text-white`,
44
- props.invalid
45
- ? 'border-red-500 focus:border-red-500 focus:ring-red-500/25 dark:border-red-400 dark:focus:border-red-400'
46
- : 'border-gray-300 focus:border-lightPrimary focus:ring-lightPrimary/25 dark:border-gray-600 dark:focus:border-darkPrimary dark:focus:ring-darkPrimary/25',
47
- ].join(' '));
48
-
49
- // Focus callbacks are deferred with requestAnimationFrame and may run after unmount, when the root is gone.
50
- function getInputs(): HTMLInputElement[] {
51
- return otpRoot.value ? Array.from(otpRoot.value.querySelectorAll<HTMLInputElement>(`input.${INPUT_CLASS}`)) : [];
52
- }
53
-
54
- function focusFirstEmptyInput() {
55
- const inputs = getInputs();
56
- (inputs.find((input) => !input.value) ?? inputs[0])?.focus();
57
- }
58
-
59
- // While the code is asked, focus stays in the code inputs, so typing and pasting always go into the code.
60
- function handleFocusIn(event: FocusEvent) {
61
- if (!getInputs().includes(event.target as HTMLInputElement)) {
62
- requestAnimationFrame(focusFirstEmptyInput);
63
- }
64
- }
65
-
66
- function handleFocusOut() {
67
- requestAnimationFrame(() => {
68
- if (!getInputs().includes(document.activeElement as HTMLInputElement)) {
69
- focusFirstEmptyInput();
70
- }
71
- });
72
- }
73
-
74
- // A paste into one of the inputs is handled by vue3-otp-input itself.
75
- function handlePaste(event: ClipboardEvent) {
76
- event.preventDefault();
77
- if ((event.target as HTMLElement).classList.contains(INPUT_CLASS)) {
78
- return;
79
- }
80
- const pastedText = event.clipboardData?.getData('text') ?? '';
81
- if (pastedText.length === CODE_LENGTH) {
82
- otpInput.value!.fillInput(pastedText);
83
- }
84
- }
85
-
86
- function tagInputs() {
87
- getInputs().forEach((input, index) => {
88
- input.setAttribute('name', 'mfaCode');
89
- input.setAttribute('id', `mfaCode-${index + 1}`);
90
- input.setAttribute('autocomplete', 'one-time-code');
91
- input.setAttribute('aria-labelledby', LABEL_ID);
92
- });
93
- }
94
-
95
- onMounted(() => {
96
- tagInputs();
97
- focusFirstEmptyInput();
98
- document.addEventListener('focusin', handleFocusIn, true);
99
- otpRoot.value!.addEventListener('focusout', handleFocusOut, true);
100
- window.addEventListener('paste', handlePaste);
101
- });
102
-
103
- onBeforeUnmount(() => {
104
- document.removeEventListener('focusin', handleFocusIn, true);
105
- otpRoot.value!.removeEventListener('focusout', handleFocusOut, true);
106
- window.removeEventListener('paste', handlePaste);
107
- });
40
+ const codeInput = ref<InstanceType<typeof CodeInput> | null>(null);
108
41
 
109
42
  defineExpose({
110
- clear: () => otpInput.value!.clearInput(),
43
+ clear: () => codeInput.value!.clear(),
111
44
  });
112
45
  </script>
113
-
114
- <style scoped>
115
- /* vue3-otp-input renders its own container and ignores container classes, so it is styled here */
116
- :deep(.otp-input-container) {
117
- align-items: center;
118
- justify-content: center;
119
- gap: 0.5rem;
120
- }
121
-
122
- /* a dash after the 3rd digit splits the code into two groups of three */
123
- :deep(.otp-input-container > div:nth-child(3))::after {
124
- content: '';
125
- width: 0.75rem;
126
- height: 2px;
127
- margin-left: 0.5rem;
128
- border-radius: 1px;
129
- background: currentColor;
130
- }
131
- </style>
@@ -1,9 +1,9 @@
1
1
  <template>
2
- <div class="af-2fa-hint flex items-center gap-3.5 rounded-lg border border-lightDialogBreakLine bg-gray-50 px-4 py-3.5 dark:border-darkDialogBreakLine dark:bg-gray-800">
2
+ <div class="af-2fa-hint flex items-center gap-3.5 rounded-lg border border-lightDialogBreakLine bg-lightSecondaryDarken px-4 py-3.5 dark:border-darkDialogBreakLine dark:bg-darkSecondaryLighten">
3
3
  <span class="af-2fa-hint-icon flex w-11 h-11 shrink-0 items-center justify-center rounded-full bg-lightPrimary/10 text-lightPrimary dark:bg-darkPrimary/20 dark:text-darkPrimary">
4
4
  <slot name="icon" />
5
5
  </span>
6
- <p class="af-2fa-hint-text text-sm leading-5 text-gray-500 dark:text-gray-400">
6
+ <p class="af-2fa-hint-text text-sm leading-5 text-lightInputIconColor dark:text-darkInputIconColor">
7
7
  <slot />
8
8
  </p>
9
9
  </div>
@@ -1,5 +1,10 @@
1
1
  <template>
2
- <button type="button" class="af-2fa-link-btn inline-flex h-11 items-center gap-1.5 whitespace-nowrap text-sm font-medium text-lightPrimary hover:underline dark:text-darkPrimary">
2
+ <!-- the negative margin lines the label up with the card content -->
3
+ <Button type="button" variant="ghost" class="af-2fa-link-btn -ms-3.5">
3
4
  <slot />
4
- </button>
5
+ </Button>
5
6
  </template>
7
+
8
+ <script setup lang="ts">
9
+ import { Button } from '@/afcl';
10
+ </script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="relative flex items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-800"
2
+ <div class="relative flex items-center justify-center min-h-screen bg-lightHtml dark:bg-darkHtml"
3
3
  :style="(coreStore.config?.loginBackgroundImage && coreStore.config?.loginBackgroundPosition === 'over') ? {
4
4
  'background-image': 'url(' + loadFile(coreStore.config?.loginBackgroundImage) + ')',
5
5
  'background-size': 'cover',
@@ -16,23 +16,23 @@
16
16
  >
17
17
  <TwoFaCodeInput v-if="confirmationMode === 'code'" ref="codeInput" :invalid="Boolean(codeError)" @complete="handleOnComplete" />
18
18
  <TwoFaHint v-else class="af-2fa-passkey-hint">
19
- <template #icon><IconFingerprintOutline class="w-6 h-6" /></template>
19
+ <template #icon><IconFingerprint class="w-5 h-5" /></template>
20
20
  {{ $t('Your browser will ask for Touch ID, Windows Hello or a security key.') }}
21
21
  </TwoFaHint>
22
22
 
23
23
  <template #footer>
24
24
  <TwoFaLinkButton v-if="confirmationMode === 'code' && doesUserHavePasskeys" class="af-2fa-switch-to-passkey" @click="router.push({ hash: '#passkey' })">
25
- <IconFingerprintOutline class="w-4 h-4" />
25
+ <IconFingerprint class="w-4 h-4" />
26
26
  {{ $t('Use passkey instead') }}
27
27
  </TwoFaLinkButton>
28
28
  <TwoFaLinkButton v-else-if="confirmationMode === 'passkey'" class="af-2fa-switch-to-totp" @click="router.push({ hash: '#code' })">
29
- <IconMobilePhoneOutline class="w-4 h-4" />
29
+ <IconSmartphone class="w-4 h-4" />
30
30
  {{ $t('Use code instead') }}
31
31
  </TwoFaLinkButton>
32
32
  <div class="af-2fa-buttons ms-auto flex gap-2 whitespace-nowrap">
33
33
  <LinkButton to="/login" variant="secondary" class="af-2fa-back-to-login">{{ $t('Back to login') }}</LinkButton>
34
34
  <Button v-if="confirmationMode === 'passkey'" @click="usePasskeyButton" :disabled="isFetchingPasskey" :loader="isFetchingPasskey" class="af-2fa-passkey-btn gap-2">
35
- <IconFingerprintOutline class="w-4 h-4" />
35
+ <IconFingerprint class="w-4 h-4" />
36
36
  {{ $t('Use passkey') }}
37
37
  </Button>
38
38
  </div>
@@ -57,8 +57,7 @@
57
57
  import { useI18n } from 'vue-i18n';
58
58
  import { useRoute } from 'vue-router'
59
59
  import { useRouter } from 'vue-router';
60
- import { IconFingerprintOutline, IconMobilePhoneOutline } from '@iconify-prerendered/vue-flowbite';
61
- import ErrorMessage from '@/components/ErrorMessage.vue';
60
+ import { IconFingerprint, IconSmartphone } from '@iconify-prerendered/vue-lucide';
62
61
  import { handlePasskeyAlert } from './utils.js';
63
62
  import TwoFaCard from './TwoFaCard.vue';
64
63
  import TwoFaCodeInput from './TwoFaCodeInput.vue';
@@ -1,29 +1,30 @@
1
1
  <template>
2
2
  <div class="af-passkeys flex flex-col justify-center mr-6 md:mr-12">
3
- <h2 class="af-passkeys-title flex items-start justify-start leading-none text-gray-800 dark:text-gray-50 text-3xl font-semibold">{{$t('Passkeys')}}</h2>
4
- <p class="af-passkeys-subtitle text-sm mt-3 text-gray-500 dark:text-gray-400"> {{$t('Manage your passwordless authentication methods')}} </p>
3
+ <h2 class="af-passkeys-title text-lg font-semibold tracking-tight text-lightCardTitle dark:text-darkCardTitle">{{$t('Passkeys')}}</h2>
4
+ <p class="af-passkeys-subtitle mt-1 text-sm text-lightCardDescription dark:text-darkCardDescription"> {{$t('Manage your passwordless authentication methods')}} </p>
5
5
 
6
6
  <div class="af-passkeys-grid mt-6 flex flex-wrap gap-4">
7
- <div
7
+ <Card
8
8
  v-for="item in passkeys"
9
9
  :key="item.id"
10
- class="af-passkey-card flex flex-col w-full h-42 lg:w-72 rounded-xl border border-lightDialogBreakLine bg-lightDialogBackgorund p-4 shadow-sm dark:border-darkDialogBreakLine dark:bg-darkDialogBackgorund"
10
+ size="md"
11
+ class="af-passkey-card flex flex-col w-full h-42 lg:w-72"
11
12
  >
12
13
  <div class="af-passkey-card-header flex items-center gap-3 mb-3">
13
- <div class="af-passkey-card-icon flex w-10 h-10 shrink-0 items-center justify-center rounded-lg bg-gray-100 dark:bg-gray-800">
14
+ <div class="af-passkey-card-icon flex w-10 h-10 shrink-0 items-center justify-center rounded-lg bg-lightSecondaryDarken dark:bg-darkSecondaryLighten">
14
15
  <img class="max-h-6 max-w-6" :src="coreStore.theme === 'light' ? item.light_icon : item.dark_icon" />
15
16
  </div>
16
17
  <div class="af-passkey-card-info flex-1 min-w-0">
17
18
  <p class="af-passkey-card-name font-semibold text-lightDialogHeaderText dark:text-darkDialogHeaderText truncate" :title="item.name">{{ item.name }}</p>
18
- <p class="af-passkey-card-type text-xs text-gray-500 dark:text-gray-400">{{$t('Synced passkey')}}</p>
19
+ <p class="af-passkey-card-type text-xs text-lightInputIconColor dark:text-darkInputIconColor">{{$t('Synced passkey')}}</p>
19
20
  </div>
20
- <span class="af-passkey-card-badge shrink-0 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-50 text-green-800 dark:bg-green-500/10 dark:text-green-300">
21
+ <span class="af-passkey-card-badge shrink-0 inline-flex items-center px-2 py-0.5 rounded-full border text-xs font-medium border-lightSuccessBorder bg-lightSuccessSoft text-lightSuccess dark:border-darkSuccessBorder dark:bg-darkSuccessSoft dark:text-darkSuccess">
21
22
  {{$t('Active')}}
22
23
  </span>
23
24
  </div>
24
25
 
25
- <div class="af-passkey-card-last-used text-xs text-gray-500 dark:text-gray-400 mb-4">
26
- <span class="af-passkey-card-last-used-label font-medium text-gray-700 dark:text-gray-300">{{$t('Last used')}}:</span>
26
+ <div class="af-passkey-card-last-used text-xs text-lightInputIconColor dark:text-darkInputIconColor mb-4">
27
+ <span class="af-passkey-card-last-used-label font-medium text-lightInputText dark:text-darkInputText">{{$t('Last used')}}:</span>
27
28
  <span v-if="item.last_used_at" class="af-passkey-card-last-used-date ml-1">{{ formatDateTime(item.last_used_at) }}</span>
28
29
  <span v-else class="af-passkey-card-last-used-never ml-1 italic">{{$t('Never')}}</span>
29
30
  </div>
@@ -32,26 +33,25 @@
32
33
  <Dialog
33
34
  class="edit-passkey-confirmation-dialog w-96"
34
35
  :buttons="[
35
- { label: 'Cancel', options: { class: 'dialog-cancel-button w-full' }, onclick: (dialog) => { passkeysNewName = ''; dialog.hide(); } },
36
- { label: 'Save', options: { class: 'dialog-save-button w-full' }, onclick: (dialog) => { renamePasskey(item.id, passkeysNewName); dialog.hide(); } },
36
+ { label: t('Cancel'), options: { class: 'dialog-cancel-button w-full', variant: 'secondary' }, onclick: (dialog) => { passkeysNewName = ''; dialog.hide(); } },
37
+ { label: t('Save'), options: { class: 'dialog-save-button w-full' }, onclick: (dialog) => { renamePasskey(item.id, passkeysNewName); dialog.hide(); } },
37
38
  ]"
38
- header="Edit Passkey"
39
+ :header="t('Edit Passkey')"
39
40
  >
40
41
  <template #trigger>
41
42
  <Button
42
43
  type="button"
43
44
  variant="secondary"
44
- :shadow="false"
45
45
  @click="passkeysNewName = ''"
46
- class="af-passkey-btn-rename w-full gap-1.5 px-3 py-2 text-sm"
46
+ class="af-passkey-btn-rename w-full"
47
47
  >
48
- <IconPenOutline class="w-4 h-4" />
48
+ <IconPencil class="w-4 h-4" />
49
49
  {{$t('Rename')}}
50
50
  </Button>
51
51
  </template>
52
52
  <div class="flex flex-col gap-2">
53
53
  <label class="text-sm font-medium">{{$t('Current name:')}}</label>
54
- <p class="font-medium text-sm text-gray-900 dark:text-white pb-2 truncate" :title="item.name">{{ item.name }}</p>
54
+ <p class="font-medium text-sm text-lightInputText dark:text-darkInputText pb-2 truncate" :title="item.name">{{ item.name }}</p>
55
55
  <label class="text-sm font-medium">{{$t('Enter new passkey name:')}}</label>
56
56
  <Input
57
57
  v-model="passkeysNewName"
@@ -62,25 +62,26 @@
62
62
  </div>
63
63
  </Dialog>
64
64
 
65
- <button
65
+ <Button
66
66
  type="button"
67
+ variant="danger-soft"
67
68
  @click="confirmAndDelete(item.id)"
68
- class="af-passkey-btn-remove flex items-center justify-center gap-1.5 px-3 py-2 text-sm font-medium rounded-default border border-red-200 dark:border-red-500/30 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10 transition-colors cursor-pointer"
69
+ class="af-passkey-btn-remove w-full"
69
70
  >
70
- <IconTrashBinOutline class="w-4 h-4" />
71
+ <IconTrash2 class="w-4 h-4" />
71
72
  {{$t('Remove')}}
72
- </button>
73
+ </Button>
73
74
  </div>
74
- </div>
75
+ </Card>
75
76
 
76
77
  <template v-if="isInitialFinished">
77
78
  <Tooltip v-if="!isPasskeySupported && (authenticatorAttachment === 'platform' || authenticatorAttachment === 'both')">
78
- <div class="af-passkey-add-card af-passkey-add-local flex flex-col items-center justify-center gap-2 p-4 rounded-xl border-2 border-dashed border-gray-300 dark:border-gray-600 w-full lg:w-72 min-h-36 opacity-50 pointer-events-none">
79
+ <div class="af-passkey-add-card af-passkey-add-local flex flex-col items-center justify-center gap-2 p-4 rounded-lg border border-dashed border-lightInputBorder dark:border-darkInputBorder w-full lg:w-72 min-h-36 opacity-50 cursor-not-allowed">
79
80
  <div class="af-passkey-add-card-icon w-10 h-10 flex items-center justify-center rounded-full bg-lightPrimary/10 text-lightPrimary dark:bg-darkPrimary/20 dark:text-darkPrimary">
80
- <IconPlusOutline class="w-5 h-5" />
81
+ <IconPlus class="w-5 h-5" />
81
82
  </div>
82
- <p class="af-passkey-add-card-title font-medium text-gray-700 dark:text-gray-300 text-sm">{{$t('Add local passkey')}}</p>
83
- <p class="af-passkey-add-card-subtitle text-xs text-gray-400 dark:text-gray-500 text-center">{{$t('Register this device')}}</p>
83
+ <p class="af-passkey-add-card-title font-medium text-lightInputText dark:text-darkInputText text-sm">{{$t('Add local passkey')}}</p>
84
+ <p class="af-passkey-add-card-subtitle text-xs text-lightInputIconColor dark:text-darkInputIconColor text-center">{{$t('Register this device')}}</p>
84
85
  </div>
85
86
  <template #tooltip>
86
87
  <p class="max-w-64">{{$t('This browser or device is reporting partial passkey support.')}}</p>
@@ -93,15 +94,14 @@
93
94
  type="button"
94
95
  :disabled="isFetchingPasskey"
95
96
  @click="addLocalPasskey"
96
- class="af-passkey-add-card af-passkey-add-local flex flex-col items-center justify-center gap-2 p-4 rounded-xl border-2 border-dashed border-gray-300 dark:border-gray-600 hover:border-lightPrimary hover:bg-lightPrimary/5 dark:hover:border-darkPrimary dark:hover:bg-darkPrimary/10 transition-colors w-full lg:w-72 min-h-36"
97
- :class="isFetchingPasskey ? 'opacity-50 pointer-events-none' : 'cursor-pointer'"
97
+ class="af-passkey-add-card af-passkey-add-local flex flex-col items-center justify-center gap-2 p-4 rounded-lg border border-dashed border-lightInputBorder dark:border-darkInputBorder enabled:hover:border-lightPrimary enabled:hover:bg-lightPrimary/5 dark:enabled:hover:border-darkPrimary dark:enabled:hover:bg-darkPrimary/10 disabled:opacity-50 disabled:cursor-not-allowed transition-colors w-full lg:w-72 min-h-36"
98
98
  >
99
99
  <span class="af-passkey-add-card-icon w-10 h-10 flex items-center justify-center rounded-full bg-lightPrimary/10 text-lightPrimary dark:bg-darkPrimary/20 dark:text-darkPrimary">
100
100
  <Spinner v-if="isFetchingPasskey && addPasskeyMode === 'platform'" class="w-5 h-5" />
101
- <IconPlusOutline v-else class="w-5 h-5" />
101
+ <IconPlus v-else class="w-5 h-5" />
102
102
  </span>
103
- <span class="af-passkey-add-card-title font-medium text-gray-700 dark:text-gray-300 text-sm">{{$t('Add local passkey')}}</span>
104
- <span class="af-passkey-add-card-subtitle text-xs text-gray-500 dark:text-gray-400 text-center">{{$t('Register this device')}}</span>
103
+ <span class="af-passkey-add-card-title font-medium text-lightInputText dark:text-darkInputText text-sm">{{$t('Add local passkey')}}</span>
104
+ <span class="af-passkey-add-card-subtitle text-xs text-lightInputIconColor dark:text-darkInputIconColor text-center">{{$t('Register this device')}}</span>
105
105
  </button>
106
106
 
107
107
  <button
@@ -109,21 +109,20 @@
109
109
  type="button"
110
110
  :disabled="isFetchingPasskey"
111
111
  @click="addCrossDevicePasskey"
112
- class="af-passkey-add-card af-passkey-add-cross flex flex-col items-center justify-center gap-2 p-4 rounded-xl border-2 border-dashed border-gray-300 dark:border-gray-600 hover:border-lightPrimary hover:bg-lightPrimary/5 dark:hover:border-darkPrimary dark:hover:bg-darkPrimary/10 transition-colors w-full lg:w-72 min-h-36"
113
- :class="isFetchingPasskey ? 'opacity-50 pointer-events-none' : 'cursor-pointer'"
112
+ class="af-passkey-add-card af-passkey-add-cross flex flex-col items-center justify-center gap-2 p-4 rounded-lg border border-dashed border-lightInputBorder dark:border-darkInputBorder enabled:hover:border-lightPrimary enabled:hover:bg-lightPrimary/5 dark:enabled:hover:border-darkPrimary dark:enabled:hover:bg-darkPrimary/10 disabled:opacity-50 disabled:cursor-not-allowed transition-colors w-full lg:w-72 min-h-36"
114
113
  >
115
114
  <span class="af-passkey-add-card-icon w-10 h-10 flex items-center justify-center rounded-full bg-lightPrimary/10 text-lightPrimary dark:bg-darkPrimary/20 dark:text-darkPrimary">
116
115
  <Spinner v-if="isFetchingPasskey && addPasskeyMode === 'cross-platform'" class="w-5 h-5" />
117
- <IconPlusOutline v-else class="w-5 h-5" />
116
+ <IconPlus v-else class="w-5 h-5" />
118
117
  </span>
119
- <span class="af-passkey-add-card-title font-medium text-gray-700 dark:text-gray-300 text-sm">{{$t('Add cross-device')}}</span>
120
- <span class="af-passkey-add-card-subtitle text-xs text-gray-500 dark:text-gray-400 text-center">{{$t('Use phone or security key')}}</span>
118
+ <span class="af-passkey-add-card-title font-medium text-lightInputText dark:text-darkInputText text-sm">{{$t('Add cross-device')}}</span>
119
+ <span class="af-passkey-add-card-subtitle text-xs text-lightInputIconColor dark:text-darkInputIconColor text-center">{{$t('Use phone or security key')}}</span>
121
120
  </button>
122
121
  </template>
123
122
  </div>
124
123
 
125
124
  <TwoFaHint class="af-passkeys-info mt-6 w-full md:w-fit">
126
- <template #icon><IconInfoCircleOutline class="af-passkeys-info-icon w-5 h-5" /></template>
125
+ <template #icon><IconInfo class="af-passkeys-info-icon w-5 h-5" /></template>
127
126
  {{$t('Passkeys are a more secure alternative to passwords. They use your device\'s biometrics or PIN and are resistant to phishing attacks.')}}
128
127
  </TwoFaHint>
129
128
 
@@ -135,8 +134,8 @@
135
134
  import adminforth, { useAdminforth } from '@/adminforth';
136
135
  import { onMounted, ref, Ref } from 'vue';
137
136
  import { useI18n } from 'vue-i18n';
138
- import { Button, Dialog, Tooltip, Spinner, Input } from '@/afcl';
139
- import { IconTrashBinOutline, IconPenOutline, IconPlusOutline, IconInfoCircleOutline } from '@iconify-prerendered/vue-flowbite';
137
+ import { Button, Card, Dialog, Tooltip, Spinner, Input } from '@/afcl';
138
+ import { IconTrash2, IconPencil, IconPlus, IconInfo } from '@iconify-prerendered/vue-lucide';
140
139
  import TwoFaHint from './TwoFaHint.vue';
141
140
  import { useCoreStore } from '@/stores/core';
142
141
  import { useTwoFactorsAuth, type TwoFaConfirmationResult } from './use2faApi';
@@ -356,18 +355,3 @@
356
355
  }
357
356
  }
358
357
  </script>
359
-
360
- <style>
361
- /* Scoped styles for dialog buttons, because there is no different way to style them */
362
- .dialog-delete-button.dialog-delete-button {
363
- background-color: rgb(179, 27, 27);
364
- color: white;
365
- border: 1px solid rgb(153, 27, 27);
366
- }
367
- .dialog-delete-button.dialog-delete-button:hover { background-color: rgb(153, 27, 27); }
368
- .dialog-delete-button.dialog-delete-button:active { background-color: rgb(127, 20, 20); }
369
-
370
- .dark .dialog-delete-button.dialog-delete-button { background-color: rgb(220, 38, 38); color: white; }
371
- .dark .dialog-delete-button.dialog-delete-button:hover { background-color: rgb(185, 28, 28); }
372
- .dark .dialog-delete-button.dialog-delete-button:active { background-color: rgb(153, 27, 27); }
373
- </style>