From 483fc6375f546ba201f6997ecd9c962bbef7449e Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 1 Jun 2024 19:56:07 +0300 Subject: [PATCH 1/2] feat: Add unit invitation code functionality and user verification using code --- .../src/modules/space/dtos/space.dto.ts | 4 ++ .../modules/space/entities/space.entity.ts | 8 ++- package-lock.json | 18 ++++++ package.json | 1 + .../controllers/building.controller.ts | 2 +- .../controllers/community.controller.ts | 2 +- src/floor/controllers/floor.controller.ts | 2 +- src/room/controllers/room.controller.ts | 2 +- src/unit/controllers/unit.controller.ts | 43 ++++++++++++- src/unit/dtos/add.unit.dto.ts | 26 ++++++++ src/unit/services/unit.service.ts | 62 ++++++++++++++++++- 11 files changed, 161 insertions(+), 9 deletions(-) diff --git a/libs/common/src/modules/space/dtos/space.dto.ts b/libs/common/src/modules/space/dtos/space.dto.ts index cc5ad83..98706d0 100644 --- a/libs/common/src/modules/space/dtos/space.dto.ts +++ b/libs/common/src/modules/space/dtos/space.dto.ts @@ -16,4 +16,8 @@ export class SpaceDto { @IsString() @IsNotEmpty() public spaceTypeUuid: string; + + @IsString() + @IsNotEmpty() + public invitationCode: string; } diff --git a/libs/common/src/modules/space/entities/space.entity.ts b/libs/common/src/modules/space/entities/space.entity.ts index 56f7010..b337e91 100644 --- a/libs/common/src/modules/space/entities/space.entity.ts +++ b/libs/common/src/modules/space/entities/space.entity.ts @@ -1,4 +1,4 @@ -import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; +import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm'; import { SpaceDto } from '../dtos'; import { AbstractEntity } from '../../abstract/entities/abstract.entity'; import { SpaceTypeEntity } from '../../space-type/entities'; @@ -6,6 +6,7 @@ import { UserSpaceEntity } from '../../user-space/entities'; import { DeviceEntity } from '../../device/entities'; @Entity({ name: 'space' }) +@Unique(['invitationCode']) export class SpaceEntity extends AbstractEntity { @Column({ type: 'uuid', @@ -18,6 +19,11 @@ export class SpaceEntity extends AbstractEntity { nullable: false, }) public spaceName: string; + + @Column({ + nullable: true, + }) + public invitationCode: string; @ManyToOne(() => SpaceEntity, (space) => space.children, { nullable: true }) parent: SpaceEntity; diff --git a/package-lock.json b/package-lock.json index f333ed5..a629215 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "helmet": "^7.1.0", "ioredis": "^5.3.2", "morgan": "^1.10.0", + "nanoid": "^5.0.7", "nodemailer": "^6.9.10", "passport-jwt": "^4.0.1", "pg": "^8.11.3", @@ -7165,6 +7166,23 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nanoid": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", + "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", diff --git a/package.json b/package.json index a8502be..a5d2431 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "helmet": "^7.1.0", "ioredis": "^5.3.2", "morgan": "^1.10.0", + "nanoid": "^5.0.7", "nodemailer": "^6.9.10", "passport-jwt": "^4.0.1", "pg": "^8.11.3", diff --git a/src/building/controllers/building.controller.ts b/src/building/controllers/building.controller.ts index 34c973a..a76d620 100644 --- a/src/building/controllers/building.controller.ts +++ b/src/building/controllers/building.controller.ts @@ -30,7 +30,7 @@ export class BuildingController { constructor(private readonly buildingService: BuildingService) {} @ApiBearerAuth() - @UseGuards(AdminRoleGuard, CheckCommunityTypeGuard) + @UseGuards(JwtAuthGuard, CheckCommunityTypeGuard) @Post() async addBuilding(@Body() addBuildingDto: AddBuildingDto) { try { diff --git a/src/community/controllers/community.controller.ts b/src/community/controllers/community.controller.ts index df59979..c88acf1 100644 --- a/src/community/controllers/community.controller.ts +++ b/src/community/controllers/community.controller.ts @@ -32,7 +32,7 @@ export class CommunityController { constructor(private readonly communityService: CommunityService) {} @ApiBearerAuth() - @UseGuards(AdminRoleGuard) + @UseGuards(JwtAuthGuard) @Post() async addCommunity(@Body() addCommunityDto: AddCommunityDto) { try { diff --git a/src/floor/controllers/floor.controller.ts b/src/floor/controllers/floor.controller.ts index 0df9efc..b4940fe 100644 --- a/src/floor/controllers/floor.controller.ts +++ b/src/floor/controllers/floor.controller.ts @@ -30,7 +30,7 @@ export class FloorController { constructor(private readonly floorService: FloorService) {} @ApiBearerAuth() - @UseGuards(AdminRoleGuard, CheckBuildingTypeGuard) + @UseGuards(JwtAuthGuard, CheckBuildingTypeGuard) @Post() async addFloor(@Body() addFloorDto: AddFloorDto) { try { diff --git a/src/room/controllers/room.controller.ts b/src/room/controllers/room.controller.ts index 434370d..0a92e57 100644 --- a/src/room/controllers/room.controller.ts +++ b/src/room/controllers/room.controller.ts @@ -28,7 +28,7 @@ export class RoomController { constructor(private readonly roomService: RoomService) {} @ApiBearerAuth() - @UseGuards(AdminRoleGuard, CheckUnitTypeGuard) + @UseGuards(JwtAuthGuard, CheckUnitTypeGuard) @Post() async addRoom(@Body() addRoomDto: AddRoomDto) { try { diff --git a/src/unit/controllers/unit.controller.ts b/src/unit/controllers/unit.controller.ts index 7c410b6..5c55bc7 100644 --- a/src/unit/controllers/unit.controller.ts +++ b/src/unit/controllers/unit.controller.ts @@ -12,7 +12,11 @@ import { UseGuards, } from '@nestjs/common'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; -import { AddUnitDto, AddUserUnitDto } from '../dtos/add.unit.dto'; +import { + AddUnitDto, + AddUserUnitDto, + AddUserUnitUsingCodeDto, +} from '../dtos/add.unit.dto'; import { GetUnitChildDto } from '../dtos/get.unit.dto'; import { UpdateUnitNameDto } from '../dtos/update.unit.dto'; import { CheckFloorTypeGuard } from 'src/guards/floor.type.guard'; @@ -30,7 +34,7 @@ export class UnitController { constructor(private readonly unitService: UnitService) {} @ApiBearerAuth() - @UseGuards(AdminRoleGuard, CheckFloorTypeGuard) + @UseGuards(JwtAuthGuard, CheckFloorTypeGuard) @Post() async addUnit(@Body() addUnitDto: AddUnitDto) { try { @@ -147,4 +151,39 @@ export class UnitController { ); } } + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, UnitPermissionGuard) + @Get(':unitUuid/invitation-code') + async getUnitInvitationCode(@Param('unitUuid') unitUuid: string) { + try { + const unit = await this.unitService.getUnitInvitationCode(unitUuid); + return unit; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @Post('user/verify-code') + async verifyCodeAndAddUserUnit( + @Body() addUserUnitUsingCodeDto: AddUserUnitUsingCodeDto, + ) { + try { + await this.unitService.verifyCodeAndAddUserUnit(addUserUnitUsingCodeDto); + return { + statusCode: HttpStatus.CREATED, + success: true, + message: 'user unit added successfully', + }; + } catch (error) { + throw new HttpException( + error.message || 'Internal server error', + error.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } } diff --git a/src/unit/dtos/add.unit.dto.ts b/src/unit/dtos/add.unit.dto.ts index e42d1bb..6d6c52a 100644 --- a/src/unit/dtos/add.unit.dto.ts +++ b/src/unit/dtos/add.unit.dto.ts @@ -40,3 +40,29 @@ export class AddUserUnitDto { Object.assign(this, dto); } } +export class AddUserUnitUsingCodeDto { + @ApiProperty({ + description: 'unitUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public unitUuid: string; + @ApiProperty({ + description: 'userUuid', + required: true, + }) + @IsString() + @IsNotEmpty() + public userUuid: string; + @ApiProperty({ + description: 'inviteCode', + required: true, + }) + @IsString() + @IsNotEmpty() + public inviteCode: string; + constructor(dto: Partial) { + Object.assign(this, dto); + } +} diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts index 11d654f..d6d93b6 100644 --- a/src/unit/services/unit.service.ts +++ b/src/unit/services/unit.service.ts @@ -1,4 +1,5 @@ import { GetUnitChildDto } from '../dtos/get.unit.dto'; +import { nanoid } from 'nanoid'; import { SpaceTypeRepository } from '../../../libs/common/src/modules/space-type/repositories/space.type.repository'; import { Injectable, @@ -7,7 +8,7 @@ import { BadRequestException, } from '@nestjs/common'; import { SpaceRepository } from '@app/common/modules/space/repositories'; -import { AddUnitDto, AddUserUnitDto } from '../dtos'; +import { AddUnitDto, AddUserUnitDto, AddUserUnitUsingCodeDto } from '../dtos'; import { UnitChildInterface, UnitParentInterface, @@ -227,7 +228,7 @@ export class UnitService { async addUserUnit(addUserUnitDto: AddUserUnitDto) { try { - await this.userSpaceRepository.save({ + return await this.userSpaceRepository.save({ user: { uuid: addUserUnitDto.userUuid }, space: { uuid: addUserUnitDto.unitUuid }, }); @@ -282,4 +283,61 @@ export class UnitService { } } } + async getUnitInvitationCode(unitUuid: string): Promise { + try { + // Generate a 6-character random invitation code + const invitationCode = nanoid(6); + + // Update the unit with the new invitation code + await this.spaceRepository.update({ uuid: unitUuid }, { invitationCode }); + + // Fetch the updated unit + const updatedUnit = await this.spaceRepository.findOneOrFail({ + where: { uuid: unitUuid }, + relations: ['spaceType'], + }); + + return { + uuid: updatedUnit.uuid, + invitationCode: updatedUnit.invitationCode, + type: updatedUnit.spaceType.type, + }; + } catch (err) { + if (err instanceof BadRequestException) { + throw err; + } else { + throw new HttpException('Unit not found', HttpStatus.NOT_FOUND); + } + } + } + async verifyCodeAndAddUserUnit( + addUserUnitUsingCodeDto: AddUserUnitUsingCodeDto, + ) { + try { + const unit = await this.spaceRepository.findOneOrFail({ + where: { + invitationCode: addUserUnitUsingCodeDto.inviteCode, + spaceType: { type: 'unit' }, + }, + relations: ['spaceType'], + }); + if (unit.invitationCode) { + const user = await this.addUserUnit({ + userUuid: addUserUnitUsingCodeDto.userUuid, + unitUuid: unit.uuid, + }); + if (user.uuid) { + await this.spaceRepository.update( + { uuid: unit.uuid }, + { invitationCode: null }, + ); + } + } + } catch (err) { + throw new HttpException( + 'Invalid invitation code', + HttpStatus.BAD_REQUEST, + ); + } + } } From 0e99df8037aa9528a751bb0ecfc1be1f509d9804 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sat, 1 Jun 2024 20:04:04 +0300 Subject: [PATCH 2/2] Refactor getUnitInvitationCode method to use dynamic import for nanoid library --- src/unit/services/unit.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/unit/services/unit.service.ts b/src/unit/services/unit.service.ts index d6d93b6..8051469 100644 --- a/src/unit/services/unit.service.ts +++ b/src/unit/services/unit.service.ts @@ -1,5 +1,4 @@ import { GetUnitChildDto } from '../dtos/get.unit.dto'; -import { nanoid } from 'nanoid'; import { SpaceTypeRepository } from '../../../libs/common/src/modules/space-type/repositories/space.type.repository'; import { Injectable, @@ -285,6 +284,8 @@ export class UnitService { } async getUnitInvitationCode(unitUuid: string): Promise { try { + const { nanoid } = await import('nanoid'); + // Generate a 6-character random invitation code const invitationCode = nanoid(6);