feat: add user profile picture, region, and timezone update functionality

This commit is contained in:
faris Aljohari
2024-07-15 15:47:13 +03:00
parent 4a87a42827
commit 4fc880d869
7 changed files with 377 additions and 65 deletions

View File

@ -0,0 +1,45 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { BadRequestException } from '@nestjs/common';
@Injectable()
export class CheckProfilePictureGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
try {
if (req.body) {
const { profilePicture } = req.body;
if (profilePicture) {
const isBase64 = /^data:image\/[a-z]+;base64,/.test(profilePicture);
if (!isBase64) {
throw new BadRequestException(
'Profile picture must be in base64 format.',
);
}
// Get the size of the base64 string (in bytes)
const base64StringLength =
profilePicture.length - 'data:image/[a-z]+;base64,'.length;
const base64ImageSizeInBytes = base64StringLength * 0.75; // Base64 encoding expands data by 33%
const maxSizeInBytes = 1 * 1024 * 1024; // 1 MB
// Check if the size exceeds the limit
if (base64ImageSizeInBytes > maxSizeInBytes) {
throw new BadRequestException(
'Profile picture size exceeds the allowed limit.',
);
}
}
// Check if profilePicture is a base64 string
} else {
throw new BadRequestException('Invalid request parameters');
}
return true;
} catch (error) {
console.log('Profile picture guard error: ', error);
throw error;
}
}
}

View File

@ -1,8 +1,22 @@
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import {
Body,
Controller,
Get,
HttpException,
HttpStatus,
Param,
Put,
UseGuards,
} from '@nestjs/common';
import { UserService } from '../services/user.service';
import { UserListDto } from '../dtos/user.list.dto';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { AdminRoleGuard } from 'src/guards/admin.role.guard';
import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard';
import {
UpdateProfilePictureDataDto,
UpdateRegionDataDto,
UpdateTimezoneDataDto,
} from '../dtos';
import { CheckProfilePictureGuard } from 'src/guards/profile.picture.guard';
@ApiTags('User Module')
@Controller({
@ -13,13 +27,91 @@ export class UserController {
constructor(private readonly userService: UserService) {}
@ApiBearerAuth()
@UseGuards(AdminRoleGuard)
@Get('list')
async userList(@Query() userListDto: UserListDto) {
@UseGuards(JwtAuthGuard)
@Get(':userUuid')
async getUserDetailsByUserUuid(@Param('userUuid') userUuid: string) {
try {
return await this.userService.userDetails(userListDto);
} catch (err) {
throw new Error(err);
return await this.userService.getUserDetailsByUserUuid(userUuid);
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, CheckProfilePictureGuard)
@Put('/profile-picture/:userUuid')
async updateProfilePictureByUserUuid(
@Param('userUuid') userUuid: string,
@Body() updateProfilePictureDataDto: UpdateProfilePictureDataDto,
) {
try {
const userData = await this.userService.updateProfilePictureByUserUuid(
userUuid,
updateProfilePictureDataDto,
);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'profile picture updated successfully',
data: userData,
};
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put('/region/:userUuid')
async updateRegionByUserUuid(
@Param('userUuid') userUuid: string,
@Body() updateRegionDataDto: UpdateRegionDataDto,
) {
try {
const userData = await this.userService.updateRegionByUserUuid(
userUuid,
updateRegionDataDto,
);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'region updated successfully',
data: userData,
};
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put('/timezone/:userUuid')
async updateTimezoneByUserUuid(
@Param('userUuid') userUuid: string,
@Body() updateTimezoneDataDto: UpdateTimezoneDataDto,
) {
try {
const userData = await this.userService.updateTimezoneByUserUuid(
userUuid,
updateTimezoneDataDto,
);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'timezone updated successfully',
data: userData,
};
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -1 +1 @@
export * from './user.list.dto';
export * from './update.user.dto';

View File

@ -0,0 +1,42 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class UpdateProfilePictureDataDto {
@ApiProperty({
description: 'profilePicture',
required: true,
})
@IsString()
@IsNotEmpty()
public profilePicture: string;
constructor(dto: Partial<UpdateProfilePictureDataDto>) {
Object.assign(this, dto);
}
}
export class UpdateRegionDataDto {
@ApiProperty({
description: 'regionUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public regionUuid: string;
constructor(dto: Partial<UpdateProfilePictureDataDto>) {
Object.assign(this, dto);
}
}
export class UpdateTimezoneDataDto {
@ApiProperty({
description: 'timezoneUuid',
required: true,
})
@IsString()
@IsNotEmpty()
public timezoneUuid: string;
constructor(dto: Partial<UpdateProfilePictureDataDto>) {
Object.assign(this, dto);
}
}

View File

@ -1,32 +0,0 @@
import {
IsNotEmpty,
IsNumberString,
IsOptional,
IsString,
} from 'class-validator';
export class UserListDto {
@IsString()
@IsOptional()
schema: string;
@IsNumberString()
@IsNotEmpty()
page_no: number;
@IsNumberString()
@IsNotEmpty()
page_size: number;
@IsString()
@IsOptional()
username: string;
@IsNumberString()
@IsOptional()
start_time: number;
@IsNumberString()
@IsOptional()
end_time: number;
}

View File

@ -1,28 +1,185 @@
import { Injectable } from '@nestjs/common';
import { TuyaContext } from '@tuya/tuya-connector-nodejs';
import { UserListDto } from '../dtos/user.list.dto';
import { ConfigService } from '@nestjs/config';
import {
UpdateProfilePictureDataDto,
UpdateRegionDataDto,
UpdateTimezoneDataDto,
} from './../dtos/update.user.dto';
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { UserRepository } from '@app/common/modules/user/repositories';
import { RegionRepository } from '@app/common/modules/region/repositories';
import { TimeZoneRepository } from '@app/common/modules/timezone/repositories';
@Injectable()
export class UserService {
private tuya: TuyaContext;
constructor(private readonly configService: ConfigService) {
const accessKey = this.configService.get<string>('auth-config.ACCESS_KEY');
const secretKey = this.configService.get<string>('auth-config.SECRET_KEY');
const tuyaEuUrl = this.configService.get<string>('tuya-config.TUYA_EU_URL');
this.tuya = new TuyaContext({
baseUrl: tuyaEuUrl,
accessKey,
secretKey,
});
constructor(
private readonly userRepository: UserRepository,
private readonly regionRepository: RegionRepository,
private readonly timeZoneRepository: TimeZoneRepository,
) {}
async getUserDetailsByUserUuid(userUuid: string) {
try {
const user = await this.userRepository.findOne({
where: {
uuid: userUuid,
},
relations: ['region', 'timezone'],
});
if (!user) {
throw new BadRequestException('Invalid room UUID');
}
return {
uuid: user.uuid,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
profilePicture: user.profilePicture,
region: user.region,
timeZone: user.timezone,
};
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
}
}
async userDetails(userListDto: UserListDto) {
const path = `/v2.0/apps/${userListDto.schema}/users`;
const data = await this.tuya.request({
method: 'GET',
path,
query: userListDto,
});
return data;
async updateProfilePictureByUserUuid(
userUuid: string,
updateProfilePictureDataDto: UpdateProfilePictureDataDto,
) {
try {
await this.userRepository.update(
{ uuid: userUuid },
{ ...updateProfilePictureDataDto },
);
const updatedUser = await this.getUserDetailsByUserUuid(userUuid);
return {
uuid: updatedUser.uuid,
firstName: updatedUser.firstName,
lastName: updatedUser.lastName,
profilePicture: updatedUser.profilePicture,
region: updatedUser.region,
timeZoneUuid: updatedUser.timeZone,
};
} catch (err) {
console.log('err', err);
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
}
}
async updateRegionByUserUuid(
userUuid: string,
updateRegionDataDto: UpdateRegionDataDto,
) {
try {
const user = await this.getUserDetailsByUserUuid(userUuid);
if (!user) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
// Ensure the region UUID is provided
if (!updateRegionDataDto.regionUuid) {
throw new BadRequestException('Region UUID is required');
}
// Ensure the region exists
const region = await this.regionRepository.findOne({
where: {
uuid: updateRegionDataDto.regionUuid,
},
});
if (!region) {
throw new BadRequestException('Invalid region UUID');
}
await this.userRepository.update(
{ uuid: userUuid },
{
region: region,
},
);
const updatedUser = await this.getUserDetailsByUserUuid(userUuid);
if (!updatedUser.region) {
throw new BadRequestException('Region update failed');
}
return {
uuid: updatedUser.uuid,
firstName: updatedUser.firstName,
lastName: updatedUser.lastName,
profilePicture: updatedUser.profilePicture,
region: updatedUser.region,
timeZoneUuid: updatedUser.timeZone,
};
} catch (err) {
throw new HttpException(
err.message || 'User not found',
HttpStatus.NOT_FOUND,
);
}
}
async updateTimezoneByUserUuid(
userUuid: string,
updateTimezoneDataDto: UpdateTimezoneDataDto,
) {
try {
const user = await this.getUserDetailsByUserUuid(userUuid);
if (!user) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
// Ensure the region UUID is provided
if (!updateTimezoneDataDto.timezoneUuid) {
throw new BadRequestException('Timezone UUID is required');
}
// Ensure the region exists
const timezone = await this.timeZoneRepository.findOne({
where: {
uuid: updateTimezoneDataDto.timezoneUuid,
},
});
if (!timezone) {
throw new BadRequestException('Invalid timezone UUID');
}
await this.userRepository.update(
{ uuid: userUuid },
{
timezone: timezone,
},
);
const updatedUser = await this.getUserDetailsByUserUuid(userUuid);
if (!updatedUser.timeZone) {
throw new BadRequestException('Timezone update failed');
}
return {
uuid: updatedUser.uuid,
firstName: updatedUser.firstName,
lastName: updatedUser.lastName,
profilePicture: updatedUser.profilePicture,
region: updatedUser.region,
timeZoneUuid: updatedUser.timeZone,
};
} catch (err) {
throw new HttpException(
err.message || 'User not found',
HttpStatus.NOT_FOUND,
);
}
}
}

View File

@ -2,11 +2,19 @@ import { Module } from '@nestjs/common';
import { UserService } from './services/user.service';
import { UserController } from './controllers/user.controller';
import { ConfigModule } from '@nestjs/config';
import { UserRepository } from '@app/common/modules/user/repositories';
import { RegionRepository } from '@app/common/modules/region/repositories';
import { TimeZoneRepository } from '@app/common/modules/timezone/repositories';
@Module({
imports: [ConfigModule],
controllers: [UserController],
providers: [UserService],
providers: [
UserService,
UserRepository,
RegionRepository,
TimeZoneRepository,
],
exports: [UserService],
})
export class UserModule {}