Make merge user step automatic after email verification

This commit is contained in:
Tuan Dang
2024-04-30 21:33:27 -07:00
parent ce2a9c8640
commit 0482424a1c
13 changed files with 159 additions and 398 deletions
-3
View File
@@ -1,13 +1,11 @@
export {
useAddUserToWsE2EE,
useAddUserToWsNonE2EE,
useMergeUsers,
useSendEmailVerificationCode,
useVerifyEmailVerificationCode
} from "./mutation";
export {
fetchOrgUsers,
fetchUsersWithMyEmail,
useAddUserToOrg,
useCreateAPIKey,
useDeleteAPIKey,
@@ -21,7 +19,6 @@ export {
useGetOrgUsers,
useGetUser,
useGetUserAction,
useListUsersWithMyEmail,
useLogoutUser,
useRegisterUserAction,
useRevokeMySessions,
+9 -40
View File
@@ -5,11 +5,9 @@ import {
encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request";
import { setAuthToken } from "@app/reactQuery";
import { workspaceKeys } from "../workspace/queries";
import { userKeys } from "./queries";
import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE, User } from "./types";
import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE } from "./types";
export const useAddUserToWsE2EE = () => {
const queryClient = useQueryClient();
@@ -64,58 +62,29 @@ export const useAddUserToWsNonE2EE = () => {
});
};
export const sendEmailVerificationCode = async () => {
return apiRequest.post("/api/v2/users/me/emails/code");
export const sendEmailVerificationCode = async (username: string) => {
return apiRequest.post("/api/v2/users/me/emails/code", {
username
});
};
export const useSendEmailVerificationCode = () => {
return useMutation({
mutationFn: async () => {
await sendEmailVerificationCode();
mutationFn: async (username: string) => {
await sendEmailVerificationCode(username);
return {};
}
});
};
export const useVerifyEmailVerificationCode = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ code }: { code: string }) => {
mutationFn: async ({ username, code }: { username: string; code: string }) => {
await apiRequest.post("/api/v2/users/me/emails/verify", {
username,
code
});
return {};
},
onSuccess: () => {
queryClient.invalidateQueries(userKeys.usersWithMyEmail);
}
});
};
export const useMergeUsers = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ username }: { username: string }) => {
const { data } = await apiRequest.post<{ user: User }>("/api/v2/users/me/users/merge-user", {
username
});
return data;
},
onSuccess: () => {
setAuthToken("");
// Delete the cookie by not setting a value; Alternatively clear the local storage
localStorage.removeItem("protectedKey");
localStorage.removeItem("protectedKeyIV");
localStorage.removeItem("protectedKeyTag");
localStorage.removeItem("publicKey");
localStorage.removeItem("encryptedPrivateKey");
localStorage.removeItem("iv");
localStorage.removeItem("tag");
localStorage.removeItem("PRIVATE_KEY");
localStorage.removeItem("orgData.id");
localStorage.removeItem("projectData.id");
queryClient.clear();
}
});
};
+1 -19
View File
@@ -26,8 +26,7 @@ export const userKeys = {
myAPIKeys: ["api-keys"] as const,
myAPIKeysV2: ["api-keys-v2"] as const,
mySessions: ["sessions"] as const,
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const,
usersWithMyEmail: ["users-with-my-email"] as const
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
};
export const fetchUserDetails = async () => {
@@ -352,20 +351,3 @@ export const useGetMyOrganizationProjects = (orgId: string) => {
enabled: true
});
};
export const fetchUsersWithMyEmail = async () => {
const {
data: { users }
} = await apiRequest.get<{ users: User[] }>("/api/v2/users/me/users/same-email");
return users;
};
export const useListUsersWithMyEmail = () => {
return useQuery({
queryKey: userKeys.usersWithMyEmail,
queryFn: async () => {
return fetchUsersWithMyEmail();
},
enabled: true
});
};
+28 -18
View File
@@ -1,12 +1,7 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import jwt_decode from "jwt-decode";
import {
BackupPDFStep,
EmailConfirmationStep,
MergeUsersStep,
UserInfoSSOStep
} from "./components";
import { BackupPDFStep, EmailConfirmationStep, UserInfoSSOStep } from "./components";
type Props = {
providerAuthToken: string;
@@ -27,13 +22,30 @@ export const SignupSSO = ({ providerAuthToken }: Props) => {
isEmailVerified
} = jwt_decode(providerAuthToken) as any;
useEffect(() => {
if (!isEmailVerified) {
setStep(0);
} else {
setStep(1);
}
}, []);
const renderView = () => {
switch (step) {
case 0:
return (
<EmailConfirmationStep
authType={authType}
username={username}
email={email}
organizationSlug={organizationSlug}
setStep={setStep}
/>
);
case 1:
return (
<UserInfoSSOStep
username={username}
isEmailVerified={isEmailVerified}
name={`${firstName} ${lastName}`}
providerOrganizationName={organizationName}
password={password}
@@ -42,17 +54,15 @@ export const SignupSSO = ({ providerAuthToken }: Props) => {
providerAuthToken={providerAuthToken}
/>
);
case 1:
return <EmailConfirmationStep email={email} setStep={setStep} />;
// case 2:
// return (
// <MergeUsersStep
// username={username}
// authType={authType}
// organizationSlug={organizationSlug}
// />
// );
case 2:
return (
<MergeUsersStep
username={username}
authType={authType}
organizationSlug={organizationSlug}
/>
);
case 3:
return (
<BackupPDFStep email={username} password={password} name={`${firstName} ${lastName}`} />
);
@@ -2,18 +2,19 @@
// if same email exists, then trigger fn to merge automatically
import { useState } from "react";
import ReactCodeInput from "react-code-input";
import { useRouter } from "next/router";
import Error from "@app/components/basic/Error";
import { createNotification } from "@app/components/notifications";
import { Button } from "@app/components/v2";
import {
fetchUsersWithMyEmail,
useSendEmailVerificationCode,
useVerifyEmailVerificationCode
} from "@app/hooks/api";
import { useSendEmailVerificationCode, useVerifyEmailVerificationCode } from "@app/hooks/api";
import { UserAliasType } from "@app/hooks/api/users/types";
type Props = {
authType?: UserAliasType;
username: string;
email: string;
organizationSlug: string;
setStep: (step: number) => void;
};
@@ -55,7 +56,14 @@ const propsPhone = {
}
} as const;
export const EmailConfirmationStep = ({ email, setStep }: Props) => {
export const EmailConfirmationStep = ({
authType,
username,
email,
organizationSlug,
setStep
}: Props) => {
const router = useRouter();
const [code, setCode] = useState("");
const [codeError, setCodeError] = useState(false);
const [isResendingVerificationEmail] = useState(false);
@@ -66,19 +74,29 @@ export const EmailConfirmationStep = ({ email, setStep }: Props) => {
const checkCode = async () => {
try {
await verifyEmailVerificationCode({ code });
await verifyEmailVerificationCode({ username, code });
setCodeError(false);
const usersWithSameEmail = await fetchUsersWithMyEmail();
if (usersWithSameEmail.length > 1) {
setStep(2);
}
createNotification({
text: "Successfully verified code",
type: "success"
});
switch (authType) {
case UserAliasType.SAML: {
window.open(`/api/v1/sso/redirect/saml2/organizations/${organizationSlug}`);
window.close();
break;
}
case UserAliasType.LDAP: {
router.push(`/login/ldap?organizationSlug=${organizationSlug}`);
break;
}
default: {
setStep(1);
break;
}
}
} catch (err) {
createNotification({
text: "Failed to verify code",
@@ -91,7 +109,11 @@ export const EmailConfirmationStep = ({ email, setStep }: Props) => {
const resendCode = async () => {
try {
await sendEmailVerificationCode();
await sendEmailVerificationCode(username);
createNotification({
text: "Successfully resent code",
type: "success"
});
} catch (err) {
createNotification({
text: "Failed to resend code",
@@ -1,157 +0,0 @@
import { useState } from "react";
import { useRouter } from "next/router";
import { faUsers } from "@fortawesome/free-solid-svg-icons";
import { createNotification } from "@app/components/notifications";
import {
Button,
EmptyState,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useListUsersWithMyEmail, useMergeUsers } from "@app/hooks/api";
import { UserAliasType } from "@app/hooks/api/users/types";
type Props = {
username: string;
authType?: UserAliasType;
organizationSlug: string;
};
export const MergeUsersStep = ({ username, authType, organizationSlug }: Props) => {
const router = useRouter();
const [isOpen, setIsOpen] = useState(false);
const [targetUsername, setTargetUsername] = useState("");
const { data: users, isLoading: isLoadingUsers } = useListUsersWithMyEmail();
const { mutateAsync: mergeUser, isLoading: isLoadingMerge } = useMergeUsers();
const handleMergeUser = async (mergeWithUsername: string) => {
try {
if (!mergeWithUsername) return;
await mergeUser({ username: mergeWithUsername });
createNotification({
text: "Successfully merged user",
type: "success"
});
setIsOpen(false);
switch (authType) {
case UserAliasType.SAML: {
window.open(`/api/v1/sso/redirect/saml2/organizations/${organizationSlug}`);
window.close();
break;
}
case UserAliasType.LDAP: {
router.push(`/login/ldap?organizationSlug=${organizationSlug}`);
break;
}
default: {
router.push("/login");
break;
}
}
setTargetUsername("");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to merge user",
type: "error"
});
}
};
return (
<div className="mx-auto h-full max-w-xl">
<p className="text-md flex justify-center text-bunker-200">
We found an account with the same verified email.
</p>
<p className="text-md mb-8 flex justify-center text-bunker-200">
Select the account to merge with it.
</p>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Username</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoadingUsers && <TableSkeleton columns={3} innerKey="same-email-users" />}
{!isLoadingUsers &&
users
?.filter((user) => user.username !== username)
?.map((user) => {
return (
<Tr className="h-10 items-center" key={`same-email-user-${user.id}`}>
<Td>{`${user.firstName ?? ""} ${user.lastName ?? ""}`}</Td>
<Td>{user.username}</Td>
<Td>
<Button
colorSchema="primary"
variant="outline_bg"
type="submit"
onClick={() => {
setIsOpen(true);
setTargetUsername(user.username);
}}
>
Merge
</Button>
</Td>
</Tr>
);
})}
{!isLoadingUsers && !users?.length && (
<Tr>
<Td colSpan={3}>
<EmptyState title="No users found with the same email" icon={faUsers} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
<Modal isOpen={isOpen} onOpenChange={setIsOpen}>
<ModalContent title="Merge User Confirmation">
<p className="mb-4 text-bunker-300">
The merge operation will transfer / consolidate your existing organization membership to
the target user you&apos;re merging with.
</p>
<p className="mb-4 text-bunker-300">
If the target user is not yet part of the same organization, then they will be added to
it under your current organization membership. Conversely, if the target user is already
part of the organization, then their existing organization membership will remain.
</p>
<p className="text-bunker-300">
Once the merge operation is complete, you&apos;ll be prompted to re-login.
</p>
<div className="mt-8 flex items-center">
<Button
isLoading={isLoadingMerge}
colorSchema="primary"
onClick={async () => handleMergeUser(targetUsername)}
className="mr-4"
>
Confirm
</Button>
<Button colorSchema="secondary" variant="plain" onClick={() => setIsOpen(false)}>
Cancel
</Button>
</div>
</ModalContent>
</Modal>
</div>
);
};
@@ -1 +0,0 @@
export { MergeUsersStep } from "./MergeUsersStep";
@@ -17,7 +17,6 @@ import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Input } from "@app/components/v2";
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { sendEmailVerificationCode } from "@app/hooks/api/users/mutation";
import ProjectService from "@app/services/ProjectService";
// eslint-disable-next-line new-cap
@@ -26,7 +25,6 @@ const client = new jsrp.client();
type Props = {
setStep: (step: number) => void;
username: string;
isEmailVerified?: boolean;
password: string;
setPassword: (value: string) => void;
name: string;
@@ -60,7 +58,6 @@ type Errors = {
*/
export const UserInfoSSOStep = ({
username,
isEmailVerified,
name,
providerOrganizationName,
password,
@@ -204,14 +201,7 @@ export const UserInfoSSOStep = ({
localStorage.setItem("orgData.id", orgId);
localStorage.setItem("projectData.id", project.id);
if (isEmailVerified) {
// move to backup PDF step
setStep(3);
} else {
// move to verify email
await sendEmailVerificationCode();
setStep(1);
}
setStep(2);
} catch (error) {
setIsLoading(false);
console.error(error);
@@ -1,4 +1,3 @@
export { BackupPDFStep } from "./BackupPDFStep";
export { EmailConfirmationStep } from "./EmailConfirmationStep";
export { MergeUsersStep } from "./MergeUsersStep";
export { UserInfoSSOStep } from "./UserInfoSSOStep";