Skip to content

Commit

Permalink
Merge pull request #145 from Xitija/main
Browse files Browse the repository at this point in the history
PS-3707 : Implement addition of custom role in keycloak token
  • Loading branch information
snehal0904 authored Jan 30, 2025
2 parents 48bf2e8 + 71fe59b commit 1d4e6c5
Show file tree
Hide file tree
Showing 7 changed files with 46 additions and 24 deletions.
33 changes: 16 additions & 17 deletions src/adapters/postgres/user-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ export class PostgresUserService implements IServicelocator {
if (key === "firstName") {
whereCondition += ` U."${key}" ILIKE '%${value}%'`;
} else {
if (key === "status" || key === "email") {
if (key === "status" || key === "email" || key === "username") {
if (
Array.isArray(value) &&
value.every((item) => typeof item === "string")
Expand Down Expand Up @@ -1060,7 +1060,6 @@ export class PostgresUserService implements IServicelocator {
userCreateDto.username = userCreateDto.username.toLocaleLowerCase();
const userSchema = new UserCreateDto(userCreateDto);

let errKeycloak = "";
let resKeycloak;

const keycloakResponse = await getKeycloakAdminToken();
Expand All @@ -1079,8 +1078,8 @@ export class PostgresUserService implements IServicelocator {
);
}

resKeycloak = await createUserInKeyCloak(userSchema, token)

// Multi tenant for roles is not currently supported in keycloak
resKeycloak = await createUserInKeyCloak(userSchema, token, validatedRoles[0]?.title)

if (resKeycloak.statusCode !== 201) {
if (resKeycloak.statusCode === 409) {
Expand Down Expand Up @@ -1345,7 +1344,7 @@ export class PostgresUserService implements IServicelocator {
}


// Can be Implemeneted after we know what are the unique entties
// Can be Implemented after we know what are the unique entities
async checkUserinKeyCloakandDb(userDto) {
const keycloakResponse = await getKeycloakAdminToken();
const token = keycloakResponse.data.access_token;
Expand Down Expand Up @@ -1403,7 +1402,7 @@ export class PostgresUserService implements IServicelocator {
query
);

// will add data only if cohort is found with acadmic year
// will add data only if cohort is found with academic year
let cohortData = {
userId: result?.userId,
cohortId: cohortIds,
Expand Down Expand Up @@ -1764,7 +1763,7 @@ export class PostgresUserService implements IServicelocator {
const invalidFieldIds = userCreateDto.customFields
.filter((fieldValue) => !validFieldIds.has(fieldValue.fieldId))
.map((fieldValue) => fieldValue.fieldId);

if (invalidFieldIds.length > 0) {
return `The following fields are not valid for this user: ${invalidFieldIds.join(
", "
Expand Down Expand Up @@ -2268,7 +2267,7 @@ export class PostgresUserService implements IServicelocator {
// For other types, return the value as is or implement specific sanitization logic
return value;
}


async suggestUsername(request: Request, response: Response, suggestUserDto: SuggestUserDto) {
const apiId = APIID.USER_LIST;
Expand All @@ -2277,41 +2276,41 @@ export class PostgresUserService implements IServicelocator {
const findData = await this.usersRepository.findOne({
where: { username: suggestUserDto?.username },
});

if (findData) {
// Define a function to generate a username
const generateUsername = (): string => {
const randomNum = randomInt(100, 1000); // Secure random 3-digit number
return `${suggestUserDto.firstName}${suggestUserDto.lastName}${randomNum}`;
};

// Check if the generated username exists in the database
let newUsername = generateUsername();
let isUnique = false;

while (!isUnique) {
const existingUser = await this.usersRepository.findOne({
where: { username: newUsername },
});

if (!existingUser) {
isUnique = true; // Username is unique
} else {
// Generate a new username and try again
newUsername = generateUsername();
}
}

// Return the unique suggested username
return await APIResponse.success(
response,
apiId,
{suggestedUsername: newUsername},
{ suggestedUsername: newUsername },
HttpStatus.OK,
API_RESPONSES.USERNAME_SUGGEST_SUCCESSFULLY
);
}

// If findData is not present, return a message indicating that the user was not found
return APIResponse.error(
response,
Expand All @@ -2320,7 +2319,7 @@ export class PostgresUserService implements IServicelocator {
API_RESPONSES.NOT_FOUND,
HttpStatus.NOT_FOUND
);

} catch (error) {
// Handle errors gracefully
const errorMessage = error.message || API_RESPONSES.SERVER_ERROR;
Expand All @@ -2333,5 +2332,5 @@ export class PostgresUserService implements IServicelocator {
);
}
}

}
6 changes: 5 additions & 1 deletion src/common/utils/keycloak.adapter.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ async function getKeycloakAdminToken() {
}


async function createUserInKeyCloak(query, token) {
async function createUserInKeyCloak(query, token, role: string) {
if (!query.password) {
return "User cannot be created, Password missing";
}
Expand All @@ -75,6 +75,10 @@ async function createUserInKeyCloak(query, token) {
value: query.password,
},
],
attributes : {
// Multi tenant for roles is not currently supported in keycloak
user_roles: [role] // Added in attribute and mappers
}
});

const config = {
Expand Down
3 changes: 3 additions & 0 deletions src/user/dto/otpSend.dto.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsIn, IsMobilePhone, IsString, Matches } from 'class-validator';

export class OtpSendDTO {

@ApiProperty()
@IsString({ message: 'Mobile number must be a string.' })
@IsMobilePhone(null, { message: 'Invalid mobile phone number format.' })
@Matches(/^\d{10}$/, { message: 'Mobile number must be exactly 10 digits.' })
mobile: string;

@ApiProperty()
@IsString({ message: 'Reason must be a string.' })
@IsIn(['signup'], { message: 'Reason must be "signup".' })
reason: string
Expand Down
6 changes: 6 additions & 0 deletions src/user/dto/otpVerify.dto.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsIn, IsString, Length, Matches, ValidateIf } from 'class-validator';

export class OtpVerifyDTO {

@ApiProperty()
@ValidateIf(o => o.reason === 'signup') // Only validate if reason is 'signup'
@IsString({ message: 'Mobile number must be a string.' })
@Matches(/^\d{10}$/, { message: 'Mobile number must be exactly 10 digits.' })
mobile: string;

@ApiProperty()
@IsString({ message: 'OTP must be a string.' })
@Matches(/^\d{6}$/, { message: 'OTP must be exactly 6 digits.' })
otp: string;

@ApiProperty()
@IsString({ message: 'Hash must be a string.' })
@Length(10, 256, { message: 'Hash must be between 10 and 256 characters long.' })
hash: string;

@ApiProperty()
@IsString({ message: 'Reason must be a string.' })
@IsIn(['signup', 'forgot'], { message: 'Reason must be either "signup" or "forgot".' })
reason: string

@ApiProperty()
@ValidateIf(o => o.reason === 'forgot') // Only validate if reason is 'forgot'
@IsString({ message: 'Username must be a string.' })
username: string;
Expand Down
5 changes: 5 additions & 0 deletions src/user/dto/passwordReset.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,23 @@ export class SendPasswordResetLinkDto {
}

export class ResetUserPasswordDto {
@ApiProperty()
userName: string;

@ApiProperty()
@IsString()
@IsNotEmpty()
newPassword: string;
}

export class ForgotPasswordDto {

@ApiProperty()
@IsString()
@IsNotEmpty()
newPassword: string;

@ApiProperty()
@IsString()
@IsNotEmpty()
token: string;
Expand Down
6 changes: 4 additions & 2 deletions src/user/dto/user-search.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ export class setFilters {
role: string;

@ApiPropertyOptional({
type: String,
type: [String],
description: "User Name",
})
username: string;
@IsOptional()
@IsArray()
username: string[];

@ApiPropertyOptional({
type: String,
Expand Down
11 changes: 7 additions & 4 deletions src/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
Delete,
ParseUUIDPipe,
UseFilters,
BadRequestException,
} from "@nestjs/common";

import {
Expand Down Expand Up @@ -204,6 +203,7 @@ export class UserController {

@Post("/forgot-password")
@ApiOkResponse({ description: "Forgot password reset successfully." })
@ApiBody({ type: ForgotPasswordDto })
@UsePipes(new ValidationPipe({ transform: true }))
public async forgotPassword(
@Req() request: Request,
Expand All @@ -222,7 +222,7 @@ export class UserController {
@ApiOkResponse({ description: "Password reset successfully." })
@UsePipes(new ValidationPipe({ transform: true }))
@ApiForbiddenResponse({ description: "Forbidden" })
@ApiBody({ type: Object })
@ApiBody({ type: ResetUserPasswordDto })
public async resetUserPassword(
@Req() request: Request,
@Res() response: Response,
Expand Down Expand Up @@ -259,8 +259,8 @@ export class UserController {
@UseFilters(new AllExceptionsFilter(APIID.SUGGEST_USERNAME))
@Post("/suggestUsername")
@ApiBody({ type: SuggestUserDto })
@ApiOkResponse({ description: "Username suggestion generated successfully" })
@ApiBadRequestResponse({ description: "Invalid input parameters" })
@ApiOkResponse({ description: "Username suggestion generated successfully" })
@ApiBadRequestResponse({ description: "Invalid input parameters" })
@UsePipes(new ValidationPipe())
async suggestUsername(
@Req() request: Request,
Expand Down Expand Up @@ -294,6 +294,7 @@ export class UserController {
.buildUserAdapter()
.deleteUserById(userId, response);
}

@UseFilters(new AllExceptionsFilter(APIID.SEND_OTP))
@Post('send-otp')
@ApiBody({ type: OtpSendDTO })
Expand All @@ -302,6 +303,7 @@ export class UserController {
async sendOtp(@Body() body: OtpSendDTO, @Res() response: Response) {
return await this.userAdapter.buildUserAdapter().sendOtp(body, response)
}

@UseFilters(new AllExceptionsFilter(APIID.VERIFY_OTP))
@Post('verify-otp')
@ApiBody({ type: OtpVerifyDTO })
Expand All @@ -310,6 +312,7 @@ export class UserController {
async verifyOtp(@Body() body: OtpVerifyDTO, @Res() response: Response) {
return this.userAdapter.buildUserAdapter().verifyOtp(body, response);
}

@Post("password-reset-otp")
@ApiOkResponse({ description: "Password reset link sent successfully." })
@UsePipes(new ValidationPipe({ transform: true }))
Expand Down

0 comments on commit 1d4e6c5

Please sign in to comment.