Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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);
}
}
27 changes: 23 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 @@ -76,6 +77,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
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;
}
32 changes: 30 additions & 2 deletions apps/user/repositories/user.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import {
IOrgUsers,
IRestrictedUserSession,
ISendVerificationEmail,
ISession,
IShareUserCertificate,
Expand Down Expand Up @@ -31,6 +32,7 @@ import {
import { ProviderType, UserRole } from '@credebl/enum/enum';

import { PrismaService } from '@credebl/prisma-service';
import { RpcException } from '@nestjs/microservices';

interface UserQueryOptions {
id?: string; // Use the appropriate type based on your data model
Expand Down Expand Up @@ -687,7 +689,8 @@ export class UserRepository {
refreshToken,
accountId,
sessionType,
expiresAt
expiresAt,
...(tokenDetails.clientInfo ? { clientInfo: tokenDetails.clientInfo } : { clientInfo: { clientToken: true } })
}
});
return sessionResponse;
Expand All @@ -697,11 +700,19 @@ export class UserRepository {
}
}

async fetchUserSessions(userId: string): Promise<session[]> {
async fetchUserSessions(userId: string): Promise<IRestrictedUserSession[]> {
try {
const userSessionCount = await this.prisma.session.findMany({
where: {
userId
},
select: {
id: true,
userId: true,
expiresAt: true,
createdAt: true,
clientInfo: true,
sessionType: true
}
});
return userSessionCount;
Expand Down Expand Up @@ -974,6 +985,23 @@ export class UserRepository {
}
}

async deleteSessionBySessionId(sessionId: string, userId: string): Promise<{ message: string }> {
try {
const result = await this.prisma.session.deleteMany({
where: { id: sessionId, userId }
});

if (0 === result.count) {
throw new RpcException(new NotFoundException(`Session not found for userId: ${userId}`));
}

return { message: 'Session deleted successfully' };
} catch (error) {
this.logger.error(`Error in Deleting Session: ${error.message}`);
throw error;
}
}

async fetchSessionByRefreshToken(refreshToken: string): Promise<session> {
try {
const sessionDetails = await this.prisma.session.findFirst({
Expand Down
11 changes: 11 additions & 0 deletions apps/user/src/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ICheckUserDetails,
IOrgUsers,
IRestrictedUserSession,
ISessionDetails,
ISessions,
IUserDeletedActivity,
Expand Down Expand Up @@ -89,6 +90,16 @@ export class UserController {
return this.userService.refreshTokenDetails(refreshToken);
}

@MessagePattern({ cmd: 'session-details-by-userId' })
async userSessions(userId: string): Promise<IRestrictedUserSession[]> {
return this.userService.userSessions(userId);
}

@MessagePattern({ cmd: 'delete-session-by-sessionId' })
async deleteSession(payload: { sessionId: string; userId: string }): Promise<{ message: string }> {
return this.userService.deleteSession(payload.sessionId, payload.userId);
}

@MessagePattern({ cmd: 'user-reset-password' })
async resetPassword(payload: IUserResetPassword): Promise<IResetPasswordResponse> {
return this.userService.resetPassword(payload);
Expand Down
Loading