Autofill totp_identifier for user/email based on creds (#4200)
This commit is contained in:
@@ -528,6 +528,7 @@ export type PasswordCredential = {
|
||||
password: string;
|
||||
totp: string | null;
|
||||
totp_type: "authenticator" | "email" | "text" | "none";
|
||||
totp_identifier?: string | null;
|
||||
};
|
||||
|
||||
export type CreditCardCredential = {
|
||||
|
||||
@@ -28,6 +28,7 @@ const PASSWORD_CREDENTIAL_INITIAL_VALUES = {
|
||||
password: "",
|
||||
totp: "",
|
||||
totp_type: "none" as "none" | "authenticator" | "email" | "text",
|
||||
totp_identifier: "",
|
||||
};
|
||||
|
||||
const CREDIT_CARD_CREDENTIAL_INITIAL_VALUES = {
|
||||
@@ -141,6 +142,7 @@ function CredentialsModal({ onCredentialCreated }: Props) {
|
||||
const username = passwordCredentialValues.username.trim();
|
||||
const password = passwordCredentialValues.password.trim();
|
||||
const totp = passwordCredentialValues.totp.trim();
|
||||
const totpIdentifier = passwordCredentialValues.totp_identifier.trim();
|
||||
|
||||
if (username === "" || password === "") {
|
||||
toast({
|
||||
@@ -158,6 +160,7 @@ function CredentialsModal({ onCredentialCreated }: Props) {
|
||||
password,
|
||||
totp: totp === "" ? null : totp,
|
||||
totp_type: passwordCredentialValues.totp_type,
|
||||
totp_identifier: totpIdentifier === "" ? null : totpIdentifier,
|
||||
},
|
||||
});
|
||||
} else if (type === CredentialModalTypes.CREDIT_CARD) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
EyeOpenIcon,
|
||||
MobileIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
type Props = {
|
||||
@@ -25,6 +25,7 @@ type Props = {
|
||||
password: string;
|
||||
totp: string;
|
||||
totp_type: "authenticator" | "email" | "text" | "none";
|
||||
totp_identifier: string;
|
||||
};
|
||||
onChange: (values: {
|
||||
name: string;
|
||||
@@ -32,27 +33,80 @@ type Props = {
|
||||
password: string;
|
||||
totp: string;
|
||||
totp_type: "authenticator" | "email" | "text" | "none";
|
||||
totp_identifier: string;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
function PasswordCredentialContent({
|
||||
values: { name, username, password, totp, totp_type },
|
||||
onChange,
|
||||
}: Props) {
|
||||
function PasswordCredentialContent({ values, onChange }: Props) {
|
||||
const { name, username, password, totp, totp_type, totp_identifier } = values;
|
||||
const [totpMethod, setTotpMethod] = useState<
|
||||
"authenticator" | "email" | "text"
|
||||
>("authenticator");
|
||||
>(
|
||||
totp_type === "email" || totp_type === "text" ? totp_type : "authenticator",
|
||||
);
|
||||
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
|
||||
const handleTotpMethodChange = (
|
||||
method: "authenticator" | "email" | "text",
|
||||
) => {
|
||||
setTotpMethod(method);
|
||||
onChange({
|
||||
name,
|
||||
username,
|
||||
password,
|
||||
updateValues({
|
||||
totp: method === "authenticator" ? totp : "",
|
||||
totp_type: method,
|
||||
});
|
||||
@@ -69,33 +123,17 @@ function PasswordCredentialContent({
|
||||
</div>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
name: e.target.value,
|
||||
username,
|
||||
password,
|
||||
totp,
|
||||
totp_type,
|
||||
})
|
||||
}
|
||||
onChange={(e) => updateValues({ name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center gap-12">
|
||||
<div className="w-40 shrink-0">
|
||||
<Label>Username or email</Label>
|
||||
<Label>Username or Email</Label>
|
||||
</div>
|
||||
<Input
|
||||
value={username}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
name,
|
||||
username: e.target.value,
|
||||
password,
|
||||
totp,
|
||||
totp_type,
|
||||
})
|
||||
}
|
||||
onChange={(e) => updateValues({ username: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-12">
|
||||
@@ -107,15 +145,7 @@ function PasswordCredentialContent({
|
||||
className="pr-9"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
name,
|
||||
username,
|
||||
totp,
|
||||
password: e.target.value,
|
||||
totp_type,
|
||||
})
|
||||
}
|
||||
onChange={(e) => updateValues({ password: e.target.value })}
|
||||
/>
|
||||
<div
|
||||
className="absolute right-0 top-0 flex size-9 cursor-pointer items-center justify-center"
|
||||
@@ -183,27 +213,46 @@ function PasswordCredentialContent({
|
||||
</div>
|
||||
</div>
|
||||
{(totpMethod === "text" || totpMethod === "email") && (
|
||||
<p className="text-sm text-slate-400">
|
||||
<Link
|
||||
to="https://meetings.hubspot.com/skyvern/demo"
|
||||
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>
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-12">
|
||||
<div className="w-40 shrink-0">
|
||||
<Label>{totpIdentifierLabel}</Label>
|
||||
</div>
|
||||
<Input
|
||||
value={totp_identifier}
|
||||
onChange={(e) =>
|
||||
updateValues({ totp_identifier: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-slate-400">
|
||||
{totpIdentifierHelper}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-slate-400">
|
||||
<Link
|
||||
to="https://meetings.hubspot.com/skyvern/demo"
|
||||
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" && (
|
||||
<div className="space-y-4">
|
||||
@@ -215,15 +264,7 @@ function PasswordCredentialContent({
|
||||
</div>
|
||||
<Input
|
||||
value={totp}
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
name,
|
||||
username,
|
||||
password,
|
||||
totp: e.target.value,
|
||||
totp_type,
|
||||
})
|
||||
}
|
||||
onChange={(e) => updateValues({ totp: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-slate-400">
|
||||
|
||||
Reference in New Issue
Block a user