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
+44 -7
View File
@@ -81,25 +81,62 @@ export const updateMyMfaEnabled = async (req: Request, res: Response) => {
} }
/** /**
* Update the current user's name [firstName, lastName]. * Update name of the current user to [firstName, lastName].
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const updateName = async (req: Request, res: Response) => { export const updateName = async (req: Request, res: Response) => {
const { firstName, lastName }: { firstName: string; lastName: string; } = req.body; const {
req.user.firstName = firstName; firstName,
req.user.lastName = lastName || ""; lastName
}: {
firstName: string;
lastName: string;
} = req.body;
await req.user.save(); const user = await User.findByIdAndUpdate(
req.user._id.toString(),
const user = req.user; {
firstName,
lastName: lastName ?? ""
},
{
new: true
}
);
return res.status(200).send({ return res.status(200).send({
user, user,
}); });
} }
/**
* Update auth provider of the current user to [authProvider]
* @param req
* @param res
* @returns
*/
export const updateAuthProvider = async (req: Request, res: Response) => {
const {
authProvider
} = req.body;
const user = await User.findByIdAndUpdate(
req.user._id.toString(),
{
authProvider
},
{
new: true
}
);
return res.status(200).send({
user
});
}
/** /**
* Return organizations that the current user is part of. * Return organizations that the current user is part of.
* @param req * @param req
+2 -2
View File
@@ -56,7 +56,7 @@ export const login1 = async (req: Request, res: Response) => {
if (!user) throw new Error("Failed to find user"); if (!user) throw new Error("Failed to find user");
if (user.authProvider) { if (user.authProvider && user.authProvider !== AuthProvider.EMAIL) {
await validateProviderAuthToken({ await validateProviderAuthToken({
email, email,
user, user,
@@ -117,7 +117,7 @@ export const login2 = async (req: Request, res: Response) => {
if (!user) throw new Error("Failed to find user"); if (!user) throw new Error("Failed to find user");
if (user.authProvider) { if (user.authProvider && user.authProvider !== AuthProvider.EMAIL) {
await validateProviderAuthToken({ await validateProviderAuthToken({
email, email,
user, user,
+1
View File
@@ -1,6 +1,7 @@
import { Document, Schema, Types, model } from "mongoose"; import { Document, Schema, Types, model } from "mongoose";
export enum AuthProvider { export enum AuthProvider {
EMAIL = "email",
GOOGLE = "google", GOOGLE = "google",
OKTA_SAML = "okta-saml" OKTA_SAML = "okta-saml"
} }
+18 -1
View File
@@ -10,6 +10,9 @@ import {
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_JWT, AUTH_MODE_JWT,
} from "../../variables"; } from "../../variables";
import {
AuthProvider
} from "../../models";
router.get( router.get(
"/me", "/me",
@@ -34,11 +37,25 @@ router.patch(
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}), }),
body("firstName").exists(), body("firstName").exists().isString(),
body("lastName").isString(),
validateRequest, validateRequest,
usersController.updateName usersController.updateName
); );
router.patch(
"/me/auth-provider",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
body("authProvider").exists().isString().isIn([
AuthProvider.EMAIL,
AuthProvider.GOOGLE
]),
validateRequest,
usersController.updateAuthProvider
);
router.get( router.get(
"/me/organizations", "/me/organizations",
requireAuth({ requireAuth({
+1 -1
View File
@@ -227,7 +227,7 @@
}, },
"password": { "password": {
"password": "Password", "password": "Password",
"change": "Change password", "change": "Change Password",
"current": "Current password", "current": "Current password",
"current-wrong": "The current password may be wrong", "current-wrong": "The current password may be wrong",
"new": "New password", "new": "New password",
+3 -1
View File
@@ -13,4 +13,6 @@ export {
useLogoutUser, useLogoutUser,
useRegisterUserAction, useRegisterUserAction,
useRevokeMySessions, 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) => export const useGetUserAction = (action: string) =>
useQuery({ useQuery({
queryKey: userKeys.userAction, queryKey: userKeys.userAction,
+7
View File
@@ -1,11 +1,18 @@
import { UserWsKeyPair } from "../keys/types"; import { UserWsKeyPair } from "../keys/types";
export enum AuthProvider {
EMAIL = "email",
GOOGLE = "google",
OKTA_SAML = "okta-saml"
}
export type User = { export type User = {
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
email?: string; email?: string;
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
authProvider?: AuthProvider;
encryptionVersion?: number; encryptionVersion?: number;
protectedKey?: string; protectedKey?: string;
protectedKeyIV?: string; protectedKeyIV?: string;
@@ -2,8 +2,8 @@ import { FormEvent, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// import { faGoogle } from "@fortawesome/free-brands-svg-icons"; import { faGoogle } from "@fortawesome/free-brands-svg-icons";
// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import axios from "axios" import axios from "axios"
import Error from "@app/components/basic/Error"; 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> <span className='px-4 text-sm text-bunker-400'>or</span>
<div className='w-1/2 border-t border-mineshaft-500' /> <div className='w-1/2 border-t border-mineshaft-500' />
</div> </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 <Button
colorSchema="primary" colorSchema="primary"
variant="solid" variant="solid"
@@ -172,7 +172,7 @@ export const InitialStep = ({
> >
{t("login.continue-with-google")} {t("login.continue-with-google")}
</Button> </Button>
</div> */} </div>
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'> <div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
<Button <Button
colorSchema="primary" colorSchema="primary"
@@ -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="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8"> <div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white"> <h2 className="text-xl font-semibold flex-1 text-white">
Configuration SAML SSO Configuration
</h2> </h2>
{!isLoading && ( {!isLoading && (
<Button <Button
@@ -24,7 +24,7 @@ export const OrgTabGroup = () => {
if (isRoleSufficient) { if (isRoleSufficient) {
tabs.push( 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 { ChangeLanguageSection } from "../ChangeLanguageSection";
import { ChangePasswordSection } from "../ChangePasswordSection";
import { EmergencyKitSection } from "../EmergencyKitSection"; import { EmergencyKitSection } from "../EmergencyKitSection";
import { SecuritySection } from "../SecuritySection";
import { SessionsSection } from "../SessionsSection"; import { SessionsSection } from "../SessionsSection";
import { UserNameSection } from "../UserNameSection"; import { UserNameSection } from "../UserNameSection";
export const PersonalSecurityTab = () => { export const PersonalGeneralTab = () => {
return ( return (
<div> <div>
<UserNameSection /> <UserNameSection />
<ChangeLanguageSection /> <ChangeLanguageSection />
<SecuritySection />
<SessionsSection /> <SessionsSection />
<ChangePasswordSection />
<EmergencyKitSection /> <EmergencyKitSection />
</div> </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 { Tab } from "@headlessui/react"
import { PersonalAPIKeyTab } from "../PersonalAPIKeyTab"; import { PersonalAPIKeyTab } from "../PersonalAPIKeyTab";
import { PersonalSecurityTab } from "../PersonalSecurityTab"; import { PersonalAuthTab } from "../PersonalAuthTab";
import { PersonalGeneralTab } from "../PersonalGeneralTab";
const tabs = [ 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" } { name: "API Keys", key: "tab-account-api-keys" }
]; ];
@@ -28,7 +30,10 @@ export const PersonalTabGroup = () => {
</Tab.List> </Tab.List>
<Tab.Panels> <Tab.Panels>
<Tab.Panel> <Tab.Panel>
<PersonalSecurityTab /> <PersonalGeneralTab />
</Tab.Panel>
<Tab.Panel>
<PersonalAuthTab />
</Tab.Panel> </Tab.Panel>
<Tab.Panel> <Tab.Panel>
<PersonalAPIKeyTab /> <PersonalAPIKeyTab />
@@ -9,7 +9,7 @@ import { useGetUser } from "../../../../hooks/api";
import { User } from "../../../../hooks/api/types"; import { User } from "../../../../hooks/api/types";
import updateMyMfaEnabled from "../../../../pages/api/user/updateMyMfaEnabled"; import updateMyMfaEnabled from "../../../../pages/api/user/updateMyMfaEnabled";
export const SecuritySection = () => { export const MFASection = () => {
const [isMfaEnabled, setIsMfaEnabled] = useState(false); const [isMfaEnabled, setIsMfaEnabled] = useState(false);
const { data: user } = useGetUser(); const { data: user } = useGetUser();
const { createNotification } = useNotificationContext(); 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" 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"> <p className="text-xl font-semibold text-mineshaft-100 mb-4">
User name Name
</p> </p>
<div className="mb-2 max-w-md"> <div className="mb-2 max-w-md">
<Controller <Controller