add cursor
This commit is contained in:
8
.cursor/rules/nestjs-core-module-guidelines.mdc.mdc
Normal file
8
.cursor/rules/nestjs-core-module-guidelines.mdc.mdc
Normal 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.
|
||||||
26
.cursor/rules/nestjs-general-guidelines.mdc.mdc
Normal file
26
.cursor/rules/nestjs-general-guidelines.mdc.mdc
Normal 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.
|
||||||
12
.cursor/rules/nestjs-module-structure-guidelines.mdc
Normal file
12
.cursor/rules/nestjs-module-structure-guidelines.mdc
Normal 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.
|
||||||
6
.cursor/rules/nestjs-shared-module-guidelines.mdc
Normal file
6
.cursor/rules/nestjs-shared-module-guidelines.mdc
Normal 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
|
||||||
8
.cursor/rules/nestjs-testing-guidelines.mdc
Normal file
8
.cursor/rules/nestjs-testing-guidelines.mdc
Normal 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.
|
||||||
66
.cursor/rules/typescript-general-guidelines.mdc
Normal file
66
.cursor/rules/typescript-general-guidelines.mdc
Normal 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
251
.cursorrules
Normal 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.
|
||||||
@@ -5,31 +5,30 @@ import globals from 'globals';
|
|||||||
import tseslint from 'typescript-eslint';
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{
|
{
|
||||||
ignores: ['eslint.config.mjs'],
|
ignores: ['eslint.config.mjs'],
|
||||||
|
},
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
eslintPluginPrettierRecommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
sourceType: 'commonjs',
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
eslint.configs.recommended,
|
},
|
||||||
...tseslint.configs.recommendedTypeChecked,
|
{
|
||||||
// eslintPluginPrettierRecommended,
|
rules: {
|
||||||
{
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
languageOptions: {
|
'@typescript-eslint/no-floating-promises': 'warn',
|
||||||
globals: {
|
'@typescript-eslint/no-unsafe-argument': 'warn'
|
||||||
...globals.node,
|
|
||||||
...globals.jest,
|
|
||||||
},
|
|
||||||
sourceType: 'commonjs',
|
|
||||||
parserOptions: {
|
|
||||||
projectService: true,
|
|
||||||
tsconfigRootDir: import.meta.dirname,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
'@typescript-eslint/no-floating-promises': 'warn',
|
|
||||||
'@typescript-eslint/no-unsafe-argument': 'warn'
|
|
||||||
'@typescript-eslint/no-unsafe-return': 'off',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
7599
package-lock.json
generated
7599
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
43
package.json
43
package.json
@@ -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",
|
||||||
|
|||||||
22
src/app.controller.spec.ts
Normal file
22
src/app.controller.spec.ts
Normal 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!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,50 +1,11 @@
|
|||||||
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({
|
controllers: [AppController],
|
||||||
imports: [ConfigModule],
|
providers: [AppService],
|
||||||
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],
|
|
||||||
providers: [AppService],
|
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
65
src/authentication/authentication.controller.spec.ts
Normal file
65
src/authentication/authentication.controller.spec.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('register')
|
/**
|
||||||
async register(@Body() registrationData: RegisterDto) {
|
* Login endpoint for user authentication
|
||||||
return this.authenticationService.register(registrationData);
|
* @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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
@HttpCode(200)
|
* Admin test endpoint for smoke testing
|
||||||
@UseGuards(LocalAuthenticationGuard)
|
* @returns Object - Test response
|
||||||
@Post('log-in')
|
*/
|
||||||
@ApiBody({type: LogInDto})
|
@Post('admin/test')
|
||||||
async logIn(@Req() request: RequestWithUser) {
|
@HttpCode(HttpStatus.OK)
|
||||||
const {user} = request;
|
async test(): Promise<{ message: string; timestamp: string }> {
|
||||||
const accessTokenCookie = this.authenticationService.getCookieWithJwtAccessToken(
|
return {
|
||||||
user.id,
|
message: 'Authentication controller is working',
|
||||||
);
|
timestamp: new Date().toISOString(),
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 {}
|
||||||
}
|
|
||||||
|
|||||||
62
src/authentication/authentication.service.spec.ts
Normal file
62
src/authentication/authentication.service.spec.ts
Normal 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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Login successful',
|
||||||
|
user: {
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
// TODO: Generate JWT token
|
||||||
|
token: 'placeholder-token',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public async register(registrationData: RegisterDto) {
|
/**
|
||||||
const hashedPassword = await bcrypt.hash(registrationData.password, 10);
|
* Validates user credentials
|
||||||
try {
|
* @param email - User email
|
||||||
const createdUser = await this.usersService.create({
|
* @param password - User password
|
||||||
...registrationData,
|
* @returns Promise<boolean> - Whether credentials are valid
|
||||||
password: hashedPassword,
|
*/
|
||||||
});
|
private async validateUser(email: string, password: string): Promise<boolean> {
|
||||||
createdUser.password = undefined;
|
// TODO: Implement actual user validation against database
|
||||||
return createdUser;
|
// For now, using placeholder validation
|
||||||
} catch (error) {
|
return email === 'test@example.com' && password === 'password123';
|
||||||
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 {
|
|
||||||
cookie,
|
|
||||||
token,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCookiesForLogOut() {
|
|
||||||
return [
|
|
||||||
'Authentication=; HttpOnly; Path=/; Max-Age=0',
|
|
||||||
'Refresh=; HttpOnly; Path=/; Max-Age=0',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getAuthenticatedUser(email: string, plainTextPassword: string) {
|
|
||||||
try {
|
|
||||||
const user = await this.usersService.getByEmail(email);
|
|
||||||
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`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export default class JwtAuthenticationGuard extends AuthGuard('jwt') {}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class LocalAuthenticationGuard extends AuthGuard('local') {}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { Request } from 'express';
|
|
||||||
import User from '../users/entities/user.entity';
|
|
||||||
|
|
||||||
interface RequestWithUser extends Request {
|
|
||||||
user: User;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RequestWithUser;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
interface TokenPayload {
|
|
||||||
userId: number;
|
|
||||||
isSecondFactorAuthenticated?: boolean;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 {
|
|
||||||
}
|
|
||||||
@@ -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`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export class CreateCategoryDto {}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { PartialType } from '@nestjs/swagger';
|
|
||||||
import { CreateCategoryDto } from './create-category.dto';
|
|
||||||
|
|
||||||
export class UpdateCategoryDto extends PartialType(CreateCategoryDto) {}
|
|
||||||
@@ -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;
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { NotFoundException } from '@nestjs/common';
|
|
||||||
|
|
||||||
export class CategoryNotFoundException extends NotFoundException {
|
|
||||||
constructor(categoryId: number) {
|
|
||||||
super(`Category with id ${categoryId} not found`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
});
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
enum PostgresErrorCode {
|
|
||||||
UniqueViolation = '23505',
|
|
||||||
}
|
|
||||||
export default PostgresErrorCode;
|
|
||||||
@@ -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 {}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
21
src/main.ts
21
src/main.ts
@@ -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());
|
await app.listen(process.env.PORT ?? 3000);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export class CreatePostDto {}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { PartialType } from '@nestjs/swagger';
|
|
||||||
import { CreatePostDto } from './create-post.dto';
|
|
||||||
|
|
||||||
export class UpdatePostDto extends PartialType(CreatePostDto) {}
|
|
||||||
@@ -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;
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { NotFoundException } from '@nestjs/common';
|
|
||||||
|
|
||||||
export class PostNotFoundException extends NotFoundException {
|
|
||||||
constructor(postId: number) {
|
|
||||||
super(`Post with id ${postId} not found`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 {
|
|
||||||
}
|
|
||||||
@@ -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`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export class CreateUserDto {
|
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CreateUserDto;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 {}
|
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user