|
1 | 1 | import ICAL from 'ical.js'; |
| 2 | +import { URI as OTPURI } from 'otpauth-migration'; |
2 | 3 |
|
3 | 4 | import { translate as t } from '@/plugins/i18n.plugin'; |
4 | 5 |
|
| 6 | +interface OTPAuthURI { |
| 7 | + type: string |
| 8 | + label: { |
| 9 | + issuer?: string |
| 10 | + account?: string |
| 11 | + raw: string |
| 12 | + } |
| 13 | + params: Record<string, string> |
| 14 | + uri: string |
| 15 | +} |
| 16 | + |
| 17 | +function parseOtpAuthUri(uri: string): OTPAuthURI | null { |
| 18 | + const url = new URL(uri); |
| 19 | + |
| 20 | + if (url.protocol !== 'otpauth:') { |
| 21 | + return null; |
| 22 | + } |
| 23 | + |
| 24 | + // url.hostname contains the OTP type (totp, hotp, etc.) |
| 25 | + const type = url.hostname; |
| 26 | + |
| 27 | + // url.pathname starts with "/", so strip it |
| 28 | + const rawLabel = decodeURIComponent(url.pathname.slice(1)); |
| 29 | + |
| 30 | + let issuer: string | undefined; |
| 31 | + let account: string | undefined; |
| 32 | + |
| 33 | + const labelParts = rawLabel.split(':'); |
| 34 | + if (labelParts.length > 1) { |
| 35 | + issuer = labelParts[0]; |
| 36 | + account = labelParts.slice(1).join(':'); |
| 37 | + } |
| 38 | + else { |
| 39 | + account = rawLabel; |
| 40 | + } |
| 41 | + |
| 42 | + // Extract query parameters |
| 43 | + const params: Record<string, string> = {}; |
| 44 | + url.searchParams.forEach((value, key) => { |
| 45 | + params[key] = value; |
| 46 | + }); |
| 47 | + |
| 48 | + // If issuer missing in label but present in params, use it |
| 49 | + if (!issuer && params.issuer) { |
| 50 | + issuer = params.issuer; |
| 51 | + } |
| 52 | + |
| 53 | + return { |
| 54 | + type, |
| 55 | + label: { |
| 56 | + issuer, |
| 57 | + account, |
| 58 | + raw: rawLabel, |
| 59 | + }, |
| 60 | + params, |
| 61 | + uri, |
| 62 | + }; |
| 63 | +} |
| 64 | + |
5 | 65 | export function parseQRData(qrContent: string | null) { |
6 | 66 | if (!qrContent) { |
7 | 67 | return { type: t('tools.qr-code-decoder.service.text.unknown'), value: '' }; |
@@ -72,6 +132,19 @@ export function parseQRData(qrContent: string | null) { |
72 | 132 | }, |
73 | 133 | }; |
74 | 134 | } |
| 135 | + if (qrContent.startsWith('otpauth:')) { |
| 136 | + return { |
| 137 | + type: t('tools.qr-code-decoder.service.text.otpauth'), |
| 138 | + value: parseOtpAuthUri(qrContent), |
| 139 | + }; |
| 140 | + } |
| 141 | + if (qrContent.startsWith('otpauth-migration:')) { |
| 142 | + const otpauthUris = OTPURI.toOTPAuthURIs(qrContent); |
| 143 | + return { |
| 144 | + type: t('tools.qr-code-decoder.service.text.otpmigration'), |
| 145 | + value: otpauthUris.map(otpauthUri => parseOtpAuthUri(otpauthUri)), |
| 146 | + }; |
| 147 | + } |
75 | 148 | if (/^(?:https?|ftp):\/\//.test(qrContent)) { |
76 | 149 | return { |
77 | 150 | type: t('tools.websocket-tester.texts.label-url'), |
|
0 commit comments