diff --git a/.gitignore b/.gitignore index d8aaabb..3086bca 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,6 @@ lerna-debug.log* !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json -ormconfig.json \ No newline at end of file +ormconfig.json + +uploads/* \ No newline at end of file diff --git a/package.json b/package.json index f9e8bac..0bc766c 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@nestjs/testing": "^8.0.0", "@types/express": "^4.17.13", "@types/jest": "27.0.2", + "@types/multer": "^1.4.7", "@types/node": "^16.0.0", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", diff --git a/src/app.controller.ts b/src/app.controller.ts index de37367..ce77371 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,12 +1,12 @@ -import { Controller, Get } from '@nestjs/common'; +import { Controller, Get, Param, Res } from '@nestjs/common'; import { AppService } from './app.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} - @Get() - GetHello(): string { - return this.appService.getHello(); + @Get('avatars/:imgpath') + seeUploadedFile(@Param('imgpath') image, @Res() res) { + return res.sendFile(image, { root: './uploads/avatars' }); } } \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index 47c6ecc..b3397ba 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,24 +5,25 @@ import { AppService } from './app.service'; import { Users } from './components/users/entities/user.entity'; import { UsersModule } from './components/users/users.module'; import { ConfigModule } from '@nestjs/config'; -import { BooksModule } from './components/books/books.module'; -import { Books } from './components/books/entities/book.entity'; import { AuthModule } from './components/auth/auth.module'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { AuthInterceptor } from './interceptor/auth.interceptor'; +import { MulterModule } from '@nestjs/platform-express'; @Module({ imports: [ + MulterModule.register({ + dest: './uploads', + }), ConfigModule.forRoot({ isGlobal: true, }), TypeOrmModule.forRoot({ - entities: [Users, Books], + entities: [Users], synchronize: false, }), AuthModule, UsersModule, - BooksModule, ], controllers: [AppController], providers: [ diff --git a/src/app.service.ts b/src/app.service.ts index 927d7cc..a031ef5 100644 --- a/src/app.service.ts +++ b/src/app.service.ts @@ -2,7 +2,4 @@ import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { - getHello(): string { - return 'Hello World!'; - } } diff --git a/src/components/auth/auth.controller.ts b/src/components/auth/auth.controller.ts index 1a01518..af84f44 100644 --- a/src/components/auth/auth.controller.ts +++ b/src/components/auth/auth.controller.ts @@ -8,7 +8,7 @@ import { Res, UseGuards, } from '@nestjs/common'; -import { CreateUserDto } from '../users/dto/create-user.dto'; +import { RegisterUserDto } from '../users/dto/register-user-dto'; import { AuthService } from './auth.service'; import { JwtAuthGuard } from './guard/jwt-auth.guard'; import { LocalAuthGuard } from './guard/local-auth.guard'; @@ -24,8 +24,8 @@ export class AuthController { } @Post('register') - async register(@Body() createUserDto: CreateUserDto) { - return this.authService.register(createUserDto); + async register(@Body() registerUserDto: RegisterUserDto) { + return this.authService.register(registerUserDto); } @UseGuards(JwtAuthGuard) diff --git a/src/components/auth/auth.service.ts b/src/components/auth/auth.service.ts index b3391b3..fb74c6d 100644 --- a/src/components/auth/auth.service.ts +++ b/src/components/auth/auth.service.ts @@ -11,12 +11,16 @@ export class AuthService { ) {} async validateUser(email: string, pass: string): Promise { - const user = await this.usersService.findOne({ where: { email: email } }); - if (!user) return null; + const user = await this.usersService.findOne({ + where: { email: email }, + select: ['id', 'password'], + }); + if (!user || !user.password) return null; const validPassword = await bcrypt.compare(pass, user.password); - if (user && validPassword) { - const { password, ...result } = user; - return result; + if (validPassword) { + return await this.usersService.findOne({ + where: { id: user.id }, + }); } return null; } @@ -32,7 +36,7 @@ export class AuthService { }, }; return { - user: payload.user, + user: req.user, access_token: this.jwtService.sign(payload), }; } diff --git a/src/components/books/books.controller.spec.ts b/src/components/books/books.controller.spec.ts deleted file mode 100644 index 2c73406..0000000 --- a/src/components/books/books.controller.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { BooksController } from './books.controller'; -import { BooksService } from './books.service'; - -describe('BooksController', () => { - let controller: BooksController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [BooksController], - providers: [BooksService], - }).compile(); - - controller = module.get(BooksController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/src/components/books/books.controller.ts b/src/components/books/books.controller.ts deleted file mode 100644 index a3867f6..0000000 --- a/src/components/books/books.controller.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { - Controller, - Get, - Post, - Body, - Patch, - Param, - Delete, - Res, - HttpStatus, - UseGuards, -} from '@nestjs/common'; -import { Response } from 'express'; -import { JwtAuthGuard } from '../auth/guard/jwt-auth.guard'; -import { BooksService } from './books.service'; -import { CreateBookDto } from './dto/create-book.dto'; -import { UpdateBookDto } from './dto/update-book.dto'; -import { Books as Book } from './entities/book.entity'; - -@Controller('books') -export class BooksController { - constructor(private readonly booksService: BooksService) {} - - @UseGuards(JwtAuthGuard) - @Post() - create(@Body() createBookDto: CreateBookDto, @Res() res: Response) { - return this.booksService.create(createBookDto); - } - - @Get() - findAll() { - return this.booksService.findAll(); - } - - @Get(':id') - async findOne( - @Param('id') id: string, - @Res() res: Response, - ): Promise { - const book = await this.booksService.findOne(+id); - if (book) return res.status(HttpStatus.OK).json(book); - return res - .status(HttpStatus.NOT_FOUND) - .json({ error: 'This resource no longer exist or has been removed' }); - } - - @UseGuards(JwtAuthGuard) - @Patch(':id') - async update( - @Param('id') id: string, - @Body() updateBookDto: UpdateBookDto, - @Res() res: Response, - ) { - const response = await this.booksService.update(+id, updateBookDto); - if (response) - return res - .status(HttpStatus.OK) - .json({ message: 'Book information updated successfully' }); - return res - .status(HttpStatus.NOT_FOUND) - .json({ error: 'The resource to be updated no longer exist' }); - } - - @UseGuards(JwtAuthGuard) - @Delete(':id') - async remove(@Param('id') id: string, @Res() res: Response) { - await this.booksService.remove(+id); - res - .status(HttpStatus.OK) - .json({ message: 'Book details deleted successfully' }); - } -} diff --git a/src/components/books/books.module.ts b/src/components/books/books.module.ts deleted file mode 100644 index bc36036..0000000 --- a/src/components/books/books.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; -import { BooksService } from './books.service'; -import { BooksController } from './books.controller'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { Books } from './entities/book.entity'; - -@Module({ - imports: [TypeOrmModule.forFeature([Books])], - controllers: [BooksController], - providers: [BooksService], -}) -export class BooksModule {} diff --git a/src/components/books/books.service.spec.ts b/src/components/books/books.service.spec.ts deleted file mode 100644 index 6343e6b..0000000 --- a/src/components/books/books.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { BooksService } from './books.service'; - -describe('BooksService', () => { - let service: BooksService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [BooksService], - }).compile(); - - service = module.get(BooksService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/src/components/books/books.service.ts b/src/components/books/books.service.ts deleted file mode 100644 index 4f03ff8..0000000 --- a/src/components/books/books.service.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, UpdateResult } from 'typeorm'; -import { Books as Book } from './entities/book.entity'; - -@Injectable() -export class BooksService { - constructor( - @InjectRepository(Book) - private booksRepository: Repository, - ) {} - - async create(data: object) { - return await this.booksRepository.save(data).then((res) => res); - } - - findAll(): Promise { - return this.booksRepository.find(); - } - - findOne(id: number): Promise { - return this.booksRepository.findOne({ where: { id: id } }); - } - - async update( - id: number, - data: object, - ): Promise { - const book = await this.findOne(id).then((res) => res); - if (book) - return await this.booksRepository.update(id, data).then((res) => res); - return; - } - - async remove(id: number) { - return await this.booksRepository.delete(id); - } -} diff --git a/src/components/books/dto/create-book.dto.ts b/src/components/books/dto/create-book.dto.ts deleted file mode 100644 index e3b5d48..0000000 --- a/src/components/books/dto/create-book.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IsNotEmpty, IsBoolean, IsEmail } from 'class-validator'; - -export class CreateBookDto { - @IsNotEmpty() - title: string; - @IsNotEmpty() - description: number; - - thumbnail: string; - - author: string[]; -} diff --git a/src/components/books/dto/update-book.dto.ts b/src/components/books/dto/update-book.dto.ts deleted file mode 100644 index 883336e..0000000 --- a/src/components/books/dto/update-book.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { OmitType } from '@nestjs/mapped-types'; -import { CreateBookDto } from './create-book.dto'; - -export class UpdateBookDto extends OmitType(CreateBookDto, [] as const) { - id: number; -} diff --git a/src/components/books/entities/book.entity.ts b/src/components/books/entities/book.entity.ts deleted file mode 100644 index 1a5ab75..0000000 --- a/src/components/books/entities/book.entity.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm'; -import { Users as User } from 'src/components/users/entities/user.entity'; -@Entity() -export class Books { - @PrimaryGeneratedColumn() - id: number; - - @Column() - title: string; - - @Column() - description: string; - - @Column() - thumbnail: string; - - @Column() - author: string; - - @Column({ type: 'datetime', default: null }) - created_at?: Date; - - @Column({ type: 'datetime', default: null }) - updated_at?: Date; - - @ManyToOne(() => User, (user) => user.books) - user: User; -} diff --git a/src/components/users/dto/create-user.dto.ts b/src/components/users/dto/create-user.dto.ts index ecaab0a..c6d44df 100644 --- a/src/components/users/dto/create-user.dto.ts +++ b/src/components/users/dto/create-user.dto.ts @@ -1,4 +1,4 @@ -import { IsNotEmpty, IsBoolean, IsEmail } from 'class-validator'; +import { IsNotEmpty, IsBoolean, IsEmail, IsString, IsOptional } from 'class-validator'; export class CreateUserDto { @IsEmail() email: string; @@ -9,6 +9,14 @@ export class CreateUserDto { @IsNotEmpty() username: string; + @IsString() + @IsOptional() + firstname: string; + + @IsString() + @IsOptional() + lastname: string; + @IsBoolean() is_active: boolean; } diff --git a/src/components/users/dto/register-user-dto.ts b/src/components/users/dto/register-user-dto.ts new file mode 100644 index 0000000..ba2b6c1 --- /dev/null +++ b/src/components/users/dto/register-user-dto.ts @@ -0,0 +1,20 @@ +import { IsNotEmpty, IsEmail, IsString } from 'class-validator'; +import { Match } from 'src/decorators/match.decorator'; +export class RegisterUserDto { + @IsNotEmpty() + @IsEmail() + email: string; + + @IsNotEmpty() + @IsString() + password: string; + + @IsNotEmpty() + @IsString() + @Match('password') + confirm_password: string; + + @IsNotEmpty() + @IsString() + username: string; +} diff --git a/src/components/users/dto/update-user.dto.ts b/src/components/users/dto/update-user.dto.ts new file mode 100644 index 0000000..abacbaf --- /dev/null +++ b/src/components/users/dto/update-user.dto.ts @@ -0,0 +1,25 @@ +import { + IsNotEmpty, + IsBoolean, + IsEmail, + IsString, + IsOptional, +} from 'class-validator'; +export class UpdateUserDto { + @IsEmail() + email: string; + + @IsNotEmpty() + username: string; + + @IsBoolean() + is_active: boolean; + + @IsString() + @IsOptional() + firstname: string; + + @IsString() + @IsOptional() + lastname: string; +} diff --git a/src/components/users/entities/user.entity.ts b/src/components/users/entities/user.entity.ts index 72cde85..04548b0 100644 --- a/src/components/users/entities/user.entity.ts +++ b/src/components/users/entities/user.entity.ts @@ -1,5 +1,6 @@ import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; -import { Books as Book } from 'src/components/books/entities/book.entity'; +//import { Books as Book } from 'src/components/books/entities/book.entity'; + @Entity() export class Users { @PrimaryGeneratedColumn('increment') @@ -8,18 +9,21 @@ export class Users { @Column({ unique: true }) email: string; - @Column() + @Column({ select: false }) password: string; @Column() username: string; - @Column({ nullable: true }) + @Column({ nullable: true, default: null }) firstname: string; - @Column({ nullable: true }) + @Column({ nullable: true, default: null }) lastname: string; + @Column({ nullable: true, default: null }) + avatar: string; + @Column({ type: 'boolean', default: true }) is_active: boolean; @@ -29,6 +33,6 @@ export class Users { @Column({ type: 'datetime', default: null }) updated_at?: Date; - @OneToMany(() => Book, (book) => book.user) - books?: Book[]; +/* @OneToMany(() => Book, (book) => book.user) + books?: Book[]; */ } diff --git a/src/components/users/users.controller.ts b/src/components/users/users.controller.ts index ad19dd4..be854c3 100644 --- a/src/components/users/users.controller.ts +++ b/src/components/users/users.controller.ts @@ -2,15 +2,24 @@ import { Controller, HttpStatus, Get, + Post, + Patch, + Delete, Res, Body, Param, UseGuards, + UseInterceptors, + UploadedFile, } from '@nestjs/common'; -import { Post } from '@nestjs/common/decorators/http'; +import { Express } from 'express'; +import { FileInterceptor } from '@nestjs/platform-express'; import { JwtAuthGuard } from '../auth/guard/jwt-auth.guard'; import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; import { UsersService } from './users.service'; +import { diskStorage } from 'multer'; +import { editFileName, imageFileFilter } from 'src/utils/file-upload.utils'; @Controller('users') export class UsersController { @@ -23,18 +32,63 @@ export class UsersController { return res.status(HttpStatus.OK).json(users); } + @UseGuards(JwtAuthGuard) @Post() async addUser(@Res() res, @Body() createUserDto: CreateUserDto) { const newUser = await this.userService.create(createUserDto); return res.status(HttpStatus.OK).json({ - message: 'User has been submitted successfully!', + message: 'User has been created successfully!', user: newUser, }); } - + @UseGuards(JwtAuthGuard) @Get(':id') async getUser(@Res() res, @Param('id') userId) { const user = await this.userService.findOne({ where: { id: userId } }); return res.status(HttpStatus.OK).json(user); } + @UseGuards(JwtAuthGuard) + @Delete(':id') + async deleteUser(@Res() res, @Param('id') userId) { + await this.userService.remove(userId); + return res.status(HttpStatus.OK).json({ + message: 'User has been deleted successfully!', + }); + } + @UseGuards(JwtAuthGuard) + @Patch(':id') + async editUser( + @Res() res, + @Param('id') userId, + @Body() updateUserDto: UpdateUserDto, + ) { + await this.userService.edit(userId, updateUserDto); + return res.status(HttpStatus.OK).json({ + message: 'User has been updated successfully!', + }); + } + @UseGuards(JwtAuthGuard) + @Post('/avatar/:id') + @UseInterceptors( + FileInterceptor('avatar', { + storage: diskStorage({ + destination: './uploads/avatars', + filename: editFileName, + }), + fileFilter: imageFileFilter, + limits: { + fileSize: Math.pow(1024, 2), + }, + }), + ) + async uploadAvatar( + @Res() res, + @Param('id') userId, + @UploadedFile() file: Express.Multer.File, + ) { + await this.userService.editAvatar(userId, file.filename); + return res.status(HttpStatus.OK).json({ + message: 'Avatar has been uploaded successfully!', + }); + } } diff --git a/src/components/users/users.service.ts b/src/components/users/users.service.ts index 58d0245..10c600d 100644 --- a/src/components/users/users.service.ts +++ b/src/components/users/users.service.ts @@ -2,8 +2,10 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; import { Users as User } from './entities/user.entity'; import * as bcrypt from 'bcrypt'; +import { RegisterUserDto } from './dto/register-user-dto'; @Injectable() export class UsersService { @@ -13,14 +15,14 @@ export class UsersService { ) {} findAll(): Promise { - return this.usersRepository.find({ relations: ['books'] }); + return this.usersRepository.find(); } async findOne(data: number | any): Promise { return await this.usersRepository.findOne(data); } - async create(createUserDto: CreateUserDto): Promise { + async create(createUserDto: CreateUserDto | RegisterUserDto): Promise { createUserDto.password = await bcrypt.hash(createUserDto.password, 10); return this.usersRepository.save(createUserDto); } @@ -28,4 +30,15 @@ export class UsersService { async remove(id: string): Promise { await this.usersRepository.delete(id); } + + async edit(id: string, updateUserDto: UpdateUserDto): Promise { + console.log(updateUserDto) + await this.usersRepository.update(id, updateUserDto); + } + + async editAvatar(id: string, fileName: string): Promise { + await this.usersRepository.update(id, { + avatar: fileName, + }); + } } diff --git a/src/decorators/match.decorator.ts b/src/decorators/match.decorator.ts new file mode 100644 index 0000000..bef0284 --- /dev/null +++ b/src/decorators/match.decorator.ts @@ -0,0 +1,33 @@ +import { + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +export function Match(property: string, validationOptions?: ValidationOptions) { + return (object: any, propertyName: string) => { + registerDecorator({ + target: object.constructor, + propertyName, + options: validationOptions, + constraints: [property], + validator: MatchConstraint, + }); + }; +} + +@ValidatorConstraint({ name: 'Match' }) +export class MatchConstraint implements ValidatorConstraintInterface { + validate(value: any, args: ValidationArguments) { + const [relatedPropertyName] = args.constraints; + const relatedValue = (args.object as any)[relatedPropertyName]; + return value === relatedValue; + } + + defaultMessage(args: ValidationArguments) { + const [relatedPropertyName] = args.constraints; + return `${relatedPropertyName} and ${args.property} don't match`; + } +} diff --git a/src/migrations/1654328367398-AddAvatarColumnToUser.ts b/src/migrations/1654328367398-AddAvatarColumnToUser.ts new file mode 100644 index 0000000..c1025f9 --- /dev/null +++ b/src/migrations/1654328367398-AddAvatarColumnToUser.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class AddAvatarColumnToUser1654328367398 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn( + 'users', + new TableColumn({ + name: 'avatar', + type: 'varchar', + isNullable: true, + default: null, + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn('users', 'avatar'); + } +} diff --git a/src/utils/file-upload.utils.ts b/src/utils/file-upload.utils.ts new file mode 100644 index 0000000..85bd4ed --- /dev/null +++ b/src/utils/file-upload.utils.ts @@ -0,0 +1,22 @@ +import { BadRequestException } from '@nestjs/common'; +import { extname } from 'path'; + +export const imageFileFilter = (req, file, callback) => { + if (!file.mimetype.includes('image')) { + return callback( + new BadRequestException('Please, provide a valid image'), + false, + ); + } + callback(null, true); +}; + +export const editFileName = (req, file, callback) => { + const name = file.originalname.split('.')[0]; + const fileExtName = extname(file.originalname); + const randomName = Array(4) + .fill(null) + .map(() => Math.round(Math.random() * 16).toString(16)) + .join(''); + callback(null, `${name}-${randomName}${fileExtName}`); +}; diff --git a/yarn.lock b/yarn.lock index 4d8d96c..64e80bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -978,6 +978,13 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== +"@types/multer@^1.4.7": + version "1.4.7" + resolved "https://registry.yarnpkg.com/@types/multer/-/multer-1.4.7.tgz#89cf03547c28c7bbcc726f029e2a76a7232cc79e" + integrity sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA== + dependencies: + "@types/express" "*" + "@types/node@*": version "17.0.35" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.35.tgz#635b7586086d51fb40de0a2ec9d1014a5283ba4a" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy