Skip to content

Commit d2f9482

Browse files
sujitawankita-p17
authored andcommitted
feat: add APIs for session listing and deletion (#1449)
* feat/api to fetch all sessions and delete session Signed-off-by: sujitaw <[email protected]> * feat/update signin api to store session details Signed-off-by: sujitaw <[email protected]> * feat/added prisma migration file for clientInfo column Signed-off-by: sujitaw <[email protected]> * fix/sonarcube comment Signed-off-by: sujitaw <[email protected]> * fix/code rabbit comments Signed-off-by: sujitaw <[email protected]> * fix/rabbit comments Signed-off-by: sujitaw <[email protected]> * fix/pr comments Signed-off-by: sujitaw <[email protected]> * fix/changed the package from uaparser to express-parser Signed-off-by: sujitaw <[email protected]> * feat/update jwt logic to throw 401 if session is deleted Signed-off-by: sujitaw <[email protected]> * fix/resolve pr comments Signed-off-by: sujitaw <[email protected]> --------- Signed-off-by: sujitaw <[email protected]> Signed-off-by: Ankita Patidar <[email protected]>
1 parent a0833b7 commit d2f9482

File tree

15 files changed

+301
-49
lines changed

15 files changed

+301
-49
lines changed

apps/api-gateway/src/authz/authz.controller.ts

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import {
2+
BadRequestException,
23
Body,
34
Controller,
5+
Delete,
6+
ForbiddenException,
47
Get,
58
HttpStatus,
69
Logger,
710
Param,
11+
ParseUUIDPipe,
812
Post,
913
Query,
1014
Req,
@@ -15,10 +19,19 @@ import {
1519
} from '@nestjs/common';
1620
import { AuthzService } from './authz.service';
1721
import { CommonService } from '../../../../libs/common/src/common.service';
18-
import { ApiBearerAuth, ApiBody, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
22+
import {
23+
ApiBearerAuth,
24+
ApiBody,
25+
ApiForbiddenResponse,
26+
ApiOperation,
27+
ApiQuery,
28+
ApiResponse,
29+
ApiTags,
30+
ApiUnauthorizedResponse
31+
} from '@nestjs/swagger';
1932
import { ApiResponseDto } from '../dtos/apiResponse.dto';
2033
import { UserEmailVerificationDto } from '../user/dto/create-user.dto';
21-
import IResponseType from '@credebl/common/interfaces/response.interface';
34+
import IResponseType, { IResponse } from '@credebl/common/interfaces/response.interface';
2235
import { ResponseMessages } from '@credebl/common/response-messages';
2336
import { Response, Request } from 'express';
2437
import { EmailVerificationDto } from '../user/dto/email-verify.dto';
@@ -36,6 +49,12 @@ import { SessionGuard } from './guards/session.guard';
3649
import { UserLogoutDto } from './dtos/user-logout.dto';
3750
import { AuthGuard } from '@nestjs/passport';
3851
import { ISessionData } from 'apps/user/interfaces/user.interface';
52+
import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto';
53+
import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto';
54+
import { User } from './decorators/user.decorator';
55+
import { user } from '@prisma/client';
56+
import * as useragent from 'express-useragent';
57+
3958
@Controller('auth')
4059
@ApiTags('auth')
4160
@UseFilters(CustomExceptionFilter)
@@ -158,9 +177,19 @@ export class AuthzController {
158177
})
159178
@ApiResponse({ status: HttpStatus.OK, description: 'Success', type: AuthTokenResponse })
160179
@ApiBody({ type: LoginUserDto })
161-
async login(@Body() loginUserDto: LoginUserDto, @Res() res: Response): Promise<Response> {
180+
async login(@Req() req: Request, @Body() loginUserDto: LoginUserDto, @Res() res: Response): Promise<Response> {
162181
if (loginUserDto.email) {
163-
const userData = await this.authzService.login(loginUserDto.email, loginUserDto.password);
182+
const ip = (req.headers['x-forwarded-for'] as string)?.split(',')[0] || req.socket.remoteAddress;
183+
const ua = req.headers['user-agent'];
184+
const expressUa = useragent.parse(ua);
185+
const device = {
186+
browser: `${expressUa.browser} ${expressUa.version ?? ''}`.trim(),
187+
os: expressUa.platform,
188+
deviceType: expressUa.isDesktop ? 'desktop' : 'mobile'
189+
};
190+
191+
const clientInfo = JSON.stringify({ ...device, rawDetail: ua, ip });
192+
const userData = await this.authzService.login(clientInfo, loginUserDto.email, loginUserDto.password);
164193

165194
const finalResponse: IResponseType = {
166195
statusCode: HttpStatus.OK,
@@ -331,4 +360,82 @@ export class AuthzController {
331360

332361
return res.status(HttpStatus.OK).json(finalResponse);
333362
}
363+
364+
/**
365+
* Get all sessions by userId
366+
* @param userId The ID of the user
367+
* @returns All sessions related to the user
368+
*/
369+
@Get('/:userId/sessions')
370+
@ApiOperation({
371+
summary: 'Get all sessions by userId',
372+
description: 'Retrieve sessions for the user. Based on userId.'
373+
})
374+
@ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto })
375+
@ApiUnauthorizedResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized', type: UnauthorizedErrorDto })
376+
@ApiForbiddenResponse({ status: HttpStatus.FORBIDDEN, description: 'Forbidden', type: ForbiddenErrorDto })
377+
@ApiBearerAuth()
378+
@UseGuards(AuthGuard('jwt'))
379+
async userSessions(
380+
@User() reqUser: user,
381+
@Res() res: Response,
382+
@Param(
383+
'userId',
384+
new ParseUUIDPipe({
385+
exceptionFactory: (): Error => {
386+
throw new BadRequestException(`Invalid format for User Id`);
387+
}
388+
})
389+
)
390+
userId: string
391+
): Promise<Response> {
392+
if (reqUser.id !== userId) {
393+
throw new ForbiddenException('You are not allowed to access sessions of another user');
394+
}
395+
const response = await this.authzService.userSessions(userId);
396+
397+
const finalResponse: IResponse = {
398+
statusCode: HttpStatus.OK,
399+
message: ResponseMessages.user.success.fetchAllSession,
400+
data: response
401+
};
402+
return res.status(HttpStatus.OK).json(finalResponse);
403+
}
404+
405+
/**
406+
* Delete session by sessionId
407+
* @param sessionId The ID of the session record to delete
408+
* @returns Acknowledgement on deletion
409+
*/
410+
@Delete('/:sessionId/sessions')
411+
@ApiOperation({
412+
summary: 'Delete a particular session using its sessionId',
413+
description: 'Delete a particular session using its sessionId'
414+
})
415+
@ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto })
416+
@ApiUnauthorizedResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized', type: UnauthorizedErrorDto })
417+
@ApiForbiddenResponse({ status: HttpStatus.FORBIDDEN, description: 'Forbidden', type: ForbiddenErrorDto })
418+
@ApiBearerAuth()
419+
@UseGuards(AuthGuard('jwt'))
420+
async deleteSession(
421+
@User() reqUser: user,
422+
@Res() res: Response,
423+
@Param(
424+
'sessionId',
425+
new ParseUUIDPipe({
426+
exceptionFactory: (): Error => {
427+
throw new BadRequestException(`Invalid format for session Id`);
428+
}
429+
})
430+
)
431+
sessionId: string
432+
): Promise<Response> {
433+
const response = await this.authzService.deleteSession(sessionId, reqUser.id);
434+
435+
const finalResponse: IResponse = {
436+
statusCode: HttpStatus.OK,
437+
message: response.message
438+
};
439+
return res.status(HttpStatus.OK).json(finalResponse);
440+
}
334441
}

apps/api-gateway/src/authz/authz.service.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Injectable, Inject } from '@nestjs/common';
1+
import { Injectable, Inject, HttpException } from '@nestjs/common';
22
import { ClientProxy } from '@nestjs/microservices';
33
import { BaseService } from '../../../../libs/service/base.service';
44
import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
@@ -17,8 +17,9 @@ import { ForgotPasswordDto } from './dtos/forgot-password.dto';
1717
import { ResetTokenPasswordDto } from './dtos/reset-token-password';
1818
import { NATSClient } from '@credebl/common/NATSClient';
1919
import { user } from '@prisma/client';
20-
import { ISessionDetails } from 'apps/user/interfaces/user.interface';
20+
import { IRestrictedUserSession, ISessionDetails } from 'apps/user/interfaces/user.interface';
2121
import { UserLogoutDto } from './dtos/user-logout.dto';
22+
import type { Prisma } from '@prisma/client';
2223
@Injectable()
2324
@WebSocketGateway()
2425
export class AuthzService extends BaseService {
@@ -50,8 +51,8 @@ export class AuthzService extends BaseService {
5051
return this.natsClient.sendNatsMessage(this.authServiceProxy, 'user-email-verification', payload);
5152
}
5253

53-
async login(email: string, password?: string, isPasskey = false): Promise<ISignInUser> {
54-
const payload = { email, password, isPasskey };
54+
async login(clientInfo: Prisma.JsonValue, email: string, password?: string, isPasskey = false): Promise<ISignInUser> {
55+
const payload = { email, password, isPasskey, clientInfo };
5556
return this.natsClient.sendNatsMessage(this.authServiceProxy, 'user-holder-login', payload);
5657
}
5758

@@ -60,6 +61,10 @@ export class AuthzService extends BaseService {
6061
return this.natsClient.sendNatsMessage(this.authServiceProxy, 'fetch-session-details', payload);
6162
}
6263

64+
async checkSession(sessionId): Promise<ISessionDetails> {
65+
return this.natsClient.sendNatsMessage(this.authServiceProxy, 'check-session-details', sessionId);
66+
}
67+
6368
async resetPassword(resetPasswordDto: ResetPasswordDto): Promise<IResetPasswordResponse> {
6469
return this.natsClient.sendNatsMessage(this.authServiceProxy, 'user-reset-password', resetPasswordDto);
6570
}
@@ -76,6 +81,24 @@ export class AuthzService extends BaseService {
7681
return this.natsClient.sendNatsMessage(this.authServiceProxy, 'refresh-token-details', refreshToken);
7782
}
7883

84+
async userSessions(userId: string): Promise<IRestrictedUserSession[]> {
85+
return this.natsClient.sendNatsMessage(this.authServiceProxy, 'session-details-by-userId', userId);
86+
}
87+
88+
async deleteSession(sessionId: string, userId: string): Promise<{ message: string }> {
89+
try {
90+
return await this.natsClient.sendNatsMessage(this.authServiceProxy, 'delete-session-by-sessionId', {
91+
sessionId,
92+
userId
93+
});
94+
} catch (error) {
95+
if (error?.response && error?.status) {
96+
throw new HttpException(error.response, error.status);
97+
}
98+
throw error;
99+
}
100+
}
101+
79102
async addUserDetails(userInfo: AddUserDetailsDto): Promise<ISignUpUserResponse> {
80103
const payload = { userInfo };
81104
return this.natsClient.sendNatsMessage(this.authServiceProxy, 'add-user', payload);
Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
export interface JwtPayload {
2-
iss: string;
3-
sub: string;
4-
aud: string[];
5-
iat?: number;
6-
exp?: number;
7-
azp: string;
8-
scope: string;
9-
gty?: string;
10-
permissions: string[];
11-
email?: string
12-
}
13-
2+
iss: string;
3+
sub: string;
4+
aud: string[];
5+
iat?: number;
6+
exp?: number;
7+
azp: string;
8+
scope: string;
9+
gty?: string;
10+
permissions: string[];
11+
email?: string;
12+
sid: string;
13+
}

apps/api-gateway/src/authz/jwt.strategy.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
6262
let userDetails = null;
6363
let userInfo;
6464

65+
const sessionId = payload?.sid;
66+
let sessionDetails = null;
67+
if (sessionId) {
68+
try {
69+
sessionDetails = await this.authzService.checkSession(sessionId);
70+
} catch (error) {
71+
this.logger.log('Error in JWT Stratergy while fetching session details', JSON.stringify(error, null, 2));
72+
}
73+
if (!sessionDetails) {
74+
throw new UnauthorizedException(ResponseMessages.user.error.invalidAccessToken);
75+
}
76+
}
77+
6578
if (payload?.email) {
6679
userInfo = await this.usersService.getUserByUserIdInKeycloak(payload?.email);
6780
}

apps/api-gateway/src/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { CommonConstants } from '@credebl/common/common.constant';
1616
import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter';
1717
import { NatsInterceptor } from '@credebl/common';
1818
import { UpdatableValidationPipe } from '@credebl/common/custom-overrideable-validation-pipe';
19+
import * as useragent from 'express-useragent';
20+
1921
dotenv.config();
2022

2123
async function bootstrap(): Promise<void> {
@@ -46,6 +48,7 @@ async function bootstrap(): Promise<void> {
4648
app.use(express.json({ limit: '100mb' }));
4749
app.use(express.urlencoded({ limit: '100mb', extended: true }));
4850
app.use(cookieParser());
51+
app.use(useragent.express());
4952

5053
app.use((req, res, next) => {
5154
let err = null;

apps/user/dtos/login-user.dto.ts

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
1-
import { trim } from '@credebl/common/cast.helper';
1+
import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
2+
23
import { ApiProperty } from '@nestjs/swagger';
4+
import type { Prisma } from '@prisma/client';
35
import { Transform } from 'class-transformer';
4-
import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
6+
import { trim } from '@credebl/common/cast.helper';
57

68
export class LoginUserDto {
7-
@ApiProperty({ example: '[email protected]' })
8-
@IsEmail({}, { message: 'Please provide a valid email' })
9-
@IsNotEmpty({ message: 'Email is required' })
10-
@IsString({ message: 'Email should be a string' })
11-
@Transform(({ value }) => trim(value))
12-
email: string;
13-
14-
@ApiProperty({ example: 'Password@1' })
15-
@IsOptional()
16-
@IsString({ message: 'password should be string' })
17-
password?: string;
9+
@ApiProperty({ example: '[email protected]' })
10+
@IsEmail({}, { message: 'Please provide a valid email' })
11+
@IsNotEmpty({ message: 'Email is required' })
12+
@IsString({ message: 'Email should be a string' })
13+
@Transform(({ value }) => trim(value))
14+
email: string;
15+
16+
@ApiProperty({ example: 'Password@1' })
17+
@IsOptional()
18+
@IsString({ message: 'password should be string' })
19+
password?: string;
20+
21+
@ApiProperty({ example: 'false' })
22+
@IsOptional()
23+
@IsBoolean({ message: 'isPasskey should be boolean' })
24+
isPasskey?: boolean;
1825

19-
@ApiProperty({ example: 'false' })
20-
@IsOptional()
21-
@IsBoolean({ message: 'isPasskey should be boolean' })
22-
isPasskey?: boolean;
26+
@ApiProperty({ example: 'false' })
27+
@IsOptional()
28+
clientInfo?: Prisma.JsonValue;
2329
}

apps/user/interfaces/user.interface.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ export interface IUserSignIn {
179179
email: string;
180180
password: string;
181181
isPasskey?: boolean;
182+
clientInfo: Prisma.JsonValue;
182183
}
183184

184185
export interface ISession {
@@ -192,6 +193,7 @@ export interface ISession {
192193
accountId?: string;
193194
sessionType?: string;
194195
expiresAt?: Date;
196+
clientInfo?: Prisma.JsonValue | null;
195197
}
196198

197199
export interface IUpdateAccountDetails {
@@ -290,3 +292,12 @@ export interface IAccountDetails {
290292
export interface ISessionData {
291293
sessionId: string;
292294
}
295+
296+
export interface IRestrictedUserSession {
297+
id: string;
298+
userId: string;
299+
expiresAt: Date;
300+
createdAt: Date;
301+
clientInfo: Prisma.JsonValue | null;
302+
sessionType: string;
303+
}

0 commit comments

Comments
 (0)