Compare commits
1 Commits
main
...
versions/b
| Author | SHA1 | Date | |
|---|---|---|---|
| b48e59b073 |
2344
package-lock.json
generated
2344
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,12 +21,18 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@types/pg": "^8.15.5",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"pg": "^8.16.3",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
|
||||
@@ -1,10 +1,33 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { AuthenticationModule } from './authentication/authentication.module';
|
||||
import { UsersModule } from './users/users.module';
|
||||
|
||||
@Module({
|
||||
imports: [AuthenticationModule],
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: '.env',
|
||||
}),
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
type: 'postgres',
|
||||
url: configService.get('DATABASE_URL') || 'postgresql://postgres.zgmimnyowgdnlwccjubz:renolation29@aws-1-ap-southeast-1.pooler.supabase.com:5432/postgres',
|
||||
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||
synchronize: true,
|
||||
ssl: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
AuthenticationModule,
|
||||
UsersModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthenticationController } from './authentication.controller';
|
||||
import { AuthenticationService } from './authentication.service';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
|
||||
/**
|
||||
* Authentication module for handling user authentication
|
||||
*/
|
||||
@Module({
|
||||
imports: [UsersModule],
|
||||
controllers: [AuthenticationController],
|
||||
providers: [AuthenticationService],
|
||||
exports: [AuthenticationService],
|
||||
|
||||
@@ -24,22 +24,19 @@ describe('AuthenticationService', () => {
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
};
|
||||
const expectedResponse = {
|
||||
message: 'Login successful',
|
||||
user: {
|
||||
email: 'test@example.com',
|
||||
},
|
||||
token: 'placeholder-token',
|
||||
};
|
||||
|
||||
const actualResponse = await service.login(inputLoginDto);
|
||||
|
||||
expect(actualResponse).toEqual(expectedResponse);
|
||||
expect(actualResponse).toHaveProperty('message', 'Login successful');
|
||||
expect(actualResponse).toHaveProperty('user');
|
||||
expect(actualResponse.user).toHaveProperty('email', 'test@example.com');
|
||||
expect(actualResponse).toHaveProperty('token');
|
||||
expect(actualResponse.token).toMatch(/^auth_token_\d+_[a-z0-9]+$/);
|
||||
});
|
||||
|
||||
it('should throw UnauthorizedException with invalid email', async () => {
|
||||
it('should throw UnauthorizedException with empty email', async () => {
|
||||
const inputLoginDto: LoginDto = {
|
||||
email: 'invalid@example.com',
|
||||
email: '',
|
||||
password: 'password123',
|
||||
};
|
||||
|
||||
@@ -48,10 +45,10 @@ describe('AuthenticationService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw UnauthorizedException with invalid password', async () => {
|
||||
it('should throw UnauthorizedException with empty password', async () => {
|
||||
const inputLoginDto: LoginDto = {
|
||||
email: 'test@example.com',
|
||||
password: 'wrongpassword',
|
||||
password: '',
|
||||
};
|
||||
|
||||
await expect(service.login(inputLoginDto)).rejects.toThrow(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { UserService } from '../users/user.service';
|
||||
|
||||
/**
|
||||
* Response interface for successful login
|
||||
@@ -7,9 +8,11 @@ import { LoginDto } from './dto/login.dto';
|
||||
export interface LoginResponse {
|
||||
message: string;
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
};
|
||||
token?: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -17,6 +20,8 @@ export interface LoginResponse {
|
||||
*/
|
||||
@Injectable()
|
||||
export class AuthenticationService {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
/**
|
||||
* Authenticates a user with email and password
|
||||
* @param loginDto - Login credentials containing email and password
|
||||
@@ -26,33 +31,33 @@ export class AuthenticationService {
|
||||
async login(loginDto: LoginDto): Promise<LoginResponse> {
|
||||
const { email, password } = loginDto;
|
||||
|
||||
// TODO: Implement actual user validation logic
|
||||
// This is a placeholder implementation
|
||||
const isValidUser = await this.validateUser(email, password);
|
||||
if (!email || !password) {
|
||||
throw new UnauthorizedException('Email and password are required');
|
||||
}
|
||||
|
||||
if (!isValidUser) {
|
||||
// Find user by email
|
||||
const user = await this.userService.findUserByEmail(email);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Invalid email or password');
|
||||
}
|
||||
|
||||
// Simple password validation (in production, use bcrypt)
|
||||
if (user.password !== password) {
|
||||
throw new UnauthorizedException('Invalid email or password');
|
||||
}
|
||||
|
||||
// Generate a simple token (in production, use JWT)
|
||||
const token = `auth_token_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
return {
|
||||
message: 'Login successful',
|
||||
user: {
|
||||
email,
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
},
|
||||
// TODO: Generate JWT token
|
||||
token: 'placeholder-token',
|
||||
token,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates user credentials
|
||||
* @param email - User email
|
||||
* @param password - User password
|
||||
* @returns Promise<boolean> - Whether credentials are valid
|
||||
*/
|
||||
private async validateUser(email: string, password: string): Promise<boolean> {
|
||||
// TODO: Implement actual user validation against database
|
||||
// For now, using placeholder validation
|
||||
return email === 'test@example.com' && password === 'password123';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export default class JwtRefreshGuard extends AuthGuard('jwt-refresh-token') {}
|
||||
@@ -1,44 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
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;
|
||||
@@ -3,6 +3,6 @@ import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
await app.listen(process.env.PORT ?? 3000);
|
||||
await app.listen(4003);
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
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 +0,0 @@
|
||||
export const GET_POSTS_CACHE_KEY = 'GET_POSTS_CACHE';
|
||||
@@ -1,98 +0,0 @@
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
interface PostCountResult {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export default PostCountResult;
|
||||
@@ -1,6 +0,0 @@
|
||||
export interface PostSearchBody {
|
||||
id: number,
|
||||
title: string,
|
||||
content: string,
|
||||
authorId: number
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import {PostSearchBody} from "./postSearchBody.interface";
|
||||
|
||||
export interface PostSearchResult {
|
||||
hits: {
|
||||
total: number;
|
||||
hits: Array<{
|
||||
_source: PostSearchBody;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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;
|
||||
@@ -1,23 +0,0 @@
|
||||
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;
|
||||
@@ -1,4 +0,0 @@
|
||||
export interface BookProperties {
|
||||
authors: string[];
|
||||
publicationYear: string;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export interface CarProperties {
|
||||
brand: string;
|
||||
engine: {
|
||||
fuel: string;
|
||||
numberOfCylinders: number;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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 {}
|
||||
20
src/users/dto/create-user.dto.ts
Normal file
20
src/users/dto/create-user.dto.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { IsEmail, IsNotEmpty, IsOptional, IsString, IsUrl, MinLength } from 'class-validator';
|
||||
|
||||
export class CreateUserDto {
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
email: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MinLength(6, { message: 'Password must be at least 6 characters long' })
|
||||
password: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
avatarUrl?: string;
|
||||
}
|
||||
4
src/users/dto/update-user.dto.ts
Normal file
4
src/users/dto/update-user.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateUserDto } from './create-user.dto';
|
||||
|
||||
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
||||
31
src/users/user.entity.ts
Normal file
31
src/users/user.entity.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('users')
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ unique: true })
|
||||
email: string;
|
||||
|
||||
@Column()
|
||||
password: string;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
avatarUrl?: string;
|
||||
|
||||
@Column()
|
||||
token: string;
|
||||
|
||||
@Column({ type: 'timestamp', nullable: true })
|
||||
tokenExpiry?: Date;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
133
src/users/user.service.spec.ts
Normal file
133
src/users/user.service.spec.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { UserService } from './user.service';
|
||||
import { User } from './user.entity';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
let repository: Repository<User>;
|
||||
|
||||
const mockRepository = {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
UserService,
|
||||
{
|
||||
provide: getRepositoryToken(User),
|
||||
useValue: mockRepository,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<UserService>(UserService);
|
||||
repository = module.get<Repository<User>>(getRepositoryToken(User));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('createUser', () => {
|
||||
it('should create a user with token and tokenExpiry', async () => {
|
||||
const inputCreateUserDto: CreateUserDto = {
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
};
|
||||
|
||||
const expectedUser = {
|
||||
...inputCreateUserDto,
|
||||
id: 'uuid',
|
||||
token: 'mock_jwt_token_1234567890',
|
||||
tokenExpiry: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
mockRepository.create.mockReturnValue(expectedUser);
|
||||
mockRepository.save.mockResolvedValue(expectedUser);
|
||||
|
||||
const actualUser = await service.createUser(inputCreateUserDto);
|
||||
|
||||
expect(mockRepository.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
email: inputCreateUserDto.email,
|
||||
name: inputCreateUserDto.name,
|
||||
avatarUrl: inputCreateUserDto.avatarUrl,
|
||||
token: expect.stringMatching(/^mock_jwt_token_\d+$/),
|
||||
tokenExpiry: expect.any(Date),
|
||||
}),
|
||||
);
|
||||
expect(mockRepository.save).toHaveBeenCalledWith(expectedUser);
|
||||
expect(actualUser).toEqual(expectedUser);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findUserById', () => {
|
||||
it('should return a user when found', async () => {
|
||||
const inputId = 'test-id';
|
||||
const expectedUser = {
|
||||
id: inputId,
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
token: 'mock_jwt_token_1234567890',
|
||||
tokenExpiry: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
mockRepository.findOne.mockResolvedValue(expectedUser);
|
||||
|
||||
const actualUser = await service.findUserById(inputId);
|
||||
|
||||
expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { id: inputId } });
|
||||
expect(actualUser).toEqual(expectedUser);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException when user not found', async () => {
|
||||
const inputId = 'non-existent-id';
|
||||
mockRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.findUserById(inputId)).rejects.toThrow(NotFoundException);
|
||||
expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { id: inputId } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshToken', () => {
|
||||
it('should refresh user token and tokenExpiry', async () => {
|
||||
const inputId = 'test-id';
|
||||
const mockUser = {
|
||||
id: inputId,
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
token: 'old_token',
|
||||
tokenExpiry: new Date(),
|
||||
save: jest.fn(),
|
||||
};
|
||||
|
||||
mockRepository.findOne.mockResolvedValue(mockUser);
|
||||
mockRepository.save.mockResolvedValue(mockUser);
|
||||
|
||||
const actualUser = await service.refreshToken(inputId);
|
||||
|
||||
expect(mockUser.token).toMatch(/^mock_jwt_token_\d+$/);
|
||||
expect(mockUser.tokenExpiry).toBeInstanceOf(Date);
|
||||
expect(mockRepository.save).toHaveBeenCalledWith(mockUser);
|
||||
expect(actualUser).toEqual(mockUser);
|
||||
});
|
||||
});
|
||||
});
|
||||
75
src/users/user.service.ts
Normal file
75
src/users/user.service.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
async createUser(createUserDto: CreateUserDto): Promise<User> {
|
||||
const token = `mock_jwt_token_${Date.now()}`;
|
||||
const tokenExpiry = new Date();
|
||||
tokenExpiry.setDate(tokenExpiry.getDate() + 365);
|
||||
|
||||
const user = this.userRepository.create({
|
||||
...createUserDto,
|
||||
token,
|
||||
tokenExpiry,
|
||||
});
|
||||
|
||||
return this.userRepository.save(user);
|
||||
}
|
||||
|
||||
async findAllUsers(): Promise<User[]> {
|
||||
return this.userRepository.find();
|
||||
}
|
||||
|
||||
async findUserById(id: string): Promise<User> {
|
||||
const user = await this.userRepository.findOne({ where: { id } });
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID ${id} not found`);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async findUserByEmail(email: string): Promise<User | null> {
|
||||
return this.userRepository.findOne({ where: { email } });
|
||||
}
|
||||
|
||||
async updateUser(id: string, updateUserDto: UpdateUserDto): Promise<User> {
|
||||
const user = await this.findUserById(id);
|
||||
Object.assign(user, updateUserDto);
|
||||
return this.userRepository.save(user);
|
||||
}
|
||||
|
||||
async deleteUser(id: string): Promise<void> {
|
||||
const user = await this.findUserById(id);
|
||||
await this.userRepository.remove(user);
|
||||
}
|
||||
|
||||
async refreshToken(id: string): Promise<User> {
|
||||
const user = await this.findUserById(id);
|
||||
const token = `mock_jwt_token_${Date.now()}`;
|
||||
const tokenExpiry = new Date();
|
||||
tokenExpiry.setDate(tokenExpiry.getDate() + 365);
|
||||
|
||||
user.token = token;
|
||||
user.tokenExpiry = tokenExpiry;
|
||||
|
||||
return this.userRepository.save(user);
|
||||
}
|
||||
|
||||
async getUserIfRefreshTokenMatches(refreshToken: string, userId: string): Promise<User | null> {
|
||||
const user = await this.findUserById(userId);
|
||||
if (user && user.token === refreshToken) {
|
||||
return user;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
192
src/users/users.controller.spec.ts
Normal file
192
src/users/users.controller.spec.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UsersController } from './users.controller';
|
||||
import { UserService } from './user.service';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
|
||||
describe('UsersController', () => {
|
||||
let controller: UsersController;
|
||||
let service: UserService;
|
||||
|
||||
const mockUserService = {
|
||||
createUser: jest.fn(),
|
||||
findAllUsers: jest.fn(),
|
||||
findUserById: jest.fn(),
|
||||
updateUser: jest.fn(),
|
||||
deleteUser: jest.fn(),
|
||||
refreshToken: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [UsersController],
|
||||
providers: [
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: mockUserService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<UsersController>(UsersController);
|
||||
service = module.get<UserService>(UserService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
describe('createUser', () => {
|
||||
it('should create a user', async () => {
|
||||
const inputCreateUserDto: CreateUserDto = {
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
};
|
||||
|
||||
const expectedUser = {
|
||||
id: 'uuid',
|
||||
...inputCreateUserDto,
|
||||
token: 'mock_jwt_token_1234567890',
|
||||
tokenExpiry: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
mockUserService.createUser.mockResolvedValue(expectedUser);
|
||||
|
||||
const actualUser = await controller.createUser(inputCreateUserDto);
|
||||
|
||||
expect(mockUserService.createUser).toHaveBeenCalledWith(inputCreateUserDto);
|
||||
expect(actualUser).toEqual(expectedUser);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAllUsers', () => {
|
||||
it('should return all users', async () => {
|
||||
const expectedUsers = [
|
||||
{
|
||||
id: 'uuid1',
|
||||
email: 'test1@example.com',
|
||||
name: 'Test User 1',
|
||||
token: 'mock_jwt_token_1234567890',
|
||||
tokenExpiry: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
{
|
||||
id: 'uuid2',
|
||||
email: 'test2@example.com',
|
||||
name: 'Test User 2',
|
||||
token: 'mock_jwt_token_1234567891',
|
||||
tokenExpiry: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
mockUserService.findAllUsers.mockResolvedValue(expectedUsers);
|
||||
|
||||
const actualUsers = await controller.findAllUsers();
|
||||
|
||||
expect(mockUserService.findAllUsers).toHaveBeenCalled();
|
||||
expect(actualUsers).toEqual(expectedUsers);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findUserById', () => {
|
||||
it('should return a user by id', async () => {
|
||||
const inputId = 'test-id';
|
||||
const expectedUser = {
|
||||
id: inputId,
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
token: 'mock_jwt_token_1234567890',
|
||||
tokenExpiry: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
mockUserService.findUserById.mockResolvedValue(expectedUser);
|
||||
|
||||
const actualUser = await controller.findUserById(inputId);
|
||||
|
||||
expect(mockUserService.findUserById).toHaveBeenCalledWith(inputId);
|
||||
expect(actualUser).toEqual(expectedUser);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateUser', () => {
|
||||
it('should update a user', async () => {
|
||||
const inputId = 'test-id';
|
||||
const inputUpdateUserDto: UpdateUserDto = {
|
||||
name: 'Updated Name',
|
||||
};
|
||||
|
||||
const expectedUser = {
|
||||
id: inputId,
|
||||
email: 'test@example.com',
|
||||
name: 'Updated Name',
|
||||
token: 'mock_jwt_token_1234567890',
|
||||
tokenExpiry: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
mockUserService.updateUser.mockResolvedValue(expectedUser);
|
||||
|
||||
const actualUser = await controller.updateUser(inputId, inputUpdateUserDto);
|
||||
|
||||
expect(mockUserService.updateUser).toHaveBeenCalledWith(inputId, inputUpdateUserDto);
|
||||
expect(actualUser).toEqual(expectedUser);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteUser', () => {
|
||||
it('should delete a user', async () => {
|
||||
const inputId = 'test-id';
|
||||
|
||||
mockUserService.deleteUser.mockResolvedValue(undefined);
|
||||
|
||||
await controller.deleteUser(inputId);
|
||||
|
||||
expect(mockUserService.deleteUser).toHaveBeenCalledWith(inputId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshToken', () => {
|
||||
it('should refresh user token', async () => {
|
||||
const inputId = 'test-id';
|
||||
const expectedUser = {
|
||||
id: inputId,
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
token: 'new_mock_jwt_token_1234567890',
|
||||
tokenExpiry: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
mockUserService.refreshToken.mockResolvedValue(expectedUser);
|
||||
|
||||
const actualUser = await controller.refreshToken(inputId);
|
||||
|
||||
expect(mockUserService.refreshToken).toHaveBeenCalledWith(inputId);
|
||||
expect(actualUser).toEqual(expectedUser);
|
||||
});
|
||||
});
|
||||
|
||||
describe('testEndpoint', () => {
|
||||
it('should return test message', async () => {
|
||||
const expectedMessage = { message: 'User module is working correctly' };
|
||||
|
||||
const actualMessage = await controller.testEndpoint();
|
||||
|
||||
expect(actualMessage).toEqual(expectedMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
57
src/users/users.controller.ts
Normal file
57
src/users/users.controller.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { UserService } from './user.service';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
@Post()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
async createUser(@Body() createUserDto: CreateUserDto) {
|
||||
console.log('createUserDto', createUserDto);
|
||||
return this.userService.createUser(createUserDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
async findAllUsers() {
|
||||
return this.userService.findAllUsers();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findUserById(@Param('id') id: string) {
|
||||
return this.userService.findUserById(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
async updateUser(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
|
||||
return this.userService.updateUser(id, updateUserDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async deleteUser(@Param('id') id: string) {
|
||||
await this.userService.deleteUser(id);
|
||||
}
|
||||
|
||||
@Post(':id/refresh-token')
|
||||
async refreshToken(@Param('id') id: string) {
|
||||
return this.userService.refreshToken(id);
|
||||
}
|
||||
|
||||
@Get('admin/test')
|
||||
async testEndpoint() {
|
||||
return { message: 'User module is working correctly' };
|
||||
}
|
||||
}
|
||||
13
src/users/users.module.ts
Normal file
13
src/users/users.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { UserService } from './user.service';
|
||||
import { UsersController } from './users.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])],
|
||||
controllers: [UsersController],
|
||||
providers: [UserService],
|
||||
exports: [UserService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
@@ -1,23 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
import { App } from 'supertest/types';
|
||||
import request from 'supertest';
|
||||
import { AppModule } from './../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
let app: INestApplication;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
|
||||
Reference in New Issue
Block a user