├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── extensions.json ├── README.md ├── apps ├── .gitkeep ├── account │ ├── .eslintrc.json │ ├── jest.config.js │ ├── project.json │ ├── src │ │ ├── app │ │ │ ├── .gitkeep │ │ │ ├── app.module.ts │ │ │ ├── auth │ │ │ │ ├── auth.controller.spec.ts │ │ │ │ ├── auth.controller.ts │ │ │ │ ├── auth.module.ts │ │ │ │ └── auth.service.ts │ │ │ ├── configs │ │ │ │ ├── jwt.config.ts │ │ │ │ ├── mongo.config.ts │ │ │ │ └── rmq.config.ts │ │ │ └── user │ │ │ │ ├── entities │ │ │ │ └── user.entity.ts │ │ │ │ ├── models │ │ │ │ └── user.model.ts │ │ │ │ ├── repositories │ │ │ │ └── user.repository.ts │ │ │ │ ├── sagas │ │ │ │ ├── buy-course.saga.ts │ │ │ │ ├── buy-course.state.ts │ │ │ │ └── buy-course.steps.ts │ │ │ │ ├── user.commands.ts │ │ │ │ ├── user.controller.spec.ts │ │ │ │ ├── user.event-immiter.ts │ │ │ │ ├── user.module.ts │ │ │ │ ├── user.queries.ts │ │ │ │ └── user.service.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ └── main.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json └── api │ ├── .eslintrc.json │ ├── jest.config.js │ ├── project.json │ ├── src │ ├── app │ │ ├── .gitkeep │ │ ├── app.module.ts │ │ ├── configs │ │ │ ├── jwt.config.ts │ │ │ └── rmq.config.ts │ │ ├── controllers │ │ │ ├── auth.controller.ts │ │ │ └── user.controller.ts │ │ ├── dtos │ │ │ ├── login.dto.ts │ │ │ └── register.dto.ts │ │ ├── guards │ │ │ ├── jwt.guard.ts │ │ │ └── user.decorator.ts │ │ └── strategies │ │ │ └── jwt.straragy.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ └── main.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── jest.config.js ├── jest.preset.js ├── libs ├── .gitkeep ├── contracts │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.js │ ├── project.json │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── account │ │ │ ├── account.buy-course.ts │ │ │ ├── account.change-profile.ts │ │ │ ├── account.changed-course.ts │ │ │ ├── account.check-payment.ts │ │ │ ├── account.login.ts │ │ │ ├── account.register.ts │ │ │ ├── account.user-courses.ts │ │ │ └── account.user-info.ts │ │ │ ├── course │ │ │ └── course.get-course.ts │ │ │ └── payment │ │ │ ├── payment.check.ts │ │ │ └── payment.generate-link.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json └── interfaces │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.js │ ├── project.json │ ├── src │ ├── index.ts │ └── lib │ │ ├── auth.interface.ts │ │ ├── course.interface.ts │ │ ├── events.interface.ts │ │ └── user.interface.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── nx.json ├── package-lock.json ├── package.json ├── tools ├── generators │ └── .gitkeep └── tsconfig.tools.json ├── tsconfig.base.json └── workspace.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nrwl/nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nrwl/nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nrwl/nx/typescript"], 27 | "rules": {} 28 | }, 29 | { 30 | "files": ["*.js", "*.jsx"], 31 | "extends": ["plugin:@nrwl/nx/javascript"], 32 | "rules": {} 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /envs 8 | 9 | # dependencies 10 | node_modules 11 | 12 | # IDEs and editors 13 | /.idea 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | /typings 37 | 38 | # System Files 39 | .DS_Store 40 | Thumbs.db 41 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "esbenp.prettier-vscode", 5 | "firsttris.vscode-jest-runner", 6 | "dbaeumer.vscode-eslint" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Purple 4 | 5 | This project was generated using [Nx](https://nx.dev). 6 | 7 |

8 | 9 | 🔎 **Smart, Fast and Extensible Build System** 10 | 11 | ## Adding capabilities to your workspace 12 | 13 | Nx supports many plugins which add capabilities for developing different types of applications and different tools. 14 | 15 | These capabilities include generating applications, libraries, etc as well as the devtools to test, and build projects as well. 16 | 17 | Below are our core plugins: 18 | 19 | - [React](https://reactjs.org) 20 | - `npm install --save-dev @nrwl/react` 21 | - Web (no framework frontends) 22 | - `npm install --save-dev @nrwl/web` 23 | - [Angular](https://angular.io) 24 | - `npm install --save-dev @nrwl/angular` 25 | - [Nest](https://nestjs.com) 26 | - `npm install --save-dev @nrwl/nest` 27 | - [Express](https://expressjs.com) 28 | - `npm install --save-dev @nrwl/express` 29 | - [Node](https://nodejs.org) 30 | - `npm install --save-dev @nrwl/node` 31 | 32 | There are also many [community plugins](https://nx.dev/community) you could add. 33 | 34 | ## Generate an application 35 | 36 | Run `nx g @nrwl/react:app my-app` to generate an application. 37 | 38 | > You can use any of the plugins above to generate applications as well. 39 | 40 | When using Nx, you can create multiple applications and libraries in the same workspace. 41 | 42 | ## Generate a library 43 | 44 | Run `nx g @nrwl/react:lib my-lib` to generate a library. 45 | 46 | > You can also use any of the plugins above to generate libraries as well. 47 | 48 | Libraries are shareable across libraries and applications. They can be imported from `@purple/mylib`. 49 | 50 | ## Development server 51 | 52 | Run `nx serve my-app` for a dev server. Navigate to http://localhost:4200/. The app will automatically reload if you change any of the source files. 53 | 54 | ## Code scaffolding 55 | 56 | Run `nx g @nrwl/react:component my-component --project=my-app` to generate a new component. 57 | 58 | ## Build 59 | 60 | Run `nx build my-app` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 61 | 62 | ## Running unit tests 63 | 64 | Run `nx test my-app` to execute the unit tests via [Jest](https://jestjs.io). 65 | 66 | Run `nx affected:test` to execute the unit tests affected by a change. 67 | 68 | ## Running end-to-end tests 69 | 70 | Run `nx e2e my-app` to execute the end-to-end tests via [Cypress](https://www.cypress.io). 71 | 72 | Run `nx affected:e2e` to execute the end-to-end tests affected by a change. 73 | 74 | ## Understand your workspace 75 | 76 | Run `nx graph` to see a diagram of the dependencies of your projects. 77 | 78 | ## Further help 79 | 80 | Visit the [Nx Documentation](https://nx.dev) to learn more. 81 | 82 | 83 | 84 | ## ☁ Nx Cloud 85 | 86 | ### Distributed Computation Caching & Distributed Task Execution 87 | 88 |

89 | 90 | Nx Cloud pairs with Nx in order to enable you to build and test code more rapidly, by up to 10 times. Even teams that are new to Nx can connect to Nx Cloud and start saving time instantly. 91 | 92 | Teams using Nx gain the advantage of building full-stack applications with their preferred framework alongside Nx’s advanced code generation and project dependency graph, plus a unified experience for both frontend and backend developers. 93 | 94 | Visit [Nx Cloud](https://nx.app/) to learn more. 95 | -------------------------------------------------------------------------------- /apps/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlariCode/6-microservices-demo-1/cd8ccd792c313ffebe7d98d280e972ddbcde5ac4/apps/.gitkeep -------------------------------------------------------------------------------- /apps/account/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/account/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'account', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | testEnvironment: 'node', 10 | transform: { 11 | '^.+\\.[tj]s$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'html'], 14 | coverageDirectory: '../../coverage/apps/account', 15 | }; 16 | -------------------------------------------------------------------------------- /apps/account/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "apps/account", 3 | "sourceRoot": "apps/account/src", 4 | "projectType": "application", 5 | "targets": { 6 | "build": { 7 | "executor": "@nrwl/node:webpack", 8 | "outputs": ["{options.outputPath}"], 9 | "options": { 10 | "outputPath": "dist/apps/account", 11 | "main": "apps/account/src/main.ts", 12 | "tsConfig": "apps/account/tsconfig.app.json", 13 | "assets": ["apps/account/src/assets"] 14 | }, 15 | "configurations": { 16 | "production": { 17 | "optimization": true, 18 | "extractLicenses": true, 19 | "inspect": false, 20 | "fileReplacements": [ 21 | { 22 | "replace": "apps/account/src/environments/environment.ts", 23 | "with": "apps/account/src/environments/environment.prod.ts" 24 | } 25 | ] 26 | } 27 | } 28 | }, 29 | "serve": { 30 | "executor": "@nrwl/node:node", 31 | "options": { 32 | "buildTarget": "account:build" 33 | } 34 | }, 35 | "lint": { 36 | "executor": "@nrwl/linter:eslint", 37 | "outputs": ["{options.outputFile}"], 38 | "options": { 39 | "lintFilePatterns": ["apps/account/**/*.ts"] 40 | } 41 | }, 42 | "test": { 43 | "executor": "@nrwl/jest:jest", 44 | "outputs": ["coverage/apps/account"], 45 | "options": { 46 | "jestConfig": "apps/account/jest.config.js", 47 | "passWithNoTests": true 48 | } 49 | } 50 | }, 51 | "tags": [] 52 | } 53 | -------------------------------------------------------------------------------- /apps/account/src/app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlariCode/6-microservices-demo-1/cd8ccd792c313ffebe7d98d280e972ddbcde5ac4/apps/account/src/app/.gitkeep -------------------------------------------------------------------------------- /apps/account/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserModule } from './user/user.module'; 3 | import { AuthModule } from './auth/auth.module'; 4 | import { ConfigModule } from '@nestjs/config'; 5 | import { MongooseModule } from '@nestjs/mongoose'; 6 | import { getMongoConfig } from './configs/mongo.config'; 7 | import { RMQModule } from 'nestjs-rmq'; 8 | import { getRMQConfig } from './configs/rmq.config'; 9 | 10 | @Module({ 11 | imports: [ 12 | ConfigModule.forRoot({ isGlobal: true, envFilePath: 'envs/.account.env' }), 13 | RMQModule.forRootAsync(getRMQConfig()), 14 | UserModule, 15 | AuthModule, 16 | MongooseModule.forRootAsync(getMongoConfig()) 17 | ], 18 | }) 19 | export class AppModule {} 20 | -------------------------------------------------------------------------------- /apps/account/src/app/auth/auth.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { RMQModule, RMQService, RMQTestService } from 'nestjs-rmq'; 4 | import { UserModule } from '../user/user.module'; 5 | import { AuthModule } from './auth.module'; 6 | import { MongooseModule } from '@nestjs/mongoose'; 7 | import { getMongoConfig } from '../configs/mongo.config' 8 | import { INestApplication } from '@nestjs/common'; 9 | import { UserRepository } from '../user/repositories/user.repository'; 10 | import { AccountLogin, AccountRegister } from '@purple/contracts'; 11 | 12 | const authLogin: AccountLogin.Request = { 13 | email: 'a@a.ru', 14 | password: '1' 15 | } 16 | 17 | const authRegister: AccountRegister.Request = { 18 | ...authLogin, 19 | displayName: 'Вася' 20 | } 21 | 22 | describe('AuthController', () => { 23 | let app: INestApplication; 24 | let userRepository: UserRepository; 25 | let rmqService: RMQTestService; 26 | 27 | beforeAll(async () => { 28 | const module: TestingModule = await Test.createTestingModule({ 29 | imports: [ 30 | ConfigModule.forRoot({ isGlobal: true, envFilePath: 'envs/.account.env' }), 31 | RMQModule.forTest({}), 32 | UserModule, 33 | AuthModule, 34 | MongooseModule.forRootAsync(getMongoConfig()) 35 | ] 36 | }).compile(); 37 | app = module.createNestApplication(); 38 | userRepository = app.get(UserRepository); 39 | rmqService = app.get(RMQService); 40 | await app.init(); 41 | }) 42 | 43 | it('Register', async () => { 44 | const res = await rmqService.triggerRoute( 45 | AccountRegister.topic, 46 | authRegister 47 | ); 48 | expect(res.email).toEqual(authRegister.email); 49 | }); 50 | 51 | 52 | it('Login', async () => { 53 | const res = await rmqService.triggerRoute( 54 | AccountLogin.topic, 55 | authLogin 56 | ); 57 | expect(res.access_token).toBeDefined(); 58 | }); 59 | 60 | afterAll(async () => { 61 | await userRepository.deleteUser(authRegister.email); 62 | app.close(); 63 | }); 64 | }); -------------------------------------------------------------------------------- /apps/account/src/app/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Logger } from '@nestjs/common'; 2 | import { AccountLogin, AccountRegister } from '@purple/contracts'; 3 | import { Message, RMQMessage, RMQRoute, RMQValidate } from 'nestjs-rmq'; 4 | import { AuthService } from './auth.service'; 5 | 6 | @Controller() 7 | export class AuthController { 8 | constructor( 9 | private readonly authService: AuthService 10 | ) {} 11 | 12 | @RMQValidate() 13 | @RMQRoute(AccountRegister.topic) 14 | async register(dto: AccountRegister.Request, @RMQMessage msg: Message): Promise { 15 | const rid = msg.properties.headers['requestId']; 16 | const logger = new Logger(rid); 17 | logger.error('sdfsdf') 18 | return this.authService.register(dto); 19 | } 20 | 21 | @RMQValidate() 22 | @RMQRoute(AccountLogin.topic) 23 | async login(@Body() { email, password }: AccountLogin.Request): Promise { 24 | const { id } = await this.authService.validateUser(email, password); 25 | return this.authService.login(id); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/account/src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { JwtModule } from '@nestjs/jwt'; 3 | import { getJWTConfig } from '../configs/jwt.config'; 4 | import { UserModule } from '../user/user.module'; 5 | import { AuthController } from './auth.controller'; 6 | import { AuthService } from './auth.service'; 7 | 8 | @Module({ 9 | imports: [UserModule, JwtModule.registerAsync(getJWTConfig())], 10 | controllers: [AuthController], 11 | providers: [AuthService], 12 | }) 13 | export class AuthModule {} 14 | -------------------------------------------------------------------------------- /apps/account/src/app/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import { AccountRegister } from '@purple/contracts'; 4 | import { UserRole } from '@purple/interfaces'; 5 | import { UserEntity } from '../user/entities/user.entity'; 6 | import { UserRepository } from '../user/repositories/user.repository'; 7 | 8 | @Injectable() 9 | export class AuthService { 10 | constructor( 11 | private readonly userRepository: UserRepository, 12 | private readonly jwtService: JwtService 13 | ) {} 14 | 15 | async register({ email, password, displayName }: AccountRegister.Request) { 16 | const oldUser = await this.userRepository.findUser(email); 17 | if (oldUser) { 18 | throw new Error('Такой пользователь уже зарегистрирован'); 19 | } 20 | const newUserEntity = await new UserEntity({ 21 | displayName, 22 | email, 23 | passwordHash: '', 24 | role: UserRole.Student 25 | }).setPassword(password); 26 | const newUser = await this.userRepository.createUser(newUserEntity); 27 | return { email: newUser.email }; 28 | } 29 | 30 | async validateUser(email: string, password: string) { 31 | const user = await this.userRepository.findUser(email); 32 | if (!user) { 33 | throw new Error('Неверный логин или пароль'); 34 | } 35 | const userEntity = new UserEntity(user); 36 | const isCorrectPassword = await userEntity.validatePassword(password); 37 | if (!isCorrectPassword) { 38 | throw new Error('Неверный логин или пароль'); 39 | } 40 | return { id: user._id }; 41 | } 42 | 43 | async login(id: string) { 44 | return { 45 | access_token: await this.jwtService.signAsync({ id }) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /apps/account/src/app/configs/jwt.config.ts: -------------------------------------------------------------------------------- 1 | import { ConfigModule, ConfigService } from '@nestjs/config'; 2 | import { JwtModuleAsyncOptions } from '@nestjs/jwt' 3 | 4 | export const getJWTConfig = (): JwtModuleAsyncOptions => ({ 5 | imports: [ConfigModule], 6 | inject: [ConfigService], 7 | useFactory: (configService: ConfigService) => ({ 8 | secret: configService.get('JWT_SECRET') 9 | }) 10 | }); -------------------------------------------------------------------------------- /apps/account/src/app/configs/mongo.config.ts: -------------------------------------------------------------------------------- 1 | import { ConfigModule, ConfigService } from '@nestjs/config'; 2 | import { MongooseModuleAsyncOptions } from '@nestjs/mongoose'; 3 | 4 | export const getMongoConfig = (): MongooseModuleAsyncOptions => { 5 | return { 6 | useFactory: (configService: ConfigService) => ({ 7 | uri: getMongoString(configService) 8 | }), 9 | inject: [ConfigService], 10 | imports: [ConfigModule] 11 | } 12 | } 13 | 14 | const getMongoString = (configService: ConfigService) => 15 | 'mongodb://' + 16 | configService.get('MONGO_LOGIN') + 17 | ':' + 18 | configService.get('MONGO_PASSWORD') + 19 | '@' + 20 | configService.get('MONGO_HOST') + 21 | ':' + 22 | configService.get('MONGO_PORT') + 23 | '/' + 24 | configService.get('MONGO_DATABASE') + 25 | '?authSource=' + 26 | configService.get('MONGO_AUTHDATABASE'); 27 | -------------------------------------------------------------------------------- /apps/account/src/app/configs/rmq.config.ts: -------------------------------------------------------------------------------- 1 | import { ConfigModule, ConfigService } from '@nestjs/config'; 2 | import { IRMQServiceAsyncOptions } from 'nestjs-rmq'; 3 | 4 | export const getRMQConfig = (): IRMQServiceAsyncOptions => ({ 5 | inject: [ConfigService], 6 | imports: [ConfigModule], 7 | useFactory: (configService: ConfigService) => ({ 8 | exchangeName: configService.get('AMQP_EXCHANGE') ?? '', 9 | connections: [ 10 | { 11 | login: configService.get('AMQP_USER') ?? '', 12 | password: configService.get('AMQP_PASSWORD') ?? '', 13 | host: configService.get('AMQP_HOSTNAME') ?? '' 14 | } 15 | ], 16 | queueName: configService.get('AMQP_QUEUE'), 17 | prefetchCount: 32, 18 | serviceName: 'purple-account' 19 | }) 20 | }) -------------------------------------------------------------------------------- /apps/account/src/app/user/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { AccountChangedCourse } from '@purple/contracts'; 2 | import { IDomainEvent, IUser, IUserCourses, PurchaseState, UserRole } from '@purple/interfaces'; 3 | import { compare, genSalt, hash } from 'bcryptjs'; 4 | 5 | export class UserEntity implements IUser { 6 | _id?: string; 7 | displayName?: string; 8 | email: string; 9 | passwordHash: string; 10 | role: UserRole; 11 | courses?: IUserCourses[]; 12 | events: IDomainEvent[] = []; 13 | 14 | constructor(user: IUser) { 15 | this._id = user._id; 16 | this.passwordHash = user.passwordHash; 17 | this.displayName = user.displayName; 18 | this.email = user.email; 19 | this.role = user.role; 20 | this.courses = user.courses; 21 | } 22 | 23 | public setCourseStatus(courseId: string, state: PurchaseState) { 24 | const exist = this.courses.find(c => c.courseId === courseId); 25 | if (!exist) { 26 | this.courses.push({ 27 | courseId, 28 | purchaseState: state 29 | }); 30 | return this; 31 | } 32 | if (state === PurchaseState.Cenceled) { 33 | this.courses = this.courses.filter(c => c.courseId !== courseId); 34 | return this; 35 | } 36 | this.courses = this.courses.map(c => { 37 | if (c.courseId === courseId) { 38 | c.purchaseState = state; 39 | return c; 40 | } 41 | return c; 42 | }); 43 | this.events.push({ 44 | topic: AccountChangedCourse.topic, 45 | data: { courseId, userId: this._id, state } 46 | }); 47 | return this; 48 | } 49 | 50 | public getCourseState(courseId: string): PurchaseState { 51 | return this.courses.find(c => c.courseId === courseId)?.purchaseState ?? PurchaseState.Started; 52 | } 53 | 54 | public getPublicProfile() { 55 | return { 56 | email: this.email, 57 | role: this.role, 58 | displayName: this.displayName 59 | } 60 | } 61 | 62 | public async setPassword(password: string) { 63 | const salt = await genSalt(10); 64 | this.passwordHash = await hash(password, salt); 65 | return this; 66 | } 67 | 68 | public validatePassword(password: string) { 69 | return compare(password, this.passwordHash); 70 | } 71 | 72 | public updateProfile(displayName: string) { 73 | this.displayName = displayName; 74 | return this; 75 | } 76 | } -------------------------------------------------------------------------------- /apps/account/src/app/user/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import { IUser, IUserCourses, PurchaseState, UserRole } from '@purple/interfaces'; 3 | import { Document, Types } from 'mongoose'; 4 | 5 | @Schema() 6 | export class UserCourses extends Document implements IUserCourses { 7 | @Prop({ required: true }) 8 | courseId: string; 9 | 10 | @Prop({ required: true, enum: PurchaseState, type: String }) 11 | purchaseState: PurchaseState; 12 | } 13 | 14 | export const UserCoursesSchema = SchemaFactory.createForClass(UserCourses); 15 | 16 | 17 | @Schema() 18 | export class User extends Document implements IUser { 19 | @Prop() 20 | displayName?: string; 21 | 22 | @Prop({ required: true }) 23 | email: string; 24 | 25 | @Prop({ required: true }) 26 | passwordHash: string; 27 | 28 | @Prop({ required: true, enum: UserRole, type: String, default: UserRole.Student }) 29 | role: UserRole; 30 | 31 | @Prop({ type: [UserCoursesSchema], _id: false }) 32 | courses: Types.Array 33 | } 34 | 35 | export const UserSchema = SchemaFactory.createForClass(User); -------------------------------------------------------------------------------- /apps/account/src/app/user/repositories/user.repository.ts: -------------------------------------------------------------------------------- 1 | import { InjectModel } from '@nestjs/mongoose'; 2 | import { User } from '../models/user.model'; 3 | import { Model } from 'mongoose'; 4 | import { Injectable } from '@nestjs/common'; 5 | import { UserEntity } from '../entities/user.entity'; 6 | 7 | @Injectable() 8 | export class UserRepository { 9 | constructor( 10 | @InjectModel(User.name) private readonly userModel: Model 11 | ) {} 12 | 13 | async createUser(user: UserEntity) { 14 | const newUser = new this.userModel(user); 15 | return newUser.save(); 16 | } 17 | 18 | async updateUser({ _id, ...rest }: UserEntity) { 19 | return this.userModel.updateOne({ _id }, { $set: { ...rest } }).exec(); 20 | } 21 | 22 | async findUser(email: string) { 23 | return this.userModel.findOne({ email }).exec(); 24 | } 25 | 26 | async findUserById(id: string) { 27 | return this.userModel.findById(id).exec(); 28 | } 29 | 30 | async deleteUser(email: string) { 31 | this.userModel.deleteOne({ email }).exec(); 32 | } 33 | } -------------------------------------------------------------------------------- /apps/account/src/app/user/sagas/buy-course.saga.ts: -------------------------------------------------------------------------------- 1 | import { PurchaseState } from '@purple/interfaces'; 2 | import { RMQService } from 'nestjs-rmq'; 3 | import { UserEntity } from '../entities/user.entity'; 4 | import { BuyCourseSagaState } from './buy-course.state'; 5 | import { BuyCourseSagaStateCanceled, BuyCourseSagaStatePurchased, BuyCourseSagaStateWaitingForPayment, BuyCourseSagaStateStarted } from './buy-course.steps'; 6 | 7 | export class BuyCourseSaga { 8 | private state: BuyCourseSagaState; 9 | 10 | constructor(public user: UserEntity, public courseId: string, public rmqService: RMQService) { 11 | this.setState(user.getCourseState(courseId), courseId); 12 | } 13 | 14 | setState(state: PurchaseState, courseId: string) { 15 | switch (state) { 16 | case PurchaseState.Started: 17 | this.state = new BuyCourseSagaStateStarted(); 18 | break; 19 | case PurchaseState.WaitingForPayment: 20 | this.state = new BuyCourseSagaStateWaitingForPayment(); 21 | break; 22 | case PurchaseState.Purchased: 23 | this.state = new BuyCourseSagaStatePurchased(); 24 | break; 25 | case PurchaseState.Cenceled: 26 | this.state = new BuyCourseSagaStateCanceled(); 27 | break; 28 | } 29 | this.state.setContext(this); 30 | this.user.setCourseStatus(courseId, state); 31 | } 32 | 33 | getState() { 34 | return this.state; 35 | } 36 | } -------------------------------------------------------------------------------- /apps/account/src/app/user/sagas/buy-course.state.ts: -------------------------------------------------------------------------------- 1 | import { PaymentStatus } from '@purple/contracts'; 2 | import { UserEntity } from '../entities/user.entity'; 3 | import { BuyCourseSaga } from './buy-course.saga'; 4 | 5 | export abstract class BuyCourseSagaState { 6 | public saga: BuyCourseSaga; 7 | 8 | public setContext(saga: BuyCourseSaga) { 9 | this.saga = saga; 10 | } 11 | 12 | public abstract pay(): Promise<{ paymentLink: string, user: UserEntity }>; 13 | public abstract checkPayment(): Promise<{ user: UserEntity, status: PaymentStatus }>; 14 | public abstract cencel(): Promise<{ user: UserEntity }>; 15 | } -------------------------------------------------------------------------------- /apps/account/src/app/user/sagas/buy-course.steps.ts: -------------------------------------------------------------------------------- 1 | import { CourseGetCourse, PaymentCheck, PaymentGenerateLink, PaymentStatus } from '@purple/contracts'; 2 | import { PurchaseState } from '@purple/interfaces'; 3 | import { UserEntity } from '../entities/user.entity'; 4 | import { BuyCourseSagaState } from './buy-course.state'; 5 | 6 | export class BuyCourseSagaStateStarted extends BuyCourseSagaState { 7 | public async pay(): Promise<{ paymentLink: string; user: UserEntity; }> { 8 | const { course } = await this.saga.rmqService.send(CourseGetCourse.topic, { 9 | id: this.saga.courseId 10 | }); 11 | if (!course) { 12 | throw new Error('Такого курса не существует'); 13 | } 14 | if (course.price == 0) { 15 | this.saga.setState(PurchaseState.Purchased, course._id); 16 | return { paymentLink: null, user: this.saga.user }; 17 | } 18 | const { paymentLink } = await this.saga.rmqService.send(PaymentGenerateLink.topic, { 19 | courseId: course._id, 20 | userId: this.saga.user._id, 21 | sum: course.price 22 | }); 23 | this.saga.setState(PurchaseState.WaitingForPayment, course._id); 24 | return { paymentLink, user: this.saga.user }; 25 | } 26 | public checkPayment(): Promise<{ user: UserEntity; status: PaymentStatus }> { 27 | throw new Error('Нельзя проверить платёж, который не начался'); 28 | } 29 | public async cencel(): Promise<{ user: UserEntity; }> { 30 | this.saga.setState(PurchaseState.Cenceled, this.saga.courseId); 31 | return { user: this.saga.user }; 32 | } 33 | } 34 | 35 | export class BuyCourseSagaStateWaitingForPayment extends BuyCourseSagaState { 36 | public pay(): Promise<{ paymentLink: string; user: UserEntity; }> { 37 | throw new Error('Нельзя создать ссылку на оплату в процессе'); 38 | } 39 | public async checkPayment(): Promise<{ user: UserEntity; status: PaymentStatus }> { 40 | const { status } = await this.saga.rmqService.send(PaymentCheck.topic, { 41 | userId: this.saga.user._id, 42 | courseId: this.saga.courseId 43 | }); 44 | if (status === 'canceled') { 45 | this.saga.setState(PurchaseState.Cenceled, this.saga.courseId); 46 | return { user: this.saga.user, status: 'canceled' }; 47 | } 48 | if (status === 'success') { 49 | return { user: this.saga.user, status: 'success' }; 50 | } 51 | this.saga.setState(PurchaseState.Purchased, this.saga.courseId); 52 | return { user: this.saga.user, status: 'progress' }; 53 | } 54 | public cencel(): Promise<{ user: UserEntity; }> { 55 | throw new Error('Нельзя отменить платёж в процессе'); 56 | } 57 | } 58 | 59 | export class BuyCourseSagaStatePurchased extends BuyCourseSagaState { 60 | public pay(): Promise<{ paymentLink: string; user: UserEntity; }> { 61 | throw new Error('Нельзя оплатить купленный курс'); 62 | } 63 | public checkPayment(): Promise<{ user: UserEntity; status: PaymentStatus }> { 64 | throw new Error('Нельзя проверить платёж по купленному курсу'); 65 | } 66 | public cencel(): Promise<{ user: UserEntity; }> { 67 | throw new Error('Нельзя отменить купленный курс'); 68 | } 69 | } 70 | 71 | export class BuyCourseSagaStateCanceled extends BuyCourseSagaState { 72 | public pay(): Promise<{ paymentLink: string; user: UserEntity; }> { 73 | this.saga.setState(PurchaseState.Started, this.saga.courseId); 74 | return this.saga.getState().pay(); 75 | } 76 | public checkPayment(): Promise<{ user: UserEntity; status: PaymentStatus }> { 77 | throw new Error('Нельзя проверить платёж по отменённому курсу'); 78 | } 79 | public cencel(): Promise<{ user: UserEntity; }> { 80 | throw new Error('Нельзя отменить откменённый курс'); 81 | } 82 | } -------------------------------------------------------------------------------- /apps/account/src/app/user/user.commands.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller } from '@nestjs/common'; 2 | import { AccountBuyCourse, AccountChangeProfile, AccountCheckPayment } from '@purple/contracts'; 3 | import { RMQRoute, RMQValidate } from 'nestjs-rmq'; 4 | import { UserService } from './user.service'; 5 | 6 | @Controller() 7 | export class UserCommands { 8 | constructor(private readonly userService: UserService) {} 9 | 10 | @RMQValidate() 11 | @RMQRoute(AccountChangeProfile.topic) 12 | async changeProfile(@Body() { user, id }: AccountChangeProfile.Request): Promise { 13 | return this.userService.changeProfile(user, id); 14 | } 15 | 16 | @RMQValidate() 17 | @RMQRoute(AccountBuyCourse.topic) 18 | async buyCourse(@Body() { userId, courseId }: AccountBuyCourse.Request): Promise { 19 | return this.userService.buyCourse(userId, courseId); 20 | } 21 | 22 | @RMQValidate() 23 | @RMQRoute(AccountCheckPayment.topic) 24 | async checkPayment(@Body() { userId, courseId }: AccountCheckPayment.Request): Promise { 25 | return this.userService.checkPayments(userId, courseId); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/account/src/app/user/user.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | import { RMQModule, RMQService, RMQTestService } from 'nestjs-rmq'; 4 | import { UserModule } from './user.module'; 5 | import { AuthModule } from '../auth/auth.module'; 6 | import { MongooseModule } from '@nestjs/mongoose'; 7 | import { getMongoConfig } from '../configs/mongo.config' 8 | import { INestApplication } from '@nestjs/common'; 9 | import { UserRepository } from './repositories/user.repository'; 10 | import { AccountBuyCourse, AccountCheckPayment, AccountLogin, AccountRegister, AccountUserInfo, CourseGetCourse, PaymentCheck, PaymentGenerateLink } from '@purple/contracts'; 11 | import { verify } from 'jsonwebtoken'; 12 | 13 | const authLogin: AccountLogin.Request = { 14 | email: 'a2@a.ru', 15 | password: '1' 16 | } 17 | 18 | const authRegister: AccountRegister.Request = { 19 | ...authLogin, 20 | displayName: 'Вася' 21 | } 22 | 23 | const courseId = 'courseId'; 24 | 25 | describe('UserController', () => { 26 | let app: INestApplication; 27 | let userRepository: UserRepository; 28 | let rmqService: RMQTestService; 29 | let configService: ConfigService; 30 | let token: string; 31 | let userId: string; 32 | 33 | beforeAll(async () => { 34 | const module: TestingModule = await Test.createTestingModule({ 35 | imports: [ 36 | ConfigModule.forRoot({ isGlobal: true, envFilePath: 'envs/.account.env' }), 37 | RMQModule.forTest({}), 38 | UserModule, 39 | AuthModule, 40 | MongooseModule.forRootAsync(getMongoConfig()) 41 | ] 42 | }).compile(); 43 | app = module.createNestApplication(); 44 | userRepository = app.get(UserRepository); 45 | rmqService = app.get(RMQService); 46 | configService = app.get(ConfigService); 47 | await app.init(); 48 | 49 | 50 | await rmqService.triggerRoute( 51 | AccountRegister.topic, 52 | authRegister 53 | ); 54 | const { access_token } = await rmqService.triggerRoute( 55 | AccountLogin.topic, 56 | authLogin 57 | ); 58 | token = access_token; 59 | const data = verify(token, configService.get('JWT_SECRET')); 60 | userId = data['id']; 61 | }) 62 | 63 | 64 | it('AccountUserInfo', async () => { 65 | const res = await rmqService.triggerRoute( 66 | AccountUserInfo.topic, 67 | { id: userId } 68 | ); 69 | expect(res.profile.displayName).toEqual(authRegister.displayName); 70 | }); 71 | 72 | it('BuyCourse', async () => { 73 | const paymentLink = 'paymentLink'; 74 | rmqService.mockReply(CourseGetCourse.topic, { 75 | course: { 76 | _id: courseId, 77 | price: 1000 78 | } 79 | }); 80 | rmqService.mockReply(PaymentGenerateLink.topic, { 81 | paymentLink 82 | }); 83 | const res = await rmqService.triggerRoute( 84 | AccountBuyCourse.topic, 85 | { userId, courseId } 86 | ); 87 | expect(res.paymentLink).toEqual(paymentLink); 88 | await expect( 89 | rmqService.triggerRoute( 90 | AccountBuyCourse.topic, 91 | { userId, courseId } 92 | ) 93 | ).rejects.toThrowError(); 94 | }); 95 | 96 | it('BuyCourse', async () => { 97 | rmqService.mockReply(PaymentCheck.topic, { 98 | status: 'success' 99 | }); 100 | const res = await rmqService.triggerRoute( 101 | AccountCheckPayment.topic, 102 | { userId, courseId } 103 | ); 104 | expect(res.status).toEqual('success'); 105 | }); 106 | 107 | afterAll(async () => { 108 | await userRepository.deleteUser(authRegister.email); 109 | app.close(); 110 | }); 111 | }); -------------------------------------------------------------------------------- /apps/account/src/app/user/user.event-immiter.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { RMQService } from 'nestjs-rmq'; 3 | import { UserEntity } from './entities/user.entity'; 4 | 5 | @Injectable() 6 | export class UserEventEmiiter { 7 | constructor(private readonly rmqService: RMQService) {} 8 | 9 | async handle(user: UserEntity) { 10 | for (const event of user.events) { 11 | await this.rmqService.notify(event.topic, event.data); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /apps/account/src/app/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | import { User, UserSchema } from './models/user.model'; 4 | import { UserRepository } from './repositories/user.repository'; 5 | import { UserCommands } from './user.commands'; 6 | import { UserEventEmiiter } from './user.event-immiter'; 7 | import { UserQueries } from './user.queries'; 8 | import { UserService } from './user.service'; 9 | 10 | @Module({ 11 | imports: [MongooseModule.forFeature([ 12 | { name: User.name, schema: UserSchema } 13 | ])], 14 | providers: [UserRepository, UserEventEmiiter, UserService], 15 | exports: [UserRepository], 16 | controllers: [UserCommands, UserQueries], 17 | }) 18 | export class UserModule {} 19 | -------------------------------------------------------------------------------- /apps/account/src/app/user/user.queries.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller } from '@nestjs/common'; 2 | import { AccountUserCourses, AccountUserInfo } from '@purple/contracts'; 3 | import { RMQValidate, RMQRoute } from 'nestjs-rmq'; 4 | import { UserEntity } from './entities/user.entity'; 5 | import { UserRepository } from './repositories/user.repository'; 6 | 7 | @Controller() 8 | export class UserQueries { 9 | constructor(private readonly userRepository: UserRepository) {} 10 | 11 | @RMQValidate() 12 | @RMQRoute(AccountUserInfo.topic) 13 | async userInfo(@Body() { id }: AccountUserInfo.Request): Promise { 14 | const user = await this.userRepository.findUserById(id); 15 | const profile = new UserEntity(user).getPublicProfile(); 16 | return { 17 | profile 18 | }; 19 | } 20 | 21 | @RMQValidate() 22 | @RMQRoute(AccountUserCourses.topic) 23 | async userCourses(@Body() { id }: AccountUserCourses.Request): Promise { 24 | const user = await this.userRepository.findUserById(id); 25 | return { 26 | courses: user.courses 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/account/src/app/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { IUser } from '@purple/interfaces'; 3 | import { RMQService } from 'nestjs-rmq'; 4 | import { UserEntity } from './entities/user.entity'; 5 | import { UserRepository } from './repositories/user.repository'; 6 | import { BuyCourseSaga } from './sagas/buy-course.saga'; 7 | import { UserEventEmiiter } from './user.event-immiter'; 8 | 9 | @Injectable() 10 | export class UserService { 11 | constructor( 12 | private readonly userRepository: UserRepository, 13 | private readonly rmqService: RMQService, 14 | private readonly userEventEmmiter: UserEventEmiiter 15 | ) {} 16 | 17 | public async changeProfile(user: Pick, id: string) { 18 | const existedUser = await this.userRepository.findUserById(id); 19 | if (!existedUser) { 20 | throw new Error('Такого пользователя не существует'); 21 | } 22 | const userEntity = new UserEntity(existedUser).updateProfile(user.displayName); 23 | await this.updateUser(userEntity); 24 | return {}; 25 | } 26 | 27 | public async buyCourse(userId: string, courseId: string) { 28 | const existedUser = await this.userRepository.findUserById(userId); 29 | if (!existedUser) { 30 | throw new Error('Такого пользователя нет'); 31 | } 32 | const userEntity = new UserEntity(existedUser); 33 | const saga = new BuyCourseSaga(userEntity, courseId, this.rmqService); 34 | const { user, paymentLink } = await saga.getState().pay(); 35 | await this.updateUser(user); 36 | return { paymentLink }; 37 | } 38 | 39 | public async checkPayments(userId: string, courseId: string) { 40 | const existedUser = await this.userRepository.findUserById(userId); 41 | if (!existedUser) { 42 | throw new Error('Такого пользователя нет'); 43 | } 44 | const userEntity = new UserEntity(existedUser); 45 | const saga = new BuyCourseSaga(userEntity, courseId, this.rmqService); 46 | const { user, status } = await saga.getState().checkPayment(); 47 | await this.updateUser(user); 48 | return { status }; 49 | } 50 | 51 | private updateUser(user: UserEntity) { 52 | return Promise.all([ 53 | this.userEventEmmiter.handle(user), 54 | this.userRepository.updateUser(user) 55 | ]); 56 | } 57 | } -------------------------------------------------------------------------------- /apps/account/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlariCode/6-microservices-demo-1/cd8ccd792c313ffebe7d98d280e972ddbcde5ac4/apps/account/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/account/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/account/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/account/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { AppModule } from './app/app.module'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | await app.init(); 8 | Logger.log( 9 | `🚀 Accounts is running` 10 | ); 11 | } 12 | 13 | bootstrap(); 14 | -------------------------------------------------------------------------------- /apps/account/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["node"], 7 | "emitDecoratorMetadata": true, 8 | "target": "es2015" 9 | }, 10 | "exclude": ["**/*.spec.ts", "**/*.test.ts"], 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /apps/account/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /apps/account/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts", "src/app/auth/auth.controller.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/api/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/api/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'api', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | testEnvironment: 'node', 10 | transform: { 11 | '^.+\\.[tj]s$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'html'], 14 | coverageDirectory: '../../coverage/apps/api', 15 | }; 16 | -------------------------------------------------------------------------------- /apps/api/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "apps/api", 3 | "sourceRoot": "apps/api/src", 4 | "projectType": "application", 5 | "targets": { 6 | "build": { 7 | "executor": "@nrwl/node:webpack", 8 | "outputs": ["{options.outputPath}"], 9 | "options": { 10 | "outputPath": "dist/apps/api", 11 | "main": "apps/api/src/main.ts", 12 | "tsConfig": "apps/api/tsconfig.app.json", 13 | "assets": ["apps/api/src/assets"] 14 | }, 15 | "configurations": { 16 | "production": { 17 | "optimization": true, 18 | "extractLicenses": true, 19 | "inspect": false, 20 | "fileReplacements": [ 21 | { 22 | "replace": "apps/api/src/environments/environment.ts", 23 | "with": "apps/api/src/environments/environment.prod.ts" 24 | } 25 | ] 26 | } 27 | } 28 | }, 29 | "serve": { 30 | "executor": "@nrwl/node:node", 31 | "options": { 32 | "buildTarget": "api:build" 33 | } 34 | }, 35 | "lint": { 36 | "executor": "@nrwl/linter:eslint", 37 | "outputs": ["{options.outputFile}"], 38 | "options": { 39 | "lintFilePatterns": ["apps/api/**/*.ts"] 40 | } 41 | }, 42 | "test": { 43 | "executor": "@nrwl/jest:jest", 44 | "outputs": ["coverage/apps/api"], 45 | "options": { 46 | "jestConfig": "apps/api/jest.config.js", 47 | "passWithNoTests": true 48 | } 49 | } 50 | }, 51 | "tags": [] 52 | } 53 | -------------------------------------------------------------------------------- /apps/api/src/app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlariCode/6-microservices-demo-1/cd8ccd792c313ffebe7d98d280e972ddbcde5ac4/apps/api/src/app/.gitkeep -------------------------------------------------------------------------------- /apps/api/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | import { PassportModule } from '@nestjs/passport'; 5 | import { ScheduleModule } from '@nestjs/schedule'; 6 | import { RMQModule } from 'nestjs-rmq'; 7 | import { getJWTConfig } from './configs/jwt.config'; 8 | import { getRMQConfig } from './configs/rmq.config'; 9 | import { AuthController } from './controllers/auth.controller'; 10 | import { UserController } from './controllers/user.controller'; 11 | 12 | @Module({ 13 | imports: [ 14 | ConfigModule.forRoot({ envFilePath: 'envs/.api.env', isGlobal: true }), 15 | RMQModule.forRootAsync(getRMQConfig()), 16 | JwtModule.registerAsync(getJWTConfig()), 17 | PassportModule, 18 | ScheduleModule.forRoot() 19 | ], 20 | controllers: [AuthController, UserController] 21 | }) 22 | export class AppModule {} 23 | -------------------------------------------------------------------------------- /apps/api/src/app/configs/jwt.config.ts: -------------------------------------------------------------------------------- 1 | import { ConfigModule, ConfigService } from '@nestjs/config'; 2 | import { JwtModuleAsyncOptions } from '@nestjs/jwt' 3 | 4 | export const getJWTConfig = (): JwtModuleAsyncOptions => ({ 5 | imports: [ConfigModule], 6 | inject: [ConfigService], 7 | useFactory: (configService: ConfigService) => ({ 8 | secret: configService.get('JWT_SECRET') 9 | }) 10 | }); -------------------------------------------------------------------------------- /apps/api/src/app/configs/rmq.config.ts: -------------------------------------------------------------------------------- 1 | import { ConfigModule, ConfigService } from '@nestjs/config'; 2 | import { IRMQServiceAsyncOptions } from 'nestjs-rmq'; 3 | 4 | export const getRMQConfig = (): IRMQServiceAsyncOptions => ({ 5 | inject: [ConfigService], 6 | imports: [ConfigModule], 7 | useFactory: (configService: ConfigService) => ({ 8 | exchangeName: configService.get('AMQP_EXCHANGE') ?? '', 9 | connections: [ 10 | { 11 | login: configService.get('AMQP_USER') ?? '', 12 | password: configService.get('AMQP_PASSWORD') ?? '', 13 | host: configService.get('AMQP_HOSTNAME') ?? '' 14 | } 15 | ], 16 | prefetchCount: 32, 17 | serviceName: 'purple-account' 18 | }) 19 | }) -------------------------------------------------------------------------------- /apps/api/src/app/controllers/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post, UnauthorizedException } from '@nestjs/common'; 2 | import { AccountLogin, AccountRegister } from '@purple/contracts'; 3 | import { RMQService } from 'nestjs-rmq'; 4 | import { LoginDto } from '../dtos/login.dto'; 5 | import { RegisterDto } from '../dtos/register.dto'; 6 | 7 | @Controller('auth') 8 | export class AuthController { 9 | constructor( 10 | private readonly rmqService: RMQService 11 | ) {} 12 | 13 | @Post('register') 14 | async register(@Body() dto: RegisterDto) { 15 | try { 16 | return await this.rmqService.send(AccountRegister.topic, dto, { headers: { requestId: 'adad' } }); 17 | } catch (e) { 18 | if (e instanceof Error) { 19 | throw new UnauthorizedException(e.message); 20 | } 21 | } 22 | } 23 | 24 | @Post('login') 25 | async login(@Body() dto: LoginDto) { 26 | try { 27 | return await this.rmqService.send(AccountLogin.topic, dto); 28 | } catch (e) { 29 | if (e instanceof Error) { 30 | throw new UnauthorizedException(e.message); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/api/src/app/controllers/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Logger, Post, UseGuards } from '@nestjs/common'; 2 | import { Cron } from '@nestjs/schedule'; 3 | import { JWTAuthGuard } from '../guards/jwt.guard'; 4 | import { UserId } from '../guards/user.decorator'; 5 | 6 | @Controller('user') 7 | export class UserController { 8 | constructor() {} 9 | 10 | @UseGuards(JWTAuthGuard) 11 | @Post('info') 12 | async info(@UserId() userId: string) {} 13 | 14 | @Cron('*/5 * * * * *') 15 | async cron() { 16 | Logger.log('Done') 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/api/src/app/dtos/login.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString } from 'class-validator'; 2 | 3 | export class LoginDto { 4 | @IsEmail() 5 | email: string; 6 | 7 | @IsString() 8 | password: string; 9 | } 10 | -------------------------------------------------------------------------------- /apps/api/src/app/dtos/register.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString, IsOptional } from 'class-validator'; 2 | 3 | export class RegisterDto { 4 | @IsEmail() 5 | email: string; 6 | 7 | @IsString() 8 | password: string; 9 | 10 | @IsOptional() 11 | @IsString() 12 | displayName?: string; 13 | } -------------------------------------------------------------------------------- /apps/api/src/app/guards/jwt.guard.ts: -------------------------------------------------------------------------------- 1 | import { AuthGuard } from '@nestjs/passport'; 2 | 3 | export class JWTAuthGuard extends AuthGuard('jwt') {} -------------------------------------------------------------------------------- /apps/api/src/app/guards/user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | 3 | export const UserId = createParamDecorator((data: unknown, ctx: ExecutionContext) => { 4 | return ctx.switchToHttp().getRequest()?.user; 5 | }) -------------------------------------------------------------------------------- /apps/api/src/app/strategies/jwt.straragy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { PassportStrategy } from '@nestjs/passport' 4 | import { IJWTPayload } from '@purple/interfaces'; 5 | import { ExtractJwt, Strategy } from 'passport-jwt'; 6 | 7 | @Injectable() 8 | export class JwtStratagy extends PassportStrategy(Strategy) { 9 | constructor(configService: ConfigService) { 10 | super({ 11 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken, 12 | ignoreExpiration: true, 13 | secretOrKey: configService.get('JWT_SECRET') 14 | }) 15 | } 16 | 17 | async validate({ id }: IJWTPayload) { 18 | return id; 19 | } 20 | } -------------------------------------------------------------------------------- /apps/api/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlariCode/6-microservices-demo-1/cd8ccd792c313ffebe7d98d280e972ddbcde5ac4/apps/api/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/api/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/api/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/api/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | const globalPrefix = 'api'; 9 | app.setGlobalPrefix(globalPrefix); 10 | const port = process.env.PORT || 3333; 11 | await app.listen(port); 12 | Logger.log( 13 | `🚀 API is running on: http://localhost:${port}/${globalPrefix}` 14 | ); 15 | } 16 | 17 | bootstrap(); 18 | -------------------------------------------------------------------------------- /apps/api/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["node"], 7 | "emitDecoratorMetadata": true, 8 | "target": "es2015" 9 | }, 10 | "exclude": ["**/*.spec.ts", "**/*.test.ts"], 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /apps/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /apps/api/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { getJestProjects } = require('@nrwl/jest'); 2 | 3 | module.exports = { 4 | projects: getJestProjects(), 5 | }; 6 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nrwl/jest/preset'); 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlariCode/6-microservices-demo-1/cd8ccd792c313ffebe7d98d280e972ddbcde5ac4/libs/.gitkeep -------------------------------------------------------------------------------- /libs/contracts/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": { 8 | "@typescript-eslint/no-namespace": "off" 9 | } 10 | }, 11 | { 12 | "files": ["*.ts", "*.tsx"], 13 | "rules": {} 14 | }, 15 | { 16 | "files": ["*.js", "*.jsx"], 17 | "rules": {} 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /libs/contracts/README.md: -------------------------------------------------------------------------------- 1 | # contracts 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test contracts` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /libs/contracts/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'contracts', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | testEnvironment: 'node', 10 | transform: { 11 | '^.+\\.[tj]s$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'html'], 14 | coverageDirectory: '../../coverage/libs/contracts', 15 | }; 16 | -------------------------------------------------------------------------------- /libs/contracts/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "libs/contracts", 3 | "sourceRoot": "libs/contracts/src", 4 | "targets": { 5 | "lint": { 6 | "executor": "@nrwl/linter:eslint", 7 | "outputs": ["{options.outputFile}"], 8 | "options": { 9 | "lintFilePatterns": ["libs/contracts/**/*.ts"] 10 | } 11 | }, 12 | "test": { 13 | "executor": "@nrwl/jest:jest", 14 | "outputs": ["coverage/libs/contracts"], 15 | "options": { 16 | "jestConfig": "libs/contracts/jest.config.js", 17 | "passWithNoTests": true 18 | } 19 | } 20 | }, 21 | "tags": [] 22 | } 23 | -------------------------------------------------------------------------------- /libs/contracts/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/account/account.login'; 2 | export * from './lib/account/account.register'; 3 | export * from './lib/account/account.user-info'; 4 | export * from './lib/account/account.user-courses'; 5 | export * from './lib/account/account.change-profile'; 6 | export * from './lib/account/account.buy-course'; 7 | export * from './lib/account/account.check-payment'; 8 | export * from './lib/account/account.changed-course'; 9 | 10 | export * from './lib/course/course.get-course'; 11 | 12 | export * from './lib/payment/payment.generate-link'; 13 | export * from './lib/payment/payment.check'; 14 | 15 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/account/account.buy-course.ts: -------------------------------------------------------------------------------- 1 | import { IsString } from 'class-validator'; 2 | 3 | export namespace AccountBuyCourse { 4 | export const topic = 'account.buy-course.query'; 5 | 6 | export class Request { 7 | @IsString() 8 | userId: string; 9 | 10 | @IsString() 11 | courseId: string; 12 | } 13 | 14 | export class Response { 15 | paymentLink: string; 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/account/account.change-profile.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from '@purple/interfaces'; 2 | import { IsString } from 'class-validator'; 3 | 4 | export namespace AccountChangeProfile { 5 | export const topic = 'account.change-profile.command'; 6 | 7 | export class Request { 8 | @IsString() 9 | id: string; 10 | 11 | @IsString() 12 | user: Pick; 13 | } 14 | 15 | export class Response {} 16 | } 17 | 18 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/account/account.changed-course.ts: -------------------------------------------------------------------------------- 1 | import { PurchaseState } from '@purple/interfaces'; 2 | import { IsString } from 'class-validator'; 3 | 4 | export namespace AccountChangedCourse { 5 | export const topic = 'account.changed-course.event'; 6 | 7 | export class Request { 8 | @IsString() 9 | userId: string; 10 | 11 | @IsString() 12 | courseId: string; 13 | 14 | @IsString() 15 | state: PurchaseState; 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/account/account.check-payment.ts: -------------------------------------------------------------------------------- 1 | import { IsString } from 'class-validator'; 2 | import { PaymentStatus } from '../payment/payment.check'; 3 | 4 | export namespace AccountCheckPayment { 5 | export const topic = 'account.check-payment.command'; 6 | 7 | export class Request { 8 | @IsString() 9 | userId: string; 10 | 11 | @IsString() 12 | courseId: string; 13 | } 14 | 15 | export class Response { 16 | status: PaymentStatus; 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/account/account.login.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString } from 'class-validator'; 2 | 3 | export namespace AccountLogin { 4 | export const topic = 'account.login.command'; 5 | 6 | export class Request { 7 | @IsEmail() 8 | email: string; 9 | 10 | @IsString() 11 | password: string; 12 | } 13 | 14 | export class Response { 15 | access_token: string; 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/account/account.register.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsOptional, IsString } from 'class-validator'; 2 | 3 | export namespace AccountRegister { 4 | export const topic = 'account.register.command'; 5 | 6 | export class Request { 7 | @IsEmail() 8 | email: string; 9 | 10 | @IsString() 11 | password: string; 12 | 13 | @IsOptional() 14 | @IsString() 15 | displayName?: string; 16 | } 17 | 18 | export class Response { 19 | email: string; 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/account/account.user-courses.ts: -------------------------------------------------------------------------------- 1 | import { IUserCourses } from '@purple/interfaces'; 2 | import { IsString } from 'class-validator'; 3 | 4 | export namespace AccountUserCourses { 5 | export const topic = 'account.user-courses.query'; 6 | 7 | export class Request { 8 | @IsString() 9 | id: string; 10 | } 11 | 12 | export class Response { 13 | courses: IUserCourses[]; 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/account/account.user-info.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from '@purple/interfaces'; 2 | import { IsString } from 'class-validator'; 3 | 4 | export namespace AccountUserInfo { 5 | export const topic = 'account.user-info.query'; 6 | 7 | export class Request { 8 | @IsString() 9 | id: string; 10 | } 11 | 12 | export class Response { 13 | profile: Omit; 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/course/course.get-course.ts: -------------------------------------------------------------------------------- 1 | import { ICourse } from '@purple/interfaces'; 2 | import { IsString } from 'class-validator'; 3 | 4 | export namespace CourseGetCourse { 5 | export const topic = 'course.get-course.query'; 6 | 7 | export class Request { 8 | @IsString() 9 | id: string; 10 | } 11 | 12 | export class Response { 13 | course: ICourse | null; 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/payment/payment.check.ts: -------------------------------------------------------------------------------- 1 | import { IsString } from 'class-validator'; 2 | 3 | export type PaymentStatus = 'canceled' | 'success' | 'progress'; 4 | 5 | export namespace PaymentCheck { 6 | export const topic = 'payment.check.query'; 7 | 8 | export class Request { 9 | @IsString() 10 | courseId: string; 11 | 12 | @IsString() 13 | userId: string; 14 | } 15 | 16 | export class Response { 17 | status: PaymentStatus; 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/payment/payment.generate-link.ts: -------------------------------------------------------------------------------- 1 | import { IsNumber, IsString } from 'class-validator'; 2 | 3 | export namespace PaymentGenerateLink { 4 | export const topic = 'payment.generate-link.command'; 5 | 6 | export class Request { 7 | @IsString() 8 | courseId: string; 9 | 10 | @IsString() 11 | userId: string; 12 | 13 | @IsNumber() 14 | sum: number; 15 | } 16 | 17 | export class Response { 18 | paymentLink: string; 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /libs/contracts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | }, 6 | "files": [], 7 | "include": [], 8 | "references": [ 9 | { 10 | "path": "./tsconfig.lib.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /libs/contracts/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": [], 7 | "target": "es6" 8 | }, 9 | "include": ["**/*.ts"], 10 | "exclude": ["**/*.spec.ts", "**/*.test.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /libs/contracts/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/interfaces/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/interfaces/README.md: -------------------------------------------------------------------------------- 1 | # interfaces 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test interfaces` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /libs/interfaces/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'interfaces', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | testEnvironment: 'node', 10 | transform: { 11 | '^.+\\.[tj]s$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'html'], 14 | coverageDirectory: '../../coverage/libs/interfaces', 15 | }; 16 | -------------------------------------------------------------------------------- /libs/interfaces/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "libs/interfaces", 3 | "sourceRoot": "libs/interfaces/src", 4 | "targets": { 5 | "lint": { 6 | "executor": "@nrwl/linter:eslint", 7 | "outputs": ["{options.outputFile}"], 8 | "options": { 9 | "lintFilePatterns": ["libs/interfaces/**/*.ts"] 10 | } 11 | }, 12 | "test": { 13 | "executor": "@nrwl/jest:jest", 14 | "outputs": ["coverage/libs/interfaces"], 15 | "options": { 16 | "jestConfig": "libs/interfaces/jest.config.js", 17 | "passWithNoTests": true 18 | } 19 | } 20 | }, 21 | "tags": [] 22 | } 23 | -------------------------------------------------------------------------------- /libs/interfaces/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/user.interface'; 2 | export * from './lib/auth.interface'; 3 | export * from './lib/course.interface'; 4 | export * from './lib/events.interface'; -------------------------------------------------------------------------------- /libs/interfaces/src/lib/auth.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IJWTPayload { 2 | id: string; 3 | } -------------------------------------------------------------------------------- /libs/interfaces/src/lib/course.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ICourse { 2 | _id: string; 3 | price: number; 4 | } -------------------------------------------------------------------------------- /libs/interfaces/src/lib/events.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IDomainEvent { 2 | topic: string; 3 | data: unknown; 4 | } -------------------------------------------------------------------------------- /libs/interfaces/src/lib/user.interface.ts: -------------------------------------------------------------------------------- 1 | export enum UserRole { 2 | Teacher = 'Teacher', 3 | Student = 'Student' 4 | } 5 | 6 | export enum PurchaseState { 7 | Started = 'Started', 8 | WaitingForPayment = 'WaitingForPayment', 9 | Purchased = 'Purchased', 10 | Cenceled = 'Cenceled' 11 | } 12 | 13 | export interface IUser { 14 | _id?: string; 15 | displayName?: string; 16 | email: string; 17 | passwordHash: string; 18 | role: UserRole; 19 | courses?: IUserCourses[]; 20 | } 21 | 22 | export interface IUserCourses { 23 | courseId: string; 24 | purchaseState: PurchaseState; 25 | } -------------------------------------------------------------------------------- /libs/interfaces/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | }, 6 | "files": [], 7 | "include": [], 8 | "references": [ 9 | { 10 | "path": "./tsconfig.lib.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /libs/interfaces/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": [], 7 | "target": "es6" 8 | }, 9 | "include": ["**/*.ts"], 10 | "exclude": ["**/*.spec.ts", "**/*.test.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /libs/interfaces/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "purple", 3 | "affected": { 4 | "defaultBase": "main" 5 | }, 6 | "cli": { 7 | "defaultCollection": "@nrwl/nest" 8 | }, 9 | "implicitDependencies": { 10 | "package.json": { 11 | "dependencies": "*", 12 | "devDependencies": "*" 13 | }, 14 | ".eslintrc.json": "*" 15 | }, 16 | "tasksRunnerOptions": { 17 | "default": { 18 | "runner": "@nrwl/nx-cloud", 19 | "options": { 20 | "cacheableOperations": [ 21 | "build", 22 | "lint", 23 | "test", 24 | "e2e" 25 | ], 26 | "accessToken": "OGEyNDlkMmUtMDczYi00OWJmLWE1NjEtMzhlM2M3ZTdlOGU1fHJlYWQtd3JpdGU=" 27 | } 28 | } 29 | }, 30 | "targetDependencies": { 31 | "build": [ 32 | { 33 | "target": "build", 34 | "projects": "dependencies" 35 | } 36 | ] 37 | }, 38 | "defaultProject": "account" 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purple", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "nx serve", 7 | "build": "nx build", 8 | "test": "nx test" 9 | }, 10 | "private": true, 11 | "dependencies": { 12 | "@nestjs/common": "^8.0.0", 13 | "@nestjs/config": "^2.0.0", 14 | "@nestjs/core": "^8.0.0", 15 | "@nestjs/jwt": "^8.0.0", 16 | "@nestjs/mongoose": "^9.0.3", 17 | "@nestjs/passport": "^8.2.1", 18 | "@nestjs/platform-express": "^8.0.0", 19 | "@nestjs/schedule": "^2.0.1", 20 | "bcryptjs": "^2.4.3", 21 | "class-transformer": "^0.5.1", 22 | "class-validator": "^0.13.2", 23 | "jsonwebtoken": "^8.5.1", 24 | "mongoose": "^6.3.1", 25 | "nestjs-rmq": "^2.7.2", 26 | "passport": "^0.5.2", 27 | "passport-jwt": "^4.0.0", 28 | "reflect-metadata": "^0.1.13", 29 | "rxjs": "^7.0.0", 30 | "tslib": "^2.0.0" 31 | }, 32 | "devDependencies": { 33 | "@nestjs/schematics": "^8.0.0", 34 | "@nestjs/testing": "^8.0.0", 35 | "@nrwl/cli": "13.10.3", 36 | "@nrwl/eslint-plugin-nx": "13.10.3", 37 | "@nrwl/jest": "13.10.3", 38 | "@nrwl/linter": "13.10.3", 39 | "@nrwl/nest": "13.10.3", 40 | "@nrwl/node": "13.10.3", 41 | "@nrwl/nx-cloud": "latest", 42 | "@nrwl/workspace": "13.10.3", 43 | "@types/bcryptjs": "^2.4.2", 44 | "@types/jest": "27.0.2", 45 | "@types/node": "16.11.7", 46 | "@types/passport-jwt": "^3.0.6", 47 | "@typescript-eslint/eslint-plugin": "~5.18.0", 48 | "@typescript-eslint/parser": "~5.18.0", 49 | "eslint": "~8.12.0", 50 | "eslint-config-prettier": "8.1.0", 51 | "jest": "27.2.3", 52 | "nx": "13.10.3", 53 | "prettier": "^2.5.1", 54 | "ts-jest": "27.0.5", 55 | "typescript": "~4.6.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tools/generators/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlariCode/6-microservices-demo-1/cd8ccd792c313ffebe7d98d280e972ddbcde5ac4/tools/generators/.gitkeep -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2017", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@purple/contracts": ["libs/contracts/src/index.ts"], 19 | "@purple/interfaces": ["libs/interfaces/src/index.ts"] 20 | } 21 | }, 22 | "exclude": ["node_modules", "tmp"] 23 | } 24 | -------------------------------------------------------------------------------- /workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "projects": { 4 | "account": "apps/account", 5 | "api": "apps/api", 6 | "contracts": "libs/contracts", 7 | "interfaces": "libs/interfaces" 8 | } 9 | } 10 | --------------------------------------------------------------------------------