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>
This commit is contained in:
Mir Arif Hasan
2024-06-21 13:10:21 +06:00
committed by GitHub
parent cfb77f2bfe
commit c2085b8b6f
8 changed files with 100 additions and 5 deletions

View File

@@ -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:

View File

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

View File

@@ -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[]

View File

@@ -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 {}

View File

@@ -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);
}),
);
}
}

View File

@@ -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);

View File

@@ -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',
})

View File

@@ -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