aboutsummaryrefslogtreecommitdiff
path: root/extension/src/entries/options
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2024-01-04 11:13:31 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2024-01-04 11:13:31 -0500
commite87c4b69036e32b4fcf3df89e8158fb52df6a4e0 (patch)
tree83ce96172100abb0949f60e3c733daf738cbcf2d /extension/src/entries/options
parent8dec218a1aa259f83b8178265a7d0d0f08817cac (diff)
package updates & partial account page added
Diffstat (limited to 'extension/src/entries/options')
-rw-r--r--extension/src/entries/options/App.vue23
-rw-r--r--extension/src/entries/options/components/Account.vue34
-rw-r--r--extension/src/entries/options/components/Identities.vue23
-rw-r--r--extension/src/entries/options/components/Pki.vue237
-rw-r--r--extension/src/entries/options/components/Privacy.vue19
-rw-r--r--extension/src/entries/options/components/SiteSettings.vue6
-rw-r--r--extension/src/entries/options/components/Totp.vue234
-rw-r--r--extension/src/entries/options/main.js7
8 files changed, 563 insertions, 20 deletions
diff --git a/extension/src/entries/options/App.vue b/extension/src/entries/options/App.vue
index dd71209..d1da48e 100644
--- a/extension/src/entries/options/App.vue
+++ b/extension/src/entries/options/App.vue
@@ -1,5 +1,9 @@
<template>
<main id="injected-root">
+
+ <!-- Global password/confirm promps -->
+ <ConfirmPrompt />
+ <PasswordPrompt />
<notifications class="toaster" group="form" position="top-right" />
@@ -17,6 +21,11 @@
</Tab>
<Tab v-slot="{ selected }">
<button class="tab-title" :class="{ selected }">
+ Account
+ </button>
+ </Tab>
+ <Tab v-slot="{ selected }">
+ <button class="tab-title" :class="{ selected }">
Privacy
</button>
</Tab>
@@ -56,6 +65,9 @@
<Identities :all-keys="allKeys" @edit-key="editKey"/>
</TabPanel>
<TabPanel>
+ <Account/>
+ </TabPanel>
+ <TabPanel>
<Privacy/>
</TabPanel>
<TabPanel>
@@ -115,6 +127,9 @@ import SiteSettings from './components/SiteSettings.vue';
import Identities from './components/Identities.vue';
import Privacy from "./components/Privacy.vue";
import { useStore } from "../store";
+import Account from "./components/Account.vue";
+import ConfirmPrompt from "../../components/ConfirmPrompt.vue";
+import PasswordPrompt from "../../components/PasswordPrompt.vue";
//Configure the notifier to use the notification library
@@ -128,7 +143,7 @@ const keyBuffer = ref<NostrPubKey>({} as NostrPubKey)
const editKey = (key: NostrPubKey) =>{
//Goto hidden tab
- selectedTab.value = 3
+ selectedTab.value = 4
//Set selected key
keyBuffer.value = { ...key }
}
@@ -165,7 +180,7 @@ watchEffect(() => darkMode.value ? document.body.classList.add('dark') : documen
</script>
-<style lang="scss" scoped>
+<style lang="scss">
main {
font-family: Avenir, Helvetica, Arial, sans-serif;
@@ -198,4 +213,8 @@ main {
}
}
+.text-color-background{
+ @apply text-gray-400 dark:text-gray-500;
+}
+
</style> \ No newline at end of file
diff --git a/extension/src/entries/options/components/Account.vue b/extension/src/entries/options/components/Account.vue
new file mode 100644
index 0000000..574dc51
--- /dev/null
+++ b/extension/src/entries/options/components/Account.vue
@@ -0,0 +1,34 @@
+<template>
+ <div id="account-settings-template" class="mt-4">
+ <div class="flex flex-col w-full max-w-lg gap-4 mx-auto">
+ <div class="">
+ <h3 class="text-center">Account Settings</h3>
+ <div class="">
+
+ </div>
+ </div>
+ <div class="">
+ <div class="w-full font-bold border-b border-gray-200 dark:border-dark-500">
+ Multi Factor
+ </div>
+ <div class="mt-4">
+ <Totp />
+ </div>
+ <div class="mt-8 ">
+ <Pki />
+ </div>
+ </div>
+ <div class="mt-6">
+ <h4>Password</h4>
+ <div class="">
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import Pki from './Pki.vue';
+import Totp from './Totp.vue';
+
+</script> \ No newline at end of file
diff --git a/extension/src/entries/options/components/Identities.vue b/extension/src/entries/options/components/Identities.vue
index 0e3e79d..b7765be 100644
--- a/extension/src/entries/options/components/Identities.vue
+++ b/extension/src/entries/options/components/Identities.vue
@@ -38,6 +38,13 @@
</PopoverPanel>
</Popover>
</div>
+ <div class="">
+ <div class="">
+ <button class="rounded btn sm" @click="store.refreshIdentities()">
+ <fa-icon icon="refresh" class="" />
+ </button>
+ </div>
+ </div>
</div>
<div v-for="key in allKeys" :key="key.Id" class="mt-2 mb-3">
<div class="" :class="{'selected': isSelected(key)}" @click.self="selectKey(key)">
@@ -90,7 +97,7 @@ import {
PopoverPanel,
PopoverOverlay
} from '@headlessui/vue'
-import { apiCall, configureNotifier } from '@vnuge/vnlib.browser';
+import { apiCall, configureNotifier, useConfirm } from '@vnuge/vnlib.browser';
import { NostrPubKey } from '../../../features';
import { notify } from "@kyvg/vue3-notification";
import { get, useClipboard } from '@vueuse/core';
@@ -106,7 +113,7 @@ const downloadAnchor = ref<HTMLAnchorElement>()
const store = useStore()
const { selectedKey, allKeys } = storeToRefs(store)
const { copy } = useClipboard()
-
+const { reveal } = useConfirm()
const isSelected = (me : NostrPubKey) => isEqual(me, selectedKey.value)
const editKey = (key : NostrPubKey) => emit('edit-key', key);
@@ -134,7 +141,17 @@ const prettyPrintDate = (key : NostrPubKey) => {
const onDeleteKey = async (key : NostrPubKey) => {
- if(!confirm(`Are you sure you want to delete ${key.UserName}?`)){
+ const { isCanceled } = await reveal({
+ title: 'Are you sure?',
+ text: `You are about to perminantly delete your identity ${key.UserName}.`,
+ subtext: 'This is a perminant action and cannot be undone.',
+ })
+
+ if(isCanceled){
+ return;
+ }
+
+ if(!confirm(`Are you REALLY sure you want to delete ${key.UserName}?`)){
return;
}
diff --git a/extension/src/entries/options/components/Pki.vue b/extension/src/entries/options/components/Pki.vue
new file mode 100644
index 0000000..ff64840
--- /dev/null
+++ b/extension/src/entries/options/components/Pki.vue
@@ -0,0 +1,237 @@
+<template>
+ <div id="pki-settings" class="container">
+ <div class="panel-content">
+
+ <div class="flex flex-row flex-wrap justify-between">
+ <div class="text-sm font-bold">OTP Auth Keys</div>
+ <div class="">
+ <div v-if="pkiEnabled" class="button-group">
+ <button class="btn xs" @click.prevent="setIsOpen(true)">
+ <fa-icon icon="plus" />
+ <span class="pl-2">Add Key</span>
+ </button>
+ <button class="btn red xs" @click.prevent="onDisable">
+ <fa-icon icon="minus-circle" />
+ <span class="pl-2">Disable</span>
+ </button>
+ </div>
+ <div v-else class="">
+ <button class="btn xs" @click.prevent="setIsOpen(true)">
+ <fa-icon icon="plus" />
+ <span class="pl-2">Add Key</span>
+ </button>
+ </div>
+ </div>
+
+ <div v-if="store.pkiServerKeys.length > 0" class="w-full mt-2">
+ <table class="min-w-full text-sm divide-y-2 divide-gray-200 dark:divide-dark-500">
+ <thead class="text-left">
+ <tr>
+ <th class="p-2 font-medium whitespace-nowrap dark:text-white">
+ KeyID
+ </th>
+ <th class="p-2 font-medium whitespace-nowrap dark:text-white">
+ Algorithm
+ </th>
+ <th class="p-2 font-medium whitespace-nowrap dark:text-white">
+ Curve
+ </th>
+ <th class="p-2"></th>
+ </tr>
+ </thead>
+
+ <tbody class="divide-y divide-gray-200 dark:divide-dark-500">
+ <tr v-for="key in store.pkiServerKeys">
+ <td class="p-2 t font-medium truncate max-w-[8rem] whitespace-nowrap dark:text-white">
+ {{ key.kid }}
+ </td>
+ <td class="p-2 text-gray-700 whitespace-nowrap dark:text-gray-200">
+ {{ key.alg }}
+ </td>
+ <td class="p-2 text-gray-700 whitespace-nowrap dark:text-gray-200">
+ {{ key.crv }}
+ </td>
+ <td class="p-2 text-right whitespace-nowrap">
+ <button class="rounded btn red xs borderless" @click="onRemoveKey(key)">
+ <span class="hidden sm:inline">Remove</span>
+ <fa-icon icon="trash-can" class="inline sm:hidden" />
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <p v-else class="p-1 pt-3 text-sm text-color-background">
+ PKI authentication is a method of authenticating your user account with signed messages and a shared
+ public key. This method implementation
+ uses client signed Json Web Tokens to authenticate user generated outside this website as a One Time
+ Password (OTP). This allows for you to
+ use your favorite hardware or software tools, to generate said OTPs to authenticate your user.
+ </p>
+ </div>
+ </div>
+ </div>
+ <Dialog :open="isOpen" @close="setIsOpen" class="relative z-30">
+ <div class="fixed inset-0 bg-black/30" aria-hidden="true" />
+
+ <div class="fixed inset-0 flex justify-center">
+ <DialogPanel class="w-full max-w-lg p-4 m-auto mt-24 bg-white rounded dark:bg-dark-700 dark:text-gray-300">
+ <h4>Configure your authentication key</h4>
+ <p class="mt-2 text-sm">
+ Please paste your authenticator's public key as a Json Web Key (JWK) object. Your JWK must include a kid
+ (key id) and a kty (key type) field.
+ </p>
+ <div class="p-2 mt-3">
+ <textarea class="w-full p-1 text-sm border dark:bg-dark-700 ring-0 dark:border-dark-400" rows="10"
+ v-model="keyData" />
+ </div>
+ <div class="flex justify-end gap-2 mt-4">
+ <button class="rounded btn sm primary" @click.prevent="onSubmitKeys">Submit</button>
+ <button class="rounded btn sm" @click.prevent="setIsOpen(false)">Cancel</button>
+ </div>
+ </DialogPanel>
+ </div>
+ </Dialog>
+</template>
+
+<script setup lang="ts">
+import { includes, isEmpty } from 'lodash'
+import { apiCall, useConfirm, useSession, debugLog, useFormToaster, MfaMethod } from '@vnuge/vnlib.browser'
+import { computed, ref, watch } from 'vue'
+import { Dialog, DialogPanel } from '@headlessui/vue'
+import { } from 'pinia'
+import { useStore } from '../../store'
+import { PkiPubKey } from '../../../features'
+
+const store = useStore()
+const { reveal } = useConfirm()
+const { isLocalAccount } = useSession()
+const { error, success } = useFormToaster()
+
+const pkiEnabled = computed(() => isLocalAccount.value && includes(store.mfaEnabledMethods, "pki" as MfaMethod))
+
+const isOpen = ref(false)
+const keyData = ref('')
+const pemFormat = ref(false)
+const explicitCurve = ref("")
+
+watch(isOpen, () => {
+ keyData.value = ''
+ pemFormat.value = false
+ explicitCurve.value = ""
+ //Reload status
+ store.mfaRefresh()
+})
+
+const setIsOpen = (value: boolean) => isOpen.value = value
+
+const onRemoveKey = async (single: PkiPubKey) => {
+ const { isCanceled } = await reveal({
+ title: 'Are you sure?',
+ text: `This will remove key ${single.kid} from your account.`
+ })
+ if (isCanceled) {
+ return;
+ }
+
+ //Delete pki
+ await apiCall(async () => {
+
+ //TODO: require password or some upgrade to disable
+ await store.pkiRemoveKey(single);
+
+ success({
+ title: 'Key was removed successfully.',
+ })
+
+ //Refresh the status
+ store.mfaRefresh()
+ });
+}
+
+const onDisable = async () => {
+ const { isCanceled } = await reveal({
+ title: 'Are you sure?',
+ text: 'This will disable PKI authentication for your account.'
+ })
+ if (isCanceled) {
+ return;
+ }
+
+ //Delete pki
+ await apiCall(async ({ toaster }) => {
+
+ //Disable pki
+ //TODO: require password or some upgrade to disable
+ const { success } = await store.disable();
+
+ if (success) {
+ toaster.general.success({
+ title: 'Success',
+ text: 'PKI authentication has been disabled.'
+ })
+ }
+ else {
+ toaster.general.error({
+ title: 'Error',
+ text: 'PKI authentication could not be disabled.'
+ })
+ }
+
+ //Refresh the status
+ store.mfaRefresh()
+ });
+}
+
+const onSubmitKeys = async () => {
+
+ if (window.crypto.subtle == null) {
+ error({ title: "Your browser does not support PKI authentication." })
+ return;
+ }
+
+ //Validate key data
+ if (isEmpty(keyData.value)) {
+ error({ title: "Please enter key data" })
+ return;
+ }
+
+ let jwk: PkiPubKey;
+ try {
+ //Try to parse as jwk
+ jwk = JSON.parse(keyData.value)
+ if (isEmpty(jwk.use)
+ || isEmpty(jwk.kty)
+ || isEmpty(jwk.alg)
+ || isEmpty(jwk.kid)
+ || isEmpty(jwk.x)
+ || isEmpty(jwk.y)) {
+ throw new Error("Invalid JWK");
+ }
+ }
+ catch (e) {
+ //Write error to debug log
+ debugLog(e)
+ error({ title: "The key is not a valid Json Web Key (JWK)" })
+ return;
+ }
+
+ //Send to server
+ await apiCall(async () => {
+
+ //init/update the key
+ //TODO: require password or some upgrade to disable
+ await store.pkiAddKey(jwk);
+
+ success({
+ title: 'Successfully update your PKI keys.',
+ })
+
+ setIsOpen(false)
+ })
+}
+
+</script>
+
+<style></style>
diff --git a/extension/src/entries/options/components/Privacy.vue b/extension/src/entries/options/components/Privacy.vue
index d54f679..46261b3 100644
--- a/extension/src/entries/options/components/Privacy.vue
+++ b/extension/src/entries/options/components/Privacy.vue
@@ -1,18 +1,19 @@
<template>
- <div class="flex flex-col w-full mt-4 sm:px-2">
- <div class="flex flex-row gap-1">
- <div class="text-2xl">
- Tracking protection
- </div>
- <div class="mt-auto" :class="[isOriginProtectionOn ? 'text-primary-600' : 'text-red-500']">
- {{ isOriginProtectionOn ? 'active' : 'inactive' }}
+ <div class="flex flex-col w-full max-w-md mx-auto mt-4 sm:px-2">
+ <div class="flex flex-row gap-1 mx-auto">
+ <div class="mb-auto mr-1" >
+ <div class="w-2 h-2 rounded-full" :class="[isOriginProtectionOn ? 'bg-primary-600' : 'bg-red-500']">
+ </div>
</div>
+ <h3 class="text-2xl">
+ Tracking protection
+ </h3>
</div>
<div class="">
<div class="p-2">
<div class="my-1">
- <form @submit.prevent="allowOrigin()">
- <input class="w-full max-w-xs input primary" type="text" v-model="newOrigin" placeholder="Add new origin"/>
+ <form class="flex flex-row w-full" @submit.prevent="allowOrigin()">
+ <input class="flex-1 input primary" type="text" v-model="newOrigin" placeholder="Add new origin"/>
<button type="submit" class="ml-1 btn xs" >
<fa-icon icon="plus" />
</button>
diff --git a/extension/src/entries/options/components/SiteSettings.vue b/extension/src/entries/options/components/SiteSettings.vue
index b31bb9c..17f41c4 100644
--- a/extension/src/entries/options/components/SiteSettings.vue
+++ b/extension/src/entries/options/components/SiteSettings.vue
@@ -128,9 +128,9 @@ import { useStore } from '../../store';
import { storeToRefs } from 'pinia';
import useVuelidate from '@vuelidate/core'
-const { waiting } = useWait();
const store = useStore()
-const { settings, isOriginProtectionOn } = storeToRefs(store)
+const { settings } = storeToRefs(store)
+const { waiting } = useWait();
const { apply, data, buffer, modified, update } = useDataBuffer(settings.value, async sb =>{
const newConfig = await store.saveSiteConfig(sb.buffer)
@@ -142,7 +142,7 @@ const { apply, data, buffer, modified, update } = useDataBuffer(settings.value,
watch(settings, v => apply(v))
const originProtection = computed({
- get: () => isOriginProtectionOn.value,
+ get: () => store.isOriginProtectionOn,
set: v => store.setOriginProtection(v)
})
diff --git a/extension/src/entries/options/components/Totp.vue b/extension/src/entries/options/components/Totp.vue
new file mode 100644
index 0000000..ed836b7
--- /dev/null
+++ b/extension/src/entries/options/components/Totp.vue
@@ -0,0 +1,234 @@
+<template>
+ <div id="totp-settings">
+
+ <div v-if="showTotpCode" class="w-full py-2 text-center">
+ <h5 class="text-center" />
+ <p class="py-2">
+ Scan the QR code with your TOTP authenticator app.
+ </p>
+
+ <div class="flex">
+ <VueQrcode class="m-auto" :value="qrCode" />
+ </div>
+
+ <p class="py-2">
+ Your secret, if your application requires it.
+ </p>
+
+ <p
+ class="flex flex-row flex-wrap justify-center p-2 bg-gray-200 border border-gray-300 dark:bg-dark-800 dark:border-dark-500">
+ <span v-for="code in secretSegments" :key="code" class="px-2 font-mono tracking-wider">
+ {{ code }}
+ </span>
+ </p>
+
+ <p class="py-2 text-color-background">
+ Please enter your code from your authenticator app to continue.
+ </p>
+
+ <div class="m-auto w-min">
+ <VOtpInput class="otp-input" input-type="letter-numeric" separator="" value="" :is-disabled="showSubmitButton"
+ input-classes="primary input rounded" :num-inputs="6" @on-change="onInput" @on-complete="VerifyTotp" />
+ </div>
+
+ <div v-if="showSubmitButton" class="flex flex-row justify-end my-2">
+ <button class="btn primary" @click.prevent="CloseQrWindow">
+ Complete
+ </button>
+ </div>
+ </div>
+
+ <div v-else class="flex flex-row flex-wrap justify-between">
+ <div class="text-sm font-bold">TOTP Authenticator App</div>
+
+ <div v-if="totpEnabled" class="button-group">
+ <button class="btn xs" @click.prevent="regenTotp">
+ <fa-icon icon="sync" />
+ <span class="pl-2">Regenerate</span>
+ </button>
+ <button class="btn red xs" @click.prevent="disable">
+ <fa-icon icon="minus-circle" />
+ <span class="pl-2">Disable</span>
+ </button>
+ </div>
+
+ <div v-else>
+ <button class="btn xs" @click.prevent="configTotp">
+ <fa-icon icon="plus" />
+ <span class="pl-2">Setup</span>
+ </button>
+ </div>
+ <p v-if="!totpEnabled" class="p-1 pt-3 text-sm text-color-background">
+ TOTP is a time based one time password. You can use it as a form of Multi Factor Authentication when
+ using another device such as a smart phone or TOTP hardware device. You can use TOTP with your smart
+ phone
+ using apps like Google Authenticator, Authy, or Duo. Read more on
+ <a class="link" href="https://en.wikipedia.org/wiki/Time-based_one-time_password" target="_blank">
+ Wikipedia.
+ </a>
+ </p>
+
+ <p v-else class="w-full p-1 pt-1 text-sm text-color-background">
+ TOTP is enabled for your account.
+ </p>
+ </div>
+
+ </div>
+</template>
+
+<script setup lang="ts">
+import { isNil, chunk, defaultTo, includes, map, join } from 'lodash'
+import { TOTP } from 'otpauth'
+import base32Encode from 'base32-encode'
+import VueQrcode from '@chenfengyuan/vue-qrcode'
+import VOtpInput from "vue3-otp-input";
+import { computed, ref } from 'vue'
+import {
+ useSession,
+ useMessage,
+ useConfirm,
+ usePassConfirm,
+ useFormToaster,
+ MfaMethod
+} from '@vnuge/vnlib.browser'
+import { storeToRefs } from 'pinia';
+import { useStore } from '../../store';
+import { Mutable } from '@vueuse/core';
+import { TotpUpdateMessage } from '../../../features/types';
+
+type TotpConfig = Mutable<TotpUpdateMessage>
+
+const store = useStore()
+const { userName } = storeToRefs(store);
+
+const { KeyStore } = useSession()
+const { reveal } = useConfirm()
+const { elevatedApiCall } = usePassConfirm()
+const { onInput, setMessage } = useMessage()
+
+const totpEnabled = computed(() => includes(store.mfaEnabledMethods, MfaMethod.TOTP))
+
+const totpMessage = ref<TotpConfig>()
+const showSubmitButton = ref(false)
+const toaster = useFormToaster()
+
+const showTotpCode = computed(() => !isNil(totpMessage.value?.secret))
+
+const secretSegments = computed<string[]>(() => {
+ //Chunk the secret into 6 character segments
+ const chunks = chunk(totpMessage.value?.secret, 6)
+ //Join the chunks into their chunk arrays
+ return map(chunks, chunk => join(chunk, ''))
+})
+
+const qrCode = computed(() => {
+ if (isNil(totpMessage.value?.secret)) {
+ return ''
+ }
+
+ const m = totpMessage.value!;
+
+ // Build the totp qr codeurl
+ const params = new URLSearchParams()
+ params.append('secret', m.secret)
+ params.append('issuer', m.issuer)
+ params.append('algorithm', m.algorithm)
+ params.append('digits', defaultTo(m.digits, 6).toString())
+ params.append('period', defaultTo(m.period, 30).toString())
+ const url = `otpauth://totp/${m.issuer}:${userName.value}?${params.toString()}`
+ return url
+})
+
+const ProcessAddOrUpdate = async () => {
+ await elevatedApiCall(async ({ password }) => {
+
+ // Init or update the totp method and get the encrypted totp message
+ const totp = await store.mfaUpsertMethod(MfaMethod.TOTP, password) as TotpConfig
+
+ // Decrypt the totp secret
+ const secretBuf = await KeyStore.decryptDataAsync(totp.secret)
+
+ // Encode the secret to base32
+ totp.secret = base32Encode(secretBuf, 'RFC3548', { padding: false })
+
+ totpMessage.value = totp
+ })
+}
+
+const configTotp = async () => {
+ const { isCanceled } = await reveal({
+ title: 'Enable TOTP multi factor?',
+ text: 'Are you sure you understand TOTP multi factor and wish to enable it?',
+ })
+
+ if (!isCanceled) {
+ ProcessAddOrUpdate()
+ }
+}
+
+const regenTotp = async () => {
+ // If totp is enabled, show a prompt to regenerate totp
+ if (!totpEnabled.value) {
+ return
+ }
+
+ const { isCanceled } = await reveal({
+ title: 'Are you sure?',
+ text: 'If you continue your previous TOTP authenticator and recovery codes will no longer be valid.'
+ })
+
+ if (!isCanceled) {
+ ProcessAddOrUpdate()
+ }
+}
+
+const disable = async () => {
+ // Show a confrimation prompt
+ const { isCanceled } = await reveal({
+ title: 'Disable TOTP',
+ text: 'Are you sure you want to disable TOTP? You may re-enable TOTP later.'
+ })
+
+ if (isCanceled) {
+ return
+ }
+
+ await elevatedApiCall(async ({ password }) => {
+ // Disable the totp method
+ await store.mfaDisableMethod(MfaMethod.TOTP, password)
+ })
+}
+
+const VerifyTotp = async (code: string) => {
+ // Create a new TOTP instance from the current message
+ const totp = new TOTP(totpMessage.value)
+
+ // validate the code
+ const valid = totp.validate({ token: code, window: 4 })
+
+ if (valid) {
+ showSubmitButton.value = true
+ toaster.success({
+ title: 'Success',
+ text: 'Your TOTP code is valid and your account is now verified.'
+ })
+ } else {
+ setMessage('Your TOTP code is not valid.')
+ }
+}
+
+const CloseQrWindow = () => {
+ showSubmitButton.value = false
+ totpMessage.value = undefined
+
+ //Fresh methods
+ store.mfaRefresh()
+}
+
+</script>
+
+<style>
+#totp-settings .otp-input input {
+ @apply w-12 text-center text-lg mx-1 focus:border-primary-500;
+}
+</style>
diff --git a/extension/src/entries/options/main.js b/extension/src/entries/options/main.js
index 827c426..7747735 100644
--- a/extension/src/entries/options/main.js
+++ b/extension/src/entries/options/main.js
@@ -22,12 +22,12 @@ import Notifications from "@kyvg/vue3-notification";
/* FONT AWESOME CONFIG */
import { library } from '@fortawesome/fontawesome-svg-core'
-import { faChevronLeft, faChevronRight, faCopy, faDownload, faEdit, faExternalLinkAlt, faLock, faLockOpen, faMoon, faPlus, faSun, faTrash } from '@fortawesome/free-solid-svg-icons'
+import { faChevronLeft, faChevronRight, faCopy, faDownload, faEdit, faExternalLinkAlt, faLock, faLockOpen, faMinusCircle, faMoon, faPlus, faRefresh, faSun, faTrash, faTrashCan } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { createPinia } from "pinia";
-import { identityPlugin, originPlugin, useBackgroundPiniaPlugin } from "../store";
+import { identityPlugin, mfaConfigPlugin, originPlugin, useBackgroundPiniaPlugin } from "../store";
-library.add(faCopy, faEdit, faChevronLeft, faMoon, faSun, faLock, faLockOpen, faExternalLinkAlt, faTrash, faDownload, faChevronRight, faPlus)
+library.add(faCopy, faEdit, faChevronLeft, faMoon, faSun, faLock, faLockOpen, faExternalLinkAlt, faTrash, faDownload, faChevronRight, faPlus, faRefresh, faTrashCan, faMinusCircle)
//Create the background feature wiring
const bgPlugins = useBackgroundPiniaPlugin('options')
@@ -36,6 +36,7 @@ const pinia = createPinia()
.use(bgPlugins) //Add the background pinia plugin
.use(identityPlugin) //Add the identity plugin
.use(originPlugin) //Add the origin plugin
+ .use(mfaConfigPlugin) //Add the mfa config plugin
createApp(App)
.use(Notifications)