aboutsummaryrefslogtreecommitdiff
path: root/front-end/src/components/Settings/TotpSettings.vue
diff options
context:
space:
mode:
Diffstat (limited to 'front-end/src/components/Settings/TotpSettings.vue')
-rw-r--r--front-end/src/components/Settings/TotpSettings.vue169
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