add cursor

This commit is contained in:
2025-09-26 19:49:56 +07:00
parent ae7f2cd114
commit 5deae69553
54 changed files with 2763 additions and 6644 deletions

View File

@@ -0,0 +1,8 @@
---
description: Enforces specific guidelines for the core module in NestJS, focusing on global filters, middleware, guards, and interceptors.
globs: src/core/**/*.*
---
- Global filters for exception handling.
- Global middlewares for request management.
- Guards for permission management.
- Interceptors for request management.

View File

@@ -0,0 +1,26 @@
---
description: Specifies NestJS-specific architectural principles, modular design, and testing practices within the 'src' directory.
globs: src/**/*.*
---
- Use modular architecture
- Encapsulate the API in modules.
- One module per main domain/route.
- One controller for its route.
- And other controllers for secondary routes.
- A models folder with data types.
- DTOs validated with class-validator for inputs.
- Declare simple types for outputs.
- A services module with business logic and persistence.
- One service per entity.
- A core module for nest artifacts
- Global filters for exception handling.
- Global middlewares for request management.
- Guards for permission management.
- Interceptors for request management.
- A shared module for services shared between modules.
- Utilities
- Shared business logic
- Use the standard Jest framework for testing.
- Write tests for each controller and service.
- Write end to end tests for each api module.
- Add a admin/test method to each controller as a smoke test.

View File

@@ -0,0 +1,12 @@
---
description: Prescribes the structure and components within NestJS modules, including controllers, models, DTOs, and services, ensuring API encapsulation.
globs: src/modules/**/*.*
---
- One module per main domain/route.
- One controller for its route.
- And other controllers for secondary routes.
- A models folder with data types.
- DTOs validated with class-validator for inputs.
- Declare simple types for outputs.
- A services module with business logic and persistence.
- One service per entity.

View File

@@ -0,0 +1,6 @@
---
description: Defines standards for the shared module in NestJS, emphasizing utilities and shared business logic accessible across modules.
globs: src/shared/**/*.*
---
- Utilities
- Shared business logic

View File

@@ -0,0 +1,8 @@
---
description: Sets standards for testing NestJS applications, including unit, integration, and end-to-end tests, plus the use of Jest.
globs: **/*.spec.ts
---
- Use the standard Jest framework for testing.
- Write tests for each controller and service.
- Write end to end tests for each api module.
- Add a admin/test method to each controller as a smoke test.

View File

@@ -0,0 +1,66 @@
---
description: Applies general TypeScript coding standards across the project, including naming conventions, function structure, data handling, and exception handling.
globs: **/*.ts
---
- Use English for all code and documentation.
- Always declare the type of each variable and function (parameters and return value).
- Avoid using any.
- Create necessary types.
- Use JSDoc to document public classes and methods.
- Don't leave blank lines within a function.
- One export per file.
- Use PascalCase for classes.
- Use camelCase for variables, functions, and methods.
- Use kebab-case for file and directory names.
- Use UPPERCASE for environment variables.
- Avoid magic numbers and define constants.
- Start each function with a verb.
- Use verbs for boolean variables. Example: isLoading, hasError, canDelete, etc.
- Use complete words instead of abbreviations and correct spelling.
- Except for standard abbreviations like API, URL, etc.
- Except for well-known abbreviations:
- i, j for loops
- err for errors
- ctx for contexts
- req, res, next for middleware function parameters
- Write short functions with a single purpose. Less than 20 instructions.
- Name functions with a verb and something else.
- If it returns a boolean, use isX or hasX, canX, etc.
- If it doesn't return anything, use executeX or saveX, etc.
- Avoid nesting blocks by:
- Early checks and returns.
- Extraction to utility functions.
- Use higher-order functions (map, filter, reduce, etc.) to avoid function nesting.
- Use arrow functions for simple functions (less than 3 instructions).
- Use named functions for non-simple functions.
- Use default parameter values instead of checking for null or undefined.
- Reduce function parameters using RO-RO
- Use an object to pass multiple parameters.
- Use an object to return results.
- Declare necessary types for input arguments and output.
- Use a single level of abstraction.
- Don't abuse primitive types and encapsulate data in composite types.
- Avoid data validations in functions and use classes with internal validation.
- Prefer immutability for data.
- Use readonly for data that doesn't change.
- Use as const for literals that don't change.
- Follow SOLID principles.
- Prefer composition over inheritance.
- Declare interfaces to define contracts.
- Write small classes with a single purpose.
- Less than 200 instructions.
- Less than 10 public methods.
- Less than 10 properties.
- Use exceptions to handle errors you don't expect.
- If you catch an exception, it should be to:
- Fix an expected problem.
- Add context.
- Otherwise, use a global handler.
- Follow the Arrange-Act-Assert convention for tests.
- Name test variables clearly.
- Follow the convention: inputX, mockX, actualX, expectedX, etc.
- Write unit tests for each public function.
- Use test doubles to simulate dependencies.
- Except for third-party dependencies that are not expensive to execute.
- Write acceptance tests for each module.
- Follow the Given-When-Then convention.

251
.cursorrules Normal file
View File

@@ -0,0 +1,251 @@
You are a senior TypeScript programmer with experience in the NestJS framework and a preference for clean programming and design patterns. Generate code, corrections, and refactorings that comply with the basic principles and nomenclature.
## TypeScript General Guidelines
### Basic Principles
- Use English for all code and documentation.
- Always declare the type of each variable and function (parameters and return value).
- Avoid using any.
- Create necessary types.
- Use JSDoc to document public classes and methods.
- Don't leave blank lines within a function.
- One export per file.
### Nomenclature
- Use PascalCase for classes.
- Use camelCase for variables, functions, and methods.
- Use kebab-case for file and directory names.
- Use UPPERCASE for environment variables.
- Avoid magic numbers and define constants.
- Start each function with a verb.
- Use verbs for boolean variables. Example: isLoading, hasError, canDelete, etc.
- Use complete words instead of abbreviations and correct spelling.
- Except for standard abbreviations like API, URL, etc.
- Except for well-known abbreviations:
- i, j for loops
- err for errors
- ctx for contexts
- req, res, next for middleware function parameters
### Functions
- In this context, what is understood as a function will also apply to a method.
- Write short functions with a single purpose. Less than 20 instructions.
- Name functions with a verb and something else.
- If it returns a boolean, use isX or hasX, canX, etc.
- If it doesn't return anything, use executeX or saveX, etc.
- Avoid nesting blocks by:
- Early checks and returns.
- Extraction to utility functions.
- Use higher-order functions (map, filter, reduce, etc.) to avoid function nesting.
- Use arrow functions for simple functions (less than 3 instructions).
- Use named functions for non-simple functions.
- Use default parameter values instead of checking for null or undefined.
- Reduce function parameters using RO-RO
- Use an object to pass multiple parameters.
- Use an object to return results.
- Declare necessary types for input arguments and output.
- Use a single level of abstraction.
### Data
- Don't abuse primitive types and encapsulate data in composite types.
- Avoid data validations in functions and use classes with internal validation.
- Prefer immutability for data.
- Use readonly for data that doesn't change.
- Use as const for literals that don't change.
### Classes
- Follow SOLID principles.
- Prefer composition over inheritance.
- Declare interfaces to define contracts.
- Write small classes with a single purpose.
- Less than 200 instructions.
- Less than 10 public methods.
- Less than 10 properties.
### Exceptions
- Use exceptions to handle errors you don't expect.
- If you catch an exception, it should be to:
- Fix an expected problem.
- Add context.
- Otherwise, use a global handler.
### Testing
- Follow the Arrange-Act-Assert convention for tests.
- Name test variables clearly.
- Follow the convention: inputX, mockX, actualX, expectedX, etc.
- Write unit tests for each public function.
- Use test doubles to simulate dependencies.
- Except for third-party dependencies that are not expensive to execute.
- Write acceptance tests for each module.
- Follow the Given-When-Then convention.
## Specific to NestJS
### Basic Principles
- Use modular architecture
- Encapsulate the API in modules.
- One module per main domain/route.
- One controller for its route.
- And other controllers for secondary routes.
- A models folder with data types.
- DTOs validated with class-validator for inputs.
- Declare simple types for outputs.
- A services module with business logic and persistence.
- One service per entity.
- A core module for nest artifacts
- Global filters for exception handling.
- Global middlewares for request management.
- Guards for permission management.
- Interceptors for request management.
- A shared module for services shared between modules.
- Utilities
- Shared business logic
### Testing
- Use the standard Jest framework for testing.
- Write tests for each controller and service.
- Write end to end tests for each api module.
- Add a admin/test method to each controller as a smoke test.
---
description: Enforces specific guidelines for the core module in NestJS, focusing on global filters, middleware, guards, and interceptors.
globs: src/core/**/*.*
---
- Global filters for exception handling.
- Global middlewares for request management.
- Guards for permission management.
- Interceptors for request management.
---
description: Specifies NestJS-specific architectural principles, modular design, and testing practices within the 'src' directory.
globs: src/**/*.*
---
- Use modular architecture
- Encapsulate the API in modules.
- One module per main domain/route.
- One controller for its route.
- And other controllers for secondary routes.
- A models folder with data types.
- DTOs validated with class-validator for inputs.
- Declare simple types for outputs.
- A services module with business logic and persistence.
- One service per entity.
- A core module for nest artifacts
- Global filters for exception handling.
- Global middlewares for request management.
- Guards for permission management.
- Interceptors for request management.
- A shared module for services shared between modules.
- Utilities
- Shared business logic
- Use the standard Jest framework for testing.
- Write tests for each controller and service.
- Write end to end tests for each api module.
- Add a admin/test method to each controller as a smoke test.
---
description: Prescribes the structure and components within NestJS modules, including controllers, models, DTOs, and services, ensuring API encapsulation.
globs: src/modules/**/*.*
---
- One module per main domain/route.
- One controller for its route.
- And other controllers for secondary routes.
- A models folder with data types.
- DTOs validated with class-validator for inputs.
- Declare simple types for outputs.
- A services module with business logic and persistence.
- One service per entity.
---
description: Defines standards for the shared module in NestJS, emphasizing utilities and shared business logic accessible across modules.
globs: src/shared/**/*.*
---
- Utilities
- Shared business logic
---
description: Sets standards for testing NestJS applications, including unit, integration, and end-to-end tests, plus the use of Jest.
globs: **/*.spec.ts
---
- Use the standard Jest framework for testing.
- Write tests for each controller and service.
- Write end to end tests for each api module.
- Add a admin/test method to each controller as a smoke test.
---
description: Applies general TypeScript coding standards across the project, including naming conventions, function structure, data handling, and exception handling.
globs: **/*.ts
---
- Use English for all code and documentation.
- Always declare the type of each variable and function (parameters and return value).
- Avoid using any.
- Create necessary types.
- Use JSDoc to document public classes and methods.
- Don't leave blank lines within a function.
- One export per file.
- Use PascalCase for classes.
- Use camelCase for variables, functions, and methods.
- Use kebab-case for file and directory names.
- Use UPPERCASE for environment variables.
- Avoid magic numbers and define constants.
- Start each function with a verb.
- Use verbs for boolean variables. Example: isLoading, hasError, canDelete, etc.
- Use complete words instead of abbreviations and correct spelling.
- Except for standard abbreviations like API, URL, etc.
- Except for well-known abbreviations:
- i, j for loops
- err for errors
- ctx for contexts
- req, res, next for middleware function parameters
- Write short functions with a single purpose. Less than 20 instructions.
- Name functions with a verb and something else.
- If it returns a boolean, use isX or hasX, canX, etc.
- If it doesn't return anything, use executeX or saveX, etc.
- Avoid nesting blocks by:
- Early checks and returns.
- Extraction to utility functions.
- Use higher-order functions (map, filter, reduce, etc.) to avoid function nesting.
- Use arrow functions for simple functions (less than 3 instructions).
- Use named functions for non-simple functions.
- Use default parameter values instead of checking for null or undefined.
- Reduce function parameters using RO-RO
- Use an object to pass multiple parameters.
- Use an object to return results.
- Declare necessary types for input arguments and output.
- Use a single level of abstraction.
- Don't abuse primitive types and encapsulate data in composite types.
- Avoid data validations in functions and use classes with internal validation.
- Prefer immutability for data.
- Use readonly for data that doesn't change.
- Use as const for literals that don't change.
- Follow SOLID principles.
- Prefer composition over inheritance.
- Declare interfaces to define contracts.
- Write small classes with a single purpose.
- Less than 200 instructions.
- Less than 10 public methods.
- Less than 10 properties.
- Use exceptions to handle errors you don't expect.
- If you catch an exception, it should be to:
- Fix an expected problem.
- Add context.
- Otherwise, use a global handler.
- Follow the Arrange-Act-Assert convention for tests.
- Name test variables clearly.
- Follow the convention: inputX, mockX, actualX, expectedX, etc.
- Write unit tests for each public function.
- Use test doubles to simulate dependencies.
- Except for third-party dependencies that are not expensive to execute.
- Write acceptance tests for each module.
- Follow the Given-When-Then convention.

View File

@@ -10,7 +10,7 @@ export default tseslint.config(
}, },
eslint.configs.recommended, eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked, ...tseslint.configs.recommendedTypeChecked,
// eslintPluginPrettierRecommended, eslintPluginPrettierRecommended,
{ {
languageOptions: { languageOptions: {
globals: { globals: {
@@ -29,7 +29,6 @@ export default tseslint.config(
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn', '@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn' '@typescript-eslint/no-unsafe-argument': 'warn'
'@typescript-eslint/no-unsafe-return': 'off',
}, },
}, },
); );

7599
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{ {
"name": "nest-base", "name": "base_nestjs",
"version": "0.0.1", "version": "0.0.1",
"description": "", "description": "",
"author": "", "author": "",
@@ -20,36 +20,13 @@
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@hapi/joi": "^17.1.1",
"@neondatabase/serverless": "^1.0.0",
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.0",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.2.0",
"@nestjs/typeorm": "^11.0.0",
"@types/bcrypt": "^5.0.2",
"@types/cookie-parser": "^1.4.8",
"@types/hapi__joi": "^17.1.15",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"@types/uuid": "^10.0.0",
"aws-sdk": "^2.1692.0",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.2", "class-validator": "^0.14.2",
"cookie-parser": "^1.4.7",
"joi": "^17.13.3",
"multer": "^1.4.5-lts.2",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"pg": "^8.15.6",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1"
"uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.2.0", "@eslint/eslintrc": "^3.2.0",
@@ -57,18 +34,16 @@
"@nestjs/cli": "^11.0.0", "@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0", "@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1", "@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.6.0", "@types/express": "^5.0.0",
"@swc/core": "^1.10.7", "@types/jest": "^30.0.0",
"@types/express": "^5.0.1",
"@types/jest": "^29.5.14",
"@types/multer": "^1.4.12",
"@types/node": "^22.10.7", "@types/node": "^22.10.7",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"eslint": "^9.26.0", "eslint": "^9.18.0",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.4.0", "eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0", "globals": "^16.0.0",
"jest": "^29.7.0", "jest": "^30.0.0",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"supertest": "^7.0.0", "supertest": "^7.0.0",
"ts-jest": "^29.2.5", "ts-jest": "^29.2.5",

View File

@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@@ -1,49 +1,10 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm'; import { AuthenticationModule } from './authentication/authentication.module';
import { ConfigModule, ConfigService } from '@nestjs/config';
import * as Joi from 'joi';
import {UsersModule} from "./users/user.module";
import {AuthenticationModule} from "./authentication/authentication.module";
import { PostsModule } from './posts/posts.module';
import { CategoriesModule } from './categories/categories.module';
import {FilesModule} from "./files/file.module";
@Module({ @Module({
imports: [ imports: [AuthenticationModule],
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get<string>('POSTGRES_HOST'),
port: parseInt(configService.get<string>('POSTGRES_PORT', '5432'), 10),
username: configService.get<string>('POSTGRES_USER'),
password: configService.get<string>('POSTGRES_PASSWORD'),
database: configService.get<string>('POSTGRES_DB'),
autoLoadEntities: true,
ssl: {
rejectUnauthorized: false, // Needed for Neon and similar managed DBs
},
synchronize: true, // Disable in production
}),
}),
ConfigModule.forRoot({
validationSchema: Joi.object({
JWT_SECRET: Joi.string().required(),
JWT_EXPIRATION_TIME: Joi.string().required(),
S3_BUCKET: Joi.string().required(),
S3_ACCESS_KEY: Joi.string().required(),
S3_ENDPOINT: Joi.string().required(),
})
}),
UsersModule,
AuthenticationModule,
PostsModule,
CategoriesModule,
FilesModule,
],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],
}) })

View File

@@ -0,0 +1,65 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UnauthorizedException } from '@nestjs/common';
import { AuthenticationController } from './authentication.controller';
import { AuthenticationService } from './authentication.service';
import { LoginDto } from './dto/login.dto';
describe('AuthenticationController', () => {
let controller: AuthenticationController;
let service: AuthenticationService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthenticationController],
providers: [AuthenticationService],
}).compile();
controller = module.get<AuthenticationController>(AuthenticationController);
service = module.get<AuthenticationService>(AuthenticationService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('login', () => {
it('should return successful login response with valid credentials', async () => {
const inputLoginDto: LoginDto = {
email: 'test@example.com',
password: 'password123',
};
const expectedResponse = {
message: 'Login successful',
user: {
email: 'test@example.com',
},
token: 'placeholder-token',
};
const actualResponse = await controller.login(inputLoginDto);
expect(actualResponse).toEqual(expectedResponse);
});
it('should throw UnauthorizedException with invalid credentials', async () => {
const inputLoginDto: LoginDto = {
email: 'invalid@example.com',
password: 'wrongpassword',
};
await expect(controller.login(inputLoginDto)).rejects.toThrow(
UnauthorizedException,
);
});
});
describe('test', () => {
it('should return test response', async () => {
const actualResponse = await controller.test();
expect(actualResponse).toHaveProperty('message');
expect(actualResponse).toHaveProperty('timestamp');
expect(actualResponse.message).toBe('Authentication controller is working');
});
});
});

View File

@@ -1,90 +1,35 @@
import { import { Body, Controller, Post, HttpCode, HttpStatus } from '@nestjs/common';
Body, import { AuthenticationService, LoginResponse } from './authentication.service';
Req, import { LoginDto } from './dto/login.dto';
Controller,
HttpCode,
Post,
UseGuards,
Get,
ClassSerializerInterceptor,
UseInterceptors, SerializeOptions,
} from '@nestjs/common';
import {AuthenticationService} from './authentication.service';
import RegisterDto from './dto/register.dto';
import RequestWithUser from './requestWithUser.interface';
import {LocalAuthenticationGuard} from './localAuthentication.guard';
import JwtAuthenticationGuard from './jwt-authentication.guard';
// import {EmailConfirmationService} from '../emailConfirmation/emailConfirmation.service';
import {ApiBody} from '@nestjs/swagger';
import LogInDto from './dto/logIn.dto';
import { UsersService } from 'src/users/user.service';
@Controller('authentication') /**
@UseInterceptors(ClassSerializerInterceptor) * Authentication controller for handling user authentication endpoints
@SerializeOptions({ */
strategy: 'excludeAll' @Controller('auth')
})
export class AuthenticationController { export class AuthenticationController {
constructor( constructor(private readonly authenticationService: AuthenticationService) {}
private readonly authenticationService: AuthenticationService,
private readonly usersService: UsersService, /**
// private readonly emailConfirmationService: EmailConfirmationService, * Login endpoint for user authentication
) { * @param loginDto - Login credentials containing email and password
* @returns Promise<LoginResponse> - Authentication response
*/
@Post('login')
@HttpCode(HttpStatus.OK)
async login(@Body() loginDto: LoginDto): Promise<LoginResponse> {
return this.authenticationService.login(loginDto);
} }
@Post('register') /**
async register(@Body() registrationData: RegisterDto) { * Admin test endpoint for smoke testing
return this.authenticationService.register(registrationData); * @returns Object - Test response
*/
@Post('admin/test')
@HttpCode(HttpStatus.OK)
async test(): Promise<{ message: string; timestamp: string }> {
return {
message: 'Authentication controller is working',
timestamp: new Date().toISOString(),
};
} }
@HttpCode(200)
@UseGuards(LocalAuthenticationGuard)
@Post('log-in')
@ApiBody({type: LogInDto})
async logIn(@Req() request: RequestWithUser) {
const {user} = request;
const accessTokenCookie = this.authenticationService.getCookieWithJwtAccessToken(
user.id,
);
const {
cookie: refreshTokenCookie,
token: refreshToken,
} = this.authenticationService.getCookieWithJwtRefreshToken(user.id);
await this.usersService.setCurrentRefreshToken(refreshToken, user.id);
request.res.setHeader('Set-Cookie', [
accessTokenCookie,
refreshTokenCookie,
]);
if (user.isTwoFactorAuthenticationEnabled) {
return;
}
return user;
}
@UseGuards(JwtAuthenticationGuard)
@Post('log-out')
@HttpCode(200)
async logOut(@Req() request: RequestWithUser) {
await this.usersService.removeRefreshToken(request.user.id);
request.res.setHeader(
'Set-Cookie',
this.authenticationService.getCookiesForLogOut(),
);
}
@UseGuards(JwtAuthenticationGuard)
@Get()
authenticate(@Req() request: RequestWithUser) {
const user = request.user;
user.password = undefined;
return user;
}
} }

View File

@@ -1,31 +1,13 @@
import {Module} from '@nestjs/common'; import { Module } from '@nestjs/common';
import {AuthenticationService} from './authentication.service'; import { AuthenticationController } from './authentication.controller';
import {AuthenticationController} from './authentication.controller'; import { AuthenticationService } from './authentication.service';
import {PassportModule} from '@nestjs/passport';
import {LocalStrategy} from './local.strategy';
import {ConfigModule, ConfigService} from '@nestjs/config';
import {JwtModule} from '@nestjs/jwt';
import {JwtStrategy} from "./jwt.strategy";
import {UsersModule} from "../users/user.module";
/**
* Authentication module for handling user authentication
*/
@Module({ @Module({
imports: [UsersModule, PassportModule, controllers: [AuthenticationController],
providers: [AuthenticationService],
ConfigModule, exports: [AuthenticationService],
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get('JWT_SECRET'),
signOptions: {
expiresIn: `${configService.get('JWT_EXPIRATION_TIME')}s`,
},
}),
}),
] as const,
providers: [AuthenticationService, LocalStrategy, JwtStrategy] as const,
controllers: [AuthenticationController] as const,
}) })
export class AuthenticationModule { export class AuthenticationModule {}
}

View File

@@ -0,0 +1,62 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UnauthorizedException } from '@nestjs/common';
import { AuthenticationService } from './authentication.service';
import { LoginDto } from './dto/login.dto';
describe('AuthenticationService', () => {
let service: AuthenticationService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthenticationService],
}).compile();
service = module.get<AuthenticationService>(AuthenticationService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('login', () => {
it('should return successful login response with valid credentials', async () => {
const inputLoginDto: LoginDto = {
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);
});
it('should throw UnauthorizedException with invalid email', async () => {
const inputLoginDto: LoginDto = {
email: 'invalid@example.com',
password: 'password123',
};
await expect(service.login(inputLoginDto)).rejects.toThrow(
UnauthorizedException,
);
});
it('should throw UnauthorizedException with invalid password', async () => {
const inputLoginDto: LoginDto = {
email: 'test@example.com',
password: 'wrongpassword',
};
await expect(service.login(inputLoginDto)).rejects.toThrow(
UnauthorizedException,
);
});
});
});

View File

@@ -1,122 +1,58 @@
import {HttpException, HttpStatus, Injectable} from '@nestjs/common'; import { Injectable, UnauthorizedException } from '@nestjs/common';
import RegisterDto from './dto/register.dto'; import { LoginDto } from './dto/login.dto';
import * as bcrypt from 'bcrypt';
import {JwtService} from '@nestjs/jwt';
import {ConfigService} from '@nestjs/config';
import {UsersService} from "../users/user.service";
import PostgresErrorCode from 'src/database/postgresErrorCodes.enum';
/**
* Response interface for successful login
*/
export interface LoginResponse {
message: string;
user: {
email: string;
};
token?: string;
}
/**
* Authentication service for handling user authentication
*/
@Injectable() @Injectable()
export class AuthenticationService { export class AuthenticationService {
constructor( /**
private readonly usersService: UsersService, * Authenticates a user with email and password
private readonly jwtService: JwtService, * @param loginDto - Login credentials containing email and password
private readonly configService: ConfigService, * @returns Promise<LoginResponse> - Authentication response
) { * @throws UnauthorizedException - When credentials are invalid
*/
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 (!isValidUser) {
throw new UnauthorizedException('Invalid email or password');
} }
public async register(registrationData: RegisterDto) {
const hashedPassword = await bcrypt.hash(registrationData.password, 10);
try {
const createdUser = await this.usersService.create({
...registrationData,
password: hashedPassword,
});
createdUser.password = undefined;
return createdUser;
} catch (error) {
if (error?.code === PostgresErrorCode.UniqueViolation) {
throw new HttpException(
'User with that email already exists',
HttpStatus.BAD_REQUEST,
);
}
console.log(error);
throw new HttpException(
'Something went wrong',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
public getCookieWithJwtAccessToken(
userId: number,
isSecondFactorAuthenticated = false,
) {
const payload: TokenPayload = {userId, isSecondFactorAuthenticated};
const token = this.jwtService.sign(payload, {
secret: this.configService.get('JWT_ACCESS_TOKEN_SECRET'),
expiresIn: `${this.configService.get(
'JWT_ACCESS_TOKEN_EXPIRATION_TIME',
)}s`,
});
return `Authentication=${token}; HttpOnly; Path=/; Max-Age=${this.configService.get(
'JWT_ACCESS_TOKEN_EXPIRATION_TIME',
)}`;
}
public getCookieWithJwtRefreshToken(userId: number) {
const payload: TokenPayload = {userId};
const token = this.jwtService.sign(payload, {
secret: this.configService.get('JWT_REFRESH_TOKEN_SECRET'),
expiresIn: this.configService.get('JWT_ACCESS_TOKEN_EXPIRATION_TIME'),
});
const cookie = `Refresh=${token}; HttpOnly; Path=/; Max-Age=${this.configService.get(
'JWT_REFRESH_TOKEN_EXPIRATION_TIME',
)}`;
return { return {
cookie, message: 'Login successful',
token, user: {
email,
},
// TODO: Generate JWT token
token: 'placeholder-token',
}; };
} }
public getCookiesForLogOut() { /**
return [ * Validates user credentials
'Authentication=; HttpOnly; Path=/; Max-Age=0', * @param email - User email
'Refresh=; HttpOnly; Path=/; Max-Age=0', * @param password - User password
]; * @returns Promise<boolean> - Whether credentials are valid
} */
private async validateUser(email: string, password: string): Promise<boolean> {
public async getAuthenticatedUser(email: string, plainTextPassword: string) { // TODO: Implement actual user validation against database
try { // For now, using placeholder validation
const user = await this.usersService.getByEmail(email); return email === 'test@example.com' && password === 'password123';
await this.verifyPassword(plainTextPassword, user.password);
return user;
} catch (error) {
throw new HttpException(
'Wrong credentials provided',
HttpStatus.BAD_REQUEST,
);
}
}
private async verifyPassword(
plainTextPassword: string,
hashedPassword: string,
) {
const isPasswordMatching = await bcrypt.compare(
plainTextPassword,
hashedPassword,
);
if (!isPasswordMatching) {
throw new HttpException(
'Wrong credentials provided',
HttpStatus.BAD_REQUEST,
);
}
}
public async getUserFromAuthenticationToken(token: string) {
const payload: TokenPayload = this.jwtService.verify(token, {
secret: this.configService.get('JWT_ACCESS_TOKEN_SECRET'),
});
if (payload.userId) {
return this.usersService.getById(payload.userId);
}
}
public getCookieForLogOut() {
return `Authentication=; HttpOnly; Path=/; Max-Age=0`;
} }
} }

View File

@@ -1,13 +1,15 @@
import { IsEmail, IsString, IsNotEmpty, MinLength } from 'class-validator'; import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
export class LogInDto { /**
@IsEmail() * Data Transfer Object for user login
*/
export class LoginDto {
@IsEmail({}, { message: 'Please provide a valid email address' })
@IsNotEmpty({ message: 'Email is required' })
email: string; email: string;
@IsString() @IsString({ message: 'Password must be a string' })
@IsNotEmpty() @IsNotEmpty({ message: 'Password is required' })
@MinLength(7) @MinLength(6, { message: 'Password must be at least 6 characters long' })
password: string; password: string;
} }
export default LogInDto;

View File

@@ -1,39 +0,0 @@
import {
IsEmail,
IsString,
IsNotEmpty,
MinLength,
Matches,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class RegisterDto {
@IsEmail()
email: string;
@IsString()
@IsNotEmpty()
name: string;
@ApiProperty({
deprecated: true,
description: 'Use the name property instead',
})
fullName: string;
@IsString()
@IsNotEmpty()
@MinLength(7)
password: string;
@ApiProperty({
description: 'Has to match a regular expression: /^\\+[1-9]\\d{1,14}$/',
example: '+123123123123',
})
@IsString()
@IsNotEmpty()
@Matches(/^\+[1-9]\d{1,14}$/)
phoneNumber: string;
}
export default RegisterDto;

View File

@@ -1,5 +0,0 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export default class JwtAuthenticationGuard extends AuthGuard('jwt') {}

View File

@@ -1,25 +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 'src/users/user.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private readonly configService: ConfigService,
private readonly userService: UsersService,
) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([(request: Request): string | null => {
return request?.cookies?.Authentication || null;
}]),
secretOrKey: configService.get('JWT_SECRET')
});
}
async validate(payload: TokenPayload) {
return this.userService.getById(payload.userId);
}
}

View File

@@ -1,17 +0,0 @@
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { AuthenticationService } from './authentication.service';
import User from '../users/entities/user.entity';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authenticationService: AuthenticationService) {
super({
usernameField: 'email'
});
}
async validate(email: string, password: string): Promise<User> {
return this.authenticationService.getAuthenticatedUser(email, password);
}
}

View File

@@ -1,5 +0,0 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthenticationGuard extends AuthGuard('local') {}

View File

@@ -1,8 +0,0 @@
import { Request } from 'express';
import User from '../users/entities/user.entity';
interface RequestWithUser extends Request {
user: User;
}
export default RequestWithUser;

View File

@@ -1,5 +0,0 @@
interface TokenPayload {
userId: number;
isSecondFactorAuthenticated?: boolean;
}

View File

@@ -1,34 +0,0 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { CategoriesService } from './categories.service';
import { CreateCategoryDto } from './dto/create-category.dto';
import { UpdateCategoryDto } from './dto/update-category.dto';
@Controller('categories')
export class CategoriesController {
constructor(private readonly categoriesService: CategoriesService) {}
@Post()
create(@Body() createCategoryDto: CreateCategoryDto) {
return this.categoriesService.create(createCategoryDto);
}
@Get()
findAll() {
return this.categoriesService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.categoriesService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateCategoryDto: UpdateCategoryDto) {
return this.categoriesService.update(+id, updateCategoryDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.categoriesService.remove(+id);
}
}

View File

@@ -1,16 +0,0 @@
import {Module} from '@nestjs/common';
import {CategoriesService} from './categories.service';
import {CategoriesController} from './categories.controller';
import {TypeOrmModule} from "@nestjs/typeorm";
import Post from "../posts/entities/post.entity";
import Category from "./entities/category.entity";
@Module({
controllers: [CategoriesController],
imports: [TypeOrmModule.forFeature([Category])],
providers: [CategoriesService],
exports: [CategoriesService],
})
export class CategoriesModule {
}

View File

@@ -1,70 +0,0 @@
import { Injectable } from '@nestjs/common';
import { CreateCategoryDto } from './dto/create-category.dto';
import { UpdateCategoryDto } from './dto/update-category.dto';
import {InjectRepository} from "@nestjs/typeorm";
import Post from "../posts/entities/post.entity";
import {Repository} from "typeorm";
import Category from "./entities/category.entity";
import {CategoryNotFoundException} from "./exception/categoryNotFound.exception";
@Injectable()
export class CategoriesService {
constructor(
@InjectRepository(Category) private repo: Repository<Category>,
) {
}
getAllCategories() {
return this.repo.find({ relations: ['posts'] });
}
async getCategoryById(id: number) {
const category = await this.repo.findOne({
where: { id },
relations: {
posts: true,
}
});
if (category) {
return category;
}
throw new CategoryNotFoundException(id);
}
async updateCategory(id: number, category: UpdateCategoryDto) {
await this.repo.update(id, category);
const updatedCategory = await this.repo.findOne({
where: { id },
relations: {
posts: true,
}
});
if (updatedCategory) {
return updatedCategory
}
throw new CategoryNotFoundException(id);
}
create(createCategoryDto: CreateCategoryDto) {
return 'This action adds a new category';
}
findAll() {
return `This action returns all categories`;
}
findOne(id: number) {
return `This action returns a #${id} category`;
}
update(id: number, updateCategoryDto: UpdateCategoryDto) {
return `This action updates a #${id} category`;
}
remove(id: number) {
return `This action removes a #${id} category`;
}
}

View File

@@ -1 +0,0 @@
export class CreateCategoryDto {}

View File

@@ -1,4 +0,0 @@
import { PartialType } from '@nestjs/swagger';
import { CreateCategoryDto } from './create-category.dto';
export class UpdateCategoryDto extends PartialType(CreateCategoryDto) {}

View File

@@ -1,16 +0,0 @@
import {Column, Entity, ManyToMany, PrimaryGeneratedColumn} from 'typeorm';
import Post from "../../posts/entities/post.entity";
@Entity()
class Category {
@PrimaryGeneratedColumn()
public id: number;
@Column()
public name: string;
@ManyToMany(() => Post, (post: Post) => post.categories)
public posts: Post[];
}
export default Category;

View File

@@ -1,7 +0,0 @@
import { NotFoundException } from '@nestjs/common';
export class CategoryNotFoundException extends NotFoundException {
constructor(categoryId: number) {
super(`Category with id ${categoryId} not found`);
}
}

View File

@@ -1,11 +0,0 @@
export interface File {
fieldname: string;
originalname: string;
encoding: string;
mimetype: string;
size: number;
destination: string;
filename: string;
path: string;
buffer: Buffer;
}

View File

@@ -1,21 +0,0 @@
export const BUCKET = 'nest-reno';
export const IMAGES = 'images';
export const FILES = 'files';
export const THUMBNAILS = 'thumbnails';
export const ACCESSKEY = '004a8f97aa7c9b90000000004';
export const SECRETKEY = 'K004lokMgbjY5cfVoFwrzl2gD4AW6yk';
const aws = require('aws-sdk');
aws.config.update({
accessKeyId: ACCESSKEY,
secretAccessKey: SECRETKEY
});
const spacesEndpoint = new aws.Endpoint('s3.us-west-004.backblazeb2.com');
export const s3 = new aws.S3({
endpoint: spacesEndpoint
});

View File

@@ -1,4 +0,0 @@
enum PostgresErrorCode {
UniqueViolation = '23505',
}
export default PostgresErrorCode;

View File

@@ -1,12 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FilesService } from './files.service';
import { ConfigModule } from '@nestjs/config';
import PublicFile from './publicFile.entity';
@Module({
imports: [TypeOrmModule.forFeature([PublicFile]), ConfigModule],
providers: [FilesService],
exports: [FilesService],
})
export class FilesModule {}

View File

@@ -1,78 +0,0 @@
import {Injectable} from '@nestjs/common';
import {InjectRepository} from '@nestjs/typeorm';
import {Repository, QueryRunner} from 'typeorm';
import PublicFile from './publicFile.entity';
import {S3} from 'aws-sdk';
import {ConfigService} from '@nestjs/config';
import {v4 as uuid} from 'uuid';
import {File} from "../core/file.interface";
import {s3} from "../core/s3-config";
@Injectable()
export class FilesService {
constructor(
@InjectRepository(PublicFile)
private publicFilesRepository: Repository<PublicFile>,
private readonly configService: ConfigService,
) {
}
async uploadPublicFile(dataBuffer: Buffer, filename: string) {
const uploadResult = await s3.upload({
ACL: 'public-read',
Bucket: 'nest-reno',
Body: dataBuffer,
Key: filename,
})
.promise();
const newFile = this.publicFilesRepository.create({
key: uploadResult.Key,
url: uploadResult.Location,
});
await this.publicFilesRepository.save(newFile);
return newFile;
}
async uploadFile(file: File, folder: String) {
const uploadResult = await s3.upload({
ACL: 'public-read',
Bucket: folder,
Body: file.buffer,
Key: file.originalname,
ContentType: file.mimetype,
})
.promise();
return uploadResult['Location'];
}
async deletePublicFile(fileId: number) {
const file = await this.publicFilesRepository.findOneBy({id: fileId});
await s3
.deleteObject({
Bucket: this.configService.get('S3_BUCKET'),
Key: file.key,
})
.promise();
await this.publicFilesRepository.delete(fileId);
}
async deletePublicFileWithQueryRunner(
fileId: number,
queryRunner: QueryRunner,
) {
const file = await queryRunner.manager.findOneBy(PublicFile, {
id: fileId,
});
const s3 = new S3();
await s3
.deleteObject({
Bucket: this.configService.get('S3_BUCKET'),
Key: file.key,
})
.promise();
await queryRunner.manager.delete(PublicFile, fileId);
}
}

View File

@@ -1,15 +0,0 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
class PublicFile {
@PrimaryGeneratedColumn()
public id: number;
@Column()
public url: string;
@Column()
public key: string;
}
export default PublicFile;

View File

@@ -1,21 +1,8 @@
import {HttpAdapterHost, NestFactory, Reflector} from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import {AppModule} from './app.module'; import { AppModule } from './app.module';
import * as cookieParser from 'cookie-parser';
import {ClassSerializerInterceptor, ValidationPipe} from "@nestjs/common";
import {ExceptionsLoggerFilter} from "./utils/exceptionsLogger.filter";
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
app.use(cookieParser());
app.useGlobalPipes(new ValidationPipe());
const {httpAdapter} = app.get(HttpAdapterHost);
app.useGlobalFilters(new ExceptionsLoggerFilter(httpAdapter));
app.useGlobalInterceptors(new ClassSerializerInterceptor(
app.get(Reflector))
);
await app.listen(process.env.PORT ?? 3000); await app.listen(process.env.PORT ?? 3000);
} }
bootstrap(); bootstrap();

View File

@@ -1 +0,0 @@
export class CreatePostDto {}

View File

@@ -1,4 +0,0 @@
import { PartialType } from '@nestjs/swagger';
import { CreatePostDto } from './create-post.dto';
export class UpdatePostDto extends PartialType(CreatePostDto) {}

View File

@@ -1,29 +0,0 @@
import {Column, Entity, ManyToOne, PrimaryGeneratedColumn, ManyToMany, JoinTable} from 'typeorm';
import User from "../../users/entities/user.entity";
import Category from "../../categories/entities/category.entity";
@Entity()
class Post {
@PrimaryGeneratedColumn()
public id: number;
@Column()
public title: string;
@Column()
public content: string;
@Column({nullable: true})
public category?: string;
@ManyToOne(() => User, (author: User) => author.posts)
public author: User;
@ManyToMany(() => Category, (category: Category) => category.posts)
@JoinTable()
public categories: Category[];
}
export default Post;

View File

@@ -1,7 +0,0 @@
import { NotFoundException } from '@nestjs/common';
export class PostNotFoundException extends NotFoundException {
constructor(postId: number) {
super(`Post with id ${postId} not found`);
}
}

View File

@@ -1,38 +0,0 @@
import {Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Req} from '@nestjs/common';
import {PostsService} from './posts.service';
import {CreatePostDto} from './dto/create-post.dto';
import {UpdatePostDto} from './dto/update-post.dto';
import JwtAuthenticationGuard from "../authentication/jwt-authentication.guard";
import RequestWithUser from "../authentication/requestWithUser.interface";
@Controller('posts')
export class PostsController {
constructor(private readonly postsService: PostsService) {
}
@Post()
@UseGuards(JwtAuthenticationGuard)
async createPost(@Body() post: CreatePostDto, @Req() req: RequestWithUser) {
return this.postsService.createPost(post, req.user);
}
@Get()
findAll() {
return this.postsService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.postsService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updatePostDto: UpdatePostDto) {
return this.postsService.update(+id, updatePostDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.postsService.remove(+id);
}
}

View File

@@ -1,15 +0,0 @@
import {Module} from '@nestjs/common';
import {PostsService} from './posts.service';
import {PostsController} from './posts.controller';
import {TypeOrmModule} from "@nestjs/typeorm";
import User from "../users/entities/user.entity";
import Post from "./entities/post.entity";
@Module({
controllers: [PostsController],
imports: [TypeOrmModule.forFeature([Post])],
providers: [PostsService],
exports: [PostsService],
})
export class PostsModule {
}

View File

@@ -1,74 +0,0 @@
import {Injectable} from '@nestjs/common';
import {CreatePostDto} from './dto/create-post.dto';
import {UpdatePostDto} from './dto/update-post.dto';
import User from "../users/entities/user.entity";
import {InjectRepository} from "@nestjs/typeorm";
import {Repository} from "typeorm";
import Post from './entities/post.entity';
import {PostNotFoundException} from "./exception/postNotFound.exception";
@Injectable()
export class PostsService {
constructor(
@InjectRepository(Post) private repo: Repository<Post>,
) {
}
async createPost(post: CreatePostDto, user: User) {
const newPost = this.repo.create({
...post,
author: user
});
await this.repo.save(newPost);
return newPost;
}
getAllPosts() {
return this.repo.find({relations: ['author']});
}
async getPostById(id: number) {
const post = await this.repo.findOne(
{
where: {id},
relations: {author: true}
}
);
if (post) {
return post;
}
throw new PostNotFoundException(id);
}
async updatePost(id: number, post: UpdatePostDto) {
await this.repo.update(id, post);
const updatedPost = await this.repo.findOne({
where: {id},
relations: {author: true}
});
if (updatedPost) {
return updatedPost
}
throw new PostNotFoundException(id);
}
findAll() {
return `This action returns all posts`;
}
findOne(id: number) {
return `This action returns a #${id} post`;
}
update(id: number, updatePostDto: UpdatePostDto) {
return `This action updates a #${id} post`;
}
remove(id: number) {
return `This action removes a #${id} post`;
}
}

View File

@@ -1,7 +0,0 @@
export class CreateUserDto {
email: string;
name: string;
password: string;
}
export default CreateUserDto;

View File

@@ -1,22 +0,0 @@
import {Column, Entity, OneToOne, PrimaryGeneratedColumn} from 'typeorm';
import User from "./user.entity";
@Entity()
class Address {
@PrimaryGeneratedColumn()
public id: number;
@Column()
public street: string;
@Column()
public city: string;
@Column()
public country: string;
@OneToOne(() => User, (user: User) => user.address)
public user: User;
}
export default Address;

View File

@@ -1,55 +0,0 @@
import {Column, Entity, PrimaryGeneratedColumn, OneToOne, JoinColumn, OneToMany} from 'typeorm';
import {Exclude, Expose} from 'class-transformer';
import Address from "./address.entity";
import Post from "../../posts/entities/post.entity";
import PublicFile from "../../files/publicFile.entity";
@Entity()
class User {
@PrimaryGeneratedColumn()
public id?: number;
@Column({unique: true})
@Expose()
public email: string;
@Column()
@Expose()
public name: string;
@Column()
public password: string;
@OneToOne(() => Address, {
eager: true,
cascade: true
})
@JoinColumn()
public address: Address;
@OneToMany(() => Post, (post: Post) => post.author)
public posts: Post[];
@JoinColumn()
@OneToOne(
() => PublicFile,
{
eager: true,
nullable: true
}
)
public avatar?: PublicFile;
@Column({
nullable: true,
})
@Exclude()
public currentHashedRefreshToken?: string;
@Column({default: false})
public isTwoFactorAuthenticationEnabled: boolean;
}
export default User;

View File

@@ -1,21 +0,0 @@
import { Controller, Post, Req, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common';
import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard';
import RequestWithUser from '../authentication/requestWithUser.interface';
import { FileInterceptor } from '@nestjs/platform-express';
import { Express } from 'express';
import {UsersService} from "./user.service";
import 'multer';
@Controller('user')
export class UsersController {
constructor(
private readonly usersService: UsersService,
) {}
@Post('avatar')
@UseGuards(JwtAuthenticationGuard)
@UseInterceptors(FileInterceptor('file'))
async addAvatar(@Req() request: RequestWithUser, @UploadedFile() file: Express.Multer.File) {
return this.usersService.addAvatar(request.user.id, file.buffer, file.originalname);
}
}

View File

@@ -1,20 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import User from './entities/user.entity';
import { ConfigModule } from '@nestjs/config';
import {UsersService} from "./user.service";
import {UsersController} from "./user.controller";
import {FilesModule} from "../files/file.module";
import Address from "./entities/address.entity";
@Module({
imports: [
TypeOrmModule.forFeature([User, Address]),
ConfigModule,
FilesModule,
],
providers: [UsersService],
exports: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}

View File

@@ -1,66 +0,0 @@
import {HttpException, HttpStatus, Injectable} from '@nestjs/common';
import {InjectRepository} from '@nestjs/typeorm';
import {Repository} from 'typeorm';
import User from './entities/user.entity';
import CreateUserDto from './dto/createUser.dto';
import * as bcrypt from 'bcrypt';
import {FilesService} from "../files/files.service";
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
private readonly filesService: FilesService
) {
}
async getByEmail(email: string) {
const user = await this.usersRepository.findOne({where: {email}});
if (user) {
return user;
}
throw new HttpException(
'User with this email does not exist',
HttpStatus.NOT_FOUND,
);
}
async getById(id: number) {
const user = await this.usersRepository.findOne({where: {id}});
if (user) {
return user;
}
throw new HttpException('User with this id does not exist', HttpStatus.NOT_FOUND);
}
async addAvatar(userId: number, imageBuffer: Buffer, filename: string) {
const avatar = await this.filesService.uploadPublicFile(imageBuffer, filename);
const user = await this.getById(userId);
await this.usersRepository.update(userId, {
...user,
avatar
});
return avatar;
}
async create(userData: CreateUserDto) {
const newUser = this.usersRepository.create(userData);
await this.usersRepository.save(newUser);
return newUser;
}
async setCurrentRefreshToken(refreshToken: string, userId: number) {
const currentHashedRefreshToken = await bcrypt.hash(refreshToken, 10);
await this.usersRepository.update(userId, {
currentHashedRefreshToken,
});
}
async removeRefreshToken(userId: number) {
return this.usersRepository.update(userId, {
currentHashedRefreshToken: null,
});
}
}

View File

@@ -1,10 +0,0 @@
import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
@Catch()
export class ExceptionsLoggerFilter extends BaseExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
console.log('Exception thrown', exception);
super.catch(exception, host);
}
}

View File

@@ -1,6 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "nodenext",
"moduleResolution": "nodenext",
"resolvePackageJsonExports": true,
"esModuleInterop": true,
"isolatedModules": true,
"declaration": true, "declaration": true,
"removeComments": true, "removeComments": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
@@ -12,11 +16,10 @@
"baseUrl": "./", "baseUrl": "./",
"incremental": true, "incremental": true,
"skipLibCheck": true, "skipLibCheck": true,
"strictNullChecks": false, "strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false, "noImplicitAny": false,
"strictBindCallApply": false, "strictBindCallApply": false,
"forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false
"noFallthroughCasesInSwitch": false,
"lib": ["dom"] // dom.value is required for the browser
} }
} }