diff --git a/package-lock.json b/package-lock.json index 4b72573..f904edc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,11 @@ "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.0.0", + "@nestjs/mapped-types": "*", "@nestjs/platform-express": "^10.0.0", + "argon2": "^0.41.1", "dotenv": "^16.4.7", + "lodash": "^4.17.21", "pg": "^8.13.1", "pg-hstore": "^2.3.4", "reflect-metadata": "^0.2.0", @@ -1708,6 +1711,26 @@ } } }, + "node_modules/@nestjs/mapped-types": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.6.tgz", + "integrity": "sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/platform-express": { "version": "10.4.15", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.15.tgz", @@ -1837,6 +1860,15 @@ "npm": ">=5.0.0" } }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2848,6 +2880,21 @@ "dev": true, "license": "MIT" }, + "node_modules/argon2": { + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.41.1.tgz", + "integrity": "sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@phc/format": "^1.0.0", + "node-addon-api": "^8.1.0", + "node-gyp-build": "^4.8.1" + }, + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -6903,6 +6950,15 @@ "dev": true, "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.0.tgz", + "integrity": "sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -6933,6 +6989,17 @@ } } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", diff --git a/package.json b/package.json index b339f87..95ac647 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,11 @@ "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.0.0", + "@nestjs/mapped-types": "*", "@nestjs/platform-express": "^10.0.0", + "argon2": "^0.41.1", "dotenv": "^16.4.7", + "lodash": "^4.17.21", "pg": "^8.13.1", "pg-hstore": "^2.3.4", "reflect-metadata": "^0.2.0", diff --git a/src/app.module.ts b/src/app.module.ts index 1ee7153..2b7c62a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,10 +4,15 @@ import { AppService } from './app.service'; import { DatabaseModule } from './core/database/database.module'; import { ConfigModule } from '@nestjs/config'; import { UsersModule } from './modules/users/users.module'; +import { UsersController } from './modules/users/users.controller'; @Module({ - imports: [DatabaseModule, ConfigModule.forRoot({ isGlobal: true }), UsersModule], - controllers: [AppController], + imports: [ + DatabaseModule, + ConfigModule.forRoot({ isGlobal: true }), + UsersModule, + ], + controllers: [AppController, UsersController], providers: [AppService], }) export class AppModule {} diff --git a/src/modules/users/dto/create-user.dto.ts b/src/modules/users/dto/create-user.dto.ts new file mode 100644 index 0000000..019d108 --- /dev/null +++ b/src/modules/users/dto/create-user.dto.ts @@ -0,0 +1,9 @@ +export class CreateUserDto { + firstName: string; + lastName: string; + email: string; + password: string; + phoneNumber: string; + gender: string; + city: string; +} diff --git a/src/modules/users/dto/index.ts b/src/modules/users/dto/index.ts new file mode 100644 index 0000000..9375b1b --- /dev/null +++ b/src/modules/users/dto/index.ts @@ -0,0 +1,2 @@ +export * from './create-user.dto'; +export * from './update-user.dto'; diff --git a/src/modules/users/dto/update-user.dto.ts b/src/modules/users/dto/update-user.dto.ts new file mode 100644 index 0000000..dfd37fb --- /dev/null +++ b/src/modules/users/dto/update-user.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateUserDto } from './create-user.dto'; + +export class UpdateUserDto extends PartialType(CreateUserDto) {} diff --git a/src/modules/users/dto/user.dto.ts b/src/modules/users/dto/user.dto.ts deleted file mode 100644 index 441517c..0000000 --- a/src/modules/users/dto/user.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class UserDto { - readonly name: string; - readonly email: string; - readonly password: string; - readonly gender: string; -} diff --git a/src/modules/users/user.entity.ts b/src/modules/users/user.entity.ts index 7fc2369..7f4aa45 100644 --- a/src/modules/users/user.entity.ts +++ b/src/modules/users/user.entity.ts @@ -1,12 +1,25 @@ import { Table, Column, Model, DataType } from 'sequelize-typescript'; -@Table +@Table({ tableName: 'users' }) export class User extends Model { + @Column({ + type: DataType.UUID, + allowNull: false, + defaultValue: DataType.UUIDV4, + }) + uuid: string; + + @Column({ + type: DataType.STRING, + allowNull: false, + }) + firstName: string; + @Column({ type: DataType.STRING, allowNull: false, }) - name: string; + lastName: string; @Column({ type: DataType.STRING, @@ -21,10 +34,22 @@ export class User extends Model { }) password: string; + @Column({ + type: DataType.STRING, + allowNull: true, + }) + phoneNumber: string; + + @Column({ + type: DataType.STRING, + allowNull: true, + }) + city: string; + @Column({ type: DataType.ENUM, values: ['male', 'female'], - allowNull: false, + allowNull: true, }) gender: string; } diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts new file mode 100644 index 0000000..a3252d9 --- /dev/null +++ b/src/modules/users/users.controller.ts @@ -0,0 +1,42 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, +} from '@nestjs/common'; +import { UsersService } from './users.service'; +import { CreateUserDto, UpdateUserDto } from './dto'; +import { UUID } from 'crypto'; + +@Controller('users') +export class UsersController { + constructor(private usersService: UsersService) {} + + @Get() + findAll() { + return this.usersService.findAll(); + } + + @Get(':id') + findOne(@Param('id') id: UUID) { + return this.usersService.findOne(id); + } + + @Post() + create(@Body() user: CreateUserDto) { + return this.usersService.create(user); + } + + @Patch() + update(@Param('id') id: UUID, @Body() user: UpdateUserDto) { + return this.usersService.update(id, user); + } + + @Delete(':id') + remove(@Param('id') id: UUID) { + return this.usersService.remove(id); + } +} diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts index 949e0c8..7ddf719 100644 --- a/src/modules/users/users.module.ts +++ b/src/modules/users/users.module.ts @@ -1,9 +1,11 @@ import { Module } from '@nestjs/common'; import { UsersService } from './users.service'; import { usersProviders } from './users.providers'; +import { UsersController } from './users.controller'; @Module({ providers: [UsersService, ...usersProviders], exports: [UsersService], + controllers: [UsersController], }) export class UsersModule {} diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index 94f0006..aa4cca4 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -1,7 +1,10 @@ import { Injectable, Inject } from '@nestjs/common'; import { User } from './user.entity'; -import { UserDto } from './dto/user.dto'; +import { CreateUserDto, UpdateUserDto } from './dto'; import { USER_REPOSITORY } from '../../core/constants'; +import * as argon from 'argon2'; +import * as _ from 'lodash'; +import { UUID } from 'crypto'; @Injectable() export class UsersService { @@ -9,15 +12,44 @@ export class UsersService { @Inject(USER_REPOSITORY) private readonly userRepository: typeof User, ) {} - async create(user: UserDto): Promise { - return await this.userRepository.create(user); + async findAll() { + return await this.userRepository.findAll(); } - async findOneByEmail(email: string): Promise { - return await this.userRepository.findOne({ where: { email } }); + async findOne(id: UUID) { + return await this.userRepository.findAll({ where: { uuid: id } }); } - async findOneById(id: number): Promise { - return await this.userRepository.findOne({ where: { id } }); + async create(user: CreateUserDto) { + const hashedPassword = await argon.hash(user.password); + user.password = hashedPassword; + + const newUser = await this.userRepository.create(user); + + return _.pick(newUser, [ + 'firstName', + 'lastName', + 'email', + 'uuid', + 'createdAt', + 'updatedAt', + ]); + } + + async update(id: UUID, user: UpdateUserDto) { + const [numberOfAffectedRows, [updatedUser]] = + await this.userRepository.update( + { ...user }, + { where: { uuid: id }, returning: true }, + ); + return { numberOfAffectedRows, updatedUser }; + } + + async remove(id: UUID) { + const deletedUser = await this.findOne(id); + + await this.userRepository.destroy({ where: { uuid: id } }); + + return deletedUser; } }