mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 14:26:38 +00:00
Make merge user step automatic after email verification
This commit is contained in:
@@ -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 };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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}`} />
|
||||||
);
|
);
|
||||||
|
|||||||
+36
-14
@@ -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'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'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";
|
||||||
|
|||||||
Reference in New Issue
Block a user