├── .prettierrc ├── src ├── tags │ ├── dto │ │ ├── tag.dto.ts │ │ └── search-tag.dto.ts │ ├── tags.module.ts │ ├── tag.entity.ts │ ├── tags.controller.ts │ └── tags.service.ts ├── categories │ ├── dto │ │ └── category.dto.ts │ ├── category.entity.ts │ ├── categories.module.ts │ ├── categories.controller.ts │ └── categories.service.ts ├── answers │ ├── dto │ │ ├── params-answer.dto.ts │ │ └── answer.dto.ts │ ├── answers.module.ts │ ├── answer.entity.ts │ ├── answers.controller.ts │ └── answers.service.ts ├── users │ ├── dto │ │ ├── params-user.dto.ts │ │ ├── update-user.dto.ts │ │ └── user.dto.ts │ ├── user.decorator.ts │ ├── users.module.ts │ ├── user.entity.ts │ ├── users.controller.ts │ └── users.service.ts ├── comments │ ├── dto │ │ ├── create-comment.dto.ts │ │ └── params-comment.dto.ts │ ├── comments.module.ts │ ├── comment.entity.ts │ ├── comments.controller.ts │ └── comments.service.ts ├── utils │ ├── validation │ │ ├── validation.exception.ts │ │ └── validation.pipe.ts │ └── base.ts ├── posts │ ├── dto │ │ ├── post.dto.ts │ │ └── params-post.dto.ts │ ├── posts.module.ts │ ├── posts.controller.ts │ ├── post.entity.ts │ └── posts.service.ts ├── questions │ ├── dto │ │ ├── question.dto.ts │ │ └── params-question.dto.ts │ ├── questions.module.ts │ ├── question.entity.ts │ ├── questions.controller.ts │ └── questions.service.ts ├── auth │ ├── dto │ │ └── login-user.dto.ts │ ├── auth.module.ts │ ├── strategies │ │ ├── github.strategy.ts │ │ └── google.strategy.ts │ ├── auth.controller.ts │ └── auth.service.ts ├── files │ ├── files.module.ts │ ├── files.controller.ts │ └── files.service.ts ├── main.ts ├── guards │ └── jwt-auth.guard.ts └── app.module.ts ├── tsconfig.build.json ├── nest-cli.json ├── .env ├── .gitignore ├── README.md ├── tsconfig.json ├── .eslintrc.js └── package.json /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /src/tags/dto/tag.dto.ts: -------------------------------------------------------------------------------- 1 | export class TagDto { 2 | readonly name: string; 3 | readonly description: string; 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/categories/dto/category.dto.ts: -------------------------------------------------------------------------------- 1 | export class CategoryDto { 2 | readonly label: string; 3 | readonly description?: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/answers/dto/params-answer.dto.ts: -------------------------------------------------------------------------------- 1 | export class ParamsAnswerDto { 2 | readonly questionId: number; 3 | readonly orderBy: string; 4 | } 5 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PORT=7777 2 | POSTGRES_HOST=localhost 3 | POSTGRES_USERNAME=postgres 4 | POSTGRES_PASSWORD=1036845297 5 | POSTGRES_DB=nest_forum 6 | POSTGRES_PORT=5432 -------------------------------------------------------------------------------- /src/tags/dto/search-tag.dto.ts: -------------------------------------------------------------------------------- 1 | export class SearchTagDto { 2 | readonly search?: string; 3 | readonly limit?: number; 4 | readonly page?: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/users/dto/params-user.dto.ts: -------------------------------------------------------------------------------- 1 | export class ParamsUserDto { 2 | readonly limit?: number; 3 | readonly page?: number; 4 | readonly search?: number; 5 | } -------------------------------------------------------------------------------- /src/comments/dto/create-comment.dto.ts: -------------------------------------------------------------------------------- 1 | export class CommentDto { 2 | readonly text: string; 3 | readonly questionId?: number; 4 | readonly answerId?: number; 5 | } -------------------------------------------------------------------------------- /src/comments/dto/params-comment.dto.ts: -------------------------------------------------------------------------------- 1 | export class ParamsCommentDto { 2 | readonly questionId?: number; 3 | readonly answerId?: number; 4 | readonly postId?: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/users/dto/update-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/mapped-types'; 2 | import { UserDto } from './user.dto'; 3 | 4 | export class UpdateUserDto extends PartialType(UserDto) {} 5 | -------------------------------------------------------------------------------- /src/answers/dto/answer.dto.ts: -------------------------------------------------------------------------------- 1 | import { OutputBlockData } from 'src/questions/dto/question.dto'; 2 | 3 | export class AnswerDto { 4 | readonly body: OutputBlockData[]; 5 | questionId: number; 6 | readonly isAnswer: boolean; 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/validation/validation.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus } from '@nestjs/common'; 2 | 3 | export class ValidationException extends HttpException { 4 | messages; 5 | 6 | constructor(resp) { 7 | super(resp, HttpStatus.BAD_REQUEST); 8 | this.messages = resp; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/posts/dto/post.dto.ts: -------------------------------------------------------------------------------- 1 | import { OutputBlockData } from 'src/questions/dto/question.dto'; 2 | import { TagEntity } from 'src/tags/tag.entity'; 3 | 4 | export class PostDto { 5 | readonly title: string; 6 | readonly body: OutputBlockData[]; 7 | readonly categoryId: number; 8 | tags: TagEntity[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/base.ts: -------------------------------------------------------------------------------- 1 | import { CreateDateColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; 2 | 3 | export abstract class Base { 4 | @PrimaryGeneratedColumn() 5 | id: number; 6 | 7 | @CreateDateColumn() 8 | createdAt: string; 9 | 10 | @UpdateDateColumn() 11 | updateAt: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/questions/dto/question.dto.ts: -------------------------------------------------------------------------------- 1 | import { TagEntity } from 'src/tags/tag.entity'; 2 | 3 | export type OutputBlockData = { 4 | id?: number; 5 | type: any; 6 | data: any; 7 | }; 8 | 9 | export class QuestionDto { 10 | readonly title: string; 11 | readonly body: OutputBlockData[]; 12 | tags: TagEntity[]; 13 | } 14 | -------------------------------------------------------------------------------- /src/questions/dto/params-question.dto.ts: -------------------------------------------------------------------------------- 1 | export class ParamsQuestionDto { 2 | readonly limit?: number; 3 | readonly page?: number; 4 | readonly orderBy?: string; 5 | readonly tagBy?: string; 6 | readonly userId?: number; 7 | readonly search?: string; 8 | readonly isAnswer?: string; 9 | readonly favorites?: boolean; 10 | } 11 | -------------------------------------------------------------------------------- /src/users/user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | import { UserEntity } from './user.entity'; 3 | 4 | export const User = createParamDecorator( 5 | (_: unknown, ctx: ExecutionContext): UserEntity => { 6 | const request = ctx.switchToHttp().getRequest(); 7 | return request.user.id; 8 | }, 9 | ); -------------------------------------------------------------------------------- /src/auth/dto/login-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString } from 'class-validator'; 2 | 3 | export class LoginUserDto { 4 | @IsString({ message: 'Поле должно быть строкой' }) 5 | @IsEmail({}, { message: 'Некорректный email' }) 6 | readonly email: string; 7 | 8 | @IsString({ message: 'Поле должно быть строкой' }) 9 | readonly password: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/posts/dto/params-post.dto.ts: -------------------------------------------------------------------------------- 1 | export class ParamsPostDto { 2 | readonly limit?: number; 3 | readonly page?: number; 4 | readonly orderBy?: string; 5 | readonly searchBy?: string; 6 | readonly categoryId?: number; 7 | readonly tagBy?: number; 8 | readonly userId?: number; 9 | readonly favorites?: boolean; 10 | readonly isShort?: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /src/files/files.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FilesService } from './files.service'; 3 | import { FilesController } from './files.controller'; 4 | import { AuthModule } from 'src/auth/auth.module'; 5 | 6 | @Module({ 7 | imports: [AuthModule], 8 | controllers: [FilesController], 9 | providers: [FilesService], 10 | exports: [FilesService], 11 | }) 12 | export class FilesModule {} 13 | -------------------------------------------------------------------------------- /src/categories/category.entity.ts: -------------------------------------------------------------------------------- 1 | import { Base } from 'src/utils/base'; 2 | import { Column, Entity } from 'typeorm'; 3 | 4 | @Entity('categories') 5 | export class CategoryEntity extends Base { 6 | @Column({ unique: true }) 7 | value: string; 8 | 9 | @Column({ unique: true }) 10 | label: string; 11 | 12 | @Column() 13 | description: string; 14 | 15 | @Column({ default: 0 }) 16 | postsCount: number; 17 | } 18 | -------------------------------------------------------------------------------- /src/tags/tags.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TagsService } from './tags.service'; 3 | import { TagsController } from './tags.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { TagEntity } from './tag.entity'; 6 | 7 | @Module({ 8 | controllers: [TagsController], 9 | providers: [TagsService], 10 | imports: [TypeOrmModule.forFeature([TagEntity])], 11 | 12 | }) 13 | export class TagsModule {} 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { ValidationPipe } from './utils/validation/validation.pipe'; 4 | 5 | async function start() { 6 | const port = process.env.PORT || 7777; 7 | const app = await NestFactory.create(AppModule); 8 | 9 | app.enableCors(); 10 | app.useGlobalPipes(new ValidationPipe()); 11 | 12 | await app.listen(port, () => console.log(`Server starting in port: ${port}`)); 13 | } 14 | start(); 15 | -------------------------------------------------------------------------------- /src/categories/categories.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CategoriesService } from './categories.service'; 3 | import { CategoriesController } from './categories.controller'; 4 | import { CategoryEntity } from './category.entity'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([CategoryEntity])], 9 | controllers: [CategoriesController], 10 | providers: [CategoriesService], 11 | }) 12 | export class CategoriesModule {} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json -------------------------------------------------------------------------------- /src/comments/comments.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CommentsService } from './comments.service'; 3 | import { CommentsController } from './comments.controller'; 4 | import { AuthModule } from 'src/auth/auth.module'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { CommentEntity } from './comment.entity'; 7 | 8 | @Module({ 9 | controllers: [CommentsController], 10 | providers: [CommentsService], 11 | imports: [TypeOrmModule.forFeature([CommentEntity]), AuthModule], 12 | }) 13 | export class CommentsModule {} 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NEST DIPLOM FORUM 🕹 2 | 3 | The project is a backend part of the thesis, which is a forum for programmers. It is written on the NestJS framework. 4 | 5 | 📌Frontend: https://github.com/FINIKKKK/next-diplom-forum 6 | 7 | - Framework: **NestJS** 8 | - ORM: **TypeOrm** 9 | - DB: **Postgress** 10 | 11 | ## Backend includes functionality 🛠: 12 | - Authorization and registration with JWT 13 | - File uploading 14 | - DTO validation 15 | - JWT auth guard (Granting access using a token) 16 | - It incorporates various entities such as **questions**, **tags**, **users**, **comments** with various methods -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/answers/answers.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AnswersService } from './answers.service'; 3 | import { AnswersController } from './answers.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { AnswerEntity } from './answer.entity'; 6 | import { AuthModule } from 'src/auth/auth.module'; 7 | import { CommentEntity } from 'src/comments/comment.entity'; 8 | 9 | @Module({ 10 | controllers: [AnswersController], 11 | providers: [AnswersService], 12 | imports: [ 13 | TypeOrmModule.forFeature([AnswerEntity, CommentEntity]), 14 | AuthModule, 15 | ], 16 | }) 17 | export class AnswersModule {} 18 | -------------------------------------------------------------------------------- /src/files/files.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Post, 5 | UploadedFile, 6 | UseGuards, 7 | UseInterceptors, 8 | } from '@nestjs/common'; 9 | import { FileInterceptor } from '@nestjs/platform-express'; 10 | import { JwtAuthGuard } from 'src/guards/jwt-auth.guard'; 11 | import { FilesService } from './files.service'; 12 | 13 | @Controller('files') 14 | export class FilesController { 15 | constructor(private readonly filesService: FilesService) {} 16 | 17 | @UseGuards(JwtAuthGuard) 18 | @Post() 19 | @UseInterceptors(FileInterceptor('image')) 20 | upload(@UploadedFile() image, @Body() dto: { imagePath: string }) { 21 | return this.filesService.uploadImage(image, dto); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/tags/tag.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, ManyToMany } from 'typeorm'; 2 | import { Base } from 'src/utils/base'; 3 | import { QuestionEntity } from 'src/questions/question.entity'; 4 | import { PostEntity } from 'src/posts/post.entity'; 5 | 6 | @Entity('tags') 7 | export class TagEntity extends Base { 8 | @Column({ unique: true }) 9 | name: string; 10 | 11 | @Column() 12 | description: string; 13 | 14 | @ManyToMany(() => QuestionEntity, (question) => question.tags) 15 | questions: QuestionEntity[]; 16 | 17 | @Column({ default: 0 }) 18 | questionCount: number; 19 | 20 | @ManyToMany(() => PostEntity, (post) => post.tags) 21 | posts: PostEntity[]; 22 | 23 | @Column({ default: 0 }) 24 | postsCount: number; 25 | } 26 | -------------------------------------------------------------------------------- /src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { UsersService } from './users.service'; 3 | import { UsersController } from './users.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { UserEntity } from './user.entity'; 6 | import { FilesModule } from 'src/files/files.module'; 7 | import { AuthModule } from 'src/auth/auth.module'; 8 | import { QuestionEntity } from 'src/questions/question.entity'; 9 | 10 | @Module({ 11 | controllers: [UsersController], 12 | providers: [UsersService], 13 | imports: [ 14 | TypeOrmModule.forFeature([UserEntity, QuestionEntity]), 15 | FilesModule, 16 | forwardRef(() => AuthModule), 17 | ], 18 | exports: [UsersService], 19 | }) 20 | export class UsersModule {} 21 | -------------------------------------------------------------------------------- /src/utils/validation/validation.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, PipeTransform } from '@nestjs/common/interfaces'; 2 | import { plainToClass } from 'class-transformer'; 3 | import { validate } from 'class-validator'; 4 | import { ValidationException } from './validation.exception'; 5 | 6 | export class ValidationPipe implements PipeTransform { 7 | async transform(value: any, metadata: ArgumentMetadata) { 8 | const obj = plainToClass(metadata.metatype, value); 9 | const errors = await validate(obj); 10 | if (errors.length) { 11 | let messages = errors.map((err) => { 12 | return `${err.property} = ${Object.values(err.constraints).join(' ')}`; 13 | }); 14 | throw new ValidationException(messages); 15 | } 16 | return value; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { AuthController } from './auth.controller'; 4 | import { UsersModule } from 'src/users/users.module'; 5 | import { JwtModule } from '@nestjs/jwt/dist'; 6 | import { UserEntity } from 'src/users/user.entity'; 7 | import { TypeOrmModule } from '@nestjs/typeorm'; 8 | 9 | @Module({ 10 | controllers: [AuthController], 11 | providers: [AuthService], 12 | imports: [ 13 | forwardRef(() => UsersModule), 14 | JwtModule.register({ 15 | secret: process.env.SECRET_KEY || 'SECRET', 16 | signOptions: { 17 | expiresIn: '14d', 18 | }, 19 | }), 20 | ], 21 | exports: [AuthService, JwtModule], 22 | }) 23 | export class AuthModule {} 24 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | endOfLine: 'auto', 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/auth/strategies/github.strategy.ts: -------------------------------------------------------------------------------- 1 | // import { Injectable } from '@nestjs/common'; 2 | // import { PassportStrategy } from '@nestjs/passport'; 3 | // import { Profile, Strategy } from 'passport-github'; 4 | // 5 | // @Injectable() 6 | // export class GithubStrategy extends PassportStrategy(Strategy, 'github') { 7 | // constructor() { 8 | // super({ 9 | // clientID: process.env.GITHUB_CLIENT_ID, 10 | // clientSecret: process.env.GITHUB_CLIENT_SERVER, 11 | // callbackURL: process.env.GITHUB_CALLBACK_URL, 12 | // scope: ['public_profile'], 13 | // }); 14 | // } 15 | // 16 | // async validate( 17 | // _accessToken: string, 18 | // _refreshToken: string, 19 | // profile: Profile, 20 | // ): Promise { 21 | // return profile; 22 | // } 23 | // } 24 | -------------------------------------------------------------------------------- /src/users/dto/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsOptional, IsString, Length } from 'class-validator'; 2 | 3 | export class UserDto { 4 | @IsString({ message: 'Поле должно быть строкой' }) 5 | @Length(3, Number.MAX_SAFE_INTEGER, { 6 | message: 'Логин должен состоять минимум из 3 символов', 7 | }) 8 | readonly login: string; 9 | 10 | @IsString({ message: 'Поле должно быть строкой' }) 11 | @IsEmail({}, { message: 'Некорректный email' }) 12 | readonly email: string; 13 | 14 | @IsOptional() 15 | @IsString({ message: 'Поле должно быть строкой' }) 16 | @Length(9, Number.MAX_SAFE_INTEGER, { 17 | message: 'Пароль должен состоять минимум из 9 символов', 18 | }) 19 | readonly password?: string; 20 | 21 | readonly name?: string; 22 | 23 | readonly about?: string; 24 | 25 | readonly location?: string; 26 | } 27 | -------------------------------------------------------------------------------- /src/posts/posts.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PostsService } from './posts.service'; 3 | import { PostsController } from './posts.controller'; 4 | import { PostEntity } from './post.entity'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { AuthModule } from 'src/auth/auth.module'; 7 | import { CategoryEntity } from 'src/categories/category.entity'; 8 | import { TagEntity } from 'src/tags/tag.entity'; 9 | import { CommentEntity } from 'src/comments/comment.entity'; 10 | import { UserEntity } from 'src/users/user.entity'; 11 | 12 | @Module({ 13 | imports: [ 14 | TypeOrmModule.forFeature([ 15 | PostEntity, 16 | CategoryEntity, 17 | TagEntity, 18 | CommentEntity, 19 | UserEntity, 20 | ]), 21 | AuthModule, 22 | ], 23 | controllers: [PostsController], 24 | providers: [PostsService], 25 | }) 26 | export class PostsModule {} 27 | -------------------------------------------------------------------------------- /src/answers/answer.entity.ts: -------------------------------------------------------------------------------- 1 | import { OutputBlockData } from 'src/questions/dto/question.dto'; 2 | import { QuestionEntity } from 'src/questions/question.entity'; 3 | import { UserEntity } from 'src/users/user.entity'; 4 | import { Base } from 'src/utils/base'; 5 | import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne } from 'typeorm'; 6 | 7 | @Entity('answers') 8 | export class AnswerEntity extends Base { 9 | @CreateDateColumn() 10 | updated: Date; 11 | 12 | @Column({ type: 'jsonb' }) 13 | body: OutputBlockData[]; 14 | 15 | @ManyToOne(() => UserEntity, (user) => user.id) 16 | @JoinColumn({ name: 'user' }) 17 | user: UserEntity; 18 | 19 | @ManyToOne(() => QuestionEntity, (question) => question.id) 20 | @JoinColumn({ name: 'question' }) 21 | question: QuestionEntity; 22 | 23 | @Column({ default: false }) 24 | isAnswer: boolean; 25 | 26 | @Column({ default: 0 }) 27 | rating: number; 28 | } 29 | -------------------------------------------------------------------------------- /src/questions/questions.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { QuestionsService } from './questions.service'; 3 | import { QuestionsController } from './questions.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { QuestionEntity } from './question.entity'; 6 | import { AuthModule } from 'src/auth/auth.module'; 7 | import { AnswerEntity } from 'src/answers/answer.entity'; 8 | import { CommentEntity } from 'src/comments/comment.entity'; 9 | import { UserEntity } from 'src/users/user.entity'; 10 | import { TagEntity } from 'src/tags/tag.entity'; 11 | 12 | @Module({ 13 | controllers: [QuestionsController], 14 | providers: [QuestionsService], 15 | imports: [ 16 | TypeOrmModule.forFeature([ 17 | QuestionEntity, 18 | AnswerEntity, 19 | CommentEntity, 20 | UserEntity, 21 | TagEntity, 22 | ]), 23 | AuthModule, 24 | ], 25 | }) 26 | export class QuestionsModule {} 27 | -------------------------------------------------------------------------------- /src/comments/comment.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, ManyToOne, JoinColumn } from 'typeorm'; 2 | import { Base } from 'src/utils/base'; 3 | import { UserEntity } from 'src/users/user.entity'; 4 | import { QuestionEntity } from 'src/questions/question.entity'; 5 | import { AnswerEntity } from 'src/answers/answer.entity'; 6 | import { PostEntity } from 'src/posts/post.entity'; 7 | 8 | @Entity('comments') 9 | export class CommentEntity extends Base { 10 | @Column() 11 | text: string; 12 | 13 | @ManyToOne(() => UserEntity, (user) => user.id) 14 | @JoinColumn({ name: 'user' }) 15 | user: UserEntity; 16 | 17 | @ManyToOne(() => QuestionEntity, (question) => question.id) 18 | @JoinColumn({ name: 'question' }) 19 | question: QuestionEntity; 20 | 21 | @ManyToOne(() => AnswerEntity, (answer) => answer.id) 22 | @JoinColumn({ name: 'answer' }) 23 | answer: AnswerEntity; 24 | 25 | @ManyToOne(() => PostEntity, (post) => post.id) 26 | @JoinColumn({ name: 'post' }) 27 | post: PostEntity; 28 | } 29 | -------------------------------------------------------------------------------- /src/categories/categories.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | } from '@nestjs/common'; 10 | import { CategoriesService } from './categories.service'; 11 | import { CategoryDto } from './dto/category.dto'; 12 | 13 | @Controller('categories') 14 | export class CategoriesController { 15 | constructor(private readonly categoriesService: CategoriesService) {} 16 | 17 | @Post() 18 | create(@Body() dto: CategoryDto) { 19 | return this.categoriesService.createCategory(dto); 20 | } 21 | 22 | @Get() 23 | findAll() { 24 | return this.categoriesService.getAllCategories(); 25 | } 26 | 27 | @Get(':id') 28 | findOne(@Param('id') id: number) { 29 | return this.categoriesService.getCategoryById(id); 30 | } 31 | 32 | @Patch(':id') 33 | update(@Param('id') id: number, @Body() dto: CategoryDto) { 34 | return this.categoriesService.updateCategory(id, dto); 35 | } 36 | 37 | @Delete(':id') 38 | remove(@Param('id') id: number) { 39 | return this.categoriesService.removeCategory(id); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/tags/tags.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | } from '@nestjs/common'; 10 | import { Query } from '@nestjs/common/decorators'; 11 | import { SearchTagDto } from './dto/search-tag.dto'; 12 | import { TagDto } from './dto/tag.dto'; 13 | import { TagsService } from './tags.service'; 14 | 15 | @Controller('tags') 16 | export class TagsController { 17 | constructor(private readonly tagsService: TagsService) {} 18 | 19 | @Post() 20 | create(@Body() dto: TagDto) { 21 | return this.tagsService.createTag(dto); 22 | } 23 | 24 | @Get() 25 | findAll(@Query() dto: SearchTagDto) { 26 | return this.tagsService.getAll(dto); 27 | } 28 | 29 | @Get(':id') 30 | findOne(@Param('id') id: number) { 31 | return this.tagsService.getTagById(id); 32 | } 33 | 34 | @Patch(':id') 35 | update(@Param('id') id: number, @Body() dto: TagDto) { 36 | return this.tagsService.updateTag(id, dto); 37 | } 38 | 39 | @Delete(':id') 40 | remove(@Param('id') id: number) { 41 | return this.tagsService.removeTag(id); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/auth/strategies/google.strategy.ts: -------------------------------------------------------------------------------- 1 | // import { Injectable } from '@nestjs/common'; 2 | // import { PassportStrategy } from '@nestjs/passport'; 3 | // import { Strategy, VerifyCallback } from 'passport-google-oauth2'; 4 | // 5 | // @Injectable() 6 | // export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { 7 | // constructor() { 8 | // super({ 9 | // clientID: process.env.GOOGLE_CLIENT_ID, 10 | // clientSecret: process.env.GOOGLE_CLIENT_SERVER, 11 | // callbackURL: process.env.GOOGLE_CALLBACK_URL, 12 | // scope: ['profile', 'email'], 13 | // }); 14 | // } 15 | // 16 | // async validate( 17 | // _accessToken: string, 18 | // _refreshToken: string, 19 | // profile: any, 20 | // done: VerifyCallback, 21 | // ): Promise { 22 | // const { id, name, emails, photos } = profile; 23 | // 24 | // const user = { 25 | // provider: 'google', 26 | // providerId: id, 27 | // email: emails[0].value, 28 | // name: `${name.givenName} ${name.familyName}`, 29 | // picture: photos[0].value, 30 | // }; 31 | // 32 | // done(null, user); 33 | // } 34 | // } 35 | -------------------------------------------------------------------------------- /src/guards/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | Injectable, 5 | UnauthorizedException, 6 | } from '@nestjs/common'; 7 | import { JwtService } from '@nestjs/jwt'; 8 | import { Observable } from 'rxjs'; 9 | 10 | @Injectable() 11 | export class JwtAuthGuard implements CanActivate { 12 | constructor(private jwtService: JwtService) {} 13 | 14 | canActivate( 15 | context: ExecutionContext, 16 | ): boolean | Promise | Observable { 17 | const req = context.switchToHttp().getRequest(); 18 | try { 19 | const authHeader = req.headers.authorization; 20 | const bearer = authHeader.split(' ')[0]; 21 | const token = authHeader.split(' ')[1]; 22 | 23 | if (bearer !== 'Bearer' || !token) { 24 | throw new UnauthorizedException({ 25 | message: 'Пользователь не авторизован', 26 | }); 27 | } 28 | 29 | const user = this.jwtService.verify(token); 30 | req.user = user; 31 | return true; 32 | } catch (err) { 33 | throw new UnauthorizedException({ 34 | message: 'Пользователь не авторизован', 35 | }); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/questions/question.entity.ts: -------------------------------------------------------------------------------- 1 | import { TagEntity } from 'src/tags/tag.entity'; 2 | import { UserEntity } from 'src/users/user.entity'; 3 | import { AnswerEntity } from 'src/answers/answer.entity'; 4 | import { Base } from 'src/utils/base'; 5 | import { 6 | Column, 7 | CreateDateColumn, 8 | Entity, 9 | JoinColumn, 10 | JoinTable, 11 | ManyToMany, 12 | ManyToOne, 13 | OneToMany, 14 | RelationCount, 15 | } from 'typeorm'; 16 | import { OutputBlockData } from './dto/question.dto'; 17 | 18 | @Entity('questions') 19 | export class QuestionEntity extends Base { 20 | @CreateDateColumn() 21 | updated: Date; 22 | 23 | @Column() 24 | title: string; 25 | 26 | @Column({ type: 'jsonb' }) 27 | body: OutputBlockData[]; 28 | 29 | @Column({ default: 0 }) 30 | views: number; 31 | 32 | @ManyToOne(() => UserEntity, (user) => user.id) 33 | @JoinColumn({ name: 'user' }) 34 | user: UserEntity; 35 | 36 | @OneToMany(() => AnswerEntity, (answer) => answer.question) 37 | answers: AnswerEntity[]; 38 | 39 | @RelationCount((question: QuestionEntity) => question.answers) 40 | answerCount: number; 41 | 42 | @ManyToMany(() => TagEntity, (tag) => tag.questions) 43 | @JoinTable() 44 | tags: TagEntity[]; 45 | 46 | @Column({ type: 'boolean', default: false }) 47 | isAnswer: boolean; 48 | } 49 | -------------------------------------------------------------------------------- /src/posts/posts.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | UseGuards, 10 | Query, 11 | } from '@nestjs/common'; 12 | import { JwtAuthGuard } from 'src/guards/jwt-auth.guard'; 13 | import { User } from 'src/users/user.decorator'; 14 | import { ParamsPostDto } from './dto/params-post.dto'; 15 | import { PostDto } from './dto/post.dto'; 16 | import { PostsService } from './posts.service'; 17 | 18 | @Controller('posts') 19 | export class PostsController { 20 | constructor(private readonly postsService: PostsService) {} 21 | 22 | @UseGuards(JwtAuthGuard) 23 | @Post() 24 | create(@Body() dto: PostDto, @User() userId: number) { 25 | return this.postsService.createPost(dto, userId); 26 | } 27 | 28 | @Get() 29 | findAll(@Query() dto: ParamsPostDto) { 30 | return this.postsService.getAllPosts(dto); 31 | } 32 | 33 | @Get(':id') 34 | findOne(@Param('id') id: number) { 35 | return this.postsService.getPostById(id); 36 | } 37 | 38 | @UseGuards(JwtAuthGuard) 39 | @Patch(':id') 40 | update(@Param('id') id: number, @Body() dto: PostDto) { 41 | return this.postsService.updatePost(id, dto); 42 | } 43 | 44 | @UseGuards(JwtAuthGuard) 45 | @Delete(':id') 46 | remove(@Param('id') id: number) { 47 | return this.postsService.removePost(id); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | Post, 6 | Req, 7 | UseGuards, 8 | } from '@nestjs/common'; 9 | import {AuthGuard} from '@nestjs/passport'; 10 | import {UserDto} from 'src/users/dto/user.dto'; 11 | import {AuthService} from './auth.service'; 12 | import {LoginUserDto} from './dto/login-user.dto'; 13 | 14 | @Controller('auth') 15 | export class AuthController { 16 | constructor(private readonly authService: AuthService) { 17 | } 18 | 19 | @Post('/login') 20 | login(@Body() dto: LoginUserDto) { 21 | return this.authService.login(dto); 22 | } 23 | 24 | @Post('/register') 25 | register(@Body() dto: UserDto) { 26 | return this.authService.register(dto); 27 | } 28 | 29 | // @Get('google') 30 | // @UseGuards(AuthGuard('google')) 31 | // async authGoogle() {} 32 | 33 | // @Get('google/callback') 34 | // @UseGuards(AuthGuard('google')) 35 | // async googleAuthCallback(@Req() req) { 36 | // this.authService.authSocial(req.user) 37 | // } 38 | // 39 | // @Get('github') 40 | // @UseGuards(AuthGuard('github')) 41 | // async authGithub() {} 42 | // 43 | // @Get('github/callback') 44 | // @UseGuards(AuthGuard('github')) 45 | // async githubAuthCallback(@Req() req) { 46 | // console.log(req.user); 47 | // this.authService.authSocial(req.user)s 48 | // } 49 | } 50 | -------------------------------------------------------------------------------- /src/comments/comments.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | Query, 10 | UseGuards, 11 | } from '@nestjs/common'; 12 | import { JwtAuthGuard } from 'src/guards/jwt-auth.guard'; 13 | import { User } from 'src/users/user.decorator'; 14 | import { CommentsService } from './comments.service'; 15 | import { CommentDto } from './dto/create-comment.dto'; 16 | import { ParamsCommentDto } from './dto/params-comment.dto'; 17 | 18 | @Controller('comments') 19 | export class CommentsController { 20 | constructor(private readonly commentsService: CommentsService) {} 21 | 22 | @UseGuards(JwtAuthGuard) 23 | @Post() 24 | create(@Body() dto: CommentDto, @User() userId: number) { 25 | return this.commentsService.createComment(dto, userId); 26 | } 27 | 28 | @Get() 29 | findAll(@Query() dto: ParamsCommentDto) { 30 | return this.commentsService.getAllComments(dto); 31 | } 32 | 33 | @Get(':id') 34 | findOne(@Param('id') id: number) { 35 | return this.commentsService.getCommentById(id); 36 | } 37 | 38 | @UseGuards(JwtAuthGuard) 39 | @Patch(':id') 40 | update(@Param('id') id: number, @Body() dto: CommentDto) { 41 | return this.commentsService.updateComment(id, dto); 42 | } 43 | 44 | @UseGuards(JwtAuthGuard) 45 | @Delete(':id') 46 | remove(@Param('id') id: number) { 47 | return this.commentsService.removeComment(id); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/questions/questions.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | UseGuards, 10 | Query, 11 | } from '@nestjs/common'; 12 | import { QuestionsService } from './questions.service'; 13 | import { QuestionDto } from './dto/question.dto'; 14 | import { User } from 'src/users/user.decorator'; 15 | import { JwtAuthGuard } from 'src/guards/jwt-auth.guard'; 16 | import { ParamsQuestionDto } from './dto/params-question.dto'; 17 | 18 | @Controller('questions') 19 | export class QuestionsController { 20 | constructor(private readonly questionsService: QuestionsService) {} 21 | 22 | @UseGuards(JwtAuthGuard) 23 | @Post() 24 | create(@Body() dto: QuestionDto, @User() userId: number) { 25 | return this.questionsService.createQuestion(dto, userId); 26 | } 27 | 28 | @Get() 29 | findAll(@Query() dto: ParamsQuestionDto) { 30 | return this.questionsService.getAll(dto); 31 | } 32 | 33 | @Get(':id') 34 | findOne(@Param('id') id: number) { 35 | return this.questionsService.getQuestionById(id); 36 | } 37 | 38 | @UseGuards(JwtAuthGuard) 39 | @Patch(':id') 40 | update(@Param('id') id: number, @Body() dto: QuestionDto) { 41 | return this.questionsService.updateQuestion(id, dto); 42 | } 43 | 44 | @UseGuards(JwtAuthGuard) 45 | @Delete(':id') 46 | remove(@Param('id') id: number) { 47 | return this.questionsService.removeQuestion(id); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/users/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | Entity, 4 | OneToMany, 5 | RelationCount, 6 | } from 'typeorm'; 7 | import { Base } from 'src/utils/base'; 8 | import { QuestionEntity } from 'src/questions/question.entity'; 9 | import { AnswerEntity } from 'src/answers/answer.entity'; 10 | 11 | @Entity('users') 12 | export class UserEntity extends Base { 13 | @Column({ unique: true }) 14 | login: string; 15 | 16 | @Column({ unique: true }) 17 | email: string; 18 | 19 | @Column({ nullable: true }) 20 | password: string; 21 | 22 | @Column({ nullable: true }) 23 | name?: string; 24 | 25 | @Column({ nullable: true }) 26 | avatar: string; 27 | 28 | @OneToMany(() => QuestionEntity, (question) => question.user) 29 | questions: QuestionEntity[]; 30 | 31 | @RelationCount((user: UserEntity) => user.questions) 32 | questionCount: number; 33 | 34 | @OneToMany(() => AnswerEntity, (answer) => answer.user) 35 | answers: AnswerEntity[]; 36 | 37 | @RelationCount((user: UserEntity) => user.answers) 38 | answerCount: number; 39 | 40 | @Column({ nullable: true }) 41 | about?: string; 42 | 43 | @Column({ nullable: true }) 44 | location?: string; 45 | 46 | // @ManyToMany(() => QuestionEntity, (question) => question.id) 47 | // @JoinTable() 48 | // favorites?: QuestionEntity[]; 49 | 50 | @Column({ type: 'jsonb', default: [] }) 51 | favorites!: Number[]; 52 | 53 | @Column({ default: true }) 54 | showEmail: boolean; 55 | 56 | @Column({ default: false }) 57 | isAdmin: boolean; 58 | } 59 | -------------------------------------------------------------------------------- /src/answers/answers.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | } from '@nestjs/common'; 10 | import { Query, UseGuards } from '@nestjs/common/decorators'; 11 | import { JwtAuthGuard } from 'src/guards/jwt-auth.guard'; 12 | import { User } from 'src/users/user.decorator'; 13 | import { AnswersService } from './answers.service'; 14 | import { AnswerDto } from './dto/answer.dto'; 15 | import { ParamsAnswerDto } from './dto/params-answer.dto'; 16 | 17 | @Controller('answers') 18 | export class AnswersController { 19 | constructor(private readonly answersService: AnswersService) {} 20 | 21 | @UseGuards(JwtAuthGuard) 22 | @Post() 23 | create(@Body() dto: AnswerDto, @User() userId: number) { 24 | return this.answersService.createAnswer(dto, userId); 25 | } 26 | 27 | @Get() 28 | findAll(@Query() dto: ParamsAnswerDto) { 29 | return this.answersService.getAllAnswers(dto); 30 | } 31 | 32 | @Get(':id') 33 | findOne(@Param('id') id: number) { 34 | return this.answersService.getAnswerById(id); 35 | } 36 | 37 | @Patch('/isAnswer/:id') 38 | setIsSolved(@Param('id') id: number, @Body() dto: AnswerDto) { 39 | return this.answersService.setAnswerIsSolved(id, dto); 40 | } 41 | 42 | @Patch(':id') 43 | update(@Param('id') id: number, @Body() dto: AnswerDto) { 44 | return this.answersService.updateAnswer(id, dto); 45 | } 46 | 47 | @Delete(':id') 48 | remove(@Param('id') id: number) { 49 | return this.answersService.removeAnswer(id); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/posts/post.entity.ts: -------------------------------------------------------------------------------- 1 | import { CategoryEntity } from 'src/categories/category.entity'; 2 | import { CommentEntity } from 'src/comments/comment.entity'; 3 | import { OutputBlockData } from 'src/questions/dto/question.dto'; 4 | import { TagEntity } from 'src/tags/tag.entity'; 5 | import { UserEntity } from 'src/users/user.entity'; 6 | import { Base } from 'src/utils/base'; 7 | import { 8 | Column, 9 | CreateDateColumn, 10 | Entity, 11 | JoinColumn, 12 | JoinTable, 13 | ManyToMany, 14 | ManyToOne, 15 | OneToMany, 16 | RelationCount, 17 | } from 'typeorm'; 18 | 19 | @Entity('posts') 20 | export class PostEntity extends Base { 21 | @Column() 22 | title: string; 23 | 24 | @Column({ type: 'jsonb' }) 25 | body: OutputBlockData[]; 26 | 27 | @Column() 28 | image: string; 29 | 30 | @Column() 31 | description: string; 32 | 33 | @Column() 34 | slug: string; 35 | 36 | @Column({ default: 0 }) 37 | views: number; 38 | 39 | @ManyToOne(() => CategoryEntity, (category) => category.id) 40 | @JoinColumn({ name: 'category' }) 41 | category: CategoryEntity; 42 | 43 | @ManyToMany(() => TagEntity, (tag) => tag.posts) 44 | @JoinTable() 45 | tags: TagEntity[]; 46 | 47 | @ManyToOne(() => UserEntity, (user) => user.id) 48 | @JoinColumn({ name: 'user' }) 49 | user: UserEntity; 50 | 51 | @OneToMany(() => CommentEntity, (comment) => comment.post) 52 | comments: CommentEntity[]; 53 | 54 | @RelationCount((post: PostEntity) => post.comments) 55 | commentsCount: number; 56 | 57 | @CreateDateColumn() 58 | updated: Date; 59 | } 60 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { UsersModule } from './users/users.module'; 5 | import { AuthModule } from './auth/auth.module'; 6 | import { FilesModule } from './files/files.module'; 7 | import { ServeStaticModule } from '@nestjs/serve-static'; 8 | import { QuestionsModule } from './questions/questions.module'; 9 | import { TagsModule } from './tags/tags.module'; 10 | import * as path from 'path'; 11 | import { AnswersModule } from './answers/answers.module'; 12 | import { CommentsModule } from './comments/comments.module'; 13 | import { CategoriesModule } from './categories/categories.module'; 14 | import { PostsModule } from './posts/posts.module'; 15 | 16 | @Module({ 17 | imports: [ 18 | ServeStaticModule.forRoot({ 19 | rootPath: path.resolve(__dirname, 'static'), 20 | }), 21 | ConfigModule.forRoot({ 22 | envFilePath: '.env', 23 | }), 24 | TypeOrmModule.forRoot({ 25 | type: 'postgres', 26 | host: process.env.POSTGRES_HOST, 27 | port: Number(process.env.POSTGRES_PORT), 28 | username: process.env.POSTGRES_USERNAME, 29 | password: process.env.POSTGRES_PASSWORD, 30 | database: process.env.POSTGRES_DB, 31 | autoLoadEntities: true, 32 | synchronize: true, 33 | }), 34 | UsersModule, 35 | AuthModule, 36 | FilesModule, 37 | QuestionsModule, 38 | TagsModule, 39 | AnswersModule, 40 | CommentsModule, 41 | CategoriesModule, 42 | PostsModule, 43 | ], 44 | }) 45 | export class AppModule {} 46 | -------------------------------------------------------------------------------- /src/files/files.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | HttpException, 4 | HttpStatus, 5 | Injectable, 6 | } from '@nestjs/common'; 7 | import * as path from 'path'; 8 | import * as fs from 'fs'; 9 | import * as uuid from 'uuid'; 10 | 11 | @Injectable() 12 | export class FilesService { 13 | async uploadImage(image, dto) { 14 | try { 15 | if (image.size > 2 * 1024 * 1024) { 16 | throw new BadRequestException('Файл должен быть размером меньше 2мб'); 17 | } 18 | const exp = image.originalname.match(/\.[^.]+$/)[0]; 19 | if (exp === '.jpg' || exp === '.png' || exp === '.jpeg') { 20 | const fileName = uuid.v4() + exp; 21 | const filePath = path.resolve( 22 | __dirname, 23 | '..', 24 | `static/img/${dto.imagePath}`, 25 | ); 26 | if (!fs.existsSync(filePath)) { 27 | fs.mkdirSync(filePath, { recursive: true }); 28 | } 29 | fs.writeFileSync(path.join(filePath, fileName), image.buffer); 30 | const fullFileName = `http://localhost:7777/img${ 31 | '/' + dto.imagePath 32 | }/${fileName}`; 33 | return fullFileName; 34 | } 35 | throw new BadRequestException( 36 | 'Файл должен быть расширением png, jpg, jpeg', 37 | ); 38 | } catch (err) { 39 | if (err instanceof BadRequestException) { 40 | throw new HttpException(err.message, HttpStatus.BAD_REQUEST); 41 | } else { 42 | throw new HttpException( 43 | 'Не удалось загрузить файл', 44 | HttpStatus.INTERNAL_SERVER_ERROR, 45 | ); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/categories/categories.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { CategoryEntity } from './category.entity'; 5 | import { CategoryDto } from './dto/category.dto'; 6 | import * as translit from 'transliteration'; 7 | 8 | @Injectable() 9 | export class CategoriesService { 10 | constructor( 11 | @InjectRepository(CategoryEntity) 12 | private categoriesRepository: Repository, 13 | ) {} 14 | 15 | // createCategory ---------------------------------------------- 16 | async createCategory(dto: CategoryDto) { 17 | const slug = translit.slugify(dto.label); 18 | const categories = await this.categoriesRepository.save({ 19 | ...dto, 20 | value: slug, 21 | ...(dto.description 22 | ? { description: dto.description } 23 | : { description: dto.label }), 24 | }); 25 | return categories; 26 | } 27 | 28 | // getAllCategories ---------------------------------------------- 29 | async getAllCategories() { 30 | const qb = this.categoriesRepository.createQueryBuilder('categories'); 31 | const categories = qb.orderBy('categories.postsCount', 'DESC').getMany(); 32 | return categories; 33 | } 34 | 35 | // getCategoryById ---------------------------------------------- 36 | async getCategoryById(id: number) { 37 | const category = await this.categoriesRepository.findOne({ 38 | where: { 39 | id, 40 | }, 41 | }); 42 | return category; 43 | } 44 | 45 | // updateCategory ---------------------------------------------- 46 | async updateCategory(id: number, dto: CategoryDto) { 47 | const category = await this.categoriesRepository.update(id, dto); 48 | return category; 49 | } 50 | 51 | // removeCategory ---------------------------------------------- 52 | async removeCategory(id: number) { 53 | const category = await this.categoriesRepository.delete(id); 54 | return category; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/tags/tags.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { SearchTagDto } from './dto/search-tag.dto'; 5 | import { TagDto } from './dto/tag.dto'; 6 | import { TagEntity } from './tag.entity'; 7 | 8 | @Injectable() 9 | export class TagsService { 10 | constructor( 11 | @InjectRepository(TagEntity) 12 | private tagsRepository: Repository, 13 | ) {} 14 | 15 | async createTag(dto: TagDto) { 16 | const tagName = dto.name.toLocaleLowerCase(); 17 | const findTag = await this.tagsRepository.findOne({ 18 | where: { name: tagName }, 19 | }); 20 | 21 | if (findTag) { 22 | throw new HttpException( 23 | 'Такая метка уже существует', 24 | HttpStatus.BAD_REQUEST, 25 | ); 26 | } 27 | 28 | const tag = await this.tagsRepository.save({ 29 | ...dto, 30 | name: tagName, 31 | }); 32 | return tag; 33 | } 34 | 35 | async getAll(dto: SearchTagDto) { 36 | const qb = await this.tagsRepository.createQueryBuilder('tags'); 37 | 38 | const limit = dto.limit || 2; 39 | const page = dto.page || 2; 40 | 41 | if (dto.limit) { 42 | qb.take(dto.limit); 43 | } 44 | if (dto.page) { 45 | qb.skip((page - 1) * limit); 46 | } 47 | 48 | if (dto.search) { 49 | qb.where('LOWER(tags.name) LIKE LOWER(:name)', { 50 | name: `%${dto.search}%`, 51 | }); 52 | } 53 | 54 | const [items, total] = await qb 55 | .orderBy('tags.questionCount', 'DESC') 56 | .getManyAndCount(); 57 | 58 | return { total, items }; 59 | } 60 | 61 | async getTagById(id: number) { 62 | const tag = await this.tagsRepository.findOne({ 63 | where: { 64 | id, 65 | }, 66 | }); 67 | return tag; 68 | } 69 | 70 | async updateTag(id: number, dto: TagDto) { 71 | const tag = await this.tagsRepository.update(id, dto); 72 | return tag; 73 | } 74 | 75 | async removeTag(id: number) { 76 | const tag = await this.tagsRepository.delete(id); 77 | return tag; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Param, 7 | Patch, 8 | Request, 9 | } from '@nestjs/common'; 10 | import { 11 | Post, 12 | Query, 13 | UploadedFile, 14 | UseGuards, 15 | UseInterceptors, 16 | } from '@nestjs/common/decorators'; 17 | import { FileInterceptor } from '@nestjs/platform-express'; 18 | import { JwtAuthGuard } from 'src/guards/jwt-auth.guard'; 19 | import { ParamsUserDto } from './dto/params-user.dto'; 20 | import { UpdateUserDto } from './dto/update-user.dto'; 21 | import { UsersService } from './users.service'; 22 | 23 | @Controller('users') 24 | export class UsersController { 25 | constructor(private readonly usersService: UsersService) {} 26 | 27 | @UseGuards(JwtAuthGuard) 28 | @Post('/favorite') 29 | favorite(@Request() req, @Body() dto: { questionId: number }) { 30 | return this.usersService.addQuestionToFavorite(req.user.id, dto.questionId); 31 | } 32 | 33 | @UseGuards(JwtAuthGuard) 34 | @Get('/profile') 35 | getProfile(@Request() req) { 36 | return this.usersService.getUserById(req.user.id); 37 | } 38 | 39 | @Get() 40 | findAll(@Query() dto: ParamsUserDto) { 41 | return this.usersService.getAll(dto); 42 | } 43 | 44 | @Get(':login') 45 | findOne(@Param('login') login: number) { 46 | return this.usersService.getUserByLogin(`${login}`); 47 | } 48 | 49 | @UseGuards(JwtAuthGuard) 50 | @Patch('/avatar/:id') 51 | @UseInterceptors(FileInterceptor('avatar')) 52 | updateAvatar(@Param('id') id: number, @UploadedFile() avatar) { 53 | return this.usersService.updateUserAvatar(id, avatar); 54 | } 55 | 56 | @UseGuards(JwtAuthGuard) 57 | @Patch('/password/:id') 58 | updatePassword( 59 | @Param('id') id: number, 60 | @Body() dto: { oldPassword: string; newPassword: string }, 61 | ) { 62 | return this.usersService.updateUserPassword(id, dto); 63 | } 64 | 65 | @UseGuards(JwtAuthGuard) 66 | @Patch(':id') 67 | update(@Param('id') id: number, @Body() dto: UpdateUserDto) { 68 | return this.usersService.updateUser(id, dto); 69 | } 70 | 71 | @UseGuards(JwtAuthGuard) 72 | @Delete(':id') 73 | remove(@Param('id') id: number) { 74 | return this.usersService.removeUser(id); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { HttpStatus } from '@nestjs/common/enums'; 3 | import { 4 | HttpException, 5 | UnauthorizedException, 6 | } from '@nestjs/common/exceptions'; 7 | import { JwtService } from '@nestjs/jwt'; 8 | import { UserDto } from 'src/users/dto/user.dto'; 9 | import { UserEntity } from 'src/users/user.entity'; 10 | import { UsersService } from 'src/users/users.service'; 11 | import { LoginUserDto } from './dto/login-user.dto'; 12 | import * as bcrypt from 'bcryptjs'; 13 | 14 | @Injectable() 15 | export class AuthService { 16 | constructor( 17 | private usersService: UsersService, 18 | private jwtService: JwtService, 19 | ) {} 20 | 21 | async login(dto: LoginUserDto) { 22 | const user = await this.usersService.getUserByEmail(dto.email); 23 | const isTruePassword = user 24 | ? await bcrypt.compare(dto.password, user.password) 25 | : undefined; 26 | 27 | if (user && isTruePassword) { 28 | const { password, ...userData } = user; 29 | const token = await this.generateToken(user); 30 | return { 31 | ...userData, 32 | token, 33 | }; 34 | } 35 | throw new UnauthorizedException({ 36 | message: 'Неверный email или пароль', 37 | }); 38 | } 39 | 40 | async register(dto: UserDto) { 41 | const findUserByName = await this.usersService.getUserByLogin( 42 | dto.login.toLocaleLowerCase(), 43 | ); 44 | const findUserByEmail = await this.usersService.getUserByEmail(dto.email); 45 | if (findUserByName) { 46 | throw new HttpException( 47 | 'Данный логин уже используется', 48 | HttpStatus.BAD_REQUEST, 49 | ); 50 | } 51 | if (findUserByEmail) { 52 | throw new HttpException( 53 | 'Данный email уже используется', 54 | HttpStatus.BAD_REQUEST, 55 | ); 56 | } 57 | const hashPassword = dto.password 58 | ? await bcrypt.hash(dto.password, 5) 59 | : null; 60 | const user = await this.usersService.createUser({ 61 | ...dto, 62 | password: hashPassword, 63 | }); 64 | 65 | const { password, ...userData } = user; 66 | const token = await this.generateToken(user); 67 | return { 68 | ...userData, 69 | token, 70 | }; 71 | } 72 | 73 | async authSocial(dto: UserDto) { 74 | const user = await this.usersService.getUserByEmail(dto.email); 75 | 76 | if (user) { 77 | const { password, ...userData } = user; 78 | const token = await this.generateToken(user); 79 | return { 80 | ...userData, 81 | token, 82 | }; 83 | } else { 84 | return dto; 85 | } 86 | } 87 | 88 | private async generateToken(user: UserEntity) { 89 | const payload = { id: user.id, name: user.login, email: user.email }; 90 | return this.jwtService.sign(payload); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-forum", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^9.0.0", 25 | "@nestjs/config": "^2.3.0", 26 | "@nestjs/core": "^9.0.0", 27 | "@nestjs/jwt": "^10.0.2", 28 | "@nestjs/mapped-types": "*", 29 | "@nestjs/passport": "^9.0.3", 30 | "@nestjs/platform-express": "^9.0.0", 31 | "@nestjs/serve-static": "^3.0.0", 32 | "@nestjs/typeorm": "^9.0.1", 33 | "bcryptjs": "^2.4.3", 34 | "class-transformer": "^0.5.1", 35 | "class-validator": "^0.13.2", 36 | "lodash.debounce": "^4.0.8", 37 | "passport": "^0.6.0", 38 | "passport-github": "^1.1.0", 39 | "passport-google-oauth2": "^0.2.0", 40 | "passport-google-oauth20": "^2.0.0", 41 | "passport-jwt": "^4.0.1", 42 | "passport-local": "^1.0.0", 43 | "pg": "^8.9.0", 44 | "pg-format": "^1.0.4", 45 | "qs": "^6.11.0", 46 | "reflect-metadata": "^0.1.13", 47 | "rimraf": "^3.0.2", 48 | "rxjs": "^7.2.0", 49 | "transliteration": "^2.3.5", 50 | "typeorm": "^0.3.11", 51 | "uuid": "^9.0.0" 52 | }, 53 | "devDependencies": { 54 | "@nestjs/cli": "^9.0.0", 55 | "@nestjs/schematics": "^9.0.0", 56 | "@nestjs/testing": "^9.0.0", 57 | "@types/express": "^4.17.13", 58 | "@types/jest": "28.1.8", 59 | "@types/node": "^16.0.0", 60 | "@types/passport-google-oauth2": "^0.1.5", 61 | "@types/passport-jwt": "^3.0.8", 62 | "@types/passport-local": "^1.0.35", 63 | "@types/supertest": "^2.0.11", 64 | "@typescript-eslint/eslint-plugin": "^5.0.0", 65 | "@typescript-eslint/parser": "^5.0.0", 66 | "eslint": "^8.0.1", 67 | "eslint-config-prettier": "^8.3.0", 68 | "eslint-plugin-prettier": "^4.0.0", 69 | "jest": "^28.1.3", 70 | "prettier": "^2.3.2", 71 | "source-map-support": "^0.5.20", 72 | "supertest": "^6.1.3", 73 | "ts-jest": "28.0.8", 74 | "ts-loader": "^9.2.3", 75 | "ts-node": "^10.0.0", 76 | "tsconfig-paths": "4.1.0", 77 | "typescript": "^4.7.4" 78 | }, 79 | "jest": { 80 | "moduleFileExtensions": [ 81 | "js", 82 | "json", 83 | "ts" 84 | ], 85 | "rootDir": "src", 86 | "testRegex": ".*\\.spec\\.ts$", 87 | "transform": { 88 | "^.+\\.(t|j)s$": "ts-jest" 89 | }, 90 | "collectCoverageFrom": [ 91 | "**/*.(t|j)s" 92 | ], 93 | "coverageDirectory": "../coverage", 94 | "testEnvironment": "node" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/comments/comments.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { CommentEntity } from './comment.entity'; 5 | import { CommentDto } from './dto/create-comment.dto'; 6 | import { ParamsCommentDto } from './dto/params-comment.dto'; 7 | 8 | @Injectable() 9 | export class CommentsService { 10 | constructor( 11 | @InjectRepository(CommentEntity) 12 | private commentsRepository: Repository, 13 | ) {} 14 | 15 | async createComment(dto: CommentDto, userId: number) { 16 | const comment = await this.commentsRepository.save({ 17 | ...dto, 18 | user: { 19 | id: userId, 20 | }, 21 | question: { 22 | id: dto.questionId, 23 | }, 24 | answer: { 25 | id: dto.answerId, 26 | }, 27 | }); 28 | return comment; 29 | } 30 | 31 | async getAllComments(dto: ParamsCommentDto) { 32 | const qb = this.commentsRepository 33 | .createQueryBuilder('c') 34 | .orderBy('c.createdAt', 'DESC') 35 | .leftJoinAndSelect('c.user', 'user') 36 | .leftJoinAndSelect('c.question', 'question') 37 | .leftJoinAndSelect('c.answer', 'answer') 38 | .leftJoinAndSelect('c.post', 'post'); 39 | 40 | if (dto.questionId) { 41 | qb.where('question.id = :questionId', { questionId: dto.questionId }); 42 | } 43 | 44 | if (dto.answerId) { 45 | qb.where('answer.id = :answerId', { 46 | answerId: dto.answerId, 47 | }); 48 | } 49 | 50 | const [comments, total] = await qb.getManyAndCount(); 51 | 52 | const items = comments.map((obj) => { 53 | const newObj = { 54 | ...obj, 55 | user: { 56 | id: obj.user.id, 57 | login: obj.user.login, 58 | name: obj.user.name, 59 | }, 60 | question: null, 61 | answer: null, 62 | }; 63 | 64 | if (obj.question) { 65 | newObj.question = { 66 | id: obj.question.id, 67 | }; 68 | } 69 | if (obj.answer) { 70 | newObj.answer = { 71 | id: obj.answer.id, 72 | }; 73 | } 74 | 75 | return newObj; 76 | }); 77 | 78 | return { total, items }; 79 | } 80 | 81 | async getCommentById(id: number) { 82 | const comment = await this.commentsRepository.findOne({ 83 | where: { 84 | id, 85 | }, 86 | }); 87 | return comment; 88 | } 89 | 90 | async updateComment(id: number, dto: CommentDto) { 91 | await this.commentsRepository.update(id, dto); 92 | 93 | const comment = await this.commentsRepository 94 | .createQueryBuilder('c') 95 | .whereInIds(id) 96 | .leftJoinAndSelect('c.user', 'user') 97 | .getOne(); 98 | 99 | return { 100 | ...comment, 101 | user: { 102 | id: comment.user.id, 103 | login: comment.user.login, 104 | }, 105 | }; 106 | } 107 | 108 | async removeComment(id: number) { 109 | const comment = await this.commentsRepository.delete(id); 110 | return comment; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/answers/answers.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { CommentEntity } from 'src/comments/comment.entity'; 4 | import { Repository } from 'typeorm'; 5 | import { AnswerEntity } from './answer.entity'; 6 | import { AnswerDto } from './dto/answer.dto'; 7 | import { ParamsAnswerDto } from './dto/params-answer.dto'; 8 | 9 | @Injectable() 10 | export class AnswersService { 11 | constructor( 12 | @InjectRepository(AnswerEntity) 13 | private answersRepository: Repository, 14 | @InjectRepository(CommentEntity) 15 | private commentsRepository: Repository, 16 | ) {} 17 | 18 | async createAnswer(dto: AnswerDto, userId: number) { 19 | const answer = await this.answersRepository.save({ 20 | ...dto, 21 | user: { id: userId }, 22 | question: { id: dto.questionId }, 23 | }); 24 | return answer; 25 | } 26 | 27 | async getAllAnswers(dto: ParamsAnswerDto) { 28 | const qb = this.answersRepository.createQueryBuilder('a'); 29 | const questionId = dto.questionId; 30 | 31 | if (dto.questionId) { 32 | qb.where('a.question = :questionId', { questionId }); 33 | } 34 | 35 | if (dto.orderBy === 'date1') { 36 | qb.orderBy('a.isAnswer', 'ASC'); 37 | qb.addOrderBy('a.createdAt', 'DESC'); 38 | } else if (dto.orderBy === 'date2') { 39 | qb.orderBy('a.isAnswer', 'ASC'); 40 | qb.addOrderBy('a.rating', 'ASC'); 41 | } else { 42 | qb.orderBy('a.isAnswer', 'DESC'); 43 | qb.addOrderBy('a.rating', 'DESC'); 44 | } 45 | 46 | const [items, total] = await qb 47 | .leftJoinAndSelect('a.user', 'user') 48 | .leftJoinAndSelect('a.question', 'question') 49 | .getManyAndCount(); 50 | 51 | const answers = items.map((obj) => { 52 | return { 53 | ...obj, 54 | user: { 55 | id: obj.user.id, 56 | login: obj.user.login, 57 | name: obj.user.name, 58 | avatar: obj.user.avatar, 59 | }, 60 | question: { 61 | id: obj.question.id, 62 | }, 63 | }; 64 | }); 65 | 66 | return answers; 67 | } 68 | 69 | async getAnswerById(id: number) { 70 | const answer = await this.answersRepository.findOne({ 71 | where: { 72 | id, 73 | }, 74 | }); 75 | return answer; 76 | } 77 | 78 | async setAnswerIsSolved(id: number, dto: AnswerDto) { 79 | const qb = this.answersRepository 80 | .createQueryBuilder('answers') 81 | .leftJoinAndSelect('answers.question', 'question'); 82 | await qb 83 | .update() 84 | .set({ isAnswer: false }) 85 | .where('answers.question = :questionId', { 86 | questionId: dto.questionId, 87 | }) 88 | .execute(); 89 | 90 | delete dto.questionId; 91 | const answer = await this.answersRepository.update(id, dto); 92 | 93 | return answer; 94 | } 95 | 96 | async updateAnswer(id: number, dto: AnswerDto) { 97 | const answer = await this.answersRepository.findOne({ where: { id } }); 98 | answer.updated = new Date(); 99 | await this.answersRepository.save(answer); 100 | await this.answersRepository.update(id, dto); 101 | return answer; 102 | } 103 | 104 | async removeAnswer(id: number) { 105 | await this.commentsRepository.delete({ answer: { id } }); 106 | await this.answersRepository.delete(id); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpException, 3 | HttpStatus, 4 | Injectable, 5 | UnauthorizedException, 6 | } from '@nestjs/common'; 7 | import { InjectRepository } from '@nestjs/typeorm'; 8 | import { FilesService } from 'src/files/files.service'; 9 | import { Repository } from 'typeorm'; 10 | import { UserDto } from './dto/user.dto'; 11 | import { UserEntity } from './user.entity'; 12 | import * as bcrypt from 'bcryptjs'; 13 | import { ParamsUserDto } from './dto/params-user.dto'; 14 | import { UpdateUserDto } from './dto/update-user.dto'; 15 | import { QuestionEntity } from 'src/questions/question.entity'; 16 | 17 | @Injectable() 18 | export class UsersService { 19 | constructor( 20 | @InjectRepository(UserEntity) 21 | private usersRepository: Repository, 22 | private fileService: FilesService, 23 | @InjectRepository(QuestionEntity) 24 | private questionRepository: Repository, 25 | ) {} 26 | 27 | async createUser(dto: UserDto) { 28 | const userName = dto.login.replace(/ /g, '_').toLocaleLowerCase(); 29 | const user = await this.usersRepository.save({ 30 | ...dto, 31 | login: userName, 32 | }); 33 | return user; 34 | } 35 | 36 | async getAll(dto: ParamsUserDto) { 37 | const qb = await this.usersRepository.createQueryBuilder('u'); 38 | 39 | const limit = dto.limit || 2; 40 | const page = dto.page || 2; 41 | 42 | if (dto.limit) { 43 | qb.take(dto.limit); 44 | } 45 | if (dto.page) { 46 | qb.skip((page - 1) * limit); 47 | } 48 | 49 | if (dto.search) { 50 | qb.where('LOWER(u.login) LIKE LOWER(:login)', { 51 | login: `%${dto.search}%`, 52 | }); 53 | } 54 | 55 | const [users, total] = await qb.getManyAndCount(); 56 | 57 | const items = users 58 | .filter((obj) => !obj.isAdmin) 59 | .map(({ password, ...obj }) => obj); 60 | 61 | return { total, items }; 62 | } 63 | 64 | async getUserByLogin(login: string) { 65 | const user = await this.usersRepository.findOne({ where: { login } }); 66 | return user; 67 | } 68 | 69 | async getUserByEmail(email: string) { 70 | const user = await this.usersRepository.findOne({ where: { email } }); 71 | return user; 72 | } 73 | 74 | async getUserById(id: number) { 75 | const user = await this.usersRepository.findOne({ where: { id } }); 76 | const { password, ...userData } = user; 77 | return userData; 78 | } 79 | 80 | async addQuestionToFavorite(userId: number, questionId: number) { 81 | const user = await this.usersRepository.findOne({ where: { id: userId } }); 82 | if (!user) { 83 | throw new HttpException('Пользователь не найден', HttpStatus.BAD_REQUEST); 84 | } 85 | 86 | const question = await this.questionRepository.findOne({ 87 | where: { id: questionId }, 88 | }); 89 | if (!question) { 90 | throw new HttpException('Вопрос не найден', HttpStatus.BAD_REQUEST); 91 | } 92 | 93 | if (user.favorites.includes(question.id)) { 94 | user.favorites = user.favorites.filter((id) => id !== question.id); 95 | } else { 96 | user.favorites.push(question.id); 97 | } 98 | await this.usersRepository.save(user); 99 | 100 | const { password, ...userData } = user; 101 | return userData; 102 | } 103 | 104 | async updateUser(id: number, dto: UpdateUserDto) { 105 | const hashPassword = dto.password 106 | ? await bcrypt.hash(dto.password, 5) 107 | : undefined; 108 | 109 | const userName = dto.login 110 | ? dto.login.replace(/ /g, '_').toLocaleLowerCase() 111 | : undefined; 112 | 113 | const user = await this.usersRepository.update(id, { 114 | ...dto, 115 | ...(userName && { login: userName }), 116 | ...(hashPassword && { password: hashPassword }), 117 | }); 118 | return user; 119 | } 120 | 121 | async updateUserAvatar(id: number, avatar: any) { 122 | const fileName = await this.fileService.uploadImage(avatar, { 123 | imagePath: 'avatars', 124 | }); 125 | 126 | await this.usersRepository.update(id, { 127 | avatar: fileName, 128 | }); 129 | return fileName; 130 | } 131 | 132 | async updateUserPassword( 133 | id: number, 134 | dto: { oldPassword: string; newPassword: string }, 135 | ) { 136 | const user = await this.usersRepository.findOne({ where: { id } }); 137 | const isTruePassword = await bcrypt.compare(dto.oldPassword, user.password); 138 | 139 | if (!isTruePassword) { 140 | throw new UnauthorizedException({ 141 | message: 'Неверный пароль', 142 | }); 143 | } 144 | 145 | const hashPassword = dto.newPassword 146 | ? await bcrypt.hash(dto.newPassword, 5) 147 | : undefined; 148 | 149 | await this.usersRepository.update(id, { 150 | password: hashPassword, 151 | }); 152 | return 'Пароль обновлен'; 153 | } 154 | 155 | async removeUser(id: number) { 156 | await this.usersRepository.delete(id); 157 | return 'Пользователь удален'; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/questions/questions.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { In, Repository } from 'typeorm'; 4 | import { QuestionDto } from './dto/question.dto'; 5 | import { ParamsQuestionDto } from './dto/params-question.dto'; 6 | import { QuestionEntity } from './question.entity'; 7 | import { AnswerEntity } from 'src/answers/answer.entity'; 8 | import { CommentEntity } from 'src/comments/comment.entity'; 9 | import { UserEntity } from 'src/users/user.entity'; 10 | import { TagEntity } from 'src/tags/tag.entity'; 11 | 12 | @Injectable() 13 | export class QuestionsService { 14 | constructor( 15 | @InjectRepository(QuestionEntity) 16 | private questionsRepository: Repository, 17 | @InjectRepository(AnswerEntity) 18 | private answersRepository: Repository, 19 | @InjectRepository(CommentEntity) 20 | private commentsRepository: Repository, 21 | @InjectRepository(UserEntity) 22 | private usersRepository: Repository, 23 | @InjectRepository(TagEntity) 24 | private tagsRepository: Repository, 25 | ) {} 26 | 27 | async createQuestion(dto: QuestionDto, userId: number) { 28 | const tags = dto.tags.map((tag) => { 29 | tag.questionCount++; 30 | return tag; 31 | }); 32 | await this.tagsRepository.save(tags); 33 | 34 | const question = await this.questionsRepository.save({ 35 | ...dto, 36 | user: { id: userId }, 37 | }); 38 | return question; 39 | } 40 | 41 | async getAll(dto: ParamsQuestionDto) { 42 | const qb = await this.questionsRepository.createQueryBuilder('questions'); 43 | 44 | qb.leftJoinAndSelect('questions.user', 'user'); 45 | 46 | const limit = dto.limit || 2; 47 | const page = dto.page || 2; 48 | 49 | if (dto.limit) { 50 | qb.take(dto.limit); 51 | } 52 | if (dto.page) { 53 | qb.skip((page - 1) * limit); 54 | } 55 | 56 | if (dto.orderBy === 'popular') { 57 | qb.orderBy('questions.views', 'DESC'); 58 | } else { 59 | qb.orderBy('questions.createdAt', 'DESC'); 60 | } 61 | 62 | if (dto.tagBy) { 63 | const tag = dto.tagBy; 64 | qb.innerJoin('questions.tags', 'tag').where('tag.name = :tag', { tag }); 65 | } 66 | 67 | if (dto.userId && !dto.favorites) { 68 | qb.where('user.id = :user', { 69 | user: dto.userId, 70 | }); 71 | } 72 | 73 | if (dto.favorites && dto.userId) { 74 | const user = await this.usersRepository.findOne({ 75 | where: { id: dto.userId }, 76 | }); 77 | if (!!user.favorites.length) { 78 | qb.where('questions.id IN (:...ids)', { ids: user.favorites }); 79 | } else { 80 | qb.where('1=0'); 81 | } 82 | } 83 | 84 | if (dto.search) { 85 | qb.andWhere('LOWER(questions.title) LIKE LOWER(:title)', { 86 | title: `%${dto.search}%`, 87 | }); 88 | } 89 | 90 | if (dto.isAnswer === 'true') { 91 | qb.andWhere('questions.isAnswer IS TRUE'); 92 | } else if (dto.isAnswer === 'false') { 93 | qb.andWhere('questions.isAnswer IS FALSE'); 94 | } 95 | 96 | const [questions, total] = await qb 97 | .leftJoinAndSelect('questions.tags', 'tags') 98 | .getManyAndCount(); 99 | 100 | const items = questions.map((obj) => { 101 | delete obj.body; 102 | return { 103 | ...obj, 104 | tags: obj.tags.map((item) => ({ 105 | id: item.id, 106 | name: item.name, 107 | })), 108 | user: { 109 | id: obj.user.id, 110 | }, 111 | }; 112 | }); 113 | return { total, items }; 114 | } 115 | 116 | async getQuestionById(id: number) { 117 | await this.questionsRepository 118 | .createQueryBuilder('questions') 119 | .whereInIds(id) 120 | .update() 121 | .set({ 122 | views: () => 'views + 1', 123 | }) 124 | .execute(); 125 | 126 | const question = await this.questionsRepository 127 | .createQueryBuilder('q') 128 | .whereInIds(id) 129 | .leftJoinAndSelect('q.user', 'user') 130 | .leftJoinAndSelect('q.tags', 'tags') 131 | .getOne(); 132 | 133 | return { 134 | ...question, 135 | user: { 136 | id: question.user.id, 137 | login: question.user.login, 138 | name: question.user.name, 139 | avatar: question.user.avatar, 140 | }, 141 | }; 142 | } 143 | 144 | async updateQuestion(id: number, dto: QuestionDto) { 145 | const question = await this.questionsRepository 146 | .createQueryBuilder('q') 147 | .whereInIds(id) 148 | .leftJoinAndSelect('q.tags', 'tags') 149 | .getOne(); 150 | 151 | if (dto.tags) { 152 | const newTags = dto.tags.filter( 153 | (tag) => !question.tags.find((t) => t.id === tag.id), 154 | ); 155 | const oldTags = question.tags.filter( 156 | (tag) => !dto.tags.find((t) => t.id === tag.id), 157 | ); 158 | 159 | if (newTags.length > 0) { 160 | newTags.forEach((tag) => { 161 | tag.questionCount++; 162 | }); 163 | } 164 | if (oldTags.length > 0) { 165 | oldTags.forEach((tag) => { 166 | tag.questionCount--; 167 | }); 168 | } 169 | 170 | question.tags = dto.tags; 171 | delete dto.tags; 172 | 173 | if (oldTags.length > 0) { 174 | await this.tagsRepository.save(oldTags); 175 | } 176 | if (newTags.length > 0) { 177 | await this.tagsRepository.save(newTags); 178 | } 179 | } 180 | 181 | question.updated = new Date(); 182 | await this.questionsRepository.save(question); 183 | await this.questionsRepository.update(id, dto); 184 | return question; 185 | } 186 | 187 | async removeQuestion(id: number) { 188 | const answers = await this.answersRepository.find({ 189 | where: { question: { id } }, 190 | }); 191 | const question = await this.questionsRepository 192 | .createQueryBuilder('q') 193 | .whereInIds(id) 194 | .leftJoinAndSelect('q.tags', 'tags') 195 | .getOne(); 196 | 197 | const tags = question.tags.map((tag) => { 198 | tag.questionCount--; 199 | return tag; 200 | }); 201 | await this.tagsRepository.save(tags); 202 | 203 | for (const answer of answers) { 204 | await this.commentsRepository.delete({ answer: { id: answer.id } }); 205 | } 206 | await this.answersRepository.delete({ question: { id } }); 207 | await this.questionsRepository.delete(id); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/posts/posts.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { CategoryEntity } from 'src/categories/category.entity'; 4 | import { CommentEntity } from 'src/comments/comment.entity'; 5 | import { FilesService } from 'src/files/files.service'; 6 | import { TagEntity } from 'src/tags/tag.entity'; 7 | import { UserEntity } from 'src/users/user.entity'; 8 | import { Repository } from 'typeorm'; 9 | import { ParamsPostDto } from './dto/params-post.dto'; 10 | import { PostDto } from './dto/post.dto'; 11 | import { PostEntity } from './post.entity'; 12 | import * as translit from 'transliteration'; 13 | 14 | @Injectable() 15 | export class PostsService { 16 | constructor( 17 | @InjectRepository(PostEntity) 18 | private postsRepository: Repository, 19 | @InjectRepository(CategoryEntity) 20 | private categoriesRepository: Repository, 21 | @InjectRepository(TagEntity) 22 | private tagsRepository: Repository, 23 | @InjectRepository(UserEntity) 24 | private usersRepository: Repository, 25 | @InjectRepository(CommentEntity) 26 | private commentsRepository: Repository, 27 | ) {} 28 | 29 | // createPost ---------------------------------------------- 30 | async createPost(dto: PostDto, userId: number) { 31 | const slug = translit.slugify(dto.title); 32 | const description = dto.body.find((obj) => obj.type === 'paragraph')?.data 33 | ?.text; 34 | 35 | const category = await this.categoriesRepository.findOne({ 36 | where: { id: dto.categoryId }, 37 | }); 38 | category.postsCount++; 39 | await this.categoriesRepository.save(category); 40 | 41 | const tags = dto.tags.map((tag) => { 42 | tag.postsCount++; 43 | return tag; 44 | }); 45 | await this.tagsRepository.save(tags); 46 | 47 | const post = await this.postsRepository.save({ 48 | ...dto, 49 | slug, 50 | description: description || '', 51 | user: { 52 | id: userId, 53 | }, 54 | category: { 55 | id: dto.categoryId, 56 | }, 57 | }); 58 | return post; 59 | } 60 | 61 | // getAllPosts ---------------------------------------------- 62 | async getAllPosts(dto: ParamsPostDto) { 63 | const qb = await this.postsRepository 64 | .createQueryBuilder('posts') 65 | .leftJoinAndSelect('posts.user', 'user') 66 | .leftJoinAndSelect('posts.category', 'category') 67 | .leftJoinAndSelect('posts.tags', 'tags'); 68 | 69 | const limit = dto.limit || 2; 70 | const page = dto.page || 2; 71 | 72 | if (dto.limit) { 73 | qb.take(dto.limit); 74 | } 75 | if (dto.page) { 76 | qb.skip((page - 1) * limit); 77 | } 78 | 79 | if (dto.orderBy === 'popular') { 80 | qb.orderBy('posts.views', 'DESC'); 81 | } else { 82 | qb.orderBy('posts.createdAt', 'DESC'); 83 | } 84 | 85 | if (dto.tagBy) { 86 | const tag = dto.tagBy; 87 | qb.where('tag.name = :tag', { tag }); 88 | } 89 | 90 | if (dto.userId && !dto.favorites) { 91 | qb.where('user.id = :user', { 92 | user: dto.userId, 93 | }); 94 | } 95 | 96 | if (dto.favorites && dto.userId) { 97 | const user = await this.usersRepository.findOne({ 98 | where: { id: dto.userId }, 99 | }); 100 | if (!!user.favorites.length) { 101 | qb.where('posts.id IN (:...ids)', { ids: user.favorites }); 102 | } else { 103 | qb.where('1=0'); 104 | } 105 | } 106 | 107 | if (dto.searchBy) { 108 | qb.andWhere('LOWER(posts.title) LIKE LOWER(:title)', { 109 | title: `%${dto.searchBy}%`, 110 | }); 111 | } 112 | 113 | const [posts, total] = await qb.getManyAndCount(); 114 | 115 | if (dto.isShort) { 116 | const items = posts.map((obj) => { 117 | delete obj.body; 118 | delete obj.image; 119 | delete obj.views; 120 | delete obj.updated; 121 | delete obj.tags; 122 | delete obj.user; 123 | delete obj.commentsCount; 124 | return { 125 | ...obj, 126 | category: { 127 | id: obj.category.id, 128 | label: obj.category.label, 129 | value: obj.category.value, 130 | }, 131 | }; 132 | }); 133 | return { total, items }; 134 | } 135 | 136 | const items = posts.map((obj) => { 137 | delete obj.body; 138 | return { 139 | ...obj, 140 | tags: obj.tags.map((item) => ({ 141 | id: item.id, 142 | name: item.name, 143 | })), 144 | }; 145 | }); 146 | return { total, items }; 147 | } 148 | 149 | // getPostById ---------------------------------------------- 150 | async getPostById(id: number) { 151 | await this.postsRepository 152 | .createQueryBuilder('posts') 153 | .whereInIds(id) 154 | .update() 155 | .set({ 156 | views: () => 'views + 1', 157 | }) 158 | .execute(); 159 | 160 | const post = await this.postsRepository 161 | .createQueryBuilder('posts') 162 | .whereInIds(id) 163 | .leftJoinAndSelect('postsq.user', 'user') 164 | .leftJoinAndSelect('posts.category', 'category') 165 | .leftJoinAndSelect('posts.tags', 'tags') 166 | .getOne(); 167 | 168 | return { 169 | ...post, 170 | user: { 171 | id: post.user.id, 172 | login: post.user.login, 173 | name: post.user.name, 174 | avatar: post.user.avatar, 175 | }, 176 | }; 177 | } 178 | 179 | // updatePost ---------------------------------------------- 180 | async updatePost(id: number, dto: PostDto) { 181 | const post = await this.postsRepository 182 | .createQueryBuilder('posts') 183 | .whereInIds(id) 184 | .leftJoinAndSelect('posts.category', 'category') 185 | .leftJoinAndSelect('posts.tags', 'tags') 186 | .getOne(); 187 | 188 | if (dto.tags) { 189 | const newTags = dto.tags.filter( 190 | (tag) => !post.tags.find((t) => t.id === tag.id), 191 | ); 192 | const oldTags = post.tags.filter( 193 | (tag) => !dto.tags.find((t) => t.id === tag.id), 194 | ); 195 | 196 | if (newTags.length > 0) { 197 | newTags.forEach((tag) => { 198 | tag.postsCount++; 199 | }); 200 | } 201 | if (oldTags.length > 0) { 202 | oldTags.forEach((tag) => { 203 | tag.postsCount--; 204 | }); 205 | } 206 | 207 | post.tags = dto.tags; 208 | delete dto.tags; 209 | 210 | if (oldTags.length > 0) { 211 | await this.tagsRepository.save(oldTags); 212 | } 213 | if (newTags.length > 0) { 214 | await this.tagsRepository.save(newTags); 215 | } 216 | } 217 | 218 | if (dto.categoryId) { 219 | if (post.category.id !== dto.categoryId) { 220 | const oldCategory = post.category; 221 | const newCategory = await this.categoriesRepository.findOne({ 222 | where: { id: dto.categoryId }, 223 | }); 224 | 225 | if (oldCategory) { 226 | oldCategory.postsCount--; 227 | await this.categoriesRepository.save(oldCategory); 228 | } 229 | if (newCategory) { 230 | newCategory.postsCount++; 231 | await this.categoriesRepository.save(newCategory); 232 | } 233 | } 234 | } 235 | 236 | post.updated = new Date(); 237 | await this.postsRepository.save(post); 238 | await this.postsRepository.update(id, dto); 239 | return post; 240 | } 241 | 242 | // removePost ---------------------------------------------- 243 | async removePost(id: number) { 244 | const post = await this.postsRepository 245 | .createQueryBuilder('posts') 246 | .leftJoinAndSelect('posts.category', 'category') 247 | .leftJoinAndSelect('posts.tags', 'tags') 248 | .getOne(); 249 | 250 | const category = post.category; 251 | category.postsCount--; 252 | await this.categoriesRepository.save(category); 253 | 254 | const tags = post.tags.map((tag) => { 255 | tag.postsCount--; 256 | return tag; 257 | }); 258 | await this.tagsRepository.save(tags); 259 | 260 | await this.commentsRepository.delete({ post: { id } }); 261 | 262 | await this.postsRepository.delete(id); 263 | } 264 | } 265 | --------------------------------------------------------------------------------