Merge 5035cb888c into 07eea0f484
				
					
				
			This commit is contained in:
		
						commit
						b0a2b12d98
					
				
							
								
								
									
										30
									
								
								.devcontainer/devcontainer.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								.devcontainer/devcontainer.json
									
									
									
									
									
										Normal 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
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -13,6 +13,7 @@ dist | ||||
| dist-ssr | ||||
| coverage | ||||
| *.local | ||||
| .pnpm-store | ||||
| 
 | ||||
| /cypress/videos/ | ||||
| /cypress/screenshots/ | ||||
|  | ||||
							
								
								
									
										8
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -98,6 +98,7 @@ declare module '@vue/runtime-core' { | ||||
|     IconMdiEye: typeof import('~icons/mdi/eye')['default'] | ||||
|     IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default'] | ||||
|     IconMdiHeart: typeof import('~icons/mdi/heart')['default'] | ||||
|     IconMdiRefresh: typeof import('~icons/mdi/refresh')['default'] | ||||
|     IconMdiSearch: typeof import('~icons/mdi/search')['default'] | ||||
|     IconMdiTranslate: typeof import('~icons/mdi/translate')['default'] | ||||
|     IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default'] | ||||
| @ -135,13 +136,20 @@ declare module '@vue/runtime-core' { | ||||
|     NConfigProvider: typeof import('naive-ui')['NConfigProvider'] | ||||
|     NDivider: typeof import('naive-ui')['NDivider'] | ||||
|     NEllipsis: typeof import('naive-ui')['NEllipsis'] | ||||
|     NForm: typeof import('naive-ui')['NForm'] | ||||
|     NFormItem: typeof import('naive-ui')['NFormItem'] | ||||
|     NH1: typeof import('naive-ui')['NH1'] | ||||
|     NH3: typeof import('naive-ui')['NH3'] | ||||
|     NIcon: typeof import('naive-ui')['NIcon'] | ||||
|     NImage: typeof import('naive-ui')['NImage'] | ||||
|     NInputNumber: typeof import('naive-ui')['NInputNumber'] | ||||
|     NLayout: typeof import('naive-ui')['NLayout'] | ||||
|     NLayoutSider: typeof import('naive-ui')['NLayoutSider'] | ||||
|     NMenu: typeof import('naive-ui')['NMenu'] | ||||
|     NProgress: typeof import('naive-ui')['NProgress'] | ||||
|     NSlider: typeof import('naive-ui')['NSlider'] | ||||
|     NSpace: typeof import('naive-ui')['NSpace'] | ||||
|     NSwitch: typeof import('naive-ui')['NSwitch'] | ||||
|     NTable: typeof import('naive-ui')['NTable'] | ||||
|     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'] | ||||
|  | ||||
| @ -444,7 +444,7 @@ tools: | ||||
|   otp-generator: | ||||
|     title: OTP-Code-Generator | ||||
|     description: >- | ||||
|       Generiere und validiere zeitbasierte OTPs (Einmalpasswörter) für | ||||
|       Generiere und validiere zeitbasierte und ereignisgesteuertes OTPs (Einmalpasswörter) für | ||||
|       Multi-Faktor-Authentifizierung. | ||||
|   url-encoder: | ||||
|     title: Kodieren/Decodieren von URL-formatierten Zeichenfolgen | ||||
|  | ||||
| @ -383,7 +383,7 @@ tools: | ||||
| 
 | ||||
|   otp-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: | ||||
|     title: Encode/decode URL-formatted strings | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
| 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 { base32toHex, buildKeyUri, generateHOTP, generateSecret, generateTOTP, getCounterFromTime } from './otp.service'; | ||||
| import TokenDisplay from './token-display.vue'; | ||||
| import { useStyleStore } from '@/stores/style.store'; | ||||
| import InputCopyable from '@/components/InputCopyable.vue'; | ||||
| @ -19,6 +19,16 @@ function refreshSecret() { | ||||
|   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( | ||||
|   () => ({ | ||||
|     previous: generateTOTP({ key: secret.value, now: now.value - 30000 }), | ||||
| @ -68,23 +78,6 @@ const secretValidationRules = [ | ||||
|         </c-tooltip> | ||||
|       </template> | ||||
|     </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 | ||||
|       label="Secret in hexadecimal" | ||||
|       :value="base32toHex(secret)" | ||||
| @ -93,11 +86,41 @@ const secretValidationRules = [ | ||||
|       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 | ||||
|       label="Epoch" | ||||
|       :value="Math.floor(now / 1000).toString()" | ||||
|       readonly | ||||
|       mb-5 | ||||
|       placeholder="Epoch in sec will be displayed here" | ||||
|     /> | ||||
| 
 | ||||
| @ -122,6 +145,15 @@ const secretValidationRules = [ | ||||
|       label-align="right" | ||||
|       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> | ||||
| </template> | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user