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
@@ -21,9 +21,12 @@ import {
} from "@app/lib/crypto/encryption"; } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { AuthTokenType } from "@app/services/auth/auth-type"; import { AuthTokenType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TokenType } from "@app/services/auth-token/auth-token-types";
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal"; import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
import { TOrgDALFactory } from "@app/services/org/org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal";
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
import { normalizeUsername } from "@app/services/user/user-fns"; import { normalizeUsername } from "@app/services/user/user-fns";
@@ -48,6 +51,8 @@ type TSamlConfigServiceFactoryDep = {
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">; orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
tokenService: Pick<TAuthTokenServiceFactory, "createTokenForUser">;
smtpService: Pick<TSmtpService, "sendMail">;
}; };
export type TSamlConfigServiceFactory = ReturnType<typeof samlConfigServiceFactory>; export type TSamlConfigServiceFactory = ReturnType<typeof samlConfigServiceFactory>;
@@ -60,7 +65,9 @@ export const samlConfigServiceFactory = ({
userDAL, userDAL,
userAliasDAL, userAliasDAL,
permissionService, permissionService,
licenseService licenseService,
tokenService,
smtpService
}: TSamlConfigServiceFactoryDep) => { }: TSamlConfigServiceFactoryDep) => {
const createSamlCfg = async ({ const createSamlCfg = async ({
cert, cert,
@@ -439,6 +446,22 @@ export const samlConfigServiceFactory = ({
await samlConfigDAL.update({ orgId }, { lastUsed: new Date() }); await samlConfigDAL.update({ orgId }, { lastUsed: new Date() });
if (user.email && !user.isEmailVerified) {
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_VERIFICATION,
userId: user.id
});
await smtpService.sendMail({
template: SmtpTemplates.EmailVerification,
subjectLine: "Infisical confirmation code",
recipients: [user.email],
substitutions: {
code: token
}
});
}
return { isUserCompleted, providerAuthToken }; return { isUserCompleted, providerAuthToken };
}; };
+4 -2
View File
@@ -255,6 +255,7 @@ export const registerRoutes = async (
permissionService, permissionService,
secretApprovalPolicyDAL secretApprovalPolicyDAL
}); });
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
const samlService = samlConfigServiceFactory({ const samlService = samlConfigServiceFactory({
permissionService, permissionService,
orgBotDAL, orgBotDAL,
@@ -263,7 +264,9 @@ export const registerRoutes = async (
userDAL, userDAL,
userAliasDAL, userAliasDAL,
samlConfigDAL, samlConfigDAL,
licenseService licenseService,
tokenService,
smtpService
}); });
const groupService = groupServiceFactory({ const groupService = groupServiceFactory({
userDAL, userDAL,
@@ -333,7 +336,6 @@ export const registerRoutes = async (
queueService queueService
}); });
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
const userService = userServiceFactory({ const userService = userServiceFactory({
userDAL, userDAL,
userAliasDAL, userAliasDAL,
+6 -59
View File
@@ -2,7 +2,6 @@ import { z } from "zod";
import { AuthTokenSessionsSchema, OrganizationsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; import { AuthTokenSessionsSchema, OrganizationsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
import { ApiKeysSchema } from "@app/db/schemas/api-keys"; import { ApiKeysSchema } from "@app/db/schemas/api-keys";
import { getConfig } from "@app/lib/config/env";
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMethod, AuthMode } from "@app/services/auth/auth-type"; import { AuthMethod, AuthMode } from "@app/services/auth/auth-type";
@@ -15,13 +14,15 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
rateLimit: authRateLimit rateLimit: authRateLimit
}, },
schema: { schema: {
body: z.object({
username: z.string().trim()
}),
response: { response: {
200: z.object({}) 200: z.object({})
} }
}, },
preHandler: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
await server.services.user.sendEmailVerificationCode(req.permission.id); await server.services.user.sendEmailVerificationCode(req.body.username);
return {}; return {};
} }
}); });
@@ -34,73 +35,19 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
body: z.object({ body: z.object({
username: z.string().trim(),
code: z.string().trim() code: z.string().trim()
}), }),
response: { response: {
200: z.object({}) 200: z.object({})
} }
}, },
preHandler: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
await server.services.user.verifyEmailVerificationCode(req.permission.id, req.body.code); await server.services.user.verifyEmailVerificationCode(req.body.username, req.body.code);
return {}; return {};
} }
}); });
server.route({
method: "GET",
url: "/me/users/same-email",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
users: UsersSchema.array()
})
}
},
preHandler: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const users = await server.services.user.listUsersWithSameEmail(req.permission.id);
return {
users
};
}
});
server.route({
method: "POST",
url: "/me/users/merge-user",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
username: z.string().trim()
}),
response: {
200: z.object({
user: UsersSchema
})
}
},
preHandler: verifyAuth([AuthMode.JWT]),
handler: async (req, res) => {
const appCfg = getConfig();
const user = await server.services.user.mergeUsers(req.permission.id, req.body.username);
void res.cookie("jid", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
return {
user
};
}
});
server.route({ server.route({
method: "PATCH", method: "PATCH",
url: "/me/mfa", url: "/me/mfa",
+50 -72
View File
@@ -16,6 +16,7 @@ type TUserServiceFactoryDep = {
| "findById" | "findById"
| "transaction" | "transaction"
| "updateById" | "updateById"
| "update"
| "deleteById" | "deleteById"
| "findOneUserAction" | "findOneUserAction"
| "createUserAction" | "createUserAction"
@@ -36,8 +37,8 @@ export const userServiceFactory = ({
tokenService, tokenService,
smtpService smtpService
}: TUserServiceFactoryDep) => { }: TUserServiceFactoryDep) => {
const sendEmailVerificationCode = async (userId: string) => { const sendEmailVerificationCode = async (username: string) => {
const user = await userDAL.findById(userId); const user = await userDAL.findOne({ username });
if (!user) throw new BadRequestError({ name: "Failed to find user" }); if (!user) throw new BadRequestError({ name: "Failed to find user" });
if (!user.email) if (!user.email)
throw new BadRequestError({ name: "Failed to send email verification code due to no email on user" }); throw new BadRequestError({ name: "Failed to send email verification code due to no email on user" });
@@ -59,8 +60,8 @@ export const userServiceFactory = ({
}); });
}; };
const verifyEmailVerificationCode = async (userId: string, code: string) => { const verifyEmailVerificationCode = async (username: string, code: string) => {
const user = await userDAL.findById(userId); const user = await userDAL.findOne({ username });
if (!user) throw new BadRequestError({ name: "Failed to find user" }); if (!user) throw new BadRequestError({ name: "Failed to find user" });
if (user.isEmailVerified) if (user.isEmailVerified)
throw new BadRequestError({ name: "Failed to verify email verification code due to email already verified" }); throw new BadRequestError({ name: "Failed to verify email verification code due to email already verified" });
@@ -71,86 +72,65 @@ export const userServiceFactory = ({
code code
}); });
await userDAL.updateById(userId, { isEmailVerified: true }); await userDAL.transaction(async (tx) => {
}; await userDAL.updateById(
user.id,
// lists users with same verified email only
const listUsersWithSameEmail = async (userId: string) => {
const user = await userDAL.findById(userId);
if (!user) throw new BadRequestError({ name: "Failed to find user" });
if (!user.email)
throw new BadRequestError({ name: "Failed to list users with same email due to no email on user" });
if (!user.isEmailVerified)
throw new BadRequestError({ name: "Failed to list users with same email due to email not verified" });
const users = await userDAL.find({
email: user.email,
isEmailVerified: true
});
return users;
};
/**
* Merges two users with the same email. Specifically:
* - Deletes the current user with id [userId] and transfers any resources to the user with username [username]
* @param userId
* @param username
*/
const mergeUsers = async (userId: string, username: string) => {
const targetUser = await userDAL.transaction(async (tx) => {
const myUser = await userDAL.findById(userId, tx);
if (!myUser || !myUser.isEmailVerified) throw new BadRequestError({});
const mergeUser = await userDAL.findOne(
{ {
username isEmailVerified: true
}, },
tx tx
); );
if (!mergeUser || !mergeUser.isEmailVerified) throw new BadRequestError({});
if (myUser.email !== mergeUser.email) throw new BadRequestError({}); // check if there are users with the same email.
const users = await userDAL.find(
const mergeUserOrgMembershipSet = new Set(
(await orgMembershipDAL.find({ userId: mergeUser.id }, { tx })).map((m) => m.orgId)
);
const myOrgMemberships = (await orgMembershipDAL.find({ userId: myUser.id }, { tx })).filter(
(m) => !mergeUserOrgMembershipSet.has(m.orgId)
);
const userAliases = await userAliasDAL.find(
{ {
userId: myUser.id email: user.email,
isEmailVerified: true
}, },
{ tx } { tx }
); );
await userDAL.deleteById(myUser.id, tx);
if (myOrgMemberships.length) { if (users.length > 1) {
await orgMembershipDAL.insertMany( // merge users
myOrgMemberships.map((orgMembership) => ({ const mergeUser = users.find((u) => u.id !== user.id);
...orgMembership, if (!mergeUser) throw new BadRequestError({ name: "Failed to find merge user" });
userId: mergeUser.id
})), const mergeUserOrgMembershipSet = new Set(
tx (await orgMembershipDAL.find({ userId: mergeUser.id }, { tx })).map((m) => m.orgId)
); );
} const myOrgMemberships = (await orgMembershipDAL.find({ userId: user.id }, { tx })).filter(
(m) => !mergeUserOrgMembershipSet.has(m.orgId)
if (userAliases.length) {
await userAliasDAL.insertMany(
userAliases.map((userAlias) => ({
...userAlias,
userId: mergeUser.id
})),
tx
); );
}
return mergeUser; const userAliases = await userAliasDAL.find(
{
userId: user.id
},
{ tx }
);
await userDAL.deleteById(user.id, tx);
if (myOrgMemberships.length) {
await orgMembershipDAL.insertMany(
myOrgMemberships.map((orgMembership) => ({
...orgMembership,
userId: mergeUser.id
})),
tx
);
}
if (userAliases.length) {
await userAliasDAL.insertMany(
userAliases.map((userAlias) => ({
...userAlias,
userId: mergeUser.id
})),
tx
);
}
}
}); });
return targetUser;
}; };
const toggleUserMfa = async (userId: string, isMfaEnabled: boolean) => { const toggleUserMfa = async (userId: string, isMfaEnabled: boolean) => {
@@ -217,8 +197,6 @@ export const userServiceFactory = ({
return { return {
sendEmailVerificationCode, sendEmailVerificationCode,
verifyEmailVerificationCode, verifyEmailVerificationCode,
listUsersWithSameEmail,
mergeUsers,
toggleUserMfa, toggleUserMfa,
updateUserName, updateUserName,
updateAuthMethods, updateAuthMethods,
-3
View File
@@ -1,13 +1,11 @@
export { export {
useAddUserToWsE2EE, useAddUserToWsE2EE,
useAddUserToWsNonE2EE, useAddUserToWsNonE2EE,
useMergeUsers,
useSendEmailVerificationCode, useSendEmailVerificationCode,
useVerifyEmailVerificationCode useVerifyEmailVerificationCode
} from "./mutation"; } from "./mutation";
export { export {
fetchOrgUsers, fetchOrgUsers,
fetchUsersWithMyEmail,
useAddUserToOrg, useAddUserToOrg,
useCreateAPIKey, useCreateAPIKey,
useDeleteAPIKey, useDeleteAPIKey,
@@ -21,7 +19,6 @@ export {
useGetOrgUsers, useGetOrgUsers,
useGetUser, useGetUser,
useGetUserAction, useGetUserAction,
useListUsersWithMyEmail,
useLogoutUser, useLogoutUser,
useRegisterUserAction, useRegisterUserAction,
useRevokeMySessions, useRevokeMySessions,
+9 -40
View File
@@ -5,11 +5,9 @@ import {
encryptAssymmetric encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto"; } from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { setAuthToken } from "@app/reactQuery";
import { workspaceKeys } from "../workspace/queries"; import { workspaceKeys } from "../workspace/queries";
import { userKeys } from "./queries"; import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE } from "./types";
import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE, User } from "./types";
export const useAddUserToWsE2EE = () => { export const useAddUserToWsE2EE = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -64,58 +62,29 @@ export const useAddUserToWsNonE2EE = () => {
}); });
}; };
export const sendEmailVerificationCode = async () => { export const sendEmailVerificationCode = async (username: string) => {
return apiRequest.post("/api/v2/users/me/emails/code"); return apiRequest.post("/api/v2/users/me/emails/code", {
username
});
}; };
export const useSendEmailVerificationCode = () => { export const useSendEmailVerificationCode = () => {
return useMutation({ return useMutation({
mutationFn: async () => { mutationFn: async (username: string) => {
await sendEmailVerificationCode(); await sendEmailVerificationCode(username);
return {}; return {};
} }
}); });
}; };
export const useVerifyEmailVerificationCode = () => { export const useVerifyEmailVerificationCode = () => {
const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: async ({ code }: { code: string }) => { mutationFn: async ({ username, code }: { username: string; code: string }) => {
await apiRequest.post("/api/v2/users/me/emails/verify", { await apiRequest.post("/api/v2/users/me/emails/verify", {
username,
code code
}); });
return {}; 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, myAPIKeys: ["api-keys"] as const,
myAPIKeysV2: ["api-keys-v2"] as const, myAPIKeysV2: ["api-keys-v2"] as const,
mySessions: ["sessions"] as const, mySessions: ["sessions"] as const,
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const, myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
usersWithMyEmail: ["users-with-my-email"] as const
}; };
export const fetchUserDetails = async () => { export const fetchUserDetails = async () => {
@@ -352,20 +351,3 @@ export const useGetMyOrganizationProjects = (orgId: string) => {
enabled: true 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 jwt_decode from "jwt-decode";
import { import { BackupPDFStep, EmailConfirmationStep, UserInfoSSOStep } from "./components";
BackupPDFStep,
EmailConfirmationStep,
MergeUsersStep,
UserInfoSSOStep
} from "./components";
type Props = { type Props = {
providerAuthToken: string; providerAuthToken: string;
@@ -27,13 +22,30 @@ export const SignupSSO = ({ providerAuthToken }: Props) => {
isEmailVerified isEmailVerified
} = jwt_decode(providerAuthToken) as any; } = jwt_decode(providerAuthToken) as any;
useEffect(() => {
if (!isEmailVerified) {
setStep(0);
} else {
setStep(1);
}
}, []);
const renderView = () => { const renderView = () => {
switch (step) { switch (step) {
case 0: case 0:
return (
<EmailConfirmationStep
authType={authType}
username={username}
email={email}
organizationSlug={organizationSlug}
setStep={setStep}
/>
);
case 1:
return ( return (
<UserInfoSSOStep <UserInfoSSOStep
username={username} username={username}
isEmailVerified={isEmailVerified}
name={`${firstName} ${lastName}`} name={`${firstName} ${lastName}`}
providerOrganizationName={organizationName} providerOrganizationName={organizationName}
password={password} password={password}
@@ -42,17 +54,15 @@ export const SignupSSO = ({ providerAuthToken }: Props) => {
providerAuthToken={providerAuthToken} providerAuthToken={providerAuthToken}
/> />
); );
case 1: // case 2:
return <EmailConfirmationStep email={email} setStep={setStep} />; // return (
// <MergeUsersStep
// username={username}
// authType={authType}
// organizationSlug={organizationSlug}
// />
// );
case 2: case 2:
return (
<MergeUsersStep
username={username}
authType={authType}
organizationSlug={organizationSlug}
/>
);
case 3:
return ( return (
<BackupPDFStep email={username} password={password} name={`${firstName} ${lastName}`} /> <BackupPDFStep email={username} password={password} name={`${firstName} ${lastName}`} />
); );
@@ -2,18 +2,19 @@
// if same email exists, then trigger fn to merge automatically // if same email exists, then trigger fn to merge automatically
import { useState } from "react"; import { useState } from "react";
import ReactCodeInput from "react-code-input"; import ReactCodeInput from "react-code-input";
import { useRouter } from "next/router";
import Error from "@app/components/basic/Error"; import Error from "@app/components/basic/Error";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button } from "@app/components/v2"; import { Button } from "@app/components/v2";
import { import { useSendEmailVerificationCode, useVerifyEmailVerificationCode } from "@app/hooks/api";
fetchUsersWithMyEmail, import { UserAliasType } from "@app/hooks/api/users/types";
useSendEmailVerificationCode,
useVerifyEmailVerificationCode
} from "@app/hooks/api";
type Props = { type Props = {
authType?: UserAliasType;
username: string;
email: string; email: string;
organizationSlug: string;
setStep: (step: number) => void; setStep: (step: number) => void;
}; };
@@ -55,7 +56,14 @@ const propsPhone = {
} }
} as const; } 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 [code, setCode] = useState("");
const [codeError, setCodeError] = useState(false); const [codeError, setCodeError] = useState(false);
const [isResendingVerificationEmail] = useState(false); const [isResendingVerificationEmail] = useState(false);
@@ -66,19 +74,29 @@ export const EmailConfirmationStep = ({ email, setStep }: Props) => {
const checkCode = async () => { const checkCode = async () => {
try { try {
await verifyEmailVerificationCode({ code }); await verifyEmailVerificationCode({ username, code });
setCodeError(false); setCodeError(false);
const usersWithSameEmail = await fetchUsersWithMyEmail();
if (usersWithSameEmail.length > 1) {
setStep(2);
}
createNotification({ createNotification({
text: "Successfully verified code", text: "Successfully verified code",
type: "success" 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) { } catch (err) {
createNotification({ createNotification({
text: "Failed to verify code", text: "Failed to verify code",
@@ -91,7 +109,11 @@ export const EmailConfirmationStep = ({ email, setStep }: Props) => {
const resendCode = async () => { const resendCode = async () => {
try { try {
await sendEmailVerificationCode(); await sendEmailVerificationCode(username);
createNotification({
text: "Successfully resent code",
type: "success"
});
} catch (err) { } catch (err) {
createNotification({ createNotification({
text: "Failed to resend code", 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 { Button, Input } from "@app/components/v2";
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries"; import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { sendEmailVerificationCode } from "@app/hooks/api/users/mutation";
import ProjectService from "@app/services/ProjectService"; import ProjectService from "@app/services/ProjectService";
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
@@ -26,7 +25,6 @@ const client = new jsrp.client();
type Props = { type Props = {
setStep: (step: number) => void; setStep: (step: number) => void;
username: string; username: string;
isEmailVerified?: boolean;
password: string; password: string;
setPassword: (value: string) => void; setPassword: (value: string) => void;
name: string; name: string;
@@ -60,7 +58,6 @@ type Errors = {
*/ */
export const UserInfoSSOStep = ({ export const UserInfoSSOStep = ({
username, username,
isEmailVerified,
name, name,
providerOrganizationName, providerOrganizationName,
password, password,
@@ -204,14 +201,7 @@ export const UserInfoSSOStep = ({
localStorage.setItem("orgData.id", orgId); localStorage.setItem("orgData.id", orgId);
localStorage.setItem("projectData.id", project.id); localStorage.setItem("projectData.id", project.id);
if (isEmailVerified) { setStep(2);
// move to backup PDF step
setStep(3);
} else {
// move to verify email
await sendEmailVerificationCode();
setStep(1);
}
} catch (error) { } catch (error) {
setIsLoading(false); setIsLoading(false);
console.error(error); console.error(error);
@@ -1,4 +1,3 @@
export { BackupPDFStep } from "./BackupPDFStep"; export { BackupPDFStep } from "./BackupPDFStep";
export { EmailConfirmationStep } from "./EmailConfirmationStep"; export { EmailConfirmationStep } from "./EmailConfirmationStep";
export { MergeUsersStep } from "./MergeUsersStep";
export { UserInfoSSOStep } from "./UserInfoSSOStep"; export { UserInfoSSOStep } from "./UserInfoSSOStep";