Browse Source

HSB-450 feature: user last active on (#4121)

* feat: userLastActiveOnInterceptor added and update func added in userService

* chore: changed user model parameter description

* chore: commented out docker volume for hopp-old-service

* chore: changed backend to work with secure cookies

---------

Co-authored-by: Balu Babu <balub997@gmail.com>
Mir Arif Hasan 8 months ago
parent
commit
c2085b8b6f

+ 2 - 2
docker-compose.yml

@@ -112,7 +112,7 @@ services:
     build:
       dockerfile: packages/hoppscotch-backend/Dockerfile
       context: .
-      target: dev
+      target: prod
     env_file:
       - ./.env
     restart: always
@@ -122,7 +122,7 @@ services:
       - PORT=3000
     volumes:
       # Uncomment the line below when modifying code. Only applicable when using the "dev" target.
-      - ./packages/hoppscotch-backend/:/usr/src/app
+      # - ./packages/hoppscotch-backend/:/usr/src/app
       - /usr/src/app/node_modules/
     depends_on:
       hoppscotch-db:

+ 2 - 0
packages/hoppscotch-backend/prisma/migrations/20240621062554_user_last_active_on/migration.sql

@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "User" ADD COLUMN     "lastActiveOn" TIMESTAMP(3);

+ 2 - 1
packages/hoppscotch-backend/prisma/schema.prisma

@@ -104,7 +104,8 @@ model User {
   userRequests         UserRequest[]
   currentRESTSession   Json?
   currentGQLSession    Json?
-  lastLoggedOn         DateTime?
+  lastLoggedOn         DateTime?             @db.Timestamp(3)
+  lastActiveOn         DateTime?             @db.Timestamp(3)
   createdOn            DateTime              @default(now()) @db.Timestamp(3)
   invitedUsers         InvitedUsers[]
   shortcodes           Shortcode[]

+ 5 - 1
packages/hoppscotch-backend/src/app.module.ts

@@ -28,6 +28,7 @@ import { PosthogModule } from './posthog/posthog.module';
 import { ScheduleModule } from '@nestjs/schedule';
 import { HealthModule } from './health/health.module';
 import { AccessTokenModule } from './access-token/access-token.module';
+import { UserLastActiveOnInterceptor } from './interceptors/user-last-active-on.interceptor';
 
 @Module({
   imports: [
@@ -105,7 +106,10 @@ import { AccessTokenModule } from './access-token/access-token.module';
     HealthModule,
     AccessTokenModule,
   ],
-  providers: [GQLComplexityPlugin],
+  providers: [
+    GQLComplexityPlugin,
+    { provide: 'APP_INTERCEPTOR', useClass: UserLastActiveOnInterceptor },
+  ],
   controllers: [AppController],
 })
 export class AppModule {}

+ 67 - 0
packages/hoppscotch-backend/src/interceptors/user-last-active-on.interceptor.ts

@@ -0,0 +1,67 @@
+import {
+  Injectable,
+  NestInterceptor,
+  ExecutionContext,
+  CallHandler,
+} from '@nestjs/common';
+import { GqlContextType, GqlExecutionContext } from '@nestjs/graphql';
+import { Observable, throwError } from 'rxjs';
+import { catchError, tap } from 'rxjs/operators';
+import { AuthUser } from 'src/types/AuthUser';
+import { UserService } from 'src/user/user.service';
+
+@Injectable()
+export class UserLastActiveOnInterceptor implements NestInterceptor {
+  constructor(private userService: UserService) {}
+
+  user: AuthUser; // 'user', who executed the request
+
+  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
+    if (context.getType() === 'http') {
+      return this.restHandler(context, next);
+    } else if (context.getType<GqlContextType>() === 'graphql') {
+      return this.graphqlHandler(context, next);
+    }
+  }
+
+  restHandler(context: ExecutionContext, next: CallHandler): Observable<any> {
+    const request = context.switchToHttp().getRequest();
+    this.user = request.user;
+
+    return next.handle().pipe(
+      tap(() => {
+        if (this.user && typeof this.user === 'object') {
+          this.userService.updateUserLastActiveOn(this.user.uid);
+        }
+      }),
+      catchError((error) => {
+        if (this.user && typeof this.user === 'object') {
+          this.userService.updateUserLastActiveOn(this.user.uid);
+        }
+        return throwError(() => error);
+      }),
+    );
+  }
+
+  graphqlHandler(
+    context: ExecutionContext,
+    next: CallHandler,
+  ): Observable<any> {
+    const contextObject = GqlExecutionContext.create(context).getContext();
+    this.user = contextObject.req.user;
+
+    return next.handle().pipe(
+      tap(() => {
+        if (this.user && typeof this.user === 'object') {
+          this.userService.updateUserLastActiveOn(this.user.uid);
+        }
+      }),
+      catchError((error) => {
+        if (this.user && typeof this.user === 'object') {
+          this.userService.updateUserLastActiveOn(this.user.uid);
+        }
+        return throwError(() => error);
+      }),
+    );
+  }
+}

+ 0 - 1
packages/hoppscotch-backend/src/interceptors/user-last-login.interceptor.ts

@@ -16,7 +16,6 @@ export class UserLastLoginInterceptor implements NestInterceptor {
   intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
     const user: AuthUser = context.switchToHttp().getRequest().user;
 
-    const now = Date.now();
     return next.handle().pipe(
       tap(() => {
         this.userService.updateUserLastLoggedOn(user.uid);

+ 6 - 0
packages/hoppscotch-backend/src/user/user.model.ts

@@ -36,6 +36,12 @@ export class User {
   })
   lastLoggedOn: Date;
 
+  @Field({
+    nullable: true,
+    description: 'Date when the user last interacted with the app',
+  })
+  lastActiveOn: Date;
+
   @Field({
     description: 'Date when the user account was created',
   })

+ 16 - 0
packages/hoppscotch-backend/src/user/user.service.ts

@@ -334,6 +334,22 @@ export class UserService {
     }
   }
 
+  /**
+   * Update user's lastActiveOn timestamp
+   * @param userUID User UID
+   */
+  async updateUserLastActiveOn(userUid: string) {
+    try {
+      await this.prisma.user.update({
+        where: { uid: userUid },
+        data: { lastActiveOn: new Date() },
+      });
+      return E.right(true);
+    } catch (e) {
+      return E.left(USER_NOT_FOUND);
+    }
+  }
+
   /**
    * Validate and parse currentRESTSession and currentGQLSession
    * @param sessionData string of the session