diff --git a/src/authentication/jwt-refresh-token.strategy.ts b/src/authentication/jwt-refresh-token.strategy.ts new file mode 100644 index 0000000..abf9006 --- /dev/null +++ b/src/authentication/jwt-refresh-token.strategy.ts @@ -0,0 +1,30 @@ +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Request } from 'express'; +import {UsersService} from "../users/user.service"; + +@Injectable() +export class JwtRefreshTokenStrategy extends PassportStrategy( + Strategy, + 'jwt-refresh-token' +) { + constructor( + private readonly configService: ConfigService, + private readonly userService: UsersService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromExtractors([(request: Request) => { + return request?.cookies?.Refresh; + }]), + secretOrKey: configService.get('JWT_REFRESH_TOKEN_SECRET'), + passReqToCallback: true, + }); + } + + async validate(request: Request, payload: TokenPayload) { + const refreshToken = request.cookies?.Refresh; + return this.userService.getUserIfRefreshTokenMatches(refreshToken, payload.userId); + } +} \ No newline at end of file diff --git a/src/authentication/jwtRefreshGuard.guard.ts b/src/authentication/jwtRefreshGuard.guard.ts new file mode 100644 index 0000000..2b7317e --- /dev/null +++ b/src/authentication/jwtRefreshGuard.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export default class JwtRefreshGuard extends AuthGuard('jwt-refresh-token') {} \ No newline at end of file diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts new file mode 100644 index 0000000..1c10f8c --- /dev/null +++ b/src/chat/chat.gateway.ts @@ -0,0 +1,44 @@ +import { + ConnectedSocket, + MessageBody, OnGatewayConnection, + SubscribeMessage, + WebSocketGateway, + WebSocketServer, +} from '@nestjs/websockets'; +import {Server, Socket} from 'socket.io'; +import {ChatService} from "./chat.service"; + +@WebSocketGateway() +export class ChatGateway implements OnGatewayConnection { + @WebSocketServer() + server: Server; + + constructor( + private readonly chatService: ChatService + ) { + } + + async handleConnection(socket: Socket) { + await this.chatService.getUserFromSocket(socket); + } + + @SubscribeMessage('send_message') + async listenForMessages(@MessageBody() content: string, @ConnectedSocket() socket: Socket,) { + const author = await this.chatService.getUserFromSocket(socket); + const message = await this.chatService.saveMessage(content, author); + this.server.sockets.emit('receive_message', message); + return message; + } + + @SubscribeMessage('request_all_messages') + async requestAllMessages( + @ConnectedSocket() socket: Socket, + ) { + await this.chatService.getUserFromSocket(socket); + const messages = await this.chatService.getAllMessages(); + + socket.emit('send_all_messages', messages); + } + + +} \ No newline at end of file diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts new file mode 100644 index 0000000..17cd034 --- /dev/null +++ b/src/chat/chat.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@nestjs/common'; +import { AuthenticationService } from '../authentication/authentication.service'; +import { Socket } from 'socket.io'; +import { parse } from 'cookie'; +import { WsException } from '@nestjs/websockets'; +import UserEntity from "../users/entities/user.entity"; +import {InjectRepository} from "@nestjs/typeorm"; +import Message from "./mesage.entity"; +import {Repository} from "typeorm"; + +@Injectable() +export class ChatService { + constructor( + private readonly authenticationService: AuthenticationService, + @InjectRepository(Message) + private messagesRepository: Repository, + ) { + } + + async saveMessage(content: string, author: UserEntity) { + const newMessage = this.messagesRepository.create({ + content, + author + }); + await this.messagesRepository.save(newMessage); + return newMessage; + } + + async getAllMessages() { + return this.messagesRepository.find({ + relations: { + author: true, + } + }); + } + + async getUserFromSocket(socket: Socket) { + const cookie = socket.handshake.headers.cookie; + const { Authentication: authenticationToken } = parse(cookie); + const user = await this.authenticationService.getUserFromAuthenticationToken(authenticationToken); + if (!user) { + throw new WsException('Invalid credentials.'); + } + return user; + } +} + diff --git a/src/chat/mesage.entity.ts b/src/chat/mesage.entity.ts new file mode 100644 index 0000000..65912c0 --- /dev/null +++ b/src/chat/mesage.entity.ts @@ -0,0 +1,16 @@ +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import User from '../users/entities/user.entity'; + +@Entity() +class Message { + @PrimaryGeneratedColumn() + public id: number; + + @Column() + public content: string; + + @ManyToOne(() => User) + public author: User; +} + +export default Message; \ No newline at end of file diff --git a/src/posts/httpCache.interceptor.ts b/src/posts/httpCache.interceptor.ts new file mode 100644 index 0000000..4f77122 --- /dev/null +++ b/src/posts/httpCache.interceptor.ts @@ -0,0 +1,19 @@ +import { CACHE_KEY_METADATA, CacheInterceptor, } from '@nestjs/cache-manager'; +import {ExecutionContext, Injectable} from "@nestjs/common"; + +@Injectable() +export class HttpCacheInterceptor extends CacheInterceptor { + trackBy(context: ExecutionContext): string | undefined { + const cacheKey = this.reflector.get( + CACHE_KEY_METADATA, + context.getHandler(), + ); + + if (cacheKey) { + const request = context.switchToHttp().getRequest(); + return `${cacheKey}-${request._parsedUrl.query}`; + } + + return super.trackBy(context); + } +} \ No newline at end of file diff --git a/src/posts/postsCacheKey.constant.ts b/src/posts/postsCacheKey.constant.ts new file mode 100644 index 0000000..78140f6 --- /dev/null +++ b/src/posts/postsCacheKey.constant.ts @@ -0,0 +1 @@ +export const GET_POSTS_CACHE_KEY = 'GET_POSTS_CACHE'; \ No newline at end of file diff --git a/src/posts/postsSearch.service.ts b/src/posts/postsSearch.service.ts new file mode 100644 index 0000000..4d765b8 --- /dev/null +++ b/src/posts/postsSearch.service.ts @@ -0,0 +1,98 @@ +import {Injectable} from '@nestjs/common'; +import {ElasticsearchService} from '@nestjs/elasticsearch'; +import Post from "./entities/post.entity"; +import {PostSearchResult} from "./types/postSearchResult.interface"; +import {PostSearchBody} from "./types/postSearchBody.interface"; +import PostCountResult from "./types/postCountBody.interface"; + +@Injectable() +export default class PostsSearchService { + index = 'posts'; + + constructor( + private readonly elasticsearchService: ElasticsearchService + ) { + } + + async indexPost(post: Post) { + return this.elasticsearchService.index({ + index: this.index, + document: { + id: post.id, + title: post.title, + content: post.content, + authorId: post.author.id + } + }); + } + + async count(query: string, fields: string[]) { + const result = await this.elasticsearchService.count({ + index: this.index, + query: { + multi_match: { + query, + fields, + }, + }, + }); + + return result.count; + } + + + + async search(text: string) { + const result = await this.elasticsearchService.search({ + index: this.index, + query: { + multi_match: { + query: text, + fields: ['title', 'content'], + }, + }, + }); + + const hits = result.hits.hits; + return hits.map((item) => item._source); + } + + + async remove(postId: number) { + await this.elasticsearchService.deleteByQuery({ + index: this.index, + query: { + match: { + id: postId, + } + } + + }) + } + + async update(post: Post) { + const newBody: PostSearchBody = { + id: post.id, + title: post.title, + content: post.content, + authorId: post.author.id + }; + + const script = Object.entries(newBody).reduce((result, [key, value]) => { + return `${result} ctx._source.${key}='${value}';`; + }, ''); + + await this.elasticsearchService.updateByQuery({ + index: this.index, + query: { + match: { + id: post.id, + } + }, + script: { + source: script + } + }); + } + +} \ No newline at end of file diff --git a/src/posts/types/postCountBody.interface.ts b/src/posts/types/postCountBody.interface.ts new file mode 100644 index 0000000..19e322f --- /dev/null +++ b/src/posts/types/postCountBody.interface.ts @@ -0,0 +1,5 @@ +interface PostCountResult { + count: number; +} + +export default PostCountResult; \ No newline at end of file diff --git a/src/posts/types/postSearchBody.interface.ts b/src/posts/types/postSearchBody.interface.ts new file mode 100644 index 0000000..61dacc3 --- /dev/null +++ b/src/posts/types/postSearchBody.interface.ts @@ -0,0 +1,6 @@ +export interface PostSearchBody { + id: number, + title: string, + content: string, + authorId: number +} \ No newline at end of file diff --git a/src/posts/types/postSearchResult.interface.ts b/src/posts/types/postSearchResult.interface.ts new file mode 100644 index 0000000..96a9afb --- /dev/null +++ b/src/posts/types/postSearchResult.interface.ts @@ -0,0 +1,10 @@ +import {PostSearchBody} from "./postSearchBody.interface"; + +export interface PostSearchResult { + hits: { + total: number; + hits: Array<{ + _source: PostSearchBody; + }>; + }; +} \ No newline at end of file diff --git a/src/productCategories/productCategory.entity.ts b/src/productCategories/productCategory.entity.ts new file mode 100644 index 0000000..1c41f4a --- /dev/null +++ b/src/productCategories/productCategory.entity.ts @@ -0,0 +1,19 @@ +import { Column, Entity, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; +import Product from '../products/product.entity'; + +@Entity() +class ProductCategory { + @PrimaryGeneratedColumn() + public id: number; + + @Column() + public name: string; + + @OneToMany( + () => Product, + (product: Product) => product.category, + ) + public products: Product[]; +} + +export default ProductCategory; \ No newline at end of file diff --git a/src/products/product.entity.ts b/src/products/product.entity.ts new file mode 100644 index 0000000..9a6587e --- /dev/null +++ b/src/products/product.entity.ts @@ -0,0 +1,23 @@ +import { Column, Entity, PrimaryGeneratedColumn, ManyToOne } from 'typeorm'; +import ProductCategory from '../productCategories/productCategory.entity'; +import { CarProperties } from './types/carProperties.interface'; +import { BookProperties } from './types/bookProperties.interface'; + +@Entity() +class Product { + @PrimaryGeneratedColumn() + public id: number; + + @Column() + public name: string; + + @ManyToOne(() => ProductCategory, (category: ProductCategory) => category.products) + public category: ProductCategory; + + @Column({ + type: 'jsonb' + }) + public properties: CarProperties | BookProperties; +} + +export default Product; \ No newline at end of file diff --git a/src/products/types/bookProperties.interface.ts b/src/products/types/bookProperties.interface.ts new file mode 100644 index 0000000..b3dae4e --- /dev/null +++ b/src/products/types/bookProperties.interface.ts @@ -0,0 +1,4 @@ +export interface BookProperties { + authors: string[]; + publicationYear: string; +} \ No newline at end of file diff --git a/src/products/types/carProperties.interface.ts b/src/products/types/carProperties.interface.ts new file mode 100644 index 0000000..a1c4c28 --- /dev/null +++ b/src/products/types/carProperties.interface.ts @@ -0,0 +1,7 @@ +export interface CarProperties { + brand: string; + engine: { + fuel: string; + numberOfCylinders: number; + } +} \ No newline at end of file diff --git a/src/search/search.module.ts b/src/search/search.module.ts new file mode 100644 index 0000000..5f5c6d9 --- /dev/null +++ b/src/search/search.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { ElasticsearchModule } from '@nestjs/elasticsearch'; + +@Module({ + imports: [ + ConfigModule, + ElasticsearchModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + node: configService.get('ELASTICSEARCH_NODE'), + auth: { + username: configService.get('ELASTICSEARCH_USERNAME'), + password: configService.get('ELASTICSEARCH_PASSWORD'), + } + }), + inject: [ConfigService], + }), + ], + exports: [ElasticsearchModule] +}) +export class SearchModule {} \ No newline at end of file diff --git a/src/utils/types/paginationParams.ts b/src/utils/types/paginationParams.ts new file mode 100644 index 0000000..39600b5 --- /dev/null +++ b/src/utils/types/paginationParams.ts @@ -0,0 +1,23 @@ +import { IsNumber, Min, IsOptional } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class PaginationParams { + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(1) + startId?: number; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + offset?: number; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(1) + limit?: number; +} \ No newline at end of file