diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index dea99c5ec..000000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "recommendations": [ - "antfu.iconify", - "vue.volar", - "esbenp.prettier-vscode", - "dbaeumer.vscode-eslint", - "editorconfig.editorconfig", - "csstools.postcss", - "folke.vscode-monorepo-workspace" - ], - "unwantedRecommendations": [ - "octref.vetur" - ] -} diff --git a/packages/hoppscotch-backend/src/admin/admin.module.ts b/packages/hoppscotch-backend/src/admin/admin.module.ts index 68e4848c1..273062d10 100644 --- a/packages/hoppscotch-backend/src/admin/admin.module.ts +++ b/packages/hoppscotch-backend/src/admin/admin.module.ts @@ -11,6 +11,7 @@ import { TeamEnvironmentsModule } from '../team-environments/team-environments.m import { TeamCollectionModule } from '../team-collection/team-collection.module'; import { TeamRequestModule } from '../team-request/team-request.module'; import { InfraResolver } from './infra.resolver'; +import { ShortcodeModule } from 'src/shortcode/shortcode.module'; @Module({ imports: [ @@ -23,6 +24,7 @@ import { InfraResolver } from './infra.resolver'; TeamEnvironmentsModule, TeamCollectionModule, TeamRequestModule, + ShortcodeModule, ], providers: [InfraResolver, AdminResolver, AdminService], exports: [AdminService], diff --git a/packages/hoppscotch-backend/src/admin/admin.resolver.ts b/packages/hoppscotch-backend/src/admin/admin.resolver.ts index ebf1c08df..deeb90dbe 100644 --- a/packages/hoppscotch-backend/src/admin/admin.resolver.ts +++ b/packages/hoppscotch-backend/src/admin/admin.resolver.ts @@ -443,6 +443,23 @@ export class AdminResolver { return true; } + @Mutation(() => Boolean, { + description: 'Revoke Shortcode by ID', + }) + @UseGuards(GqlAuthGuard, GqlAdminGuard) + async revokeShortcodeByAdmin( + @Args({ + name: 'code', + description: 'The shortcode to delete', + type: () => ID, + }) + code: string, + ): Promise { + const res = await this.adminService.deleteShortcode(code); + if (E.isLeft(res)) throwErr(res.left); + return true; + } + /* Subscriptions */ @Subscription(() => InvitedUser, { diff --git a/packages/hoppscotch-backend/src/admin/admin.service.spec.ts b/packages/hoppscotch-backend/src/admin/admin.service.spec.ts index 7242997bd..9682905e5 100644 --- a/packages/hoppscotch-backend/src/admin/admin.service.spec.ts +++ b/packages/hoppscotch-backend/src/admin/admin.service.spec.ts @@ -15,6 +15,7 @@ import { INVALID_EMAIL, USER_ALREADY_INVITED, } from '../errors'; +import { ShortcodeService } from 'src/shortcode/shortcode.service'; const mockPrisma = mockDeep(); const mockPubSub = mockDeep(); @@ -25,6 +26,7 @@ const mockTeamRequestService = mockDeep(); const mockTeamInvitationService = mockDeep(); const mockTeamCollectionService = mockDeep(); const mockMailerService = mockDeep(); +const mockShortcodeService = mockDeep(); const adminService = new AdminService( mockUserService, @@ -36,6 +38,7 @@ const adminService = new AdminService( mockPubSub as any, mockPrisma as any, mockMailerService, + mockShortcodeService, ); const invitedUsers: InvitedUsers[] = [ diff --git a/packages/hoppscotch-backend/src/admin/admin.service.ts b/packages/hoppscotch-backend/src/admin/admin.service.ts index 98c5d254b..c08be5f97 100644 --- a/packages/hoppscotch-backend/src/admin/admin.service.ts +++ b/packages/hoppscotch-backend/src/admin/admin.service.ts @@ -24,6 +24,7 @@ import { TeamRequestService } from '../team-request/team-request.service'; import { TeamEnvironmentsService } from '../team-environments/team-environments.service'; import { TeamInvitationService } from '../team-invitation/team-invitation.service'; import { TeamMemberRole } from '../team/team.model'; +import { ShortcodeService } from 'src/shortcode/shortcode.service'; @Injectable() export class AdminService { @@ -37,6 +38,7 @@ export class AdminService { private readonly pubsub: PubSubService, private readonly prisma: PrismaService, private readonly mailerService: MailerService, + private readonly shortcodeService: ShortcodeService, ) {} /** @@ -432,4 +434,35 @@ export class AdminService { return E.right(teamInvite.right); } + + /** + * Fetch all created ShortCodes + * + * @param args Pagination arguments + * @param userEmail User email + * @returns ShortcodeWithUserEmail + */ + async fetchAllShortcodes( + cursorID: string, + take: number, + userEmail: string = null, + ) { + return this.shortcodeService.fetchAllShortcodes( + { cursor: cursorID, take }, + userEmail, + ); + } + + /** + * Delete a Shortcode + * + * @param shortcodeID ID of Shortcode being deleted + * @returns Boolean on successful deletion + */ + async deleteShortcode(shortcodeID: string) { + const result = await this.shortcodeService.deleteShortcode(shortcodeID); + + if (E.isLeft(result)) return E.left(result.left); + return E.right(result.right); + } } diff --git a/packages/hoppscotch-backend/src/admin/infra.resolver.ts b/packages/hoppscotch-backend/src/admin/infra.resolver.ts index e0a63a788..40b438a9e 100644 --- a/packages/hoppscotch-backend/src/admin/infra.resolver.ts +++ b/packages/hoppscotch-backend/src/admin/infra.resolver.ts @@ -15,6 +15,7 @@ import { InvitedUser } from './invited-user.model'; import { Team } from 'src/team/team.model'; import { TeamInvitation } from 'src/team-invitation/team-invitation.model'; import { GqlAdmin } from './decorators/gql-admin.decorator'; +import { ShortcodeWithUserEmail } from 'src/shortcode/shortcode.model'; @UseGuards(GqlThrottlerGuard) @Resolver(() => Infra) @@ -202,4 +203,23 @@ export class InfraResolver { async teamRequestsCount() { return this.adminService.getTeamRequestsCount(); } + + @ResolveField(() => [ShortcodeWithUserEmail], { + description: 'Returns a list of all the shortcodes in the infra', + }) + async allShortcodes( + @Args() args: PaginationArgs, + @Args({ + name: 'userEmail', + nullable: true, + description: 'Users email to filter shortcodes by', + }) + userEmail: string, + ) { + return await this.adminService.fetchAllShortcodes( + args.cursor, + args.take, + userEmail, + ); + } } diff --git a/packages/hoppscotch-backend/src/shortcode/shortcode.model.ts b/packages/hoppscotch-backend/src/shortcode/shortcode.model.ts index 47828ff79..40dc28b2e 100644 --- a/packages/hoppscotch-backend/src/shortcode/shortcode.model.ts +++ b/packages/hoppscotch-backend/src/shortcode/shortcode.model.ts @@ -1,4 +1,5 @@ import { Field, ID, ObjectType } from '@nestjs/graphql'; +import { User } from 'src/user/user.model'; @ObjectType() export class Shortcode { @@ -23,3 +24,46 @@ export class Shortcode { }) createdOn: Date; } + +@ObjectType() +export class ShortcodeCreator { + @Field({ + description: 'Uid of user who created the shortcode', + }) + uid: string; + + @Field({ + description: 'Email of user who created the shortcode', + }) + email: string; +} + +@ObjectType() +export class ShortcodeWithUserEmail { + @Field(() => ID, { + description: 'The 12 digit alphanumeric code', + }) + id: string; + + @Field({ + description: 'JSON string representing the request data', + }) + request: string; + + @Field({ + description: 'JSON string representing the properties for an embed', + nullable: true, + }) + properties: string; + + @Field({ + description: 'Timestamp of when the Shortcode was created', + }) + createdOn: Date; + + @Field({ + description: 'Details of user who created the shortcode', + nullable: true, + }) + creator: ShortcodeCreator; +} diff --git a/packages/hoppscotch-backend/src/shortcode/shortcode.resolver.ts b/packages/hoppscotch-backend/src/shortcode/shortcode.resolver.ts index df0158e1e..7c1069633 100644 --- a/packages/hoppscotch-backend/src/shortcode/shortcode.resolver.ts +++ b/packages/hoppscotch-backend/src/shortcode/shortcode.resolver.ts @@ -8,7 +8,7 @@ import { } from '@nestjs/graphql'; import * as E from 'fp-ts/Either'; import { UseGuards } from '@nestjs/common'; -import { Shortcode } from './shortcode.model'; +import { Shortcode, ShortcodeWithUserEmail } from './shortcode.model'; import { ShortcodeService } from './shortcode.service'; import { throwErr } from 'src/utils'; import { GqlUser } from 'src/decorators/gql-user.decorator'; @@ -19,6 +19,7 @@ import { AuthUser } from '../types/AuthUser'; import { PaginationArgs } from 'src/types/input-types.args'; import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard'; import { SkipThrottle } from '@nestjs/throttler'; +import { GqlAdminGuard } from 'src/admin/guards/gql-admin.guard'; @UseGuards(GqlThrottlerGuard) @Resolver(() => Shortcode) @@ -121,7 +122,7 @@ export class ShortcodeResolver { @Args({ name: 'code', type: () => ID, - description: 'The shortcode to resolve', + description: 'The shortcode to remove', }) code: string, ) { diff --git a/packages/hoppscotch-backend/src/shortcode/shortcode.service.spec.ts b/packages/hoppscotch-backend/src/shortcode/shortcode.service.spec.ts index 27ab9f5f1..cf35f4f06 100644 --- a/packages/hoppscotch-backend/src/shortcode/shortcode.service.spec.ts +++ b/packages/hoppscotch-backend/src/shortcode/shortcode.service.spec.ts @@ -1,12 +1,13 @@ import { mockDeep, mockReset } from 'jest-mock-extended'; import { PrismaService } from '../prisma/prisma.service'; import { + INVALID_EMAIL, SHORTCODE_INVALID_PROPERTIES_JSON, SHORTCODE_INVALID_REQUEST_JSON, SHORTCODE_NOT_FOUND, SHORTCODE_PROPERTIES_NOT_FOUND, } from 'src/errors'; -import { Shortcode } from './shortcode.model'; +import { Shortcode, ShortcodeWithUserEmail } from './shortcode.model'; import { ShortcodeService } from './shortcode.service'; import { UserService } from 'src/user/user.service'; import { AuthUser } from 'src/types/AuthUser'; @@ -97,6 +98,35 @@ const shortcodes = [ }, ]; +const shortcodesWithUserEmail = [ + { + id: 'blablabla', + request: { + hello: 'there', + }, + embedProperties: { + foo: 'bar', + }, + creatorUid: user.uid, + createdOn: new Date(), + updatedOn: createdOn, + User: user, + }, + { + id: 'blablabla1', + request: { + hello: 'there', + }, + embedProperties: { + foo: 'bar', + }, + creatorUid: user.uid, + createdOn: new Date(), + updatedOn: createdOn, + User: user, + }, +]; + describe('ShortcodeService', () => { describe('getShortCode', () => { test('should return a valid Shortcode with valid Shortcode ID', async () => { @@ -441,4 +471,99 @@ describe('ShortcodeService', () => { ); }); }); + + describe('deleteShortcode', () => { + test('should return true on successful deletion of Shortcode with valid inputs', async () => { + mockPrisma.shortcode.delete.mockResolvedValueOnce(mockEmbed); + + const result = await shortcodeService.deleteShortcode(mockEmbed.id); + expect(result).toEqualRight(true); + }); + + test('should return SHORTCODE_NOT_FOUND error when Shortcode is invalid', async () => { + mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound'); + + expect(shortcodeService.deleteShortcode('invalid')).resolves.toEqualLeft( + SHORTCODE_NOT_FOUND, + ); + }); + }); + + describe('fetchAllShortcodes', () => { + test('should return list of Shortcodes with valid inputs and no cursor', async () => { + mockPrisma.shortcode.findMany.mockResolvedValueOnce( + shortcodesWithUserEmail, + ); + + const result = await shortcodeService.fetchAllShortcodes( + { + cursor: null, + take: 10, + }, + user.email, + ); + expect(result).toEqual([ + { + id: shortcodes[0].id, + request: JSON.stringify(shortcodes[0].request), + properties: JSON.stringify(shortcodes[0].embedProperties), + createdOn: shortcodes[0].createdOn, + creator: { + uid: user.uid, + email: user.email, + }, + }, + { + id: shortcodes[1].id, + request: JSON.stringify(shortcodes[1].request), + properties: JSON.stringify(shortcodes[1].embedProperties), + createdOn: shortcodes[1].createdOn, + creator: { + uid: user.uid, + email: user.email, + }, + }, + ]); + }); + + test('should return list of Shortcode with valid inputs and cursor', async () => { + mockPrisma.shortcode.findMany.mockResolvedValue([ + shortcodesWithUserEmail[1], + ]); + + const result = await shortcodeService.fetchAllShortcodes( + { + cursor: 'blablabla', + take: 10, + }, + user.email, + ); + expect(result).toEqual([ + { + id: shortcodes[1].id, + request: JSON.stringify(shortcodes[1].request), + properties: JSON.stringify(shortcodes[1].embedProperties), + createdOn: shortcodes[1].createdOn, + creator: { + uid: user.uid, + email: user.email, + }, + }, + ]); + }); + + test('should return an empty array for an invalid cursor', async () => { + mockPrisma.shortcode.findMany.mockResolvedValue([]); + + const result = await shortcodeService.fetchAllShortcodes( + { + cursor: 'invalidcursor', + take: 10, + }, + user.email, + ); + + expect(result).toHaveLength(0); + }); + }); }); diff --git a/packages/hoppscotch-backend/src/shortcode/shortcode.service.ts b/packages/hoppscotch-backend/src/shortcode/shortcode.service.ts index cb6537239..e6a02fdb1 100644 --- a/packages/hoppscotch-backend/src/shortcode/shortcode.service.ts +++ b/packages/hoppscotch-backend/src/shortcode/shortcode.service.ts @@ -10,7 +10,7 @@ import { SHORTCODE_PROPERTIES_NOT_FOUND, } from 'src/errors'; import { UserDataHandler } from 'src/user/user.data.handler'; -import { Shortcode } from './shortcode.model'; +import { Shortcode, ShortcodeWithUserEmail } from './shortcode.model'; import { Shortcode as DBShortCode } from '@prisma/client'; import { PubSubService } from 'src/pubsub/pubsub.service'; import { UserService } from 'src/user/user.service'; @@ -180,7 +180,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit { } /** - * Delete a ShortCode + * Delete a ShortCode created by User of uid * * @param shortcode ShortCode * @param uid User Uid @@ -223,6 +223,26 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit { return deletedShortCodes.count; } + /** + * Delete a Shortcode + * + * @param shortcodeID ID of Shortcode being deleted + * @returns Boolean on successful deletion + */ + async deleteShortcode(shortcodeID: string) { + try { + await this.prisma.shortcode.delete({ + where: { + id: shortcodeID, + }, + }); + + return E.right(true); + } catch (error) { + return E.left(SHORTCODE_NOT_FOUND); + } + } + /** * Update a created Shortcode * @param shortcodeID Shortcode ID @@ -263,4 +283,57 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit { return E.left(SHORTCODE_NOT_FOUND); } } + + /** + * Fetch all created ShortCodes + * + * @param args Pagination arguments + * @param userEmail User email + * @returns ShortcodeWithUserEmail + */ + async fetchAllShortcodes( + args: PaginationArgs, + userEmail: string | null = null, + ) { + const shortCodes = await this.prisma.shortcode.findMany({ + where: userEmail + ? { + User: { + email: userEmail, + }, + } + : undefined, + orderBy: { + createdOn: 'desc', + }, + skip: args.cursor ? 1 : 0, + take: args.take, + cursor: args.cursor ? { id: args.cursor } : undefined, + include: { + User: true, + }, + }); + + const fetchedShortCodes: ShortcodeWithUserEmail[] = shortCodes.map( + (code) => { + return { + id: code.id, + request: JSON.stringify(code.request), + properties: + code.embedProperties != null + ? JSON.stringify(code.embedProperties) + : null, + createdOn: code.createdOn, + creator: code.User + ? { + uid: code.User.uid, + email: code.User.email, + } + : null, + }; + }, + ); + + return fetchedShortCodes; + } }