refactor(credential-management): improve regex validation and prompt handling (#67)
- Updated regex validation logic to allow empty inputs for username and password prompts, enhancing user experience. - Changed labels to indicate optional fields for username and password regex inputs. - Refactored credential handling to improve clarity and maintainability in the CredentialManagementTab component. - Introduced new utility functions for better prompt detection and fallback credential handling in the credential autofill logic.
This commit is contained in:
1 parent
68fb3ef19e
commit
171e7f0cb2
3 files changed
+48
-36
No files matched your search
@@ -44,10 +44,13 @@ function CredentialEditor({
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const usernamePromptRegex = entry.username_prompt_regex ?? "";
|
||||
const passwordPromptRegex = entry.password_prompt_regex ?? "";
|
||||
const usernameRegexValid = validatePromptRegex(usernamePromptRegex);
|
||||
const passwordRegexValid = validatePromptRegex(passwordPromptRegex);
|
||||
const regexError = (value: string) =>
|
||||
value.trim() ? t("credentialManager.invalidRegex") : t("credentialManager.regexRequired");
|
||||
const usernameRegexValid =
|
||||
!usernamePromptRegex.trim() || validatePromptRegex(usernamePromptRegex);
|
||||
const passwordRegexValid =
|
||||
!passwordPromptRegex.trim() || validatePromptRegex(passwordPromptRegex);
|
||||
const showUsernameRegexValid = Boolean(usernamePromptRegex.trim() && usernameRegexValid);
|
||||
const showPasswordRegexValid = Boolean(passwordPromptRegex.trim() && passwordRegexValid);
|
||||
const regexError = (value: string) => (value.trim() ? t("credentialManager.invalidRegex") : "");
|
||||
|
||||
return (
|
||||
<div className="border-b bg-accent/25 p-3">
|
||||
@@ -80,12 +83,12 @@ function CredentialEditor({
|
||||
<div className="space-y-2 border-t pt-3">
|
||||
<div className="flex items-center gap-1.5 text-xs font-medium">
|
||||
<UserRound className="h-3.5 w-3.5 text-primary" />
|
||||
{t("credentialManager.usernameLabel")}
|
||||
{t("credentialManager.usernameOptionalLabel")}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[0.6875rem] text-muted-foreground">
|
||||
{t("credentialManager.promptRegexLabel")}
|
||||
{t("credentialManager.promptRegexOptionalLabel")}
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
@@ -95,7 +98,7 @@ function CredentialEditor({
|
||||
onChange={(event) => onChange({ username_prompt_regex: event.target.value })}
|
||||
aria-invalid={!usernameRegexValid}
|
||||
/>
|
||||
{usernameRegexValid ? (
|
||||
{showUsernameRegexValid ? (
|
||||
<CheckCircle2 className="pointer-events-none absolute top-1/2 right-2 h-3.5 w-3.5 -translate-y-1/2 text-emerald-500" />
|
||||
) : null}
|
||||
</div>
|
||||
@@ -128,7 +131,7 @@ function CredentialEditor({
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[0.6875rem] text-muted-foreground">
|
||||
{t("credentialManager.promptRegexLabel")}
|
||||
{t("credentialManager.promptRegexOptionalLabel")}
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
@@ -138,7 +141,7 @@ function CredentialEditor({
|
||||
onChange={(event) => onChange({ password_prompt_regex: event.target.value })}
|
||||
aria-invalid={!passwordRegexValid}
|
||||
/>
|
||||
{passwordRegexValid ? (
|
||||
{showPasswordRegexValid ? (
|
||||
<CheckCircle2 className="pointer-events-none absolute top-1/2 right-2 h-3.5 w-3.5 -translate-y-1/2 text-emerald-500" />
|
||||
) : null}
|
||||
</div>
|
||||
@@ -322,16 +325,14 @@ export function CredentialManagementTab({
|
||||
setEditEntry((prev) => ({ ...prev, ...patch }));
|
||||
}, []);
|
||||
|
||||
const usernamePromptRegex = editEntry.username_prompt_regex ?? "";
|
||||
const passwordPromptRegex = editEntry.password_prompt_regex ?? "";
|
||||
const regexValid =
|
||||
validatePromptRegex(editEntry.username_prompt_regex ?? "") &&
|
||||
validatePromptRegex(editEntry.password_prompt_regex ?? "");
|
||||
(!usernamePromptRegex.trim() || validatePromptRegex(usernamePromptRegex)) &&
|
||||
(!passwordPromptRegex.trim() || validatePromptRegex(passwordPromptRegex));
|
||||
|
||||
const saveDisabled =
|
||||
passwordLoading ||
|
||||
!editEntry.name?.trim() ||
|
||||
!editEntry.username?.trim() ||
|
||||
(isNew && !editEntry.password) ||
|
||||
!regexValid;
|
||||
passwordLoading || !editEntry.name?.trim() || (isNew && !editEntry.password) || !regexValid;
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (saveDisabled) return;
|
||||
@@ -427,7 +428,7 @@ export function CredentialManagementTab({
|
||||
</div>
|
||||
<div className="mt-1 flex items-start gap-0.5">
|
||||
<span className="min-w-0 select-text break-all text-[0.6875rem] text-muted-foreground">
|
||||
{entry.username}
|
||||
{entry.username || t("credentialManager.passwordOnlyCredential")}
|
||||
</span>
|
||||
{entry.username ? <CopyButton value={entry.username} /> : null}
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,11 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
type CredentialPromptKind,
|
||||
compilePromptRegex,
|
||||
extractCredentialPromptText,
|
||||
findMatchingCredentials,
|
||||
findPasswordOnlyFallbackCredentials,
|
||||
getCredentialPromptPattern,
|
||||
isDefaultPasswordPrompt,
|
||||
stripTerminalControlSequences,
|
||||
} from "@/lib/credentialAutofill";
|
||||
import { invoke } from "@/lib/invoke";
|
||||
@@ -34,24 +37,10 @@ export interface CredentialPanelState {
|
||||
promptText: string;
|
||||
}
|
||||
|
||||
function extractCredentialPromptText(output: string) {
|
||||
const lastChar = output[output.length - 1];
|
||||
if (lastChar === "\r" || lastChar === "\n") return "";
|
||||
|
||||
const normalized = output.replace(/\r/g, "\n").replace(/\n+/g, "\n");
|
||||
const lines = normalized.split("\n");
|
||||
const prompt = (lines[lines.length - 1] ?? "").trim();
|
||||
return prompt.length > 500 ? prompt.slice(-500) : prompt;
|
||||
}
|
||||
|
||||
function extractCurrentPromptLine(promptText: string) {
|
||||
return promptText.trim();
|
||||
}
|
||||
|
||||
function looksLikePasswordPrompt(promptText: string) {
|
||||
return /\b(pass(word|phrase|code)?|pin)\b/i.test(promptText);
|
||||
}
|
||||
|
||||
function credentialPatternMatches(
|
||||
credential: SavedCredential,
|
||||
kind: CredentialPromptKind,
|
||||
@@ -326,12 +315,11 @@ export function useCredentialAutofill(
|
||||
return;
|
||||
}
|
||||
|
||||
if (looksLikePasswordPrompt(currentLine)) {
|
||||
if (isDefaultPasswordPrompt(currentLine)) {
|
||||
pendingCredentialRef.current = null;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordMatches = findMatchingCredentials(credentials, "password", promptText);
|
||||
@@ -342,6 +330,16 @@ export function useCredentialAutofill(
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDefaultPasswordPrompt(promptText)) {
|
||||
const fallbackMatches = findPasswordOnlyFallbackCredentials(credentials);
|
||||
if (fallbackMatches.length > 0) {
|
||||
if (!rememberPrompt("password", promptText, now)) return;
|
||||
|
||||
showPanel("password", fallbackMatches, promptText);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const usernameMatches = findMatchingCredentials(credentials, "username", promptText);
|
||||
if (usernameMatches.length === 0) return;
|
||||
if (!rememberPrompt("username", promptText, now)) return;
|
||||
@@ -365,7 +363,6 @@ export function useCredentialAutofill(
|
||||
(payload: string) => {
|
||||
if (performanceModeRef.current === "overloaded") return;
|
||||
if (!activeRef.current || !visibleRef.current) return;
|
||||
if (credentialsRef.current.length === 0) return;
|
||||
|
||||
const visible = stripTerminalControlSequences(payload);
|
||||
if (!visible) return;
|
||||
|
||||
@@ -9,6 +9,7 @@ const USERNAME_PROMPT_PATTERN =
|
||||
/\b(user\s*name|username|login|login\s+as|account|user)\b|(?:\u7528\u6237\u540d|\u7528\u6237|\u8d26\u53f7|\u8d26\u6237|\u767b\u5f55\u540d)/i;
|
||||
const PASSWORD_PROMPT_PATTERN =
|
||||
/\b(pass(word|phrase|code)?|pin|otp|verification\s*code|auth(entication)?\s*code|2fa|mfa)\b|(?:\u5bc6\u7801|\u53e3\u4ee4|\u9a8c\u8bc1\u7801|\u52a8\u6001\u7801|\u52a8\u6001\u53e3\u4ee4)/i;
|
||||
const PROMPT_TERMINATOR_PATTERN = /[:\uff1a]\s*$/u;
|
||||
|
||||
export function stripTerminalControlSequences(text: string): string {
|
||||
return text.replace(OSC_PATTERN, "").replace(ANSI_PATTERN, "");
|
||||
@@ -27,12 +28,19 @@ export function extractCredentialPromptText(output: string): string {
|
||||
|
||||
export function detectCredentialPromptKind(output: string): CredentialPromptKind | null {
|
||||
const prompt = extractCredentialPromptText(output);
|
||||
if (!prompt || !/[:\uff1a]\s*$/u.test(prompt)) return null;
|
||||
if (!prompt || !PROMPT_TERMINATOR_PATTERN.test(prompt)) return null;
|
||||
if (PASSWORD_PROMPT_PATTERN.test(prompt)) return "password";
|
||||
if (USERNAME_PROMPT_PATTERN.test(prompt)) return "username";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isDefaultPasswordPrompt(output: string): boolean {
|
||||
const prompt = extractCredentialPromptText(output);
|
||||
return Boolean(
|
||||
prompt && PROMPT_TERMINATOR_PATTERN.test(prompt) && PASSWORD_PROMPT_PATTERN.test(prompt),
|
||||
);
|
||||
}
|
||||
|
||||
export function compilePromptRegex(pattern: string): RegExp | null {
|
||||
try {
|
||||
return new RegExp(pattern, "im");
|
||||
@@ -74,6 +82,12 @@ export function findMatchingCredentials(
|
||||
return credentials.filter((credential) => credentialMatchesPrompt(credential, kind, output));
|
||||
}
|
||||
|
||||
export function findPasswordOnlyFallbackCredentials(
|
||||
credentials: SavedCredential[],
|
||||
): SavedCredential[] {
|
||||
return credentials.filter((credential) => credential.enabled);
|
||||
}
|
||||
|
||||
export function validatePromptRegex(pattern: string): boolean {
|
||||
const trimmed = pattern.trim();
|
||||
return Boolean(trimmed && compilePromptRegex(trimmed));
|
||||
|
||||
Reference in new issue
Block a user