From 59c1b595a650d50fe701259e5d7ee720f9c880a7 Mon Sep 17 00:00:00 2001 From: Mir Arif Hasan Date: Thu, 26 Mar 2026 00:58:36 +0600 Subject: [PATCH] feat: show user workspace memberships in admin dashboard (#5968) Co-authored-by: Anwarul Islam Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com> --- packages/hoppscotch-backend/src/gql-schema.ts | 2 - .../src/team/team.resolver.ts | 38 ++++++ .../src/team/team.service.ts | 10 +- packages/hoppscotch-sh-admin/locales/en.json | 10 ++ .../src/components/users/Teams.vue | 112 ++++++++++++++++++ .../gql/queries/TeamsOfUserByAdmin.graphql | 13 ++ .../src/pages/users/_id.vue | 26 +++- 7 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 packages/hoppscotch-sh-admin/src/components/users/Teams.vue create mode 100644 packages/hoppscotch-sh-admin/src/helpers/backend/gql/queries/TeamsOfUserByAdmin.graphql diff --git a/packages/hoppscotch-backend/src/gql-schema.ts b/packages/hoppscotch-backend/src/gql-schema.ts index 708f7691..1e278e35 100644 --- a/packages/hoppscotch-backend/src/gql-schema.ts +++ b/packages/hoppscotch-backend/src/gql-schema.ts @@ -45,7 +45,6 @@ const RESOLVERS = [ AdminResolver, ShortcodeResolver, TeamResolver, - TeamEnvsTeamResolver, TeamMemberResolver, TeamCollectionResolver, TeamTeamInviteExtResolver, @@ -54,7 +53,6 @@ const RESOLVERS = [ TeamInvitationResolver, TeamRequestResolver, UserResolver, - UserCollectionResolver, UserEnvironmentsResolver, UserEnvsUserResolver, UserHistoryUserResolver, diff --git a/packages/hoppscotch-backend/src/team/team.resolver.ts b/packages/hoppscotch-backend/src/team/team.resolver.ts index 4760ae0c..91c6bf10 100644 --- a/packages/hoppscotch-backend/src/team/team.resolver.ts +++ b/packages/hoppscotch-backend/src/team/team.resolver.ts @@ -18,10 +18,14 @@ import { RequiresTeamRole } from './decorators/requires-team-role.decorator'; import { GqlTeamMemberGuard } from './guards/gql-team-member.guard'; import { PubSubService } from '../pubsub/pubsub.service'; import * as E from 'fp-ts/Either'; +import * as O from 'fp-ts/Option'; import { throwErr } from 'src/utils'; import { AuthUser } from 'src/types/AuthUser'; import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard'; import { SkipThrottle } from '@nestjs/throttler'; +import { GqlAdminGuard } from 'src/admin/guards/gql-admin.guard'; +import { UserService } from 'src/user/user.service'; +import { USER_NOT_FOUND } from 'src/errors'; @UseGuards(GqlThrottlerGuard) @Resolver(() => Team) @@ -29,6 +33,7 @@ export class TeamResolver { constructor( private readonly teamService: TeamService, private readonly pubsub: PubSubService, + private readonly userService: UserService, ) {} // Field Resolvers @@ -141,6 +146,39 @@ export class TeamResolver { return this.teamService.getTeamWithID(teamID); } + @Query(() => [Team], { + description: 'Returns the list of teams a user is a member of (admin-only)', + }) + @UseGuards(GqlAuthGuard, GqlAdminGuard) + async teamsOfUserByAdmin( + @Args({ + name: 'userUid', + type: () => ID, + description: 'UID of the user to fetch teams for', + }) + userUid: string, + @Args({ + name: 'cursor', + type: () => ID, + description: + 'The ID of the last returned team entry (used for pagination)', + nullable: true, + }) + cursor?: string, + @Args({ + name: 'take', + type: () => Int, + description: 'Number of teams to return per page', + nullable: true, + defaultValue: 10, + }) + take?: number, + ): Promise { + const user = await this.userService.findUserById(userUid); + if (O.isNone(user)) throwErr(USER_NOT_FOUND); + return this.teamService.getTeamsOfUser(userUid, cursor ?? null, take); + } + // Mutation @Mutation(() => Team, { description: 'Creates a team owned by the executing user', diff --git a/packages/hoppscotch-backend/src/team/team.service.ts b/packages/hoppscotch-backend/src/team/team.service.ts index 225c5fe5..b0d618e7 100644 --- a/packages/hoppscotch-backend/src/team/team.service.ts +++ b/packages/hoppscotch-backend/src/team/team.service.ts @@ -261,10 +261,14 @@ export class TeamService implements UserDataHandler, OnModuleInit { return E.right(team); } - async getTeamsOfUser(uid: string, cursor: string | null): Promise { + async getTeamsOfUser( + uid: string, + cursor: string | null, + take = 10, + ): Promise { if (!cursor) { const entries = await this.prisma.teamMember.findMany({ - take: 10, + take, where: { userUid: uid, }, @@ -276,7 +280,7 @@ export class TeamService implements UserDataHandler, OnModuleInit { return entries.map((entry) => entry.team); } else { const entries = await this.prisma.teamMember.findMany({ - take: 10, + take, skip: 1, cursor: { teamID_userUid: { diff --git a/packages/hoppscotch-sh-admin/locales/en.json b/packages/hoppscotch-sh-admin/locales/en.json index 21029ba4..15c9d1b0 100644 --- a/packages/hoppscotch-sh-admin/locales/en.json +++ b/packages/hoppscotch-sh-admin/locales/en.json @@ -432,6 +432,16 @@ "valid_name": "Please enter a valid workspace name", "valid_owner_email": "Please enter a valid owner email" }, + "user_teams": { + "load_error": "Unable to load workspace memberships", + "no_teams": "This user does not belong to any workspaces", + "role": "Role", + "role_unknown": "Unknown", + "show_more": "Show more", + "title": "Workspaces", + "workspace_id": "Workspace ID", + "workspace_name": "Workspace Name" + }, "users": { "add_user": "Add User", "admin": "Admin", diff --git a/packages/hoppscotch-sh-admin/src/components/users/Teams.vue b/packages/hoppscotch-sh-admin/src/components/users/Teams.vue new file mode 100644 index 00000000..1d4efeda --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/components/users/Teams.vue @@ -0,0 +1,112 @@ + + + diff --git a/packages/hoppscotch-sh-admin/src/helpers/backend/gql/queries/TeamsOfUserByAdmin.graphql b/packages/hoppscotch-sh-admin/src/helpers/backend/gql/queries/TeamsOfUserByAdmin.graphql new file mode 100644 index 00000000..c0643116 --- /dev/null +++ b/packages/hoppscotch-sh-admin/src/helpers/backend/gql/queries/TeamsOfUserByAdmin.graphql @@ -0,0 +1,13 @@ +query TeamsOfUserByAdmin($userUid: ID!, $cursor: ID, $take: Int) { + teamsOfUserByAdmin(userUid: $userUid, cursor: $cursor, take: $take) { + id + name + teamMembers { + membershipID + role + user { + uid + } + } + } +} diff --git a/packages/hoppscotch-sh-admin/src/pages/users/_id.vue b/packages/hoppscotch-sh-admin/src/pages/users/_id.vue index 414e7d05..7881fe12 100644 --- a/packages/hoppscotch-sh-admin/src/pages/users/_id.vue +++ b/packages/hoppscotch-sh-admin/src/pages/users/_id.vue @@ -36,6 +36,13 @@ + + + @@ -62,7 +69,7 @@