Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
115 changes: 111 additions & 4 deletions apps/api-gateway/src/authz/authz.controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import {
BadRequestException,
Body,
Controller,
Delete,
ForbiddenException,
Get,
HttpStatus,
Logger,
Param,
ParseUUIDPipe,
Post,
Query,
Req,
Expand All @@ -15,10 +19,19 @@ import {
} from '@nestjs/common';
import { AuthzService } from './authz.service';
import { CommonService } from '../../../../libs/common/src/common.service';
import { ApiBearerAuth, ApiBody, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
import {
ApiBearerAuth,
ApiBody,
ApiForbiddenResponse,
ApiOperation,
ApiQuery,
ApiResponse,
ApiTags,
ApiUnauthorizedResponse
} from '@nestjs/swagger';
import { ApiResponseDto } from '../dtos/apiResponse.dto';
import { UserEmailVerificationDto } from '../user/dto/create-user.dto';
import IResponseType from '@credebl/common/interfaces/response.interface';
import IResponseType, { IResponse } from '@credebl/common/interfaces/response.interface';
import { ResponseMessages } from '@credebl/common/response-messages';
import { Response, Request } from 'express';
import { EmailVerificationDto } from '../user/dto/email-verify.dto';
Expand All @@ -36,6 +49,12 @@ import { SessionGuard } from './guards/session.guard';
import { UserLogoutDto } from './dtos/user-logout.dto';
import { AuthGuard } from '@nestjs/passport';
import { ISessionData } from 'apps/user/interfaces/user.interface';
import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto';
import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto';
import { User } from './decorators/user.decorator';
import { user } from '@prisma/client';
import * as useragent from 'express-useragent';

@Controller('auth')
@ApiTags('auth')
@UseFilters(CustomExceptionFilter)
Expand Down Expand Up @@ -158,9 +177,19 @@ export class AuthzController {
})
@ApiResponse({ status: HttpStatus.OK, description: 'Success', type: AuthTokenResponse })
@ApiBody({ type: LoginUserDto })
async login(@Body() loginUserDto: LoginUserDto, @Res() res: Response): Promise<Response> {
async login(@Req() req: Request, @Body() loginUserDto: LoginUserDto, @Res() res: Response): Promise<Response> {
if (loginUserDto.email) {
const userData = await this.authzService.login(loginUserDto.email, loginUserDto.password);
const ip = (req.headers['x-forwarded-for'] as string)?.split(',')[0] || req.socket.remoteAddress;
const ua = req.headers['user-agent'];
const expressUa = useragent.parse(ua);
const device = {
browser: `${expressUa.browser} ${expressUa.version ?? ''}`.trim(),
os: expressUa.platform,
deviceType: expressUa.isDesktop ? 'desktop' : 'mobile'
};

const clientInfo = JSON.stringify({ ...device, rawDetail: ua, ip });
const userData = await this.authzService.login(clientInfo, loginUserDto.email, loginUserDto.password);

const finalResponse: IResponseType = {
statusCode: HttpStatus.OK,
Expand Down Expand Up @@ -331,4 +360,82 @@ export class AuthzController {

return res.status(HttpStatus.OK).json(finalResponse);
}

/**
* Get all sessions by userId
* @param userId The ID of the user
* @returns All sessions related to the user
*/
@Get('/:userId/sessions')
@ApiOperation({
summary: 'Get all sessions by userId',
description: 'Retrieve sessions for the user. Based on userId.'
})
@ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto })
@ApiUnauthorizedResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized', type: UnauthorizedErrorDto })
@ApiForbiddenResponse({ status: HttpStatus.FORBIDDEN, description: 'Forbidden', type: ForbiddenErrorDto })
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'))
async userSessions(
@User() reqUser: user,
@Res() res: Response,
@Param(
'userId',
new ParseUUIDPipe({
exceptionFactory: (): Error => {
throw new BadRequestException(`Invalid format for User Id`);
}
})
)
userId: string
): Promise<Response> {
if (reqUser.id !== userId) {
throw new ForbiddenException('You are not allowed to access sessions of another user');
}
const response = await this.authzService.userSessions(userId);

const finalResponse: IResponse = {
statusCode: HttpStatus.OK,
message: ResponseMessages.user.success.fetchAllSession,
data: response
};
return res.status(HttpStatus.OK).json(finalResponse);
}

/**
* Delete session by sessionId
* @param sessionId The ID of the session record to delete
* @returns Acknowledgement on deletion
*/
@Delete('/:sessionId/sessions')
@ApiOperation({
summary: 'Delete a particular session using its sessionId',
description: 'Delete a particular session using its sessionId'
})
@ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto })
@ApiUnauthorizedResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized', type: UnauthorizedErrorDto })
@ApiForbiddenResponse({ status: HttpStatus.FORBIDDEN, description: 'Forbidden', type: ForbiddenErrorDto })
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'))
async deleteSession(
@User() reqUser: user,
@Res() res: Response,
@Param(
'sessionId',
new ParseUUIDPipe({
exceptionFactory: (): Error => {
throw new BadRequestException(`Invalid format for session Id`);
}
})
)
sessionId: string
): Promise<Response> {
const response = await this.authzService.deleteSession(sessionId, reqUser.id);

const finalResponse: IResponse = {
statusCode: HttpStatus.OK,
message: response.message
};
return res.status(HttpStatus.OK).json(finalResponse);
}
}
31 changes: 27 additions & 4 deletions apps/api-gateway/src/authz/authz.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, Inject } from '@nestjs/common';
import { Injectable, Inject, HttpException } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { BaseService } from '../../../../libs/service/base.service';
import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
Expand All @@ -17,8 +17,9 @@ import { ForgotPasswordDto } from './dtos/forgot-password.dto';
import { ResetTokenPasswordDto } from './dtos/reset-token-password';
import { NATSClient } from '@credebl/common/NATSClient';
import { user } from '@prisma/client';
import { ISessionDetails } from 'apps/user/interfaces/user.interface';
import { IRestrictedUserSession, ISessionDetails } from 'apps/user/interfaces/user.interface';
import { UserLogoutDto } from './dtos/user-logout.dto';
import type { Prisma } from '@prisma/client';
@Injectable()
@WebSocketGateway()
export class AuthzService extends BaseService {
Expand Down Expand Up @@ -50,8 +51,8 @@ export class AuthzService extends BaseService {
return this.natsClient.sendNatsMessage(this.authServiceProxy, 'user-email-verification', payload);
}

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

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

async checkSession(sessionId): Promise<ISessionDetails> {
return this.natsClient.sendNatsMessage(this.authServiceProxy, 'check-session-details', sessionId);
}

async resetPassword(resetPasswordDto: ResetPasswordDto): Promise<IResetPasswordResponse> {
return this.natsClient.sendNatsMessage(this.authServiceProxy, 'user-reset-password', resetPasswordDto);
}
Expand All @@ -76,6 +81,24 @@ export class AuthzService extends BaseService {
return this.natsClient.sendNatsMessage(this.authServiceProxy, 'refresh-token-details', refreshToken);
}

async userSessions(userId: string): Promise<IRestrictedUserSession[]> {
return this.natsClient.sendNatsMessage(this.authServiceProxy, 'session-details-by-userId', userId);
}

async deleteSession(sessionId: string, userId: string): Promise<{ message: string }> {
try {
return await this.natsClient.sendNatsMessage(this.authServiceProxy, 'delete-session-by-sessionId', {
sessionId,
userId
});
} catch (error) {
if (error?.response && error?.status) {
throw new HttpException(error.response, error.status);
}
throw error;
}
}

async addUserDetails(userInfo: AddUserDetailsDto): Promise<ISignUpUserResponse> {
const payload = { userInfo };
return this.natsClient.sendNatsMessage(this.authServiceProxy, 'add-user', payload);
Expand Down
24 changes: 12 additions & 12 deletions apps/api-gateway/src/authz/jwt-payload.interface.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
export interface JwtPayload {
iss: string;
sub: string;
aud: string[];
iat?: number;
exp?: number;
azp: string;
scope: string;
gty?: string;
permissions: string[];
email?: string
}
iss: string;
sub: string;
aud: string[];
iat?: number;
exp?: number;
azp: string;
scope: string;
gty?: string;
permissions: string[];
email?: string;
sid: string;
}
13 changes: 13 additions & 0 deletions apps/api-gateway/src/authz/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
let userDetails = null;
let userInfo;

const sessionId = payload?.sid;
let sessionDetails = null;
if (sessionId) {
try {
sessionDetails = await this.authzService.checkSession(sessionId);
} catch (error) {
this.logger.log('Error in JWT Stratergy while fetching session details', JSON.stringify(error, null, 2));
}
if (!sessionDetails) {
throw new UnauthorizedException(ResponseMessages.user.error.invalidAccessToken);
}
}

if (payload?.email) {
userInfo = await this.usersService.getUserByUserIdInKeycloak(payload?.email);
}
Expand Down
3 changes: 3 additions & 0 deletions apps/api-gateway/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { CommonConstants } from '@credebl/common/common.constant';
import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter';
import { NatsInterceptor } from '@credebl/common';
import { UpdatableValidationPipe } from '@credebl/common/custom-overrideable-validation-pipe';
import * as useragent from 'express-useragent';

dotenv.config();

async function bootstrap(): Promise<void> {
Expand Down Expand Up @@ -46,6 +48,7 @@ async function bootstrap(): Promise<void> {
app.use(express.json({ limit: '100mb' }));
app.use(express.urlencoded({ limit: '100mb', extended: true }));
app.use(cookieParser());
app.use(useragent.express());

app.use((req, res, next) => {
let err = null;
Expand Down
40 changes: 23 additions & 17 deletions apps/user/dtos/login-user.dto.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
import { trim } from '@credebl/common/cast.helper';
import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';

import { ApiProperty } from '@nestjs/swagger';
import type { Prisma } from '@prisma/client';
import { Transform } from 'class-transformer';
import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { trim } from '@credebl/common/cast.helper';

export class LoginUserDto {
@ApiProperty({ example: '[email protected]' })
@IsEmail({}, { message: 'Please provide a valid email' })
@IsNotEmpty({ message: 'Email is required' })
@IsString({ message: 'Email should be a string' })
@Transform(({ value }) => trim(value))
email: string;

@ApiProperty({ example: 'Password@1' })
@IsOptional()
@IsString({ message: 'password should be string' })
password?: string;
@ApiProperty({ example: '[email protected]' })
@IsEmail({}, { message: 'Please provide a valid email' })
@IsNotEmpty({ message: 'Email is required' })
@IsString({ message: 'Email should be a string' })
@Transform(({ value }) => trim(value))
email: string;

@ApiProperty({ example: 'Password@1' })
@IsOptional()
@IsString({ message: 'password should be string' })
password?: string;

@ApiProperty({ example: 'false' })
@IsOptional()
@IsBoolean({ message: 'isPasskey should be boolean' })
isPasskey?: boolean;

@ApiProperty({ example: 'false' })
@IsOptional()
@IsBoolean({ message: 'isPasskey should be boolean' })
isPasskey?: boolean;
@ApiProperty({ example: 'false' })
@IsOptional()
clientInfo?: Prisma.JsonValue;
}
11 changes: 11 additions & 0 deletions apps/user/interfaces/user.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export interface IUserSignIn {
email: string;
password: string;
isPasskey?: boolean;
clientInfo: Prisma.JsonValue;
}

export interface ISession {
Expand All @@ -192,6 +193,7 @@ export interface ISession {
accountId?: string;
sessionType?: string;
expiresAt?: Date;
clientInfo?: Prisma.JsonValue | null;
}

export interface IUpdateAccountDetails {
Expand Down Expand Up @@ -290,3 +292,12 @@ export interface IAccountDetails {
export interface ISessionData {
sessionId: string;
}

export interface IRestrictedUserSession {
id: string;
userId: string;
expiresAt: Date;
createdAt: Date;
clientInfo: Prisma.JsonValue | null;
sessionType: string;
}
Loading