From 41da5289636f59930c39fdaafd3e97c2bbcb8547 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:33:44 -0600 Subject: [PATCH 1/3] Add agreement acceptance fields to UserEntity --- libs/common/src/modules/user/entities/user.entity.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts index 097b154..023864a 100644 --- a/libs/common/src/modules/user/entities/user.entity.ts +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -82,6 +82,18 @@ export class UserEntity extends AbstractEntity { }) 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[]; From 6dd6c79d87ad3e0d831f4da641d7123ba19a6e83 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:34:47 -0600 Subject: [PATCH 2/3] Add app agreement acceptance check and validation --- libs/common/src/auth/services/auth.service.ts | 6 +++++- src/auth/dtos/user-auth.dto.ts | 20 ++++++++++++++++++- src/auth/services/user-auth.service.ts | 12 +++++++++-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index bc25e0e..528db56 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -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); diff --git a/src/auth/dtos/user-auth.dto.ts b/src/auth/dtos/user-auth.dto.ts index dad1e07..d2f2e8a 100644 --- a/src/auth/dtos/user-auth.dto.ts +++ b/src/auth/dtos/user-auth.dto.ts @@ -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; } diff --git a/src/auth/services/user-auth.service.ts b/src/auth/services/user-auth.service.ts index afdf6d2..c9c9436 100644 --- a/src/auth/services/user-auth.service.ts +++ b/src/auth/services/user-auth.service.ts @@ -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; From f675064b68c4bf769ba39c17f4b9fb4a5e82c5ee Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:34:54 -0600 Subject: [PATCH 3/3] Add endpoint to update user web agreement --- libs/common/src/constants/controller-route.ts | 4 +++ src/users/controllers/user.controller.ts | 16 +++++++++++ src/users/services/user-space.service.ts | 5 +--- src/users/services/user.service.ts | 27 +++++++++++++++---- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index d82aebe..4cee3fe 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -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 { diff --git a/src/users/controllers/user.controller.ts b/src/users/controllers/user.controller.ts index 6f0eded..ce9991a 100644 --- a/src/users/controllers/user.controller.ts +++ b/src/users/controllers/user.controller.ts @@ -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 { + return this.userService.acceptWebAgreement(userUuid); + } } diff --git a/src/users/services/user-space.service.ts b/src/users/services/user-space.service.ts index fc15c3d..e06bbd5 100644 --- a/src/users/services/user-space.service.ts +++ b/src/users/services/user-space.service.ts @@ -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); diff --git a/src/users/services/user.service.ts b/src/users/services/user.service.ts index 96a669b..268dc7c 100644 --- a/src/users/services/user.service.ts +++ b/src/users/services/user.service.ts @@ -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 { return await this.userRepository.findOne({ where: { uuid: id } }); }