mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
Compare commits
2 Commits
7b3be2c819
...
d5c2ab2b0f
| Author | SHA1 | Date | |
|---|---|---|---|
| d5c2ab2b0f | |||
| 9e0908f43c |
@@ -6,7 +6,7 @@ import { t } from '@/lib/i18n';
|
|||||||
import type { Cipher } from '@/lib/types';
|
import type { Cipher } from '@/lib/types';
|
||||||
import LoadingState from '@/components/LoadingState';
|
import LoadingState from '@/components/LoadingState';
|
||||||
import WebsiteIcon from '@/components/vault/WebsiteIcon';
|
import WebsiteIcon from '@/components/vault/WebsiteIcon';
|
||||||
import { isCipherVisibleInNormalVault } from '@/components/vault/vault-page-helpers';
|
import { formatTotp, isCipherVisibleInNormalVault } from '@/components/vault/vault-page-helpers';
|
||||||
|
|
||||||
interface TotpCodesPageProps {
|
interface TotpCodesPageProps {
|
||||||
ciphers: Cipher[];
|
ciphers: Cipher[];
|
||||||
@@ -26,13 +26,6 @@ function getTotpTimeState(): { windowId: number; remain: number } {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTotp(code: string): string {
|
|
||||||
if (!code) return code;
|
|
||||||
if (code.length === 5) return `${code.slice(0, 2)} ${code.slice(2)}`;
|
|
||||||
if (code.length < 6) return code;
|
|
||||||
return `${code.slice(0, 3)} ${code.slice(3, 6)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TotpListIcon({ cipher }: { cipher: Cipher }) {
|
function TotpListIcon({ cipher }: { cipher: Cipher }) {
|
||||||
return <WebsiteIcon cipher={cipher} fallback={<Globe size={18} />} />;
|
return <WebsiteIcon cipher={cipher} fallback={<Globe size={18} />} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -507,8 +507,9 @@ export function maskSecret(value: string): string {
|
|||||||
export function formatTotp(code: string): string {
|
export function formatTotp(code: string): string {
|
||||||
if (!code) return code;
|
if (!code) return code;
|
||||||
if (code.length === 5) return `${code.slice(0, 2)} ${code.slice(2)}`;
|
if (code.length === 5) return `${code.slice(0, 2)} ${code.slice(2)}`;
|
||||||
if (code.length < 6) return code;
|
if (code.length <= 4) return code;
|
||||||
return `${code.slice(0, 3)} ${code.slice(3, 6)}`;
|
if (code.length === 8) return `${code.slice(0, 4)} ${code.slice(4)}`;
|
||||||
|
return code.replace(/(.{3})(?=.)/g, '$1 ');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatHistoryTime(value: string | null | undefined): string {
|
export function formatHistoryTime(value: string | null | undefined): string {
|
||||||
|
|||||||
@@ -220,6 +220,25 @@ function normalizeTotpSecret(secret: string): string {
|
|||||||
return secret.toUpperCase().replace(/[\s-]/g, '').replace(/=+$/g, '');
|
return secret.toUpperCase().replace(/[\s-]/g, '').replace(/=+$/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readOtpAuthParam(raw: string, name: string): string {
|
||||||
|
const queryStart = raw.indexOf('?');
|
||||||
|
if (queryStart < 0) return '';
|
||||||
|
const fragmentStart = raw.indexOf('#', queryStart + 1);
|
||||||
|
const query = raw.slice(queryStart + 1, fragmentStart > queryStart ? fragmentStart : undefined);
|
||||||
|
for (const part of query.split('&')) {
|
||||||
|
const eq = part.indexOf('=');
|
||||||
|
const key = eq >= 0 ? part.slice(0, eq) : part;
|
||||||
|
if (key.trim().toLowerCase() !== name.toLowerCase()) continue;
|
||||||
|
const value = eq >= 0 ? part.slice(eq + 1) : '';
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(value.replace(/\+/g, ' '));
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function parseSteamSecret(raw: string): string {
|
function parseSteamSecret(raw: string): string {
|
||||||
const match = raw.trim().match(/^steam:\/\/([^/?#]+)(?:[/?#].*)?$/i);
|
const match = raw.trim().match(/^steam:\/\/([^/?#]+)(?:[/?#].*)?$/i);
|
||||||
if (!match?.[1]) return '';
|
if (!match?.[1]) return '';
|
||||||
@@ -276,7 +295,8 @@ function parseTotpConfig(raw: string): TotpConfig {
|
|||||||
if (/^otpauth:\/\//i.test(s)) {
|
if (/^otpauth:\/\//i.test(s)) {
|
||||||
try {
|
try {
|
||||||
const u = new URL(s);
|
const u = new URL(s);
|
||||||
if (u.hostname.toLowerCase() !== 'totp') {
|
const otpType = u.hostname.toLowerCase();
|
||||||
|
if (otpType !== 'totp') {
|
||||||
return { secret: '', steam: false, ...DEFAULT_TOTP_CONFIG };
|
return { secret: '', steam: false, ...DEFAULT_TOTP_CONFIG };
|
||||||
}
|
}
|
||||||
const label = decodeURIComponent((u.pathname || '').replace(/^\/+/, '')).toLowerCase();
|
const label = decodeURIComponent((u.pathname || '').replace(/^\/+/, '')).toLowerCase();
|
||||||
@@ -291,7 +311,16 @@ function parseTotpConfig(raw: string): TotpConfig {
|
|||||||
period: parseTotpPositiveInt(u.searchParams.get('period'), DEFAULT_TOTP_CONFIG.period, 1, 3600),
|
period: parseTotpPositiveInt(u.searchParams.get('period'), DEFAULT_TOTP_CONFIG.period, 1, 3600),
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return { secret: '', steam: false, ...DEFAULT_TOTP_CONFIG };
|
const issuer = readOtpAuthParam(s, 'issuer').trim().toLowerCase();
|
||||||
|
const algorithm = readOtpAuthParam(s, 'algorithm').trim().toLowerCase();
|
||||||
|
const steam = issuer === 'steam' || algorithm === 'steam';
|
||||||
|
return {
|
||||||
|
secret: normalizeTotpSecret(readOtpAuthParam(s, 'secret')),
|
||||||
|
steam,
|
||||||
|
algorithm: steam ? 'SHA-1' : parseTotpHashAlgorithm(algorithm),
|
||||||
|
digits: steam ? 5 : parseTotpPositiveInt(readOtpAuthParam(s, 'digits'), DEFAULT_TOTP_CONFIG.digits, 1, 10),
|
||||||
|
period: parseTotpPositiveInt(readOtpAuthParam(s, 'period'), DEFAULT_TOTP_CONFIG.period, 1, 3600),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { secret: normalizeTotpSecret(s), steam: false, ...DEFAULT_TOTP_CONFIG };
|
return { secret: normalizeTotpSecret(s), steam: false, ...DEFAULT_TOTP_CONFIG };
|
||||||
|
|||||||
@@ -892,6 +892,10 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.totp-code-main strong {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-module .field,
|
.settings-module .field,
|
||||||
.auth-card .field {
|
.auth-card .field {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|||||||
@@ -952,7 +952,7 @@ select.input.duplicate-mode-toolbar-select {
|
|||||||
|
|
||||||
.totp-codes-list {
|
.totp-codes-list {
|
||||||
@apply grid w-full items-start gap-2.5;
|
@apply grid w-full items-start gap-2.5;
|
||||||
grid-template-columns: repeat(var(--totp-columns, 1), minmax(300px, 1fr));
|
grid-template-columns: repeat(var(--totp-columns, 1), minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.totp-code-row {
|
.totp-code-row {
|
||||||
@@ -977,7 +977,7 @@ select.input.duplicate-mode-toolbar-select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.totp-code-main strong {
|
.totp-code-main strong {
|
||||||
@apply whitespace-nowrap text-[22px] leading-none;
|
@apply min-w-0 whitespace-nowrap text-[22px] leading-none;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user