From 19975c942a48c0ace312f323985e51104e3f5794 Mon Sep 17 00:00:00 2001 From: shitrerohit Date: Thu, 28 Aug 2025 17:17:35 +0530 Subject: [PATCH 1/3] WIP:delete inactive sessions Signed-off-by: shitrerohit --- apps/api-gateway/src/user/user.controller.ts | 167 +++++++++++-------- apps/api-gateway/src/user/user.service.ts | 24 +-- apps/user/repositories/user.repository.ts | 26 +++ apps/user/src/user.controller.ts | 5 + apps/user/src/user.service.ts | 9 + 5 files changed, 149 insertions(+), 82 deletions(-) diff --git a/apps/api-gateway/src/user/user.controller.ts b/apps/api-gateway/src/user/user.controller.ts index c9f138a52..3ee603c28 100644 --- a/apps/api-gateway/src/user/user.controller.ts +++ b/apps/api-gateway/src/user/user.controller.ts @@ -62,7 +62,7 @@ export class UserController { private readonly userService: UserService, private readonly commonService: CommonService, private readonly awsService: AwsService - ) { } + ) {} /** * @@ -105,9 +105,9 @@ export class UserController { return res.status(HttpStatus.OK).json(finalResponse); } - /** + /** * Get public profile details of a user by username. - * + * * @param username The username of the user. * @returns Public profile information. */ @@ -134,7 +134,6 @@ export class UserController { return res.status(HttpStatus.OK).json(finalResponse); } - /** * Retrieves the profile details of the currently logged-in user. * @@ -149,7 +148,7 @@ export class UserController { @ApiBearerAuth() async getProfile(@User() reqUser: user, @Res() res: Response): Promise { const userData = await this.userService.getProfile(reqUser.id); - + const finalResponse: IResponse = { statusCode: HttpStatus.OK, message: ResponseMessages.user.success.fetchProfile, @@ -159,9 +158,9 @@ export class UserController { return res.status(HttpStatus.OK).json(finalResponse); } - /** + /** * Retrieves all platform settings. - * + * * @returns The platform settings. */ @Get('/platform-settings') @@ -185,11 +184,11 @@ export class UserController { } /** - * Fetch user activities. - * - * @param limit - Number of activities to fetch. - * @returns A response containing user activity data. - */ + * Fetch user activities. + * + * @param limit - Number of activities to fetch. + * @returns A response containing user activity data. + */ @Get('/activity') @ApiOperation({ summary: 'Fetch users activity', @@ -214,12 +213,11 @@ export class UserController { return res.status(HttpStatus.OK).json(finalResponse); } - - /** - * Fetch organization invitations. - * - * @returns A paginated list of organization invitations. - */ + /** + * Fetch organization invitations. + * + * @returns A paginated list of organization invitations. + */ @Get('/org-invitations') @ApiOperation({ summary: 'organization invitations', @@ -248,7 +246,7 @@ export class UserController { required: false }) async invitations( - @Query() getAllInvitationsDto: GetAllInvitationsDto, + @Query() getAllInvitationsDto: GetAllInvitationsDto, @User() reqUser: user, @Res() res: Response ): Promise { @@ -271,14 +269,17 @@ export class UserController { return res.status(HttpStatus.OK).json(finalResponse); } - /** - * Checks if a user is registered and verifies email existence. - * - * @param email The email address to check. - * @returns Returns user registration and email verification status. - */ + /** + * Checks if a user is registered and verifies email existence. + * + * @param email The email address to check. + * @returns Returns user registration and email verification status. + */ @Get('/:email') - @ApiOperation({ summary: 'Check user registration and email verification status', description: 'Check if a user is already registered and if their email already exists.' }) + @ApiOperation({ + summary: 'Check user registration and email verification status', + description: 'Check if a user is already registered and if their email already exists.' + }) async checkUserExist(@Param() emailParam: EmailValidator, @Res() res: Response): Promise { const userDetails = await this.userService.checkUserExist(emailParam.email); @@ -292,12 +293,12 @@ export class UserController { } /** - * Accept or reject an organization invitation. - * - * @param invitationId The ID of the organization invitation. - * @body AcceptRejectInvitationDto - * @returns The status of the organization invitation response. - */ + * Accept or reject an organization invitation. + * + * @param invitationId The ID of the organization invitation. + * @body AcceptRejectInvitationDto + * @returns The status of the organization invitation response. + */ @Post('/org-invitations/:invitationId') @ApiOperation({ summary: 'accept/reject organization invitation', @@ -306,8 +307,17 @@ export class UserController { @UseGuards(AuthGuard('jwt'), UserAccessGuard) @ApiBearerAuth() async acceptRejectInvitaion( - @Body() acceptRejectInvitation: AcceptRejectInvitationDto, - @Param('invitationId', TrimStringParamPipe, new ParseUUIDPipe({exceptionFactory: (): Error => { throw new BadRequestException(`Invalid format for InvitationId`); }})) invitationId: string, + @Body() acceptRejectInvitation: AcceptRejectInvitationDto, + @Param( + 'invitationId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(`Invalid format for InvitationId`); + } + }) + ) + invitationId: string, @User() reqUser: user, @Res() res: Response ): Promise { @@ -320,13 +330,13 @@ export class UserController { }; return res.status(HttpStatus.CREATED).json(finalResponse); } - + /** - * Updates the user profile. - * - * @body UpdateUserProfileDto - * @returns A response indicating the success of the update operation. - */ + * Updates the user profile. + * + * @body UpdateUserProfileDto + * @returns A response indicating the success of the update operation. + */ @Put('/') @ApiOperation({ summary: 'Update user profile', @@ -343,7 +353,7 @@ export class UserController { const userId = reqUser.id; updateUserProfileDto.id = userId; await this.userService.updateUserProfile(updateUserProfileDto); - + const finalResponse: IResponse = { statusCode: HttpStatus.OK, message: ResponseMessages.user.success.update @@ -351,40 +361,40 @@ export class UserController { return res.status(HttpStatus.OK).json(finalResponse); } - /** + /** * @body AddPasskeyDetailsDto * @returns User's profile update status */ - - - @Put('/password/:email') - @ApiOperation({ summary: 'Store user password details', description: 'Securely store and update the user’s password details.' }) - @ApiExcludeEndpoint() - @ApiBearerAuth() - @UseGuards(AuthGuard('jwt'), UserAccessGuard) - - async addPasskey( - @Body() userInfo: AddPasskeyDetailsDto, - @User() reqUser: user, - @Res() res: Response - ): Promise { - - const userDetails = await this.userService.addPasskey(reqUser.email, userInfo); - const finalResponse = { - statusCode: HttpStatus.OK, - message: ResponseMessages.user.success.update, - data: userDetails - }; - - return res.status(HttpStatus.OK).json(finalResponse); - } + + @Put('/password/:email') + @ApiOperation({ + summary: 'Store user password details', + description: 'Securely store and update the user’s password details.' + }) + @ApiExcludeEndpoint() + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt'), UserAccessGuard) + async addPasskey( + @Body() userInfo: AddPasskeyDetailsDto, + @User() reqUser: user, + @Res() res: Response + ): Promise { + const userDetails = await this.userService.addPasskey(reqUser.email, userInfo); + const finalResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.user.success.update, + data: userDetails + }; + + return res.status(HttpStatus.OK).json(finalResponse); + } /** - * Updates platform settings. - * @body UpdatePlatformSettingsDto - * - * @returns Status of the update operation. - */ + * Updates platform settings. + * @body UpdatePlatformSettingsDto + * + * @returns Status of the update operation. + */ @Put('/platform-settings') @ApiOperation({ summary: 'Update platform settings', @@ -406,4 +416,19 @@ export class UserController { return res.status(HttpStatus.OK).json(finalResponse); } -} \ No newline at end of file + + @Post('/delete-inactive-sessions') + @ApiOperation({ + summary: 'Update platform settings', + description: 'Modify platform settings. Only accessible by platform admins.' + }) + async deleteInactiveSessions(@Res() res: Response): Promise { + await this.userService.deleteInactiveSessions(); + const finalResponse = { + statusCode: HttpStatus.OK, + message: '' + }; + + return res.status(HttpStatus.OK).json(finalResponse); + } +} diff --git a/apps/api-gateway/src/user/user.service.ts b/apps/api-gateway/src/user/user.service.ts index c3988d810..f08ab3772 100644 --- a/apps/api-gateway/src/user/user.service.ts +++ b/apps/api-gateway/src/user/user.service.ts @@ -15,7 +15,10 @@ import { NATSClient } from '@credebl/common/NATSClient'; @Injectable() export class UserService extends BaseService { - constructor(@Inject('NATS_CLIENT') private readonly serviceProxy: ClientProxy, private readonly natsClient : NATSClient) { + constructor( + @Inject('NATS_CLIENT') private readonly serviceProxy: ClientProxy, + private readonly natsClient: NATSClient + ) { super('User Service'); } @@ -25,8 +28,8 @@ export class UserService extends BaseService { } async getPublicProfile(username: string): Promise { - const payload = { username }; - return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-user-public-profile', payload); + const payload = { username }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-user-public-profile', payload); } async updateUserProfile(updateUserProfileDto: UpdateUserProfileDto): Promise { @@ -43,24 +46,19 @@ export class UserService extends BaseService { const payload = { id }; return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-user-by-keycloak', payload); } - + async invitations(id: string, status: string, getAllInvitationsDto: GetAllInvitationsDto): Promise { const { pageNumber, pageSize, search } = getAllInvitationsDto; const payload = { id, status, pageNumber, pageSize, search }; return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-org-invitations', payload); } - async acceptRejectInvitaion( - acceptRejectInvitation: AcceptRejectInvitationDto, - userId: string - ): Promise { + async acceptRejectInvitaion(acceptRejectInvitation: AcceptRejectInvitationDto, userId: string): Promise { const payload = { acceptRejectInvitation, userId }; return this.natsClient.sendNatsMessage(this.serviceProxy, 'accept-reject-invitations', payload); } - async get( - paginationDto:PaginationDto - ): Promise { + async get(paginationDto: PaginationDto): Promise { const { pageNumber, pageSize, search } = paginationDto; const payload = { pageNumber, pageSize, search }; return this.natsClient.sendNatsMessage(this.serviceProxy, 'fetch-users', payload); @@ -94,4 +92,8 @@ export class UserService extends BaseService { const payload = { email }; return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-user-info-by-user-email-keycloak', payload); } + + async deleteInactiveSessions(): Promise { + return this.natsClient.sendNatsMessage(this.serviceProxy, 'delete-inactive-sessions', ''); + } } diff --git a/apps/user/repositories/user.repository.ts b/apps/user/repositories/user.repository.ts index b66899d99..c3be9054c 100644 --- a/apps/user/repositories/user.repository.ts +++ b/apps/user/repositories/user.repository.ts @@ -987,4 +987,30 @@ export class UserRepository { throw error; } } + + async deleteInactiveSessions(): Promise { + try { + // await this.prisma.session.deleteMany({ + // where: { + // AND: [ + // { + // createdAt: { + // lt: new Date(Date.now() - expires), // Current time minus expire interval + // }, + // }, + // // You can add more conditions here + // ], + // }, + // }); + const response = await this.prisma.$executeRaw` + DELETE FROM "session" + WHERE ("createdAt" + make_interval(secs => "expires")) < NOW() + `; + this.logger.debug('Response::', response); + return response; + } catch (error) { + this.logger.error(`Error in deleting the in active sessions::${error.message}`); + throw error; + } + } } diff --git a/apps/user/src/user.controller.ts b/apps/user/src/user.controller.ts index a539d8a99..eff76fff1 100644 --- a/apps/user/src/user.controller.ts +++ b/apps/user/src/user.controller.ts @@ -269,4 +269,9 @@ export class UserController { async logout(logoutUserDto: ISessions): Promise { return this.userService.logout(logoutUserDto); } + + @MessagePattern({ cmd: 'delete-inactive-sessions' }) + async deleteInActiveSessions(): Promise { + return this.userService.deleteInActiveSessions(); + } } diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index 7101c8678..717b9a1e9 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -1335,4 +1335,13 @@ export class UserService { throw new RpcException(error.response ? error.response : error); } } + + async deleteInActiveSessions(): Promise { + try { + return await this.userRepository.deleteInactiveSessions(); + } catch (error) { + this.logger.error(`Error in deleting in-active sessions`); + throw new RpcException(error.response ? error.response : error); + } + } } From bd4cc44a381c5940eb6b44c06e40f3057867f8d3 Mon Sep 17 00:00:00 2001 From: shitrerohit Date: Fri, 29 Aug 2025 20:03:44 +0530 Subject: [PATCH 2/3] Added delete inactive session functionality during login Signed-off-by: shitrerohit --- apps/api-gateway/src/user/user.controller.ts | 15 --------- apps/api-gateway/src/user/user.service.ts | 4 --- apps/organization/src/organization.service.ts | 11 ++++++- apps/user/interfaces/user.interface.ts | 1 + apps/user/repositories/user.repository.ts | 33 ++++++++----------- apps/user/src/user.controller.ts | 5 --- apps/user/src/user.service.ts | 22 ++++++------- .../migration.sql | 2 ++ libs/prisma-service/prisma/schema.prisma | 1 + 9 files changed, 38 insertions(+), 56 deletions(-) create mode 100644 libs/prisma-service/prisma/migrations/20250829091021_add_expiresat_in_session_table/migration.sql diff --git a/apps/api-gateway/src/user/user.controller.ts b/apps/api-gateway/src/user/user.controller.ts index 3ee603c28..588f7e259 100644 --- a/apps/api-gateway/src/user/user.controller.ts +++ b/apps/api-gateway/src/user/user.controller.ts @@ -416,19 +416,4 @@ export class UserController { return res.status(HttpStatus.OK).json(finalResponse); } - - @Post('/delete-inactive-sessions') - @ApiOperation({ - summary: 'Update platform settings', - description: 'Modify platform settings. Only accessible by platform admins.' - }) - async deleteInactiveSessions(@Res() res: Response): Promise { - await this.userService.deleteInactiveSessions(); - const finalResponse = { - statusCode: HttpStatus.OK, - message: '' - }; - - return res.status(HttpStatus.OK).json(finalResponse); - } } diff --git a/apps/api-gateway/src/user/user.service.ts b/apps/api-gateway/src/user/user.service.ts index f08ab3772..1d0281af1 100644 --- a/apps/api-gateway/src/user/user.service.ts +++ b/apps/api-gateway/src/user/user.service.ts @@ -92,8 +92,4 @@ export class UserService extends BaseService { const payload = { email }; return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-user-info-by-user-email-keycloak', payload); } - - async deleteInactiveSessions(): Promise { - return this.natsClient.sendNatsMessage(this.serviceProxy, 'delete-inactive-sessions', ''); - } } diff --git a/apps/organization/src/organization.service.ts b/apps/organization/src/organization.service.ts index b1e31764d..72b0e5926 100644 --- a/apps/organization/src/organization.service.ts +++ b/apps/organization/src/organization.service.ts @@ -64,6 +64,8 @@ import { DeleteOrgInvitationsEmail } from '../templates/delete-organization-invi import { IOrgRoles } from 'libs/org-roles/interfaces/org-roles.interface'; import { NATSClient } from '@credebl/common/NATSClient'; import { UserRepository } from 'apps/user/repositories/user.repository'; +import * as jwt from 'jsonwebtoken'; + @Injectable() export class OrganizationService { constructor( @@ -708,17 +710,24 @@ export class OrganizationService { let addSessionDetails; // Fetch owner organization details for getting the user id const orgRoleDetails = await this.organizationRepository.getOrgAndOwnerUser(clientId); + // called seprate method to delete exp session + this.userRepository.deleteInactiveSessions(orgRoleDetails['user'].id); + // Fetch the total number of sessions for the requested user to check and restrict the creation of multiple sessions. const userSessionDetails = await this.userRepository.fetchUserSessions(orgRoleDetails['user'].id); if (Number(process.env.SESSIONS_LIMIT) <= userSessionDetails?.length) { throw new BadRequestException(ResponseMessages.user.error.sessionLimitReached); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const decodedToken: any = jwt.decode(authenticationResult?.access_token); + const expiresAt = new Date(decodedToken.exp * 1000); // Session payload const sessionData = { sessionToken: authenticationResult?.access_token, userId: orgRoleDetails['user'].id, expires: authenticationResult?.expires_in, - sessionType: SessionType.ORG_SESSION + sessionType: SessionType.ORG_SESSION, + expiresAt }; // Note: // Fetch account details to check whether the requested user account exists diff --git a/apps/user/interfaces/user.interface.ts b/apps/user/interfaces/user.interface.ts index cc1d4f97a..a0c3ba997 100644 --- a/apps/user/interfaces/user.interface.ts +++ b/apps/user/interfaces/user.interface.ts @@ -191,6 +191,7 @@ export interface ISession { type?: string; accountId?: string; sessionType?: string; + expiresAt?: Date; } export interface IUpdateAccountDetails { diff --git a/apps/user/repositories/user.repository.ts b/apps/user/repositories/user.repository.ts index c3be9054c..91e444097 100644 --- a/apps/user/repositories/user.repository.ts +++ b/apps/user/repositories/user.repository.ts @@ -678,7 +678,7 @@ export class UserRepository { async createSession(tokenDetails: ISession): Promise { try { - const { sessionToken, userId, expires, refreshToken, accountId, sessionType } = tokenDetails; + const { sessionToken, userId, expires, refreshToken, accountId, sessionType, expiresAt } = tokenDetails; const sessionResponse = await this.prisma.session.create({ data: { id: tokenDetails.id, @@ -687,7 +687,8 @@ export class UserRepository { userId, refreshToken, accountId, - sessionType + sessionType, + expiresAt } }); return sessionResponse; @@ -988,24 +989,16 @@ export class UserRepository { } } - async deleteInactiveSessions(): Promise { - try { - // await this.prisma.session.deleteMany({ - // where: { - // AND: [ - // { - // createdAt: { - // lt: new Date(Date.now() - expires), // Current time minus expire interval - // }, - // }, - // // You can add more conditions here - // ], - // }, - // }); - const response = await this.prisma.$executeRaw` - DELETE FROM "session" - WHERE ("createdAt" + make_interval(secs => "expires")) < NOW() - `; + async deleteInactiveSessions(userId: string): Promise { + try { + const response = await this.prisma.session.deleteMany({ + where: { + expiresAt: { + lt: new Date() + }, + userId + } + }); this.logger.debug('Response::', response); return response; } catch (error) { diff --git a/apps/user/src/user.controller.ts b/apps/user/src/user.controller.ts index eff76fff1..a539d8a99 100644 --- a/apps/user/src/user.controller.ts +++ b/apps/user/src/user.controller.ts @@ -269,9 +269,4 @@ export class UserController { async logout(logoutUserDto: ISessions): Promise { return this.userService.logout(logoutUserDto); } - - @MessagePattern({ cmd: 'delete-inactive-sessions' }) - async deleteInActiveSessions(): Promise { - return this.userService.deleteInActiveSessions(); - } } diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index 717b9a1e9..0237993e6 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -459,6 +459,8 @@ export class UserService { if (true === isPasskey && false === userData?.isFidoVerified) { throw new UnauthorizedException(ResponseMessages.user.error.registerFido); } + // called seprate method to delete exp session + this.userRepository.deleteInactiveSessions(userData?.id); const userSessionDetails = await this.userRepository.fetchUserSessions(userData?.id); if (Number(process.env.SESSIONS_LIMIT) <= userSessionDetails?.length) { throw new BadRequestException(ResponseMessages.user.error.sessionLimitReached); @@ -474,13 +476,16 @@ export class UserService { } // eslint-disable-next-line @typescript-eslint/no-explicit-any const decodedToken: any = jwt.decode(tokenDetails?.access_token); + const expiresAt = new Date(decodedToken.exp * 1000); + const sessionData = { id: decodedToken.sid, sessionToken: tokenDetails?.access_token, userId: userData?.id, expires: tokenDetails?.expires_in, refreshToken: tokenDetails?.refresh_token, - sessionType: SessionType.USER_SESSION + sessionType: SessionType.USER_SESSION, + expiresAt }; const fetchAccountDetails = await this.userRepository.checkAccountDetails(userData?.id); @@ -555,13 +560,17 @@ export class UserService { if (!deletePreviousSession) { throw new InternalServerErrorException(ResponseMessages.user.error.errorInDeleteSession); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const decodedToken: any = jwt.decode(tokenResponse?.access_token); + const expiresAt = new Date(decodedToken.exp * 1000); const sessionData = { sessionToken: tokenResponse.access_token, userId: userByKeycloakId?.['id'], expires: tokenResponse.expires_in, refreshToken: tokenResponse.refresh_token, sessionType: SessionType.USER_SESSION, - accountId: userAccountDetails.id + accountId: userAccountDetails.id, + expiresAt }; const addSessionDetails = await this.userRepository.createSession(sessionData); if (!addSessionDetails) { @@ -1335,13 +1344,4 @@ export class UserService { throw new RpcException(error.response ? error.response : error); } } - - async deleteInActiveSessions(): Promise { - try { - return await this.userRepository.deleteInactiveSessions(); - } catch (error) { - this.logger.error(`Error in deleting in-active sessions`); - throw new RpcException(error.response ? error.response : error); - } - } } diff --git a/libs/prisma-service/prisma/migrations/20250829091021_add_expiresat_in_session_table/migration.sql b/libs/prisma-service/prisma/migrations/20250829091021_add_expiresat_in_session_table/migration.sql new file mode 100644 index 000000000..70b14e0a1 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20250829091021_add_expiresat_in_session_table/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "session" ADD COLUMN "expiresAt" TIMESTAMP(6); diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index 01c803d54..337c953a2 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -66,6 +66,7 @@ model session { accountId String? @db.Uuid sessionType String? account account? @relation(fields: [accountId], references:[id]) + expiresAt DateTime? @db.Timestamp(6) } model token { From 7f4071bc18d93c1089cef8192ea431f45c062e5e Mon Sep 17 00:00:00 2001 From: shitrerohit Date: Fri, 29 Aug 2025 20:24:09 +0530 Subject: [PATCH 3/3] Added logs for delete inactive session Signed-off-by: shitrerohit --- apps/user/repositories/user.repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/user/repositories/user.repository.ts b/apps/user/repositories/user.repository.ts index 91e444097..7fb743ec9 100644 --- a/apps/user/repositories/user.repository.ts +++ b/apps/user/repositories/user.repository.ts @@ -999,7 +999,7 @@ export class UserRepository { userId } }); - this.logger.debug('Response::', response); + this.logger.debug('Deleted inactive sessions::', response); return response; } catch (error) { this.logger.error(`Error in deleting the in active sessions::${error.message}`);