diff --git a/docker-compose.yml b/docker-compose.yml index 14efa4527..7bae7928b 100644 --- a/docker-compose.yml +++ b/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: diff --git a/packages/hoppscotch-backend/prisma/migrations/20240621062554_user_last_active_on/migration.sql b/packages/hoppscotch-backend/prisma/migrations/20240621062554_user_last_active_on/migration.sql new file mode 100644 index 000000000..9e0ff1c93 --- /dev/null +++ b/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); diff --git a/packages/hoppscotch-backend/prisma/schema.prisma b/packages/hoppscotch-backend/prisma/schema.prisma index 50b29ea99..7a3fad4bf 100644 --- a/packages/hoppscotch-backend/prisma/schema.prisma +++ b/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[] diff --git a/packages/hoppscotch-backend/src/app.module.ts b/packages/hoppscotch-backend/src/app.module.ts index 3323fc3a0..a4c62cf11 100644 --- a/packages/hoppscotch-backend/src/app.module.ts +++ b/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 {} diff --git a/packages/hoppscotch-backend/src/interceptors/user-last-active-on.interceptor.ts b/packages/hoppscotch-backend/src/interceptors/user-last-active-on.interceptor.ts new file mode 100644 index 000000000..11a79e3f3 --- /dev/null +++ b/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 { + if (context.getType() === 'http') { + return this.restHandler(context, next); + } else if (context.getType() === 'graphql') { + return this.graphqlHandler(context, next); + } + } + + restHandler(context: ExecutionContext, next: CallHandler): Observable { + 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 { + 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); + }), + ); + } +} diff --git a/packages/hoppscotch-backend/src/interceptors/user-last-login.interceptor.ts b/packages/hoppscotch-backend/src/interceptors/user-last-login.interceptor.ts index ae204a1dc..db019d9e6 100644 --- a/packages/hoppscotch-backend/src/interceptors/user-last-login.interceptor.ts +++ b/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 { const user: AuthUser = context.switchToHttp().getRequest().user; - const now = Date.now(); return next.handle().pipe( tap(() => { this.userService.updateUserLastLoggedOn(user.uid); diff --git a/packages/hoppscotch-backend/src/user/user.model.ts b/packages/hoppscotch-backend/src/user/user.model.ts index df3c33db7..ffe1e6b62 100644 --- a/packages/hoppscotch-backend/src/user/user.model.ts +++ b/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', }) diff --git a/packages/hoppscotch-backend/src/user/user.service.ts b/packages/hoppscotch-backend/src/user/user.service.ts index ff2ab5980..f28b4167e 100644 --- a/packages/hoppscotch-backend/src/user/user.service.ts +++ b/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