diff --git a/apps/api-gateway/src/user/user.controller.ts b/apps/api-gateway/src/user/user.controller.ts index c9f138a52..588f7e259 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,4 @@ export class UserController { return res.status(HttpStatus.OK).json(finalResponse); } -} \ No newline at end of file +} diff --git a/apps/api-gateway/src/user/user.service.ts b/apps/api-gateway/src/user/user.service.ts index c3988d810..1d0281af1 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); 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 b66899d99..7fb743ec9 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; @@ -987,4 +988,22 @@ export class UserRepository { throw error; } } + + async deleteInactiveSessions(userId: string): Promise { + try { + const response = await this.prisma.session.deleteMany({ + where: { + expiresAt: { + lt: new Date() + }, + userId + } + }); + this.logger.debug('Deleted inactive sessions::', 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.service.ts b/apps/user/src/user.service.ts index 7101c8678..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) { 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 {