Autofill totp_identifier for user/email based on creds (#4200)
This commit is contained in:
@@ -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 = {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user