it-tools/src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue

146 lines
3.9 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { useTimestamp } from '@vueuse/core';
import { useThemeVars } from 'naive-ui';
import { useQRCode } from '../qr-code-generator/useQRCode';
import { base32toHex, buildKeyUri, generateSecret, generateTOTP, getCounterFromTime } from './otp.service';
import TokenDisplay from './token-display.vue';
import { useStyleStore } from '@/stores/style.store';
import InputCopyable from '@/components/InputCopyable.vue';
import { computedRefreshable } from '@/composable/computedRefreshable';
const now = useTimestamp();
const interval = computed(() => (now.value / 1000) % 30);
const theme = useThemeVars();
const styleStore = useStyleStore();
const secret = ref(generateSecret());
function refreshSecret() {
secret.value = generateSecret();
}
const [tokens] = computedRefreshable(
() => ({
previous: generateTOTP({ key: secret.value, now: now.value - 30000 }),
current: generateTOTP({ key: secret.value, now: now.value }),
next: generateTOTP({ key: secret.value, now: now.value + 30000 }),
}),
{ throttle: 500 },
);
const keyUri = computed(() => buildKeyUri({ secret: secret.value }));
const { qrcode } = useQRCode({
text: keyUri,
color: {
background: computed(() => (styleStore.isDarkTheme ? '#ffffff' : '#00000000')),
foreground: '#000000',
},
options: { width: 210 },
});
const secretValidationRules = [
{
message: 'Secret should be a base32 string',
validator: (value: string) => value.toUpperCase().match(/^[A-Z234567]+$/),
},
{
message: 'Please set a secret',
validator: (value: string) => value !== '',
},
];
</script>
2022-08-24 00:10:53 +02:00
<template>
<div style="max-width: 350px">
<c-input-text
v-model:value="secret"
label="Secret"
placeholder="Paste your TOTP secret..."
mb-5
:validation-rules="secretValidationRules"
>
<template #suffix>
<n-tooltip trigger="hover">
<template #trigger>
<c-button circle variant="text" size="small" @click="refreshSecret">
<icon-mdi-refresh />
</c-button>
</template>
Generate secret token
</n-tooltip>
</template>
</c-input-text>
2022-08-24 00:10:53 +02:00
<div>
<TokenDisplay :tokens="tokens" style="margin-top: 2px" />
2022-08-24 00:10:53 +02:00
<n-progress :percentage="(100 * interval) / 30" :color="theme.primaryColor" :show-indicator="false" />
<div style="text-align: center">
Next in {{ String(Math.floor(30 - interval)).padStart(2, '0') }}s
</div>
2022-08-24 00:10:53 +02:00
</div>
2023-05-27 17:36:15 +02:00
<div mt-4 flex flex-col items-center justify-center gap-3>
<n-image :src="qrcode" />
<c-button :href="keyUri" target="_blank">
Open Key URI in new tab
</c-button>
2023-05-27 17:36:15 +02:00
</div>
2022-08-24 00:10:53 +02:00
</div>
<div style="max-width: 350px">
<InputCopyable
label="Secret in hexadecimal"
:value="base32toHex(secret)"
readonly
placeholder="Secret in hex will be displayed here"
mb-5
/>
<InputCopyable
label="Epoch"
:value="Math.floor(now / 1000).toString()"
readonly
mb-5
placeholder="Epoch in sec will be displayed here"
/>
<p>Iteration</p>
<InputCopyable
:value="String(getCounterFromTime({ now, timeStep: 30 }))"
readonly
label="Count:"
label-position="left"
label-width="90px"
label-align="right"
placeholder="Iteration count will be displayed here"
/>
<InputCopyable
:value="getCounterFromTime({ now, timeStep: 30 }).toString(16).padStart(16, '0')"
readonly
placeholder="Iteration count in hex will be displayed here"
label-position="left"
label-width="90px"
label-align="right"
label="Padded hex:"
/>
2022-08-24 00:10:53 +02:00
</div>
</template>
<style lang="less" scoped>
.n-progress {
margin-top: 10px;
::v-deep(.n-progress-graph-line-fill) {
transition-duration: 0.05s !important;
}
}
.token {
text-align: center;
&.token-current {
font-size: 20px;
}
}
</style>