Merge pull request #220 from SyncrowIOT/SP-1086-be-implement-the-user-agreement-flow

Sp 1086 be implement the user agreement flow
This commit is contained in:
faris Aljohari
2025-01-22 02:49:00 -06:00
committed by GitHub
8 changed files with 89 additions and 13 deletions

View File

@ -48,7 +48,9 @@ export class AuthService {
if (!user.isActive) {
throw new BadRequestException('User is not active');
}
if (!user.hasAcceptedAppAgreement) {
throw new BadRequestException('User has not accepted app agreement');
}
const passwordMatch = await this.helperHashService.bcryptCompare(
pass,
user.password,
@ -92,6 +94,8 @@ export class AuthService {
sessionId: user.sessionId,
role: user?.role,
googleCode: user.googleCode,
hasAcceptedWebAgreement: user.hasAcceptedWebAgreement,
hasAcceptedAppAgreement: user.hasAcceptedAppAgreement,
};
if (payload.googleCode) {
const profile = await this.getProfile(payload.googleCode);

View File

@ -349,6 +349,10 @@ export class ControllerRoute {
public static readonly DELETE_USER_SUMMARY = 'Delete user by UUID';
public static readonly DELETE_USER_DESCRIPTION =
'This endpoint deletes a user identified by their UUID. Accessible only by users with the Super Admin role.';
public static readonly UPDATE_USER_WEB_AGREEMENT_SUMMARY =
'Update user web agreement by user UUID';
public static readonly UPDATE_USER_WEB_AGREEMENT_DESCRIPTION =
'This endpoint updates the web agreement for a user identified by their UUID.';
};
};
static AUTHENTICATION = class {

View File

@ -82,6 +82,18 @@ export class UserEntity extends AbstractEntity<UserDto> {
})
public isActive: boolean;
@Column({ default: false })
hasAcceptedWebAgreement: boolean;
@Column({ default: false })
hasAcceptedAppAgreement: boolean;
@Column({ type: 'timestamp', nullable: true })
webAgreementAcceptedAt: Date;
@Column({ type: 'timestamp', nullable: true })
appAgreementAcceptedAt: Date;
@OneToMany(() => UserSpaceEntity, (userSpace) => userSpace.user)
userSpaces: UserSpaceEntity[];

View File

@ -1,5 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import {
IsBoolean,
IsEmail,
IsNotEmpty,
IsOptional,
IsString,
} from 'class-validator';
import { IsPasswordStrong } from 'src/validators/password.validator';
export class UserSignUpDto {
@ -39,7 +45,19 @@ export class UserSignUpDto {
@IsNotEmpty()
public lastName: string;
@ApiProperty({
description: 'regionUuid',
required: false,
})
@IsString()
@IsOptional()
public regionUuid?: string;
@ApiProperty({
description: 'hasAcceptedAppAgreement',
required: true,
})
@IsBoolean()
@IsNotEmpty()
public hasAcceptedAppAgreement: boolean;
}

View File

@ -46,12 +46,17 @@ export class UserAuthService {
);
try {
const { regionUuid, ...rest } = userSignUpDto;
const { regionUuid, hasAcceptedAppAgreement, ...rest } = userSignUpDto;
if (!hasAcceptedAppAgreement) {
throw new BadRequestException('Please accept the terms and conditions');
}
const spaceMemberRole = await this.roleService.findRoleByType(
RoleType.SPACE_MEMBER,
);
const user = await this.userRepository.save({
...rest,
appAgreementAcceptedAt: new Date(),
hasAcceptedAppAgreement,
password: hashedPassword,
roleType: { uuid: spaceMemberRole.uuid },
region: regionUuid
@ -65,7 +70,7 @@ export class UserAuthService {
return user;
} catch (error) {
throw new BadRequestException('Failed to register user');
throw new BadRequestException(error.message || 'Failed to register user');
}
}
@ -116,6 +121,7 @@ export class UserAuthService {
firstName: googleUserData['given_name'],
lastName: googleUserData['family_name'],
password: googleUserData['email'],
hasAcceptedAppAgreement: true,
});
}
data.email = googleUserData['email'];
@ -147,6 +153,8 @@ export class UserAuthService {
userId: user.uuid,
uuid: user.uuid,
role: user.roleType,
hasAcceptedWebAgreement: user.hasAcceptedWebAgreement,
hasAcceptedAppAgreement: user.hasAcceptedAppAgreement,
sessionId: session[1].uuid,
});
return res;

View File

@ -5,6 +5,7 @@ import {
Get,
HttpStatus,
Param,
Patch,
Put,
UseGuards,
} from '@nestjs/common';
@ -21,6 +22,7 @@ import { CheckProfilePictureGuard } from 'src/guards/profile.picture.guard';
import { SuperAdminRoleGuard } from 'src/guards/super.admin.role.guard';
import { EnableDisableStatusEnum } from '@app/common/constants/days.enum';
import { ControllerRoute } from '@app/common/constants/controller-route';
import { BaseResponseDto } from '@app/common/dto/base.response.dto';
@ApiTags('User Module')
@Controller({
@ -151,4 +153,18 @@ export class UserController {
message: 'User deleted successfully',
};
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Patch('agreements/web/:userUuid')
@ApiOperation({
summary: ControllerRoute.USER.ACTIONS.UPDATE_USER_WEB_AGREEMENT_SUMMARY,
description:
ControllerRoute.USER.ACTIONS.UPDATE_USER_WEB_AGREEMENT_DESCRIPTION,
})
async acceptWebAgreement(
@Param('userUuid') userUuid: string,
): Promise<BaseResponseDto> {
return this.userService.acceptWebAgreement(userUuid);
}
}

View File

@ -59,10 +59,7 @@ export class UserSpaceService {
const { inviteCode } = params;
try {
const inviteSpace = await this.findInviteSpaceByInviteCode(inviteCode);
const user = await this.userService.getUserDetailsByUserUuid(
userUuid,
true,
);
const user = await this.userService.getUserDetailsByUserUuid(userUuid);
await this.checkSpaceMemberRole(user);
await this.addUserToSpace(userUuid, inviteSpace.space.uuid);

View File

@ -15,6 +15,7 @@ import { RegionRepository } from '@app/common/modules/region/repositories';
import { TimeZoneRepository } from '@app/common/modules/timezone/repositories';
import { removeBase64Prefix } from '@app/common/helper/removeBase64Prefix';
import { UserEntity } from '@app/common/modules/user/entities';
import { SuccessResponseDto } from '@app/common/dto/success.response.dto';
@Injectable()
export class UserService {
@ -23,15 +24,13 @@ export class UserService {
private readonly regionRepository: RegionRepository,
private readonly timeZoneRepository: TimeZoneRepository,
) {}
async getUserDetailsByUserUuid(userUuid: string, withRole = false) {
async getUserDetailsByUserUuid(userUuid: string) {
try {
const user = await this.userRepository.findOne({
where: {
uuid: userUuid,
},
...(withRole
? { relations: ['roleType'] }
: { relations: ['region', 'timezone'] }),
relations: ['region', 'timezone', 'roleType'],
});
if (!user) {
throw new BadRequestException('Invalid room UUID');
@ -48,7 +47,11 @@ export class UserService {
profilePicture: cleanedProfilePicture,
region: user?.region,
timeZone: user?.timezone,
...(withRole && { role: user?.roleType }),
hasAcceptedWebAgreement: user?.hasAcceptedWebAgreement,
webAgreementAcceptedAt: user?.webAgreementAcceptedAt,
hasAcceptedAppAgreement: user?.hasAcceptedAppAgreement,
appAgreementAcceptedAt: user?.appAgreementAcceptedAt,
role: user?.roleType,
};
} catch (err) {
if (err instanceof BadRequestException) {
@ -241,6 +244,20 @@ export class UserService {
);
}
}
async acceptWebAgreement(userUuid: string) {
await this.userRepository.update(
{ uuid: userUuid },
{
hasAcceptedWebAgreement: true,
webAgreementAcceptedAt: new Date(),
},
);
return new SuccessResponseDto({
statusCode: HttpStatus.OK,
success: true,
message: 'Web agreement accepted successfully',
});
}
async findOneById(id: string): Promise<UserEntity> {
return await this.userRepository.findOne({ where: { uuid: id } });
}