Browse Source

feat(backend): add the ability to disable tracking request history (#4594)

HSB-505
Mir Arif Hasan 2 months ago
parent
commit
e30a6c9db5

+ 2 - 0
packages/hoppscotch-backend/src/admin/admin.module.ts

@@ -12,6 +12,7 @@ import { TeamRequestModule } from '../team-request/team-request.module';
 import { InfraResolver } from './infra.resolver';
 import { ShortcodeModule } from 'src/shortcode/shortcode.module';
 import { InfraConfigModule } from 'src/infra-config/infra-config.module';
+import { UserHistoryModule } from 'src/user-history/user-history.module';
 
 @Module({
   imports: [
@@ -25,6 +26,7 @@ import { InfraConfigModule } from 'src/infra-config/infra-config.module';
     TeamRequestModule,
     ShortcodeModule,
     InfraConfigModule,
+    UserHistoryModule,
   ],
   providers: [InfraResolver, AdminResolver, AdminService],
   exports: [AdminService],

+ 12 - 3
packages/hoppscotch-backend/src/admin/admin.resolver.ts

@@ -117,9 +117,8 @@ export class AdminResolver {
     })
     userUIDs: string[],
   ): Promise<UserDeletionResult[]> {
-    const deletionResults = await this.adminService.removeUserAccounts(
-      userUIDs,
-    );
+    const deletionResults =
+      await this.adminService.removeUserAccounts(userUIDs);
     if (E.isLeft(deletionResults)) throwErr(deletionResults.left);
     return deletionResults.right;
   }
@@ -360,6 +359,16 @@ export class AdminResolver {
     return true;
   }
 
+  @Mutation(() => Boolean, {
+    description: 'Revoke all User History',
+  })
+  @UseGuards(GqlAuthGuard, GqlAdminGuard)
+  async revokeAllUserHistoryByAdmin(): Promise<boolean> {
+    const isDeleted = await this.adminService.deleteAllUserHistory();
+    if (E.isLeft(isDeleted)) throwErr(isDeleted.left);
+    return true;
+  }
+
   /* Subscriptions */
 
   @Subscription(() => InvitedUser, {

+ 14 - 0
packages/hoppscotch-backend/src/admin/admin.service.spec.ts

@@ -22,6 +22,7 @@ import { ShortcodeService } from 'src/shortcode/shortcode.service';
 import { ConfigService } from '@nestjs/config';
 import { OffsetPaginationArgs } from 'src/types/input-types.args';
 import * as E from 'fp-ts/Either';
+import { UserHistoryService } from 'src/user-history/user-history.service';
 
 const mockPrisma = mockDeep<PrismaService>();
 const mockPubSub = mockDeep<PubSubService>();
@@ -34,6 +35,7 @@ const mockTeamCollectionService = mockDeep<TeamCollectionService>();
 const mockMailerService = mockDeep<MailerService>();
 const mockShortcodeService = mockDeep<ShortcodeService>();
 const mockConfigService = mockDeep<ConfigService>();
+const mockUserHistoryService = mockDeep<UserHistoryService>();
 
 const adminService = new AdminService(
   mockUserService,
@@ -47,6 +49,7 @@ const adminService = new AdminService(
   mockMailerService,
   mockShortcodeService,
   mockConfigService,
+  mockUserHistoryService,
 );
 
 const invitedUsers: InvitedUsers[] = [
@@ -292,4 +295,15 @@ describe('AdminService', () => {
       expect(result).toEqual(10);
     });
   });
+
+  describe('deleteAllUserHistory', () => {
+    test('should resolve right and delete all user history', async () => {
+      mockUserHistoryService.deleteAllHistories.mockResolvedValueOnce(
+        E.right(true),
+      );
+
+      const result = await adminService.deleteAllUserHistory();
+      expect(result).toEqualRight(true);
+    });
+  });
 });

+ 13 - 0
packages/hoppscotch-backend/src/admin/admin.service.ts

@@ -31,6 +31,7 @@ import { ShortcodeService } from 'src/shortcode/shortcode.service';
 import { ConfigService } from '@nestjs/config';
 import { OffsetPaginationArgs } from 'src/types/input-types.args';
 import { UserDeletionResult } from 'src/user/user.model';
+import { UserHistoryService } from 'src/user-history/user-history.service';
 
 @Injectable()
 export class AdminService {
@@ -46,6 +47,7 @@ export class AdminService {
     private readonly mailerService: MailerService,
     private readonly shortcodeService: ShortcodeService,
     private readonly configService: ConfigService,
+    private readonly userHistoryService: UserHistoryService,
   ) {}
 
   /**
@@ -650,4 +652,15 @@ export class AdminService {
     if (E.isLeft(result)) return E.left(result.left);
     return E.right(result.right);
   }
+
+  /**
+   * Delete all user history
+   * @returns Boolean on successful deletion
+   */
+  async deleteAllUserHistory() {
+    const result = await this.userHistoryService.deleteAllHistories();
+
+    if (E.isLeft(result)) return E.left(result.left);
+    return E.right(result.right);
+  }
 }

+ 24 - 7
packages/hoppscotch-backend/src/admin/infra.resolver.ts

@@ -214,9 +214,8 @@ export class InfraResolver {
     })
     teamID: string,
   ) {
-    const invitations = await this.adminService.pendingInvitationCountInTeam(
-      teamID,
-    );
+    const invitations =
+      await this.adminService.pendingInvitationCountInTeam(teamID);
     return invitations;
   }
 
@@ -352,9 +351,8 @@ export class InfraResolver {
     })
     providerInfo: EnableAndDisableSSOArgs[],
   ) {
-    const isUpdated = await this.infraConfigService.enableAndDisableSSO(
-      providerInfo,
-    );
+    const isUpdated =
+      await this.infraConfigService.enableAndDisableSSO(providerInfo);
     if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
 
     return true;
@@ -372,7 +370,26 @@ export class InfraResolver {
     })
     status: ServiceStatus,
   ) {
-    const isUpdated = await this.infraConfigService.enableAndDisableSMTP(
+    const isUpdated =
+      await this.infraConfigService.enableAndDisableSMTP(status);
+    if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
+    return true;
+  }
+
+  @Mutation(() => Boolean, {
+    description: 'Enable or Disable User History Storing in DB',
+  })
+  @UseGuards(GqlAuthGuard, GqlAdminGuard)
+  async toggleUserHistoryStore(
+    @Args({
+      name: 'status',
+      type: () => ServiceStatus,
+      description: 'Toggle User History Store',
+    })
+    status: ServiceStatus,
+  ) {
+    const isUpdated = await this.infraConfigService.toggleServiceStatus(
+      InfraConfigEnum.USER_HISTORY_STORE_ENABLED,
       status,
     );
     if (E.isLeft(isUpdated)) throwErr(isUpdated.left);

+ 13 - 1
packages/hoppscotch-backend/src/errors.ts

@@ -492,7 +492,19 @@ export const USER_ENVIRONMENT_INVALID_ENVIRONMENT_NAME =
  */
 export const USER_HISTORY_NOT_FOUND = 'user_history/history_not_found' as const;
 
-/*
+/**
+ * User history deletion failed
+ * (UserHistoryService)
+ */
+export const USER_HISTORY_DELETION_FAILED =
+  'user_history/deletion_failed' as const;
+
+/**
+ * User history feature flag is disabled
+ * (UserHistoryService)
+ */
+export const USER_HISTORY_FEATURE_FLAG_DISABLED =
+  'user_history/feature_flag_disabled';
 
 /**
  * Invalid Request Type in History

+ 6 - 0
packages/hoppscotch-backend/src/infra-config/helper.ts

@@ -262,6 +262,12 @@ export async function getDefaultInfraConfigs(): Promise<DefaultInfraConfig[]> {
       lastSyncedEnvFileValue: null,
       isEncrypted: false,
     },
+    {
+      name: InfraConfigEnum.USER_HISTORY_STORE_ENABLED,
+      value: 'true',
+      lastSyncedEnvFileValue: null,
+      isEncrypted: false,
+    },
   ];
 
   return infraConfigDefaultObjs;

+ 2 - 1
packages/hoppscotch-backend/src/infra-config/infra-config.module.ts

@@ -3,9 +3,10 @@ import { InfraConfigService } from './infra-config.service';
 import { PrismaModule } from 'src/prisma/prisma.module';
 import { SiteController } from './infra-config.controller';
 import { InfraConfigResolver } from './infra-config.resolver';
+import { PubSubModule } from 'src/pubsub/pubsub.module';
 
 @Module({
-  imports: [PrismaModule],
+  imports: [PrismaModule, PubSubModule],
   providers: [InfraConfigResolver, InfraConfigService],
   exports: [InfraConfigService],
   controllers: [SiteController],

+ 41 - 2
packages/hoppscotch-backend/src/infra-config/infra-config.resolver.ts

@@ -1,14 +1,24 @@
 import { UseGuards } from '@nestjs/common';
-import { Query, Resolver } from '@nestjs/graphql';
+import { Args, Query, Resolver, Subscription } from '@nestjs/graphql';
 import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
 import { InfraConfig } from './infra-config.model';
 import { InfraConfigService } from './infra-config.service';
 import { GqlAuthGuard } from 'src/guards/gql-auth.guard';
+import { SkipThrottle } from '@nestjs/throttler';
+import { PubSubService } from 'src/pubsub/pubsub.service';
+import { InfraConfigEnum } from 'src/types/InfraConfig';
+import * as E from 'fp-ts/Either';
+import { throwErr } from 'src/utils';
 
 @UseGuards(GqlThrottlerGuard)
 @Resolver(() => InfraConfig)
 export class InfraConfigResolver {
-  constructor(private infraConfigService: InfraConfigService) {}
+  constructor(
+    private infraConfigService: InfraConfigService,
+    private pubsub: PubSubService,
+  ) {}
+
+  /* Query */
 
   @Query(() => Boolean, {
     description: 'Check if the SMTP is enabled or not',
@@ -17,4 +27,33 @@ export class InfraConfigResolver {
   isSMTPEnabled() {
     return this.infraConfigService.isSMTPEnabled();
   }
+
+  @Query(() => InfraConfig, {
+    description: 'Check if user history is enabled or not',
+  })
+  @UseGuards(GqlAuthGuard)
+  async isUserHistoryEnabled() {
+    const isEnabled = await this.infraConfigService.isUserHistoryEnabled();
+    if (E.isLeft(isEnabled)) throwErr(isEnabled.left);
+    return isEnabled.right;
+  }
+
+  /* Subscriptions */
+
+  @Subscription(() => String, {
+    description: 'Subscription for infra config update',
+    resolve: (value) => value,
+  })
+  @SkipThrottle()
+  @UseGuards(GqlAuthGuard)
+  infraConfigUpdate(
+    @Args({
+      name: 'configName',
+      description: 'Infra config key',
+      type: () => InfraConfigEnum,
+    })
+    configName: string,
+  ) {
+    return this.pubsub.asyncIterator(`infra_config/${configName}/updated`);
+  }
 }

+ 60 - 0
packages/hoppscotch-backend/src/infra-config/infra-config.service.spec.ts

@@ -11,15 +11,20 @@ import { ConfigService } from '@nestjs/config';
 import * as helper from './helper';
 import { InfraConfig as dbInfraConfig } from '@prisma/client';
 import { InfraConfig } from './infra-config.model';
+import { PubSubService } from 'src/pubsub/pubsub.service';
+import { ServiceStatus } from './helper';
+import * as E from 'fp-ts/Either';
 
 const mockPrisma = mockDeep<PrismaService>();
 const mockConfigService = mockDeep<ConfigService>();
+const mockPubsub = mockDeep<PubSubService>();
 
 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
 // @ts-ignore
 const infraConfigService = new InfraConfigService(
   mockPrisma,
   mockConfigService,
+  mockPubsub,
 );
 
 const INITIALIZED_DATE_CONST = new Date();
@@ -243,4 +248,59 @@ describe('InfraConfigService', () => {
       );
     });
   });
+
+  describe('toggleServiceStatus', () => {
+    it('should toggle the service status', async () => {
+      const configName = infraConfigs[0].name;
+      const configStatus = ServiceStatus.DISABLE;
+
+      jest
+        .spyOn(infraConfigService, 'update')
+        .mockResolvedValueOnce(
+          E.right({ name: configName, value: configStatus }),
+        );
+
+      expect(
+        await infraConfigService.toggleServiceStatus(configName, configStatus),
+      ).toEqualRight(true);
+    });
+    it('should publish the updated config value', async () => {
+      const configName = infraConfigs[0].name;
+      const configStatus = ServiceStatus.DISABLE;
+
+      jest
+        .spyOn(infraConfigService, 'update')
+        .mockResolvedValueOnce(
+          E.right({ name: configName, value: configStatus }),
+        );
+
+      await infraConfigService.toggleServiceStatus(configName, configStatus);
+
+      expect(mockPubsub.publish).toHaveBeenCalledTimes(1);
+      expect(mockPubsub.publish).toHaveBeenCalledWith(
+        'infra_config/GOOGLE_CLIENT_ID/updated',
+        configStatus,
+      );
+    });
+  });
+
+  describe('isUserHistoryEnabled', () => {
+    it('should return true if the user history is enabled', async () => {
+      const response = {
+        name: InfraConfigEnum.USER_HISTORY_STORE_ENABLED,
+        value: ServiceStatus.ENABLE,
+      };
+
+      jest.spyOn(infraConfigService, 'get').mockResolvedValueOnce(
+        E.right({
+          name: InfraConfigEnum.USER_HISTORY_STORE_ENABLED,
+          value: ServiceStatus.ENABLE,
+        }),
+      );
+
+      expect(await infraConfigService.isUserHistoryEnabled()).toEqualRight(
+        response,
+      );
+    });
+  });
 });

Some files were not shown because too many files changed in this diff