Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions apps/api-gateway/common/interface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Prisma } from '@prisma/client';

export interface ResponseType {
statusCode: number;
message: string;
Expand All @@ -6,7 +8,21 @@ export interface ResponseType {
}

export interface ExceptionResponse {
message: string | string[]
error: string
statusCode: number
message: string | string[];
error: string;
statusCode: number;
}

export interface ISession {
id?: string;
sessionToken?: string;
userId?: string;
expires?: number;
refreshToken?: string;
keycloakUserId?: string;
type?: string;
accountId?: string;
sessionType?: string;
expiresAt?: Date;
clientInfo?: Prisma.JsonValue | null;
}
5 changes: 0 additions & 5 deletions apps/api-gateway/src/authz/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,6 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
// Todo: We need to add this logic in seprate jwt gurd to handle the token expiration functionality.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const decodedToken: any = jwt.decode(jwtToken);
const currentTime = Math.floor(Date.now() / 1000);
if (decodedToken?.exp < currentTime) {
const sessionIds = { sessions: [decodedToken?.sid] };
await this.authzService.logout(sessionIds);
}
if (!decodedToken) {
throw new UnauthorizedException(ResponseMessages.user.error.invalidAccessToken);
}
Expand Down
7 changes: 7 additions & 0 deletions apps/user/interfaces/user.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,3 +302,10 @@ export interface IRestrictedUserSession {
clientInfo: Prisma.JsonValue | null;
sessionType: string;
}

export interface ITokenData {
sessionToken: string;
expires: number;
refreshToken: string;
expiresAt: Date;
}
55 changes: 52 additions & 3 deletions apps/user/repositories/user.repository.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
/* eslint-disable camelcase */
/* eslint-disable prefer-destructuring */

import {
BadRequestException,
Injectable,
InternalServerErrorException,
Logger,
NotFoundException
} from '@nestjs/common';
import {
IOrgUsers,
IRestrictedUserSession,
ISendVerificationEmail,
ISession,
IShareUserCertificate,
ITokenData,
IUserDeletedActivity,
IUserInformation,
IUsersProfile,
Expand All @@ -17,7 +25,6 @@ import {
UserRoleDetails,
UserRoleMapping
} from '../interfaces/user.interface';
import { Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common';
import {
Prisma,
RecordType,
Expand Down Expand Up @@ -722,6 +729,21 @@ export class UserRepository {
}
}

//this function is to fetch all session details for a user including token details without any restriction
async fetchUserSessionDetails(userId: string): Promise<ISession[]> {
try {
const userSessionCount = await this.prisma.session.findMany({
where: {
userId
}
});
return userSessionCount;
} catch (error) {
this.logger.error(`Error in getting user session details: ${error.message} `);
throw error;
}
}

async checkAccountDetails(userId: string): Promise<account> {
try {
const accountDetails = await this.prisma.account.findUnique({
Expand Down Expand Up @@ -980,8 +1002,13 @@ export class UserRepository {
});
return userSession;
} catch (error) {
this.logger.error(`Error in logging out user: ${error.message}`);
throw error;
if (error instanceof Prisma.PrismaClientKnownRequestError && 'P2025' === error.code) {
this.logger.warn(`Session not found for deletion: ${sessionId}`);
throw new NotFoundException('Record to be deleted not found');
} else {
this.logger.error(`Error in logging out user: ${error.message}`);
throw error;
}
}
}

Expand Down Expand Up @@ -1032,4 +1059,26 @@ export class UserRepository {
throw error;
}
}

async updateSessionToken(id: string, tokenData: ITokenData): Promise<session> {
if (!id || !tokenData) {
throw new BadRequestException(`Missing id or tokenData for session details update`);
}
try {
const sessionResponse = await this.prisma.session.update({
where: {
id
},
data: tokenData
});
return sessionResponse;
} catch (error) {
this.logger.error(`Error in creating session: ${error.message} `);
if (error instanceof Prisma.PrismaClientKnownRequestError && 'P2025' === error.code) {
this.logger.warn(`Session not found for update: ${id}`);
throw new NotFoundException('Session not found');
}
throw error;
}
}
}
92 changes: 44 additions & 48 deletions apps/user/src/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ 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) {
Expand All @@ -466,8 +466,10 @@ export class UserService {
const decryptedPassword = await this.commonService.decryptPassword(password);
tokenDetails = await this.generateToken(email.toLowerCase(), decryptedPassword, userData);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const decodedToken: any = jwt.decode(tokenDetails?.access_token);
const decodedToken = jwt.decode(tokenDetails?.refresh_token);
if (!decodedToken || 'object' !== typeof decodedToken || !decodedToken.exp || !decodedToken.sid) {
throw new UnauthorizedException(ResponseMessages.user.error.refreshTokenExpired);
}
const expiresAt = new Date(decodedToken.exp * 1000);

const sessionData = {
Expand Down Expand Up @@ -537,53 +539,47 @@ export class UserService {

async refreshTokenDetails(refreshToken: string): Promise<ISignInUser> {
try {
try {
const data = jwt.decode(refreshToken) as jwt.JwtPayload;
const userByKeycloakId = await this.userRepository.getUserByKeycloakId(data?.sub);
this.logger.debug(`User details::;${JSON.stringify(userByKeycloakId)}`);
const tokenResponse = await this.clientRegistrationService.getAccessToken(
refreshToken,
userByKeycloakId?.['clientId'],
userByKeycloakId?.['clientSecret']
);
this.logger.debug(`tokenResponse::::${JSON.stringify(tokenResponse)}`);
// Fetch the details from account table based on userid and refresh token
const userAccountDetails = await this.userRepository.checkAccountDetails(userByKeycloakId?.['id']);
// Update the account details with latest access token, refresh token and exp date
if (!userAccountDetails) {
throw new NotFoundException(ResponseMessages.user.error.userAccountNotFound);
}
// Fetch session details
const sessionDetails = await this.userRepository.fetchSessionByRefreshToken(refreshToken);
if (!sessionDetails) {
throw new NotFoundException(ResponseMessages.user.error.userSeesionNotFound);
}
// Delete previous session
const deletePreviousSession = await this.userRepository.deleteSession(sessionDetails.id);
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,
expiresAt
};
const addSessionDetails = await this.userRepository.createSession(sessionData);
if (!addSessionDetails) {
throw new InternalServerErrorException(ResponseMessages.user.error.errorInSessionCreation);
}
const data = jwt.decode(refreshToken) as jwt.JwtPayload;
const refreshTokenExp = new Date(data.exp * 1000);
const currentTime = new Date();
if (refreshTokenExp < currentTime) {
await this.userRepository.deleteSession(data?.sid);
throw new UnauthorizedException(ResponseMessages.user.error.refreshTokenExpired);
}
const userByKeycloakId = await this.userRepository.getUserByKeycloakId(data?.sub);
const tokenResponse = await this.clientRegistrationService.getAccessToken(
refreshToken,
userByKeycloakId?.['clientId'],
userByKeycloakId?.['clientSecret']
);
// Fetch the details from account table based on userid and refresh token
const userAccountDetails = await this.userRepository.checkAccountDetails(userByKeycloakId?.['id']);
// Update the account details with latest access token, refresh token and exp date
if (!userAccountDetails) {
throw new NotFoundException(ResponseMessages.user.error.userAccountNotFound);
}
// Fetch session details

return tokenResponse;
} catch (error) {
throw new BadRequestException(ResponseMessages.user.error.invalidRefreshToken);
const sessionDetails = await this.userRepository.getSession(data.sid);
if (!sessionDetails) {
throw new NotFoundException(ResponseMessages.user.error.userSessionNotFound);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const decodedToken: any = jwt.decode(tokenResponse?.refresh_token);
const expiresAt = new Date(decodedToken.exp * 1000);
const sessionData = {
sessionToken: tokenResponse.access_token,
expires: tokenResponse.expires_in,
refreshToken: tokenResponse.refresh_token,
expiresAt
};
const addSessionDetails = await this.userRepository.updateSessionToken(tokenResponse.session_state, sessionData);
if (!addSessionDetails) {
throw new InternalServerErrorException(ResponseMessages.user.error.errorInSessionCreation);
}

return tokenResponse;
} catch (error) {
this.logger.error(`In refreshTokenDetails : ${JSON.stringify(error)}`);
throw new RpcException(error.response ? error.response : error);
Expand Down
1 change: 1 addition & 0 deletions libs/common/src/interfaces/user.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface ISignInUser {
refresh_token?: string;
isRegisteredToSupabase?: boolean;
sessionId?: string;
refresh_expires_in?: number;
}
export interface IVerifyUserEmail {
email: string;
Expand Down
3 changes: 2 additions & 1 deletion libs/common/src/response-messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ export const ResponseMessages = {
errorInDeleteSession: 'Error in deleting the session',
errorInSessionCreation: 'Error in create session',
userAccountNotFound: 'User account not found',
userSeesionNotFound: 'User session not found'
userSessionNotFound: 'User session not found',
refreshTokenExpired: 'Refresh token has expired'
}
},
organisation: {
Expand Down