From 2345bea7f0c0b4f852e985afe379d503486f0ea1 Mon Sep 17 00:00:00 2001 From: Baozier Date: Wed, 11 Dec 2024 21:45:07 -0500 Subject: [PATCH] Show user linked account in admin panel user detail page (#200) --- .../app/[lang]/users/[authId]/page.test.tsx | 7 ++- .../app/[lang]/users/[authId]/page.tsx | 44 +++++++++++++++++++ .../users/[authId]/account-linking/route.ts | 16 +++++++ admin-panel/services/auth/api.ts | 41 +++++++++++++++++ admin-panel/translations/en.json | 2 + admin-panel/translations/fr.json | 2 + 6 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 admin-panel/app/api/v1/users/[authId]/account-linking/route.ts diff --git a/admin-panel/app/[lang]/users/[authId]/page.test.tsx b/admin-panel/app/[lang]/users/[authId]/page.test.tsx index 1c86ca1..04e14cd 100644 --- a/admin-panel/app/[lang]/users/[authId]/page.test.tsx +++ b/admin-panel/app/[lang]/users/[authId]/page.test.tsx @@ -22,6 +22,7 @@ import { usePostApiV1UsersByAuthIdSmsMfaMutation, usePostApiV1UsersByAuthIdVerifyEmailMutation, usePutApiV1UsersByAuthIdMutation, + useDeleteApiV1UsersByAuthIdAccountLinkingMutation, } from 'services/auth/api' import { users } from 'tests/userMock' import { roles } from 'tests/roleMock' @@ -57,6 +58,7 @@ vi.mock( usePostApiV1UsersByAuthIdSmsMfaMutation: vi.fn(), usePostApiV1UsersByAuthIdVerifyEmailMutation: vi.fn(), usePutApiV1UsersByAuthIdMutation: vi.fn(), + useDeleteApiV1UsersByAuthIdAccountLinkingMutation: vi.fn(), }), ) @@ -93,6 +95,7 @@ const mockEnrollSmsMfa = vi.fn() const mockUnenrollEmailMfa = vi.fn() const mockUnenrollSmsMfa = vi.fn() const mockUnenrollOtpMfa = vi.fn() +const mockUnlinkAccount = vi.fn() describe( 'user', @@ -121,7 +124,9 @@ describe( (useDeleteApiV1UsersByAuthIdEmailMfaMutation as Mock) .mockReturnValue([mockUnenrollEmailMfa, { isLoading: false }]); (useDeleteApiV1UsersByAuthIdOtpMfaMutation as Mock).mockReturnValue([mockUnenrollOtpMfa, { isLoading: false }]); - (useDeleteApiV1UsersByAuthIdSmsMfaMutation as Mock).mockReturnValue([mockUnenrollSmsMfa, { isLoading: false }]) + (useDeleteApiV1UsersByAuthIdSmsMfaMutation as Mock).mockReturnValue([mockUnenrollSmsMfa, { isLoading: false }]); + (useDeleteApiV1UsersByAuthIdAccountLinkingMutation as Mock) + .mockReturnValue([mockUnlinkAccount, { isLoading: false }]) }) it( diff --git a/admin-panel/app/[lang]/users/[authId]/page.tsx b/admin-panel/app/[lang]/users/[authId]/page.tsx index 57e44d7..96e551f 100644 --- a/admin-panel/app/[lang]/users/[authId]/page.tsx +++ b/admin-panel/app/[lang]/users/[authId]/page.tsx @@ -11,6 +11,7 @@ import { useParams } from 'next/navigation' import { useEffect, useMemo, useState, } from 'react' +import { ArrowTopRightOnSquareIcon } from '@heroicons/react/16/solid' import UserEmailVerified from 'components/UserEmailVerified' import { routeTool } from 'tools' import EntityStatusLabel from 'components/EntityStatusLabel' @@ -26,6 +27,7 @@ import DeleteButton from 'components/DeleteButton' import useLocaleRouter from 'hooks/useLocaleRoute' import { PutUserReq, + useDeleteApiV1UsersByAuthIdAccountLinkingMutation, useDeleteApiV1UsersByAuthIdConsentedAppsAndAppIdMutation, useDeleteApiV1UsersByAuthIdEmailMfaMutation, useDeleteApiV1UsersByAuthIdLockedIpsMutation, @@ -95,6 +97,7 @@ const Page = () => { const [unenrollEmailMfa] = useDeleteApiV1UsersByAuthIdEmailMfaMutation() const [unenrollSmsMfa] = useDeleteApiV1UsersByAuthIdSmsMfaMutation() const [unenrollOtpMfa] = useDeleteApiV1UsersByAuthIdOtpMfaMutation() + const [unlinkAccount] = useDeleteApiV1UsersByAuthIdAccountLinkingMutation() const updateObj = useMemo( () => { @@ -180,6 +183,10 @@ const Page = () => { await enrollEmailMfa({ authId: String(authId) }) } + const handleUnlink = async () => { + await unlinkAccount({ authId: String(authId) }) + } + const handleToggleUserRole = (role: string) => { const newRoles = userRoles?.includes(role) ? userRoles.filter((userRole) => role !== userRole) @@ -187,6 +194,10 @@ const Page = () => { setUserRoles(newRoles) } + const handleClickLinkedAccount = () => { + router.push(`${routeTool.Internal.Users}/${user?.linkedAuthId}`) + } + const renderEmailButtons = (user: UserDetail) => { if (user.socialAccountId) return null return ( @@ -277,6 +288,17 @@ const Page = () => { ) } + const renderUnlinkAccountButtons = () => { + return ( + + ) + } + if (!user) return null return ( @@ -384,6 +406,28 @@ const Page = () => { )} + {user.linkedAuthId && ( + + {t('users.linkedWith')} + +
+ + {user.linkedAuthId} + + +
+ {renderUnlinkAccountButtons()} +
+
+
+ + {renderUnlinkAccountButtons()} + +
+ )} {t('users.locale')} diff --git a/admin-panel/app/api/v1/users/[authId]/account-linking/route.ts b/admin-panel/app/api/v1/users/[authId]/account-linking/route.ts new file mode 100644 index 0000000..92d902a --- /dev/null +++ b/admin-panel/app/api/v1/users/[authId]/account-linking/route.ts @@ -0,0 +1,16 @@ +import { sendS2SRequest } from 'app/api/request' + +type Params = { + authId: string; +} + +export async function DELETE ( + request: Request, context: { params: Params }, +) { + const authId = context.params.authId + + return sendS2SRequest({ + method: 'DELETE', + uri: `/api/v1/users/${authId}/account-linking`, + }) +} diff --git a/admin-panel/services/auth/api.ts b/admin-panel/services/auth/api.ts index 7373995..8906360 100644 --- a/admin-panel/services/auth/api.ts +++ b/admin-panel/services/auth/api.ts @@ -285,6 +285,26 @@ const injectedRtkApi = api }), invalidatesTags: ['Users'], }), + postApiV1UsersByAuthIdAccountLinkingAndLinkingAuthId: build.mutation< + PostApiV1UsersByAuthIdAccountLinkingAndLinkingAuthIdApiResponse, + PostApiV1UsersByAuthIdAccountLinkingAndLinkingAuthIdApiArg + >({ + query: (queryArg) => ({ + url: `/api/v1/users/${queryArg.authId}/account-linking/${queryArg.linkingAuthId}`, + method: 'POST', + }), + invalidatesTags: ['Users'], + }), + deleteApiV1UsersByAuthIdAccountLinking: build.mutation< + DeleteApiV1UsersByAuthIdAccountLinkingApiResponse, + DeleteApiV1UsersByAuthIdAccountLinkingApiArg + >({ + query: (queryArg) => ({ + url: `/api/v1/users/${queryArg.authId}/account-linking`, + method: 'DELETE', + }), + invalidatesTags: ['Users'], + }), getApiV1LogsEmail: build.query< GetApiV1LogsEmailApiResponse, GetApiV1LogsEmailApiArg @@ -543,6 +563,24 @@ export type DeleteApiV1UsersByAuthIdSmsMfaApiArg = { /** The authId of the user */ authId: string; }; +export type PostApiV1UsersByAuthIdAccountLinkingAndLinkingAuthIdApiResponse = + /** status 200 undefined */ { + success?: boolean; + }; +export type PostApiV1UsersByAuthIdAccountLinkingAndLinkingAuthIdApiArg = { + /** The authId of the user */ + authId: string; + /** The authId of the account to link with */ + linkingAuthId: string; +}; +export type DeleteApiV1UsersByAuthIdAccountLinkingApiResponse = + /** status 200 undefined */ { + success?: boolean; + }; +export type DeleteApiV1UsersByAuthIdAccountLinkingApiArg = { + /** The authId of the user */ + authId: string; +}; export type GetApiV1LogsEmailApiResponse = /** status 200 A list of email logs */ { logs?: EmailLog[]; @@ -686,6 +724,7 @@ export type User = { id: number; authId: string; email: string | null; + linkedAuthId?: string | null; socialAccountId: string | null; socialAccountType: string | null; firstName: string | null; @@ -785,6 +824,8 @@ export const { useDeleteApiV1UsersByAuthIdOtpMfaMutation, usePostApiV1UsersByAuthIdSmsMfaMutation, useDeleteApiV1UsersByAuthIdSmsMfaMutation, + usePostApiV1UsersByAuthIdAccountLinkingAndLinkingAuthIdMutation, + useDeleteApiV1UsersByAuthIdAccountLinkingMutation, useGetApiV1LogsEmailQuery, useLazyGetApiV1LogsEmailQuery, useGetApiV1LogsEmailByIdQuery, diff --git a/admin-panel/translations/en.json b/admin-panel/translations/en.json index 339361a..0f05f8c 100644 --- a/admin-panel/translations/en.json +++ b/admin-panel/translations/en.json @@ -70,6 +70,8 @@ "name": "Name", "locale": "Locale", "status": "Status", + "linkedWith": "Linked Account", + "unlink": "Unlink", "loginCount": "Login Count", "emailVerified": "Email Verified", "emailNotVerified": "Email not Verified", diff --git a/admin-panel/translations/fr.json b/admin-panel/translations/fr.json index 6eb8079..4543af4 100644 --- a/admin-panel/translations/fr.json +++ b/admin-panel/translations/fr.json @@ -70,6 +70,8 @@ "name": "Nom", "locale": "Langue", "status": "Statut", + "linkedWith": "Compte associé", + "unlink": "Dissocier", "loginCount": "Nombre de connexions", "emailVerified": "E-mail vérifié", "emailNotVerified": "E-mail non vérifié",