Autofill totp_identifier for user/email based on creds (#4200)

This commit is contained in:
Marc Kelechava
2025-12-04 13:26:51 -08:00
committed by GitHub
parent 4b9c89e22b
commit b9b3d9bb69
3 changed files with 113 additions and 68 deletions

View File

@@ -528,6 +528,7 @@ export type PasswordCredential = {
password: string; password: string;
totp: string | null; totp: string | null;
totp_type: "authenticator" | "email" | "text" | "none"; totp_type: "authenticator" | "email" | "text" | "none";
totp_identifier?: string | null;
}; };
export type CreditCardCredential = { export type CreditCardCredential = {

View File

@@ -28,6 +28,7 @@ const PASSWORD_CREDENTIAL_INITIAL_VALUES = {
password: "", password: "",
totp: "", totp: "",
totp_type: "none" as "none" | "authenticator" | "email" | "text", totp_type: "none" as "none" | "authenticator" | "email" | "text",
totp_identifier: "",
}; };
const CREDIT_CARD_CREDENTIAL_INITIAL_VALUES = { const CREDIT_CARD_CREDENTIAL_INITIAL_VALUES = {
@@ -141,6 +142,7 @@ function CredentialsModal({ onCredentialCreated }: Props) {
const username = passwordCredentialValues.username.trim(); const username = passwordCredentialValues.username.trim();
const password = passwordCredentialValues.password.trim(); const password = passwordCredentialValues.password.trim();
const totp = passwordCredentialValues.totp.trim(); const totp = passwordCredentialValues.totp.trim();
const totpIdentifier = passwordCredentialValues.totp_identifier.trim();
if (username === "" || password === "") { if (username === "" || password === "") {
toast({ toast({
@@ -158,6 +160,7 @@ function CredentialsModal({ onCredentialCreated }: Props) {
password, password,
totp: totp === "" ? null : totp, totp: totp === "" ? null : totp,
totp_type: passwordCredentialValues.totp_type, totp_type: passwordCredentialValues.totp_type,
totp_identifier: totpIdentifier === "" ? null : totpIdentifier,
}, },
}); });
} else if (type === CredentialModalTypes.CREDIT_CARD) { } else if (type === CredentialModalTypes.CREDIT_CARD) {

View File

@@ -15,7 +15,7 @@ import {
EyeOpenIcon, EyeOpenIcon,
MobileIcon, MobileIcon,
} from "@radix-ui/react-icons"; } from "@radix-ui/react-icons";
import { useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
type Props = { type Props = {
@@ -25,6 +25,7 @@ type Props = {
password: string; password: string;
totp: string; totp: string;
totp_type: "authenticator" | "email" | "text" | "none"; totp_type: "authenticator" | "email" | "text" | "none";
totp_identifier: string;
}; };
onChange: (values: { onChange: (values: {
name: string; name: string;
@@ -32,27 +33,80 @@ type Props = {
password: string; password: string;
totp: string; totp: string;
totp_type: "authenticator" | "email" | "text" | "none"; totp_type: "authenticator" | "email" | "text" | "none";
totp_identifier: string;
}) => void; }) => void;
}; };
function PasswordCredentialContent({ function PasswordCredentialContent({ values, onChange }: Props) {
values: { name, username, password, totp, totp_type }, const { name, username, password, totp, totp_type, totp_identifier } = values;
onChange,
}: Props) {
const [totpMethod, setTotpMethod] = useState< const [totpMethod, setTotpMethod] = useState<
"authenticator" | "email" | "text" "authenticator" | "email" | "text"
>("authenticator"); >(
totp_type === "email" || totp_type === "text" ? totp_type : "authenticator",
);
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const prevUsernameRef = useRef(username);
const prevTotpMethodRef = useRef<typeof totpMethod>(totpMethod);
const totpIdentifierLabel =
totpMethod === "text"
? "TOTP Identifier (Phone)"
: "TOTP Identifier (Username or Email)";
const totpIdentifierHelper =
totpMethod === "text"
? "Phone number used to receive 2FA codes."
: "Email address used to receive 2FA codes.";
const updateValues = useCallback(
(updates: Partial<Props["values"]>): void => {
onChange({
name,
username,
password,
totp,
totp_type,
totp_identifier,
...updates,
});
},
[name, onChange, password, totp, totp_identifier, totp_type, username],
);
useEffect(() => {
const prevUsername = prevUsernameRef.current;
const prevMethod = prevTotpMethodRef.current;
if (totpMethod === "email") {
const usernameChanged = username !== prevUsername;
const identifierBlank = totp_identifier.trim() === "";
const identifierMatchedPrevUsername = totp_identifier === prevUsername;
const methodChanged = prevMethod !== "email";
if (
identifierBlank ||
methodChanged ||
(usernameChanged && identifierMatchedPrevUsername)
) {
updateValues({ totp_identifier: username });
}
}
if (totpMethod === "text" && prevMethod !== "text") {
const wasAutoFilled = totp_identifier === prevUsername;
if (wasAutoFilled || totp_identifier.trim() === "") {
updateValues({ totp_identifier: "" });
}
}
prevUsernameRef.current = username;
prevTotpMethodRef.current = totpMethod;
}, [totpMethod, totp_identifier, updateValues, username]);
// Update totp_type when totpMethod changes // Update totp_type when totpMethod changes
const handleTotpMethodChange = ( const handleTotpMethodChange = (
method: "authenticator" | "email" | "text", method: "authenticator" | "email" | "text",
) => { ) => {
setTotpMethod(method); setTotpMethod(method);
onChange({ updateValues({
name,
username,
password,
totp: method === "authenticator" ? totp : "", totp: method === "authenticator" ? totp : "",
totp_type: method, totp_type: method,
}); });
@@ -69,33 +123,17 @@ function PasswordCredentialContent({
</div> </div>
<Input <Input
value={name} value={name}
onChange={(e) => onChange={(e) => updateValues({ name: e.target.value })}
onChange({
name: e.target.value,
username,
password,
totp,
totp_type,
})
}
/> />
</div> </div>
<Separator /> <Separator />
<div className="flex items-center gap-12"> <div className="flex items-center gap-12">
<div className="w-40 shrink-0"> <div className="w-40 shrink-0">
<Label>Username or email</Label> <Label>Username or Email</Label>
</div> </div>
<Input <Input
value={username} value={username}
onChange={(e) => onChange={(e) => updateValues({ username: e.target.value })}
onChange({
name,
username: e.target.value,
password,
totp,
totp_type,
})
}
/> />
</div> </div>
<div className="flex items-center gap-12"> <div className="flex items-center gap-12">
@@ -107,15 +145,7 @@ function PasswordCredentialContent({
className="pr-9" className="pr-9"
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}
value={password} value={password}
onChange={(e) => onChange={(e) => updateValues({ password: e.target.value })}
onChange({
name,
username,
totp,
password: e.target.value,
totp_type,
})
}
/> />
<div <div
className="absolute right-0 top-0 flex size-9 cursor-pointer items-center justify-center" className="absolute right-0 top-0 flex size-9 cursor-pointer items-center justify-center"
@@ -183,27 +213,46 @@ function PasswordCredentialContent({
</div> </div>
</div> </div>
{(totpMethod === "text" || totpMethod === "email") && ( {(totpMethod === "text" || totpMethod === "email") && (
<p className="text-sm text-slate-400"> <>
<Link <div className="space-y-2">
to="https://meetings.hubspot.com/skyvern/demo" <div className="flex items-center gap-12">
target="_blank" <div className="w-40 shrink-0">
rel="noopener noreferrer" <Label>{totpIdentifierLabel}</Label>
className="underline underline-offset-2" </div>
> <Input
Contact us to set up two-factor authentication in workflows value={totp_identifier}
</Link>{" "} onChange={(e) =>
or{" "} updateValues({ totp_identifier: e.target.value })
<Link }
to="https://www.skyvern.com/docs/running-tasks/advanced-features#time-based-one-time-password-totp" />
target="_blank" </div>
rel="noopener noreferrer" <p className="mt-1 text-sm text-slate-400">
className="underline underline-offset-2" {totpIdentifierHelper}
> </p>
see our documentation on how to set up two-factor </div>
authentication in workflows <p className="text-sm text-slate-400">
</Link>{" "} <Link
to get started. to="https://meetings.hubspot.com/skyvern/demo"
</p> target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-2"
>
Contact us to set up two-factor authentication in
workflows
</Link>{" "}
or{" "}
<Link
to="https://www.skyvern.com/docs/running-tasks/advanced-features#time-based-one-time-password-totp"
target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-2"
>
see our documentation on how to set up two-factor
authentication in workflows
</Link>{" "}
to get started.
</p>
</>
)} )}
{totpMethod === "authenticator" && ( {totpMethod === "authenticator" && (
<div className="space-y-4"> <div className="space-y-4">
@@ -215,15 +264,7 @@ function PasswordCredentialContent({
</div> </div>
<Input <Input
value={totp} value={totp}
onChange={(e) => onChange={(e) => updateValues({ totp: e.target.value })}
onChange({
name,
username,
password,
totp: e.target.value,
totp_type,
})
}
/> />
</div> </div>
<p className="text-sm text-slate-400"> <p className="text-sm text-slate-400">