Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
108 changes: 104 additions & 4 deletions apps/api-gateway/src/authz/authz.controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import {
BadRequestException,
Body,
Controller,
Delete,
Get,
HttpStatus,
Logger,
Param,
ParseUUIDPipe,
Post,
Query,
Req,
Expand All @@ -15,10 +18,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 +48,11 @@ 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 UAParser from 'ua-parser-js';
@Controller('auth')
@ApiTags('auth')
@UseFilters(CustomExceptionFilter)
Expand Down Expand Up @@ -158,9 +175,18 @@ 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 parser = new UAParser.UAParser(ua);
const device = {
os: `${parser?.getOS()?.name} ${parser?.getOS()?.version ?? ''}`.trim(),
browser: `${parser?.getBrowser()?.name} ${parser?.getBrowser()?.version ?? ''}`.trim(),
deviceType: parser?.getDevice()?.type || 'desktop'
};
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 +357,78 @@ 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 sesssions 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(
@Res() res: Response,
@Param(
'userId',
new ParseUUIDPipe({
exceptionFactory: (): Error => {
throw new BadRequestException(`Invalid format for User Id`);
}
})
)
userId: string
): Promise<Response> {
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 @@ -16,9 +16,10 @@ import { ResetPasswordDto } from './dtos/reset-password.dto';
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 { session, user } from '@prisma/client';
import { ISessionDetails } from 'apps/user/interfaces/user.interface';
import { UserLogoutDto } from './dtos/user-logout.dto';
import { JsonValue } from 'aws-sdk/clients/glue';
@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: 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<session[]> {
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
35 changes: 20 additions & 15 deletions apps/user/dtos/login-user.dto.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import { trim } from '@credebl/common/cast.helper';
import { ApiProperty } from '@nestjs/swagger';
import { JsonValue } from '@prisma/client/runtime/library';
import { Transform } from 'class-transformer';
import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';

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: 'false' })
@IsOptional()
@IsBoolean({ message: 'isPasskey should be boolean' })
isPasskey?: boolean;
@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()
clientInfo?: JsonValue;
}
3 changes: 3 additions & 0 deletions apps/user/interfaces/user.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { $Enums, Prisma, RecordType } from '@prisma/client';
import { JsonValue } from 'aws-sdk/clients/glue';

export interface IUsersProfile {
id: string;
Expand Down Expand Up @@ -179,6 +180,7 @@ export interface IUserSignIn {
email: string;
password: string;
isPasskey?: boolean;
clientInfo: JsonValue;
}

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

export interface IUpdateAccountDetails {
Expand Down
21 changes: 20 additions & 1 deletion apps/user/repositories/user.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,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 +688,8 @@ export class UserRepository {
refreshToken,
accountId,
sessionType,
expiresAt
expiresAt,
...(tokenDetails.clientInfo ? { clientInfo: tokenDetails.clientInfo } : { clientInfo: { clientToken: true } })
}
});
return sessionResponse;
Expand Down Expand Up @@ -974,6 +976,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
12 changes: 11 additions & 1 deletion apps/user/src/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
IVerifyUserEmail
} from '@credebl/common/interfaces/user.interface';
// eslint-disable-next-line camelcase
import { client_aliases, user, user_org_roles } from '@prisma/client';
import { client_aliases, session, user, user_org_roles } from '@prisma/client';

import { AcceptRejectInvitationDto } from '../dtos/accept-reject-invitation.dto';
import { AddPasskeyDetailsDto } from 'apps/api-gateway/src/user/dto/add-user.dto';
Expand Down Expand Up @@ -89,6 +89,16 @@ export class UserController {
return this.userService.refreshTokenDetails(refreshToken);
}

@MessagePattern({ cmd: 'session-details-by-userId' })
async userSessions(userId: string): Promise<session[]> {
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
25 changes: 22 additions & 3 deletions apps/user/src/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { UserRepository } from '../repositories/user.repository';
import { VerifyEmailTokenDto } from '../dtos/verify-email.dto';
import { sendEmail } from '@credebl/common/send-grid-helper-file';
// eslint-disable-next-line camelcase
import { client_aliases, RecordType, user, user_org_roles } from '@prisma/client';
import { client_aliases, RecordType, session, user, user_org_roles } from '@prisma/client';
import {
ICheckUserDetails,
OrgInvitations,
Expand Down Expand Up @@ -435,7 +435,7 @@ export class UserService {
* @returns User access token details
*/
async login(loginUserDto: LoginUserDto): Promise<ISignInUser> {
const { email, password, isPasskey } = loginUserDto;
const { email, password, isPasskey, clientInfo } = loginUserDto;

try {
this.validateEmail(email.toLowerCase());
Expand Down Expand Up @@ -476,7 +476,8 @@ export class UserService {
expires: tokenDetails?.expires_in,
refreshToken: tokenDetails?.refresh_token,
sessionType: SessionType.USER_SESSION,
expiresAt
expiresAt,
clientInfo
};

const fetchAccountDetails = await this.userRepository.checkAccountDetails(userData?.id);
Expand Down Expand Up @@ -578,6 +579,24 @@ export class UserService {
}
}

async userSessions(userId: string): Promise<session[]> {
try {
return this.userRepository.fetchUserSessions(userId);
} catch (error) {
this.logger.error(`get user sessions: ${JSON.stringify(error)}`);
throw new RpcException(error.response ? error.response : error);
}
}

async deleteSession(sessionId: string, userId: string): Promise<{ message: string }> {
try {
return this.userRepository.deleteSessionBySessionId(sessionId, userId);
} catch (error) {
this.logger.error(`delete session by session id: ${JSON.stringify(error)}`);
throw error;
}
}

async updateFidoVerifiedUser(email: string, isFidoVerified: boolean, password: string): Promise<boolean> {
if (isFidoVerified) {
await this.userRepository.addUserPassword(email.toLowerCase(), password);
Expand Down
4 changes: 3 additions & 1 deletion libs/common/src/response-messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ export const ResponseMessages = {
countriesVerificationCode: 'All countries has been fetched successfully',
stateVerificationCode: 'All states has been fetched successfully',
cityVerificationCode: 'All cities has been fetched successfully',
logout: 'User logout successfully'
logout: 'User logout successfully',
fetchAllSession: 'User Sessions fetched Successfully',
sessionDelete: 'Session deleted Successfully'
},
error: {
exists: 'User already exists',
Expand Down
Loading