@adminforth/two-factors-auth 4.8.3 → 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.
@@ -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>
@@ -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,37 +16,38 @@
16
16
  :error="codeError"
17
17
  >
18
18
  <div>
19
- <p class="mb-3 flex items-center gap-2.5 text-sm font-medium text-gray-900 dark:text-white">
19
+ <p class="mb-3 flex items-center gap-2.5 text-sm font-medium text-lightInputText dark:text-darkInputText">
20
20
  <span class="flex w-6 h-6 shrink-0 items-center justify-center rounded-full bg-lightPrimary/10 text-xs font-semibold text-lightPrimary dark:bg-darkPrimary/20 dark:text-darkPrimary">1</span>
21
21
  {{ $t('Scan the QR code with your authenticator app') }}
22
22
  </p>
23
- <div class="flex items-center gap-4 rounded-lg border border-lightDialogBreakLine bg-gray-50 p-3.5 dark:border-darkDialogBreakLine dark:bg-gray-800">
23
+ <div class="flex items-center gap-4 rounded-lg border border-lightDialogBreakLine bg-lightSecondaryDarken p-3.5 dark:border-darkDialogBreakLine dark:bg-darkSecondaryLighten">
24
+ <!-- a QR code needs a white ground to be scanned, in the dark theme too -->
24
25
  <div class="w-32 h-32 shrink-0 rounded-lg bg-white p-2">
25
26
  <img v-if="totpQrCode" :src="totpQrCode" class="af-qr-code w-full h-full" alt="QR code" />
26
27
  </div>
27
28
  <div class="flex min-w-0 flex-col gap-2">
28
- <p class="text-sm leading-5 text-gray-500 dark:text-gray-400">{{ $t('Setting up on this device?') }}</p>
29
- <a :href="totpUri" class="inline-flex items-center gap-1.5 text-sm font-medium text-lightPrimary hover:underline dark:text-darkPrimary">
29
+ <p class="text-sm leading-5 text-lightInputIconColor dark:text-darkInputIconColor">{{ $t('Setting up on this device?') }}</p>
30
+ <Link :to="totpUri" class="inline-flex items-center gap-1.5 text-sm">
30
31
  {{ $t('Open in authenticator app') }}
31
- <IconArrowUpRightFromSquareOutline class="w-4 h-4" />
32
- </a>
32
+ <IconExternalLink class="w-4 h-4" />
33
+ </Link>
33
34
  </div>
34
35
  </div>
35
36
  </div>
36
37
 
37
38
  <div>
38
- <p class="mb-2 text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">{{ $t('Or enter the key manually') }}</p>
39
- <div class="flex items-center gap-2 rounded-lg border border-lightDialogBreakLine bg-gray-50 py-1.5 pl-3.5 pr-1.5 dark:border-darkDialogBreakLine dark:bg-gray-800">
40
- <span class="af-2fa-secret flex-1 min-w-0 select-all break-all font-mono text-sm tracking-wider text-gray-800 dark:text-gray-100">{{ totp.newSecret }}</span>
41
- <button type="button" :aria-label="$t('Copy key')" class="af-2fa-copy-btn inline-flex w-9 h-9 shrink-0 items-center justify-center rounded-lg text-gray-500 hover:bg-lightDialogCloseButtonHoverBackground hover:text-lightDialogCloseButtonHover dark:text-gray-400 dark:hover:bg-darkDialogCloseButtonHoverBackground dark:hover:text-darkDialogCloseButtonHover" @click="onCopyClick">
42
- <IconFileCopyOutline class="w-5 h-5" />
43
- </button>
39
+ <p class="mb-2 text-xs font-medium uppercase tracking-wider text-lightInputIconColor dark:text-darkInputIconColor">{{ $t('Or enter the key manually') }}</p>
40
+ <div class="flex items-center gap-2 rounded-lg border border-lightDialogBreakLine bg-lightSecondaryDarken py-1.5 pl-3.5 pr-1.5 dark:border-darkDialogBreakLine dark:bg-darkSecondaryLighten">
41
+ <span class="af-2fa-secret flex-1 min-w-0 select-all break-all font-mono text-sm tracking-wider text-lightInputText dark:text-darkInputText">{{ totp.newSecret }}</span>
42
+ <Button type="button" variant="ghost" iconOnly :aria-label="$t('Copy key')" class="af-2fa-copy-btn" @click="onCopyClick">
43
+ <IconCopy aria-hidden="true" />
44
+ </Button>
44
45
  </div>
45
46
  </div>
46
47
 
47
48
  <TwoFaCodeInput ref="codeInput" :invalid="Boolean(codeError)" @complete="handleOnComplete">
48
49
  <template #label>
49
- <p class="mb-3 flex items-center gap-2.5 text-sm font-medium text-gray-900 dark:text-white">
50
+ <p class="mb-3 flex items-center gap-2.5 text-sm font-medium text-lightInputText dark:text-darkInputText">
50
51
  <span class="flex w-6 h-6 shrink-0 items-center justify-center rounded-full bg-lightPrimary/10 text-xs font-semibold text-lightPrimary dark:bg-darkPrimary/20 dark:text-darkPrimary">2</span>
51
52
  {{ $t('Enter the 6-digit code from the app') }}
52
53
  </p>
@@ -54,9 +55,9 @@
54
55
  </TwoFaCodeInput>
55
56
 
56
57
  <template #footer>
57
- <button v-if="skipAllowed" type="button" class="af-2fa-skip-btn inline-flex h-11 items-center whitespace-nowrap text-sm font-medium text-gray-600 hover:underline dark:text-gray-300" @click="handleSkip">
58
+ <Button v-if="skipAllowed" type="button" variant="ghost" class="af-2fa-skip-btn -ms-3.5" @click="handleSkip">
58
59
  {{ $t('Skip for now') }}
59
- </button>
60
+ </Button>
60
61
  <LinkButton to="/login" variant="secondary" class="af-2fa-back-to-login ms-auto whitespace-nowrap">{{ $t('Back to login') }}</LinkButton>
61
62
  </template>
62
63
  </TwoFaCard>
@@ -70,19 +71,16 @@
70
71
  import { onMounted, ref, computed, onBeforeMount } from 'vue';
71
72
  import { useCoreStore } from '@/stores/core';
72
73
  import { useUserStore } from '@/stores/user';
73
- import { IconArrowUpRightFromSquareOutline, IconEyeSolid, IconEyeSlashSolid, IconFileCopyOutline } from '@iconify-prerendered/vue-flowbite';
74
+ import { IconCopy, IconExternalLink } from '@iconify-prerendered/vue-lucide';
74
75
  import { callAdminForthApi, loadFile } from '@/utils';
75
76
  import { useRouter } from 'vue-router';
76
- import { showErrorTost } from '@/composables/useFrontendApi';
77
- import { LinkButton } from '@/afcl';
78
- import Vue2FACodeInput from '@loltech/vue3-2fa-code-input';
77
+ import { Button, Link, LinkButton } from '@/afcl';
79
78
  import adminforth from '@/adminforth';
80
79
  import TwoFaCard from './TwoFaCard.vue';
81
80
  import TwoFaCodeInput from './TwoFaCodeInput.vue';
82
81
  import { useI18n } from 'vue-i18n';
83
82
  import { useRoute } from 'vue-router';
84
83
  import { handlePasskeyAlert } from './utils.js';
85
- import ErrorMessage from '@/components/ErrorMessage.vue';
86
84
  import { useQRCode } from '@vueuse/integrations/useQRCode'
87
85
 
88
86
  const { t } = useI18n();
@@ -113,9 +111,6 @@ const user = useUserStore();
113
111
 
114
112
  const skipAllowed = ref(false);
115
113
 
116
- // use this simple function to automatically focus on the next input
117
-
118
- const showPw = ref(false);
119
114
 
120
115
  const error = ref(null);
121
116
  const totp = ref<{ issuer?: string; userName?: string; newSecret?: string }>({});