diff options
author | vnugent <public@vaughnnugent.com> | 2024-01-20 23:49:29 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2024-01-20 23:49:29 -0500 |
commit | 6cb7da37824d02a1898d08d0f9495c77fde4dd1d (patch) | |
tree | 95e37ea3c20f416d6a205ee4ab050c307b18eafe /front-end/src/components/Settings/TotpSettings.vue |
inital commit
Diffstat (limited to 'front-end/src/components/Settings/TotpSettings.vue')
-rw-r--r-- | front-end/src/components/Settings/TotpSettings.vue | 169 |
1 files changed, 169 insertions, 0 deletions
diff --git a/front-end/src/components/Settings/TotpSettings.vue b/front-end/src/components/Settings/TotpSettings.vue new file mode 100644 index 0000000..cc8642f --- /dev/null +++ b/front-end/src/components/Settings/TotpSettings.vue @@ -0,0 +1,169 @@ +<script setup lang="ts"> +import { computed, defineAsyncComponent, shallowRef } from 'vue'; +import { useStore } from '../../store'; +import { set, get } from '@vueuse/core'; +import { MfaMethod, useGeneralToaster, usePassConfirm, useSession } from '@vnuge/vnlib.browser'; +import { defaultTo, includes, isEmpty, isNil } from 'lodash-es'; +import { TOTP } from 'otpauth' +import base32Encode from 'base32-encode' +const QrCode = defineAsyncComponent(() => import('qrcode.vue')); +const Dialog = defineAsyncComponent(() => import('../global/Dialog.vue')); +const VOtpInput = defineAsyncComponent(() => import('vue3-otp-input')) + +interface TotpConfig { + secret: string; + readonly issuer: string; + readonly algorithm: string; + readonly digits?: number; + readonly period?: number; +} + +const { KeyStore } = useSession() +const { elevatedApiCall } = usePassConfirm() +const { success, error } = useGeneralToaster() +const store = useStore() +const newTotpConfig = shallowRef<TotpConfig | undefined>() +const totpEnabled = computed(() => includes(store.mfaEndabledMethods, MfaMethod.TOTP)) + +const qrCode = computed(() => { + + const m = get(newTotpConfig); + + if (isNil(m)) { + return '' + } + + // 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()) + + return `otpauth://totp/${m.issuer}:${store.userName}?${params.toString()}` +}) + +const showUpdateDialog = computed(() => !isEmpty(get(qrCode))) + +const disableTotp = async () => { + + elevatedApiCall(async ({ password, toaster }) =>{ + const { getResultOrThrow } = await store.mfaConfig.disableMethod(MfaMethod.TOTP, password); + getResultOrThrow(); + + toaster.general.success({ + title: 'TOTP Disabled', + text: 'TOTP has been disabled for your account.' + }) + }) +} + +const addOrUpdate = async () => { + + elevatedApiCall(async ({ password }) =>{ + const { getResultOrThrow } = await store.mfaConfig.initOrUpdateMethod<TotpConfig>(MfaMethod.TOTP, password); + const newConfig = getResultOrThrow(); + + // Decrypt the server sent secret + const decSecret = await KeyStore.decryptDataAsync(newConfig.secret); + // Encode the secret to base32 + newConfig.secret = base32Encode(decSecret, 'RFC3548', { padding: false }) + + set(newTotpConfig, newConfig); + }) +} + +const onVerifyOtp = async (code: string) => { + // Create a new TOTP instance from the current message + const totp = new TOTP(get(newTotpConfig)) + + // validate the code + const valid = totp.validate({ token: code, window: 4 }) + + if (valid) { + success({ + title: 'Success', + text: 'Your code is valid and TOPT has been enabled.' + }) + + //Close the dialog + set(newTotpConfig, undefined) + + } else { + error({ title: 'The code you entered is invalid.'}) + } +} + +</script> + +<template> + + <div class="flex flex-row items-center justify-between"> + <div class="relative me-4"> + <h4 class="text-base font-bold"> + TOTP + </h4> + <span + :class="[totpEnabled ? 'visible' : 'invisible' ]" + class="absolute top-0 w-3 h-3 bg-green-500 border-2 border-white rounded-full -end-5 dark:border-gray-800" + > + </span> + </div> + <div v-if="totpEnabled" class="flex" @click="addOrUpdate()"> + <button class="btn light"> + Regenerate + </button> + <button class="btn red" @click="disableTotp()"> + Disable + </button> + </div> + <div v-else> + <button class="btn green" @click="addOrUpdate()"> + Enable + </button> + </div> + </div> + <p class="mt-2 text-sm text-gray-500 dark:text-gray-400"> + Use Time based One Time Passcodes (TOTP) to secure your account as a second factor authentication. + </p> + + <Dialog :open="showUpdateDialog" title="TOTP Secret" @cancel=""> + <template #body> + <div class="p-4 pb-8"> + <div class="flex flex-col items-center justify-center"> + <div> + <p class="text-sm text-gray-500 dark:text-gray-400"> + Scan this QR code with your authenticator app to add this account. + </p> + </div> + <div class="mt-4"> + <QrCode :size="200" level="L" :value="qrCode" /> + </div> + <div class="w-full mt-4"> + <input + type="text" + id="disabled-input-2" + aria-label="disabled input 2" + class="bg-gray-100 border border-gray-300 text-gray-900 text-sm rounded focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 cursor-not-allowed dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500" + :value="qrCode" + disabled + readonly + > + </div> + + <div class="mx-auto mt-4"> + <VOtpInput class="otp-input" input-type="letter-numeric" separator="" + input-classes="rounded" :num-inputs="6" value="" @on-change="" + @on-complete="onVerifyOtp" /> + </div> + <p class="mt-2 text-sm text-gray-500 dark:text-gray-400"> + Enter the 6 digit code from your authenticator app to verify. + </p> + </div> + </div> + </template> + </Dialog> +</template> + +<style lang="scss"></style>
\ No newline at end of file |