This commit is contained in:
Simon Haas 2025-04-06 18:43:06 -07:00 committed by GitHub
commit b0a2b12d98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 92 additions and 21 deletions

View file

@ -0,0 +1,30 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
{
"name": "Node.js & TypeScript",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "yarn install",
// Configure tool-specific properties.
"customizations": {
"vscode": {
"extensions": [
"Vue.volar",
"Lokalise.i18n-ally",
"dbaeumer.vscode-eslint"
]
}
}
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

1
.gitignore vendored
View file

@ -13,6 +13,7 @@ dist
dist-ssr dist-ssr
coverage coverage
*.local *.local
.pnpm-store
/cypress/videos/ /cypress/videos/
/cypress/screenshots/ /cypress/screenshots/

8
components.d.ts vendored
View file

@ -98,6 +98,7 @@ declare module '@vue/runtime-core' {
IconMdiEye: typeof import('~icons/mdi/eye')['default'] IconMdiEye: typeof import('~icons/mdi/eye')['default']
IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default'] IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
IconMdiHeart: typeof import('~icons/mdi/heart')['default'] IconMdiHeart: typeof import('~icons/mdi/heart')['default']
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
IconMdiSearch: typeof import('~icons/mdi/search')['default'] IconMdiSearch: typeof import('~icons/mdi/search')['default']
IconMdiTranslate: typeof import('~icons/mdi/translate')['default'] IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default'] IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default']
@ -135,13 +136,20 @@ declare module '@vue/runtime-core' {
NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDivider: typeof import('naive-ui')['NDivider'] NDivider: typeof import('naive-ui')['NDivider']
NEllipsis: typeof import('naive-ui')['NEllipsis'] NEllipsis: typeof import('naive-ui')['NEllipsis']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NH1: typeof import('naive-ui')['NH1'] NH1: typeof import('naive-ui')['NH1']
NH3: typeof import('naive-ui')['NH3'] NH3: typeof import('naive-ui')['NH3']
NIcon: typeof import('naive-ui')['NIcon'] NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NLayout: typeof import('naive-ui')['NLayout'] NLayout: typeof import('naive-ui')['NLayout']
NLayoutSider: typeof import('naive-ui')['NLayoutSider'] NLayoutSider: typeof import('naive-ui')['NLayoutSider']
NMenu: typeof import('naive-ui')['NMenu'] NMenu: typeof import('naive-ui')['NMenu']
NProgress: typeof import('naive-ui')['NProgress']
NSlider: typeof import('naive-ui')['NSlider']
NSpace: typeof import('naive-ui')['NSpace'] NSpace: typeof import('naive-ui')['NSpace']
NSwitch: typeof import('naive-ui')['NSwitch']
NTable: typeof import('naive-ui')['NTable'] NTable: typeof import('naive-ui')['NTable']
NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']

View file

@ -444,7 +444,7 @@ tools:
otp-generator: otp-generator:
title: OTP-Code-Generator title: OTP-Code-Generator
description: >- description: >-
Generiere und validiere zeitbasierte OTPs (Einmalpasswörter) für Generiere und validiere zeitbasierte und ereignisgesteuertes OTPs (Einmalpasswörter) für
Multi-Faktor-Authentifizierung. Multi-Faktor-Authentifizierung.
url-encoder: url-encoder:
title: Kodieren/Decodieren von URL-formatierten Zeichenfolgen title: Kodieren/Decodieren von URL-formatierten Zeichenfolgen

View file

@ -383,7 +383,7 @@ tools:
otp-generator: otp-generator:
title: OTP code generator title: OTP code generator
description: Generate and validate time-based OTP (one time password) for multi-factor authentication. description: Generate and validate time-based and event-based OTP (one time password) for multi-factor authentication.
url-encoder: url-encoder:
title: Encode/decode URL-formatted strings title: Encode/decode URL-formatted strings

View file

@ -2,7 +2,7 @@
import { useTimestamp } from '@vueuse/core'; import { useTimestamp } from '@vueuse/core';
import { useThemeVars } from 'naive-ui'; import { useThemeVars } from 'naive-ui';
import { useQRCode } from '../qr-code-generator/useQRCode'; import { useQRCode } from '../qr-code-generator/useQRCode';
import { base32toHex, buildKeyUri, generateSecret, generateTOTP, getCounterFromTime } from './otp.service'; import { base32toHex, buildKeyUri, generateHOTP, generateSecret, generateTOTP, getCounterFromTime } from './otp.service';
import TokenDisplay from './token-display.vue'; import TokenDisplay from './token-display.vue';
import { useStyleStore } from '@/stores/style.store'; import { useStyleStore } from '@/stores/style.store';
import InputCopyable from '@/components/InputCopyable.vue'; import InputCopyable from '@/components/InputCopyable.vue';
@ -19,6 +19,16 @@ function refreshSecret() {
secret.value = generateSecret(); secret.value = generateSecret();
} }
const counter = ref(0);
const [hotpValues] = computedRefreshable(
() =>
Object.fromEntries(
Array.from({ length: 10 }, (_, i) => [+counter.value + i, generateHOTP({ key: secret.value, counter: +counter.value + i })]),
),
{ throttle: 500 },
);
const [tokens] = computedRefreshable( const [tokens] = computedRefreshable(
() => ({ () => ({
previous: generateTOTP({ key: secret.value, now: now.value - 30000 }), previous: generateTOTP({ key: secret.value, now: now.value - 30000 }),
@ -68,23 +78,6 @@ const secretValidationRules = [
</c-tooltip> </c-tooltip>
</template> </template>
</c-input-text> </c-input-text>
<div>
<TokenDisplay :tokens="tokens" />
<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>
</div>
<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>
</div>
</div>
<div style="max-width: 350px">
<InputCopyable <InputCopyable
label="Secret in hexadecimal" label="Secret in hexadecimal"
:value="base32toHex(secret)" :value="base32toHex(secret)"
@ -93,11 +86,41 @@ const secretValidationRules = [
mb-5 mb-5
/> />
<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>
</div>
</div>
<div style="max-width: 350px">
<div>
<c-input-text
v-model:value="counter"
label="Start-value for HOTP counter"
placeholder="Start counter for HOTP at..."
type="number"
mb-5
mt-5
/>
<InputCopyable
v-for="(value, currentCounter) in hotpValues" :key="currentCounter"
:value="value"
readonly
:label="`HOTP ${currentCounter}:`"
label-position="left"
label-width="90px"
label-align="right"
placeholder="HOTP will be displayed here"
mb-1
/>
</div>
</div>
<div style="max-width: 350px">
<InputCopyable <InputCopyable
label="Epoch" label="Epoch"
:value="Math.floor(now / 1000).toString()" :value="Math.floor(now / 1000).toString()"
readonly readonly
mb-5
placeholder="Epoch in sec will be displayed here" placeholder="Epoch in sec will be displayed here"
/> />
@ -122,6 +145,15 @@ const secretValidationRules = [
label-align="right" label-align="right"
label="Padded hex:" label="Padded hex:"
/> />
<div>
<TokenDisplay :tokens="tokens" mt-5 />
<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>
</div>
</div> </div>
</template> </template>