From 02e9f874dabe712949a3ad29af29c3b295f19d48 Mon Sep 17 00:00:00 2001 From: Pravdin Egor Date: Wed, 6 Sep 2023 12:22:05 +0700 Subject: [PATCH] move api to separate repository --- .env.example | 13 +++ .eslintrc.js | 25 +++++ .gitignore | 37 +++++++ .prettierrc | 4 + README.md | 73 ++++++++++++++ docker-compose.yml | 17 ++++ migrations/1693809572293-init-schema.ts | 47 +++++++++ nest-cli.json | 8 ++ package.json | 96 +++++++++++++++++++ src/app.controller.ts | 12 +++ src/app.module.ts | 35 +++++++ src/app.service.ts | 8 ++ src/auth/auth.controller.ts | 47 +++++++++ src/auth/auth.module.ts | 30 ++++++ src/auth/auth.service.ts | 71 ++++++++++++++ src/auth/dto/register.dto.ts | 3 + src/auth/guards/jwt-auth.guard.ts | 5 + src/auth/guards/local-auth.guard.ts | 5 + .../interfaces/request-with-user.interface.ts | 6 ++ .../interfaces/token-payload.interface.ts | 4 + src/auth/strategies/jwt.strategy.ts | 30 ++++++ src/auth/strategies/local.strategy.ts | 20 ++++ src/database/postgresErrorCodes.enum.ts | 3 + src/links/dto/create-link.dto.ts | 3 + src/links/entities/links.entity.ts | 27 ++++++ src/links/links.cotroller.ts | 32 +++++++ src/links/links.module.ts | 12 +++ src/links/links.service.ts | 21 ++++ src/main.ts | 22 +++++ src/prices/entities/prices.entity.ts | 23 +++++ src/prices/prices.module.ts | 10 ++ src/prices/prices.service.ts | 4 + src/tasks/tasks.module.ts | 9 ++ src/tasks/tasks.service.ts | 44 +++++++++ src/users/dto/create-user.dto.ts | 15 +++ src/users/dto/update-user.dto.ts | 4 + src/users/entities/user.entity.ts | 32 +++++++ src/users/users.module.ts | 11 +++ src/users/users.service.ts | 39 ++++++++ src/utils/errors.ts | 31 ++++++ tsconfig.build.json | 4 + tsconfig.json | 21 ++++ typeorm.config.ts | 18 ++++ 43 files changed, 981 insertions(+) create mode 100644 .env.example create mode 100644 .eslintrc.js create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 migrations/1693809572293-init-schema.ts create mode 100644 nest-cli.json create mode 100644 package.json create mode 100644 src/app.controller.ts create mode 100644 src/app.module.ts create mode 100644 src/app.service.ts create mode 100644 src/auth/auth.controller.ts create mode 100644 src/auth/auth.module.ts create mode 100644 src/auth/auth.service.ts create mode 100644 src/auth/dto/register.dto.ts create mode 100644 src/auth/guards/jwt-auth.guard.ts create mode 100644 src/auth/guards/local-auth.guard.ts create mode 100644 src/auth/interfaces/request-with-user.interface.ts create mode 100644 src/auth/interfaces/token-payload.interface.ts create mode 100644 src/auth/strategies/jwt.strategy.ts create mode 100644 src/auth/strategies/local.strategy.ts create mode 100644 src/database/postgresErrorCodes.enum.ts create mode 100644 src/links/dto/create-link.dto.ts create mode 100644 src/links/entities/links.entity.ts create mode 100644 src/links/links.cotroller.ts create mode 100644 src/links/links.module.ts create mode 100644 src/links/links.service.ts create mode 100644 src/main.ts create mode 100644 src/prices/entities/prices.entity.ts create mode 100644 src/prices/prices.module.ts create mode 100644 src/prices/prices.service.ts create mode 100644 src/tasks/tasks.module.ts create mode 100644 src/tasks/tasks.service.ts create mode 100644 src/users/dto/create-user.dto.ts create mode 100644 src/users/dto/update-user.dto.ts create mode 100644 src/users/entities/user.entity.ts create mode 100644 src/users/users.module.ts create mode 100644 src/users/users.service.ts create mode 100644 src/utils/errors.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json create mode 100644 typeorm.config.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f2acd63 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# app port +PORT=3001 + +# db config +DB_HOST=localhost +DB_PORT=25432 +DB_USER=mam-kupi-admin +DB_PASSWORD=7TTLpNh4GtQcDAMY +DB_NAME=mam-kupi-db + +#JWT +JWT_SECRET=3XUR3uRX6KHH5LI7nsWUh7RyhpJ8ST9t +JWT_EXPIRATION_TIME=3600 \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..259de13 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,25 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: __dirname, + sourceType: 'module', + }, + plugins: ['@typescript-eslint/eslint-plugin'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + root: true, + env: { + node: true, + jest: true, + }, + ignorePatterns: ['.eslintrc.js'], + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + }, +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aabbe8f --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +.env + +# compiled output +/dist +/node_modules + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..dcb7279 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8372941 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +

+ Nest Logo +

+ +[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 +[circleci-url]: https://circleci.com/gh/nestjs/nest + +

A progressive Node.js framework for building efficient and scalable server-side applications.

+

+NPM Version +Package License +NPM Downloads +CircleCI +Coverage +Discord +Backers on Open Collective +Sponsors on Open Collective + + Support us + +

+ + +## Description + +[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. + +## Installation + +```bash +$ yarn install +``` + +## Running the app + +```bash +# development +$ yarn run start + +# watch mode +$ yarn run start:dev + +# production mode +$ yarn run start:prod +``` + +## Test + +```bash +# unit tests +$ yarn run test + +# e2e tests +$ yarn run test:e2e + +# test coverage +$ yarn run test:cov +``` + +## Support + +Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). + +## Stay in touch + +- Author - [Kamil Myƛliwiec](https://kamilmysliwiec.com) +- Website - [https://nestjs.com](https://nestjs.com/) +- Twitter - [@nestframework](https://twitter.com/nestframework) + +## License + +Nest is [MIT licensed](LICENSE). diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ca845b3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: "3.9" +services: + postgres: + image: postgres:15.3 + container_name: mam-kupi-postgres + environment: + POSTGRES_DB: "mam-kupi-db" + POSTGRES_USER: "mam-kupi-admin" + POSTGRES_PASSWORD: "7TTLpNh4GtQcDAMY" + volumes: + - mam-kupi-postgres-data:/var/lib/postgresql + ports: + - "25432:5432" + +volumes: + mam-kupi-postgres-data: + driver: local diff --git a/migrations/1693809572293-init-schema.ts b/migrations/1693809572293-init-schema.ts new file mode 100644 index 0000000..5c3b6b7 --- /dev/null +++ b/migrations/1693809572293-init-schema.ts @@ -0,0 +1,47 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class InitSchema1693809572293 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TYPE public."Users_roles_enum" AS ENUM ('admin','standard'); + + CREATE TABLE public."Users" ( + id serial4 primary key, + "name" varchar NOT NULL, + "password" varchar NOT NULL, + email varchar NOT NULL UNIQUE, + "role" public."Users_roles_enum" NOT NULL DEFAULT 'standard'::"Users_roles_enum" + ); + + CREATE TABLE public."Links" ( + id serial4 PRIMARY KEY, + url varchar NOT NULL, + "lastCheckDate" timestamp NULL, + "userId" int4 NOT NULL + ); + + ALTER TABLE public."Links" ADD CONSTRAINT "links_user" FOREIGN KEY ("userId") REFERENCES public."Users"(id); + + CREATE TABLE public."Prices" ( + id serial4 PRIMARY KEY, + value int4 NOT NULL, + "createdDate" timestamp NOT NULL DEFAULT now(), + "linkId" int4 NOT NULL + ); + + ALTER TABLE public."Prices" ADD CONSTRAINT "prices_link" FOREIGN KEY ("linkId") REFERENCES public."Links"(id); + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DROP TYPE public."Users_roles_enum"; + + DROP TABLE public."Users"; + + DROP TABLE public."Links"; + + DROP TABLE public."Prices"; + `); + } +} diff --git a/nest-cli.json b/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f7ba873 --- /dev/null +++ b/package.json @@ -0,0 +1,96 @@ +{ + "name": "api", + "version": "0.0.1", + "description": "", + "author": "Pravdin Egor Vadimovich", + "private": true, + "license": "MIT", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json", + "typeorm": "ts-node ./node_modules/typeorm/cli", + "migrate:run": "npm run typeorm migration:run -- -d ./typeorm.config.ts" + }, + "dependencies": { + "@nestjs/axios": "^2.0.0", + "@nestjs/common": "^9.0.0", + "@nestjs/config": "^2.3.3", + "@nestjs/core": "^9.0.0", + "@nestjs/jwt": "^10.1.1", + "@nestjs/mapped-types": "*", + "@nestjs/microservices": "^9.4.3", + "@nestjs/passport": "^10.0.1", + "@nestjs/platform-express": "^9.0.0", + "@nestjs/schedule": "^2.2.3", + "@nestjs/typeorm": "^10.0.0", + "axios": "^1.4.0", + "bcrypt": "^5.1.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "cookie-parser": "^1.4.6", + "lodash": "^4.17.21", + "passport": "^0.6.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", + "pg": "^8.11.3", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.8.1", + "typeorm": "^0.3.17" + }, + "devDependencies": { + "@nestjs/cli": "^9.0.0", + "@nestjs/schematics": "^9.0.0", + "@nestjs/testing": "^9.0.0", + "@types/bcrypt": "^5.0.0", + "@types/cookie-parser": "^1.4.4", + "@types/cron": "^2.0.1", + "@types/express": "^4.17.17", + "@types/jest": "29.5.1", + "@types/lodash": "^4.14.197", + "@types/node": "18.16.12", + "@types/passport-jwt": "^3.0.9", + "@types/passport-local": "^1.0.35", + "@types/supertest": "^2.0.11", + "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^8.0.1", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-prettier": "^4.0.0", + "jest": "29.5.0", + "prettier": "^2.3.2", + "source-map-support": "^0.5.20", + "supertest": "^6.1.3", + "ts-jest": "29.1.0", + "ts-loader": "^9.2.3", + "ts-node": "^10.0.0", + "tsconfig-paths": "4.2.0", + "typescript": "^5.0.0" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/src/app.controller.ts b/src/app.controller.ts new file mode 100644 index 0000000..cce879e --- /dev/null +++ b/src/app.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from '@nestjs/common'; +import { AppService } from './app.service'; + +@Controller() +export class AppController { + constructor(private readonly appService: AppService) {} + + @Get() + getHello(): string { + return this.appService.getHello(); + } +} diff --git a/src/app.module.ts b/src/app.module.ts new file mode 100644 index 0000000..4600f68 --- /dev/null +++ b/src/app.module.ts @@ -0,0 +1,35 @@ +import { Module } from '@nestjs/common'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; +import { TasksModule } from './tasks/tasks.module'; +import { ScheduleModule } from '@nestjs/schedule'; +import { ConfigModule } from '@nestjs/config'; +import { LinksModule } from './links/links.module'; +import { PricesModule } from './prices/prices.module'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UsersModule } from './users/users.module'; +import { AuthModule } from './auth/auth.module'; + +@Module({ + imports: [ + ScheduleModule.forRoot(), + ConfigModule.forRoot(), + TypeOrmModule.forRoot({ + type: 'postgres', + host: process.env.DB_HOST, + port: +process.env.DB_PORT, + username: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + entities: ['dist/**/*.entity.js'], + }), + TasksModule, + LinksModule, + PricesModule, + UsersModule, + AuthModule, + ], + controllers: [AppController], + providers: [AppService], +}) +export class AppModule {} diff --git a/src/app.service.ts b/src/app.service.ts new file mode 100644 index 0000000..927d7cc --- /dev/null +++ b/src/app.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AppService { + getHello(): string { + return 'Hello World!'; + } +} diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts new file mode 100644 index 0000000..1091e40 --- /dev/null +++ b/src/auth/auth.controller.ts @@ -0,0 +1,47 @@ +import { + Body, + Controller, + HttpCode, + Post, + Req, + Res, + UseGuards, +} from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { RegisterDto } from './dto/register.dto'; +import { Response } from 'express'; +import { omit } from 'lodash'; +import { LocalAuthGuard } from './guards/local-auth.guard'; + +import JwtAuthGuard from './guards/jwt-auth.guard'; +import { RequestWithUser } from './interfaces/request-with-user.interface'; + +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @Post('register') + async register(@Body() registerData: RegisterDto) { + return this.authService.register(registerData); + } + + @HttpCode(200) + @UseGuards(LocalAuthGuard) + @Post('login') + async login(@Req() request: RequestWithUser, @Res() response: Response) { + const user = request.user; + const cookie = await this.authService.getCookieWithJwtToken( + user.id, + user.name, + ); + response.setHeader('Set-Cookie', cookie); + response.json({ user: omit(user, 'password') }); + } + + @UseGuards(JwtAuthGuard) + @Post('log-out') + async logOut(@Req() request: RequestWithUser, @Res() response: Response) { + response.setHeader('Set-Cookie', this.authService.getCookieForLogOut()); + return response.sendStatus(200); + } +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 0000000..cf97558 --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,30 @@ +import { Module } from '@nestjs/common'; +import { PassportModule } from '@nestjs/passport'; +import { UsersModule } from 'src/users/users.module'; +import { AuthService } from './auth.service'; +import { LocalStrategy } from './strategies/local.strategy'; +import { AuthController } from './auth.controller'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; +import { JwtStrategy } from './strategies/jwt.strategy'; + +@Module({ + imports: [ + PassportModule, + UsersModule, + ConfigModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async (configService: ConfigService) => ({ + secret: configService.get('JWT_SECRET'), + signOptions: { + expiresIn: `${configService.get('JWT_EXPIRATION_TIME')}s`, + }, + }), + }), + ], + providers: [AuthService, LocalStrategy, JwtStrategy], + controllers: [AuthController], +}) +export class AuthModule {} diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts new file mode 100644 index 0000000..8a7f6b8 --- /dev/null +++ b/src/auth/auth.service.ts @@ -0,0 +1,71 @@ +import { omit } from 'lodash'; +import * as bcrypt from 'bcrypt'; +import { Injectable } from '@nestjs/common'; +import { PostgresErrorCode } from 'src/database/postgresErrorCodes.enum'; +import { UsersService } from 'src/users/users.service'; +import { RegisterDto } from './dto/register.dto'; +import { + InternalErrorException, + UserEmailExistsException, + WrongCredentialsException, +} from 'src/utils/errors'; +import { JwtService } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; +import { TokenPayload } from './interfaces/token-payload.interface'; + +@Injectable() +export class AuthService { + constructor( + private readonly usersService: UsersService, + private readonly jwtService: JwtService, + private readonly configService: ConfigService, + ) {} + + public async register(registrationData: RegisterDto) { + const hashedPassword = await bcrypt.hash(registrationData.password, 10); + try { + const createdUser = await this.usersService.create({ + ...registrationData, + password: hashedPassword, + }); + return omit(createdUser, 'password'); + } catch (error) { + if (error?.code === PostgresErrorCode.UniqueViolation) { + throw new UserEmailExistsException(); + } + throw new InternalErrorException(); + } + } + + public async getAuthenticatedUser(email: string, password: string) { + try { + const user = await this.usersService.getByEmail(email); + await this.verifyPassword(password, user.password); + return omit(user, 'password'); + } catch (error) { + throw new WrongCredentialsException(); + } + } + + private async verifyPassword(plainPassword: string, hashedPassword: string) { + const isPasswordMatching = await bcrypt.compare( + plainPassword, + hashedPassword, + ); + if (!isPasswordMatching) { + throw new WrongCredentialsException(); + } + } + + public async getCookieWithJwtToken(userId: number, username: string) { + const payload: TokenPayload = { id: userId, username }; + const token = await this.jwtService.signAsync(payload); + return `Authentication=${token}; HttpOnly; Path=/; Max-Age=${this.configService.get( + 'JWT_EXPIRATION_TIME', + )}`; + } + + public getCookieForLogOut() { + return `Authentication=; HttpOnly; Path=/; Max-Age=0`; + } +} diff --git a/src/auth/dto/register.dto.ts b/src/auth/dto/register.dto.ts new file mode 100644 index 0000000..a5417ae --- /dev/null +++ b/src/auth/dto/register.dto.ts @@ -0,0 +1,3 @@ +import { CreateUserDto } from 'src/users/dto/create-user.dto'; + +export class RegisterDto extends CreateUserDto {} diff --git a/src/auth/guards/jwt-auth.guard.ts b/src/auth/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..fea96d4 --- /dev/null +++ b/src/auth/guards/jwt-auth.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export default class JwtAuthGuard extends AuthGuard('jwt') {} diff --git a/src/auth/guards/local-auth.guard.ts b/src/auth/guards/local-auth.guard.ts new file mode 100644 index 0000000..ccf962b --- /dev/null +++ b/src/auth/guards/local-auth.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class LocalAuthGuard extends AuthGuard('local') {} diff --git a/src/auth/interfaces/request-with-user.interface.ts b/src/auth/interfaces/request-with-user.interface.ts new file mode 100644 index 0000000..4d8b290 --- /dev/null +++ b/src/auth/interfaces/request-with-user.interface.ts @@ -0,0 +1,6 @@ +import { Request } from 'express'; +import { User } from 'src/users/entities/user.entity'; + +export interface RequestWithUser extends Request { + user: User; +} diff --git a/src/auth/interfaces/token-payload.interface.ts b/src/auth/interfaces/token-payload.interface.ts new file mode 100644 index 0000000..cfc0b0f --- /dev/null +++ b/src/auth/interfaces/token-payload.interface.ts @@ -0,0 +1,4 @@ +export interface TokenPayload { + id: number; + username: string; +} diff --git a/src/auth/strategies/jwt.strategy.ts b/src/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..f3e3cac --- /dev/null +++ b/src/auth/strategies/jwt.strategy.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { UsersService } from 'src/users/users.service'; +import { ConfigService } from '@nestjs/config'; +import { Request } from 'express'; +import { TokenPayload } from '../interfaces/token-payload.interface'; +import { omit } from 'lodash'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor( + private readonly usersService: UsersService, + private readonly configService: ConfigService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromExtractors([ + (request: Request) => { + return request?.cookies?.Authentication; + }, + ]), + secretOrKey: configService.get('JWT_SECRET'), + }); + } + + async validate(payload: TokenPayload) { + const user = await this.usersService.getById(payload.id); + return omit(user, 'password'); + } +} diff --git a/src/auth/strategies/local.strategy.ts b/src/auth/strategies/local.strategy.ts new file mode 100644 index 0000000..bd9a696 --- /dev/null +++ b/src/auth/strategies/local.strategy.ts @@ -0,0 +1,20 @@ +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy } from 'passport-local'; +import { AuthService } from '../auth.service'; +import { User } from 'src/users/entities/user.entity'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class LocalStrategy extends PassportStrategy(Strategy) { + constructor(private authenticationService: AuthService) { + super({ + usernameField: 'email', + }); + } + async validate( + email: string, + password: string, + ): Promise> { + return this.authenticationService.getAuthenticatedUser(email, password); + } +} diff --git a/src/database/postgresErrorCodes.enum.ts b/src/database/postgresErrorCodes.enum.ts new file mode 100644 index 0000000..a6a3170 --- /dev/null +++ b/src/database/postgresErrorCodes.enum.ts @@ -0,0 +1,3 @@ +export enum PostgresErrorCode { + UniqueViolation = '23505', +} diff --git a/src/links/dto/create-link.dto.ts b/src/links/dto/create-link.dto.ts new file mode 100644 index 0000000..e3238bc --- /dev/null +++ b/src/links/dto/create-link.dto.ts @@ -0,0 +1,3 @@ +export class CreateLinkDTO { + url: string; +} diff --git a/src/links/entities/links.entity.ts b/src/links/entities/links.entity.ts new file mode 100644 index 0000000..034f034 --- /dev/null +++ b/src/links/entities/links.entity.ts @@ -0,0 +1,27 @@ +import { + Column, + Entity, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Price } from 'src/prices/entities/prices.entity'; +import { User } from 'src/users/entities/user.entity'; + +@Entity('Links') +export class Link { + @PrimaryGeneratedColumn() + id!: number; + + @Column() + url: string; + + @Column({ nullable: true }) + lastCheckDate: Date = new Date(); + + @OneToMany(() => Price, (price) => price.link) + prices: Price[]; + + @ManyToOne(() => User, (user) => user.links) + user: User; +} diff --git a/src/links/links.cotroller.ts b/src/links/links.cotroller.ts new file mode 100644 index 0000000..72626f9 --- /dev/null +++ b/src/links/links.cotroller.ts @@ -0,0 +1,32 @@ +import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common'; +import { LinksService } from './links.service'; +import { CreateLinkDTO } from './dto/create-link.dto'; +import JwtAuthGuard from 'src/auth/guards/jwt-auth.guard'; +import { RequestWithUser } from 'src/auth/interfaces/request-with-user.interface'; + +@Controller('/links') +export class LinksController { + constructor(private readonly linksService: LinksService) {} + + @Post('/') + @UseGuards(JwtAuthGuard) + async createLink( + @Body() body: CreateLinkDTO, + @Req() request: RequestWithUser, + ) { + const link = await this.linksService.createLink({ + ...body, + userId: request.user.id, + }); + + return { link }; + } + + @Get('/') + @UseGuards(JwtAuthGuard) + async getLinks(@Req() request: RequestWithUser) { + const links = await this.linksService.getLinks(request.user.id); + + return { links }; + } +} diff --git a/src/links/links.module.ts b/src/links/links.module.ts new file mode 100644 index 0000000..ba1f2d0 --- /dev/null +++ b/src/links/links.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { LinksService } from './links.service'; +import { Link } from './entities/links.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { LinksController } from './links.cotroller'; + +@Module({ + imports: [TypeOrmModule.forFeature([Link])], + controllers: [LinksController], + providers: [LinksService], +}) +export class LinksModule {} diff --git a/src/links/links.service.ts b/src/links/links.service.ts new file mode 100644 index 0000000..0895adf --- /dev/null +++ b/src/links/links.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { Link } from './entities/links.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +@Injectable() +export class LinksService { + constructor( + @InjectRepository(Link) + private linksRepository: Repository, + ) {} + + async createLink({ url, userId }: { url: string; userId: number }) { + const link = this.linksRepository.create({ url, user: { id: userId } }); + return this.linksRepository.save(link); + } + + async getLinks(userId: number) { + return this.linksRepository.findBy({ user: { id: userId } }); + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..e89ad38 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,22 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; +import * as cookieParser from 'cookie-parser'; +import { ValidationPipe } from '@nestjs/common'; +// import { MicroserviceOptions, Transport } from '@nestjs/microservices'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + app.useGlobalPipes(new ValidationPipe()); + app.use(cookieParser()); + await app.listen(process.env.PORT); + + // MICROSERVICE + // app.connectMicroservice({ + // transport: Transport.NATS, + // options: { retryAttempts: 5, retryDelay: 3000 }, + // }); + + // await app.startAllMicroservices(); + // await app.listen(3001); +} +bootstrap(); diff --git a/src/prices/entities/prices.entity.ts b/src/prices/entities/prices.entity.ts new file mode 100644 index 0000000..8f83d2f --- /dev/null +++ b/src/prices/entities/prices.entity.ts @@ -0,0 +1,23 @@ +import { + Column, + CreateDateColumn, + Entity, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Link } from 'src/links/entities/links.entity'; + +@Entity('Prices') +export class Price { + @PrimaryGeneratedColumn() + id!: number; + + @Column() + value: number; + + @CreateDateColumn() + createdDate: Date; + + @ManyToOne(() => Link, (link) => link.prices) + link!: Link; +} diff --git a/src/prices/prices.module.ts b/src/prices/prices.module.ts new file mode 100644 index 0000000..ca5493c --- /dev/null +++ b/src/prices/prices.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { PricesService } from './prices.service'; +import { Price } from './entities/prices.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Price])], + providers: [PricesService], +}) +export class PricesModule {} diff --git a/src/prices/prices.service.ts b/src/prices/prices.service.ts new file mode 100644 index 0000000..3e126a9 --- /dev/null +++ b/src/prices/prices.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class PricesService {} diff --git a/src/tasks/tasks.module.ts b/src/tasks/tasks.module.ts new file mode 100644 index 0000000..045a60e --- /dev/null +++ b/src/tasks/tasks.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { TasksService } from './tasks.service'; +import { HttpModule } from '@nestjs/axios'; + +@Module({ + imports: [HttpModule], + providers: [TasksService], +}) +export class TasksModule {} diff --git a/src/tasks/tasks.service.ts b/src/tasks/tasks.service.ts new file mode 100644 index 0000000..1862b57 --- /dev/null +++ b/src/tasks/tasks.service.ts @@ -0,0 +1,44 @@ +import { HttpService } from '@nestjs/axios'; +import { Injectable, Logger } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { firstValueFrom } from 'rxjs'; + +// * * * * * * +// | | | | | | +// | | | | | day of week +// | | | | months +// | | | day of month +// | | hours +// | minutes +// seconds (optional) + +@Injectable() +export class TasksService { + private readonly logger = new Logger(TasksService.name); + + constructor(private readonly httpService: HttpService) {} + + private async request(link: string) { + try { + const response = await firstValueFrom( + this.httpService.get(`http://localhost:3000/parse?link=${link}`), + ); + return response.data; + } catch (e) { + return 'Error!'; + } + } + + @Cron('0 */20 * * * *', { + name: 'sendParse', + timeZone: 'Europe/Moscow', + disabled: true, + }) + async sendParse() { + const link = + 'https://novex.ru/catalog/product/nabor-polot-c-smart-35-75-70-140-brusn/'; + const res = await this.request(link); + this.logger.debug(res); + this.logger.debug('Called when the current second is 10'); + } +} diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts new file mode 100644 index 0000000..5a74ee2 --- /dev/null +++ b/src/users/dto/create-user.dto.ts @@ -0,0 +1,15 @@ +import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; + +export class CreateUserDto { + @IsNotEmpty() + @IsString() + name: string; + + @IsNotEmpty() + @IsString() + @MinLength(5) + password: string; + + @IsEmail() + email: string; +} diff --git a/src/users/dto/update-user.dto.ts b/src/users/dto/update-user.dto.ts new file mode 100644 index 0000000..dfd37fb --- /dev/null +++ b/src/users/dto/update-user.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateUserDto } from './create-user.dto'; + +export class UpdateUserDto extends PartialType(CreateUserDto) {} diff --git a/src/users/entities/user.entity.ts b/src/users/entities/user.entity.ts new file mode 100644 index 0000000..9461407 --- /dev/null +++ b/src/users/entities/user.entity.ts @@ -0,0 +1,32 @@ +import { Link } from 'src/links/entities/links.entity'; +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; + +export enum UserRole { + ADMIN = 'admin', + STANDARD = 'standard', +} + +@Entity('Users') +export class User { + @PrimaryGeneratedColumn() + id!: number; + + @Column() + name: string; + + @Column() + password: string; + + @Column({ unique: true }) + email: string; + + @Column({ + type: 'enum', + enum: UserRole, + default: UserRole.STANDARD, + }) + role: UserRole; + + @OneToMany(() => Link, (link) => link.user) + links: Link[]; +} diff --git a/src/users/users.module.ts b/src/users/users.module.ts new file mode 100644 index 0000000..65c8b2c --- /dev/null +++ b/src/users/users.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { UsersService } from './users.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from './entities/user.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([User])], + providers: [UsersService], + exports: [UsersService], +}) +export class UsersModule {} diff --git a/src/users/users.service.ts b/src/users/users.service.ts new file mode 100644 index 0000000..17d12cd --- /dev/null +++ b/src/users/users.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@nestjs/common'; +import { CreateUserDto } from './dto/create-user.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { User } from './entities/user.entity'; +import { Repository } from 'typeorm'; +import { + UserEmailNotExistsException, + UserIdNotExistsException, +} from 'src/utils/errors'; + +@Injectable() +export class UsersService { + constructor( + @InjectRepository(User) + private usersRepository: Repository, + ) {} + + async getByEmail(email: string) { + const user = await this.usersRepository.findOneBy({ email }); + if (!user) { + throw new UserEmailNotExistsException(); + } + return user; + } + + async getById(id: number) { + const user = await this.usersRepository.findOneBy({ id }); + if (!user) { + throw new UserIdNotExistsException(); + } + return user; + } + + async create(userData: CreateUserDto) { + const user = this.usersRepository.create(userData); + await this.usersRepository.save(user); + return user; + } +} diff --git a/src/utils/errors.ts b/src/utils/errors.ts new file mode 100644 index 0000000..e177699 --- /dev/null +++ b/src/utils/errors.ts @@ -0,0 +1,31 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; + +export class UserEmailNotExistsException extends HttpException { + constructor() { + super('User with this email does not exist', HttpStatus.NOT_FOUND); + } +} + +export class WrongCredentialsException extends HttpException { + constructor() { + super('Wrong credentials provided', HttpStatus.BAD_REQUEST); + } +} + +export class UserEmailExistsException extends HttpException { + constructor() { + super('User with that email already exists', HttpStatus.BAD_REQUEST); + } +} + +export class InternalErrorException extends HttpException { + constructor() { + super('Something went wrong', HttpStatus.INTERNAL_SERVER_ERROR); + } +} + +export class UserIdNotExistsException extends HttpException { + constructor() { + super('User with this id does not exist', HttpStatus.NOT_FOUND); + } +} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..64f86c6 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..adb614c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "es2017", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false + } +} diff --git a/typeorm.config.ts b/typeorm.config.ts new file mode 100644 index 0000000..943def1 --- /dev/null +++ b/typeorm.config.ts @@ -0,0 +1,18 @@ +import { DataSource } from 'typeorm'; +import { ConfigService } from '@nestjs/config'; +import { config } from 'dotenv'; + +config(); + +const configService = new ConfigService(); + +export default new DataSource({ + type: 'postgres', + host: configService.get('DB_HOST'), + port: +configService.get('DB_PORT'), + username: configService.get('DB_USER'), + password: configService.get('DB_PASSWORD'), + database: configService.get('DB_NAME'), + entities: ['dist/**/*.entity.js'], + migrations: ['migrations/**/*'], +});