Add user support for changing auth methods

This commit is contained in:
Tuan Dang
2023-07-22 12:33:57 +07:00
parent be677fd6c2
commit ab43e32982
22 changed files with 264 additions and 31 deletions
+1 -1
View File
@@ -227,7 +227,7 @@
},
"password": {
"password": "Password",
"change": "Change password",
"change": "Change Password",
"current": "Current password",
"current-wrong": "The current password may be wrong",
"new": "New password",
+3 -1
View File
@@ -13,4 +13,6 @@ export {
useLogoutUser,
useRegisterUserAction,
useRevokeMySessions,
useUpdateOrgUserRole} from "./queries";
useUpdateOrgUserRole,
useUpdateUserAuthProvider
} from "./queries";
+21
View File
@@ -58,6 +58,27 @@ export const useRenameUser = () => {
});
};
export const useUpdateUserAuthProvider = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
authProvider
}: {
authProvider: string;
}) => {
const { data: { user } } = await apiRequest.patch("/api/v2/users/me/auth-provider", {
authProvider
});
return user;
},
onSuccess: () => {
queryClient.invalidateQueries(userKeys.getUser);
}
});
};
export const useGetUserAction = (action: string) =>
useQuery({
queryKey: userKeys.userAction,
+7
View File
@@ -1,11 +1,18 @@
import { UserWsKeyPair } from "../keys/types";
export enum AuthProvider {
EMAIL = "email",
GOOGLE = "google",
OKTA_SAML = "okta-saml"
}
export type User = {
createdAt: Date;
updatedAt: Date;
email?: string;
firstName?: string;
lastName?: string;
authProvider?: AuthProvider;
encryptionVersion?: number;
protectedKey?: string;
protectedKeyIV?: string;
@@ -2,8 +2,8 @@ import { FormEvent, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
// import { faGoogle } from "@fortawesome/free-brands-svg-icons";
// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGoogle } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import axios from "axios"
import Error from "@app/components/basic/Error";
@@ -159,7 +159,7 @@ export const InitialStep = ({
<span className='px-4 text-sm text-bunker-400'>or</span>
<div className='w-1/2 border-t border-mineshaft-500' />
</div>
{/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
<Button
colorSchema="primary"
variant="solid"
@@ -172,7 +172,7 @@ export const InitialStep = ({
>
{t("login.continue-with-google")}
</Button>
</div> */}
</div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
<Button
colorSchema="primary"
@@ -26,7 +26,7 @@ export const OrgSSOSection = (): JSX.Element => {
"upgradePlan",
"addSSO"
] as const);
const handleSamlSSOToggle = async (value: boolean) => {
try {
if (!currentOrg?._id) return;
@@ -53,7 +53,7 @@ export const OrgSSOSection = (): JSX.Element => {
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white">
Configuration
SAML SSO Configuration
</h2>
{!isLoading && (
<Button
@@ -24,7 +24,7 @@ export const OrgTabGroup = () => {
if (isRoleSufficient) {
tabs.push(
{ name: "SAML SSO", key: "tab-org-saml" }
{ name: "Authentication", key: "tab-org-auth" }
);
}
@@ -0,0 +1,132 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Select,
SelectItem} from "@app/components/v2";
import { useUser } from "@app/context";
import {
useUpdateUserAuthProvider
} from "@app/hooks/api";
const authMethods = [
{ label: "Email", value: "email" },
{ label: "Google SSO", value: "google" },
{ label: "Okta SAML 2.0", value: "okta-saml" },
];
const schema = yup.object({
authMethod: yup.string().required("Auth method is required")
});
export type FormData = yup.InferType<typeof schema>;
export const AuthMethodSection = () => {
const { createNotification } = useNotificationContext();
const { user } = useUser();
const { mutateAsync, isLoading } = useUpdateUserAuthProvider();
const {
reset,
control,
handleSubmit
} = useForm<FormData>({
defaultValues: {
authMethod: user?.authProvider ?? "email"
},
resolver: yupResolver(schema)
});
useEffect(() => {
if (user) {
reset({
authMethod: user?.authProvider ?? "email"
});
}
}, [user]);
const onFormSubmit = async ({
authMethod
}: FormData) => {
try {
if (authMethod === "okta-saml") {
createNotification({
text: "Okta SAML 2.0 can only be configured in your organization settings",
type: "error"
});
return;
}
await mutateAsync({
authProvider: authMethod
});
createNotification({
text: "Successfully updated authentication method",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update authentication method",
type: "error"
});
}
}
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
</h2>
<div className="max-w-md mb-4">
<Controller
control={control}
name="authMethod"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
className="mb-0"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{authMethods.map((authMethod) => {
return (
<SelectItem
value={authMethod.value}
key={`auth-method-${authMethod.value}`}
>
{authMethod.label}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
</div>
<Button
type="submit"
colorSchema="secondary"
isLoading={isLoading}
isDisabled={isLoading}
>
Save
</Button>
</form>
);
}
@@ -0,0 +1 @@
export { AuthMethodSection } from "./AuthMethodSection";
@@ -0,0 +1,13 @@
import { AuthMethodSection } from "../AuthMethodSection";
import { ChangePasswordSection } from "../ChangePasswordSection";
import { MFASection } from "../SecuritySection";
export const PersonalAuthTab = () => {
return (
<div>
<MFASection />
<AuthMethodSection />
<ChangePasswordSection />
</div>
);
}
@@ -0,0 +1 @@
export { PersonalAuthTab } from "./PersonalAuthTab";
@@ -1,18 +1,14 @@
import { ChangeLanguageSection } from "../ChangeLanguageSection";
import { ChangePasswordSection } from "../ChangePasswordSection";
import { EmergencyKitSection } from "../EmergencyKitSection";
import { SecuritySection } from "../SecuritySection";
import { SessionsSection } from "../SessionsSection";
import { UserNameSection } from "../UserNameSection";
export const PersonalSecurityTab = () => {
export const PersonalGeneralTab = () => {
return (
<div>
<UserNameSection />
<ChangeLanguageSection />
<SecuritySection />
<SessionsSection />
<ChangePasswordSection />
<EmergencyKitSection />
</div>
);
@@ -0,0 +1 @@
export { PersonalGeneralTab } from "./PersonalGeneralTab";
@@ -1 +0,0 @@
export { PersonalSecurityTab } from "./PersonalSecurityTab";
@@ -2,10 +2,12 @@ import { Fragment } from "react"
import { Tab } from "@headlessui/react"
import { PersonalAPIKeyTab } from "../PersonalAPIKeyTab";
import { PersonalSecurityTab } from "../PersonalSecurityTab";
import { PersonalAuthTab } from "../PersonalAuthTab";
import { PersonalGeneralTab } from "../PersonalGeneralTab";
const tabs = [
{ name: "General", key: "tab-account-security" },
{ name: "General", key: "tab-account-general" },
{ name: "Authentication", key: "tab-account-auth" },
{ name: "API Keys", key: "tab-account-api-keys" }
];
@@ -28,7 +30,10 @@ export const PersonalTabGroup = () => {
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<PersonalSecurityTab />
<PersonalGeneralTab />
</Tab.Panel>
<Tab.Panel>
<PersonalAuthTab />
</Tab.Panel>
<Tab.Panel>
<PersonalAPIKeyTab />
@@ -9,7 +9,7 @@ import { useGetUser } from "../../../../hooks/api";
import { User } from "../../../../hooks/api/types";
import updateMyMfaEnabled from "../../../../pages/api/user/updateMyMfaEnabled";
export const SecuritySection = () => {
export const MFASection = () => {
const [isMfaEnabled, setIsMfaEnabled] = useState(false);
const { data: user } = useGetUser();
const { createNotification } = useNotificationContext();
@@ -1 +1 @@
export { SecuritySection } from "./SecuritySection";
export { MFASection } from "./MFASection";
@@ -55,7 +55,7 @@ export const UserNameSection = (): JSX.Element => {
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
>
<p className="text-xl font-semibold text-mineshaft-100 mb-4">
User name
Name
</p>
<div className="mb-2 max-w-md">
<Controller