diff options
author | vnugent <public@vaughnnugent.com> | 2024-01-04 11:13:31 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2024-01-04 11:13:31 -0500 |
commit | e87c4b69036e32b4fcf3df89e8158fb52df6a4e0 (patch) | |
tree | 83ce96172100abb0949f60e3c733daf738cbcf2d /extension/src/entries/options | |
parent | 8dec218a1aa259f83b8178265a7d0d0f08817cac (diff) |
package updates & partial account page added
Diffstat (limited to 'extension/src/entries/options')
-rw-r--r-- | extension/src/entries/options/App.vue | 23 | ||||
-rw-r--r-- | extension/src/entries/options/components/Account.vue | 34 | ||||
-rw-r--r-- | extension/src/entries/options/components/Identities.vue | 23 | ||||
-rw-r--r-- | extension/src/entries/options/components/Pki.vue | 237 | ||||
-rw-r--r-- | extension/src/entries/options/components/Privacy.vue | 19 | ||||
-rw-r--r-- | extension/src/entries/options/components/SiteSettings.vue | 6 | ||||
-rw-r--r-- | extension/src/entries/options/components/Totp.vue | 234 | ||||
-rw-r--r-- | extension/src/entries/options/main.js | 7 |
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) |