├── .env.example ├── .github └── dependabot.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── nest-cli.json ├── package.json ├── renovate.json ├── src ├── app.module.ts ├── config │ └── validation.ts ├── libs │ ├── ai │ │ └── open.ai │ │ │ ├── models │ │ │ ├── open.ai.choice.ts │ │ │ ├── open.ai.completion.response.ts │ │ │ ├── open.ai.image.response.ts │ │ │ ├── open.ai.models.list.ts │ │ │ └── open.ai.usage.ts │ │ │ ├── open.ai.module.ts │ │ │ ├── open.ai.service.ts │ │ │ └── services │ │ │ ├── completion.service.ts │ │ │ ├── image.service.ts │ │ │ └── search.service.ts │ ├── database │ │ ├── database.providers.module.ts │ │ ├── index.ts │ │ └── mongo │ │ │ ├── abstract.repository.ts │ │ │ ├── abstract.schema.ts │ │ │ ├── index.ts │ │ │ ├── mongo.database.module.ts │ │ │ ├── pipes │ │ │ └── mongoose.id.pipe.ts │ │ │ └── serializers │ │ │ └── mongoose.class.serializer.ts │ └── security │ │ ├── encryption │ │ ├── encryption.service.ts │ │ └── index.ts │ │ └── jwt │ │ ├── enums │ │ ├── index.ts │ │ └── jwt.type.enum.ts │ │ ├── index.ts │ │ ├── jwt.module.ts │ │ ├── jwt.service.ts │ │ └── models │ │ ├── index.ts │ │ ├── jwt.model.ts │ │ └── jwt.payload.model.ts ├── main.ts └── modules │ ├── ai │ ├── ai.controller.ts │ ├── ai.module.ts │ ├── ai.service.ts │ └── dto │ │ ├── code.completion.dto.ts │ │ ├── generate.image.dto.ts │ │ └── text.completion.dto.ts │ ├── auth │ ├── auth.controller.ts │ ├── auth.module.ts │ ├── auth.service.ts │ ├── constants.ts │ ├── dto │ │ ├── index.ts │ │ ├── sign-in.dto.ts │ │ └── sign-up.dto.ts │ ├── guards │ │ ├── index.ts │ │ ├── jwt.auth.guard.ts │ │ ├── local.auth.guard.ts │ │ └── user.role.auth.guard.ts │ ├── index.ts │ ├── strategies │ │ ├── index.ts │ │ ├── jwt.auth.strategy.ts │ │ └── local.auth.strategy.ts │ └── types │ │ ├── index.ts │ │ └── request.user.interface.ts │ └── user │ ├── dto │ ├── create.user.dto.ts │ └── index.ts │ ├── enums │ ├── index.ts │ ├── user.role.enum.ts │ └── user.status.enum.ts │ ├── index.ts │ ├── user.controller.ts │ ├── user.module.ts │ ├── user.repository.ts │ ├── user.schema.ts │ └── user.service.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | MONGO_DB_HOST=mongodb://admin:password@mongo:27017 2 | MONGO_DB_NAME=open-ai-nestjs-template 3 | 4 | APP_PORT=3001 5 | APP_ENV=development 6 | 7 | JWT_ACCESS_EXPIRES_IN=7d 8 | JWT_REFRESH_EXPIRES_IN=30d 9 | JWT_ACCESS_SECRET=STRONG-KEY-HERE 10 | JWT_REFRESH_SECRET=ANOTHER-STRONG-KEY-HERE 11 | 12 | OPENAI_API_KEY=OPENAI-KEY-HERE 13 | OPENAI_ORG_ID=OPENAI-ORG-ID-HERE 14 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | assignees: 6 | - "alexberce" 7 | reviewers: 8 | - "alexberce" 9 | pull-request-branch-name: 10 | separator: "-" 11 | schedule: 12 | interval: "daily" 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editors 2 | .idea 3 | 4 | # Dependencies 5 | dist 6 | node_modules 7 | 8 | # Secrets 9 | .env* 10 | !.env.example 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine AS builder 2 | 3 | WORKDIR /app 4 | COPY package*.json ./ 5 | RUN npm install 6 | COPY . . 7 | RUN npm run build 8 | 9 | FROM node:18-alpine 10 | 11 | WORKDIR /app 12 | COPY package.json . 13 | RUN npm install --only-production 14 | COPY --from=builder /app/dist ./dist 15 | 16 | EXPOSE 3000 17 | CMD [ "npm", "run", "start:prod" ] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alexandru Berce 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## OpenAI ChatGPT Microservice 2 | 3 | This template makes it easy to quickly get started building [NestJs](https://github.com/nestjs/nest) microservices that utilize the power of [OpenAI](https://openai.com) and [ChatGPT](https://openai.com/blog/chatgpt/), by providing a preconfigured set of modules and services. 4 | 5 | - REST API with [MongoDB](https://www.mongodb.com) support 6 | - Swagger documentation, [Joi](https://github.com/hapijs/joi) validation 7 | - Folder structure, code samples and best practices 8 | - Crafted for Docker environments (Dockerfile support and environment variables) 9 | 10 | 11 | [![License](https://img.shields.io/github/license/alexberce/openai-nestjs-template.svg)](https://github.com/alexberce/openai-nestjs-template/blob/master/LICENSE) 12 | 13 | ## 1. Requirements 14 | 15 | Before starting, make sure you have the minimum requirements on your workstation. 16 | 17 | - An up-to-date release of NodeJS and Yarn (or npm) 18 | - A MongoDb database (you may use the provided docker-compose file to create one). 19 | 20 | ## 2. Setup 21 | 2.1. Install the dependencies. 22 | 23 | ```bash 24 | $ yarn 25 | ``` 26 | 27 | 2.2. Make a copy of the example environment variables file. 28 | 29 | On Linux systems: 30 | ```bash 31 | $ cp .env.example .env 32 | ``` 33 | On Windows: 34 | ```powershell 35 | $ copy .env.example .env 36 | ``` 37 | 38 | 2.3. Configure your environment variables in the newly created `.env` file. 39 | 40 | For a standard development configuration, you can use the default values and configure only the OpenAi and MongoDb variables. 41 | 42 | > You can find your OpenAI API key [here](https://beta.openai.com/account/api-keys) and the organization id [here](https://beta.openai.com/account/org-settings) 43 | 44 | ## 3. Run the app 45 | ```bash 46 | # development 47 | $ yarn run start 48 | 49 | # watch mode 50 | $ yarn run start:dev 51 | 52 | # production mode 53 | $ yarn run start:prod 54 | ``` 55 | 56 | > You should now be able to access the swagger docs for the API at [http://localhost:3001](http://localhost:3001) 57 | 58 | ## 4. Roadmap 59 | 60 | - [x] List Models 61 | - [x] Text Completion 62 | - [x] Code Completion 63 | - [x] Image Generation 64 | - [ ] Edit Image 65 | - [ ] Model Fine Tuning 66 | - [ ] Content Moderation 67 | 68 | ## 5. Stay in touch 69 | 70 | - Author - [Alexandru Berce](https://www.linkedin.com/in/alexandruberce) 71 | - Website - [https://devaccent.com](https://devaccent.com) 72 | 73 | 74 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | mongo: 4 | image: mongo 5 | container_name: mongo-db 6 | ports: 7 | - '27017-27019:27017-27019' 8 | env_file: 9 | - .env 10 | # networks: 11 | # - template-local-net 12 | volumes: 13 | - mongo:/data/db 14 | 15 | volumes: 16 | mongo: 17 | name: template-db-volume 18 | 19 | networks: 20 | template-local-net: 21 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceRoot": "src", 3 | "collection": "@nestjs/schematics", 4 | "compilerOptions": { 5 | "plugins": ["@nestjs/swagger"], 6 | "assets": ["**/*.js", "**/*.hbs"], 7 | "watchAssets": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-ai-microservice", 3 | "version": "0.0.1", 4 | "description": "NestJs microservice template for OpenAI and ChatGPT", 5 | "author": "alex@devaccent.com", 6 | "private": true, 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "nest build", 10 | "start": "nest start", 11 | "start:dev": "nest start --watch", 12 | "start:prod": "node dist/main" 13 | }, 14 | "engines": { 15 | "node": ">=18.0.0" 16 | }, 17 | "dependencies": { 18 | "@nestjs/common": "^9.2.1", 19 | "@nestjs/config": "^2.2.0", 20 | "@nestjs/core": "^9.2.1", 21 | "@nestjs/jwt": "^10.0.1", 22 | "@nestjs/mongoose": "^9.2.1", 23 | "@nestjs/passport": "^9.0.0", 24 | "@nestjs/platform-express": "^9.2.1", 25 | "@nestjs/swagger": "^6.1.4", 26 | "axios": "^1.1.3", 27 | "bcrypt": "^5.1.0", 28 | "class-transformer": "^0.5.1", 29 | "class-validator": "^0.14.0", 30 | "joi": "^17.7.0", 31 | "mongoose": "^6.8.2", 32 | "nestjs-command": "^3.1.2", 33 | "openai": "^3.1.0", 34 | "passport": "^0.6.0", 35 | "passport-jwt": "^4.0.1", 36 | "passport-local": "^1.0.0", 37 | "reflect-metadata": "^0.1.13", 38 | "rxjs": "^7.2.0", 39 | "swagger-ui-express": "^4.6.0" 40 | }, 41 | "devDependencies": { 42 | "@commitlint/cli": "^17.4.0", 43 | "@commitlint/config-conventional": "^17.4.0", 44 | "@nestjs/cli": "^9.1.8", 45 | "@nestjs/schematics": "^9.0.4", 46 | "@types/express": "^4.17.15", 47 | "@types/node": "^18.11.18", 48 | "@typescript-eslint/eslint-plugin": "^5.48.0", 49 | "@typescript-eslint/parser": "^5.48.0", 50 | "eslint": "^8.31.0", 51 | "eslint-config-prettier": "^8.6.0", 52 | "eslint-plugin-prettier": "^4.2.1", 53 | "lint-staged": "^13.1.0", 54 | "prettier": "^2.8.1", 55 | "ts-loader": "^9.4.2", 56 | "ts-node": "^10.9.1", 57 | "tsconfig-paths": "^4.1.2", 58 | "typescript": "^4.9.4" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "labels": ["dependencies"], 4 | "packageRules": [ 5 | { 6 | "matchDepTypes": ["devDependencies"], 7 | "automerge": true 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | 4 | /** Local Imports **/ 5 | import { UserModule } from "./modules/user"; 6 | import { AuthModule } from "./modules/auth"; 7 | import { AiModule } from "./modules/ai/ai.module"; 8 | 9 | import { validate } from './config/validation'; 10 | 11 | @Module({ 12 | imports: [ 13 | /** Common Modules **/ 14 | ConfigModule.forRoot({ 15 | validate, 16 | isGlobal: true, 17 | }), 18 | 19 | /** App Modules **/ 20 | AiModule, 21 | AuthModule, 22 | UserModule 23 | ], 24 | }) 25 | export default class AppModule {} 26 | -------------------------------------------------------------------------------- /src/config/validation.ts: -------------------------------------------------------------------------------- 1 | import { plainToClass } from 'class-transformer'; 2 | import { IsNotEmpty, IsNumber, IsString, validateSync } from 'class-validator'; 3 | 4 | class EnvironmentVariables { 5 | @IsNumber() 6 | APP_PORT: number; 7 | 8 | @IsString() 9 | @IsNotEmpty() 10 | APP_ENV: 'development' | 'production'; 11 | 12 | @IsString() 13 | OPENAI_ORG_ID: string; 14 | 15 | @IsString() 16 | OPENAI_API_KEY: string; 17 | } 18 | 19 | export function validate(config: Record) { 20 | const validatedConfig = plainToClass(EnvironmentVariables, config, { 21 | enableImplicitConversion: true, 22 | }); 23 | const errors = validateSync(validatedConfig, { 24 | skipMissingProperties: false, 25 | }); 26 | 27 | if (errors.length > 0) { 28 | throw new Error(errors.toString()); 29 | } 30 | 31 | return validatedConfig; 32 | } 33 | -------------------------------------------------------------------------------- /src/libs/ai/open.ai/models/open.ai.choice.ts: -------------------------------------------------------------------------------- 1 | export class OpenAiChoice { 2 | text: string; 3 | index: number; 4 | finish_reason: "stop" | string; 5 | } 6 | -------------------------------------------------------------------------------- /src/libs/ai/open.ai/models/open.ai.completion.response.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | 3 | import { OpenAiUsage } from "./open.ai.usage"; 4 | import { OpenAiChoice } from "./open.ai.choice"; 5 | 6 | const choiceExample = { index: 0, finish_reason: 'stop', text: 'Hello from OpenAi' }; 7 | const usageExample = { prompt_tokens: 90, total_tokens: 100, completion_tokens: 10 } as OpenAiUsage; 8 | 9 | export class OpenAiCompletionResponse { 10 | @ApiProperty() 11 | id: string; 12 | 13 | @ApiProperty() 14 | object: string; 15 | 16 | @ApiProperty() 17 | created: number; 18 | 19 | @ApiProperty() 20 | model: string; 21 | 22 | @ApiProperty({ type: OpenAiChoice, isArray: true, example: [choiceExample] }) 23 | choices: OpenAiChoice[]; 24 | 25 | @ApiProperty({ type: OpenAiUsage, example: usageExample }) 26 | usage: OpenAiUsage; 27 | } 28 | -------------------------------------------------------------------------------- /src/libs/ai/open.ai/models/open.ai.image.response.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | 3 | export type ImageObject = { 4 | src: string; 5 | } 6 | 7 | export class OpenAiImageResponse { 8 | @ApiProperty() 9 | id: string; 10 | 11 | data: ImageObject[] 12 | } 13 | -------------------------------------------------------------------------------- /src/libs/ai/open.ai/models/open.ai.models.list.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | 3 | export class OpenAiModelsList { 4 | @ApiProperty() 5 | id: string; 6 | 7 | @ApiProperty() 8 | object: string; 9 | 10 | @ApiProperty() 11 | created: number; 12 | 13 | @ApiProperty() 14 | owned_by: string; 15 | 16 | @ApiProperty() 17 | root?: string = null; 18 | 19 | @ApiProperty() 20 | parent?: string = null; 21 | } 22 | -------------------------------------------------------------------------------- /src/libs/ai/open.ai/models/open.ai.usage.ts: -------------------------------------------------------------------------------- 1 | export class OpenAiUsage { 2 | "total_tokens": number; 3 | "prompt_tokens": number; 4 | "completion_tokens": number; 5 | } 6 | -------------------------------------------------------------------------------- /src/libs/ai/open.ai/open.ai.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { OpenAiService } from "./open.ai.service"; 4 | 5 | @Module({ 6 | imports: [], 7 | providers: [OpenAiService], 8 | exports: [OpenAiService], 9 | }) 10 | export class OpenAiModule {} 11 | -------------------------------------------------------------------------------- /src/libs/ai/open.ai/open.ai.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from "@nestjs/config"; 3 | 4 | import { OpenAiImageService } from "./services/image.service"; 5 | import { OpenAiModelsList } from "./models/open.ai.models.list"; 6 | import { OpenAiSearchService } from "./services/search.service"; 7 | import { OpenAiCompletionService } from "./services/completion.service"; 8 | 9 | const { Configuration, OpenAIApi } = require("openai"); 10 | 11 | @Injectable() 12 | export class OpenAiService { 13 | private readonly client: typeof OpenAIApi; 14 | 15 | public imageService: OpenAiImageService; 16 | public searchService: OpenAiSearchService; 17 | public completionService: OpenAiCompletionService; 18 | 19 | constructor( 20 | private readonly configService: ConfigService 21 | ) { 22 | const configuration = new Configuration({ 23 | apiKey: configService.get('OPENAI_API_KEY'), 24 | organization: configService.get('OPENAI_ORG_ID'), 25 | }); 26 | 27 | this.client = new OpenAIApi(configuration); 28 | 29 | this.imageService = new OpenAiImageService(this.client); 30 | this.completionService = new OpenAiCompletionService(this.client); 31 | this.searchService = new OpenAiSearchService(this.client); 32 | } 33 | 34 | async listModels (): Promise { 35 | return this.client.listModels().then(response => response.data).then(response => response.data); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/libs/ai/open.ai/services/completion.service.ts: -------------------------------------------------------------------------------- 1 | import { OpenAiCompletionResponse } from "../models/open.ai.completion.response"; 2 | 3 | const { OpenAIApi } = require("openai"); 4 | 5 | export class OpenAiCompletionService { 6 | private readonly DEFAULT_COMPLETION_MODEL = 'text-davinci-003'; 7 | private readonly DEFAULT_CODE_COMPLETION_MODEL = 'code-davinci-002'; 8 | 9 | private readonly MAX_TOKENS = 128; 10 | private readonly MAX_CODE_COMPLETION_TOKENS = 1024; 11 | 12 | constructor( 13 | private readonly service: typeof OpenAIApi 14 | ) {} 15 | 16 | async textCompletion(query: string, modelId: string = this.DEFAULT_COMPLETION_MODEL, maxTokens: number = this.MAX_TOKENS): Promise { 17 | return this.service.createCompletion({ 18 | "prompt": query, 19 | "model": modelId, 20 | "max_tokens": maxTokens, 21 | }).then(response => response.data) as OpenAiCompletionResponse; 22 | } 23 | 24 | async codeCompletion(query: string, modelId: string = this.DEFAULT_CODE_COMPLETION_MODEL, maxTokens: number = this.MAX_CODE_COMPLETION_TOKENS): Promise { 25 | return this.service.createCompletion({ 26 | "prompt": query, 27 | "model": modelId, 28 | "max_tokens": maxTokens, 29 | }).then(response => response.data) as OpenAiCompletionResponse; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/libs/ai/open.ai/services/image.service.ts: -------------------------------------------------------------------------------- 1 | import { CreateImageRequestResponseFormatEnum } from "openai"; 2 | import { OpenAiImageResponse } from "../models/open.ai.image.response"; 3 | 4 | const { OpenAIApi, CreateImageRequestSizeEnum } = require("openai"); 5 | 6 | export class OpenAiImageService { 7 | private readonly DEFAULT_NUMBER_OF_VARIATIONS = 1; 8 | private readonly DEFAULT_IMAGE_SIZE = CreateImageRequestSizeEnum._1024x1024; 9 | private readonly DEFAULT_RESPONSE_FORMAT = CreateImageRequestResponseFormatEnum.Url; 10 | 11 | constructor( 12 | private readonly service: typeof OpenAIApi 13 | ) {} 14 | 15 | async create(query: string, size: string = this.DEFAULT_IMAGE_SIZE, variations: number = this.DEFAULT_NUMBER_OF_VARIATIONS): Promise { 16 | return this.service 17 | .createImage({ 18 | size: size, 19 | prompt: query, 20 | n: variations, 21 | response_format: this.DEFAULT_RESPONSE_FORMAT 22 | }) 23 | .then(response => response.data); 24 | } 25 | 26 | async edit(image: File, mask: File, query: string, size: string = this.DEFAULT_IMAGE_SIZE, variations: number = this.DEFAULT_NUMBER_OF_VARIATIONS): Promise { 27 | return this.service 28 | .createImageEdit(image, mask, query, variations, size, this.DEFAULT_RESPONSE_FORMAT) 29 | .then(response => response.data); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/libs/ai/open.ai/services/search.service.ts: -------------------------------------------------------------------------------- 1 | const { OpenAIApi } = require("openai"); 2 | 3 | export class OpenAiSearchService { 4 | private readonly DEFAULT_SEARCH_ENGINE = 'davinci'; 5 | 6 | constructor( 7 | private readonly service: typeof OpenAIApi 8 | ) {} 9 | 10 | async search(query: string, modelId: string = this.DEFAULT_SEARCH_ENGINE, documents: string[] = null) { 11 | return this.service.createSearch(modelId, { 12 | query: query, 13 | documents: documents, 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/libs/database/database.providers.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { MongoDatabaseModule } from './mongo'; 4 | 5 | @Module({ 6 | imports: [MongoDatabaseModule], 7 | exports: [MongoDatabaseModule], 8 | }) 9 | 10 | export class DatabaseProvidersModule {} 11 | -------------------------------------------------------------------------------- /src/libs/database/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mongo/mongo.database.module'; 2 | -------------------------------------------------------------------------------- /src/libs/database/mongo/abstract.repository.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundException } from '@nestjs/common'; 2 | import { AbstractDocument } from './abstract.schema'; 3 | import { FilterQuery, Model, Types, UpdateQuery, SaveOptions, Connection } from 'mongoose'; 4 | 5 | export abstract class AbstractRepository { 6 | protected constructor( 7 | public readonly model: Model, 8 | private readonly connection: Connection, 9 | ) {} 10 | 11 | async create( 12 | document: Partial>, 13 | options?: SaveOptions, 14 | ): Promise { 15 | const createdDocument = new this.model({ 16 | ...document, 17 | _id: new Types.ObjectId(), 18 | }); 19 | return ( 20 | await createdDocument.save(options) 21 | ).toJSON() as unknown as TDocument; 22 | } 23 | 24 | async findOne(filterQuery: FilterQuery): Promise { 25 | const document = await this.model.findOne(filterQuery); 26 | 27 | if (!document) { 28 | throw new NotFoundException('Document not found.'); 29 | } 30 | 31 | return document; 32 | } 33 | 34 | async findOneAndUpdate( 35 | filterQuery: FilterQuery, 36 | update: UpdateQuery, 37 | ) { 38 | const options = { new: true }; 39 | const document = await this.model.findOneAndUpdate(filterQuery, update, options); 40 | 41 | if (!document) { 42 | throw new NotFoundException('Document not found.'); 43 | } 44 | 45 | return document; 46 | } 47 | 48 | async upsert( 49 | filterQuery: FilterQuery, 50 | document: Partial, 51 | ) { 52 | const options = { upsert: true, new: true }; 53 | return this.model.findOneAndUpdate(filterQuery, document, options); 54 | } 55 | 56 | async find(filterQuery: FilterQuery) { 57 | return this.model.find(filterQuery); 58 | } 59 | 60 | async startTransaction() { 61 | const session = await this.connection.startSession(); 62 | session.startTransaction(); 63 | return session; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/libs/database/mongo/abstract.schema.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | import { SchemaTypes, Types } from 'mongoose'; 3 | import { Prop, Schema } from '@nestjs/mongoose'; 4 | import { Exclude, Transform, Type } from "class-transformer"; 5 | 6 | export const defaultSchemaOptions = {versionKey: false, virtuals: true, timestamps: true, toJSON: { virtuals: true }}; 7 | 8 | @Schema(defaultSchemaOptions) 9 | export class AbstractDocument { 10 | @Exclude() 11 | @Type(() => String) 12 | @Prop({ type: SchemaTypes.ObjectId }) 13 | @ApiProperty({ name: 'id', type: String }) 14 | @Transform(({ value }) => value.toString()) 15 | _id: Types.ObjectId; 16 | 17 | @ApiProperty() 18 | @Prop({ type: Date, default: Date.now }) 19 | createdAt: Date; 20 | 21 | @ApiProperty() 22 | @Prop({ type: Date, default: Date.now }) 23 | updatedAt: Date; 24 | } 25 | -------------------------------------------------------------------------------- /src/libs/database/mongo/index.ts: -------------------------------------------------------------------------------- 1 | export * from './abstract.schema'; 2 | export * from './abstract.repository'; 3 | export * from './mongo.database.module'; 4 | 5 | export * from './pipes/mongoose.id.pipe'; 6 | export * from './serializers/mongoose.class.serializer'; 7 | -------------------------------------------------------------------------------- /src/libs/database/mongo/mongo.database.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import {ConfigService} from "@nestjs/config"; 3 | import { MongooseModule } from '@nestjs/mongoose'; 4 | 5 | @Module({ 6 | imports: [ 7 | MongooseModule.forRootAsync({ 8 | inject: [ConfigService], 9 | useFactory: (configService: ConfigService) => ({ 10 | uri: configService.get('MONGO_DB_HOST') as string, 11 | dbName: configService.get('MONGO_DB_NAME') as string, 12 | }), 13 | }), 14 | ], 15 | }) 16 | export class MongoDatabaseModule {} 17 | -------------------------------------------------------------------------------- /src/libs/database/mongo/pipes/mongoose.id.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Types } from 'mongoose'; 2 | import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common'; 3 | 4 | @Injectable() 5 | export class MongooseObjectIdPipe implements PipeTransform { 6 | transform(value: string): Types.ObjectId { 7 | const validObjectId = Types.ObjectId.isValid(value); 8 | 9 | if (!validObjectId) { 10 | throw new BadRequestException('Invalid object ID'); 11 | } 12 | 13 | return Types.ObjectId.createFromHexString(value); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/libs/database/mongo/serializers/mongoose.class.serializer.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose'; 2 | import { ClassTransformOptions, plainToClass } from 'class-transformer'; 3 | import { ClassSerializerInterceptor, PlainLiteralObject, Type } from '@nestjs/common'; 4 | 5 | export function MongooseClassSerializer( 6 | classToIntercept: Type 7 | ): typeof ClassSerializerInterceptor { 8 | return class Interceptor extends ClassSerializerInterceptor { 9 | private changePlainObjectToClass(document: PlainLiteralObject) { 10 | if (!(document instanceof Document)) { 11 | return document; 12 | } 13 | 14 | return plainToClass(classToIntercept, document.toJSON()); 15 | } 16 | 17 | private prepareResponse( 18 | response: PlainLiteralObject | PlainLiteralObject[] 19 | ) { 20 | if (Array.isArray(response)) { 21 | return response.map(this.changePlainObjectToClass); 22 | } 23 | 24 | return this.changePlainObjectToClass(response); 25 | } 26 | 27 | serialize( 28 | response: PlainLiteralObject | PlainLiteralObject[], 29 | options: ClassTransformOptions 30 | ) { 31 | return super.serialize(this.prepareResponse(response), options); 32 | } 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/libs/security/encryption/encryption.service.ts: -------------------------------------------------------------------------------- 1 | import { compare, hash } from 'bcrypt'; 2 | 3 | export class EncryptionService { 4 | private static SALT_ROUNDS = 10; 5 | 6 | static hashPassword(password: string): Promise { 7 | return hash(password, EncryptionService.SALT_ROUNDS); 8 | } 9 | 10 | static validatePassword(password: string, matchAgainst: string): Promise { 11 | return compare(password, matchAgainst); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/libs/security/encryption/index.ts: -------------------------------------------------------------------------------- 1 | export * from './encryption.service'; 2 | -------------------------------------------------------------------------------- /src/libs/security/jwt/enums/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jwt.type.enum'; 2 | -------------------------------------------------------------------------------- /src/libs/security/jwt/enums/jwt.type.enum.ts: -------------------------------------------------------------------------------- 1 | export enum JwtType { 2 | AppLogin = 'app-login', 3 | AppInvitation = 'app-invitation', 4 | } 5 | -------------------------------------------------------------------------------- /src/libs/security/jwt/index.ts: -------------------------------------------------------------------------------- 1 | export * from './enums'; 2 | export * from './models'; 3 | export * from './jwt.module'; 4 | export * from './jwt.service'; 5 | -------------------------------------------------------------------------------- /src/libs/security/jwt/jwt.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigService } from "@nestjs/config"; 3 | import { JwtModule as DefaultJwtModule } from '@nestjs/jwt'; 4 | 5 | import { JwtService } from './jwt.service'; 6 | 7 | @Module({ 8 | imports: [ 9 | DefaultJwtModule.registerAsync({ 10 | inject: [ConfigService], 11 | useFactory: async (configService: ConfigService) => { 12 | return { 13 | secret: configService.get('JWT_ACCESS_SECRET'), 14 | signOptions: { 15 | expiresIn: configService.get('JWT_ACCESS_EXPIRES_IN'), 16 | }, 17 | }; 18 | }, 19 | }), 20 | ], 21 | providers: [JwtService], 22 | exports: [JwtService], 23 | }) 24 | export class JwtModule {} 25 | -------------------------------------------------------------------------------- /src/libs/security/jwt/jwt.service.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from "@nestjs/config"; 2 | import { JwtService as DefaultJwtService } from '@nestjs/jwt'; 3 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 4 | 5 | import {JwtModel, JwtPayloadModel} from "./models"; 6 | 7 | @Injectable() 8 | export class JwtService { 9 | constructor( 10 | private readonly jwtService: DefaultJwtService, 11 | private readonly configService: ConfigService 12 | ) {} 13 | 14 | public generateJwt(payload: JwtPayloadModel): JwtModel { 15 | return { 16 | accessToken: this.generateAccessToken(payload), 17 | refreshToken: this.generateRefreshToken(payload), 18 | }; 19 | } 20 | 21 | refreshToken(token: string): JwtModel { 22 | try { 23 | const secret = this.configService.get('JWT_REFRESH_SECRET'); 24 | const payload = this.jwtService.verify(token, { secret }); 25 | return this.generateJwt({ 26 | ...JwtPayloadModel.fromRefreshTokenPayload(payload), 27 | }); 28 | } catch (error) { 29 | throw new UnauthorizedException(); 30 | } 31 | } 32 | 33 | private generateAccessToken(payload: JwtPayloadModel): string { 34 | return this.jwtService.sign(payload); 35 | } 36 | 37 | private generateRefreshToken(payload: JwtPayloadModel): string { 38 | const secret = this.configService.get('JWT_REFRESH_SECRET'); 39 | const expiresIn = this.configService.get('JWT_REFRESH_EXPIRES_IN'); 40 | 41 | return this.jwtService.sign(payload, { secret, expiresIn }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/libs/security/jwt/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jwt.model'; 2 | export * from './jwt.payload.model'; 3 | -------------------------------------------------------------------------------- /src/libs/security/jwt/models/jwt.model.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class JwtModel { 4 | @ApiProperty() 5 | accessToken: string; 6 | 7 | @ApiProperty() 8 | refreshToken: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/libs/security/jwt/models/jwt.payload.model.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { ObjectId, Types } from 'mongoose'; 3 | import { ApiProperty } from '@nestjs/swagger'; 4 | 5 | /** Local Imports **/ 6 | import { JwtType } from '../enums'; 7 | 8 | export class JwtPayloadModel { 9 | @ApiProperty() 10 | @Type(() => Types.ObjectId) 11 | id: Types.ObjectId; 12 | 13 | @ApiProperty() 14 | @Type(() => String) 15 | type: JwtType; 16 | 17 | static fromRefreshTokenPayload( 18 | payload: Partial 19 | ): JwtPayloadModel { 20 | const newPayload = new JwtPayloadModel(); 21 | newPayload.id = Types.ObjectId.createFromHexString(payload.id.toString()) as Types.ObjectId; 22 | newPayload.type = payload.type; 23 | 24 | return newPayload; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { ConfigService } from '@nestjs/config'; 4 | 5 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 6 | import { Logger, ValidationPipe, VersioningType } from '@nestjs/common'; 7 | 8 | import AppModule from './app.module'; 9 | 10 | async function bootstrap() { 11 | const app = await NestFactory.create(AppModule); 12 | app.use(bodyParser.json({ limit: '1mb' })); 13 | app.useGlobalPipes(new ValidationPipe({ whitelist: true, validateCustomDecorators: true })); 14 | 15 | // Config declarations 16 | const configService = app.get(ConfigService); 17 | 18 | // Api configuration 19 | app.setGlobalPrefix(''); 20 | app.enableVersioning({ 21 | type: VersioningType.URI, 22 | defaultVersion: '1', 23 | }); 24 | 25 | // Cors configuration 26 | app.enableCors({ origin: '*' }); 27 | 28 | // Swagger configuration 29 | const documentBuilder = new DocumentBuilder() 30 | .setTitle('OpenAI Microservice Template') 31 | .setDescription('') 32 | .setVersion('1.0.0') 33 | .addBearerAuth() 34 | .build(); 35 | 36 | const document = SwaggerModule.createDocument(app, documentBuilder); 37 | SwaggerModule.setup('/', app, document); 38 | 39 | await app.listen(configService.get('APP_PORT')); 40 | Logger.log(`Application is running on: ${await app.getUrl()}`); 41 | } 42 | 43 | bootstrap(); 44 | -------------------------------------------------------------------------------- /src/modules/ai/ai.controller.ts: -------------------------------------------------------------------------------- 1 | import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; 2 | import { Get, Post, Body, Controller, UseInterceptors, ClassSerializerInterceptor } from '@nestjs/common'; 3 | 4 | import { OpenAiModelsList } from "../../libs/ai/open.ai/models/open.ai.models.list"; 5 | import { OpenAiImageResponse } from "../../libs/ai/open.ai/models/open.ai.image.response"; 6 | import { OpenAiCompletionResponse } from "../../libs/ai/open.ai/models/open.ai.completion.response"; 7 | 8 | import { AiService } from "./ai.service"; 9 | 10 | import { GenerateImageDto } from "./dto/generate.image.dto"; 11 | import { TextCompletionDto } from "./dto/text.completion.dto"; 12 | import { CodeCompletionDto } from "./dto/code.completion.dto"; 13 | 14 | @ApiTags('AI') 15 | @Controller('ai') 16 | export class AiController { 17 | constructor( 18 | private readonly service: AiService, 19 | ) {} 20 | 21 | @Get('models') 22 | @UseInterceptors(ClassSerializerInterceptor) 23 | @ApiOkResponse({ type: OpenAiModelsList, isArray: true }) 24 | @ApiOperation({ description: 'Returns a list of available OpenAI models', summary: 'Get available models' }) 25 | async getModels(): Promise { 26 | return this.service.listModels(); 27 | } 28 | 29 | @Post('completion/text') 30 | @UseInterceptors(ClassSerializerInterceptor) 31 | @ApiOkResponse({ type: OpenAiCompletionResponse }) 32 | @ApiOperation({ description: 'Performs text completion', summary: 'Performs text completion' }) 33 | async complete(@Body() dto: TextCompletionDto): Promise { 34 | return this.service.completion(dto); 35 | } 36 | 37 | @Post('completion/code') 38 | @UseInterceptors(ClassSerializerInterceptor) 39 | @ApiOkResponse({ type: OpenAiCompletionResponse }) 40 | @ApiOperation({ description: 'Performs code completion', summary: 'Performs code completion' }) 41 | async codeCompletion(@Body() dto: CodeCompletionDto): Promise { 42 | return this.service.codeCompletion(dto); 43 | } 44 | 45 | @Post('image/create') 46 | @UseInterceptors(ClassSerializerInterceptor) 47 | @ApiOkResponse({ type: OpenAiImageResponse['data'] }) 48 | @ApiOperation({ description: 'Generates an image from a prompt', summary: 'Generates an image from a prompt' }) 49 | async generateImage(@Body() dto: GenerateImageDto): Promise { 50 | return this.service.generateImage(dto); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/modules/ai/ai.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { OpenAiModule } from "../../libs/ai/open.ai/open.ai.module"; 4 | 5 | /** Local Imports **/ 6 | import { AiService } from "./ai.service"; 7 | import { AiController } from "./ai.controller"; 8 | 9 | @Module({ 10 | imports: [ 11 | OpenAiModule, 12 | ], 13 | controllers: [AiController], 14 | providers: [AiService], 15 | exports: [AiService], 16 | }) 17 | export class AiModule {} 18 | -------------------------------------------------------------------------------- /src/modules/ai/ai.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { OpenAiService } from "../../libs/ai/open.ai/open.ai.service"; 4 | import { OpenAiCompletionResponse } from "../../libs/ai/open.ai/models/open.ai.completion.response"; 5 | 6 | import { GenerateImageDto } from "./dto/generate.image.dto"; 7 | import { TextCompletionDto } from "./dto/text.completion.dto"; 8 | import { CodeCompletionDto } from "./dto/code.completion.dto"; 9 | 10 | @Injectable() 11 | export class AiService { 12 | constructor( 13 | private readonly openAiService: OpenAiService 14 | ) {} 15 | 16 | async listModels() { 17 | return this.openAiService.listModels(); 18 | } 19 | 20 | async completion(dto: TextCompletionDto): Promise { 21 | return this.openAiService.completionService.textCompletion(dto.prompt); 22 | } 23 | 24 | async codeCompletion(dto: CodeCompletionDto): Promise { 25 | return this.openAiService.completionService.codeCompletion(dto.prompt); 26 | } 27 | 28 | async generateImage(dto: GenerateImageDto): Promise { 29 | return this.openAiService.imageService.create(dto.prompt) 30 | .then(response => response.data.map(item => item.src)) 31 | .catch(() => []); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/ai/dto/code.completion.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | import { IsString, MaxLength, MinLength } from "class-validator"; 3 | 4 | export class CodeCompletionDto { 5 | @IsString() 6 | @MinLength(10) 7 | @MaxLength(20000) 8 | @ApiProperty({ example: 'Write a JavaScript function that sorts an array' }) 9 | prompt: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/ai/dto/generate.image.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | import { IsString, MaxLength, MinLength } from "class-validator"; 3 | 4 | export class GenerateImageDto { 5 | @IsString() 6 | @MinLength(10) 7 | @MaxLength(20000) 8 | @ApiProperty({ example: 'A cat standing playing with a ball on a beach' }) 9 | prompt: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/ai/dto/text.completion.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | import { IsString, MaxLength, MinLength } from "class-validator"; 3 | 4 | export class TextCompletionDto { 5 | @IsString() 6 | @MinLength(10) 7 | @MaxLength(20000) 8 | @ApiProperty({ example: 'How are you today?' }) 9 | prompt: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; 2 | import { 3 | Body, 4 | ClassSerializerInterceptor, 5 | Controller, 6 | HttpCode, 7 | Post, PreconditionFailedException, 8 | Query, 9 | Req, 10 | UseGuards, 11 | UseInterceptors, 12 | } from '@nestjs/common'; 13 | 14 | /** Libs Imports **/ 15 | import { JwtModel } from "../../libs/security/jwt"; 16 | 17 | /** Cross-Module Imports **/ 18 | import { EUserStatus } from "../user"; 19 | 20 | /** Local Imports **/ 21 | import { SignUpDto } from './dto'; 22 | import { LocalAuthGuard } from "./guards"; 23 | import { RequestWithUser } from "./types"; 24 | import { AuthService } from "./auth.service"; 25 | 26 | @ApiTags('Auth') 27 | @Controller('auth') 28 | @UseInterceptors(ClassSerializerInterceptor) 29 | export class AuthController { 30 | constructor(private readonly authService: AuthService) {} 31 | 32 | @Post('sign-in') 33 | @HttpCode(200) 34 | @UseGuards(LocalAuthGuard) 35 | @ApiOperation({ summary: 'Sign in' }) 36 | @ApiOkResponse({ type: JwtModel }) 37 | async signIn(@Req() request: RequestWithUser) { 38 | const { user } = request; 39 | 40 | if(user.status === EUserStatus.Disabled) { 41 | throw new PreconditionFailedException('Account disabled'); 42 | } 43 | 44 | return this.authService.signIn(user); 45 | } 46 | 47 | @Post('sign-up') 48 | @HttpCode(200) 49 | @ApiOperation({ summary: 'Sign up' }) 50 | @ApiOkResponse({ type: JwtModel }) 51 | async signUp(@Body() dto: SignUpDto) { 52 | return this.authService.signUp(dto); 53 | } 54 | 55 | @Post('refresh-token') 56 | @ApiOperation({ summary: 'Refresh authentication token' }) 57 | @ApiOkResponse({ type: JwtModel }) 58 | refreshToken(@Query('refreshToken') token: string) { 59 | return this.authService.refreshToken(token); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/modules/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { PassportModule } from '@nestjs/passport'; 2 | import { forwardRef, Module } from '@nestjs/common'; 3 | 4 | /** Libs Imports **/ 5 | import { JwtModule } from "../../libs/security/jwt"; 6 | 7 | /** Cross-Module Imports **/ 8 | import { UserModule } from "../user"; 9 | 10 | /** Local Imports **/ 11 | import { AuthService } from "./auth.service"; 12 | import { JwtAuthGuardName } from "./constants"; 13 | import { AuthController } from "./auth.controller"; 14 | import { JwtAuthStrategy, LocalAuthStrategy } from "./strategies"; 15 | 16 | @Module({ 17 | imports: [ 18 | JwtModule, 19 | forwardRef(() => UserModule), 20 | PassportModule.register({ defaultStrategy: [JwtAuthGuardName] }), 21 | ], 22 | providers: [AuthService, JwtAuthStrategy, LocalAuthStrategy], 23 | controllers: [AuthController], 24 | exports: [PassportModule, AuthService], 25 | }) 26 | export class AuthModule {} 27 | -------------------------------------------------------------------------------- /src/modules/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 2 | 3 | /** Libs Imports **/ 4 | import { EncryptionService } from "../../libs/security/encryption"; 5 | import { JwtModel, JwtService, JwtType } from "../../libs/security/jwt"; 6 | 7 | /** Cross-Module Imports **/ 8 | import { User, UserService } from "../user"; 9 | 10 | /** Local Imports **/ 11 | import { SignUpDto } from "./dto"; 12 | 13 | @Injectable() 14 | export class AuthService { 15 | constructor( 16 | private readonly jwtService: JwtService, 17 | private readonly userService: UserService, 18 | ) {} 19 | 20 | async signUp(data: SignUpDto): Promise { 21 | const user = await this.userService.create(data); 22 | 23 | return this.jwtService.generateJwt({ 24 | id: user._id, 25 | type: JwtType.AppLogin, 26 | }); 27 | } 28 | 29 | signIn(user: User) { 30 | return this.jwtService.generateJwt({ 31 | id: user._id, 32 | type: JwtType.AppLogin, 33 | }); 34 | } 35 | 36 | public async getAuthenticatedUser(email: string, plainTextPassword: string) { 37 | try { 38 | const user = await this.userService.findOneByEmail( 39 | email.toLowerCase() 40 | ); 41 | 42 | if (!await EncryptionService.validatePassword(plainTextPassword, user.password)) { 43 | throw new UnauthorizedException(); 44 | } 45 | 46 | user.password = undefined; 47 | return user; 48 | } catch (error) { 49 | throw new UnauthorizedException(); 50 | } 51 | } 52 | refreshToken(token: string): JwtModel { 53 | return this.jwtService.refreshToken(token); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/modules/auth/constants.ts: -------------------------------------------------------------------------------- 1 | export const JwtAuthGuardName = 'jwt'; 2 | export const LocalAuthGuardName = 'local'; 3 | -------------------------------------------------------------------------------- /src/modules/auth/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sign-in.dto' 2 | export * from './sign-up.dto' 3 | -------------------------------------------------------------------------------- /src/modules/auth/dto/sign-in.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsString, IsNotEmpty, IsEmail } from 'class-validator'; 3 | 4 | export class SignInDto { 5 | @IsEmail() 6 | @IsNotEmpty() 7 | @ApiProperty() 8 | address: string; 9 | 10 | @IsString() 11 | @IsNotEmpty() 12 | @ApiProperty() 13 | password: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/auth/dto/sign-up.dto.ts: -------------------------------------------------------------------------------- 1 | import { CreateUserDto } from "../../user"; 2 | 3 | export class SignUpDto extends CreateUserDto {} 4 | -------------------------------------------------------------------------------- /src/modules/auth/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jwt.auth.guard'; 2 | export * from './local.auth.guard'; 3 | export * from './user.role.auth.guard'; 4 | -------------------------------------------------------------------------------- /src/modules/auth/guards/jwt.auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | /** Local Imports **/ 5 | import { JwtAuthGuardName } from "../constants"; 6 | 7 | @Injectable() 8 | export class JwtAuthGuard extends AuthGuard(JwtAuthGuardName) {} 9 | -------------------------------------------------------------------------------- /src/modules/auth/guards/local.auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | /** Local Imports **/ 5 | import { LocalAuthGuardName } from "../constants"; 6 | 7 | @Injectable() 8 | export class LocalAuthGuard extends AuthGuard(LocalAuthGuardName) {} 9 | -------------------------------------------------------------------------------- /src/modules/auth/guards/user.role.auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, mixin, Type } from '@nestjs/common'; 2 | 3 | /** Cross-Module Imports **/ 4 | import { EUserRole } from "../../user"; 5 | 6 | export const UserRoleGuard = (...roles: EUserRole[]): Type => { 7 | class RoleGuardMixin implements CanActivate { 8 | canActivate(context: ExecutionContext): boolean { 9 | const request = context.switchToHttp().getRequest(); 10 | const user = request.user; 11 | 12 | return (user?.roles ?? []).filter(item => roles.includes(item)).length > 0; 13 | } 14 | } 15 | 16 | return mixin(RoleGuardMixin); 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.module'; 2 | export * from './auth.service'; 3 | export * from './auth.controller'; 4 | -------------------------------------------------------------------------------- /src/modules/auth/strategies/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jwt.auth.strategy'; 2 | export * from './local.auth.strategy'; 3 | -------------------------------------------------------------------------------- /src/modules/auth/strategies/jwt.auth.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from "@nestjs/config"; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { ExtractJwt, Strategy } from 'passport-jwt'; 4 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 5 | 6 | /** Libs Imports **/ 7 | import { JwtPayloadModel } from "../../../libs/security/jwt"; 8 | 9 | /** Cross-Module Imports **/ 10 | import { User, UserService } from "../../user"; 11 | 12 | @Injectable() 13 | export class JwtAuthStrategy extends PassportStrategy(Strategy) { 14 | constructor( 15 | private readonly accountService: UserService, 16 | private readonly configService: ConfigService, 17 | ) { 18 | super({ 19 | ignoreExpiration: false, 20 | secretOrKey: configService.get('JWT_ACCESS_SECRET'), 21 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 22 | }); 23 | } 24 | 25 | async validate(payload: JwtPayloadModel): Promise { 26 | const user = await this.accountService.fetchById(payload.id); 27 | if (!user) throw new UnauthorizedException(); 28 | 29 | return user; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/modules/auth/strategies/local.auth.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Strategy } from 'passport-local'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 4 | 5 | /** Cross-Module Imports **/ 6 | import { User } from "../../user"; 7 | 8 | /** Local Imports **/ 9 | import { AuthService } from "../auth.service"; 10 | 11 | @Injectable() 12 | export class LocalAuthStrategy extends PassportStrategy(Strategy) { 13 | constructor(private authService: AuthService) { 14 | super({ 15 | usernameField: 'email', 16 | passwordField: 'password', 17 | }); 18 | } 19 | 20 | async validate(email: string, password: string): Promise { 21 | const account = await this.authService.getAuthenticatedUser( 22 | email, 23 | password 24 | ); 25 | 26 | if (!account) throw new UnauthorizedException(); 27 | 28 | return account; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/auth/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './request.user.interface'; 2 | -------------------------------------------------------------------------------- /src/modules/auth/types/request.user.interface.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | 3 | /** Cross-Module Imports **/ 4 | import { User } from "../../user"; 5 | 6 | export interface RequestWithUser extends Request { 7 | user: User; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/modules/user/dto/create.user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsNotEmpty, IsString } from 'class-validator'; 3 | 4 | export class CreateUserDto { 5 | @IsString() 6 | @IsNotEmpty() 7 | @ApiProperty() 8 | email: string; 9 | 10 | @IsString() 11 | @IsNotEmpty() 12 | @ApiProperty() 13 | password: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/user/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create.user.dto'; 2 | -------------------------------------------------------------------------------- /src/modules/user/enums/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.role.enum'; 2 | export * from './user.status.enum'; 3 | -------------------------------------------------------------------------------- /src/modules/user/enums/user.role.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EUserRole { 2 | User = 'user', 3 | Admin = 'admin', 4 | } 5 | -------------------------------------------------------------------------------- /src/modules/user/enums/user.status.enum.ts: -------------------------------------------------------------------------------- 1 | export enum EUserStatus { 2 | Active = 'active', 3 | Disabled = 'disabled', 4 | } 5 | -------------------------------------------------------------------------------- /src/modules/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dto'; 2 | export * from './enums'; 3 | 4 | export * from './user.schema'; 5 | export * from './user.module'; 6 | export * from './user.service'; 7 | export * from './user.controller'; 8 | -------------------------------------------------------------------------------- /src/modules/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Types } from "mongoose"; 2 | import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; 3 | import { Get, Param, Req, Delete, Controller, UseGuards, UseInterceptors } from '@nestjs/common'; 4 | 5 | /** Libs Imports **/ 6 | import { MongooseClassSerializer, MongooseObjectIdPipe } from "../../libs/database/mongo"; 7 | 8 | /** Cross-Module Imports **/ 9 | import { RequestWithUser } from '../auth/types'; 10 | import { JwtAuthGuard, UserRoleGuard } from '../auth/guards'; 11 | 12 | /** Local Imports **/ 13 | import { EUserRole } from "./enums"; 14 | import { User } from './user.schema'; 15 | import { UserService } from './user.service'; 16 | 17 | @ApiTags('Users') 18 | @Controller('users') 19 | @UseInterceptors(MongooseClassSerializer(User)) 20 | export class UserController { 21 | constructor( 22 | private readonly userService: UserService, 23 | ) {} 24 | 25 | @Get('me') 26 | @UseGuards(JwtAuthGuard) 27 | @ApiOkResponse({ type: User }) 28 | @ApiOperation({ summary: 'Get own user' }) 29 | @UseInterceptors(MongooseClassSerializer(User)) 30 | async fetchCurrentUser(@Req() request: RequestWithUser): Promise { 31 | const { user } = request; 32 | 33 | return user; 34 | } 35 | 36 | @Get(':id') 37 | @UseGuards(JwtAuthGuard, UserRoleGuard(EUserRole.Admin)) 38 | @ApiOkResponse({ type: User }) 39 | @ApiOperation({ summary: 'Get user by id' }) 40 | @UseInterceptors(MongooseClassSerializer(User)) 41 | async fetchUserById(@Param("id", MongooseObjectIdPipe) id: unknown): Promise { 42 | return this.userService.fetchById(id as Types.ObjectId); 43 | } 44 | 45 | @Delete(':id') 46 | @UseGuards(JwtAuthGuard, UserRoleGuard(EUserRole.Admin)) 47 | @ApiOperation({ summary: 'Delete account', description: 'Deletes a user identified by id' }) 48 | async deleteAccount(@Param("id", MongooseObjectIdPipe) id: unknown): Promise { 49 | return this.userService.delete(id as Types.ObjectId); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/modules/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | 4 | /** Libs Imports **/ 5 | import { MongoDatabaseModule } from "../../libs/database"; 6 | 7 | /** Local Imports **/ 8 | import { UserService } from './user.service'; 9 | import { User, UserSchema } from './user.schema'; 10 | import { UserController } from './user.controller'; 11 | import { UserRepository } from "./user.repository"; 12 | 13 | @Module({ 14 | imports: [ 15 | MongoDatabaseModule, 16 | MongooseModule.forFeature([ 17 | { name: User.name, schema: UserSchema }, 18 | ]), 19 | ], 20 | controllers: [UserController], 21 | providers: [UserController, UserService, UserRepository], 22 | exports: [UserService], 23 | }) 24 | export class UserModule {} 25 | -------------------------------------------------------------------------------- /src/modules/user/user.repository.ts: -------------------------------------------------------------------------------- 1 | import { Model, Connection } from 'mongoose'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { InjectModel, InjectConnection } from '@nestjs/mongoose'; 4 | 5 | import { User } from './user.schema'; 6 | import { AbstractRepository } from "../../libs/database/mongo"; 7 | 8 | @Injectable() 9 | export class UserRepository extends AbstractRepository { 10 | constructor( 11 | @InjectConnection() connection: Connection, 12 | @InjectModel(User.name) model: Model, 13 | ) { 14 | super(model, connection); 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/modules/user/user.schema.ts: -------------------------------------------------------------------------------- 1 | import { Exclude } from 'class-transformer'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 4 | 5 | /** Libs Imports **/ 6 | import {AbstractDocument, defaultSchemaOptions} from "../../libs/database/mongo"; 7 | 8 | /** Local Imports **/ 9 | import { EUserRole, EUserStatus } from "./enums"; 10 | 11 | @Schema({ ...defaultSchemaOptions, collection: 'users' }) 12 | export class User extends AbstractDocument { 13 | @ApiProperty() 14 | @Prop({ type: String, unique: true }) 15 | email: string; 16 | 17 | @Exclude() 18 | @Prop({ type: String }) 19 | password: string; 20 | 21 | @ApiProperty({ enum: EUserRole, isArray: true }) 22 | @Prop({ type: Array, default: [EUserRole.User] }) 23 | roles: EUserRole[]; 24 | 25 | @ApiProperty({ enum: EUserStatus }) 26 | @Prop({ type: String, default: EUserStatus.Active }) 27 | status: EUserStatus; 28 | } 29 | 30 | export const UserSchema = SchemaFactory.createForClass(User); 31 | -------------------------------------------------------------------------------- /src/modules/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Types } from 'mongoose'; 2 | import { Injectable } from '@nestjs/common'; 3 | 4 | /** Libs Imports **/ 5 | import { EncryptionService } from "../../libs/security/encryption"; 6 | 7 | /** Local Imports **/ 8 | import { User } from './user.schema'; 9 | import { CreateUserDto } from './dto'; 10 | import { UserRepository } from "./user.repository"; 11 | 12 | @Injectable() 13 | export class UserService { 14 | constructor( 15 | private readonly repository: UserRepository, 16 | ) {} 17 | 18 | async fetchById(id: Types.ObjectId): Promise { 19 | return this.repository.findOne({_id: id}); 20 | } 21 | 22 | findOneByEmail(email: string): Promise { 23 | return this.repository.findOne({email: email}); 24 | } 25 | 26 | async create(dto: CreateUserDto): Promise { 27 | const data: Partial = { 28 | email: dto.email, 29 | password: await EncryptionService.hashPassword(dto.password) 30 | }; 31 | 32 | return this.repository.create(data); 33 | } 34 | 35 | async delete(id: Types.ObjectId) { 36 | await this.repository.model.deleteOne({_id: id}); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "esModuleInterop": true, 14 | "incremental": true, 15 | "skipLibCheck": true, 16 | "strictNullChecks": false, 17 | "noImplicitAny": false, 18 | "strictBindCallApply": false, 19 | "forceConsistentCasingInFileNames": false, 20 | "noFallthroughCasesInSwitch": false 21 | } 22 | } 23 | --------------------------------------------------------------------------------