├── .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 | [](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 |
--------------------------------------------------------------------------------