├── _config.yml ├── packages ├── backend │ ├── libs │ │ └── lib │ │ │ ├── src │ │ │ ├── README.md │ │ │ ├── utils │ │ │ │ ├── identity.ts │ │ │ │ ├── datetime.ts │ │ │ │ └── GrpcConfigs.ts │ │ │ ├── logger │ │ │ │ ├── index.ts │ │ │ │ ├── format.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── BootstrapLogger.ts │ │ │ │ ├── message │ │ │ │ │ ├── colorizers.ts │ │ │ │ │ ├── MessagePrinter.ts │ │ │ │ │ └── MessageBuilder.ts │ │ │ │ └── Logger.ts │ │ │ ├── exceptions │ │ │ │ ├── index.ts │ │ │ │ ├── filter │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── handlers │ │ │ │ │ │ ├── interfaces.ts │ │ │ │ │ │ ├── impl │ │ │ │ │ │ │ ├── RpcExceptionHandler.ts │ │ │ │ │ │ │ └── InternalExceptionHandler.ts │ │ │ │ │ │ └── ExceptionHandlerFactory.ts │ │ │ │ │ └── RpcExceptionFilter.ts │ │ │ │ └── impl │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── InternalException.ts │ │ │ │ │ ├── UnavailableException.ts │ │ │ │ │ ├── PermissionDeniedException.ts │ │ │ │ │ ├── NotFoundException.ts │ │ │ │ │ ├── AlreadyExistsException.ts │ │ │ │ │ ├── InvalidArgumentException.ts │ │ │ │ │ ├── code.types.ts │ │ │ │ │ ├── BaseException.ts │ │ │ │ │ └── UnauthenticatedException.ts │ │ │ ├── jwt │ │ │ │ ├── index.ts │ │ │ │ ├── JwtInterface.ts │ │ │ │ ├── JwtGuard.ts │ │ │ │ └── CertsService.ts │ │ │ ├── LibModule.ts │ │ │ └── index.ts │ │ │ ├── tslint.json │ │ │ └── tsconfig.lib.json │ ├── .dockerignore │ ├── .prettierignore │ ├── docker │ │ ├── environments │ │ │ ├── auth.env │ │ │ ├── chat.env │ │ │ ├── user.env │ │ │ └── common.env │ │ ├── postgres │ │ │ └── scripts │ │ │ │ └── create-multiple-dbs.sh │ │ └── docker-compose.yml │ ├── apps │ │ ├── auth │ │ │ ├── tslint.json │ │ │ ├── src │ │ │ │ ├── env.ts │ │ │ │ ├── api │ │ │ │ │ ├── ApiModule.ts │ │ │ │ │ └── auth │ │ │ │ │ │ ├── AuthModule.ts │ │ │ │ │ │ └── dto │ │ │ │ │ │ └── AuthReqDTO.ts │ │ │ │ ├── AppModule.ts │ │ │ │ ├── services │ │ │ │ │ ├── ServicesModule.ts │ │ │ │ │ ├── CertSubscribeService.ts │ │ │ │ │ ├── PemCertsService.ts │ │ │ │ │ └── JwtCertsService.ts │ │ │ │ ├── main.ts │ │ │ │ └── pki-dev │ │ │ │ │ └── keys.ts │ │ │ ├── test │ │ │ │ ├── jest-e2e.json │ │ │ │ └── app.e2e-spec.ts │ │ │ └── tsconfig.app.json │ │ ├── chat │ │ │ ├── tslint.json │ │ │ ├── src │ │ │ │ ├── services │ │ │ │ │ ├── dal │ │ │ │ │ │ ├── db │ │ │ │ │ │ │ ├── migrations │ │ │ │ │ │ │ │ ├── sqls │ │ │ │ │ │ │ │ │ ├── 20191201192021-InitialMigration-down.sql │ │ │ │ │ │ │ │ │ ├── 20191201192257-InsertDemoMessagesMigration-down.sql │ │ │ │ │ │ │ │ │ ├── 20191201192021-InitialMigration-up.sql │ │ │ │ │ │ │ │ │ └── 20191201192257-InsertDemoMessagesMigration-up.sql │ │ │ │ │ │ │ │ ├── 20191201192021-InitialMigration.js │ │ │ │ │ │ │ │ └── 20191201192257-InsertDemoMessagesMigration.js │ │ │ │ │ │ │ ├── database.json │ │ │ │ │ │ │ └── DbModule.ts │ │ │ │ │ │ ├── data-finders │ │ │ │ │ │ │ ├── DataFindersModule.ts │ │ │ │ │ │ │ └── MessageDataFinder.ts │ │ │ │ │ │ ├── data-removers │ │ │ │ │ │ │ ├── DataRemoversModule.ts │ │ │ │ │ │ │ └── MessageDataRemover.ts │ │ │ │ │ │ ├── data-updaters │ │ │ │ │ │ │ ├── DataUpdatersModule.ts │ │ │ │ │ │ │ └── MessageDataUpdater.ts │ │ │ │ │ │ ├── data-producers │ │ │ │ │ │ │ ├── DataProducerModule.ts │ │ │ │ │ │ │ └── MessageDataProducer.ts │ │ │ │ │ │ └── DalModule.ts │ │ │ │ │ ├── ServicesModule.ts │ │ │ │ │ └── ChatEventService.ts │ │ │ │ ├── api │ │ │ │ │ ├── ApiModule.ts │ │ │ │ │ ├── message │ │ │ │ │ │ ├── dto │ │ │ │ │ │ │ ├── DeleteMessageReqDTO.ts │ │ │ │ │ │ │ ├── AddMessageReqDTO.ts │ │ │ │ │ │ │ └── EditMessageReqDTO.ts │ │ │ │ │ │ ├── MessageModule.ts │ │ │ │ │ │ ├── MessageService.ts │ │ │ │ │ │ └── MessageController.ts │ │ │ │ │ └── chat │ │ │ │ │ │ ├── ChatModule.ts │ │ │ │ │ │ ├── ChatService.ts │ │ │ │ │ │ └── ChatController.ts │ │ │ │ ├── AppModule.ts │ │ │ │ ├── env.ts │ │ │ │ └── main.ts │ │ │ ├── test │ │ │ │ ├── jest-e2e.json │ │ │ │ └── app.e2e-spec.ts │ │ │ └── tsconfig.app.json │ │ └── user │ │ │ ├── tslint.json │ │ │ ├── src │ │ │ ├── services │ │ │ │ ├── dal │ │ │ │ │ ├── db │ │ │ │ │ │ ├── migrations │ │ │ │ │ │ │ ├── sqls │ │ │ │ │ │ │ │ ├── 20191201221322-InitialMigration-down.sql │ │ │ │ │ │ │ │ ├── 20191201221519-InsertDemoUsersMigration-down.sql │ │ │ │ │ │ │ │ ├── 20191201221322-InitialMigration-up.sql │ │ │ │ │ │ │ │ └── 20191201221519-InsertDemoUsersMigration-up.sql │ │ │ │ │ │ │ ├── 20191201221322-InitialMigration.js │ │ │ │ │ │ │ └── 20191201221519-InsertDemoUsersMigration.js │ │ │ │ │ │ ├── database.json │ │ │ │ │ │ └── DbModule.ts │ │ │ │ │ ├── data-finders │ │ │ │ │ │ ├── DataFindersModule.ts │ │ │ │ │ │ └── UserDataFinder.ts │ │ │ │ │ ├── data-removers │ │ │ │ │ │ ├── DataRemoversModule.ts │ │ │ │ │ │ └── UserDataRemover.ts │ │ │ │ │ ├── data-updaters │ │ │ │ │ │ ├── DataUpdatersModule.ts │ │ │ │ │ │ └── UserDataUpdater.ts │ │ │ │ │ ├── data-producers │ │ │ │ │ │ ├── DataProducerModule.ts │ │ │ │ │ │ └── UserDataProducer.ts │ │ │ │ │ └── DalModule.ts │ │ │ │ ├── ServicesModule.ts │ │ │ │ └── UserService.ts │ │ │ ├── api │ │ │ │ ├── ApiModule.ts │ │ │ │ └── user │ │ │ │ │ ├── dto │ │ │ │ │ ├── UserReqDTO.ts │ │ │ │ │ ├── VerifyUserReqDTO.ts │ │ │ │ │ ├── UpdateUserReqDTO.ts │ │ │ │ │ └── CreateUserReqDTO.ts │ │ │ │ │ └── UserModule.ts │ │ │ ├── AppModule.ts │ │ │ ├── env.ts │ │ │ └── main.ts │ │ │ ├── test │ │ │ ├── jest-e2e.json │ │ │ └── app.e2e-spec.ts │ │ │ └── tsconfig.app.json │ ├── .prettierrc │ ├── tsconfig.build.json │ ├── test │ │ ├── jest-e2e.json │ │ └── app.e2e-spec.ts │ ├── database.json │ ├── webpack.config.js │ ├── tslint.json │ ├── tsconfig.json │ └── nest-cli.json └── frontend │ ├── src │ ├── app │ │ ├── modules │ │ │ ├── user │ │ │ │ ├── user.component.scss │ │ │ │ ├── user.component.html │ │ │ │ ├── auth │ │ │ │ │ ├── auth.component.scss │ │ │ │ │ ├── auth.component.spec.ts │ │ │ │ │ ├── auth.component.html │ │ │ │ │ └── auth.component.ts │ │ │ │ ├── register │ │ │ │ │ ├── register.component.scss │ │ │ │ │ ├── register.component.spec.ts │ │ │ │ │ ├── register.component.html │ │ │ │ │ └── register.component.ts │ │ │ │ ├── settings │ │ │ │ │ ├── settings.component.scss │ │ │ │ │ ├── settings.component.spec.ts │ │ │ │ │ ├── settings.component.html │ │ │ │ │ └── settings.component.ts │ │ │ │ ├── user.component.ts │ │ │ │ ├── user.component.spec.ts │ │ │ │ ├── user.module.ts │ │ │ │ └── user.routes.ts │ │ │ └── chat │ │ │ │ ├── chat-list │ │ │ │ ├── chat-list.component.scss │ │ │ │ ├── chat-list.component.html │ │ │ │ ├── chat-list.component.spec.ts │ │ │ │ └── chat-list.component.ts │ │ │ │ ├── chat.component.scss │ │ │ │ ├── chat.component.html │ │ │ │ ├── form │ │ │ │ ├── form.component.scss │ │ │ │ ├── form.component.html │ │ │ │ ├── form.component.spec.ts │ │ │ │ └── form.component.ts │ │ │ │ ├── message │ │ │ │ ├── message.component.ts │ │ │ │ ├── message.component.html │ │ │ │ ├── message.component.spec.ts │ │ │ │ └── message.component.scss │ │ │ │ ├── chat.routes.ts │ │ │ │ ├── chat.component.spec.ts │ │ │ │ ├── chat.module.ts │ │ │ │ └── chat.component.ts │ │ ├── grpc │ │ │ ├── helpers │ │ │ │ ├── grpc-jwt.ts │ │ │ │ ├── grpc-get-id.ts │ │ │ │ ├── grpc-metadata.ts │ │ │ │ ├── grpc-unary.ts │ │ │ │ └── grpc-stream.ts │ │ │ ├── enums │ │ │ │ └── stream-type.grpc.enum.ts │ │ │ ├── services │ │ │ │ ├── auth │ │ │ │ │ ├── auth.service.spec.ts │ │ │ │ │ └── auth.service.ts │ │ │ │ ├── chat │ │ │ │ │ ├── chat.service.spec.ts │ │ │ │ │ ├── message.service.spec.ts │ │ │ │ │ ├── chat.service.ts │ │ │ │ │ └── message.service.ts │ │ │ │ └── user │ │ │ │ │ ├── user.service.spec.ts │ │ │ │ │ └── user.service.ts │ │ │ └── grpc.module.ts │ │ ├── app.routes.ts │ │ ├── share │ │ │ ├── services │ │ │ │ ├── message-bus.service.spec.ts │ │ │ │ ├── message-bus.service.ts │ │ │ │ ├── auth.service.spec.ts │ │ │ │ └── auth.service.ts │ │ │ ├── guards │ │ │ │ ├── auth.guard.spec.ts │ │ │ │ ├── auth-child.guard.spec.ts │ │ │ │ ├── auth.guard.ts │ │ │ │ └── auth-child.guard.ts │ │ │ └── share.module.ts │ │ ├── app.component.scss │ │ ├── app.module.ts │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ └── app.component.html │ ├── favicon.ico │ ├── assets │ │ └── avatar │ │ │ ├── avatar-1.png │ │ │ └── avatar-2.png │ ├── tsconfig.app.json │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── tslint.json │ ├── tsconfig.spec.json │ ├── main.ts │ ├── index.html │ ├── test.ts │ ├── karma.conf.js │ ├── styles.scss │ └── polyfills.ts │ ├── proxy.conf.json │ ├── .editorconfig │ ├── e2e │ ├── src │ │ ├── app.po.ts │ │ └── app.e2e-spec.ts │ ├── tsconfig.e2e.json │ └── protractor.conf.js │ ├── browserslist │ ├── tsconfig.json │ ├── README.md │ ├── package.json │ └── tslint.json ├── lerna.json ├── pages-background.png ├── grpc-proto ├── auth │ ├── auth.types.proto │ ├── index.proto │ └── auth.proto ├── chat │ ├── chat.enum.proto │ ├── index.proto │ ├── chat.proto │ ├── chat.types.proto │ └── message.proto └── user │ ├── user.enum.proto │ ├── index.proto │ ├── user.types.proto │ └── user.proto ├── devtools ├── helpers.js ├── build-grpc-back.sed ├── build-grpc-front.js └── build-grpc-back.js ├── .gitignore ├── package.json ├── README.md └── index.html /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-midnight -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/backend/.dockerignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | -------------------------------------------------------------------------------- /packages/backend/.prettierignore: -------------------------------------------------------------------------------- 1 | libs/grpc-proto 2 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/user/user.component.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/backend/docker/environments/auth.env: -------------------------------------------------------------------------------- 1 | JWT_EXPIRE=600 2 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/utils/identity.ts: -------------------------------------------------------------------------------- 1 | export type Identity = T; 2 | -------------------------------------------------------------------------------- /packages/backend/apps/auth/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/backend/apps/user/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json" 3 | } 4 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "0.0.0" 6 | } 7 | -------------------------------------------------------------------------------- /packages/backend/docker/environments/chat.env: -------------------------------------------------------------------------------- 1 | DB_USERNAME=postgres 2 | DB_PASSWORD=postgres 3 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/user/user.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pages-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexDaSoul/nestjs-grpc-angular/HEAD/pages-background.png -------------------------------------------------------------------------------- /grpc-proto/auth/auth.types.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package api.auth; 4 | 5 | message Stub { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /grpc-proto/auth/index.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package api.auth; 4 | 5 | import public "auth.proto"; 6 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/logger/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Logger'; 2 | export * from './BootstrapLogger'; 3 | -------------------------------------------------------------------------------- /packages/backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "tabWidth": 4 5 | } 6 | -------------------------------------------------------------------------------- /packages/backend/docker/environments/user.env: -------------------------------------------------------------------------------- 1 | SALT=SYqSuijVvyUE 2 | 3 | DB_USERNAME=postgres 4 | DB_PASSWORD=postgres 5 | -------------------------------------------------------------------------------- /packages/backend/apps/auth/src/env.ts: -------------------------------------------------------------------------------- 1 | const env = process.env; 2 | 3 | export const JWT_EXPIRE = env.JWT_EXPIRE || 600; 4 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/services/dal/db/migrations/sqls/20191201221322-InitialMigration-down.sql: -------------------------------------------------------------------------------- 1 | drop table api_user; 2 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './impl'; 2 | export * from './filter/RpcExceptionFilter'; 3 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/chat/chat-list/chat-list.component.scss: -------------------------------------------------------------------------------- 1 | .chat-box-messages { 2 | height: 400px; 3 | } 4 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/services/dal/db/migrations/sqls/20191201192021-InitialMigration-down.sql: -------------------------------------------------------------------------------- 1 | drop table api_message; 2 | -------------------------------------------------------------------------------- /packages/frontend/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexDaSoul/nestjs-grpc-angular/HEAD/packages/frontend/src/favicon.ico -------------------------------------------------------------------------------- /packages/backend/apps/user/src/services/dal/db/migrations/sqls/20191201221519-InsertDemoUsersMigration-down.sql: -------------------------------------------------------------------------------- 1 | truncate table api_user cascade; 2 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/jwt/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CertsService'; 2 | export * from './JwtGuard'; 3 | export * from './JwtInterface'; 4 | -------------------------------------------------------------------------------- /packages/frontend/src/app/grpc/helpers/grpc-jwt.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from 'rxjs'; 2 | 3 | export const jwtAuthError$ = new Subject(); 4 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/services/dal/db/migrations/sqls/20191201192257-InsertDemoMessagesMigration-down.sql: -------------------------------------------------------------------------------- 1 | truncate table api_message cascade; 2 | -------------------------------------------------------------------------------- /packages/frontend/src/assets/avatar/avatar-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexDaSoul/nestjs-grpc-angular/HEAD/packages/frontend/src/assets/avatar/avatar-1.png -------------------------------------------------------------------------------- /packages/frontend/src/assets/avatar/avatar-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexDaSoul/nestjs-grpc-angular/HEAD/packages/frontend/src/assets/avatar/avatar-2.png -------------------------------------------------------------------------------- /packages/frontend/src/app/grpc/enums/stream-type.grpc.enum.ts: -------------------------------------------------------------------------------- 1 | export enum StreamType { 2 | DATA = 'data', 3 | STATUS = 'status', 4 | END = 'end', 5 | } 6 | -------------------------------------------------------------------------------- /grpc-proto/chat/chat.enum.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package api.chat; 4 | 5 | enum EStatus { 6 | UNKNOWN = 0; 7 | SUCCESS = 1; 8 | ERROR = 2; 9 | } 10 | -------------------------------------------------------------------------------- /grpc-proto/user/user.enum.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package api.user; 4 | 5 | enum EStatus { 6 | UNKNOWN = 0; 7 | SUCCESS = 1; 8 | ERROR = 2; 9 | } 10 | -------------------------------------------------------------------------------- /grpc-proto/user/index.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package api.user; 4 | 5 | import public "user.types.proto"; 6 | import public "user.enum.proto"; 7 | import public "user.proto"; 8 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/LibModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | @Module({ 4 | providers: [], 5 | exports: [], 6 | }) 7 | export class LibModule { 8 | } 9 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/user/auth/auth.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: grid; 3 | width: 100%; 4 | height: 100%; 5 | align-items: center; 6 | justify-items: center; 7 | } 8 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/user/register/register.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: grid; 3 | width: 100%; 4 | height: 100%; 5 | align-items: center; 6 | justify-items: center; 7 | } 8 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/user/settings/settings.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: grid; 3 | width: 100%; 4 | height: 100%; 5 | align-items: center; 6 | justify-items: center; 7 | } 8 | -------------------------------------------------------------------------------- /packages/backend/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "node_modules", 5 | "test", 6 | "dist", 7 | "**/*spec.ts" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /grpc-proto/chat/index.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package api.chat; 4 | 5 | import public "chat.types.proto"; 6 | import public "chat.enum.proto"; 7 | import public "chat.proto"; 8 | import public "message.proto"; 9 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/utils/datetime.ts: -------------------------------------------------------------------------------- 1 | export const curUnixTime: number = Math.floor(new Date().getTime() / 1000); 2 | 3 | export const daysFromNow = (days: number = 0) => ({seconds: curUnixTime - 60 * 60 * 24 * days}); 4 | -------------------------------------------------------------------------------- /packages/frontend/src/app/grpc/helpers/grpc-get-id.ts: -------------------------------------------------------------------------------- 1 | export const getUserIdFromJWT = (jwt: string): string => { 2 | const token = jwt.split('.')[1]; 3 | const payload = JSON.parse(atob(token)); 4 | 5 | return payload.id; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/chat/chat.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | padding: 1rem; 3 | } 4 | 5 | .chat-box { 6 | width: 600px; 7 | margin: auto; 8 | background: #1b1b1b; 9 | padding: .15rem .15rem .2rem; 10 | } 11 | -------------------------------------------------------------------------------- /packages/backend/apps/auth/src/api/ApiModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { AuthModule } from './auth/AuthModule'; 4 | 5 | @Module({ 6 | imports: [AuthModule], 7 | }) 8 | export class ApiModule { 9 | } 10 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/api/ApiModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { UserModule } from './user/UserModule'; 4 | 5 | @Module({ 6 | imports: [UserModule], 7 | }) 8 | export class ApiModule { 9 | } 10 | -------------------------------------------------------------------------------- /grpc-proto/user/user.types.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package api.user; 4 | 5 | message User { 6 | string id = 1; 7 | string name = 2; 8 | string email = 3; 9 | string avatar = 4; 10 | } 11 | 12 | message Stub { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /packages/backend/apps/auth/src/AppModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ApiModule } from './api/ApiModule'; 4 | 5 | @Module({ 6 | imports: [ 7 | ApiModule, 8 | ], 9 | }) 10 | export class AppModule { 11 | } 12 | -------------------------------------------------------------------------------- /packages/frontend/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api/*": { 3 | "target": "http://127.0.0.1:443", 4 | "secure": false, 5 | "pathRewrite": { 6 | "^/api/*": "" 7 | }, 8 | "changeOrigin": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/chat/chat.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/logger/format.ts: -------------------------------------------------------------------------------- 1 | export const padStart = (data: number, padNum: number = 2): string => data.toString().padStart(padNum, '0'); 2 | 3 | export const padEnd = (data: number, padNum: number = 3): string => data.toString().padEnd(padNum, '0'); 4 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './LibModule'; 2 | export * from './utils/datetime'; 3 | export * from './utils/GrpcConfigs'; 4 | export * from './utils/identity'; 5 | export * from './logger'; 6 | export * from './jwt'; 7 | export * from './exceptions'; 8 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/chat/form/form.component.scss: -------------------------------------------------------------------------------- 1 | form { 2 | margin-top: .5rem; 3 | padding-top: .5rem; 4 | border-top: 1px #3c3c3c solid; 5 | } 6 | 7 | button { 8 | width: 100%; 9 | background: #00564d; 10 | color: #ffffff; 11 | } 12 | -------------------------------------------------------------------------------- /grpc-proto/chat/chat.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package api.chat; 4 | 5 | import "chat.types.proto"; 6 | 7 | service ChatService { 8 | rpc GetChat (Stub) returns (stream ChatList) { 9 | } 10 | } 11 | 12 | message ChatList { 13 | repeated Message messages = 1; 14 | } 15 | -------------------------------------------------------------------------------- /packages/backend/apps/auth/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "js", 4 | "json", 5 | "ts" 6 | ], 7 | "rootDir": ".", 8 | "testEnvironment": "node", 9 | "testRegex": ".e2e-spec.ts$", 10 | "transform": { 11 | "^.+\\.(t|j)s$": "ts-jest" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "js", 4 | "json", 5 | "ts" 6 | ], 7 | "rootDir": ".", 8 | "testEnvironment": "node", 9 | "testRegex": ".e2e-spec.ts$", 10 | "transform": { 11 | "^.+\\.(t|j)s$": "ts-jest" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/api/user/dto/UserReqDTO.ts: -------------------------------------------------------------------------------- 1 | import { IsUUID, IsDefined } from 'class-validator'; 2 | 3 | import { api } from '@grpc-proto/user/user'; 4 | 5 | export class UserReqDTO implements api.user.UserReq { 6 | @IsDefined() 7 | @IsUUID() 8 | public id: string; 9 | } 10 | -------------------------------------------------------------------------------- /packages/backend/apps/user/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "js", 4 | "json", 5 | "ts" 6 | ], 7 | "rootDir": ".", 8 | "testEnvironment": "node", 9 | "testRegex": ".e2e-spec.ts$", 10 | "transform": { 11 | "^.+\\.(t|j)s$": "ts-jest" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/frontend/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts", 10 | "app/grpc/proto/**/*" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/api/ApiModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ChatModule } from './chat/ChatModule'; 4 | import { MessageModule } from './message/MessageModule'; 5 | 6 | @Module({ 7 | imports: [ChatModule, MessageModule], 8 | }) 9 | export class ApiModule { 10 | } 11 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/exceptions/filter/types.ts: -------------------------------------------------------------------------------- 1 | import { RpcException } from '@nestjs/microservices'; 2 | import { BaseException } from '../impl/BaseException'; 3 | 4 | export type ExceptionType = Error | RpcException | BaseException; 5 | 6 | export const EXCEPTION_LIST = [Error, RpcException, BaseException]; 7 | -------------------------------------------------------------------------------- /packages/backend/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "js", 4 | "json", 5 | "ts" 6 | ], 7 | "rootDir": ".", 8 | "testEnvironment": "node", 9 | "testRegex": ".e2e-spec.ts$", 10 | "transform": { 11 | "^.+\\.(t|j)s$": "ts-jest" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/backend/docker/environments/common.env: -------------------------------------------------------------------------------- 1 | DEVELOPMENT=true 2 | 3 | GRPC_USER_SERVICE=user:443 4 | GRPC_AUTH_SERVICE=auth:443 5 | GRPC_CHAT_SERVICE=chat:443 6 | 7 | DB_HOST=api-postgres 8 | DB_PORT=5432 9 | DB_DATABASE_USER=user 10 | DB_DATABASE_CHAT=chat 11 | 12 | LOGGER_LEVEL=info 13 | LOGGER_COLORIZE_MESSAGES=true 14 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/jwt/JwtInterface.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { Metadata } from 'grpc'; 3 | 4 | export interface IJwtMeta extends Metadata { 5 | payload: T; 6 | } 7 | 8 | export interface IAuthService { 9 | verifyAuthToken(token: string): Observable<{ verify: boolean; }>; 10 | } 11 | -------------------------------------------------------------------------------- /packages/frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /packages/frontend/src/app/grpc/helpers/grpc-metadata.ts: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'grpc-web'; 2 | import { environment } from '@environments/environment'; 3 | 4 | export function grpcJwtMetadata(token: string = null): Metadata { 5 | return { 6 | Authorization: token || localStorage.getItem(environment.token), 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/api/message/dto/DeleteMessageReqDTO.ts: -------------------------------------------------------------------------------- 1 | import { IsUUID, IsDefined } from 'class-validator'; 2 | 3 | import { api } from '@grpc-proto/chat/message'; 4 | 5 | export class DeleteMessageReqDTO implements api.chat.DeleteMessageReq { 6 | @IsDefined() 7 | @IsUUID() 8 | public id: string; 9 | } 10 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/chat/chat-list/chat-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /packages/frontend/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root h1')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/services/dal/db/database.json: -------------------------------------------------------------------------------- 1 | { 2 | "chat": { 3 | "driver": "pg", 4 | "host": {"ENV": "DB_HOST"}, 5 | "port": {"ENV": "DB_PORT"}, 6 | "user": {"ENV": "DB_USERNAME"}, 7 | "password": {"ENV": "DB_PASSWORD"}, 8 | "database": {"ENV": "DB_DATABASE_CHAT"} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/AppModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { CertsService } from '@lib/jwt/CertsService'; 4 | import { ApiModule } from './api/ApiModule'; 5 | 6 | @Module({ 7 | imports: [ 8 | ApiModule, 9 | ], 10 | providers: [CertsService], 11 | }) 12 | export class AppModule { 13 | } 14 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/services/dal/db/database.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "driver": "pg", 4 | "host": {"ENV": "DB_HOST"}, 5 | "port": {"ENV": "DB_PORT"}, 6 | "user": {"ENV": "DB_USERNAME"}, 7 | "password": {"ENV": "DB_PASSWORD"}, 8 | "database": {"ENV": "DB_DATABASE_USER"} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/frontend/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | import { LoggerConfig, NgxLoggerLevel } from 'ngx-logger'; 2 | 3 | export const environment = { 4 | production: true, 5 | url: '/api', 6 | token: 'pAjjaWcqFQkr', 7 | authDiff: 60, 8 | logger: { 9 | level: NgxLoggerLevel.ERROR, 10 | } as LoggerConfig, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/backend/apps/auth/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../dist/apps/auth" 6 | }, 7 | "include": [ 8 | "src/**/*" 9 | ], 10 | "exclude": [ 11 | "node_modules", 12 | "dist", 13 | "test", 14 | "**/*spec.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/AppModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { CertsService } from '@lib/jwt/CertsService'; 4 | 5 | import { ApiModule } from './api/ApiModule'; 6 | 7 | @Module({ 8 | imports: [ 9 | ApiModule, 10 | ], 11 | providers: [CertsService], 12 | }) 13 | export class AppModule { 14 | } 15 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../dist/apps/chat" 6 | }, 7 | "include": [ 8 | "src/**/*" 9 | ], 10 | "exclude": [ 11 | "node_modules", 12 | "dist", 13 | "test", 14 | "**/*spec.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/backend/apps/user/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../dist/apps/user" 6 | }, 7 | "include": [ 8 | "src/**/*" 9 | ], 10 | "exclude": [ 11 | "node_modules", 12 | "dist", 13 | "test", 14 | "**/*spec.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/backend/apps/auth/src/api/auth/AuthModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ServicesModule } from '@auth/services/ServicesModule'; 4 | import { AuthController } from './AuthController'; 5 | 6 | @Module({ 7 | imports: [ServicesModule], 8 | controllers: [AuthController], 9 | }) 10 | export class AuthModule { 11 | } 12 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/api/user/UserModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ServicesModule } from '@user/services/ServicesModule'; 4 | import { UserController } from './UserController'; 5 | 6 | @Module({ 7 | imports: [ServicesModule], 8 | controllers: [UserController], 9 | }) 10 | export class UserModule { 11 | } 12 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/api/message/dto/AddMessageReqDTO.ts: -------------------------------------------------------------------------------- 1 | import { IsDefined, IsString, MaxLength } from 'class-validator'; 2 | 3 | import { api } from '@grpc-proto/chat/message'; 4 | 5 | export class AddMessageReqDTO implements api.chat.SendMessageReq { 6 | @IsDefined() 7 | @IsString() 8 | @MaxLength(500) 9 | public message: string; 10 | } 11 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/exceptions/impl/index.ts: -------------------------------------------------------------------------------- 1 | export * from './code.types'; 2 | export * from './InvalidArgumentException'; 3 | export * from './NotFoundException'; 4 | export * from './AlreadyExistsException'; 5 | export * from './PermissionDeniedException'; 6 | export * from './InternalException'; 7 | export * from './UnavailableException'; 8 | export * from './UnauthenticatedException'; 9 | -------------------------------------------------------------------------------- /packages/frontend/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | const routes: Routes = [ 5 | { 6 | path: '**', 7 | pathMatch: 'full', 8 | redirectTo: 'chat', 9 | }, 10 | ]; 11 | 12 | export const RoutingModule: ModuleWithProviders = RouterModule.forRoot(routes); 13 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/lib" 6 | }, 7 | "include": [ 8 | "src/**/*" 9 | ], 10 | "exclude": [ 11 | "node_modules", 12 | "dist", 13 | "test", 14 | "**/*spec.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/chat/form/form.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 |
8 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/services/ServicesModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { DalModule } from './dal/DalModule'; 4 | import { UserService } from './UserService'; 5 | 6 | @Module({ 7 | imports: [DalModule], 8 | providers: [UserService], 9 | exports: [UserService], 10 | }) 11 | export class ServicesModule { 12 | } 13 | 14 | export * from './dal/DalModule'; 15 | -------------------------------------------------------------------------------- /packages/frontend/src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/services/dal/data-finders/DataFindersModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { DbModule } from '@user/services/dal/db/DbModule'; 4 | 5 | import { UserDataFinder } from './UserDataFinder'; 6 | 7 | @Module({ 8 | imports: [DbModule], 9 | providers: [UserDataFinder], 10 | exports: [UserDataFinder], 11 | }) 12 | export class DataFindersModule { 13 | } 14 | -------------------------------------------------------------------------------- /packages/backend/database.json: -------------------------------------------------------------------------------- 1 | { 2 | "chat": { 3 | "driver": "pg", 4 | "host": "localhost", 5 | "user": "postgres", 6 | "password": "postgres", 7 | "database": "chat" 8 | }, 9 | "user": { 10 | "driver": "pg", 11 | "host": "localhost", 12 | "user": "postgres", 13 | "password": "postgres", 14 | "database": "user" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/exceptions/filter/handlers/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { BaseException } from '../../impl/BaseException'; 2 | import { ExceptionType } from '../types'; 3 | 4 | export interface IExceptionHandler { 5 | wrapError(): BaseException; 6 | 7 | warnAboutError(): void; 8 | } 9 | 10 | export interface IExceptionHandlerFactory { 11 | getHandler(exception: ExceptionType): IExceptionHandler; 12 | } 13 | -------------------------------------------------------------------------------- /packages/frontend/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/services/dal/data-finders/DataFindersModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { DbModule } from '@chat/services/dal/db/DbModule'; 4 | 5 | import { MessageDataFinder } from './MessageDataFinder'; 6 | 7 | @Module({ 8 | imports: [DbModule], 9 | providers: [MessageDataFinder], 10 | exports: [MessageDataFinder], 11 | }) 12 | export class DataFindersModule { 13 | } 14 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/services/ServicesModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { DalModule } from './dal/DalModule'; 4 | import { ChatEventService } from './ChatEventService'; 5 | 6 | @Module({ 7 | imports: [DalModule], 8 | providers: [ChatEventService], 9 | exports: [DalModule, ChatEventService], 10 | }) 11 | export class ServicesModule { 12 | } 13 | 14 | export * from './dal/DalModule'; 15 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/api/chat/ChatModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ServicesModule } from '@chat/services/ServicesModule'; 4 | 5 | import { ChatController } from './ChatController'; 6 | import { ChatService } from './ChatService'; 7 | 8 | @Module({ 9 | imports: [ServicesModule], 10 | controllers: [ChatController], 11 | providers: [ChatService], 12 | }) 13 | export class ChatModule { 14 | } 15 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/api/message/dto/EditMessageReqDTO.ts: -------------------------------------------------------------------------------- 1 | import { IsUUID, IsDefined, IsString, MaxLength } from 'class-validator'; 2 | 3 | import { api } from '@grpc-proto/chat/message'; 4 | 5 | export class EditMessageReqDTO implements api.chat.EditMessageReq { 6 | @IsDefined() 7 | @IsUUID() 8 | public id: string; 9 | 10 | @IsDefined() 11 | @IsString() 12 | @MaxLength(500) 13 | public message: string; 14 | } 15 | -------------------------------------------------------------------------------- /packages/frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /packages/backend/apps/auth/src/api/auth/dto/AuthReqDTO.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsDefined, IsString, MaxLength } from 'class-validator'; 2 | 3 | import { api } from '@grpc-proto/auth/auth'; 4 | 5 | export class AuthReqDTO implements api.auth.AuthReq { 6 | @IsDefined() 7 | @IsEmail() 8 | @MaxLength(50) 9 | public email: string; 10 | 11 | @IsDefined() 12 | @IsString() 13 | @MaxLength(128) 14 | public password: string; 15 | } 16 | -------------------------------------------------------------------------------- /packages/frontend/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /packages/frontend/src/app/grpc/services/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthGrpcService } from './auth.service'; 4 | 5 | describe('AuthGrpcService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: AuthGrpcService = TestBed.get(AuthGrpcService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/frontend/src/app/grpc/services/chat/chat.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatGrpcService } from './chat.service'; 4 | 5 | describe('TaskGrpcService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: ChatGrpcService = TestBed.get(ChatGrpcService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/frontend/src/app/grpc/services/user/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { UserGrpcService } from './user.service'; 4 | 5 | describe('UserGrpcService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: UserGrpcService = TestBed.get(UserGrpcService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/api/message/MessageModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ServicesModule } from '@chat/services/ServicesModule'; 4 | 5 | import { MessageController } from './MessageController'; 6 | import { MessageService } from './MessageService'; 7 | 8 | @Module({ 9 | imports: [ServicesModule], 10 | controllers: [MessageController], 11 | providers: [MessageService], 12 | }) 13 | export class MessageModule { 14 | } 15 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/api/user/dto/VerifyUserReqDTO.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsDefined, IsString, MaxLength } from 'class-validator'; 2 | 3 | import { api } from '@grpc-proto/user/user'; 4 | 5 | export class VerifyUserReqDTO implements api.user.VerifyUserReq { 6 | @IsDefined() 7 | @IsEmail() 8 | @MaxLength(50) 9 | public email: string; 10 | 11 | @IsDefined() 12 | @IsString() 13 | @MaxLength(128) 14 | public password: string; 15 | } 16 | -------------------------------------------------------------------------------- /packages/frontend/src/app/grpc/services/chat/message.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { MessageGrpcService } from './message.service'; 4 | 5 | describe('StatusGrpcService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: MessageGrpcService = TestBed.get(MessageGrpcService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/frontend/src/app/share/services/message-bus.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { MessageBusService } from './message-bus.service'; 4 | 5 | describe('MessageBusService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: MessageBusService = TestBed.get(MessageBusService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/user/user.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-user', 5 | templateUrl: './user.component.html', 6 | styleUrls: ['./user.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush, 8 | }) 9 | export class UserComponent implements OnInit { 10 | 11 | constructor() { 12 | } 13 | 14 | ngOnInit() { 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /packages/frontend/src/app/share/guards/auth.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async, inject } from '@angular/core/testing'; 2 | 3 | import { AuthGuard } from './auth.guard'; 4 | 5 | describe('AuthGuard', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [AuthGuard], 9 | }); 10 | }); 11 | 12 | it('should ...', inject([AuthGuard], (guard: AuthGuard) => { 13 | expect(guard).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /grpc-proto/chat/chat.types.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package api.chat; 4 | 5 | import "chat.enum.proto"; 6 | 7 | message ChatRes { 8 | EStatus status = 1; 9 | string message = 2; 10 | } 11 | 12 | message Stub { 13 | 14 | } 15 | 16 | message Author { 17 | string id = 1; 18 | string name = 2; 19 | string avatar = 3; 20 | } 21 | 22 | message Message { 23 | string id = 1; 24 | Author author = 2; 25 | string message = 3; 26 | string updatedAt = 4; 27 | } 28 | -------------------------------------------------------------------------------- /packages/backend/apps/auth/src/services/ServicesModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { JwtCertsService } from './JwtCertsService'; 4 | import { PemCertsService } from './PemCertsService'; 5 | import { CertSubscribeService } from './CertSubscribeService'; 6 | 7 | @Module({ 8 | providers: [JwtCertsService, PemCertsService, CertSubscribeService], 9 | exports: [JwtCertsService, PemCertsService, CertSubscribeService], 10 | }) 11 | export class ServicesModule { 12 | } 13 | -------------------------------------------------------------------------------- /packages/backend/apps/auth/src/services/CertSubscribeService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Observable, ReplaySubject } from 'rxjs'; 3 | 4 | @Injectable() 5 | export class CertSubscribeService { 6 | private readonly publicKey = new ReplaySubject(1); 7 | 8 | public getCert(): Observable { 9 | return this.publicKey.asObservable(); 10 | } 11 | 12 | public setCert(key: string): void { 13 | this.publicKey.next(key); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/chat/message/message.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | import { Message } from '@grpc/proto/chat/chat.types_pb'; 4 | 5 | @Component({ 6 | selector: 'app-message', 7 | templateUrl: './message.component.html', 8 | styleUrls: ['./message.component.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | }) 11 | export class MessageComponent { 12 | @Input() public message: Message.AsObject; 13 | } 14 | -------------------------------------------------------------------------------- /packages/frontend/src/app/share/guards/auth-child.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async, inject } from '@angular/core/testing'; 2 | 3 | import { AuthChildGuard } from './auth-child.guard'; 4 | 5 | describe('AuthChildGuard', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [AuthChildGuard], 9 | }); 10 | }); 11 | 12 | it('should ...', inject([AuthChildGuard], (guard: AuthChildGuard) => { 13 | expect(guard).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/services/dal/data-removers/DataRemoversModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { DbModule } from '@user/services/dal/db/DbModule'; 4 | import { DataFindersModule } from '@user/services/dal/data-finders/DataFindersModule'; 5 | 6 | import { UserDataRemover } from './UserDataRemover'; 7 | 8 | @Module({ 9 | imports: [DbModule, DataFindersModule], 10 | providers: [UserDataRemover], 11 | exports: [UserDataRemover], 12 | }) 13 | export class DataRemoversModule { 14 | } 15 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/services/dal/data-updaters/DataUpdatersModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { DbModule } from '@user/services/dal/db/DbModule'; 4 | import { DataFindersModule } from '@user/services/dal/data-finders/DataFindersModule'; 5 | 6 | import { UserDataUpdater } from './UserDataUpdater'; 7 | 8 | @Module({ 9 | imports: [DbModule, DataFindersModule], 10 | providers: [UserDataUpdater], 11 | exports: [UserDataUpdater], 12 | }) 13 | export class DataUpdatersModule { 14 | } 15 | -------------------------------------------------------------------------------- /packages/backend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | 3 | module.exports = function (options) { 4 | options.resolve.alias = { 5 | '@grpc-proto': join(process.cwd(), './libs/grpc-proto'), 6 | '@lib': join(process.cwd(), './libs/lib'), 7 | '@auth': join(process.cwd(), './apps/auth/src'), 8 | '@chat': join(process.cwd(), './apps/chat/src'), 9 | '@user': join(process.cwd(), './apps/user/src'), 10 | }; 11 | 12 | return { 13 | ...options, 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/services/dal/data-producers/DataProducerModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { DbModule } from '@user/services/dal/db/DbModule'; 4 | import { DataFindersModule } from '@user/services/dal/data-finders/DataFindersModule'; 5 | 6 | import { UserDataProducer } from './UserDataProducer'; 7 | 8 | @Module({ 9 | imports: [DbModule, DataFindersModule], 10 | providers: [UserDataProducer], 11 | exports: [UserDataProducer], 12 | }) 13 | export class DataProducerModule { 14 | } 15 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/chat/chat.routes.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { ChatComponent } from './chat.component'; 5 | import { AuthGuard } from '@share/guards/auth.guard'; 6 | 7 | const routes: Routes = [ 8 | { 9 | path: 'chat', 10 | component: ChatComponent, 11 | canActivate: [AuthGuard], 12 | }, 13 | ]; 14 | 15 | export const RoutingModule: ModuleWithProviders = RouterModule.forChild(routes); 16 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/chat/message/message.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | {{ message.author.name }} 5 |
{{ message.updatedat | date: 'short' }}
6 |
7 | 8 |

9 | {{ message.message }} 10 |

11 |
12 |
13 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/services/dal/data-removers/DataRemoversModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { DbModule } from '@chat/services/dal/db/DbModule'; 4 | import { DataFindersModule } from '@chat/services/dal/data-finders/DataFindersModule'; 5 | 6 | import { MessageDataRemover } from './MessageDataRemover'; 7 | 8 | @Module({ 9 | imports: [DbModule, DataFindersModule], 10 | providers: [MessageDataRemover], 11 | exports: [MessageDataRemover], 12 | }) 13 | export class DataRemoversModule { 14 | } 15 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/services/dal/data-updaters/DataUpdatersModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { DbModule } from '@chat/services/dal/db/DbModule'; 4 | import { DataFindersModule } from '@chat/services/dal/data-finders/DataFindersModule'; 5 | 6 | import { MessageDataUpdater } from './MessageDataUpdater'; 7 | 8 | @Module({ 9 | imports: [DbModule, DataFindersModule], 10 | providers: [MessageDataUpdater], 11 | exports: [MessageDataUpdater], 12 | }) 13 | export class DataUpdatersModule { 14 | } 15 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/exceptions/impl/InternalException.ts: -------------------------------------------------------------------------------- 1 | import { BaseException, ErrorCodeType, MetadataType } from './BaseException'; 2 | 3 | import { IError, ECodes } from './code.types'; 4 | 5 | export const INTERNAL_ERROR: IError = { 6 | code: ECodes.INTERNAL_ERROR, 7 | message: 'Internal error', 8 | }; 9 | 10 | export class InternalException extends BaseException { 11 | constructor(customCode?: ErrorCodeType, metadata: MetadataType = {}) { 12 | super(customCode || INTERNAL_ERROR, metadata); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/exceptions/impl/UnavailableException.ts: -------------------------------------------------------------------------------- 1 | import { BaseException, ErrorCodeType, MetadataType } from './BaseException'; 2 | 3 | import { IError, ECodes } from './code.types'; 4 | 5 | export const UNAVAILABLE: IError = { 6 | code: ECodes.UNAVAILABLE, 7 | message: 'Resource unavailable', 8 | }; 9 | 10 | export class UnavailableException extends BaseException { 11 | constructor(customCode?: ErrorCodeType, metadata: MetadataType = {}) { 12 | super(customCode || UNAVAILABLE, metadata); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/services/dal/data-producers/DataProducerModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { DbModule } from '@chat/services/dal/db/DbModule'; 4 | import { DataFindersModule } from '@chat/services/dal/data-finders/DataFindersModule'; 5 | 6 | import { MessageDataProducer } from './MessageDataProducer'; 7 | 8 | @Module({ 9 | imports: [DbModule, DataFindersModule], 10 | providers: [MessageDataProducer], 11 | exports: [MessageDataProducer], 12 | }) 13 | export class DataProducerModule { 14 | } 15 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/env.ts: -------------------------------------------------------------------------------- 1 | import { ClientConfig } from 'pg'; 2 | 3 | const env = process.env; 4 | 5 | export const dbConfig: ClientConfig = { 6 | host: env.DB_HOST || 'localhost', 7 | port: +env.DB_PORT || 5432, 8 | user: env.DB_USERNAME || 'postgres', 9 | password: env.DB_PASSWORD || 'postgres', 10 | database: env.DB_DATABASE_CHAT || 'chat', 11 | keepAlive: true, 12 | }; 13 | 14 | export const migrateConfig = { 15 | cwd: `./apps/chat/src/services/dal/db`, 16 | env: 'chat', 17 | string: './database.json', 18 | }; 19 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/exceptions/impl/PermissionDeniedException.ts: -------------------------------------------------------------------------------- 1 | import { BaseException, ErrorCodeType, MetadataType } from './BaseException'; 2 | 3 | import { IError, ECodes } from './code.types'; 4 | 5 | export const PERMISSION_DENIED: IError = { 6 | code: ECodes.PERMISSION_DENIED, 7 | message: 'Permission denied', 8 | }; 9 | 10 | export class PermissionDeniedException extends BaseException { 11 | constructor(customCode?: ErrorCodeType, metadata: MetadataType = {}) { 12 | super(customCode || PERMISSION_DENIED, metadata); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /grpc-proto/auth/auth.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package api.auth; 4 | 5 | import "auth.types.proto"; 6 | 7 | service AuthService { 8 | rpc Auth (AuthReq) returns (AuthRes) { 9 | } 10 | 11 | rpc UpdateAuth (Stub) returns (AuthRes) { 12 | } 13 | 14 | rpc GetCertStream (Stub) returns (stream GetCertStreamRes) { 15 | } 16 | } 17 | 18 | message AuthReq { 19 | string email = 1; 20 | string password = 2; 21 | } 22 | 23 | message AuthRes { 24 | string token = 1; 25 | } 26 | 27 | message GetCertStreamRes { 28 | string key = 1; 29 | } 30 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/services/ChatEventService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Observable, Subject } from 'rxjs'; 3 | 4 | import { api } from '@grpc-proto/chat/chat.types'; 5 | 6 | @Injectable() 7 | export class ChatEventService { 8 | private readonly updates$ = new Subject(); 9 | 10 | public emit(message: api.chat.Message): void { 11 | this.updates$.next([message]); 12 | } 13 | 14 | public broadcast(): Observable { 15 | return this.updates$.asObservable(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/services/dal/db/migrations/sqls/20191201192021-InitialMigration-up.sql: -------------------------------------------------------------------------------- 1 | create extension if not exists "uuid-ossp"; 2 | 3 | create table if not exists api_message ( 4 | id uuid DEFAULT uuid_generate_v4() NOT NULL, 5 | author JSONB NOT NULL, 6 | message VARCHAR(500) NOT NULL, 7 | "createdAt" TIMESTAMP DEFAULT now() NOT NULL, 8 | "updatedAt" TIMESTAMP DEFAULT now() NOT NULL, 9 | CONSTRAINT "PK_MESSAGES" 10 | PRIMARY KEY (id) 11 | ); 12 | 13 | create index if not exists IDX_AUTHOR_ID 14 | on api_message using GIN ((author -> 'id')); 15 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/api/user/dto/UpdateUserReqDTO.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsDefined, IsString, MaxLength, ValidateIf } from 'class-validator'; 2 | 3 | import { api } from '@grpc-proto/user/user'; 4 | 5 | export class UpdateUserReqDTO implements api.user.UpdateUserReq { 6 | @IsDefined() 7 | @IsEmail() 8 | @MaxLength(50) 9 | public email: string; 10 | 11 | @IsDefined() 12 | @IsString() 13 | @MaxLength(50) 14 | public name: string; 15 | 16 | @ValidateIf(user => user.avatar) 17 | @IsString() 18 | @MaxLength(500) 19 | public avatar: string; 20 | } 21 | -------------------------------------------------------------------------------- /packages/frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Frontend 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /devtools/helpers.js: -------------------------------------------------------------------------------- 1 | const glob = require('glob'); 2 | const path = require('path'); 3 | 4 | exports.getPackages = (ignore, root) => { 5 | return glob.sync('/*', { 6 | root, 7 | ignore 8 | }); 9 | }; 10 | 11 | exports.getProtosList = (ignore, root) => { 12 | const files = glob.sync('/*.proto', { 13 | root, 14 | ignore 15 | }); 16 | 17 | return files.map(file => path.basename(file)).join(' '); 18 | }; 19 | 20 | exports.getProtosListPath = (ignore, root) => { 21 | return glob.sync('/*.proto', { 22 | root, 23 | ignore 24 | }); 25 | }; 26 | 27 | -------------------------------------------------------------------------------- /grpc-proto/chat/message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package api.chat; 4 | 5 | import "chat.types.proto"; 6 | 7 | service MessageService { 8 | rpc SendMessage (SendMessageReq) returns (ChatRes) { 9 | } 10 | 11 | rpc EditMessage (EditMessageReq) returns (ChatRes) { 12 | } 13 | 14 | rpc DeleteMessage (DeleteMessageReq) returns (ChatRes) { 15 | } 16 | } 17 | 18 | message SendMessageReq { 19 | string message = 1; 20 | } 21 | 22 | message EditMessageReq { 23 | string id = 1; 24 | string message = 2; 25 | } 26 | 27 | message DeleteMessageReq { 28 | string id = 1; 29 | } 30 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/env.ts: -------------------------------------------------------------------------------- 1 | import { ClientConfig } from 'pg'; 2 | 3 | const env = process.env; 4 | 5 | export const SALT = env.SALT || 'SYqSuijVvyUE'; 6 | 7 | export const dbConfig: ClientConfig = { 8 | host: env.DB_HOST || 'localhost', 9 | port: +env.DB_PORT || 5432, 10 | user: env.DB_USERNAME || 'postgres', 11 | password: env.DB_PASSWORD || 'postgres', 12 | database: env.DB_DATABASE_USER || 'user', 13 | keepAlive: true, 14 | }; 15 | 16 | export const migrateConfig = { 17 | cwd: `./apps/user/src/services/dal/db`, 18 | env: 'user', 19 | string: './database.json', 20 | }; 21 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/services/dal/db/migrations/sqls/20191201221322-InitialMigration-up.sql: -------------------------------------------------------------------------------- 1 | create extension if not exists "uuid-ossp"; 2 | 3 | create table if not exists api_user ( 4 | id uuid default uuid_generate_v4() not null, 5 | email varchar(50) not null, 6 | name varchar(50) not null, 7 | avatar varchar(500), 8 | password varchar(128) not null, 9 | "createdAt" timestamp default now() not null, 10 | "updatedAt" timestamp default now() not null, 11 | constraint PK_USERS 12 | primary key (id), 13 | constraint UQ_USERS__EMAIL 14 | unique (email) 15 | ); 16 | -------------------------------------------------------------------------------- /devtools/build-grpc-back.sed: -------------------------------------------------------------------------------- 1 | # insert imports 2 | 1i\ 3 | /* tslint:disable */ 4 | 2i\ 5 | import { Observable } from "rxjs"; 6 | 2i\ 7 | import { Metadata } from "grpc"; 8 | 9 | # remove service functions with callback (variation 1) 10 | s/extends $protobuf.rpc.Service //g 11 | /.*(request: [^,]\+, callback: [^)]\+).*/d 12 | # remove callback definitions (variation 1) 13 | /^[[:blank:]]*type [[:alpha:]]*Callback = .*/d 14 | 15 | # modify service functions return type from Promise to Observable (variation 2) 16 | s/Promise/Observable/g 17 | # add param Metadata to variation 2 methods 18 | s/\((request: [^,)]\+\))/\1, metadata?: Metadata)/ 19 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/exceptions/impl/NotFoundException.ts: -------------------------------------------------------------------------------- 1 | import { BaseException, ErrorCodeType, MetadataType } from './BaseException'; 2 | 3 | import { IError, ECodes } from './code.types'; 4 | 5 | export const NOT_FOUND: IError = { 6 | code: ECodes.NOT_FOUND, 7 | message: 'Not found', 8 | }; 9 | 10 | export const USER_NOT_FOUND: IError = { 11 | code: ECodes.USER_NOT_FOUND, 12 | message: 'User not found', 13 | }; 14 | 15 | export class NotFoundException extends BaseException { 16 | constructor(customCode?: ErrorCodeType, metadata: MetadataType = {}) { 17 | super(customCode || NOT_FOUND, metadata); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/services/dal/DalModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { DataFindersModule } from './data-finders/DataFindersModule'; 4 | import { DataUpdatersModule } from './data-updaters/DataUpdatersModule'; 5 | import { DataProducerModule } from './data-producers/DataProducerModule'; 6 | import { DataRemoversModule } from './data-removers/DataRemoversModule'; 7 | 8 | @Module({ 9 | imports: [DataFindersModule, DataProducerModule, DataUpdatersModule, DataRemoversModule], 10 | exports: [DataFindersModule, DataProducerModule, DataUpdatersModule, DataRemoversModule], 11 | }) 12 | export class DalModule { 13 | } 14 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/services/dal/DalModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { DataFindersModule } from './data-finders/DataFindersModule'; 4 | import { DataUpdatersModule } from './data-updaters/DataUpdatersModule'; 5 | import { DataProducerModule } from './data-producers/DataProducerModule'; 6 | import { DataRemoversModule } from './data-removers/DataRemoversModule'; 7 | 8 | @Module({ 9 | imports: [DataFindersModule, DataProducerModule, DataUpdatersModule, DataRemoversModule], 10 | exports: [DataFindersModule, DataProducerModule, DataUpdatersModule, DataRemoversModule], 11 | }) 12 | export class DalModule { 13 | } 14 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/api/user/dto/CreateUserReqDTO.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsDefined, IsString, MaxLength, ValidateIf } from 'class-validator'; 2 | 3 | import { api } from '@grpc-proto/user/user'; 4 | 5 | export class CreateUserReqDTO implements api.user.CreateUserReq { 6 | @IsDefined() 7 | @IsEmail() 8 | @MaxLength(50) 9 | public email: string; 10 | 11 | @IsDefined() 12 | @IsString() 13 | @MaxLength(50) 14 | public name: string; 15 | 16 | @IsDefined() 17 | @IsString() 18 | @MaxLength(128) 19 | public password: string; 20 | 21 | @ValidateIf(user => user.avatar) 22 | @IsString() 23 | @MaxLength(500) 24 | public avatar: string; 25 | } 26 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/exceptions/impl/AlreadyExistsException.ts: -------------------------------------------------------------------------------- 1 | import { BaseException, ErrorCodeType, MetadataType } from './BaseException'; 2 | 3 | import { IError, ECodes } from './code.types'; 4 | 5 | export const ALREADY_EXIST: IError = { 6 | code: ECodes.ALREADY_EXIST, 7 | message: 'Resource already exists', 8 | }; 9 | 10 | export const EMAIL_ALREADY_EXISTS: IError = { 11 | code: ECodes.EMAIL_ALREADY_EXISTS, 12 | message: 'Email already exists', 13 | }; 14 | 15 | export class AlreadyExistsException extends BaseException { 16 | constructor(customCode?: ErrorCodeType, metadata: MetadataType = {}) { 17 | super(customCode || ALREADY_EXIST, metadata); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/exceptions/impl/InvalidArgumentException.ts: -------------------------------------------------------------------------------- 1 | import { BaseException, ErrorCodeType, MetadataType } from './BaseException'; 2 | 3 | import { IError, ECodes } from './code.types'; 4 | 5 | export const INVALID_ARGUMENT: IError = { 6 | code: ECodes.INVALID_ARGUMENT, 7 | message: 'Invalid argument', 8 | }; 9 | 10 | export const USER_ID_REQUIRED: IError = { 11 | code: ECodes.USER_ID_REQUIRED, 12 | message: 'User id is required', 13 | }; 14 | 15 | export class InvalidArgumentException extends BaseException { 16 | constructor(customCode?: ErrorCodeType, metadata: MetadataType = {}) { 17 | super(customCode || INVALID_ARGUMENT, metadata); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/frontend/src/app/grpc/helpers/grpc-unary.ts: -------------------------------------------------------------------------------- 1 | import { Observable, from, throwError } from 'rxjs'; 2 | import { map, catchError } from 'rxjs/operators'; 3 | import { Status, StatusCode } from 'grpc-web'; 4 | import * as jspb from 'google-protobuf'; 5 | 6 | import { jwtAuthError$ } from '@grpc/helpers/grpc-jwt'; 7 | 8 | export function grpcUnary(promise): Observable { 9 | return from(promise).pipe( 10 | map((response: jspb.Message) => response.toObject()), 11 | catchError((error: Status) => { 12 | if (error.code === StatusCode.UNAUTHENTICATED) { 13 | jwtAuthError$.next(); 14 | } 15 | 16 | return throwError(error); 17 | }), 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/backend/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import { AppModule } from '../apps/app.module'; 4 | 5 | describe('AppController (e2e)', () => { 6 | let app; 7 | 8 | beforeEach(async () => { 9 | const moduleFixture: TestingModule = await Test.createTestingModule({ 10 | imports: [AppModule], 11 | }).compile(); 12 | 13 | app = moduleFixture.createNestApplication(); 14 | await app.init(); 15 | }); 16 | 17 | it('/ (GET)', () => { 18 | return request(app.getHttpServer()) 19 | .get('/') 20 | .expect(200) 21 | .expect('Hello World!'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/backend/apps/auth/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import { AppModule } from './../src/AppModule'; 4 | 5 | describe('AppController (e2e)', () => { 6 | let app; 7 | 8 | beforeEach(async () => { 9 | const moduleFixture: TestingModule = await Test.createTestingModule({ 10 | imports: [AppModule], 11 | }).compile(); 12 | 13 | app = moduleFixture.createNestApplication(); 14 | await app.init(); 15 | }); 16 | 17 | it('/ (GET)', () => { 18 | return request(app.getHttpServer()) 19 | .get('/') 20 | .expect(200) 21 | .expect('Hello World!'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import { AppModule } from './../src/AppModule'; 4 | 5 | describe('AppController (e2e)', () => { 6 | let app; 7 | 8 | beforeEach(async () => { 9 | const moduleFixture: TestingModule = await Test.createTestingModule({ 10 | imports: [AppModule], 11 | }).compile(); 12 | 13 | app = moduleFixture.createNestApplication(); 14 | await app.init(); 15 | }); 16 | 17 | it('/ (GET)', () => { 18 | return request(app.getHttpServer()) 19 | .get('/') 20 | .expect(200) 21 | .expect('Hello World!'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/backend/apps/user/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import { AppModule } from './../src/AppModule'; 4 | 5 | describe('AppController (e2e)', () => { 6 | let app; 7 | 8 | beforeEach(async () => { 9 | const moduleFixture: TestingModule = await Test.createTestingModule({ 10 | imports: [AppModule], 11 | }).compile(); 12 | 13 | app = moduleFixture.createNestApplication(); 14 | await app.init(); 15 | }); 16 | 17 | it('/ (GET)', () => { 18 | return request(app.getHttpServer()) 19 | .get('/') 20 | .expect(200) 21 | .expect('Hello World!'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/backend/docker/postgres/scripts/create-multiple-dbs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -u 5 | 6 | function create_user_and_database() { 7 | local database=$1 8 | echo " Creating user and database '$database'" 9 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL 10 | CREATE USER "$database"; 11 | CREATE DATABASE "$database"; 12 | GRANT ALL PRIVILEGES ON DATABASE "$database" TO "$database"; 13 | EOSQL 14 | } 15 | 16 | if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then 17 | echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES" 18 | for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do 19 | create_user_and_database $db 20 | done 21 | echo "Multiple databases created" 22 | fi 23 | -------------------------------------------------------------------------------- /packages/frontend/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting, 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting(), 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /packages/frontend/src/app/share/services/message-bus.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, Subject } from 'rxjs'; 3 | import { map, filter } from 'rxjs/operators'; 4 | 5 | export interface Message { 6 | event: string; 7 | data: T; 8 | } 9 | 10 | @Injectable({ 11 | providedIn: 'root', 12 | }) 13 | export class MessageBusService { 14 | 15 | private message$ = new Subject>(); 16 | 17 | public emit(event: string, data: T): void { 18 | this.message$.next({ event, data }); 19 | } 20 | 21 | public on(event: string): Observable { 22 | return this.message$.pipe( 23 | filter(m => m.event === event), 24 | map(m => m.data), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/logger/constants.ts: -------------------------------------------------------------------------------- 1 | export type LogLevelType = 'debug' | 'info' | 'error' | 'security'; 2 | 3 | export const DEFAULT_LOGGER_LEVEL = 'info'; 4 | 5 | export const ALLOWED_LOG_BY_LEVEL = { 6 | debug: new Set(['debug', 'info', 'error', 'security']), 7 | info: new Set(['info', 'error', 'security']), 8 | error: new Set(['error', 'security']), 9 | security: new Set(['security']), 10 | }; 11 | 12 | export const LOG_LEVEL_NAME = { 13 | debug: 'debug' as LogLevelType, 14 | info: 'info' as LogLevelType, 15 | error: 'error' as LogLevelType, 16 | security: 'security' as LogLevelType, 17 | }; 18 | 19 | export const MESSAGE_COLOR_BY_LEVEL = { 20 | debug: 90, 21 | info: 32, 22 | error: 31, 23 | security: 36, 24 | }; 25 | -------------------------------------------------------------------------------- /packages/frontend/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('Welcome to frontend!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/chat/chat.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatComponent } from './chat.component'; 4 | 5 | describe('ChatComponent', () => { 6 | let component: ChatComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ChatComponent], 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/user/user.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserComponent } from './user.component'; 4 | 5 | describe('UserComponent', () => { 6 | let component: UserComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [UserComponent], 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(UserComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/chat/form/form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FormComponent } from './form.component'; 4 | 5 | describe('FormComponent', () => { 6 | let component: FormComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [FormComponent], 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FormComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/user/auth/auth.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthComponent } from './auth.component'; 4 | 5 | describe('AuthComponent', () => { 6 | let component: AuthComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [AuthComponent], 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AuthComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/frontend/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | bottom: 0; 7 | padding-top: 4rem; 8 | } 9 | 10 | .toolbar { 11 | position: relative; 12 | z-index: 1; 13 | background: #00564d; 14 | color: #DEEBFF; 15 | box-shadow: 0 3px 5px -1px rgba(0,0,0,.2), 0 6px 10px 0 rgba(0,0,0,.14), 0 1px 18px 0 rgba(0,0,0,.12); 16 | 17 | .mat-toolbar-row { 18 | flex-direction: row-reverse; 19 | 20 | button { 21 | font-size: 1.05rem; 22 | line-height: 1; 23 | } 24 | 25 | .chat { 26 | flex: 1; 27 | text-align: left; 28 | 29 | button { 30 | font-size: 3rem; 31 | line-height: 0; 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/exceptions/impl/code.types.ts: -------------------------------------------------------------------------------- 1 | export interface IError { 2 | code: number; 3 | message: string; 4 | } 5 | 6 | export enum ECodes { 7 | ERROR_CODE_UNDEFINED = 0, 8 | // invalid argument codes 9 | INVALID_ARGUMENT = 3, 10 | // required codes 11 | USER_ID_REQUIRED = 301, 12 | // not found codes 13 | NOT_FOUND = 5, 14 | USER_NOT_FOUND = 501, 15 | // already exist codes 16 | ALREADY_EXIST = 6, 17 | EMAIL_ALREADY_EXISTS = 601, 18 | // permission denied codes 19 | PERMISSION_DENIED = 7, 20 | // internal error codes 21 | INTERNAL_ERROR = 13, 22 | // unavailable codes 23 | UNAVAILABLE = 14, 24 | // unauthenticated codes 25 | UNAUTHENTICATED = 16, 26 | TOKEN_INVALID = 16001, 27 | TOKEN_EXPIRED = 16002, 28 | AUTH_CREDENTIALS_INVALID = 16003, 29 | } 30 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/chat/message/message.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MessageComponent } from './message.component'; 4 | 5 | describe('MessageComponent', () => { 6 | let component: MessageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [MessageComponent], 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(MessageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/services/dal/db/migrations/sqls/20191201221519-InsertDemoUsersMigration-up.sql: -------------------------------------------------------------------------------- 1 | insert into api_user (id, email, name, avatar, password, "createdAt", "updatedAt") values ('bb6d88c8-d705-4c10-b1a1-08ea1b94d4cc', 'johndoe@mail.com', 'John Doe', 'assets/avatar/avatar-2.png', 'aaee7630e579489b58e9e5933d144c6cdb14cb1d30314b0eb97379f837b46726dd5bc4fe02b32e4265812a58d61524987552423a56b1d94e8f6ffc1d170a29c7', '2019-11-17 14:40:20.763852', '2019-11-17 14:40:20.763852'); 2 | 3 | insert into api_user (id, email, name, avatar, password, "createdAt", "updatedAt") values ('34e9e3e5-56e9-4e71-9b3c-13292915970b', 'anna@mail.com', 'Anna Smith', 'assets/avatar/avatar-1.png', 'aaee7630e579489b58e9e5933d144c6cdb14cb1d30314b0eb97379f837b46726dd5bc4fe02b32e4265812a58d61524987552423a56b1d94e8f6ffc1d170a29c7', '2019-11-17 14:40:20.763852', '2019-11-17 14:40:20.763852'); 4 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/exceptions/filter/handlers/impl/RpcExceptionHandler.ts: -------------------------------------------------------------------------------- 1 | import { IExceptionHandler } from '../interfaces'; 2 | 3 | import { BaseException } from '../../../impl/BaseException'; 4 | 5 | import { Logger } from '../../../../logger'; 6 | 7 | export class RpcExceptionHandler implements IExceptionHandler { 8 | private readonly logger = new Logger('RpcExceptionHandler'); 9 | 10 | constructor(private readonly exception: BaseException) { 11 | } 12 | 13 | public wrapError(): BaseException { 14 | // not need to handle this error, 15 | // because it regular exception from backend services 16 | return this.exception; 17 | } 18 | 19 | public warnAboutError(): void { 20 | const {message}: any = this.exception; 21 | this.logger.debug(`Internal exception: ${message}`); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/chat/chat-list/chat-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChatListComponent } from './chat-list.component'; 4 | 5 | describe('ChatListComponent', () => { 6 | let component: ChatListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ChatListComponent], 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChatListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/user/register/register.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RegisterComponent } from './register.component'; 4 | 5 | describe('RegisterComponent', () => { 6 | let component: RegisterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [RegisterComponent], 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(RegisterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/user/settings/settings.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SettingsComponent } from './settings.component'; 4 | 5 | describe('SettingsComponent', () => { 6 | let component: SettingsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [SettingsComponent], 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SettingsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/api/chat/ChatService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Observable, concat } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | 5 | import { api } from '@grpc-proto/chat/chat'; 6 | 7 | import { MessageDataFinder } from '@chat/services/dal/data-finders/MessageDataFinder'; 8 | import { ChatEventService } from '@chat/services/ChatEventService'; 9 | 10 | @Injectable() 11 | export class ChatService { 12 | 13 | constructor( 14 | private readonly messageDataFinder: MessageDataFinder, 15 | private readonly chatEventService: ChatEventService, 16 | ) { 17 | } 18 | 19 | public getChatStream(): Observable { 20 | return concat(this.messageDataFinder.getMessageAll(), this.chatEventService.broadcast()) 21 | .pipe(map(messages => ({messages}))); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/backend/apps/auth/src/services/PemCertsService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { createCertificate } from 'pem'; 3 | 4 | import { serviceKey } from '@auth/pki-dev/keys'; 5 | import { CertSubscribeService } from './CertSubscribeService'; 6 | 7 | const env = process.env; 8 | 9 | @Injectable() 10 | export class PemCertsService { 11 | constructor(private readonly certSubscribeService: CertSubscribeService) { 12 | } 13 | 14 | public createCertificate(): void { 15 | createCertificate({serviceKey: env.DEVELOPMENT ? serviceKey : null}, (err, keys) => { 16 | if (err) { 17 | throw err; 18 | } 19 | 20 | env.JWT_PUB = keys.certificate; 21 | env.JWT_PRIV = keys.serviceKey; 22 | 23 | this.certSubscribeService.setCert(keys.certificate); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/backend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": { 7 | "no-unused-expression": true 8 | }, 9 | "rules": { 10 | "no-namespace": [ 11 | false 12 | ], 13 | "quotemark": [ 14 | true, 15 | "single" 16 | ], 17 | "member-access": [ 18 | false 19 | ], 20 | "ordered-imports": [ 21 | false 22 | ], 23 | "max-line-length": [ 24 | true, 25 | 150 26 | ], 27 | "member-ordering": [ 28 | false 29 | ], 30 | "interface-name": [ 31 | false 32 | ], 33 | "arrow-parens": false, 34 | "object-literal-sort-keys": false 35 | }, 36 | "rulesDirectory": [] 37 | } 38 | -------------------------------------------------------------------------------- /packages/frontend/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | 5 | import { ShareModule } from '@share/share.module'; 6 | import { RoutingModule } from './app.routes'; 7 | import { AppComponent } from './app.component'; 8 | import { UserModule } from './modules/user/user.module'; 9 | import { ChatModule } from './modules/chat/chat.module'; 10 | 11 | @NgModule({ 12 | declarations: [ 13 | AppComponent, 14 | ], 15 | imports: [ 16 | BrowserModule, 17 | BrowserAnimationsModule, 18 | ShareModule, 19 | RoutingModule, 20 | UserModule, 21 | ChatModule, 22 | ], 23 | providers: [], 24 | bootstrap: [AppComponent], 25 | }) 26 | export class AppModule { 27 | } 28 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/chat/form/form.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Output, EventEmitter } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | 4 | @Component({ 5 | selector: 'app-form', 6 | templateUrl: './form.component.html', 7 | styleUrls: ['./form.component.scss'], 8 | changeDetection: ChangeDetectionStrategy.OnPush, 9 | }) 10 | export class FormComponent { 11 | 12 | @Output() send: EventEmitter = new EventEmitter(); 13 | 14 | public form: FormGroup = this.fb.group({ 15 | message: [null], 16 | }); 17 | 18 | constructor(private fb: FormBuilder) { 19 | } 20 | 21 | public onSubmit(): void { 22 | if (this.form.valid) { 23 | this.send.emit(this.form.value.message); 24 | this.form.reset(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/backend/apps/auth/src/main.ts: -------------------------------------------------------------------------------- 1 | process.title = 'node-user'; 2 | 3 | import { NestFactory } from '@nestjs/core'; 4 | import { Logger as NestLogger, ValidationPipe } from '@nestjs/common'; 5 | 6 | import { BootstrapLogger } from '@lib/logger'; 7 | import { grpcAuth } from '@lib/utils/GrpcConfigs'; 8 | 9 | import { AppModule } from './AppModule'; 10 | 11 | const logger = new BootstrapLogger(); 12 | // override logger with our implementation for transforming logs like 13 | // "[Nest] 406 - 8/12/2019, 11:00:41 AM [NestFactory] ..." 14 | NestLogger.overrideLogger(logger); 15 | 16 | async function bootstrap() { 17 | const app = await NestFactory.createMicroservice(AppModule, grpcAuth); 18 | 19 | app.useLogger(logger); 20 | app.useGlobalPipes(new ValidationPipe()); 21 | 22 | await app.listenAsync(); 23 | } 24 | 25 | bootstrap().catch(err => { 26 | logger.error(err); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/exceptions/filter/handlers/impl/InternalExceptionHandler.ts: -------------------------------------------------------------------------------- 1 | import { IExceptionHandler } from '../interfaces'; 2 | 3 | import { BaseException } from '../../../impl/BaseException'; 4 | import { InternalException } from '../../../impl/InternalException'; 5 | 6 | import { Logger } from '../../../../logger'; 7 | 8 | export class InternalExceptionHandler implements IExceptionHandler { 9 | private readonly logger = new Logger('InternalExceptionHandler'); 10 | 11 | constructor(private readonly exception: Error, private readonly label: string) { 12 | } 13 | 14 | public wrapError(): BaseException { 15 | return new InternalException(); 16 | } 17 | 18 | public warnAboutError(): void { 19 | const {stack, message} = this.exception; 20 | this.logger.error(`${this.label} :: Internal error "${message}",\nStack: ${stack}`); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/exceptions/impl/BaseException.ts: -------------------------------------------------------------------------------- 1 | import { RpcException } from '@nestjs/microservices'; 2 | 3 | interface IErrorCode { 4 | code: number; 5 | message: string; 6 | } 7 | 8 | export type ErrorCodeType = IErrorCode | null; 9 | 10 | export interface MetadataType { 11 | [key: string]: string; 12 | } 13 | 14 | export class BaseException extends RpcException { 15 | constructor(errorCode: IErrorCode, metadata: MetadataType) { 16 | super({ 17 | code: errorCode.code, 18 | 19 | // so far it has not been possible to find normal ways in Nest 20 | // to transmit the metadata in response with an error, 21 | // so we will sew this data into the message body 22 | message: JSON.stringify({ 23 | message: errorCode.message, 24 | metadata, 25 | }), 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/main.ts: -------------------------------------------------------------------------------- 1 | process.title = 'node-chat'; 2 | 3 | import { NestFactory } from '@nestjs/core'; 4 | import { Logger as NestLogger, ValidationPipe } from '@nestjs/common'; 5 | 6 | import { BootstrapLogger } from '@lib/logger'; 7 | 8 | import { grpcChat } from '@lib/utils/GrpcConfigs'; 9 | 10 | import { AppModule } from './AppModule'; 11 | 12 | const logger = new BootstrapLogger(); 13 | // override logger with our implementation for transforming logs like 14 | // "[Nest] 406 - 8/12/2019, 11:00:41 AM [NestFactory] ..." 15 | NestLogger.overrideLogger(logger); 16 | 17 | async function bootstrap() { 18 | const app = await NestFactory.createMicroservice(AppModule, grpcChat); 19 | 20 | app.useLogger(logger); 21 | app.useGlobalPipes(new ValidationPipe()); 22 | 23 | await app.listenAsync(); 24 | } 25 | 26 | bootstrap().catch(err => { 27 | logger.error(err); 28 | process.exit(1); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/main.ts: -------------------------------------------------------------------------------- 1 | process.title = 'node-user'; 2 | 3 | import { NestFactory } from '@nestjs/core'; 4 | import { Logger as NestLogger, ValidationPipe } from '@nestjs/common'; 5 | 6 | import { BootstrapLogger } from '@lib/logger'; 7 | import { grpcUser } from '@lib/utils/GrpcConfigs'; 8 | 9 | import { AppModule } from './AppModule'; 10 | 11 | export const logger = new BootstrapLogger(); 12 | // override logger with our implementation for transforming logs like 13 | // "[Nest] 406 - 8/12/2019, 11:00:41 AM [NestFactory] ..." 14 | NestLogger.overrideLogger(logger); 15 | 16 | async function bootstrap() { 17 | const app = await NestFactory.createMicroservice(AppModule, grpcUser); 18 | 19 | app.useLogger(logger); 20 | app.useGlobalPipes(new ValidationPipe()); 21 | 22 | await app.listenAsync(); 23 | } 24 | 25 | bootstrap().catch(err => { 26 | logger.error(err); 27 | process.exit(1); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | 5 | import { GrpcModule } from '@grpc/grpc.module'; 6 | import { ShareModule } from '@share/share.module'; 7 | import { RoutingModule } from './user.routes'; 8 | import { AuthComponent } from './auth/auth.component'; 9 | import { RegisterComponent } from './register/register.component'; 10 | import { UserComponent } from './user.component'; 11 | import { SettingsComponent } from './settings/settings.component'; 12 | 13 | @NgModule({ 14 | declarations: [AuthComponent, RegisterComponent, UserComponent, SettingsComponent], 15 | imports: [ 16 | CommonModule, 17 | ReactiveFormsModule, 18 | GrpcModule, 19 | ShareModule, 20 | RoutingModule, 21 | ], 22 | }) 23 | export class UserModule { 24 | } 25 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/services/dal/data-removers/UserDataRemover.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Client } from 'pg'; 3 | import { from, Observable } from 'rxjs'; 4 | import { switchMap, mapTo } from 'rxjs/operators'; 5 | 6 | import { api } from '@grpc-proto/user/user.types'; 7 | 8 | import { UserDataFinder } from '@user/services/dal/data-finders/UserDataFinder'; 9 | 10 | @Injectable() 11 | export class UserDataRemover { 12 | 13 | constructor( 14 | private readonly db: Client, 15 | private readonly userDataFinder: UserDataFinder, 16 | ) { 17 | } 18 | 19 | public deleteUser(id: string): Observable { 20 | const query = `delete from api_user where id = $1`; 21 | 22 | return this.userDataFinder.getUserOne(id).pipe( 23 | switchMap(() => from(this.db.query(query, [id]))), 24 | mapTo(null), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/frontend/src/app/grpc/services/chat/chat.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Metadata } from 'grpc-web'; 4 | 5 | import { grpcStream } from '@grpc/helpers/grpc-stream'; 6 | import { grpcJwtMetadata } from '@grpc/helpers/grpc-metadata'; 7 | 8 | import { ChatServicePromiseClient } from '@grpc/proto/chat/chat_grpc_web_pb'; 9 | import { ChatList } from '@grpc/proto/chat/chat_pb'; 10 | import { Stub } from '@grpc/proto/chat/chat.types_pb'; 11 | 12 | 13 | @Injectable({ 14 | providedIn: 'root', 15 | }) 16 | export class ChatGrpcService { 17 | 18 | constructor(private client: ChatServicePromiseClient) { 19 | } 20 | 21 | public getChat(): Observable { 22 | const req = new Stub(); 23 | const meta: Metadata = grpcJwtMetadata(); 24 | 25 | return grpcStream(this.client.getChat(req, meta)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/services/dal/data-producers/MessageDataProducer.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Client } from 'pg'; 3 | import { from, Observable } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | import { api } from '@grpc-proto/chat/chat.types'; 7 | 8 | interface IInsertMessage { 9 | message: string; 10 | author: { id: string; name: string; avatar: string; }; 11 | } 12 | 13 | @Injectable() 14 | export class MessageDataProducer { 15 | 16 | constructor(private readonly db: Client) { 17 | } 18 | 19 | public sendMessage(data: IInsertMessage): Observable { 20 | const author = JSON.stringify(data.author); 21 | const query = `insert into api_message (author, message) values ($1, $2) returning *`; 22 | 23 | return from(this.db.query(query, [author, data.message])) 24 | .pipe(map(res => res.rows[0])); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/services/dal/data-removers/MessageDataRemover.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Client } from 'pg'; 3 | import { from, Observable } from 'rxjs'; 4 | import { switchMap, mapTo } from 'rxjs/operators'; 5 | 6 | import { api } from '@grpc-proto/chat/chat.types'; 7 | import { MessageDataFinder } from '@chat/services/dal/data-finders/MessageDataFinder'; 8 | 9 | @Injectable() 10 | export class MessageDataRemover { 11 | 12 | constructor( 13 | private readonly db: Client, 14 | private readonly messageDataFinder: MessageDataFinder, 15 | ) { 16 | } 17 | 18 | public deleteMessage(id: string): Observable { 19 | const query = `delete from api_message where id = $1`; 20 | 21 | return this.messageDataFinder.getMessageOne(id).pipe( 22 | switchMap(() => from(this.db.query(query, [id]))), 23 | mapTo(null), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/exceptions/impl/UnauthenticatedException.ts: -------------------------------------------------------------------------------- 1 | import { BaseException, ErrorCodeType, MetadataType } from './BaseException'; 2 | 3 | import { IError, ECodes } from './code.types'; 4 | 5 | export const UNAUTHENTICATED: IError = { 6 | code: ECodes.UNAUTHENTICATED, 7 | message: 'Unauthenticated', 8 | }; 9 | 10 | export const TOKEN_INVALID: IError = { 11 | code: ECodes.TOKEN_INVALID, 12 | message: 'Token invalid', 13 | }; 14 | 15 | export const TOKEN_EXPIRED: IError = { 16 | code: ECodes.TOKEN_EXPIRED, 17 | message: 'Token expired', 18 | }; 19 | 20 | export const AUTH_CREDENTIALS_INVALID: IError = { 21 | code: ECodes.AUTH_CREDENTIALS_INVALID, 22 | message: 'Auth credentials invalid', 23 | }; 24 | 25 | export class UnauthenticatedException extends BaseException { 26 | constructor(customCode?: ErrorCodeType, metadata: MetadataType = {}) { 27 | super(customCode || UNAUTHENTICATED, metadata); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/frontend/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const {SpecReporter} = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function () { 21 | } 22 | }, 23 | onPrepare() { 24 | require('ts-node').register({ 25 | project: require('path').join(__dirname, './tsconfig.e2e.json') 26 | }); 27 | jasmine.getEnv().addReporter(new SpecReporter({spec: {displayStacktrace: true}})); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/utils/GrpcConfigs.ts: -------------------------------------------------------------------------------- 1 | import { GrpcOptions, Transport } from '@nestjs/microservices'; 2 | 3 | const env = process.env; 4 | 5 | export const grpcChat = { 6 | transport: Transport.GRPC, 7 | options: { 8 | url: env.GRPC_CHAT_SERVICE || '127.0.0.1:8003', 9 | package: 'api.chat', 10 | protoPath: './libs/grpc-proto/chat/index.proto', 11 | }, 12 | } as GrpcOptions; 13 | 14 | export const grpcAuth = { 15 | transport: Transport.GRPC, 16 | options: { 17 | url: env.GRPC_AUTH_SERVICE || '127.0.0.1:8002', 18 | package: 'api.auth', 19 | protoPath: './libs/grpc-proto/auth/index.proto', 20 | }, 21 | } as GrpcOptions; 22 | 23 | export const grpcUser = { 24 | transport: Transport.GRPC, 25 | options: { 26 | url: env.GRPC_USER_SERVICE || '127.0.0.1:8001', 27 | package: 'api.user', 28 | protoPath: './libs/grpc-proto/user/index.proto', 29 | }, 30 | } as GrpcOptions; 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /*/dist 6 | /tmp 7 | /out-tsc 8 | # Only exists if Bazel was run 9 | /bazel-out 10 | 11 | # dependencies 12 | /node_modules 13 | node_modules 14 | packages/backend/**/*proto 15 | packages/frontend/**/*proto 16 | grpc-proto/.tmp/* 17 | packages/backend/dist/* 18 | 19 | # profiling files 20 | chrome-profiler-events.json 21 | speed-measure-plugin.json 22 | 23 | # IDEs and editors 24 | /.idea 25 | .project 26 | .classpath 27 | .c9/ 28 | *.launch 29 | .settings/ 30 | *.sublime-workspace 31 | 32 | # IDE - VSCode 33 | .vscode/* 34 | !.vscode/settings.json 35 | !.vscode/tasks.json 36 | !.vscode/launch.json 37 | !.vscode/extensions.json 38 | .history/* 39 | 40 | # misc 41 | /.sass-cache 42 | /connect.lock 43 | /coverage 44 | /libpeerconnection.log 45 | npm-debug.log 46 | yarn-error.log 47 | testem.log 48 | /typings 49 | 50 | # System Files 51 | .DS_Store 52 | Thumbs.db 53 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/services/dal/data-finders/MessageDataFinder.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Client } from 'pg'; 3 | import { from, Observable } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | import { api } from '@grpc-proto/chat/chat.types'; 7 | 8 | @Injectable() 9 | export class MessageDataFinder { 10 | 11 | constructor(private readonly db: Client) { 12 | } 13 | 14 | public getMessageOne(id: string): Observable { 15 | const query = `select * from api_message where id = $1`; 16 | 17 | return from(this.db.query(query, [id])) 18 | .pipe(map(res => res.rows[0])); 19 | } 20 | 21 | public getMessageAll(): Observable { 22 | const query = `select * from api_message order by "updatedAt" ASC`; 23 | 24 | return from(this.db.query(query)) 25 | .pipe(map(res => res.rows)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/exceptions/filter/handlers/ExceptionHandlerFactory.ts: -------------------------------------------------------------------------------- 1 | import { RpcException } from '@nestjs/microservices'; 2 | 3 | import { IExceptionHandler, IExceptionHandlerFactory } from './interfaces'; 4 | 5 | import { RpcExceptionHandler } from './impl/RpcExceptionHandler'; 6 | import { InternalExceptionHandler } from './impl/InternalExceptionHandler'; 7 | 8 | import { ExceptionType } from '../types'; 9 | 10 | export class ExceptionHandlerFactory implements IExceptionHandlerFactory { 11 | constructor(private readonly label: string) { 12 | } 13 | 14 | public getHandler(exception: ExceptionType): IExceptionHandler { 15 | // handle regular exceptions from current microservices 16 | if (exception instanceof RpcException) { 17 | return new RpcExceptionHandler(exception); 18 | } 19 | 20 | // handle all other internal exceptions 21 | return new InternalExceptionHandler(exception, this.label); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/logger/BootstrapLogger.ts: -------------------------------------------------------------------------------- 1 | import { LoggerService } from '@nestjs/common'; 2 | 3 | import { Logger } from './Logger'; 4 | 5 | const DEFAULT_LOGGER_NAME = 'bootstrap'; 6 | 7 | export class BootstrapLogger implements LoggerService { 8 | private logger: Logger; 9 | 10 | constructor(private readonly label?: string) { 11 | this.logger = new Logger(this.label ? this.label : DEFAULT_LOGGER_NAME); 12 | } 13 | 14 | public log(message: any, context?: string): void { 15 | this.logger.info(message); 16 | } 17 | 18 | public error(message: any, trace?: string, context?: string): void { 19 | this.logger.error(message); 20 | } 21 | 22 | public warn(message: any, context?: string): void { 23 | // our implementation of the logger does not yet need 24 | // the "warning" level, so we will write the logs 25 | // coming from here to "error" level 26 | this.logger.error(message); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | import { LoggerConfig, NgxLoggerLevel } from 'ngx-logger'; 6 | 7 | export const environment = { 8 | production: false, 9 | url: '/api', 10 | token: 'pAjjaWcqFQkr', 11 | authDiff: 60, 12 | logger: { 13 | level: NgxLoggerLevel.DEBUG, 14 | } as LoggerConfig, 15 | }; 16 | 17 | /* 18 | * For easier debugging in development mode, you can import the following file 19 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 20 | * 21 | * This import should be commented out in production mode because it will have a negative impact 22 | * on performance if an error is thrown. 23 | */ 24 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 25 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/chat/chat.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | 5 | import { ShareModule } from '@share/share.module'; 6 | import { GrpcModule } from '@grpc/grpc.module'; 7 | import { RoutingModule } from './chat.routes'; 8 | 9 | import { ChatComponent } from './chat.component'; 10 | import { ChatListComponent } from './chat-list/chat-list.component'; 11 | import { MessageComponent } from './message/message.component'; 12 | import { FormComponent } from './form/form.component'; 13 | 14 | @NgModule({ 15 | declarations: [ 16 | ChatComponent, 17 | ChatListComponent, 18 | MessageComponent, 19 | FormComponent, 20 | ], 21 | imports: [ 22 | CommonModule, 23 | ReactiveFormsModule, 24 | RoutingModule, 25 | ShareModule, 26 | GrpcModule, 27 | ], 28 | }) 29 | export class ChatModule { 30 | } 31 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/api/chat/ChatController.ts: -------------------------------------------------------------------------------- 1 | import { Controller, UseGuards, UseFilters } from '@nestjs/common'; 2 | import { GrpcMethod } from '@nestjs/microservices'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { JwtGuard } from '@lib/jwt/JwtGuard'; 6 | import { RpcExceptionFilter } from '@lib/exceptions'; 7 | import { Identity } from '@lib/utils/identity'; 8 | 9 | import { api as chatApiTypes } from '@grpc-proto/chat/chat.types'; 10 | import { api as chatApi } from '@grpc-proto/chat/chat'; 11 | 12 | import { ChatService } from './ChatService'; 13 | 14 | @Controller() 15 | export class ChatController { 16 | 17 | constructor(private readonly chatService: ChatService) { 18 | } 19 | 20 | @UseGuards(JwtGuard) 21 | @GrpcMethod('ChatService', 'GetChat') 22 | @UseFilters(RpcExceptionFilter.for('ChatService::getChat')) 23 | public getChat(data: Identity): Observable { 24 | return this.chatService.getChatStream(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/logger/message/colorizers.ts: -------------------------------------------------------------------------------- 1 | // about colorizing messages in stdout see: https://stackoverflow.com/a/41407246 2 | 3 | import { MESSAGE_COLOR_BY_LEVEL } from '../constants'; 4 | 5 | const DEFAULT_COLOR = MESSAGE_COLOR_BY_LEVEL.info; 6 | const TIMESTAMP_COLOR = '50'; 7 | const LABEL_COLOR = '33'; 8 | 9 | export function colorizeTimestamp(timestamp: string): string { 10 | return colorize(TIMESTAMP_COLOR, timestamp); 11 | } 12 | 13 | export function colorizeLevel(level: string): string { 14 | return colorize(MESSAGE_COLOR_BY_LEVEL[level] || DEFAULT_COLOR, level); 15 | } 16 | 17 | export function colorizeLabel(label: string): string { 18 | return colorize(LABEL_COLOR, label); 19 | } 20 | 21 | export function colorizeMessage(level: string, message: string): string { 22 | return colorize(MESSAGE_COLOR_BY_LEVEL[level] || DEFAULT_COLOR, message); 23 | } 24 | 25 | function colorize(color: string, message: string): string { 26 | return ['\x1b[', color, 'm', message, '\x1b[0m'].join(''); 27 | } 28 | -------------------------------------------------------------------------------- /packages/frontend/src/app/share/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; 3 | 4 | import { Observable } from 'rxjs'; 5 | import { map } from 'rxjs/operators'; 6 | 7 | import { AuthService } from '@share/services/auth.service'; 8 | 9 | @Injectable({ 10 | providedIn: 'root', 11 | }) 12 | export class AuthGuard implements CanActivate { 13 | 14 | constructor( 15 | private router: Router, 16 | private authService: AuthService, 17 | ) { 18 | } 19 | 20 | canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 21 | return this.authService.isLoggedIn().pipe( 22 | map((isLoggedIn: boolean) => { 23 | if (isLoggedIn) { 24 | return true; 25 | } 26 | 27 | this.router.navigateByUrl('/auth'); 28 | return false; 29 | }), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/frontend/src/app/grpc/grpc.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { environment } from '@environments/environment'; 5 | 6 | import { UserServicePromiseClient } from '@grpc/proto/user/user_grpc_web_pb'; 7 | import { AuthServicePromiseClient } from '@grpc/proto/auth/auth_grpc_web_pb'; 8 | import { ChatServicePromiseClient } from '@grpc/proto/chat/chat_grpc_web_pb'; 9 | import { MessageServicePromiseClient } from '@grpc/proto/chat/message_grpc_web_pb'; 10 | 11 | const GRPC_CLIENTS = [ 12 | UserServicePromiseClient, 13 | AuthServicePromiseClient, 14 | ChatServicePromiseClient, 15 | MessageServicePromiseClient, 16 | ]; 17 | 18 | @NgModule({ 19 | imports: [ 20 | CommonModule, 21 | ], 22 | providers: GRPC_CLIENTS.map(service => { 23 | return { 24 | provide: service, 25 | useFactory: () => new service(environment.url, null, null), 26 | }; 27 | }), 28 | }) 29 | export class GrpcModule { 30 | } 31 | -------------------------------------------------------------------------------- /packages/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "paths": { 6 | "@environments/*": [ 7 | "src/environments/*" 8 | ], 9 | "@grpc/*": [ 10 | "src/app/grpc/*" 11 | ], 12 | "@share/*": [ 13 | "src/app/share/*" 14 | ] 15 | }, 16 | "downlevelIteration": true, 17 | "outDir": "./dist/out-tsc", 18 | "sourceMap": true, 19 | "declaration": false, 20 | "module": "esnext", 21 | "moduleResolution": "node", 22 | "emitDecoratorMetadata": true, 23 | "experimentalDecorators": true, 24 | "importHelpers": true, 25 | "target": "es2015", 26 | "noUnusedLocals": false, 27 | "newLine": "LF", 28 | "typeRoots": [ 29 | "node_modules/@types" 30 | ], 31 | "lib": [ 32 | "es2018", 33 | "dom" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/frontend/src/app/share/guards/auth-child.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | import { AuthService } from '@share/services/auth.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class AuthChildGuard implements CanActivateChild { 11 | 12 | constructor( 13 | private router: Router, 14 | private authService: AuthService, 15 | ) { 16 | } 17 | 18 | canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { 19 | return this.authService.isLoggedIn().pipe( 20 | map((isLoggedIn: boolean) => { 21 | if (isLoggedIn) { 22 | return true; 23 | } 24 | 25 | this.router.navigateByUrl('/auth'); 26 | return false; 27 | }), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/jwt/JwtGuard.ts: -------------------------------------------------------------------------------- 1 | import { verify } from 'jsonwebtoken'; 2 | import { CanActivate, ExecutionContext } from '@nestjs/common'; 3 | import { RpcException } from '@nestjs/microservices'; 4 | import { status } from 'grpc'; 5 | 6 | import { UnauthenticatedException } from '@lib/exceptions'; 7 | 8 | const TOKEN_HEADER_NAME = 'authorization'; 9 | const DECODING_OPTIONS = { 10 | algorithms: ['RS256'], 11 | }; 12 | 13 | export class JwtGuard implements CanActivate { 14 | canActivate(context: ExecutionContext): boolean { 15 | const meta = context.getArgByIndex(1); 16 | const token = meta.get(TOKEN_HEADER_NAME)[0]; 17 | 18 | if (token) { 19 | try { 20 | meta.payload = verify(token, process.env.JWT_PUB, DECODING_OPTIONS); 21 | 22 | return true; 23 | } catch (error) { 24 | throw new RpcException({code: status.UNAUTHENTICATED, message: error.message}); 25 | } 26 | } else { 27 | throw new UnauthenticatedException(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-grpc-angular", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "@AlexDaSoul", 6 | "license": "MIT", 7 | "scripts": { 8 | "postinstall": "lerna bootstrap && npm run build:grpc", 9 | "build:grpc": "npm run build:grpc:back && npm run build:grpc:front", 10 | "build:grpc:back": "node devtools/build-grpc-back.js", 11 | "build:grpc:front": "node devtools/build-grpc-front.js", 12 | "clean:all": "npm run clean:proto:back && npm run clean:proto:front && npm run clean:nm", 13 | "clean:proto:back": "rimraf packages/backend/libs/grpc-proto", 14 | "clean:proto:front": "rimraf packages/frontend/src/app/grpc/proto", 15 | "clean:nm": "rimraf **/node_modules/**" 16 | }, 17 | "dependencies": { 18 | "app-root-path": "^2.2.1", 19 | "fs-extra": "^8.0.1", 20 | "glob": "^7.1.3", 21 | "protoc-gen-grpc-web": "^1.0.5", 22 | "rimraf": "^2.6.3" 23 | }, 24 | "devDependencies": { 25 | "lerna": "^3.19.0", 26 | "protobufjs": "^6.8.8" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/services/dal/data-updaters/MessageDataUpdater.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Client } from 'pg'; 3 | import { from, Observable } from 'rxjs'; 4 | import { map, switchMap } from 'rxjs/operators'; 5 | 6 | import { api as chatApi } from '@grpc-proto/chat/message'; 7 | import { api as chatTypes } from '@grpc-proto/chat/chat.types'; 8 | import { MessageDataFinder } from '@chat/services/dal/data-finders/MessageDataFinder'; 9 | 10 | @Injectable() 11 | export class MessageDataUpdater { 12 | 13 | constructor( 14 | private readonly db: Client, 15 | private readonly messageDataFinder: MessageDataFinder, 16 | ) { 17 | } 18 | 19 | public updateMessage(data: chatApi.chat.EditMessageReq): Observable { 20 | const query = `update api_message set message = $1 where id = $2`; 21 | 22 | return from(this.messageDataFinder.getMessageOne(data.id)).pipe( 23 | switchMap(() => from(this.db.query(query, [data.message, data.id]))), 24 | map(res => res.rows[0]), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/frontend/README.md: -------------------------------------------------------------------------------- 1 | # Frontend 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.3.7. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /grpc-proto/user/user.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package api.user; 4 | 5 | import "user.types.proto"; 6 | import "user.enum.proto"; 7 | 8 | service UserService { 9 | rpc CreateUser (CreateUserReq) returns (UserRes) { 10 | } 11 | 12 | rpc UpdateUser (UpdateUserReq) returns (UserRes) { 13 | } 14 | 15 | rpc DeleteUser (UserReq) returns (UserRes) { 16 | } 17 | 18 | rpc VerifyUser (VerifyUserReq) returns (User) { 19 | } 20 | 21 | rpc GetUser (UserReq) returns (User) { 22 | } 23 | 24 | rpc GetUsersAll (Stub) returns (UsersRes) { 25 | } 26 | } 27 | 28 | message CreateUserReq { 29 | string name = 1; 30 | string email = 2; 31 | string password = 3; 32 | string avatar = 4; 33 | } 34 | 35 | message UpdateUserReq { 36 | string name = 1; 37 | string email = 2; 38 | string avatar = 3; 39 | } 40 | 41 | message VerifyUserReq { 42 | string email = 1; 43 | string password = 2; 44 | } 45 | 46 | message UserReq { 47 | string id = 1; 48 | } 49 | 50 | message UserRes { 51 | EStatus status = 1; 52 | string message = 2; 53 | } 54 | 55 | message UsersRes { 56 | repeated User users = 1; 57 | } 58 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/services/dal/data-updaters/UserDataUpdater.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Client } from 'pg'; 3 | import { from, Observable } from 'rxjs'; 4 | import { map, switchMap } from 'rxjs/operators'; 5 | 6 | import { api as userTypes } from '@grpc-proto/user/user.types'; 7 | import { api as userApi } from '@grpc-proto/user/user'; 8 | 9 | import { UserDataFinder } from '@user/services/dal/data-finders/UserDataFinder'; 10 | 11 | @Injectable() 12 | export class UserDataUpdater { 13 | 14 | constructor( 15 | private readonly db: Client, 16 | private readonly userDataFinder: UserDataFinder, 17 | ) { 18 | } 19 | 20 | public updateUser(data: userApi.user.UpdateUserReq, id: string): Observable { 21 | const query = `update api_user set name = $1, email = $2, avatar = $3 where id = $4`; 22 | 23 | return from(this.userDataFinder.getUserOne(id)).pipe( 24 | switchMap(() => from(this.db.query(query, 25 | [data.name, data.email, data.avatar, id]))), 26 | map(res => res.rows[0]), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es2017", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true, 13 | "paths": { 14 | "@grpc-proto": [ 15 | "libs/grpc-proto" 16 | ], 17 | "@grpc-proto/*": [ 18 | "libs/grpc-proto/*" 19 | ], 20 | "@lib": [ 21 | "libs/lib/src" 22 | ], 23 | "@lib/*": [ 24 | "libs/lib/src/*" 25 | ], 26 | "@auth/*": [ 27 | "apps/auth/src/*" 28 | ], 29 | "@chat/*": [ 30 | "apps/chat/src/*" 31 | ], 32 | "@user/*": [ 33 | "apps/user/src/*" 34 | ] 35 | } 36 | }, 37 | "newLine": "LF", 38 | "exclude": [ 39 | "node_modules", 40 | "dist" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/chat/message/message.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | 4 | &.self { 5 | text-align: right; 6 | 7 | .message { 8 | background: #00564d; 9 | } 10 | } 11 | } 12 | 13 | .message { 14 | margin: 0 .15rem .1rem; 15 | padding: .5rem; 16 | background: #424242; 17 | display: inline-block; 18 | 19 | .mat-card-header { 20 | padding-bottom: .3rem; 21 | border-bottom: 1px rgba(175, 175, 175, 0.16) solid; 22 | } 23 | 24 | .mat-card-avatar { 25 | background-color: #3c3c3c; 26 | background-size: cover; 27 | width: 20px; 28 | height: 20px; 29 | } 30 | 31 | .mat-card-title { 32 | font-size: .75rem; 33 | margin: 0; 34 | display: inline; 35 | } 36 | 37 | .date { 38 | flex-grow: 1; 39 | text-align: right; 40 | color: #afafaf; 41 | font-size: .6rem; 42 | align-self: center; 43 | line-height: 1.5rem; 44 | } 45 | 46 | .mat-card-content { 47 | padding-top: .5rem; 48 | margin-bottom: 0; 49 | font-size: .75rem; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/frontend/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent, 9 | ], 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'frontend'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app.title).toEqual('frontend'); 23 | }); 24 | 25 | it('should render title in a h1 tag', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to frontend!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/frontend/src/app/grpc/services/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Metadata } from 'grpc-web'; 4 | 5 | import { grpcUnary } from '@grpc/helpers/grpc-unary'; 6 | import { grpcJwtMetadata } from '@grpc/helpers/grpc-metadata'; 7 | 8 | import { AuthServicePromiseClient } from '@grpc/proto/auth/auth_grpc_web_pb'; 9 | import { AuthReq, AuthRes } from '@grpc/proto/auth/auth_pb'; 10 | import { Stub } from '@grpc/proto/auth/auth.types_pb'; 11 | 12 | @Injectable({ 13 | providedIn: 'root', 14 | }) 15 | export class AuthGrpcService { 16 | 17 | constructor(private client: AuthServicePromiseClient) { 18 | } 19 | 20 | public auth(data: AuthReq.AsObject): Observable { 21 | const req = new AuthReq(); 22 | 23 | req.setEmail(data.email); 24 | req.setPassword(data.password); 25 | 26 | return grpcUnary(this.client.auth(req)); 27 | } 28 | 29 | public updateAuth(): Observable { 30 | const req = new Stub(); 31 | const meta: Metadata = grpcJwtMetadata(); 32 | 33 | return grpcUnary(this.client.updateAuth(req, meta)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/user/auth/auth.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Sign In 4 | 5 | 6 | 7 | 8 | Enter your email address 9 | Invalid email address 10 | 11 | 12 | 13 | 14 | Enter your password 15 | Min length is 4 symbols 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/user/user.routes.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { AuthGuard } from '@share/guards/auth.guard'; 5 | 6 | import { UserComponent } from './user.component'; 7 | import { AuthComponent } from './auth/auth.component'; 8 | import { RegisterComponent } from './register/register.component'; 9 | import { SettingsComponent } from './settings/settings.component'; 10 | 11 | const routes: Routes = [ 12 | { 13 | path: '', 14 | component: UserComponent, 15 | children: [ 16 | { 17 | path: 'auth', 18 | component: AuthComponent, 19 | }, 20 | { 21 | path: 'register', 22 | component: RegisterComponent, 23 | }, 24 | { 25 | path: 'settings', 26 | component: SettingsComponent, 27 | canActivate: [AuthGuard], 28 | }, 29 | { 30 | path: '', 31 | pathMatch: 'full', 32 | redirectTo: 'auth', 33 | }, 34 | ], 35 | }, 36 | ]; 37 | 38 | export const RoutingModule: ModuleWithProviders = RouterModule.forChild(routes); 39 | -------------------------------------------------------------------------------- /packages/frontend/src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage/frontend'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/user/settings/settings.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Profile settings 4 | 5 | 6 | 7 | 8 | Enter your name 9 | 10 | 11 | 12 | 13 | Enter your email address 14 | Invalid email address 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/services/dal/db/DbModule.ts: -------------------------------------------------------------------------------- 1 | import { Module, OnModuleInit } from '@nestjs/common'; 2 | import { Client } from 'pg'; 3 | import * as DBMigrate from 'db-migrate'; 4 | import { from } from 'rxjs'; 5 | import { take } from 'rxjs/operators'; 6 | 7 | import { Logger } from '@lib/logger'; 8 | import { dbConfig, migrateConfig } from '@chat/env'; 9 | 10 | @Module({ 11 | exports: [Client], 12 | providers: [ 13 | { 14 | provide: Client, 15 | useFactory: () => new Client(dbConfig), 16 | }, 17 | ], 18 | }) 19 | export class DbModule implements OnModuleInit { 20 | private readonly logger = new Logger('DbModule'); 21 | private readonly dbmigrate = DBMigrate.getInstance(true, migrateConfig); 22 | 23 | constructor(private readonly db: Client) { 24 | } 25 | 26 | onModuleInit() { 27 | if (this.dbmigrate) { 28 | from(this.dbmigrate.up()) 29 | .pipe(take(1)) 30 | .subscribe( 31 | () => { 32 | this.logger.info('Migrations applied successfully'); 33 | this.db.connect(); 34 | }, 35 | (error) => { 36 | this.logger.error(error); 37 | }, 38 | ); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/services/dal/db/DbModule.ts: -------------------------------------------------------------------------------- 1 | import { Module, OnModuleInit } from '@nestjs/common'; 2 | import { Client } from 'pg'; 3 | import * as DBMigrate from 'db-migrate'; 4 | import { from } from 'rxjs'; 5 | import { take } from 'rxjs/operators'; 6 | 7 | import { Logger } from '@lib/logger'; 8 | import { dbConfig, migrateConfig } from '@user/env'; 9 | 10 | @Module({ 11 | exports: [Client], 12 | providers: [ 13 | { 14 | provide: Client, 15 | useFactory: () => new Client(dbConfig), 16 | }, 17 | ], 18 | }) 19 | export class DbModule implements OnModuleInit { 20 | private readonly logger = new Logger('DbModule'); 21 | private readonly dbmigrate = DBMigrate.getInstance(true, migrateConfig); 22 | 23 | constructor(private readonly db: Client) { 24 | } 25 | 26 | onModuleInit() { 27 | if (this.dbmigrate) { 28 | from(this.dbmigrate.up()) 29 | .pipe(take(1)) 30 | .subscribe( 31 | () => { 32 | this.logger.info('Migrations applied successfully'); 33 | this.db.connect(); 34 | }, 35 | (error) => { 36 | this.logger.error(error); 37 | }, 38 | ); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/exceptions/filter/RpcExceptionFilter.ts: -------------------------------------------------------------------------------- 1 | import { Catch, ArgumentsHost } from '@nestjs/common'; 2 | import { BaseRpcExceptionFilter } from '@nestjs/microservices'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { ExceptionType, EXCEPTION_LIST } from './types'; 6 | import { IExceptionHandlerFactory } from './handlers/interfaces'; 7 | import { ExceptionHandlerFactory } from './handlers/ExceptionHandlerFactory'; 8 | 9 | @Catch(...EXCEPTION_LIST) 10 | export class RpcExceptionFilter extends BaseRpcExceptionFilter { 11 | private readonly exceptionHandlerFactory: IExceptionHandlerFactory; 12 | 13 | public static for(label: string): RpcExceptionFilter { 14 | return new RpcExceptionFilter(label); 15 | } 16 | 17 | protected constructor(protected readonly label: string) { 18 | super(); 19 | 20 | // for the admin panel, you don’t need to monitor errors 21 | // such as from CouchDb, so we pass separate AdminExceptionHandlerFactory to it, 22 | // and for web-backend - WebBackExceptionHandlerFactory 23 | this.exceptionHandlerFactory = new ExceptionHandlerFactory(this.label); 24 | } 25 | 26 | public catch(exception: ExceptionType, host: ArgumentsHost): Observable { 27 | const handler = this.exceptionHandlerFactory.getHandler(exception); 28 | 29 | handler.warnAboutError(); 30 | 31 | return super.catch(handler.wrapError(), host as any); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/logger/message/MessagePrinter.ts: -------------------------------------------------------------------------------- 1 | import { LogLevelType } from '../constants'; 2 | import { MessageBuilder } from './MessageBuilder'; 3 | 4 | const NOOP = () => ({}); 5 | 6 | export class MessagePrinter { 7 | constructor(private readonly messageBuilder: MessageBuilder) { 8 | } 9 | 10 | public print(level: LogLevelType, args: any[]): void { 11 | this.printPreparedMessage(this.messageBuilder.build(level, args) + '\n'); 12 | } 13 | 14 | // chat: check this implementation in https://sdexnt.atlassian.net/browse/WEBBACK-485 15 | private printPreparedMessage(message: string): void { 16 | // see: https://github.com/nodejs/node/blob/master/lib/internal/console/constructor.js#L232 17 | 18 | // there may be an error occurring synchronously (e.g. for files or TTYs 19 | // on POSIX systems) or asynchronously (e.g. pipes on POSIX systems), so 20 | // handle both situations. 21 | try { 22 | // add and later remove a noop error handler to catch synchronous errors. 23 | if (process.stdout.listenerCount('error') === 0) { 24 | process.stdout.once('error', NOOP); 25 | } 26 | 27 | process.stdout.write(message, NOOP); 28 | } catch (err) { 29 | // there's no proper way to pass along the error here 30 | } finally { 31 | process.stdout.removeListener('error', NOOP); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/logger/Logger.ts: -------------------------------------------------------------------------------- 1 | import { ALLOWED_LOG_BY_LEVEL, DEFAULT_LOGGER_LEVEL, LogLevelType, LOG_LEVEL_NAME } from './constants'; 2 | import { MessageBuilder } from './message/MessageBuilder'; 3 | import { MessagePrinter } from './message/MessagePrinter'; 4 | 5 | const CURRENT_LOG_LEVEL = process.env.LOGGER_LEVEL || DEFAULT_LOGGER_LEVEL; 6 | const CURRENT_ALLOWED_LEVELS = ALLOWED_LOG_BY_LEVEL[CURRENT_LOG_LEVEL]; 7 | 8 | export class Logger { 9 | private readonly messagePrinter: MessagePrinter; 10 | private readonly messageBuilder: MessageBuilder; 11 | 12 | constructor(private readonly label: string) { 13 | this.messageBuilder = new MessageBuilder(this.label); 14 | this.messagePrinter = new MessagePrinter(this.messageBuilder); 15 | } 16 | 17 | public debug(...args: any[]): void { 18 | this.logMessage(LOG_LEVEL_NAME.debug, args); 19 | } 20 | 21 | public info(...args: any[]): void { 22 | this.logMessage(LOG_LEVEL_NAME.info, args); 23 | } 24 | 25 | public error(...args: any[]): void { 26 | this.logMessage(LOG_LEVEL_NAME.error, args); 27 | } 28 | 29 | public security(...args: any[]): void { 30 | this.logMessage(LOG_LEVEL_NAME.security, args); 31 | } 32 | 33 | private logMessage(currentLevel: LogLevelType, args: any[]): void { 34 | if (CURRENT_ALLOWED_LEVELS.has(currentLevel)) { 35 | this.messagePrinter.print(currentLevel, args); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NGXLogger } from 'ngx-logger'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { jwtAuthError$ } from '@grpc/helpers/grpc-jwt'; 6 | import { AuthService } from '@share/services/auth.service'; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | templateUrl: './app.component.html', 11 | styleUrls: ['./app.component.scss'], 12 | }) 13 | export class AppComponent implements OnInit { 14 | 15 | public isLoggedIn$: Observable = this.authService.isLoggedIn(); 16 | 17 | constructor( 18 | private logger: NGXLogger, 19 | private authService: AuthService, 20 | ) { 21 | } 22 | 23 | ngOnInit() { 24 | const updateAuth = this.authService.updateAuth(); 25 | 26 | if (updateAuth instanceof Observable) { 27 | updateAuth 28 | .subscribe( 29 | res => this.authService.loggedIn(res.token), 30 | err => { 31 | this.authService.logout(); 32 | this.logger.error(err); 33 | }, 34 | ); 35 | } 36 | 37 | jwtAuthError$.asObservable() 38 | .subscribe(() => { 39 | this.logout(); 40 | this.logger.warn('JWT is not valid'); 41 | }); 42 | } 43 | 44 | public logout(): void { 45 | this.authService.logout(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/jwt/CertsService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleInit } from '@nestjs/common'; 2 | import { Client, ClientGrpc } from '@nestjs/microservices'; 3 | import { timer, throwError } from 'rxjs'; 4 | import { retryWhen, tap, mergeMap } from 'rxjs/operators'; 5 | 6 | import { Logger } from '@lib/logger'; 7 | import { grpcAuth } from '@lib/utils/GrpcConfigs'; 8 | 9 | import { api } from '@grpc-proto/auth/auth'; 10 | 11 | const RETRY = 10; 12 | 13 | @Injectable() 14 | export class CertsService implements OnModuleInit { 15 | private readonly logger = new Logger('CertsService'); 16 | 17 | @Client(grpcAuth) private readonly grpcAuthClient: ClientGrpc; 18 | private grpcAuthService: api.auth.AuthService; 19 | 20 | public onModuleInit(): void { 21 | this.grpcAuthService = this.grpcAuthClient.getService('AuthService'); 22 | 23 | this.grpcAuthService.getCertStream({}) 24 | .pipe( 25 | retryWhen(errors => 26 | errors.pipe( 27 | tap(err => this.logger.error(err.message + '. Will try again after timeout in 3s.')), 28 | mergeMap(() => (RETRY ? timer(3000) : 29 | throwError(`Can't reconnect to CertStream', timeout expired.`))), 30 | ), 31 | ), 32 | ) 33 | .subscribe((res) => { 34 | process.env.JWT_PUB = res.key; 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /devtools/build-grpc-front.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/node 2 | 3 | const cp = require('child_process'); 4 | const path = require('path'); 5 | const fs = require('fs-extra'); 6 | const rimraf = require('rimraf'); 7 | const root = require('app-root-path'); 8 | const helpers = require('./helpers'); 9 | 10 | const grpcDir = root.resolve('grpc-proto/.tmp'); 11 | const grpcFrontDir = root.resolve('packages/frontend/src/app/grpc/proto'); 12 | 13 | const IGNORE_PACKAGES = [ 14 | // ... 15 | ]; 16 | 17 | const IGNORE_PROTO_FILES = ['**/index.proto']; 18 | 19 | // copy grpc-proto 20 | rimraf.sync(grpcDir); 21 | rimraf.sync(grpcFrontDir); 22 | 23 | // **** build grpc-web 24 | helpers.getPackages(IGNORE_PACKAGES, root.resolve('grpc-proto')).forEach(package => { 25 | const protos = helpers.getProtosList(IGNORE_PROTO_FILES, package); 26 | const pkgName = path.basename(package); 27 | const pkgPath = `${grpcDir}/${pkgName}`; 28 | 29 | const CMD = 30 | `protoc -I=${package} ${protos}` + 31 | ` --js_out=import_style=commonjs:${pkgPath}` + 32 | ` --grpc-web_out=import_style=commonjs+dts,mode=grpcwebtext:${pkgPath}`; 33 | 34 | try { 35 | console.log(`Build grpc-web for '${pkgName}'`); 36 | fs.ensureDirSync(pkgPath); 37 | cp.execSync(CMD, { cwd: root.path, stdio: 'inherit' }); 38 | } catch (err) { 39 | process.exit(err.status); 40 | } 41 | }); 42 | 43 | fs.copySync(root.resolve('grpc-proto/.tmp'), grpcFrontDir); 44 | rimraf.sync(grpcDir); 45 | 46 | process.exit(0); 47 | -------------------------------------------------------------------------------- /packages/backend/apps/auth/src/services/JwtCertsService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { sign, verify, SignOptions } from 'jsonwebtoken'; 3 | 4 | import { AUTH_CREDENTIALS_INVALID, UnauthenticatedException } from '@lib/exceptions'; 5 | 6 | import { api } from '@grpc-proto/user/user.types'; 7 | 8 | import { JWT_EXPIRE } from '@auth/env'; 9 | 10 | interface IDecodedUserData { 11 | id: string; 12 | email: string; 13 | } 14 | 15 | const env = process.env; 16 | 17 | @Injectable() 18 | export class JwtCertsService { 19 | public addToken(user: api.user.User, expiresIn: number = +JWT_EXPIRE): string { 20 | if (!user) { 21 | throw new UnauthenticatedException(AUTH_CREDENTIALS_INVALID); 22 | } 23 | 24 | const options: SignOptions = { 25 | algorithm: 'RS256', 26 | }; 27 | 28 | if (expiresIn) { 29 | options.expiresIn = expiresIn; 30 | } 31 | 32 | const payload = { 33 | id: user.id, 34 | email: user.email, 35 | }; 36 | 37 | return sign(payload, env.JWT_PRIV, { 38 | expiresIn, 39 | algorithm: 'RS256', 40 | }); 41 | } 42 | 43 | public verifyToken(token: string): IDecodedUserData { 44 | try { 45 | return verify(token, env.JWT_PUB, { 46 | algorithms: ['RS256'], 47 | }) as IDecodedUserData; 48 | } catch (ignored) { 49 | throw new UnauthenticatedException(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/frontend/src/app/share/share.module.ts: -------------------------------------------------------------------------------- 1 | import 'hammerjs'; 2 | 3 | import { NgModule } from '@angular/core'; 4 | import { CommonModule } from '@angular/common'; 5 | import { RouterModule } from '@angular/router'; 6 | import { ReactiveFormsModule } from '@angular/forms'; 7 | import { HttpClientModule } from '@angular/common/http'; 8 | import { ScrollingModule } from '@angular/cdk/scrolling'; 9 | 10 | import { 11 | MatIconModule, 12 | MatButtonModule, 13 | MatCardModule, 14 | MatFormFieldModule, 15 | MatInputModule, 16 | MatSnackBarModule, 17 | MatToolbarModule, 18 | } from '@angular/material'; 19 | 20 | import { LoggerModule } from 'ngx-logger'; 21 | 22 | import { environment } from '@environments/environment'; 23 | 24 | @NgModule({ 25 | imports: [ 26 | CommonModule, 27 | RouterModule, 28 | ReactiveFormsModule, 29 | HttpClientModule, 30 | MatButtonModule, 31 | MatIconModule, 32 | MatCardModule, 33 | MatFormFieldModule, 34 | MatInputModule, 35 | MatSnackBarModule, 36 | MatToolbarModule, 37 | ScrollingModule, 38 | LoggerModule.forRoot(environment.logger), 39 | ], 40 | exports: [ 41 | MatButtonModule, 42 | MatIconModule, 43 | MatCardModule, 44 | MatFormFieldModule, 45 | MatInputModule, 46 | HttpClientModule, 47 | MatSnackBarModule, 48 | MatToolbarModule, 49 | ScrollingModule, 50 | ], 51 | }) 52 | export class ShareModule { 53 | } 54 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/services/dal/db/migrations/20191201192021-InitialMigration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dbm; 4 | var type; 5 | var seed; 6 | var fs = require('fs'); 7 | var path = require('path'); 8 | var Promise; 9 | 10 | /** 11 | * We receive the dbmigrate dependency from dbmigrate initially. 12 | * This enables us to not have to rely on NODE_PATH. 13 | */ 14 | exports.setup = function(options, seedLink) { 15 | dbm = options.dbmigrate; 16 | type = dbm.dataType; 17 | seed = seedLink; 18 | Promise = options.Promise; 19 | }; 20 | 21 | exports.up = function(db) { 22 | var filePath = path.join(__dirname, 'sqls', '20191201192021-InitialMigration-up.sql'); 23 | return new Promise( function( resolve, reject ) { 24 | fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){ 25 | if (err) return reject(err); 26 | console.log('received data: ' + data); 27 | 28 | resolve(data); 29 | }); 30 | }) 31 | .then(function(data) { 32 | return db.runSql(data); 33 | }); 34 | }; 35 | 36 | exports.down = function(db) { 37 | var filePath = path.join(__dirname, 'sqls', '20191201192021-InitialMigration-down.sql'); 38 | return new Promise( function( resolve, reject ) { 39 | fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){ 40 | if (err) return reject(err); 41 | console.log('received data: ' + data); 42 | 43 | resolve(data); 44 | }); 45 | }) 46 | .then(function(data) { 47 | return db.runSql(data); 48 | }); 49 | }; 50 | 51 | exports._meta = { 52 | "version": 1 53 | }; 54 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/services/dal/db/migrations/20191201221322-InitialMigration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dbm; 4 | var type; 5 | var seed; 6 | var fs = require('fs'); 7 | var path = require('path'); 8 | var Promise; 9 | 10 | /** 11 | * We receive the dbmigrate dependency from dbmigrate initially. 12 | * This enables us to not have to rely on NODE_PATH. 13 | */ 14 | exports.setup = function(options, seedLink) { 15 | dbm = options.dbmigrate; 16 | type = dbm.dataType; 17 | seed = seedLink; 18 | Promise = options.Promise; 19 | }; 20 | 21 | exports.up = function(db) { 22 | var filePath = path.join(__dirname, 'sqls', '20191201221322-InitialMigration-up.sql'); 23 | return new Promise( function( resolve, reject ) { 24 | fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){ 25 | if (err) return reject(err); 26 | console.log('received data: ' + data); 27 | 28 | resolve(data); 29 | }); 30 | }) 31 | .then(function(data) { 32 | return db.runSql(data); 33 | }); 34 | }; 35 | 36 | exports.down = function(db) { 37 | var filePath = path.join(__dirname, 'sqls', '20191201221322-InitialMigration-down.sql'); 38 | return new Promise( function( resolve, reject ) { 39 | fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){ 40 | if (err) return reject(err); 41 | console.log('received data: ' + data); 42 | 43 | resolve(data); 44 | }); 45 | }) 46 | .then(function(data) { 47 | return db.runSql(data); 48 | }); 49 | }; 50 | 51 | exports._meta = { 52 | "version": 1 53 | }; 54 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/services/dal/db/migrations/20191201221519-InsertDemoUsersMigration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dbm; 4 | var type; 5 | var seed; 6 | var fs = require('fs'); 7 | var path = require('path'); 8 | var Promise; 9 | 10 | /** 11 | * We receive the dbmigrate dependency from dbmigrate initially. 12 | * This enables us to not have to rely on NODE_PATH. 13 | */ 14 | exports.setup = function(options, seedLink) { 15 | dbm = options.dbmigrate; 16 | type = dbm.dataType; 17 | seed = seedLink; 18 | Promise = options.Promise; 19 | }; 20 | 21 | exports.up = function(db) { 22 | var filePath = path.join(__dirname, 'sqls', '20191201221519-InsertDemoUsersMigration-up.sql'); 23 | return new Promise( function( resolve, reject ) { 24 | fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){ 25 | if (err) return reject(err); 26 | console.log('received data: ' + data); 27 | 28 | resolve(data); 29 | }); 30 | }) 31 | .then(function(data) { 32 | return db.runSql(data); 33 | }); 34 | }; 35 | 36 | exports.down = function(db) { 37 | var filePath = path.join(__dirname, 'sqls', '20191201221519-InsertDemoUsersMigration-down.sql'); 38 | return new Promise( function( resolve, reject ) { 39 | fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){ 40 | if (err) return reject(err); 41 | console.log('received data: ' + data); 42 | 43 | resolve(data); 44 | }); 45 | }) 46 | .then(function(data) { 47 | return db.runSql(data); 48 | }); 49 | }; 50 | 51 | exports._meta = { 52 | "version": 1 53 | }; 54 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/chat/chat-list/chat-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy, ViewChild, Input, OnChanges, SimpleChanges } from '@angular/core'; 2 | import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; 3 | 4 | import { getUserIdFromJWT } from '@grpc/helpers/grpc-get-id'; 5 | import { Message } from '@grpc/proto/chat/chat.types_pb'; 6 | import { AuthService } from '@share/services/auth.service'; 7 | 8 | @Component({ 9 | selector: 'app-chat-list', 10 | templateUrl: './chat-list.component.html', 11 | styleUrls: ['./chat-list.component.scss'], 12 | changeDetection: ChangeDetectionStrategy.OnPush, 13 | }) 14 | export class ChatListComponent implements OnChanges { 15 | 16 | @ViewChild(CdkVirtualScrollViewport, { static: true }) 17 | private viewport: CdkVirtualScrollViewport; 18 | 19 | @Input() public newMessages: Message.AsObject[]; 20 | 21 | public messages: Message.AsObject[] = []; 22 | public userId: string = getUserIdFromJWT(this.authService.getToken()); 23 | public itemSize = 30; 24 | 25 | constructor(private authService: AuthService) { 26 | } 27 | 28 | ngOnChanges(changes: SimpleChanges): void { 29 | if (changes.newMessages && Array.isArray(changes.newMessages.currentValue)) { 30 | this.messages = [...this.messages, ...changes.newMessages.currentValue]; 31 | 32 | setTimeout(() => { 33 | // TODO: trash 34 | this.viewport.scrollToIndex(this.messages.length * 100); 35 | }, 100); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/services/dal/db/migrations/20191201192257-InsertDemoMessagesMigration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dbm; 4 | var type; 5 | var seed; 6 | var fs = require('fs'); 7 | var path = require('path'); 8 | var Promise; 9 | 10 | /** 11 | * We receive the dbmigrate dependency from dbmigrate initially. 12 | * This enables us to not have to rely on NODE_PATH. 13 | */ 14 | exports.setup = function(options, seedLink) { 15 | dbm = options.dbmigrate; 16 | type = dbm.dataType; 17 | seed = seedLink; 18 | Promise = options.Promise; 19 | }; 20 | 21 | exports.up = function(db) { 22 | var filePath = path.join(__dirname, 'sqls', '20191201192257-InsertDemoMessagesMigration-up.sql'); 23 | return new Promise( function( resolve, reject ) { 24 | fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){ 25 | if (err) return reject(err); 26 | console.log('received data: ' + data); 27 | 28 | resolve(data); 29 | }); 30 | }) 31 | .then(function(data) { 32 | return db.runSql(data); 33 | }); 34 | }; 35 | 36 | exports.down = function(db) { 37 | var filePath = path.join(__dirname, 'sqls', '20191201192257-InsertDemoMessagesMigration-down.sql'); 38 | return new Promise( function( resolve, reject ) { 39 | fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){ 40 | if (err) return reject(err); 41 | console.log('received data: ' + data); 42 | 43 | resolve(data); 44 | }); 45 | }) 46 | .then(function(data) { 47 | return db.runSql(data); 48 | }); 49 | }; 50 | 51 | exports._meta = { 52 | "version": 1 53 | }; 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NestJs gRPC Angular 2 | 3 | https://medium.com/ngx/nestjs-angular-grpc-f8eca5404fc7 4 | 5 | Exapmle of [Nestjs](https://nestjs.com/) microservices with [gRPC](https://grpc.io/) and [Angular](https://angular.io/) SPA. Chat with JWT 6 | (JWS) authorization 7 | and message stream. 8 | 9 | ### installation 10 | 11 | * Install [protoc](https://github.com/protocolbuffers/protobuf) and [protoc-gen-grpc-web](https://github.com/grpc/grpc-web/releases) for your OS 12 | * Install [nest cli](https://docs.nestjs.com/cli/overview) 13 | * Install [db-migrate](https://github.com/db-migrate/node-db-migrate) 14 | * Install [grpcurl](https://github.com/fullstorydev/grpcurl) 15 | * Install [docker](https://docs.docker.com/install/) and [docker-compose](https://docs.docker.com/compose/install/) 16 | * `npm install` in project root directory 17 | 18 | ### Usage 19 | 20 | Backend: 21 | * `npm run docker:dev:[up|down|restart]` for backend with docker and all microservices. Debug in `docker logs -f 22 | [auth|chat|user]` 23 | * `docker logs [auth|chat|user]` or use plugins for docker in your IDE 24 | * `nest start [--debug --watch] [auth|chat|user]` for start without docker 25 | * `nest build [auth|chat|user]` for build dist 26 | * `db-migrate [up|down|reset|create|db] [[dbname/]migrationName|all] [options]` 27 | * For example `db-migrate create -e user --sql-file -m apps/user/src/services/dal/db/migrations` 28 | 29 | Frontend: 30 | * `cd frontend && npm run start` 31 | * `npm run build --prod` 32 | 33 | If need regenerate grpc use: 34 | * `npm run build:grpc:back` for backend 35 | * `npm run build:grpc:front` for frontend 36 | * `npm run build:grpc` for all 37 | -------------------------------------------------------------------------------- /packages/frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 16 | 17 | 21 | 22 | 26 | 27 | 28 |
29 | 32 |
33 |
34 |
35 | 36 |
37 | 38 |
39 |
40 | -------------------------------------------------------------------------------- /packages/backend/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "apps/user/src", 4 | "monorepo": true, 5 | "root": "apps/user", 6 | "compilerOptions": { 7 | "webpack": true, 8 | "deleteOutDir": true, 9 | "webpackConfigPath": "webpack.config.js", 10 | "tsConfigPath": "apps/user/tsconfig.app.json" 11 | }, 12 | "projects": { 13 | "user": { 14 | "type": "application", 15 | "root": "apps/user", 16 | "entryFile": "main", 17 | "sourceRoot": "apps/user/src", 18 | "compilerOptions": { 19 | "tsConfigPath": "apps/user/tsconfig.app.json" 20 | } 21 | }, 22 | "auth": { 23 | "type": "application", 24 | "root": "apps/auth", 25 | "entryFile": "main", 26 | "sourceRoot": "apps/auth/src", 27 | "compilerOptions": { 28 | "tsConfigPath": "apps/auth/tsconfig.app.json" 29 | } 30 | }, 31 | "chat": { 32 | "type": "application", 33 | "root": "apps/chat", 34 | "entryFile": "main", 35 | "sourceRoot": "apps/chat/src", 36 | "compilerOptions": { 37 | "tsConfigPath": "apps/chat/tsconfig.app.json" 38 | } 39 | }, 40 | "lib": { 41 | "type": "library", 42 | "root": "libs/lib", 43 | "entryFile": "index", 44 | "sourceRoot": "libs/lib/src", 45 | "compilerOptions": { 46 | "tsConfigPath": "libs/lib/tsconfig.lib.json" 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/user/register/register.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Sign Up 4 | 5 | 6 | 7 | 8 | Enter your name 9 | 10 | 11 | 12 | 13 | Enter your email address 14 | Invalid email address 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Enter your password 24 | Min length is 4 symbols 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | -------------------------------------------------------------------------------- /packages/frontend/src/app/grpc/services/chat/message.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Metadata } from 'grpc-web'; 4 | 5 | import { grpcUnary } from '@grpc/helpers/grpc-unary'; 6 | import { grpcJwtMetadata } from '@grpc/helpers/grpc-metadata'; 7 | 8 | import { MessageServicePromiseClient } from '@grpc/proto/chat/message_grpc_web_pb'; 9 | import { DeleteMessageReq, EditMessageReq, SendMessageReq } from '@grpc/proto/chat/message_pb'; 10 | import { ChatRes } from '@grpc/proto/chat/chat.types_pb'; 11 | 12 | @Injectable({ 13 | providedIn: 'root', 14 | }) 15 | export class MessageGrpcService { 16 | 17 | constructor(private client: MessageServicePromiseClient) { 18 | } 19 | 20 | public sendMessage(data: SendMessageReq.AsObject): Observable { 21 | const req = new SendMessageReq(); 22 | const meta: Metadata = grpcJwtMetadata(); 23 | 24 | req.setMessage(data.message); 25 | 26 | return grpcUnary(this.client.sendMessage(req, meta)); 27 | } 28 | 29 | public editMessage(data: EditMessageReq.AsObject): Observable { 30 | const req = new EditMessageReq(); 31 | const meta: Metadata = grpcJwtMetadata(); 32 | 33 | req.setId(data.id); 34 | req.setMessage(data.message); 35 | 36 | return grpcUnary(this.client.editMessage(req, meta)); 37 | } 38 | 39 | public deleteMessage(data: DeleteMessageReq.AsObject): Observable { 40 | const req = new DeleteMessageReq(); 41 | const meta: Metadata = grpcJwtMetadata(); 42 | 43 | req.setId(data.id); 44 | 45 | return grpcUnary(this.client.deleteMessage(req, meta)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/services/dal/data-producers/UserDataProducer.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Client } from 'pg'; 3 | import { createHmac } from 'crypto'; 4 | import { from, Observable } from 'rxjs'; 5 | import { map, switchMap, mapTo } from 'rxjs/operators'; 6 | 7 | import { AlreadyExistsException, EMAIL_ALREADY_EXISTS } from '@lib/exceptions/impl'; 8 | 9 | import { api as userApi } from '@grpc-proto/user/user'; 10 | import { api as chatTypes } from '@grpc-proto/chat/chat.types'; 11 | 12 | import { UserDataFinder } from '@user/services/dal/data-finders/UserDataFinder'; 13 | 14 | import { SALT } from '@user/env'; 15 | 16 | @Injectable() 17 | export class UserDataProducer { 18 | 19 | constructor( 20 | private readonly db: Client, 21 | private readonly userDataFinder: UserDataFinder, 22 | ) { 23 | } 24 | 25 | public createUser(data: userApi.user.CreateUserReq): Observable { 26 | data.password = createHmac('sha512', SALT).update(data.password).digest('hex'); 27 | 28 | const query = `insert into api_user (email, name, avatar, password) values ($1, $2, $3, $4)`; 29 | 30 | return this.checkEmailExistence(data.email).pipe( 31 | switchMap(() => from(this.db.query(query, 32 | [data.email, data.name, data.avatar, data.password]))), 33 | mapTo(null), 34 | ); 35 | } 36 | 37 | private checkEmailExistence(email: string): Observable { 38 | return from(this.userDataFinder.getUserByConditions({ email } as userApi.user.VerifyUserReq)).pipe( 39 | map(user => { 40 | if (user) { 41 | throw new AlreadyExistsException(EMAIL_ALREADY_EXISTS); 42 | } 43 | 44 | return null; 45 | }), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "^8.0.2", 15 | "@angular/cdk": "^8.0.1", 16 | "@angular/common": "~8.0.2", 17 | "@angular/compiler": "~8.0.2", 18 | "@angular/core": "~8.0.2", 19 | "@angular/forms": "~8.0.2", 20 | "@angular/material": "^8.0.1", 21 | "@angular/platform-browser": "~8.0.2", 22 | "@angular/platform-browser-dynamic": "~8.0.2", 23 | "@angular/router": "~8.0.2", 24 | "core-js": "^2.5.4", 25 | "grpc-web": "^1.0.7", 26 | "hammerjs": "^2.0.8", 27 | "ngx-logger": "^3.4.3", 28 | "rxjs": "~6.5.2", 29 | "tslib": "^1.9.0", 30 | "zone.js": "~0.9.1" 31 | }, 32 | "devDependencies": { 33 | "@angular-devkit/build-angular": "~0.800.0", 34 | "@angular/cli": "~8.0.3", 35 | "@angular/compiler-cli": "~8.0.2", 36 | "@angular/language-service": "~8.0.2", 37 | "@types/jasmine": "~2.8.8", 38 | "@types/jasminewd2": "~2.0.3", 39 | "@types/node": "~8.9.4", 40 | "codelyzer": "^5.0.1", 41 | "google-protobuf": "^3.8.0", 42 | "jasmine-core": "~2.99.1", 43 | "jasmine-spec-reporter": "~4.2.1", 44 | "karma": "~4.0.0", 45 | "karma-chrome-launcher": "~2.2.0", 46 | "karma-coverage-istanbul-reporter": "~2.0.1", 47 | "karma-jasmine": "~1.1.2", 48 | "karma-jasmine-html-reporter": "^0.2.2", 49 | "protractor": "~5.4.0", 50 | "ts-node": "~7.0.0", 51 | "tslint": "~5.11.0", 52 | "typescript": "~3.4.5" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/frontend/src/app/grpc/helpers/grpc-stream.ts: -------------------------------------------------------------------------------- 1 | import { Observable, Observer, timer } from 'rxjs'; 2 | import { finalize, share, retryWhen, tap, delayWhen } from 'rxjs/operators'; 3 | import { StatusCode, ClientReadableStream, Status } from 'grpc-web'; 4 | import * as jspb from 'google-protobuf'; 5 | 6 | import { StreamType } from '@grpc/enums/stream-type.grpc.enum'; 7 | import { jwtAuthError$ } from '@grpc/helpers/grpc-jwt'; 8 | 9 | export function grpcStream(client: ClientReadableStream): Observable { 10 | let stream: ClientReadableStream = null; 11 | let subscriptionCounter = 0; 12 | 13 | const data: Observable = new Observable((observer: Observer) => { 14 | if (subscriptionCounter === 0) { 15 | stream = client; 16 | } 17 | subscriptionCounter++; 18 | 19 | stream.on(StreamType.DATA, (response: jspb.Message) => { 20 | observer.next(response.toObject()); 21 | }); 22 | 23 | stream.on(StreamType.STATUS, (status: Status) => { 24 | if (status.code === StatusCode.UNAUTHENTICATED) { 25 | jwtAuthError$.next(); 26 | } 27 | 28 | if (status.code !== StatusCode.OK) { 29 | observer.error(status); 30 | } 31 | }); 32 | }); 33 | 34 | return data.pipe( 35 | finalize(() => { 36 | subscriptionCounter--; 37 | 38 | if (subscriptionCounter === 0) { 39 | stream.cancel(); 40 | } 41 | }), 42 | share(), 43 | retryWhen(errors => 44 | errors.pipe( 45 | // log error message 46 | // TODO: add logger 47 | tap(val => console.warn(`Stream will be reconnected in 30 seconds`)), 48 | // restart in 30 seconds 49 | // TODO: fix deprecated 50 | delayWhen(val => timer(30000)), 51 | ), 52 | ), 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/services/UserService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { Observable } from 'rxjs'; 4 | import { map, mapTo } from 'rxjs/operators'; 5 | 6 | import { api as userTypes } from '@grpc-proto/user/user.types'; 7 | import { api as userApi } from '@grpc-proto/user/user'; 8 | 9 | import { UserDataFinder } from './dal/data-finders/UserDataFinder'; 10 | import { UserDataProducer } from './dal/data-producers/UserDataProducer'; 11 | import { UserDataRemover } from './dal/data-removers/UserDataRemover'; 12 | import { UserDataUpdater } from './dal/data-updaters/UserDataUpdater'; 13 | 14 | @Injectable() 15 | export class UserService { 16 | 17 | constructor( 18 | private readonly userDataFinder: UserDataFinder, 19 | private readonly userDataProducer: UserDataProducer, 20 | private readonly userDataUpdater: UserDataUpdater, 21 | private readonly userDataRemover: UserDataRemover, 22 | ) { 23 | } 24 | 25 | public createUser(data: userApi.user.CreateUserReq): Observable { 26 | return this.userDataProducer.createUser(data); 27 | } 28 | 29 | public updateUser(data: userApi.user.UpdateUserReq, id: string): Observable { 30 | return this.userDataUpdater.updateUser(data, id) 31 | .pipe(mapTo(null)); 32 | } 33 | 34 | public deleteUser(id: string): Observable { 35 | return this.userDataRemover.deleteUser(id) 36 | .pipe(mapTo(null)); 37 | } 38 | 39 | public getUser(id: string): Observable { 40 | return this.userDataFinder.getUserOne(id); 41 | } 42 | 43 | public getUsersAll(): Observable<{ users: userTypes.user.User[] }> { 44 | return this.userDataFinder.getUsersAll().pipe( 45 | map(users => ({users})), 46 | ); 47 | } 48 | 49 | public verifyUser(data: userApi.user.VerifyUserReq): Observable { 50 | return this.userDataFinder.getUserByConditions({...data}); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/backend/apps/auth/src/pki-dev/keys.ts: -------------------------------------------------------------------------------- 1 | export const serviceKey = '-----BEGIN RSA PRIVATE KEY-----\n' + 2 | 'MIIEowIBAAKCAQEAqtZsVS9hOIkB3NY0vMUkwGGYeBvWQOgEhBwW3Pl2FHMoxCqQ\n' + 3 | 'daodgx6PKIvnyV9F3N0RqnlNgiVfpIAdnBnkXLsRKXd5QOGgJppVT8B34yOtZGKM\n' + 4 | 'rdgz06024sB4rmJcVgSABe6mjMNzYpI8ZcgwcbdhQGhOyE2vIclYmOk87Qm1oBJd\n' + 5 | '2ORStKeAJL9sRdM5IJmeR0WEB/LKu1I8LtY6BE4WPfwfmlxQWKYJWAHr62EEy6Pb\n' + 6 | 'UFTZyro5ebFW1vn51NkKMog805pcH7UkXOuCqfnOzwmLcgbox9hvN19NAOLrzxeZ\n' + 7 | 'TJHWNYcJG2j1g5usBSebex/+mT3F8aDaMI1YnQIDAQABAoIBAE0GGBngHslKnFhh\n' + 8 | 'C64AhK1oU0Hz6wmgkkiuEXDX2HEn6r1nI3KpnFy9rnXtfjfAiNMnqQtfXZ7MEu8s\n' + 9 | 'BC2ZTuiwPvCfOUATeg1tkAFBGcyDDW4xMJRA4j0R36kkdkTJfDAcH0yNaPIWPTO4\n' + 10 | 'ExsgwxbCQ0qyvLJ6s/dbvGJU5m9IMLfpU+K57rTwRFk/H7K8al8zUT5kS2tUOQHP\n' + 11 | '/5Iz1yPLh1gWxan36EYAUQ/lWbmbPGiKgsGhvEeGt4rrU+YRXytlziM2iQ3I8zFq\n' + 12 | 'SjS15FlmK0ev0Hi78ni/LFIKoHDxRUUm2c5fe+xxMQAEzpJVU3u0u+1PIgBkUass\n' + 13 | 'g/4G4NECgYEA2FGUFIWgrszuhubK6l2fpq/5tmACSXhURBwkjIooX8ZsyH4WM/g3\n' + 14 | 'NAQFLVGvR2KlKLmAEBeJIBdnyZiXK1uHCvNyXaDAPsRBY0RmrebPyGG62f5sU5DC\n' + 15 | 'VmYfD8oJ/cRa2qUaEwAlPCQOIZE1+QMAUlP/CRhYz4UWcKJ43sYoQycCgYEAyi0I\n' + 16 | 'daYt1F0BbVMiWTSUxWMGabU9zosMPgA9X4AxJoWGRW+DhLNAS7kFKRi9Fx05ZiPk\n' + 17 | 'jb3nOawWdNsDPPidED89WfoE4TlMR5uqi63ePVr5BAM4Dr04+BbsYFoticX11h/w\n' + 18 | 'bxGaSuGRme+M/7K4M9+7yp0uQVilvRJ0nNGb0JsCgYBD4Q17hxcN4wayVDe2ZVyU\n' + 19 | 'vMG6JdRx441ltgMOCshyjVxTaaVj926zJtPNDcXXu6+h4Nu7sPb5l/6cdwJwu47b\n' + 20 | 's9reYHQS/hiaorsptLTc5zXv8/NgIZup6u+yT67k77mmxIozDiehAJtikyOBmRx/\n' + 21 | 'uRXdb8NmkxegjospNLsrnwKBgH/WueKqkZAWvzBBwRZnCStG0mdFEy/m/Ha38BbT\n' + 22 | 'GEEjbSO6v47JSX6YH4s8+VQERqcvSvXVfsAY8JozYnjLO4Vqd4DNdwhzEqi05cIs\n' + 23 | 'zro9K/g9kNTBEaTN2emTG/hiFHCxAXc5yjZPK6IKtz135MHoVvZnLThktWg4o0QF\n' + 24 | 'xmDBAoGBAJS3rwuFAMgWQcBxSKSTsT3qJnawlMVGhgcgytD0jN9X6m8/lHQ8uhXv\n' + 25 | 'rfx92S2Z6g6XAGE/vuZNQdMrQnfyqpOrcynHUaiG35PbXKQJ80ZyxL51zNQ/7xx3\n' + 26 | 'GsF4JsTLLNODYqxwsrAAxyMXWQovmWwWRV6tiQAWa0N6C4UqkPoT\n' + 27 | '-----END RSA PRIVATE KEY-----'; 28 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/chat/chat.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | import { MatSnackBar } from '@angular/material/snack-bar'; 3 | import { Observable } from 'rxjs'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | import { ChatGrpcService } from '@grpc/services/chat/chat.service'; 7 | import { MessageGrpcService } from '@grpc/services/chat/message.service'; 8 | import { Message } from '@grpc/proto/chat/chat.types_pb'; 9 | 10 | @Component({ 11 | selector: 'app-chat', 12 | templateUrl: './chat.component.html', 13 | styleUrls: ['./chat.component.scss'], 14 | changeDetection: ChangeDetectionStrategy.OnPush, 15 | }) 16 | export class ChatComponent implements OnInit { 17 | 18 | public messages: Observable = this.chatGrpcService.getChat() 19 | .pipe(map(res => res.messagesList)); 20 | 21 | constructor( 22 | private chatGrpcService: ChatGrpcService, 23 | private messageGrpcService: MessageGrpcService, 24 | private snackBar: MatSnackBar, 25 | ) { 26 | } 27 | 28 | ngOnInit() { 29 | } 30 | 31 | public onSend(message: string): void { 32 | if (message) { 33 | this.messageGrpcService.sendMessage({ message }) 34 | .subscribe( 35 | res => { 36 | this.snackBar.open(res.message, 'close', { 37 | duration: 5000, 38 | horizontalPosition: 'right', 39 | verticalPosition: 'top', 40 | panelClass: 'success-message', 41 | }); 42 | }, 43 | err => { 44 | this.snackBar.open(err.message, 'close', { 45 | duration: 5000, 46 | horizontalPosition: 'right', 47 | verticalPosition: 'top', 48 | panelClass: 'error-message', 49 | }); 50 | }); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/backend/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | 5 | ##### user 6 | user: 7 | image: node:erbium 8 | container_name: user 9 | working_dir: /home/api/ 10 | command: node_modules/.bin/nest start user --debug --watch 11 | depends_on: 12 | - api-postgres 13 | ports: 14 | - 8001:9229 # node inspect 15 | env_file: 16 | - environments/common.env 17 | - environments/user.env 18 | volumes: 19 | - ..:/home/api 20 | 21 | ##### auth 22 | auth: 23 | image: node:erbium 24 | container_name: auth 25 | working_dir: /home/api/ 26 | command: node_modules/.bin/nest start auth --debug --watch 27 | depends_on: 28 | - user 29 | ports: 30 | - 8002:9229 # node inspect 31 | env_file: 32 | - environments/common.env 33 | - environments/auth.env 34 | volumes: 35 | - ..:/home/api 36 | 37 | ##### chat 38 | chat: 39 | image: node:erbium 40 | container_name: chat 41 | working_dir: /home/api/ 42 | command: node_modules/.bin/nest start chat --debug --watch 43 | depends_on: 44 | - user 45 | ports: 46 | - 8003:9229 # node inspect 47 | env_file: 48 | - environments/common.env 49 | - environments/chat.env 50 | volumes: 51 | - ..:/home/api 52 | 53 | #### api postgres 54 | api-postgres: 55 | container_name: api-postgres 56 | image: postgres:11 57 | restart: always 58 | ports: # for debug puproses, remove in the production 59 | - 5432:5432 60 | environment: 61 | - POSTGRES_USER=postgres 62 | - POSTGRES_PASSWORD=postgres 63 | - POSTGRES_MULTIPLE_DATABASES=user,chat 64 | - PGPORT=5432 65 | volumes: 66 | - ./postgres/scripts:/docker-entrypoint-initdb.d 67 | # - ./postgres/data:/var/lib/postgresql/data 68 | 69 | #### envoy-api 70 | envoy-api-dev: 71 | image: envoyproxy/envoy-alpine:v1.12.3 72 | container_name: envoy-api-dev 73 | working_dir: /home/envoy 74 | command: /usr/local/bin/envoy -c /home/envoy/envoy.json 75 | ports: 76 | - 443:443 77 | volumes: 78 | - ./envoy:/home/envoy 79 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/user/auth/auth.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { MatSnackBar } from '@angular/material/snack-bar'; 5 | import { NGXLogger } from 'ngx-logger'; 6 | 7 | import { AuthGrpcService } from '@grpc/services/auth/auth.service'; 8 | import { AuthService } from '@share/services/auth.service'; 9 | 10 | @Component({ 11 | selector: 'app-auth', 12 | templateUrl: './auth.component.html', 13 | styleUrls: ['./auth.component.scss'], 14 | changeDetection: ChangeDetectionStrategy.OnPush, 15 | }) 16 | export class AuthComponent implements OnInit { 17 | 18 | public form: FormGroup = this.fb.group({ 19 | email: [null, [Validators.required, Validators.email]], 20 | password: [null, [Validators.required, Validators.minLength(4)]], 21 | }); 22 | 23 | constructor( 24 | private fb: FormBuilder, 25 | private router: Router, 26 | private snackBar: MatSnackBar, 27 | private logger: NGXLogger, 28 | private authGrpcService: AuthGrpcService, 29 | private authService: AuthService, 30 | ) { 31 | } 32 | 33 | ngOnInit() { 34 | } 35 | 36 | public onSubmit(): void { 37 | if (this.form.valid) { 38 | this.authGrpcService.auth(this.form.value) 39 | .subscribe( 40 | res => { 41 | this.authService.loggedIn(res.token); 42 | this.form.reset(); 43 | this.router.navigateByUrl('/chat'); 44 | }, 45 | err => { 46 | const message = err.code === 13 ? 'User not found' : err.message; 47 | 48 | this.snackBar.open(message, 'close', { 49 | duration: 5000, 50 | horizontalPosition: 'right', 51 | verticalPosition: 'top', 52 | panelClass: 'error-message', 53 | }); 54 | }); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/frontend/src/app/share/services/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AuthService } from './auth.service'; 3 | import { RouterModule, Router } from '@angular/router'; 4 | import { ShareModule } from '@share/share.module'; 5 | import { MatDialogModule } from '@angular/material/dialog'; 6 | 7 | describe('AuthService', () => { 8 | let service: AuthService; 9 | 10 | const router = { 11 | navigateByUrl: () => { 12 | }, 13 | }; 14 | 15 | beforeEach(() => { 16 | TestBed.configureTestingModule({ 17 | imports: [RouterModule, ShareModule, MatDialogModule], 18 | providers: [{ 19 | provide: Router, 20 | useValue: router, 21 | }], 22 | }); 23 | service = TestBed.get(AuthService); 24 | }); 25 | 26 | it('should be created', () => { 27 | expect(service).toBeTruthy(); 28 | }); 29 | 30 | 31 | describe('login user', () => { 32 | beforeEach(() => { 33 | spyOn(service, 'getToken').and.returnValue('eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJiNjg3ZmMxLTdmNmYt' + 34 | 'NGEzMS04MzNjLWQxNWJmYWEzMDlmMiIsImlhdCI6MTU1ODAxODYyOSwiZXhwIjoxNTU4MDE5MjI5fQ.R3sRT' + 35 | 'yvQ7Tvg-IFQU2GIfc6FDzu-VvpflGXIMGQpp9T6oCJqeMwIEvPCj7OEL3w865pr87kYYupXfkSW2-w9ghkAME' + 36 | 'vwwd_FXreLVQUftWMr8mmFE_esJfxtjh97PXybxeQVqRu-2kmXrPpZIMd0oHQYWGS4bz6XdSpyLZGeMHe-jBDix' + 37 | 'nRXMBjIfxZCQfrO5rD_YYi-BjvbB-kRVZqEzMwGqdDtkFEVSNqF-f0h5v9po-jQoaZZEHaa5wpCEo5ZHdZC9JQ7' + '' + 38 | 'HeZN9MONKank54SWpdxPsWL4Fmg2nGQlI7v6JYBk4y93ATvBTae_IC2JHvClO6f0e3poDaYgJ_QBIg'); 39 | 40 | service.updateToken(); 41 | }); 42 | 43 | it('logout()', () => { 44 | spyOn(service.router, 'navigateByUrl'); 45 | service.logout(); 46 | expect(service.router.navigateByUrl).toHaveBeenCalledWith('/login'); 47 | }); 48 | 49 | it('logout(false)', () => { 50 | spyOn(service.router, 'navigateByUrl'); 51 | service.logout(false); 52 | expect(service.router.navigateByUrl).not.toHaveBeenCalled(); 53 | }); 54 | }); 55 | 56 | }); 57 | -------------------------------------------------------------------------------- /packages/frontend/src/app/grpc/services/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Metadata } from 'grpc-web'; 4 | 5 | import { grpcUnary } from '@grpc/helpers/grpc-unary'; 6 | import { grpcJwtMetadata } from '@grpc/helpers/grpc-metadata'; 7 | 8 | import { UserServicePromiseClient } from '@grpc/proto/user/user_grpc_web_pb'; 9 | import { CreateUserReq, UserRes, UserReq, UsersRes, UpdateUserReq } from '@grpc/proto/user/user_pb'; 10 | import { User, Stub } from '@grpc/proto/user/user.types_pb'; 11 | 12 | @Injectable({ 13 | providedIn: 'root', 14 | }) 15 | export class UserGrpcService { 16 | 17 | constructor(private client: UserServicePromiseClient) { 18 | } 19 | 20 | public createUser(data: CreateUserReq.AsObject): Observable { 21 | const req = new CreateUserReq(); 22 | 23 | req.setName(data.name); 24 | req.setEmail(data.email); 25 | req.setAvatar(data.avatar); 26 | req.setPassword(data.password); 27 | 28 | return grpcUnary(this.client.createUser(req)); 29 | } 30 | 31 | public updateUser(data: UpdateUserReq.AsObject): Observable { 32 | const req = new UpdateUserReq(); 33 | const meta: Metadata = grpcJwtMetadata(); 34 | 35 | req.setName(data.name); 36 | req.setEmail(data.email); 37 | req.setAvatar(data.avatar); 38 | 39 | return grpcUnary(this.client.updateUser(req, meta)); 40 | } 41 | 42 | public deleteUser(data: UserReq.AsObject): Observable { 43 | const req = new UserReq(); 44 | 45 | req.setId(data.id); 46 | 47 | return grpcUnary(this.client.deleteUser(req)); 48 | } 49 | 50 | public getUser(data: UserReq.AsObject): Observable { 51 | const req = new UserReq(); 52 | 53 | req.setId(data.id); 54 | 55 | return grpcUnary(this.client.getUser(req)); 56 | } 57 | 58 | public getUsersById(): Observable { 59 | const req = new Stub(); 60 | const meta: Metadata = grpcJwtMetadata(); 61 | 62 | return grpcUnary(this.client.getUsersAll(req, meta)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/services/dal/db/migrations/sqls/20191201192257-InsertDemoMessagesMigration-up.sql: -------------------------------------------------------------------------------- 1 | insert into api_message (id, author, message, "createdAt", "updatedAt") values ('b231aa7f-68dd-483f-9110-50590f216b57', '{"id": "bb6d88c8-d705-4c10-b1a1-08ea1b94d4cc", "name": "John Doe", "avatar": "assets/avatar/avatar-2.png"}', 'Hi! :) 2 | ', '2019-11-30 18:21:36.537860', '2019-11-30 18:21:36.537860'); 3 | 4 | insert into api_message (id, author, message, "createdAt", "updatedAt") values ('daeb5002-686e-4f35-b853-103e472740f4', '{"id": "34e9e3e5-56e9-4e71-9b3c-13292915970b", "name": "Anna Smith", "avatar": "assets/avatar/avatar-1.png"}', 'Hi John! *)', '2019-11-30 18:21:53.334637', '2019-11-30 18:21:53.334637'); 5 | 6 | insert into api_message (id, author, message, "createdAt", "updatedAt") values ('625494b7-ada5-4551-91bd-33c1ffd66e32', '{"id": "bb6d88c8-d705-4c10-b1a1-08ea1b94d4cc", "name": "John Doe", "avatar": "assets/avatar/avatar-2.png"}', 'How are you?', '2019-11-30 18:22:08.184094', '2019-11-30 18:22:08.184094'); 7 | 8 | insert into api_message (id, author, message, "createdAt", "updatedAt") values ('860ac77e-4090-4a67-a28d-f891b83a0556', '{"id": "bb6d88c8-d705-4c10-b1a1-08ea1b94d4cc", "name": "John Doe", "avatar": "assets/avatar/avatar-2.png"}', 'What are your plans for the evening?', '2019-11-30 18:22:44.322058', '2019-11-30 18:22:44.322058'); 9 | 10 | insert into api_message (id, author, message, "createdAt", "updatedAt") values ('56829dd3-edf5-4537-a9c7-a00bdd619747', '{"id": "34e9e3e5-56e9-4e71-9b3c-13292915970b", "name": "Anna Smith", "avatar": "assets/avatar/avatar-1.png"}', 'I''m fine 11 | ', '2019-11-30 18:23:06.294880', '2019-11-30 18:23:06.294880'); 12 | 13 | insert into api_message (id, author, message, "createdAt", "updatedAt") values ('fe95451d-dedd-492b-b9ce-8d1c8b881bd1', '{"id": "34e9e3e5-56e9-4e71-9b3c-13292915970b", "name": "Anna Smith", "avatar": "assets/avatar/avatar-1.png"}', 'I''m going to go to the movies. Come with me :)', '2019-11-30 18:24:08.900434', '2019-11-30 18:24:08.900434'); 14 | 15 | insert into api_message (id, author, message, "createdAt", "updatedAt") values ('c4b5fc4a-28e6-4e18-a0ff-4bb4e1bd590d', '{"id": "bb6d88c8-d705-4c10-b1a1-08ea1b94d4cc", "name": "John Doe", "avatar": "assets/avatar/avatar-2.png"}', 'Sure! Thanks', '2019-11-30 18:25:42.860062', '2019-11-30 18:25:42.860062'); 16 | -------------------------------------------------------------------------------- /packages/frontend/src/styles.scss: -------------------------------------------------------------------------------- 1 | @import "~@angular/material/prebuilt-themes/indigo-pink.css"; 2 | 3 | * { 4 | font-family: 'Roboto', sans-serif; 5 | } 6 | 7 | html, body { 8 | margin: 0; 9 | padding: 0; 10 | background: #282828; 11 | color: #ededed; 12 | } 13 | 14 | .chat-glob { 15 | .subtitle { 16 | font-weight: 400; 17 | font-size: 20px; 18 | margin: 16px; 19 | } 20 | 21 | .mat-card { 22 | background: #00564d; 23 | color: #fefefe; 24 | } 25 | 26 | .mat-card-header-text { 27 | margin: 0 .4rem; 28 | } 29 | 30 | .form { 31 | width: 400px; 32 | } 33 | 34 | .form-field { 35 | width: 100%; 36 | margin-bottom: 8px; 37 | 38 | textarea { 39 | height: 120px; 40 | } 41 | 42 | .mat-form-field-ripple { 43 | background-color: #00897b; 44 | } 45 | 46 | .mat-form-field-label { 47 | color: #00897b; 48 | } 49 | 50 | &.mat-focused { 51 | .mat-form-field-label { 52 | color: #00897b; 53 | } 54 | 55 | .mat-form-field-ripple { 56 | background-color: #00897b; 57 | } 58 | } 59 | 60 | &.mat-form-field-invalid { 61 | .mat-form-field-label { 62 | color: #f44336; 63 | } 64 | 65 | .mat-form-field-ripple { 66 | background-color: #f44336; 67 | } 68 | } 69 | } 70 | 71 | .mat-input-element { 72 | caret-color: #00897b; 73 | } 74 | 75 | .mat-form-field-appearance-legacy .mat-form-field-underline { 76 | background-color: #00897b; 77 | } 78 | 79 | .form-field-select { 80 | width: calc(50% - 8px); 81 | 82 | &.left { 83 | margin-right: 16px; 84 | } 85 | } 86 | 87 | .form-actions { 88 | text-align: right; 89 | } 90 | } 91 | 92 | .error-message { 93 | color: #f9284d; 94 | 95 | .mat-simple-snackbar-action { 96 | color: #fff; 97 | } 98 | } 99 | 100 | .success-message { 101 | color: #00897b; 102 | 103 | .mat-simple-snackbar-action { 104 | color: #fff; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /packages/backend/libs/lib/src/logger/message/MessageBuilder.ts: -------------------------------------------------------------------------------- 1 | import { LogLevelType } from '../constants'; 2 | import { colorizeTimestamp, colorizeLevel, colorizeLabel, colorizeMessage } from './colorizers'; 3 | import { padStart, padEnd } from '../format'; 4 | 5 | const DELIMITERS = { 6 | date: '-', 7 | time: ':', 8 | logMessage: ' ', 9 | fullMessage: ' :: ', 10 | }; 11 | 12 | export class MessageBuilder { 13 | private readonly colorizeMessages = process.env.LOGGER_COLORIZE_MESSAGES === 'true'; 14 | 15 | constructor(private readonly label: string) { 16 | } 17 | 18 | public build(level: LogLevelType, args: any[]): string { 19 | const timestamp = this.getTimestamp(); 20 | const logMessage = this.prepareMessageFromArgs(args); 21 | 22 | if (!this.colorizeMessages) { 23 | return [timestamp, level, this.label, logMessage].join(DELIMITERS.fullMessage); 24 | } 25 | 26 | return [ 27 | colorizeTimestamp(timestamp), 28 | colorizeLevel(level), 29 | colorizeLabel(this.label), 30 | colorizeMessage(level, logMessage), 31 | ].join(DELIMITERS.fullMessage); 32 | } 33 | 34 | private getTimestamp(): string { 35 | const date = new Date(); 36 | const logDate = [padStart(date.getDate()), padStart(date.getMonth() + 1), date.getFullYear()].join(DELIMITERS.date); 37 | const logTime = [padStart(date.getHours()), padStart(date.getMinutes()), padEnd(date.getMilliseconds())].join(DELIMITERS.time); 38 | 39 | return `[${logDate} ${logTime}]`; 40 | } 41 | 42 | private prepareMessageFromArgs(args: any[]): string { 43 | return args 44 | .map(it => { 45 | const type = typeof it; 46 | 47 | // no need to prepare undefined, null, string & number types 48 | if (['number', 'string', 'undefined'].includes(type) || it === null) { 49 | return it; 50 | } 51 | 52 | // try add stack or message from Error 53 | if (it instanceof Error) { 54 | return `${it.stack || it.message || it}`; 55 | } 56 | 57 | // stringify other types, such as array, object 58 | return `${JSON.stringify(it, null, 2)}`; 59 | }) 60 | .join(DELIMITERS.logMessage); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /devtools/build-grpc-back.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/node 2 | 3 | const cp = require('child_process'); 4 | const path = require('path'); 5 | const fs = require('fs-extra'); 6 | const rimraf = require('rimraf'); 7 | const root = require('app-root-path'); 8 | const helpers = require('./helpers'); 9 | 10 | const npmBinPath = cp.execSync('npm bin', {cwd: process.cwd()}) 11 | .toString() 12 | .replace(/\n/, ''); 13 | 14 | const PBJS = npmBinPath + '/pbjs --no-create --no-encode --no-decode --no-verify --no-convert' + 15 | ' --no-delimited --no-beautify'; 16 | const PBTS = npmBinPath + '/pbts --no-comments'; 17 | 18 | const sedPath = root.resolve('devtools/build-grpc-back.sed'); 19 | const grpcDir = root.resolve('grpc-proto/.tmp'); 20 | const grpcBackDir = root.resolve('packages/backend/libs/grpc-proto'); 21 | 22 | const IGNORE_PACKAGES = [ 23 | // ... 24 | ]; 25 | 26 | const IGNORE_PROTO_FILES = ['**/index.proto']; 27 | 28 | // remove old 29 | rimraf.sync(grpcDir); 30 | rimraf.sync(grpcBackDir); 31 | 32 | // **** build grpc-web 33 | helpers.getPackages(IGNORE_PACKAGES, root.resolve('grpc-proto')).forEach(package => { 34 | const protos = helpers.getProtosListPath(IGNORE_PROTO_FILES, package); 35 | const pkgName = path.basename(package); 36 | const pkgPath = `${grpcDir}/${pkgName}`; 37 | 38 | protos.forEach(proto => { 39 | const protoName = path.basename(proto, '.proto'); 40 | const file = `${pkgPath}/${protoName}`; 41 | const cmd = `${PBJS} -t static ${proto} | ${PBTS} -o ${file}.d.ts - && sed -i -f ${sedPath} ${file}.d.ts`; 42 | 43 | try { 44 | console.log(`Build grpc-web for '${protoName}'.proto`); 45 | fs.ensureDirSync(pkgPath); 46 | 47 | if (path.extname(protoName) === '.enum') { 48 | cp.execSync(`${PBJS} --no-comments -t static-module ${proto} -o ${file}.js`, 49 | {cwd: root.path, stdio: 'inherit'}); 50 | } 51 | 52 | cp.execSync(cmd, {cwd: root.path, stdio: 'inherit'}); 53 | } catch (err) { 54 | process.exit(err.status); 55 | } 56 | }); 57 | }); 58 | 59 | fs.copySync(root.resolve('grpc-proto'), grpcBackDir); 60 | 61 | // copy typings and remove js and web typings 62 | fs.copySync(`${grpcBackDir}/.tmp`, grpcBackDir); 63 | rimraf.sync(`${grpcBackDir}/.tmp`); 64 | rimraf.sync(`${grpcBackDir}/!**/!*_grpc_web_*`); 65 | rimraf.sync(grpcDir); 66 | 67 | process.exit(0); 68 | -------------------------------------------------------------------------------- /packages/frontend/src/app/share/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { NGXLogger } from 'ngx-logger'; 4 | import { Observable, ReplaySubject, interval, Subject } from 'rxjs'; 5 | import { switchMap, takeUntil } from 'rxjs/operators'; 6 | 7 | import { environment } from '@environments/environment'; 8 | import { AuthGrpcService } from '@grpc/services/auth/auth.service'; 9 | import { AuthRes } from '@grpc/proto/auth/auth_pb'; 10 | 11 | @Injectable({ 12 | providedIn: 'root', 13 | }) 14 | export class AuthService { 15 | 16 | private ngOnDestroy$ = new Subject(); 17 | private loggedInSubject$ = new ReplaySubject(1); 18 | 19 | constructor( 20 | private router: Router, 21 | private logger: NGXLogger, 22 | private authGrpcService: AuthGrpcService, 23 | ) { 24 | } 25 | 26 | public updateToken(): void { 27 | const getToken = this.getToken().split('.')[1]; 28 | const jwt = JSON.parse(atob(getToken)); 29 | const now = Date.now() / 1000; 30 | const period = Math.ceil(jwt.exp - now - environment.authDiff) * 1000; 31 | 32 | interval(period) 33 | .pipe( 34 | switchMap(() => this.authGrpcService.updateAuth()), 35 | takeUntil(this.ngOnDestroy$), 36 | ) 37 | .subscribe( 38 | res => localStorage.setItem(environment.token, res.token), 39 | err => this.logger.warn(err.message)); 40 | } 41 | 42 | public updateAuth(): Observable { 43 | if (this.getToken()) { 44 | return this.authGrpcService.updateAuth(); 45 | } else { 46 | this.logout(); 47 | } 48 | } 49 | 50 | public isLoggedIn(): Observable { 51 | return this.loggedInSubject$.asObservable(); 52 | } 53 | 54 | public loggedIn(token: string): void { 55 | localStorage.setItem(environment.token, token); 56 | this.loggedInSubject$.next(true); 57 | this.updateToken(); 58 | } 59 | 60 | public getToken(): string { 61 | return localStorage.getItem(environment.token); 62 | } 63 | 64 | public logout(): void { 65 | localStorage.removeItem(environment.token); 66 | this.loggedInSubject$.next(false); 67 | this.ngOnDestroy$.next(); 68 | this.router.navigateByUrl('/auth'); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/user/register/register.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { MatSnackBar } from '@angular/material/snack-bar'; 5 | import { NGXLogger } from 'ngx-logger'; 6 | import { switchMap } from 'rxjs/operators'; 7 | 8 | import { AuthGrpcService } from '@grpc/services/auth/auth.service'; 9 | import { UserGrpcService } from '@grpc/services/user/user.service'; 10 | import { AuthService } from '@share/services/auth.service'; 11 | 12 | @Component({ 13 | selector: 'app-register', 14 | templateUrl: './register.component.html', 15 | styleUrls: ['./register.component.scss'], 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | }) 18 | export class RegisterComponent implements OnInit { 19 | 20 | public form: FormGroup = this.fb.group({ 21 | name: [null, [Validators.required]], 22 | email: [null, [Validators.required, Validators.email]], 23 | avatar: [null], 24 | password: [null, [Validators.required, Validators.minLength(4)]], 25 | }); 26 | 27 | constructor( 28 | private fb: FormBuilder, 29 | private router: Router, 30 | private snackBar: MatSnackBar, 31 | private logger: NGXLogger, 32 | private userGrpcService: UserGrpcService, 33 | private authGrpcService: AuthGrpcService, 34 | private authService: AuthService, 35 | ) { 36 | } 37 | 38 | ngOnInit() { 39 | } 40 | 41 | public onSubmit(): void { 42 | if (this.form.valid) { 43 | this.userGrpcService.createUser(this.form.value) 44 | .pipe( 45 | switchMap(() => this.authGrpcService.auth(this.form.value))) 46 | .subscribe( 47 | res => { 48 | this.authService.loggedIn(res.token); 49 | this.form.reset(); 50 | this.router.navigateByUrl('/dashboard'); 51 | }, 52 | err => { 53 | this.snackBar.open(err.message, 'close', { 54 | duration: 5000, 55 | horizontalPosition: 'right', 56 | verticalPosition: 'top', 57 | panelClass: 'error-message', 58 | }); 59 | }); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/api/message/MessageService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleInit } from '@nestjs/common'; 2 | import { Client, ClientGrpc } from '@nestjs/microservices'; 3 | import { Observable } from 'rxjs'; 4 | import { mapTo, switchMap, tap } from 'rxjs/operators'; 5 | 6 | import { grpcUser } from '@lib/utils/GrpcConfigs'; 7 | 8 | import { api as chatApi } from '@grpc-proto/chat/message'; 9 | import { api as userApi } from '@grpc-proto/user/user'; 10 | 11 | import { MessageDataProducer } from '@chat/services/dal/data-producers/MessageDataProducer'; 12 | import { MessageDataRemover } from '@chat/services/dal/data-removers/MessageDataRemover'; 13 | import { MessageDataUpdater } from '@chat/services/dal/data-updaters/MessageDataUpdater'; 14 | import { ChatEventService } from '@chat/services/ChatEventService'; 15 | 16 | @Injectable() 17 | export class MessageService implements OnModuleInit { 18 | 19 | @Client(grpcUser) private readonly grpcUserClient: ClientGrpc; 20 | private grpcUserService: userApi.user.UserService; 21 | 22 | constructor( 23 | private readonly messageDataProducer: MessageDataProducer, 24 | private readonly messageDataUpdater: MessageDataUpdater, 25 | private readonly messageDataRemover: MessageDataRemover, 26 | private readonly chatEventService: ChatEventService, 27 | ) { 28 | } 29 | 30 | onModuleInit() { 31 | this.grpcUserService = this.grpcUserClient.getService('UserService'); 32 | } 33 | 34 | public sendMessage(data: chatApi.chat.SendMessageReq, userId: string): Observable { 35 | return this.grpcUserService.getUser({id: userId}) 36 | .pipe( 37 | switchMap(user => this.messageDataProducer.sendMessage({ 38 | message: data.message, 39 | author: { 40 | id: user.id, 41 | name: user.name, 42 | avatar: user.avatar, 43 | }, 44 | })), 45 | tap(res => this.chatEventService.emit(res)), 46 | mapTo(null), 47 | ); 48 | } 49 | 50 | public editMessage(data: chatApi.chat.EditMessageReq): Observable { 51 | return this.messageDataUpdater.updateMessage(data) 52 | .pipe(mapTo(null)); 53 | } 54 | 55 | public deleteMessage(id: string): Observable { 56 | return this.messageDataRemover.deleteMessage(id) 57 | .pipe(mapTo(null)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/backend/apps/chat/src/api/message/MessageController.ts: -------------------------------------------------------------------------------- 1 | import { Controller, UseGuards, UseFilters } from '@nestjs/common'; 2 | import { GrpcMethod } from '@nestjs/microservices'; 3 | import { Observable } from 'rxjs'; 4 | import { map } from 'rxjs/internal/operators'; 5 | 6 | import { JwtGuard } from '@lib/jwt/JwtGuard'; 7 | import { IJwtMeta } from '@lib/jwt/JwtInterface'; 8 | import { RpcExceptionFilter } from '@lib/exceptions'; 9 | 10 | import { api as chatEnum } from '@grpc-proto/chat/chat.enum'; 11 | import { api as chatApi } from '@grpc-proto/chat/chat'; 12 | 13 | import { MessageService } from './MessageService'; 14 | 15 | import { AddMessageReqDTO } from './dto/AddMessageReqDTO'; 16 | import { EditMessageReqDTO } from './dto/EditMessageReqDTO'; 17 | import { DeleteMessageReqDTO } from './dto/DeleteMessageReqDTO'; 18 | 19 | @Controller() 20 | export class MessageController { 21 | 22 | constructor(private readonly messageService: MessageService) { 23 | } 24 | 25 | @UseGuards(JwtGuard) 26 | @GrpcMethod('MessageService', 'SendMessage') 27 | @UseFilters(RpcExceptionFilter.for('MessageService::sendMessage')) 28 | public sendMessage(data: AddMessageReqDTO, meta: IJwtMeta<{ id: string; }>): Observable { 29 | return this.messageService.sendMessage(data, meta.payload.id).pipe( 30 | map(res => { 31 | return { 32 | status: chatEnum.chat.EStatus.SUCCESS, 33 | message: `Message created successfully`, 34 | }; 35 | }), 36 | ); 37 | } 38 | 39 | @UseGuards(JwtGuard) 40 | @GrpcMethod('MessageService', 'EditMessage') 41 | @UseFilters(RpcExceptionFilter.for('MessageService::editMessage')) 42 | public editMessage(data: EditMessageReqDTO): Observable { 43 | return this.messageService.editMessage(data).pipe( 44 | map(() => { 45 | return { 46 | status: chatEnum.chat.EStatus.SUCCESS, 47 | message: `Messages update successfully`, 48 | }; 49 | }), 50 | ); 51 | } 52 | 53 | @UseGuards(JwtGuard) 54 | @GrpcMethod('MessageService', 'DeleteMessage') 55 | @UseFilters(RpcExceptionFilter.for('MessageService::deleteMessage')) 56 | public deleteMessage(data: DeleteMessageReqDTO): Observable { 57 | return this.messageService.deleteMessage(data.id).pipe( 58 | map(() => { 59 | return { 60 | status: chatEnum.chat.EStatus.SUCCESS, 61 | message: `Message delete successfully`, 62 | }; 63 | }), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/frontend/src/app/modules/user/settings/settings.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { MatSnackBar } from '@angular/material'; 5 | import { NGXLogger } from 'ngx-logger'; 6 | 7 | import { getUserIdFromJWT } from '@grpc/helpers/grpc-get-id'; 8 | import { User } from '@grpc/proto/user/user.types_pb'; 9 | import { UserGrpcService } from '@grpc/services/user/user.service'; 10 | import { AuthService } from '@share/services/auth.service'; 11 | 12 | @Component({ 13 | selector: 'app-settings', 14 | templateUrl: './settings.component.html', 15 | styleUrls: ['./settings.component.scss'], 16 | changeDetection: ChangeDetectionStrategy.OnPush, 17 | }) 18 | export class SettingsComponent implements OnInit { 19 | 20 | private user: User.AsObject; 21 | 22 | public form: FormGroup = this.fb.group({ 23 | name: [null, [Validators.required]], 24 | email: [null, [Validators.required, Validators.email]], 25 | avatar: [null], 26 | }); 27 | 28 | constructor( 29 | private fb: FormBuilder, 30 | private router: Router, 31 | private snackBar: MatSnackBar, 32 | private logger: NGXLogger, 33 | private userGrpcService: UserGrpcService, 34 | private authService: AuthService, 35 | ) { 36 | const token = this.authService.getToken(); 37 | const id = getUserIdFromJWT(token); 38 | 39 | this.userGrpcService.getUser({ id }) 40 | .subscribe(user => { 41 | this.user = user; 42 | this.form.get('name').setValue(user.name); 43 | this.form.get('email').setValue(user.email); 44 | this.form.get('avatar').setValue(user.avatar); 45 | }); 46 | } 47 | 48 | ngOnInit() { 49 | } 50 | 51 | private updateUser(): void { 52 | 53 | } 54 | 55 | public onSubmit(): void { 56 | if (this.form.valid) { 57 | this.userGrpcService.updateUser(this.form.value) 58 | .subscribe( 59 | res => { 60 | this.updateUser(); 61 | this.logger.debug(res); 62 | }, 63 | err => { 64 | this.snackBar.open(err.message, 'close', { 65 | duration: 5000, 66 | horizontalPosition: 'right', 67 | verticalPosition: 'top', 68 | panelClass: 'error-message', 69 | }); 70 | }); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/backend/apps/user/src/services/dal/data-finders/UserDataFinder.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Client } from 'pg'; 3 | import { createHmac } from 'crypto'; 4 | import { from, Observable } from 'rxjs'; 5 | import { map } from 'rxjs/operators'; 6 | 7 | import { NotFoundException, USER_NOT_FOUND } from '@lib/exceptions'; 8 | 9 | import { api as userTypes } from '@grpc-proto/user/user.types'; 10 | import { api as userApi } from '@grpc-proto/user/user'; 11 | 12 | import { SALT } from '@user/env'; 13 | 14 | @Injectable() 15 | export class UserDataFinder { 16 | 17 | constructor(private readonly db: Client) { 18 | } 19 | 20 | private getConditionQuery(data: userApi.user.VerifyUserReq): string { 21 | if (data.password) { 22 | data.password = createHmac('sha512', SALT).update(data.password).digest('hex'); 23 | } 24 | 25 | const keys = Object.keys(data); 26 | const conditions = keys.map((key, index) => { 27 | const and = keys.length > 1 && index < keys.length - 1 ? ' and ' : ''; 28 | return `${key}='${data[key]}'${and}`; 29 | }).join(''); 30 | 31 | return `select * from api_user where ${conditions}`; 32 | } 33 | 34 | public getUserOne(id: string): Observable { 35 | const query = `select * from api_user where id = $1`; 36 | 37 | return from(this.db.query(query, [id])) 38 | .pipe( 39 | map(res => { 40 | if (!res.rowCount) { 41 | throw new NotFoundException(USER_NOT_FOUND); 42 | } 43 | 44 | return res.rows[0]; 45 | }), 46 | ); 47 | } 48 | 49 | public getUserByConditions(data: userApi.user.VerifyUserReq): Observable { 50 | const query = this.getConditionQuery(data); 51 | 52 | return from(this.db.query(query)) 53 | .pipe(map(res => res.rows[0])); 54 | } 55 | 56 | public getUsersByIds(ids: string[]): Observable { 57 | let query = `select * from api_user where id in (`; 58 | ids.forEach((id, index) => { 59 | const end = index === ids.length - 1 ? `)` : `,`; 60 | query += `'${id}'${end}`; 61 | }); 62 | 63 | return from(this.db.query(query)) 64 | .pipe(map(res => res.rows)); 65 | } 66 | 67 | public getUsersAll(): Observable { 68 | const query = `select * from api_user`; 69 | 70 | return from(this.db.query(query)) 71 | .pipe(map(res => res.rows)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/frontend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "array-type": false, 8 | "arrow-parens": false, 9 | "deprecation": { 10 | "severity": "warn" 11 | }, 12 | "import-blacklist": [ 13 | true, 14 | "rxjs/Rx" 15 | ], 16 | "interface-name": false, 17 | "max-classes-per-file": false, 18 | "max-line-length": [ 19 | true, 20 | 140 21 | ], 22 | "indent": [ 23 | true, 24 | "spaces", 25 | 4 26 | ], 27 | "linebreak-style": [ 28 | true, 29 | "LF" 30 | ], 31 | "member-access": false, 32 | "member-ordering": [ 33 | true, 34 | { 35 | "order": [ 36 | "static-field", 37 | "instance-field", 38 | "static-method", 39 | "instance-method" 40 | ] 41 | } 42 | ], 43 | "no-consecutive-blank-lines": false, 44 | "no-console": [ 45 | true, 46 | "debug", 47 | "info", 48 | "time", 49 | "timeEnd", 50 | "trace" 51 | ], 52 | "no-empty": false, 53 | "no-inferrable-types": [ 54 | true, 55 | "ignore-params" 56 | ], 57 | "no-non-null-assertion": true, 58 | "no-redundant-jsdoc": true, 59 | "no-switch-case-fall-through": true, 60 | "no-use-before-declare": true, 61 | "no-var-requires": false, 62 | "object-literal-key-quotes": [ 63 | true, 64 | "as-needed" 65 | ], 66 | "object-literal-sort-keys": false, 67 | "ordered-imports": false, 68 | "quotemark": [ 69 | true, 70 | "single" 71 | ], 72 | "semicolon": [ 73 | true, 74 | "always" 75 | ], 76 | "trailing-comma": [true, { 77 | "multiline": "always", 78 | "singleline": "never" 79 | }], 80 | "no-output-on-prefix": true, 81 | "no-inputs-metadata-property": true, 82 | "no-outputs-metadata-property": true, 83 | "no-host-metadata-property": true, 84 | "no-input-rename": true, 85 | "no-output-rename": true, 86 | "use-lifecycle-interface": true, 87 | "use-pipe-transform-interface": true, 88 | "component-class-suffix": true, 89 | "directive-class-suffix": true 90 | }, 91 | "linterOptions": { 92 | "exclude": [ 93 | "/src/app/grpc/proto/**/*" 94 | ] 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | All important links for Как подружить Nest.js и Angular при помощи gRPC 5 | 6 | 50 | 51 | 52 |

All important links

53 | 69 | 70 |

Contacts

71 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /packages/frontend/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | --------------------------------------------------------------------------------