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)
- removeDuplicateEmails.mutate(undefined, {
- onSuccess: () => {
- createNotification({
- type: "success",
- text: "Removed duplicate accounts"
- });
- onRemoveDuplicateLater();
- }
- })
- }
+ onClick={() => handlePopUpToggle("removeDuplicateConfirm", true)}
>
- Confirm
+ Delete all other accounts
onRemoveDuplicateLater()}
className="flex-1 flex-grow"
>
- Ask Again Later?
+ Remind me later
+ handlePopUpToggle("removeDuplicateConfirm", isOpen)}
+ deleteKey="remove"
+ buttonText="Confirm"
+ onDeleteApproved={() =>
+ removeDuplicateEmails.mutateAsync(undefined, {
+ onSuccess: () => {
+ createNotification({
+ type: "success",
+ text: "Removed duplicate accounts"
+ });
+ onRemoveDuplicateLater();
+ }
+ })
+ }
+ />
);
};