diff --git a/backend/src/server/routes/v1/user-router.ts b/backend/src/server/routes/v1/user-router.ts index e5deec75b..a0c3592f7 100644 --- a/backend/src/server/routes/v1/user-router.ts +++ b/backend/src/server/routes/v1/user-router.ts @@ -55,7 +55,10 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { schema: { response: { 200: z.object({ - users: UsersSchema.extend({ isMyAccount: z.boolean() }).array() + users: UsersSchema.extend({ + isMyAccount: z.boolean(), + organizations: z.object({ name: z.string(), slug: z.string() }).array() + }).array() }) } }, diff --git a/backend/src/services/user/user-dal.ts b/backend/src/services/user/user-dal.ts index 63e4784d9..b5a29fc8c 100644 --- a/backend/src/services/user/user-dal.ts +++ b/backend/src/services/user/user-dal.ts @@ -8,10 +8,11 @@ import { TUserEncryptionKeys, TUserEncryptionKeysInsert, TUserEncryptionKeysUpdate, - TUsers + TUsers, + UsersSchema } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; export type TUserDALFactory = ReturnType; @@ -168,6 +169,38 @@ export const userDALFactory = (db: TDbClient) => { } }; + const findAllMyAccounts = async (email: string) => { + try { + const doc = await db(TableName.Users) + .where({ email }) + .leftJoin(TableName.OrgMembership, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) + .leftJoin(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`) + .select(selectAllTableCols(TableName.Users)) + .select( + db.ref("name").withSchema(TableName.Organization).as("orgName"), + db.ref("slug").withSchema(TableName.Organization).as("orgSlug") + ); + const formattedDoc = sqlNestRelationships({ + data: doc, + key: "id", + parentMapper: (el) => UsersSchema.parse(el), + childrenMapper: [ + { + key: "orgSlug", + label: "organizations" as const, + mapper: ({ orgSlug, orgName }) => ({ + slug: orgSlug, + name: orgName + }) + } + ] + }); + return formattedDoc; + } catch (error) { + throw new DatabaseError({ error, name: "Upsert user enc key" }); + } + }; + // USER ACTION FUNCTIONS // --------------------- const findOneUserAction = (filter: TUserActionsUpdate, tx?: Knex) => { @@ -200,6 +233,7 @@ export const userDALFactory = (db: TDbClient) => { createUserEncryption, findOneUserAction, createUserAction, - getUsersByFilter + getUsersByFilter, + findAllMyAccounts }; }; diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index ffc390290..29f6300d6 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -30,6 +30,7 @@ type TUserServiceFactoryDep = { | "createUserAction" | "findUserEncKeyByUserId" | "delete" + | "findAllMyAccounts" >; groupProjectDAL: Pick; orgMembershipDAL: Pick; @@ -140,7 +141,7 @@ export const userServiceFactory = ({ }; const getAllMyAccounts = async (email: string, userId: string) => { - const users = await userDAL.find({ email }); + const users = await userDAL.findAllMyAccounts(email); return users?.map((el) => ({ ...el, isMyAccount: el.id === userId })); }; diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 9a65b12fa..ea451db02 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -39,7 +39,16 @@ export const useGetUser = () => export const fetchUserDuplicateAccounts = async () => { const { data } = await apiRequest.get<{ - users: Array; + users: Array< + User & { + isMyAccount: boolean; + organizations: { name: string; slug: string }[]; + devices: { + ip: string; + userAgent: string; + }[]; + } + >; }>("/api/v1/user/duplicate-accounts"); return data.users; }; diff --git a/frontend/src/pages/auth/SelectOrgPage/EmailDuplicationConfirmation.tsx b/frontend/src/pages/auth/SelectOrgPage/EmailDuplicationConfirmation.tsx index bd98b51b1..ca56ad84f 100644 --- a/frontend/src/pages/auth/SelectOrgPage/EmailDuplicationConfirmation.tsx +++ b/frontend/src/pages/auth/SelectOrgPage/EmailDuplicationConfirmation.tsx @@ -7,7 +7,8 @@ import { Link, useNavigate } from "@tanstack/react-router"; import { format } from "date-fns"; import { createNotification } from "@app/components/notifications"; -import { Alert, Button, Tooltip } from "@app/components/v2"; +import { Button, DeleteActionModal, Tooltip } from "@app/components/v2"; +import { usePopUp } from "@app/hooks"; import { useGetMyDuplicateAccount, useLogoutUser, @@ -24,6 +25,7 @@ export const EmailDuplicationConfirmation = ({ onRemoveDuplicateLater }: Props) const { t } = useTranslation(); const navigate = useNavigate(); const logout = useLogoutUser(true); + const { popUp, handlePopUpToggle } = usePopUp(["removeDuplicateConfirm"] as const); const handleLogout = useCallback(async () => { try { console.log("Logging out..."); @@ -65,13 +67,14 @@ export const EmailDuplicationConfirmation = ({ onRemoveDuplicateLater }: Props) Your current account is: {" "} {duplicateAccounts?.data?.myAccount?.username}.

- -
+
+

We've detected multiple accounts using variations of the same email address. - Confirm to retain this account {duplicateAccounts?.data?.myAccount?.username} - . Upon confirmation other accounts will be removed. -

- +

+
+ +
+ Your other accounts
{duplicateAccounts?.data?.duplicateAccounts?.map((el) => { @@ -84,7 +87,10 @@ export const EmailDuplicationConfirmation = ({ onRemoveDuplicateLater }: Props)
{el.username}
- Last login: {format(new Date(el.updatedAt), "Pp")} + Last logged in at {format(new Date(el.updatedAt), "Pp")} +
+
+ Organizations: {el?.organizations?.map((i) => i.slug)?.join(",")}
@@ -109,26 +115,16 @@ export const EmailDuplicationConfirmation = ({ onRemoveDuplicateLater }: Props)
+ handlePopUpToggle("removeDuplicateConfirm", isOpen)} + deleteKey="remove" + buttonText="Confirm" + onDeleteApproved={() => + removeDuplicateEmails.mutateAsync(undefined, { + onSuccess: () => { + createNotification({ + type: "success", + text: "Removed duplicate accounts" + }); + onRemoveDuplicateLater(); + } + }) + } + />
); };