Merge pull request #61 from SyncrowIOT/SP-203-handle-user-profile

Sp 203 handle user profile
This commit is contained in:
faris Aljohari
2024-07-21 12:15:19 +03:00
committed by GitHub
42 changed files with 1178 additions and 67 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,197 @@
export const allCountries = [
'Afghanistan',
'Albania',
'Algeria',
'Andorra',
'Angola',
'Antigua and Barbuda',
'Argentina',
'Armenia',
'Australia',
'Austria',
'Azerbaijan',
'Bahamas',
'Bahrain',
'Bangladesh',
'Barbados',
'Belarus',
'Belgium',
'Belize',
'Benin',
'Bhutan',
'Bolivia',
'Bosnia and Herzegovina',
'Botswana',
'Brazil',
'Brunei',
'Bulgaria',
'Burkina Faso',
'Burundi',
'Cabo Verde',
'Cambodia',
'Cameroon',
'Canada',
'Central African Republic',
'Chad',
'Chile',
'China',
'Colombia',
'Comoros',
'Congo (Congo-Brazzaville)',
'Costa Rica',
'Croatia',
'Cuba',
'Cyprus',
'Czech Republic (Czechia)',
'Democratic Republic of the Congo',
'Denmark',
'Djibouti',
'Dominica',
'Dominican Republic',
'Ecuador',
'Egypt',
'El Salvador',
'Equatorial Guinea',
'Eritrea',
'Estonia',
'Eswatini (fmr. "Swaziland")',
'Ethiopia',
'Fiji',
'Finland',
'France',
'Gabon',
'Gambia',
'Georgia',
'Germany',
'Ghana',
'Greece',
'Grenada',
'Guatemala',
'Guinea',
'Guinea-Bissau',
'Guyana',
'Haiti',
'Honduras',
'Hungary',
'Iceland',
'India',
'Indonesia',
'Iran',
'Iraq',
'Ireland',
'Israel',
'Italy',
'Jamaica',
'Japan',
'Jordan',
'Kazakhstan',
'Kenya',
'Kiribati',
'Kuwait',
'Kyrgyzstan',
'Laos',
'Latvia',
'Lebanon',
'Lesotho',
'Liberia',
'Libya',
'Liechtenstein',
'Lithuania',
'Luxembourg',
'Madagascar',
'Malawi',
'Malaysia',
'Maldives',
'Mali',
'Malta',
'Marshall Islands',
'Mauritania',
'Mauritius',
'Mexico',
'Micronesia',
'Moldova',
'Monaco',
'Mongolia',
'Montenegro',
'Morocco',
'Mozambique',
'Myanmar (formerly Burma)',
'Namibia',
'Nauru',
'Nepal',
'Netherlands',
'New Zealand',
'Nicaragua',
'Niger',
'Nigeria',
'North Korea',
'North Macedonia (formerly Macedonia)',
'Norway',
'Oman',
'Pakistan',
'Palau',
'Palestine State',
'Panama',
'Papua New Guinea',
'Paraguay',
'Peru',
'Philippines',
'Poland',
'Portugal',
'Qatar',
'Romania',
'Russia',
'Rwanda',
'Saint Kitts and Nevis',
'Saint Lucia',
'Saint Vincent and the Grenadines',
'Samoa',
'San Marino',
'Sao Tome and Principe',
'Saudi Arabia',
'Senegal',
'Serbia',
'Seychelles',
'Sierra Leone',
'Singapore',
'Slovakia',
'Slovenia',
'Solomon Islands',
'Somalia',
'South Africa',
'South Korea',
'South Sudan',
'Spain',
'Sri Lanka',
'Sudan',
'Suriname',
'Sweden',
'Switzerland',
'Syria',
'Taiwan',
'Tajikistan',
'Tanzania',
'Thailand',
'Timor-Leste',
'Togo',
'Tonga',
'Trinidad and Tobago',
'Tunisia',
'Turkey',
'Turkmenistan',
'Tuvalu',
'Uganda',
'Ukraine',
'United Arab Emirates',
'United Kingdom',
'United States of America',
'Uruguay',
'Uzbekistan',
'Vanuatu',
'Vatican City',
'Venezuela',
'Vietnam',
'Yemen',
'Zambia',
'Zimbabwe',
];

View File

@ -0,0 +1,126 @@
export const allTimeZones = [
{ name: 'International Date Line West', offset: 'GMT-12:00' },
{ name: 'Hawaii', offset: 'GMT-10:00' },
{ name: 'Alaska', offset: 'GMT-09:00' },
{ name: 'Pacific Time (US & Canada)', offset: 'GMT-08:00' },
{ name: 'Arizona', offset: 'GMT-07:00' },
{ name: 'Mountain Time (US & Canada)', offset: 'GMT-07:00' },
{ name: 'Central Time (US & Canada)', offset: 'GMT-06:00' },
{ name: 'Mexico City', offset: 'GMT-06:00' },
{ name: 'Saskatchewan', offset: 'GMT-06:00' },
{ name: 'Eastern Time (US & Canada)', offset: 'GMT-05:00' },
{ name: 'Bogota', offset: 'GMT-05:00' },
{ name: 'Indiana (East)', offset: 'GMT-05:00' },
{ name: 'Atlantic Time (Canada)', offset: 'GMT-04:00' },
{ name: 'Caracas', offset: 'GMT-04:00' },
{ name: 'Santiago', offset: 'GMT-04:00' },
{ name: 'Newfoundland', offset: 'GMT-03:30' },
{ name: 'Brasilia', offset: 'GMT-03:00' },
{ name: 'Buenos Aires', offset: 'GMT-03:00' },
{ name: 'Greenland', offset: 'GMT-03:00' },
{ name: 'Mid-Atlantic', offset: 'GMT-02:00' },
{ name: 'Azores', offset: 'GMT-01:00' },
{ name: 'Cape Verde Is.', offset: 'GMT-01:00' },
{ name: 'Casablanca', offset: 'GMT+00:00' },
{ name: 'Dublin', offset: 'GMT+00:00' },
{ name: 'Lisbon', offset: 'GMT+00:00' },
{ name: 'London', offset: 'GMT+00:00' },
{ name: 'Monrovia', offset: 'GMT+00:00' },
{ name: 'Amsterdam', offset: 'GMT+01:00' },
{ name: 'Belgrade', offset: 'GMT+01:00' },
{ name: 'Berlin', offset: 'GMT+01:00' },
{ name: 'Bratislava', offset: 'GMT+01:00' },
{ name: 'Brussels', offset: 'GMT+01:00' },
{ name: 'Budapest', offset: 'GMT+01:00' },
{ name: 'Copenhagen', offset: 'GMT+01:00' },
{ name: 'Madrid', offset: 'GMT+01:00' },
{ name: 'Paris', offset: 'GMT+01:00' },
{ name: 'Prague', offset: 'GMT+01:00' },
{ name: 'Rome', offset: 'GMT+01:00' },
{ name: 'Sarajevo', offset: 'GMT+01:00' },
{ name: 'Warsaw', offset: 'GMT+01:00' },
{ name: 'West Central Africa', offset: 'GMT+01:00' },
{ name: 'Zagreb', offset: 'GMT+01:00' },
{ name: 'Athens', offset: 'GMT+02:00' },
{ name: 'Bucharest', offset: 'GMT+02:00' },
{ name: 'Cairo', offset: 'GMT+02:00' },
{ name: 'Harare', offset: 'GMT+02:00' },
{ name: 'Helsinki', offset: 'GMT+02:00' },
{ name: 'Jerusalem', offset: 'GMT+02:00' },
{ name: 'Kaliningrad', offset: 'GMT+02:00' },
{ name: 'Kyiv', offset: 'GMT+02:00' },
{ name: 'Pretoria', offset: 'GMT+02:00' },
{ name: 'Riga', offset: 'GMT+02:00' },
{ name: 'Sofia', offset: 'GMT+02:00' },
{ name: 'Tallinn', offset: 'GMT+02:00' },
{ name: 'Vilnius', offset: 'GMT+02:00' },
{ name: 'Baghdad', offset: 'GMT+03:00' },
{ name: 'Istanbul', offset: 'GMT+03:00' },
{ name: 'Kuwait', offset: 'GMT+03:00' },
{ name: 'Minsk', offset: 'GMT+03:00' },
{ name: 'Moscow', offset: 'GMT+03:00' },
{ name: 'Nairobi', offset: 'GMT+03:00' },
{ name: 'Riyadh', offset: 'GMT+03:00' },
{ name: 'St. Petersburg', offset: 'GMT+03:00' },
{ name: 'Tehran', offset: 'GMT+03:30' },
{ name: 'Abu Dhabi', offset: 'GMT+04:00' },
{ name: 'Baku', offset: 'GMT+04:00' },
{ name: 'Muscat', offset: 'GMT+04:00' },
{ name: 'Tbilisi', offset: 'GMT+04:00' },
{ name: 'Yerevan', offset: 'GMT+04:00' },
{ name: 'Kabul', offset: 'GMT+04:30' },
{ name: 'Ekaterinburg', offset: 'GMT+05:00' },
{ name: 'Islamabad', offset: 'GMT+05:00' },
{ name: 'Karachi', offset: 'GMT+05:00' },
{ name: 'Tashkent', offset: 'GMT+05:00' },
{ name: 'Chennai', offset: 'GMT+05:30' },
{ name: 'Kolkata', offset: 'GMT+05:30' },
{ name: 'Mumbai', offset: 'GMT+05:30' },
{ name: 'New Delhi', offset: 'GMT+05:30' },
{ name: 'Sri Jayawardenepura', offset: 'GMT+05:30' },
{ name: 'Kathmandu', offset: 'GMT+05:45' },
{ name: 'Almaty', offset: 'GMT+06:00' },
{ name: 'Astana', offset: 'GMT+06:00' },
{ name: 'Dhaka', offset: 'GMT+06:00' },
{ name: 'Urumqi', offset: 'GMT+06:00' },
{ name: 'Rangoon', offset: 'GMT+06:30' },
{ name: 'Bangkok', offset: 'GMT+07:00' },
{ name: 'Hanoi', offset: 'GMT+07:00' },
{ name: 'Jakarta', offset: 'GMT+07:00' },
{ name: 'Krasnoyarsk', offset: 'GMT+07:00' },
{ name: 'Beijing', offset: 'GMT+08:00' },
{ name: 'Chongqing', offset: 'GMT+08:00' },
{ name: 'Hong Kong', offset: 'GMT+08:00' },
{ name: 'Irkutsk', offset: 'GMT+08:00' },
{ name: 'Kuala Lumpur', offset: 'GMT+08:00' },
{ name: 'Perth', offset: 'GMT+08:00' },
{ name: 'Singapore', offset: 'GMT+08:00' },
{ name: 'Taipei', offset: 'GMT+08:00' },
{ name: 'Ulaan Bataar', offset: 'GMT+08:00' },
{ name: 'Osaka', offset: 'GMT+09:00' },
{ name: 'Sapporo', offset: 'GMT+09:00' },
{ name: 'Seoul', offset: 'GMT+09:00' },
{ name: 'Tokyo', offset: 'GMT+09:00' },
{ name: 'Yakutsk', offset: 'GMT+09:00' },
{ name: 'Adelaide', offset: 'GMT+09:30' },
{ name: 'Darwin', offset: 'GMT+09:30' },
{ name: 'Brisbane', offset: 'GMT+10:00' },
{ name: 'Canberra', offset: 'GMT+10:00' },
{ name: 'Guam', offset: 'GMT+10:00' },
{ name: 'Hobart', offset: 'GMT+10:00' },
{ name: 'Melbourne', offset: 'GMT+10:00' },
{ name: 'Port Moresby', offset: 'GMT+10:00' },
{ name: 'Sydney', offset: 'GMT+10:00' },
{ name: 'Vladivostok', offset: 'GMT+10:00' },
{ name: 'Magadan', offset: 'GMT+11:00' },
{ name: 'New Caledonia', offset: 'GMT+11:00' },
{ name: 'Solomon Is.', offset: 'GMT+11:00' },
{ name: 'Kamchatka', offset: 'GMT+12:00' },
{ name: 'Marshall Is.', offset: 'GMT+12:00' },
{ name: 'Fiji', offset: 'GMT+12:00' },
{ name: 'Auckland', offset: 'GMT+12:00' },
{ name: 'Wellington', offset: 'GMT+12:00' },
{ name: "Nuku'alofa", offset: 'GMT+13:00' },
{ name: 'Samoa', offset: 'GMT+13:00' },
{ name: 'Tokelau Is.', offset: 'GMT+13:00' },
];

View File

@ -16,6 +16,8 @@ import { UserRoleEntity } from '../modules/user-role/entities';
import { RoleTypeEntity } from '../modules/role-type/entities';
import { UserNotificationEntity } from '../modules/user-notification/entities';
import { DeviceNotificationEntity } from '../modules/device-notification/entities';
import { RegionEntity } from '../modules/region/entities';
import { TimeZoneEntity } from '../modules/timezone/entities';
@Module({
imports: [
@ -46,6 +48,8 @@ import { DeviceNotificationEntity } from '../modules/device-notification/entitie
RoleTypeEntity,
UserNotificationEntity,
DeviceNotificationEntity,
RegionEntity,
TimeZoneEntity,
],
namingStrategy: new SnakeNamingStrategy(),
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),

View File

@ -0,0 +1 @@
export * from './region.dto';

View File

@ -0,0 +1,11 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class RegionDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public regionName: string;
}

View File

@ -0,0 +1 @@
export * from './region.entity';

View File

@ -0,0 +1,20 @@
import { Column, Entity, OneToMany } from 'typeorm';
import { RegionDto } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { UserEntity } from '../../user/entities';
@Entity({ name: 'region' })
export class RegionEntity extends AbstractEntity<RegionDto> {
@Column({
nullable: false,
})
regionName: string;
@OneToMany(() => UserEntity, (user) => user.region)
users: UserEntity[];
constructor(partial: Partial<RegionEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RegionEntity } from './entities/region.entity';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [TypeOrmModule.forFeature([RegionEntity])],
})
export class RegionRepositoryModule {}

View File

@ -0,0 +1 @@
export * from './region.repository';

View File

@ -0,0 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { RegionEntity } from '../entities';
@Injectable()
export class RegionRepository extends Repository<RegionEntity> {
constructor(private dataSource: DataSource) {
super(RegionEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1 @@
export * from './timezone.dto';

View File

@ -0,0 +1,15 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class TimeZoneDto {
@IsString()
@IsNotEmpty()
public uuid: string;
@IsString()
@IsNotEmpty()
public cityName: string;
@IsString()
@IsNotEmpty()
public timeZoneOffset: string;
}

View File

@ -0,0 +1 @@
export * from './timezone.entity';

View File

@ -0,0 +1,23 @@
import { Column, Entity, OneToMany } from 'typeorm';
import { TimeZoneDto } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { UserEntity } from '../../user/entities';
@Entity({ name: 'timezone' })
export class TimeZoneEntity extends AbstractEntity<TimeZoneDto> {
@Column({
nullable: false,
})
cityName: string;
@Column({
nullable: false,
})
timeZoneOffset: string;
@OneToMany(() => UserEntity, (user) => user.timezone)
users: UserEntity[];
constructor(partial: Partial<TimeZoneEntity>) {
super();
Object.assign(this, partial);
}
}

View File

@ -0,0 +1 @@
export * from './timezone.repository';

View File

@ -0,0 +1,10 @@
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { TimeZoneEntity } from '../entities';
@Injectable()
export class TimeZoneRepository extends Repository<TimeZoneEntity> {
constructor(private dataSource: DataSource) {
super(TimeZoneEntity, dataSource.createEntityManager());
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TimeZoneEntity } from './entities/timezone.entity';
@Module({
providers: [],
exports: [],
controllers: [],
imports: [TypeOrmModule.forFeature([TimeZoneEntity])],
})
export class TimeZoneRepositoryModule {}

View File

@ -1,5 +1,5 @@
import { DeviceUserPermissionEntity } from '../../device-user-permission/entities/device.user.permission.entity';
import { Column, Entity, OneToMany } from 'typeorm';
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm';
import { UserDto } from '../dtos';
import { AbstractEntity } from '../../abstract/entities/abstract.entity';
import { UserSpaceEntity } from '../../user-space/entities';
@ -7,6 +7,9 @@ import { UserRoleEntity } from '../../user-role/entities';
import { DeviceNotificationEntity } from '../../device-notification/entities';
import { UserNotificationEntity } from '../../user-notification/entities';
import { DeviceEntity } from '../../device/entities';
import { defaultProfilePicture } from '@app/common/constants/default.profile.picture';
import { RegionEntity } from '../../region/entities';
import { TimeZoneEntity } from '../../timezone/entities';
@Entity({ name: 'user' })
export class UserEntity extends AbstractEntity<UserDto> {
@ -17,6 +20,13 @@ export class UserEntity extends AbstractEntity<UserDto> {
})
public uuid: string;
@Column({
nullable: true,
type: 'text',
default: defaultProfilePicture,
})
public profilePicture: string;
@Column({
nullable: false,
unique: true,
@ -78,6 +88,12 @@ export class UserEntity extends AbstractEntity<UserDto> {
nullable: true,
})
roles: UserRoleEntity[];
@ManyToOne(() => RegionEntity, (region) => region.users, { nullable: true })
region: RegionEntity;
@ManyToOne(() => TimeZoneEntity, (timezone) => timezone.users, {
nullable: true,
})
timezone: TimeZoneEntity;
constructor(partial: Partial<UserEntity>) {
super();
Object.assign(this, partial);

View File

@ -15,6 +15,10 @@ import { UserRepository } from '../modules/user/repositories';
import { UserRoleRepository } from '../modules/user-role/repositories';
import { UserRoleRepositoryModule } from '../modules/user-role/user.role.repository.module';
import { UserRepositoryModule } from '../modules/user/user.repository.module';
import { RegionSeeder } from './services/regions.seeder';
import { RegionRepository } from '../modules/region/repositories';
import { TimeZoneSeeder } from './services/timezone.seeder';
import { TimeZoneRepository } from '../modules/timezone/repositories';
@Global()
@Module({
providers: [
@ -28,6 +32,10 @@ import { UserRepositoryModule } from '../modules/user/user.repository.module';
SuperAdminSeeder,
UserRepository,
UserRoleRepository,
RegionSeeder,
RegionRepository,
TimeZoneSeeder,
TimeZoneRepository,
],
exports: [SeederService],
controllers: [],

View File

@ -0,0 +1,40 @@
import { RegionRepository } from '@app/common/modules/region/repositories';
import { allCountries } from '@app/common/constants/regions';
import { Injectable } from '@nestjs/common';
@Injectable()
export class RegionSeeder {
constructor(private readonly regionRepository: RegionRepository) {}
async addRegionDataIfNotFound(): Promise<void> {
try {
const existingRegions = await this.regionRepository.find();
const regionNames = existingRegions.map((region) => region.regionName);
const missingRegions = allCountries.filter(
(country) => !regionNames.includes(country),
);
if (missingRegions.length > 0) {
await this.addRegionData(missingRegions);
}
} catch (err) {
console.error('Error while checking region data:', err);
throw err;
}
}
private async addRegionData(regions: string[]): Promise<void> {
try {
const regionEntities = regions.map((regionName) => ({
regionName,
}));
await this.regionRepository.save(regionEntities);
} catch (err) {
console.error('Error while adding region data:', err);
throw err;
}
}
}

View File

@ -3,12 +3,16 @@ import { PermissionTypeSeeder } from './permission.type.seeder';
import { RoleTypeSeeder } from './role.type.seeder';
import { SpaceTypeSeeder } from './space.type.seeder';
import { SuperAdminSeeder } from './supper.admin.seeder';
import { RegionSeeder } from './regions.seeder';
import { TimeZoneSeeder } from './timezone.seeder';
@Injectable()
export class SeederService {
constructor(
private readonly permissionTypeSeeder: PermissionTypeSeeder,
private readonly roleTypeSeeder: RoleTypeSeeder,
private readonly spaceTypeSeeder: SpaceTypeSeeder,
private readonly regionSeeder: RegionSeeder,
private readonly timeZoneSeeder: TimeZoneSeeder,
private readonly superAdminSeeder: SuperAdminSeeder,
) {}
@ -16,6 +20,8 @@ export class SeederService {
await this.permissionTypeSeeder.addPermissionTypeDataIfNotFound();
await this.roleTypeSeeder.addRoleTypeDataIfNotFound();
await this.spaceTypeSeeder.addSpaceTypeDataIfNotFound();
await this.regionSeeder.addRegionDataIfNotFound();
await this.timeZoneSeeder.addTimeZoneDataIfNotFound();
await this.superAdminSeeder.createSuperAdminIfNotFound();
}
}

View File

@ -0,0 +1,42 @@
import { TimeZoneRepository } from '@app/common/modules/timezone/repositories';
import { allTimeZones } from '@app/common/constants/timezones';
import { Injectable } from '@nestjs/common';
@Injectable()
export class TimeZoneSeeder {
constructor(private readonly timeZoneRepository: TimeZoneRepository) {}
async addTimeZoneDataIfNotFound(): Promise<void> {
try {
const existingTimeZones = await this.timeZoneRepository.find();
const timeZoneNames = existingTimeZones.map((tz) => tz.cityName);
const missingTimeZones = allTimeZones.filter(
(tz) => !timeZoneNames.includes(tz.name),
);
if (missingTimeZones.length > 0) {
await this.addTimeZoneData(missingTimeZones);
}
} catch (err) {
console.error('Error while checking time zone data:', err);
throw err;
}
}
private async addTimeZoneData(
timeZones: { name: string; offset: string }[],
): Promise<void> {
try {
const timeZoneEntities = timeZones.map((tz) => ({
cityName: tz.name,
timeZoneOffset: tz.offset,
}));
await this.timeZoneRepository.save(timeZoneEntities);
} catch (err) {
console.error('Error while adding time zone data:', err);
throw err;
}
}
}

View File

@ -21,6 +21,8 @@ import { DoorLockModule } from './door-lock/door.lock.module';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './interceptors/logging.interceptor';
import { AutomationModule } from './automation/automation.module';
import { RegionModule } from './region/region.module';
import { TimeZoneModule } from './timezone/timezone.module';
@Module({
imports: [
ConfigModule.forRoot({
@ -44,6 +46,8 @@ import { AutomationModule } from './automation/automation.module';
SceneModule,
AutomationModule,
DoorLockModule,
RegionModule,
TimeZoneModule,
],
controllers: [AuthenticationController],
providers: [

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

@ -4,6 +4,7 @@ import rateLimit from 'express-rate-limit';
import helmet from 'helmet';
import { setupSwaggerAuthentication } from '../libs/common/src/util/user-auth.swagger.utils';
import { ValidationPipe } from '@nestjs/common';
import { json, urlencoded } from 'body-parser';
import { SeederService } from '@app/common/seed/services/seeder.service';
async function bootstrap() {
@ -11,6 +12,10 @@ async function bootstrap() {
app.enableCors();
// Set the body parser limit to 1 MB
app.use(json({ limit: '1mb' }));
app.use(urlencoded({ limit: '1mb', extended: true }));
app.use(
rateLimit({
windowMs: 5 * 60 * 1000,
@ -42,7 +47,8 @@ async function bootstrap() {
} catch (error) {
console.error('Seeding failed!', error);
}
console.log('Starting auth at port ...', process.env.PORT || 4000);
await app.listen(process.env.PORT || 4000);
}
console.log('Starting auth at port ...', process.env.PORT || 4000);
bootstrap();

View File

@ -0,0 +1 @@
export * from './region.controller';

View File

@ -0,0 +1,33 @@
import {
Controller,
Get,
HttpException,
HttpStatus,
UseGuards,
} from '@nestjs/common';
import { RegionService } from '../services/region.service';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard';
@ApiTags('Region Module')
@Controller({
version: '1',
path: 'region',
})
export class RegionController {
constructor(private readonly regionService: RegionService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get()
async getAllRegions() {
try {
return await this.regionService.getAllRegions();
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { RegionService } from './services/region.service';
import { RegionController } from './controllers/region.controller';
import { ConfigModule } from '@nestjs/config';
import { RegionRepository } from '@app/common/modules/region/repositories';
@Module({
imports: [ConfigModule],
controllers: [RegionController],
providers: [RegionService, RegionRepository],
exports: [RegionService],
})
export class RegionModule {}

View File

@ -0,0 +1 @@
export * from './region.service';

View File

@ -0,0 +1,25 @@
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { RegionRepository } from '@app/common/modules/region/repositories';
@Injectable()
export class RegionService {
constructor(private readonly regionRepository: RegionRepository) {}
async getAllRegions() {
try {
const regions = await this.regionRepository.find();
return regions;
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('Regions found', HttpStatus.NOT_FOUND);
}
}
}
}

View File

@ -0,0 +1 @@
export * from './timezone.controller';

View File

@ -0,0 +1,33 @@
import {
Controller,
Get,
HttpException,
HttpStatus,
UseGuards,
} from '@nestjs/common';
import { TimeZoneService } from '../services/timezone.service';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../../../libs/common/src/guards/jwt.auth.guard';
@ApiTags('TimeZone Module')
@Controller({
version: '1',
path: 'timezone',
})
export class TimeZoneController {
constructor(private readonly timeZoneService: TimeZoneService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Get()
async getAllTimeZones() {
try {
return await this.timeZoneService.getAllTimeZones();
} catch (error) {
throw new HttpException(
error.message || 'Internal server error',
error.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}

View File

@ -0,0 +1 @@
export * from './timezone.service';

View File

@ -0,0 +1,25 @@
import { TimeZoneRepository } from '@app/common/modules/timezone/repositories';
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
@Injectable()
export class TimeZoneService {
constructor(private readonly timeZoneRepository: TimeZoneRepository) {}
async getAllTimeZones() {
try {
const timeZones = await this.timeZoneRepository.find();
return timeZones;
} catch (err) {
if (err instanceof BadRequestException) {
throw err; // Re-throw BadRequestException
} else {
throw new HttpException('TimeZones found', HttpStatus.NOT_FOUND);
}
}
}
}

View File

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TimeZoneService } from './services/timezone.service';
import { TimeZoneController } from './controllers/timezone.controller';
import { ConfigModule } from '@nestjs/config';
import { TimeZoneRepository } from '@app/common/modules/timezone/repositories';
@Module({
imports: [ConfigModule],
controllers: [TimeZoneController],
providers: [TimeZoneService, TimeZoneRepository],
exports: [TimeZoneService],
})
export class TimeZoneModule {}

View File

@ -1,8 +1,23 @@
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 {
UpdateNameDto,
UpdateProfilePictureDataDto,
UpdateRegionDataDto,
UpdateTimezoneDataDto,
} from '../dtos';
import { CheckProfilePictureGuard } from 'src/guards/profile.picture.guard';
@ApiTags('User Module')
@Controller({
@ -13,13 +28,116 @@ 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 updateNameByUserUuid(
@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,
);
}
}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Put('/name/:userUuid')
async updateTimezoneByUserUuid(
@Param('userUuid') userUuid: string,
@Body() updateNameDto: UpdateNameDto,
) {
try {
const userData = await this.userService.updateNameByUserUuid(
userUuid,
updateNameDto,
);
return {
statusCode: HttpStatus.CREATED,
success: true,
message: 'name 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,61 @@
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);
}
}
export class UpdateNameDto {
@ApiProperty({
description: 'firstName',
required: true,
})
@IsString()
@IsNotEmpty()
public firstName: string;
@ApiProperty({
description: 'lastName',
required: true,
})
@IsString()
@IsNotEmpty()
public lastName: 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,225 @@
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 {
UpdateNameDto,
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,
);
}
}
async updateNameByUserUuid(userUuid: string, updateNameDto: UpdateNameDto) {
try {
const user = await this.getUserDetailsByUserUuid(userUuid);
if (!user) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
if (!updateNameDto.firstName || !updateNameDto.lastName) {
throw new BadRequestException('First Name and Last Name is required');
}
await this.userRepository.update(
{ uuid: userUuid },
{
firstName: updateNameDto.firstName,
lastName: updateNameDto.lastName,
},
);
const updatedUser = await this.getUserDetailsByUserUuid(userUuid);
if (!updatedUser.firstName || !updatedUser.lastName) {
throw new BadRequestException('First Name and Last Name 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 {}