mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Continue merge user
This commit is contained in:
@@ -26,8 +26,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
auditLogsRetentionDays: 0,
|
auditLogsRetentionDays: 0,
|
||||||
samlSSO: true,
|
samlSSO: true,
|
||||||
scim: true,
|
scim: true,
|
||||||
ldap: false,
|
ldap: true,
|
||||||
groups: false,
|
groups: true,
|
||||||
status: null,
|
status: null,
|
||||||
trial_end: null,
|
trial_end: null,
|
||||||
has_used_trial: true,
|
has_used_trial: true,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
|||||||
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 { TUserDALFactory } from "@app/services/user/user-dal";
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||||
|
|
||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
@@ -33,6 +34,7 @@ import { TCreateSamlCfgDTO, TGetSamlCfgDTO, TSamlLoginDTO, TUpdateSamlCfgDTO } f
|
|||||||
type TSamlConfigServiceFactoryDep = {
|
type TSamlConfigServiceFactoryDep = {
|
||||||
samlConfigDAL: TSamlConfigDALFactory;
|
samlConfigDAL: TSamlConfigDALFactory;
|
||||||
userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById">;
|
userDAL: Pick<TUserDALFactory, "create" | "findOne" | "transaction" | "updateById">;
|
||||||
|
userAliasDAL: Pick<TUserAliasDALFactory, "create" | "findOne">;
|
||||||
orgDAL: Pick<
|
orgDAL: Pick<
|
||||||
TOrgDALFactory,
|
TOrgDALFactory,
|
||||||
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
|
"createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById"
|
||||||
@@ -360,6 +362,7 @@ export const samlConfigServiceFactory = ({
|
|||||||
{
|
{
|
||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
|
isEmailVerified: false,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
authMethods: [AuthMethod.EMAIL],
|
authMethods: [AuthMethod.EMAIL],
|
||||||
@@ -382,6 +385,7 @@ export const samlConfigServiceFactory = ({
|
|||||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
|
...(user.email && { email: user.email }),
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
organizationName: organization.name,
|
organizationName: organization.name,
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ import { orgDALFactory } from "@app/services/org/org-dal";
|
|||||||
import { orgRoleDALFactory } from "@app/services/org/org-role-dal";
|
import { orgRoleDALFactory } from "@app/services/org/org-role-dal";
|
||||||
import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
|
import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||||
import { orgServiceFactory } from "@app/services/org/org-service";
|
import { orgServiceFactory } from "@app/services/org/org-service";
|
||||||
|
import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||||
import { projectDALFactory } from "@app/services/project/project-dal";
|
import { projectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { projectQueueFactory } from "@app/services/project/project-queue";
|
import { projectQueueFactory } from "@app/services/project/project-queue";
|
||||||
import { projectServiceFactory } from "@app/services/project/project-service";
|
import { projectServiceFactory } from "@app/services/project/project-service";
|
||||||
@@ -153,6 +154,7 @@ export const registerRoutes = async (
|
|||||||
const authDAL = authDALFactory(db);
|
const authDAL = authDALFactory(db);
|
||||||
const authTokenDAL = tokenDALFactory(db);
|
const authTokenDAL = tokenDALFactory(db);
|
||||||
const orgDAL = orgDALFactory(db);
|
const orgDAL = orgDALFactory(db);
|
||||||
|
const orgMembershipDAL = orgMembershipDALFactory(db);
|
||||||
const orgBotDAL = orgBotDALFactory(db);
|
const orgBotDAL = orgBotDALFactory(db);
|
||||||
const incidentContactDAL = incidentContactDALFactory(db);
|
const incidentContactDAL = incidentContactDALFactory(db);
|
||||||
const orgRoleDAL = orgRoleDALFactory(db);
|
const orgRoleDAL = orgRoleDALFactory(db);
|
||||||
@@ -328,7 +330,14 @@ export const registerRoutes = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
|
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
|
||||||
const userService = userServiceFactory({ userDAL, tokenService, smtpService });
|
const userService = userServiceFactory({
|
||||||
|
userDAL,
|
||||||
|
userAliasDAL,
|
||||||
|
orgDAL,
|
||||||
|
orgMembershipDAL,
|
||||||
|
tokenService,
|
||||||
|
smtpService
|
||||||
|
});
|
||||||
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, tokenDAL: authTokenDAL });
|
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService, orgDAL, tokenDAL: authTokenDAL });
|
||||||
const passwordService = authPaswordServiceFactory({
|
const passwordService = authPaswordServiceFactory({
|
||||||
tokenService,
|
tokenService,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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";
|
||||||
@@ -68,6 +69,38 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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",
|
||||||
|
|||||||
@@ -30,13 +30,14 @@ export const getTokenConfig = (tokenType: TokenType) => {
|
|||||||
case TokenType.TOKEN_EMAIL_VERIFICATION: {
|
case TokenType.TOKEN_EMAIL_VERIFICATION: {
|
||||||
// generate random 6-digit code
|
// generate random 6-digit code
|
||||||
const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1));
|
const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1));
|
||||||
|
const triesLeft = 3;
|
||||||
const expiresAt = new Date(new Date().getTime() + 86400000);
|
const expiresAt = new Date(new Date().getTime() + 86400000);
|
||||||
return { token, expiresAt };
|
return { token, triesLeft, expiresAt };
|
||||||
}
|
}
|
||||||
case TokenType.TOKEN_EMAIL_MFA: {
|
case TokenType.TOKEN_EMAIL_MFA: {
|
||||||
// generate random 6-digit code
|
// generate random 6-digit code
|
||||||
const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1));
|
const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1));
|
||||||
const triesLeft = 5;
|
const triesLeft = 3;
|
||||||
const expiresAt = new Date(new Date().getTime() + 300000);
|
const expiresAt = new Date(new Date().getTime() + 300000);
|
||||||
return { token, triesLeft, expiresAt };
|
return { token, triesLeft, expiresAt };
|
||||||
}
|
}
|
||||||
|
|||||||
13
backend/src/services/org-membership/org-membership-dal.ts
Normal file
13
backend/src/services/org-membership/org-membership-dal.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TOrgMembershipDALFactory = ReturnType<typeof orgMembershipDALFactory>;
|
||||||
|
|
||||||
|
export const orgMembershipDALFactory = (db: TDbClient) => {
|
||||||
|
const orgMembershipOrm = ormify(db, TableName.OrgMembership);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...orgMembershipOrm
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,20 +1,34 @@
|
|||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||||
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
|
import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
|
import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
|
||||||
|
|
||||||
import { AuthMethod } from "../auth/auth-type";
|
import { AuthMethod } from "../auth/auth-type";
|
||||||
import { TUserDALFactory } from "./user-dal";
|
import { TUserDALFactory } from "./user-dal";
|
||||||
|
|
||||||
|
// TODO: Pick all of these
|
||||||
type TUserServiceFactoryDep = {
|
type TUserServiceFactoryDep = {
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
|
userAliasDAL: TUserAliasDALFactory;
|
||||||
|
orgDAL: TOrgDALFactory;
|
||||||
|
orgMembershipDAL: TOrgMembershipDALFactory;
|
||||||
tokenService: TAuthTokenServiceFactory;
|
tokenService: TAuthTokenServiceFactory;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
|
export type TUserServiceFactory = ReturnType<typeof userServiceFactory>;
|
||||||
|
|
||||||
export const userServiceFactory = ({ userDAL, tokenService, smtpService }: TUserServiceFactoryDep) => {
|
export const userServiceFactory = ({
|
||||||
|
userDAL,
|
||||||
|
userAliasDAL,
|
||||||
|
// orgDAL,
|
||||||
|
orgMembershipDAL,
|
||||||
|
tokenService,
|
||||||
|
smtpService
|
||||||
|
}: TUserServiceFactoryDep) => {
|
||||||
const sendEmailVerificationCode = async (userId: string) => {
|
const sendEmailVerificationCode = async (userId: string) => {
|
||||||
console.log("sendEmailVerificationCode userId: ", userId);
|
console.log("sendEmailVerificationCode userId: ", userId);
|
||||||
const user = await userDAL.findById(userId);
|
const user = await userDAL.findById(userId);
|
||||||
@@ -78,6 +92,68 @@ export const userServiceFactory = ({ userDAL, tokenService, smtpService }: TUser
|
|||||||
return users;
|
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
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
if (!mergeUser || !mergeUser.isEmailVerified) throw new BadRequestError({});
|
||||||
|
|
||||||
|
if (myUser.email !== mergeUser.email) throw new BadRequestError({});
|
||||||
|
|
||||||
|
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
|
||||||
|
},
|
||||||
|
{ tx }
|
||||||
|
);
|
||||||
|
await userDAL.deleteById(myUser.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 mergeUser;
|
||||||
|
});
|
||||||
|
|
||||||
|
return targetUser;
|
||||||
|
};
|
||||||
|
|
||||||
const toggleUserMfa = async (userId: string, isMfaEnabled: boolean) => {
|
const toggleUserMfa = async (userId: string, isMfaEnabled: boolean) => {
|
||||||
const user = await userDAL.findById(userId);
|
const user = await userDAL.findById(userId);
|
||||||
|
|
||||||
@@ -143,6 +219,7 @@ export const userServiceFactory = ({ userDAL, tokenService, smtpService }: TUser
|
|||||||
sendEmailVerificationCode,
|
sendEmailVerificationCode,
|
||||||
verifyEmailVerificationCode,
|
verifyEmailVerificationCode,
|
||||||
listUsersWithSameEmail,
|
listUsersWithSameEmail,
|
||||||
|
mergeUsers,
|
||||||
toggleUserMfa,
|
toggleUserMfa,
|
||||||
updateUserName,
|
updateUserName,
|
||||||
updateAuthMethods,
|
updateAuthMethods,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
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,
|
||||||
@@ -19,10 +20,10 @@ export {
|
|||||||
useGetOrgUsers,
|
useGetOrgUsers,
|
||||||
useGetUser,
|
useGetUser,
|
||||||
useGetUserAction,
|
useGetUserAction,
|
||||||
|
useListUsersWithMyEmail,
|
||||||
useLogoutUser,
|
useLogoutUser,
|
||||||
useRegisterUserAction,
|
useRegisterUserAction,
|
||||||
useRevokeMySessions,
|
useRevokeMySessions,
|
||||||
useUpdateMfaEnabled,
|
useUpdateMfaEnabled,
|
||||||
useUpdateOrgUserRole,
|
useUpdateOrgUserRole,
|
||||||
useUpdateUserAuthMethods
|
useUpdateUserAuthMethods} from "./queries";
|
||||||
} from "./queries";
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { workspaceKeys } from "../workspace/queries";
|
import { workspaceKeys } from "../workspace/queries";
|
||||||
import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE } from "./types";
|
import { userKeys } from "./queries";
|
||||||
|
import { AddUserToWsDTOE2EE, AddUserToWsDTONonE2EE, User } from "./types";
|
||||||
|
|
||||||
export const useAddUserToWsE2EE = () => {
|
export const useAddUserToWsE2EE = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -72,12 +73,27 @@ export const useSendEmailVerificationCode = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useVerifyEmailVerificationCode = () => {
|
export const useVerifyEmailVerificationCode = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({ code }: { code: string }) => {
|
mutationFn: async ({ code }: { code: string }) => {
|
||||||
await apiRequest.post("/api/v2/users/me/emails/verify", {
|
await apiRequest.post("/api/v2/users/me/emails/verify", {
|
||||||
code
|
code
|
||||||
});
|
});
|
||||||
return {};
|
return {};
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(userKeys.usersWithMyEmail);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMergeUsers = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ username }: { username: string }) => {
|
||||||
|
const { data } = await apiRequest.post<{ user: User }>("/api/v2/users/me/users/merge-user", {
|
||||||
|
username
|
||||||
|
});
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ 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 () => {
|
||||||
@@ -351,3 +352,20 @@ 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,7 +1,11 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import jwt_decode from "jwt-decode";
|
import jwt_decode from "jwt-decode";
|
||||||
|
|
||||||
import { BackupPDFStep, EmailConfirmationStep,UserInfoSSOStep } from "./components";
|
import {
|
||||||
|
BackupPDFStep,
|
||||||
|
EmailConfirmationStep,
|
||||||
|
MergeUsersStep,
|
||||||
|
UserInfoSSOStep} from "./components";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
providerAuthToken: string;
|
providerAuthToken: string;
|
||||||
@@ -11,7 +15,9 @@ export const SignupSSO = ({ providerAuthToken }: Props) => {
|
|||||||
const [step, setStep] = useState(0);
|
const [step, setStep] = useState(0);
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
const { username, organizationName, firstName, lastName } = jwt_decode(providerAuthToken) as any;
|
const { username, email, organizationName, firstName, lastName } = jwt_decode(
|
||||||
|
providerAuthToken
|
||||||
|
) as any;
|
||||||
|
|
||||||
const renderView = () => {
|
const renderView = () => {
|
||||||
switch (step) {
|
switch (step) {
|
||||||
@@ -19,6 +25,7 @@ export const SignupSSO = ({ providerAuthToken }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<UserInfoSSOStep
|
<UserInfoSSOStep
|
||||||
username={username}
|
username={username}
|
||||||
|
email={email}
|
||||||
name={`${firstName} ${lastName}`}
|
name={`${firstName} ${lastName}`}
|
||||||
providerOrganizationName={organizationName}
|
providerOrganizationName={organizationName}
|
||||||
password={password}
|
password={password}
|
||||||
@@ -28,8 +35,10 @@ export const SignupSSO = ({ providerAuthToken }: Props) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 1:
|
case 1:
|
||||||
return <EmailConfirmationStep />;
|
return <EmailConfirmationStep email={email} setStep={setStep} />;
|
||||||
case 2:
|
case 2:
|
||||||
|
return <MergeUsersStep username={username} />;
|
||||||
|
case 3:
|
||||||
return (
|
return (
|
||||||
<BackupPDFStep email={username} password={password} name={`${firstName} ${lastName}`} />
|
<BackupPDFStep email={username} password={password} name={`${firstName} ${lastName}`} />
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,11 +3,19 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import ReactCodeInput from "react-code-input";
|
import ReactCodeInput from "react-code-input";
|
||||||
|
|
||||||
// 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 { useUser } from "@app/context";
|
import { useUser } from "@app/context";
|
||||||
import { useSendEmailVerificationCode, useVerifyEmailVerificationCode } from "@app/hooks/api";
|
import {
|
||||||
|
fetchUsersWithMyEmail,
|
||||||
|
useSendEmailVerificationCode,
|
||||||
|
useVerifyEmailVerificationCode} from "@app/hooks/api";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
email: string;
|
||||||
|
setStep: (step: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
// The style for the verification code input
|
// The style for the verification code input
|
||||||
const props = {
|
const props = {
|
||||||
@@ -47,10 +55,10 @@ const propsPhone = {
|
|||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const EmailConfirmationStep = () => {
|
export const EmailConfirmationStep = ({ email, setStep }: Props) => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
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);
|
||||||
const [isLoading] = useState(false);
|
const [isLoading] = useState(false);
|
||||||
|
|
||||||
@@ -59,22 +67,32 @@ export const EmailConfirmationStep = () => {
|
|||||||
|
|
||||||
const checkCode = async () => {
|
const checkCode = async () => {
|
||||||
try {
|
try {
|
||||||
console.log("checkCode code: ", code);
|
|
||||||
await verifyEmailVerificationCode({ code });
|
await verifyEmailVerificationCode({ code });
|
||||||
console.log("checkCode 2");
|
setCodeError(false);
|
||||||
|
|
||||||
|
const usersWithSameEmail = await fetchUsersWithMyEmail();
|
||||||
|
|
||||||
|
if (usersWithSameEmail.length > 1) {
|
||||||
|
setStep(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully verified code",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Failed to verify code",
|
text: "Failed to verify code",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCode("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const resendCode = async () => {
|
const resendCode = async () => {
|
||||||
try {
|
try {
|
||||||
console.log("resendCode");
|
|
||||||
await sendEmailVerificationCode();
|
await sendEmailVerificationCode();
|
||||||
console.log("resendCode");
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Failed to resend code",
|
text: "Failed to resend code",
|
||||||
@@ -86,7 +104,7 @@ export const EmailConfirmationStep = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="mx-auto h-full w-full pb-4 md:px-8">
|
<div className="mx-auto h-full w-full pb-4 md:px-8">
|
||||||
<p className="text-md flex justify-center text-bunker-200">
|
<p className="text-md flex justify-center text-bunker-200">
|
||||||
We've sent a verification code to
|
We've sent a verification code to {email}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-md my-1 flex justify-center font-semibold text-bunker-200">
|
<p className="text-md my-1 flex justify-center font-semibold text-bunker-200">
|
||||||
{user?.email}
|
{user?.email}
|
||||||
@@ -113,7 +131,7 @@ export const EmailConfirmationStep = () => {
|
|||||||
className="mt-2 mb-2"
|
className="mt-2 mb-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* {codeError && <Error text="Oops. Your code is wrong. Please try again." />} */}
|
{codeError && <Error text="Oops. Your code is wrong. Please try again." />}
|
||||||
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
|
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
|
||||||
<div className="text-l w-full py-1 text-lg">
|
<div className="text-l w-full py-1 text-lg">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { faUsers } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
EmptyState,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useListUsersWithMyEmail, useMergeUsers } from "@app/hooks/api";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MergeUsersStep = ({ username }: Props) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { data: users, isLoading: isLoadingUsers } = useListUsersWithMyEmail();
|
||||||
|
const { mutateAsync: mergeUser, isLoading: isLoadingMerge } = useMergeUsers();
|
||||||
|
const handleMergeUser = async (targetUsername: string) => {
|
||||||
|
try {
|
||||||
|
console.log("merge A");
|
||||||
|
await mergeUser({ username: targetUsername });
|
||||||
|
// TODO: logout, make user re-login
|
||||||
|
console.log("merge B");
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully merged user",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push("/login");
|
||||||
|
} 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>{username}</Td>
|
||||||
|
<Td>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoadingMerge}
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
type="submit"
|
||||||
|
onClick={() => handleMergeUser(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>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { MergeUsersStep } from "./MergeUsersStep";
|
||||||
@@ -25,6 +25,7 @@ const client = new jsrp.client();
|
|||||||
type Props = {
|
type Props = {
|
||||||
setStep: (step: number) => void;
|
setStep: (step: number) => void;
|
||||||
username: string;
|
username: string;
|
||||||
|
email?: string;
|
||||||
password: string;
|
password: string;
|
||||||
setPassword: (value: string) => void;
|
setPassword: (value: string) => void;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -58,6 +59,7 @@ type Errors = {
|
|||||||
*/
|
*/
|
||||||
export const UserInfoSSOStep = ({
|
export const UserInfoSSOStep = ({
|
||||||
username,
|
username,
|
||||||
|
email,
|
||||||
name,
|
name,
|
||||||
providerOrganizationName,
|
providerOrganizationName,
|
||||||
password,
|
password,
|
||||||
@@ -201,7 +203,13 @@ 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);
|
||||||
|
|
||||||
setStep(1);
|
if (email) {
|
||||||
|
// move to verify email
|
||||||
|
setStep(1);
|
||||||
|
} else {
|
||||||
|
// move to backup PDF step
|
||||||
|
setStep(2);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
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