Merge branch 'main' of https://github.com/renolation/nest-base
# Conflicts: # package-lock.json # package.json # src/app.module.ts # src/authentication/authentication.controller.ts # src/main.ts # src/posts/dto/create-post.dto.ts # src/posts/entities/post.entity.ts # src/posts/posts.controller.ts # src/posts/posts.module.ts # src/posts/posts.service.ts # src/users/user.service.ts
This commit is contained in:
30
src/authentication/jwt-refresh-token.strategy.ts
Normal file
30
src/authentication/jwt-refresh-token.strategy.ts
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
5
src/authentication/jwtRefreshGuard.guard.ts
Normal file
5
src/authentication/jwtRefreshGuard.guard.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export default class JwtRefreshGuard extends AuthGuard('jwt-refresh-token') {}
|
||||
44
src/chat/chat.gateway.ts
Normal file
44
src/chat/chat.gateway.ts
Normal file
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
47
src/chat/chat.service.ts
Normal file
47
src/chat/chat.service.ts
Normal file
@@ -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<Message>,
|
||||
) {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
16
src/chat/mesage.entity.ts
Normal file
16
src/chat/mesage.entity.ts
Normal file
@@ -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;
|
||||
19
src/posts/httpCache.interceptor.ts
Normal file
19
src/posts/httpCache.interceptor.ts
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
1
src/posts/postsCacheKey.constant.ts
Normal file
1
src/posts/postsCacheKey.constant.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const GET_POSTS_CACHE_KEY = 'GET_POSTS_CACHE';
|
||||
98
src/posts/postsSearch.service.ts
Normal file
98
src/posts/postsSearch.service.ts
Normal file
@@ -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<PostSearchBody>({
|
||||
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<PostSearchResult>({
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
5
src/posts/types/postCountBody.interface.ts
Normal file
5
src/posts/types/postCountBody.interface.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
interface PostCountResult {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export default PostCountResult;
|
||||
6
src/posts/types/postSearchBody.interface.ts
Normal file
6
src/posts/types/postSearchBody.interface.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface PostSearchBody {
|
||||
id: number,
|
||||
title: string,
|
||||
content: string,
|
||||
authorId: number
|
||||
}
|
||||
10
src/posts/types/postSearchResult.interface.ts
Normal file
10
src/posts/types/postSearchResult.interface.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import {PostSearchBody} from "./postSearchBody.interface";
|
||||
|
||||
export interface PostSearchResult {
|
||||
hits: {
|
||||
total: number;
|
||||
hits: Array<{
|
||||
_source: PostSearchBody;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
19
src/productCategories/productCategory.entity.ts
Normal file
19
src/productCategories/productCategory.entity.ts
Normal file
@@ -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;
|
||||
23
src/products/product.entity.ts
Normal file
23
src/products/product.entity.ts
Normal file
@@ -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;
|
||||
4
src/products/types/bookProperties.interface.ts
Normal file
4
src/products/types/bookProperties.interface.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface BookProperties {
|
||||
authors: string[];
|
||||
publicationYear: string;
|
||||
}
|
||||
7
src/products/types/carProperties.interface.ts
Normal file
7
src/products/types/carProperties.interface.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface CarProperties {
|
||||
brand: string;
|
||||
engine: {
|
||||
fuel: string;
|
||||
numberOfCylinders: number;
|
||||
}
|
||||
}
|
||||
22
src/search/search.module.ts
Normal file
22
src/search/search.module.ts
Normal file
@@ -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<string>('ELASTICSEARCH_NODE'),
|
||||
auth: {
|
||||
username: configService.get<string>('ELASTICSEARCH_USERNAME'),
|
||||
password: configService.get<string>('ELASTICSEARCH_PASSWORD'),
|
||||
}
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
],
|
||||
exports: [ElasticsearchModule]
|
||||
})
|
||||
export class SearchModule {}
|
||||
23
src/utils/types/paginationParams.ts
Normal file
23
src/utils/types/paginationParams.ts
Normal file
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user