Update login with multiple auth methods to toggle button and logic

This commit is contained in:
Tuan Dang
2023-08-12 13:53:55 +07:00
parent 69605a1a54
commit dbd55441f2
15 changed files with 384 additions and 429 deletions
+1 -2
View File
@@ -17,6 +17,5 @@ export {
useRevokeMySessions,
useUpdateMfaEnabled,
useUpdateOrgUserRole,
useUpdateUserAuthProvider,
useUpdateUserAuthProviders,
useUpdateUserAuthMethods
} from "./queries";
+8 -29
View File
@@ -13,14 +13,14 @@ import {
AddUserToWsDTO,
AddUserToWsRes,
APIKeyData,
AuthMethod,
CreateAPIKeyRes,
DeletOrgMembershipDTO,
OrgUser,
RenameUserDTO,
TokenVersion,
UpdateOrgUserRoleDTO,
User
} from "./types";
User} from "./types";
const userKeys = {
getUser: ["user"] as const,
@@ -61,39 +61,18 @@ export const useRenameUser = () => {
});
};
export const useUpdateUserAuthProvider = () => {
export const useUpdateUserAuthMethods = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
authProvider
authMethods
}: {
authProvider: string;
authMethods: AuthMethod[];
}) => {
const { data: { user } } = await apiRequest.patch("/api/v2/users/me/auth-provider", {
authProvider
});
return user;
},
onSuccess: () => {
queryClient.invalidateQueries(userKeys.getUser);
}
});
};
export const useUpdateUserAuthProviders = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
authProviders
}: {
authProviders: string[];
}) => {
const { data: { user } } = await apiRequest.put("/api/v2/users/me/auth-providers", {
authProviders
const { data: { user } } = await apiRequest.put("/api/v2/users/me/auth-methods", {
authMethods
});
return user;
+7 -4
View File
@@ -1,9 +1,12 @@
import { UserWsKeyPair } from "../keys/types";
export enum AuthProvider {
export enum AuthMethod {
EMAIL = "email",
GOOGLE = "google",
OKTA_SAML = "okta-saml"
GITHUB = "github",
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml"
}
export type User = {
@@ -12,8 +15,8 @@ export type User = {
email: string;
firstName?: string;
lastName?: string;
authProvider?: AuthProvider;
authProviders?: AuthProvider[];
authProvider?: AuthMethod;
authMethods: AuthMethod[];
encryptionVersion?: number;
protectedKey?: string;
protectedKeyIV?: string;
@@ -1,27 +1,33 @@
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { faGithub, faGoogle, IconDefinition } from "@fortawesome/free-brands-svg-icons";
import { faEnvelope } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
Checkbox
} from "@app/components/v2";
import { Switch } from "@app/components/v2";
import { useUser } from "@app/context";
import { useUpdateUserAuthMethods } from "@app/hooks/api";
import {
useUpdateUserAuthProviders
} from "@app/hooks/api";
AuthMethod
} from "@app/hooks/api/users/types";
const authMethodList = [
{ label: "Email", value: "email" },
{ label: "Google SSO", value: "google" },
{ label: "GitHub SSO", value: "github" },
{ label: "Okta SAML", value: "okta-saml" },
{ label: "Azure SAML", value: "azure-saml" },
{ label: "JumpCloud SAML", value: "jumpcloud-saml" }
interface AuthMethodOption {
label: string,
value: AuthMethod,
icon: IconDefinition;
}
const authMethodOpts: AuthMethodOption[] = [
{ label: "Email", value: AuthMethod.EMAIL, icon: faEnvelope },
{ label: "Google", value: AuthMethod.GOOGLE, icon: faGoogle },
{ label: "GitHub", value: AuthMethod.GITHUB, icon: faGithub }
];
const samlProviders = [AuthMethod.OKTA_SAML, AuthMethod.JUMPCLOUD_SAML, AuthMethod.AZURE_SAML];
const schema = yup.object({
authMethods: yup.array().required("Auth method is required")
});
@@ -31,108 +37,101 @@ export type FormData = yup.InferType<typeof schema>;
export const AuthMethodSection = () => {
const { createNotification } = useNotificationContext();
const { user } = useUser();
const { mutateAsync, isLoading } = useUpdateUserAuthProviders();
const defaultAuthMethods = user.authProviders?.length ?
user.authProviders :
[user?.authProvider ?? "email"];
const { mutateAsync } = useUpdateUserAuthMethods();
const {
reset,
handleSubmit,
setValue,
watch,
} = useForm<FormData>({
defaultValues: {
authMethods: defaultAuthMethods,
authMethods: user.authMethods,
},
resolver: yupResolver(schema)
});
const selectedAuthMethods = watch("authMethods");
const authMethods = watch("authMethods");
useEffect(() => {
if (user) {
reset({
authMethods: defaultAuthMethods,
authMethods: user.authMethods,
});
}
}, [user]);
const onAuthMethodToggle = async (value: boolean, authMethodOpt: AuthMethodOption) => {
const hasSamlEnabled = user.authMethods
.some((authMethod: AuthMethod) => samlProviders.includes(authMethod));
const onFormSubmit = async ({
authMethods
}: FormData) => {
try {
if (
authMethods.includes("okta-saml")
|| authMethods.includes("azure-saml")
|| authMethods.includes("jumpcloud-saml")
) {
createNotification({
text: "SAML authentication can only be configured in your organization settings",
type: "error"
});
return;
}
await mutateAsync({
authProviders: authMethods
});
if (hasSamlEnabled) {
createNotification({
text: "Successfully updated authentication method",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update authentication method",
text: "SAML authentication can only be configured in your organization settings",
type: "error"
});
}
const newAuthMethods = value
? [...authMethods, authMethodOpt.value]
: authMethods.filter(auth => auth !== authMethodOpt.value);
if (value) {
const newUser = await mutateAsync({
authMethods: newAuthMethods
});
setValue("authMethods", newUser.authMethods);
createNotification({
text: "Successfully enabled authentication method",
type: "success"
});
return;
}
if (newAuthMethods.length === 0) {
createNotification({
text: "You must keep at least 1 authentication method enabled",
type: "error"
});
return;
}
const newUser = await mutateAsync({
authMethods: newAuthMethods
});
setValue("authMethods", newUser.authMethods);
createNotification({
text: "Successfully disabled authentication method",
type: "success"
});
}
return (
<form
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
onSubmit={handleSubmit(onFormSubmit)}
>
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
Authentication Method
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">
Authentication methods
</h2>
<div className="max-w-md mb-4">
{
authMethodList.map(authMethod => (
<Checkbox
className="data-[state=checked]:bg-primary"
id={`auth-method-id-${authMethod.label}`}
key={`auth-method-${authMethod.label}`}
isChecked={selectedAuthMethods.includes(authMethod.value)}
onCheckedChange={(checked) => {
if (checked) {
setValue("authMethods", [
...selectedAuthMethods,
authMethod.value
])
} else {
setValue("authMethods", selectedAuthMethods.filter(auth => auth !== authMethod.value))
}
}}>
{authMethod.label}
</Checkbox>
))
}
<p className="text-gray-400 mb-4">
By enabling a SSO provider, you are allowing an account with that provider which uses the same email address as your existing Infisical account to be able to log in to Infisical.
</p>
<div className="mb-4">
{user && authMethodOpts.map((authMethodOpt) => {
return (
<div className="flex p-4 items-center" key={`auth-method-${authMethodOpt.value}`}>
<div className="flex items-center w-20 mr-4">
<FontAwesomeIcon icon={authMethodOpt.icon} className="mr-4" />
<p>{authMethodOpt.label}</p>
</div>
<Switch
id={`enable-${authMethodOpt.value}-auth`}
onCheckedChange={(value) => onAuthMethodToggle(value, authMethodOpt)}
isChecked={authMethods?.includes(authMethodOpt.value) ?? false}
/>
</div>
);
})}
</div>
<Button
type="submit"
colorSchema="secondary"
isLoading={isLoading}
isDisabled={isLoading}
>
Save
</Button>
</form>
</div>
);
}