├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── README.notes.md ├── README.secret.md ├── docker-compose.yml ├── nest-cli.json ├── package-lock.json ├── package.json ├── src ├── agency │ ├── agency.controller.spec.ts │ ├── agency.controller.ts │ ├── agency.module.ts │ ├── agency.service.spec.ts │ ├── agency.service.ts │ ├── dto │ │ ├── create-agency.dto.ts │ │ ├── invite-agency.dto.ts │ │ └── update-agency.dto.ts │ ├── entities │ │ └── agency.entity.ts │ └── enums │ │ └── agency.enum.ts ├── app.controller.spec.ts ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── coffees │ ├── coffees.controller.spec.ts │ ├── coffees.controller.ts │ ├── coffees.module.ts │ ├── coffees.permission.ts │ ├── coffees.service.spec.ts │ ├── coffees.service.ts │ ├── dto │ │ ├── create-coffee.dto.ts │ │ ├── refresh-token.dto.ts │ │ └── update-coffee.dto.ts │ └── entities │ │ └── coffee.entity.ts ├── email │ ├── email.controller.spec.ts │ ├── email.controller.ts │ ├── email.module.ts │ ├── email.service.spec.ts │ └── email.service.ts ├── iam │ ├── authentication │ │ ├── api-key.service.spec.ts │ │ ├── api-key.service.ts │ │ ├── authentication.controller.spec.ts │ │ ├── authentication.controller.ts │ │ ├── authentication.service.spec.ts │ │ ├── authentication.service.ts │ │ ├── decorators │ │ │ └── auth.decorator.ts │ │ ├── dto │ │ │ ├── google-token.dto.ts │ │ │ ├── sign-in.dto │ │ │ │ └── sign-in.dto.ts │ │ │ └── sign-up.dto │ │ │ │ └── sign-up.dto.ts │ │ ├── enums │ │ │ └── auth-type.enum.ts │ │ ├── guards │ │ │ ├── access-token │ │ │ │ ├── access-token.guard.spec.ts │ │ │ │ └── access-token.guard.ts │ │ │ ├── api-key.guard.ts │ │ │ └── authentication │ │ │ │ ├── authentication.guard.spec.ts │ │ │ │ └── authentication.guard.ts │ │ ├── opt-authentication │ │ │ ├── opt-authentication.service.spec.ts │ │ │ └── opt-authentication.service.ts │ │ ├── refresh-token-ids.storage │ │ │ ├── refresh-token-ids.storage.spec.ts │ │ │ └── refresh-token-ids.storage.ts │ │ └── social │ │ │ ├── google-authentication.controller.spec.ts │ │ │ ├── google-authentication.controller.ts │ │ │ ├── google-authentication.service.spec.ts │ │ │ └── google-authentication.service.ts │ ├── authorization │ │ ├── decorators │ │ │ ├── permissions.decorator.ts │ │ │ ├── policies.decorator.ts │ │ │ └── roles.decorator.ts │ │ ├── guards │ │ │ ├── permissions.guard.spec.ts │ │ │ ├── permissions.guard.ts │ │ │ ├── policies.guard.ts │ │ │ └── roles │ │ │ │ ├── roles.guard.spec.ts │ │ │ │ └── roles.guard.ts │ │ ├── permission.type.ts │ │ └── policies │ │ │ ├── framework-contributor.policy.ts │ │ │ ├── interfaces │ │ │ ├── policy-handler.interfaces.ts │ │ │ └── policy.interface.ts │ │ │ └── policy-handlers.storage.ts │ ├── config │ │ └── jwt.config.ts │ ├── decorators │ │ └── active-user.decorator.ts │ ├── hashing │ │ ├── bcrypt.service.spec.ts │ │ ├── bcrypt.service.ts │ │ ├── hashing.service.spec.ts │ │ └── hashing.service.ts │ ├── iam.constants.ts │ ├── iam.module.ts │ └── interfaces │ │ └── active-user-data.interface.ts ├── incident │ ├── entities │ │ └── incident.entities.ts │ ├── enums │ │ └── incident.enum.ts │ ├── incident.controller.spec.ts │ ├── incident.controller.ts │ ├── incident.module.ts │ ├── incident.service.spec.ts │ └── incident.service.ts ├── main.ts ├── messages │ ├── dto │ │ ├── create-message.dto.ts │ │ └── update-message.dto.ts │ ├── entities │ │ └── message.entity.ts │ ├── enums │ │ └── messages.enum.ts │ ├── messages.controller.spec.ts │ ├── messages.controller.ts │ ├── messages.module.ts │ ├── messages.service.spec.ts │ └── messages.service.ts ├── notification │ ├── dto │ │ ├── get-count.dto.ts │ │ └── update-notifications.dto.ts │ ├── entities │ │ └── notification.entity.ts │ ├── enums │ │ └── notification.enum.ts │ ├── helpers │ │ ├── notification.admin.ts │ │ ├── notification.all.ts │ │ ├── notification.manager.ts │ │ ├── notification.send.ts │ │ └── notification.suser.ts │ ├── notification.controller.spec.ts │ ├── notification.controller.ts │ ├── notification.module.ts │ ├── notification.service.spec.ts │ └── notification.service.ts ├── only-fans │ ├── only-fans.controller.spec.ts │ ├── only-fans.controller.ts │ ├── only-fans.module.ts │ ├── only-fans.service.spec.ts │ └── only-fans.service.ts ├── profile │ ├── dto │ │ └── update-profile.dto.ts │ ├── entities │ │ └── profile.entities.ts │ ├── profile.controller.spec.ts │ ├── profile.controller.ts │ ├── profile.module.ts │ ├── profile.service.spec.ts │ └── profile.service.ts ├── repl.ts ├── schedule │ ├── schedule.controller.spec.ts │ ├── schedule.controller.ts │ ├── schedule.module.ts │ ├── schedule.service.spec.ts │ └── schedule.service.ts ├── scrapper │ ├── dto │ │ ├── create-scrapper.dto.ts │ │ └── update-scrapper.dto.ts │ ├── entities │ │ └── scrapper.entity.ts │ ├── enums │ │ └── scrapper.enum.ts │ ├── scrapper.controller.spec.ts │ ├── scrapper.controller.ts │ ├── scrapper.module.ts │ ├── scrapper.service.spec.ts │ └── scrapper.service.ts ├── telegram │ ├── telegram.controller.spec.ts │ ├── telegram.controller.ts │ ├── telegram.module.ts │ ├── telegram.service.spec.ts │ └── telegram.service.ts └── users │ ├── api-keys │ └── entities │ │ └── api-key.entity │ │ └── api-key.entity.ts │ ├── dto │ ├── create-user.dto.ts │ ├── findAll.dto.ts │ └── update-user.dto.ts │ ├── entities │ └── user.entity.ts │ ├── enums │ ├── role.enum.ts │ └── user.settings.ts │ ├── users.controller.spec.ts │ ├── users.controller.ts │ ├── users.module.ts │ ├── users.service.spec.ts │ └── users.service.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | .env -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /README.notes.md: -------------------------------------------------------------------------------- 1 | ## UseFull Links 2 | 3 | - [Scheduler](https://docs.nestjs.com/techniques/task-scheduling) 4 | - [Puppeteer](https://pptr.dev/) 5 | - [Puppeteer-nest](https://www.npmjs.com/package/nest-puppeteer) 6 | - [redis-ttl](https://github.com/redis/ioredis/blob/HEAD/examples/ttl.js) 7 | - [notes](https://github.com/Barklim/auth-nestjs/blob/main/Readme.notes.md) 8 | - smtp 9 | - [smtp gmail](https://www.youtube.com/watch?v=fN25fMQZ2v0&ab_channel=UlbiTV) 10 | - [smtp sendgrid](https://www.youtube.com/watch?v=MFuqPVCiW7A&ab_channel=NaveenBommidiTechSeeker) 11 | - [smtp google article](https://blog.iamstarcode.com/how-to-send-emails-using-nestjs-nodemailer-smtp-gmail-and-oauth2) 12 | - [slash issue](https://www.youtube.com/watch?v=k-6KFSnaFTU&ab_channel=ProgrammingInBlood) 13 | 14 | ## Commands [link](https://www.youtube.com/watch?v=UN-yK0F38Sg) 15 | 16 | ``` 17 | nest g module onlyFans // Better naming scrapper? 18 | nest g controller onlyFans 19 | nest g service onlyFans 20 | npm i --save puppeteer-core 21 | ``` 22 | 23 | get bright-data api key for connect 24 | 25 | [link](https://www.youtube.com/watch?v=s94lEfpwoPQ) 26 | ``` 27 | npm i puppeteer --save 28 | npm i -D @types/puppeteer 29 | ``` 30 | 31 | [link](https://www.youtube.com/watch?v=URGkzNC-Nwo&list=PLuJJZ-W1NwdqgvE0D-1SMS7EpWIC5cKqu&index=1) 32 | 33 | generate scrapper, messages 34 | 35 | nest g module schedule 36 | nest g service schedule 37 | nest g controller schedule 38 | 39 | nest g module agency 40 | nest g service agency 41 | nest g controller agency 42 | 43 | nest g module notification 44 | nest g service notification 45 | nest g controller notification 46 | 47 | nest g module email 48 | nest g service email 49 | nest g controller email 50 | 51 | [link](https://www.youtube.com/watch?v=COLDiMlmcoI&ab_channel=Letsbuildtogether) 52 | 53 | nest g module telegram 54 | nest g service telegram 55 | nest g controller telegram 56 | 57 | 1. Создается бот в BotFather чате 58 | 2. Пишешь созданному боту @userinfobot 59 | 3. Кликаешь на это сообщение и тебя редиректит в некоторый чат, где можно получить id данного бота 60 | 4. Данный id, можно использовать для рассылки уведомлений 61 | 62 | nest g module incident 63 | nest g service incident 64 | nest g controller incident 65 | 66 | nest g module profile 67 | nest g service profile 68 | nest g controller profile 69 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | db: 4 | image: postgres 5 | restart: always 6 | ports: 7 | - "5432:5432" 8 | environment: 9 | POSTGRES_PASSWORD: pass123 10 | redis: 11 | image: redis 12 | ports: 13 | - "6379:6379" 14 | restart: always -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth-nestjs", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@nestjs-modules/mailer": "^1.9.1", 24 | "@nestjs/common": "^10.0.0", 25 | "@nestjs/config": "^3.1.1", 26 | "@nestjs/core": "^10.0.0", 27 | "@nestjs/jwt": "^10.1.1", 28 | "@nestjs/mapped-types": "*", 29 | "@nestjs/platform-express": "^10.0.0", 30 | "@nestjs/schedule": "^4.0.0", 31 | "@nestjs/typeorm": "^10.0.0", 32 | "bcrypt": "^5.1.1", 33 | "class-transformer": "^0.5.1", 34 | "class-validator": "^0.14.0", 35 | "cookie-parser": "^1.4.6", 36 | "google-auth-library": "^9.0.0", 37 | "googleapis": "^128.0.0", 38 | "ioredis": "^5.3.2", 39 | "node-telegram-bot-api": "^0.64.0", 40 | "nodemailer": "^6.9.7", 41 | "nodemailer-sparkpost-transport": "^2.2.0", 42 | "otplib": "^12.0.1", 43 | "pg": "^8.11.3", 44 | "puppeteer": "^21.4.0", 45 | "qrcode": "^1.5.3", 46 | "reflect-metadata": "^0.1.13", 47 | "rxjs": "^7.8.1", 48 | "typeorm": "^0.3.17" 49 | }, 50 | "devDependencies": { 51 | "@nestjs/cli": "^10.0.0", 52 | "@nestjs/schematics": "^10.0.0", 53 | "@nestjs/testing": "^10.0.0", 54 | "@types/bcrypt": "^5.0.0", 55 | "@types/cookie-parser": "^1.4.4", 56 | "@types/express": "^4.17.17", 57 | "@types/jest": "^29.5.2", 58 | "@types/node": "^20.3.1", 59 | "@types/nodemailer": "^6.4.13", 60 | "@types/puppeteer": "^7.0.4", 61 | "@types/qrcode": "^1.5.2", 62 | "@types/supertest": "^2.0.12", 63 | "@typescript-eslint/eslint-plugin": "^5.59.11", 64 | "@typescript-eslint/parser": "^5.59.11", 65 | "eslint": "^8.42.0", 66 | "eslint-config-prettier": "^8.8.0", 67 | "eslint-plugin-prettier": "^4.2.1", 68 | "jest": "^29.5.0", 69 | "prettier": "^2.8.8", 70 | "source-map-support": "^0.5.21", 71 | "supertest": "^6.3.3", 72 | "ts-jest": "^29.1.0", 73 | "ts-loader": "^9.4.3", 74 | "ts-node": "^10.9.1", 75 | "tsconfig-paths": "^4.2.0", 76 | "typescript": "^5.1.3" 77 | }, 78 | "jest": { 79 | "moduleFileExtensions": [ 80 | "js", 81 | "json", 82 | "ts" 83 | ], 84 | "rootDir": "src", 85 | "testRegex": ".*\\.spec\\.ts$", 86 | "transform": { 87 | "^.+\\.(t|j)s$": "ts-jest" 88 | }, 89 | "collectCoverageFrom": [ 90 | "**/*.(t|j)s" 91 | ], 92 | "coverageDirectory": "../coverage", 93 | "testEnvironment": "node" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/agency/agency.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AgencyController } from './agency.controller'; 3 | 4 | describe('AgencyController', () => { 5 | let controller: AgencyController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [AgencyController], 10 | }).compile(); 11 | 12 | controller = module.get(AgencyController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/agency/agency.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Delete, Get, Param, Patch, Post, Req } from '@nestjs/common'; 2 | import { AgencyService } from './agency.service'; 3 | import { Request } from 'express'; 4 | import { Auth } from '../iam/authentication/decorators/auth.decorator'; 5 | import { AuthType } from '../iam/authentication/enums/auth-type.enum'; 6 | import { CreateAgencyDto } from './dto/create-agency.dto'; 7 | import { UpdateAgencyDto } from './dto/update-agency.dto'; 8 | import { InviteAgencyDto } from './dto/invite-agency.dto'; 9 | 10 | @Auth(AuthType.Bearer, AuthType.ApiKey) 11 | @Controller('agency') 12 | export class AgencyController { 13 | constructor(private readonly agencyService: AgencyService) {} 14 | 15 | @Get() 16 | agencyController() { 17 | return 'agency controller working'; 18 | } 19 | 20 | @Get('me') 21 | getMeController(@Req() request: Request) { 22 | const tokenParts = request.headers.authorization.split(' '); 23 | 24 | return this.agencyService.findMe(tokenParts[1]); 25 | } 26 | 27 | @Post() 28 | create(@Req() request: Request, @Body() createAgencyDto: CreateAgencyDto) { 29 | const tokenParts = request.headers.authorization.split(' '); 30 | 31 | return this.agencyService.createAgency(tokenParts[1], createAgencyDto); 32 | } 33 | 34 | @Patch() 35 | update(@Req() request: Request, @Body() updateAgencyDto: UpdateAgencyDto) { 36 | const tokenParts = request.headers.authorization.split(' '); 37 | 38 | return this.agencyService.update(tokenParts[1], updateAgencyDto); 39 | } 40 | 41 | @Post('invite') 42 | invite(@Req() request: Request, @Body() inviteAgencyDto: InviteAgencyDto) { 43 | const tokenParts = request.headers.authorization.split(' '); 44 | 45 | return this.agencyService.inviteAgency(tokenParts[1], inviteAgencyDto); 46 | } 47 | 48 | @Get('acceptinvitation') 49 | async acceptInvite(@Req() request: Request) { 50 | const tokenParts = request.headers.authorization.split(' '); 51 | 52 | return this.agencyService.acceptInvite(tokenParts[1]); 53 | } 54 | 55 | @Delete(':id') 56 | async remove(@Req() request: Request, @Param('id') id: string) { 57 | const tokenParts = request.headers.authorization.split(' '); 58 | 59 | return this.agencyService.remove(tokenParts[1], id); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/agency/agency.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AgencyService } from './agency.service'; 3 | import { AgencyController } from './agency.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { User } from '../users/entities/user.entity'; 6 | import { Agency } from './entities/agency.entity'; 7 | import { NotificationService } from '../notification/notification.service'; 8 | import { SUserNotifications } from '../notification/helpers/notification.suser'; 9 | import { AllNotifications } from '../notification/helpers/notification.all'; 10 | import { SendNotifications } from '../notification/helpers/notification.send'; 11 | import { AdminNotifications } from '../notification/helpers/notification.admin'; 12 | import { ManagerNotifications } from '../notification/helpers/notification.manager'; 13 | import { Notification } from '../notification/entities/notification.entity'; 14 | import { ConfigModule, ConfigService } from '@nestjs/config'; 15 | import { EmailService } from '../email/email.service'; 16 | import { TelegramService } from '../telegram/telegram.service'; 17 | import { Profile } from '../profile/entities/profile.entities'; 18 | 19 | @Module({ 20 | imports: [TypeOrmModule.forFeature([User, Agency, Profile, Notification]), ConfigModule.forRoot()], 21 | providers: [ 22 | AgencyService, 23 | ConfigService, 24 | NotificationService, 25 | SUserNotifications, 26 | AllNotifications, 27 | SendNotifications, 28 | AdminNotifications, 29 | ManagerNotifications, 30 | EmailService, 31 | TelegramService 32 | ], 33 | controllers: [AgencyController] 34 | }) 35 | export class AgencyModule {} 36 | -------------------------------------------------------------------------------- /src/agency/agency.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AgencyService } from './agency.service'; 3 | 4 | describe('AgencyService', () => { 5 | let service: AgencyService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AgencyService], 10 | }).compile(); 11 | 12 | service = module.get(AgencyService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/agency/agency.service.ts: -------------------------------------------------------------------------------- 1 | import { ConflictException, Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import * as jwt from 'jsonwebtoken'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { User } from '../users/entities/user.entity'; 5 | import { Repository } from 'typeorm'; 6 | import { CreateAgencyDto } from './dto/create-agency.dto'; 7 | import { Agency } from './entities/agency.entity'; 8 | import { UpdateAgencyDto } from './dto/update-agency.dto'; 9 | import { Invite, Plan } from './enums/agency.enum'; 10 | import { Role } from '../users/enums/role.enum'; 11 | import { InviteAgencyDto } from './dto/invite-agency.dto'; 12 | import { NotificationService } from '../notification/notification.service'; 13 | import { Profile } from '../profile/entities/profile.entities'; 14 | 15 | @Injectable() 16 | export class AgencyService { 17 | constructor( 18 | @InjectRepository(User) private readonly usersRepository: Repository, 19 | @InjectRepository(Agency) private readonly agenciesRepository: Repository, 20 | @InjectRepository(Profile) private readonly profilesRepository: Repository, 21 | private notificationService: NotificationService, 22 | ){} 23 | 24 | async findMe(token: string) { 25 | try { 26 | const refreshTokenData = jwt.decode(token) as { sub: string }; 27 | const sub = refreshTokenData.sub; 28 | const agency = await this.agenciesRepository.findOneBy({ 29 | ownerId: sub, 30 | }) 31 | return agency; 32 | } catch (err) { 33 | const pgUniqueViolationErrorCode = '23505'; 34 | if (err.code === pgUniqueViolationErrorCode) { 35 | throw new ConflictException(); 36 | } 37 | throw err; 38 | } 39 | } 40 | 41 | async createAgency(token: string, createAgencyDto: CreateAgencyDto) { 42 | try { 43 | const refreshTokenData = jwt.decode(token) as { email: string }; 44 | const email = refreshTokenData.email; 45 | const user = await this.usersRepository.findOneBy({ 46 | email: email, 47 | }) 48 | if (!user) { 49 | throw new UnauthorizedException('User does not exists'); 50 | } 51 | 52 | const agency = await this.agenciesRepository.findOneBy({ 53 | ownerId: user.id, 54 | }) 55 | 56 | if (agency) { 57 | throw new ConflictException('Agency by this user already exist'); 58 | } else { 59 | const newAgency = new Agency(); 60 | newAgency.ownerId = user.id; 61 | newAgency.name = createAgencyDto.name; 62 | newAgency.plan = Plan.Free; 63 | 64 | return await this.agenciesRepository.save(newAgency).then( 65 | (data) => { 66 | user.role = Role.Admin; 67 | user.roles = [Role.Admin]; 68 | user.agencyId = newAgency.id; 69 | this.usersRepository.save(user); 70 | return data; 71 | } 72 | ); 73 | } 74 | } catch (err) { 75 | const pgUniqueViolationErrorCode = '23505'; 76 | if (err.code === pgUniqueViolationErrorCode) { 77 | throw new ConflictException(); 78 | } 79 | throw err; 80 | } 81 | } 82 | 83 | async update(token: string, updateAgencyDto: UpdateAgencyDto) { 84 | try { 85 | const refreshTokenData = jwt.decode(token) as { email: string }; 86 | const email = refreshTokenData.email; 87 | const user = await this.usersRepository.findOneBy({ 88 | email: email, 89 | }) 90 | if (!user) { 91 | throw new UnauthorizedException('User does not exists'); 92 | } 93 | 94 | const agency = await this.agenciesRepository.findOneBy({ 95 | ownerId: user.id, 96 | }) 97 | 98 | if (agency) { 99 | // TODO: create method 100 | // agency.plan = updateAgencyDto.plan; 101 | agency.name = updateAgencyDto.name; 102 | agency.stopWords = updateAgencyDto.stopWords; 103 | if (updateAgencyDto.userTimeConstraint) { 104 | agency.userTimeConstraint = { 105 | ...agency.userTimeConstraint, 106 | ...updateAgencyDto.userTimeConstraint, 107 | }; 108 | } 109 | 110 | return await this.agenciesRepository.save(agency); 111 | } else { 112 | throw new ConflictException('Agency by this user dont exist'); 113 | } 114 | } catch (err) { 115 | const pgUniqueViolationErrorCode = '23505'; 116 | if (err.code === pgUniqueViolationErrorCode) { 117 | throw new ConflictException(); 118 | } 119 | throw err; 120 | } 121 | } 122 | 123 | async inviteAgency(token: string, inviteAgencyDto: InviteAgencyDto) { 124 | try { 125 | const refreshTokenData = jwt.decode(token) as { email: string }; 126 | const email = refreshTokenData.email; 127 | const user = await this.usersRepository.findOneBy({ 128 | email: email, 129 | }) 130 | if (!user) { 131 | throw new UnauthorizedException('User does not exists'); 132 | } 133 | 134 | if (user.role !== Role.Admin) { 135 | throw new UnauthorizedException('User does not admin'); 136 | } 137 | 138 | const agency = await this.agenciesRepository.findOneBy({ 139 | ownerId: user.id, 140 | }) 141 | 142 | // TODO: let send invites for all admins, not only for owner 143 | if (agency.ownerId !== user.id) { 144 | throw new UnauthorizedException('User does not owner of this agency'); 145 | } 146 | 147 | if (agency) { 148 | const existingInvite = agency.invites.find((invite) => invite.id === inviteAgencyDto.id); 149 | if (existingInvite) { 150 | throw new ConflictException('Invite for this user already exist'); 151 | } else { 152 | const inviteObj = { 153 | id: inviteAgencyDto.id, 154 | role: inviteAgencyDto.role, 155 | accepted: false 156 | } as Invite; 157 | 158 | if (inviteObj.role === Role.SuperUser) { 159 | throw new ConflictException('Impossible operation'); 160 | } 161 | agency.invites.push(inviteObj); 162 | 163 | return await this.agenciesRepository.save(agency).then( 164 | async (data) => { 165 | 166 | const dstUser = await this.usersRepository.findOneBy({ 167 | id: inviteAgencyDto.id, 168 | }) 169 | dstUser.invitedTo = agency.id; 170 | await this.usersRepository.save(dstUser); 171 | await this.notificationService.OnSentInvite(user, inviteObj, agency) 172 | 173 | const profile = await this.profilesRepository.findOneBy({ 174 | id: dstUser.profileId, 175 | }) 176 | profile.verified = true; 177 | await this.profilesRepository.save(profile); 178 | 179 | return data; 180 | } 181 | ); 182 | } 183 | } else { 184 | throw new ConflictException('Agency by this user dont exist'); 185 | } 186 | } catch (err) { 187 | const pgUniqueViolationErrorCode = '23505'; 188 | if (err.code === pgUniqueViolationErrorCode) { 189 | throw new ConflictException(); 190 | } 191 | throw err; 192 | } 193 | } 194 | 195 | async remove(token: string, id: string) { 196 | try { 197 | const refreshTokenData = jwt.decode(token) as { email: string }; 198 | const email = refreshTokenData.email; 199 | const user = await this.usersRepository.findOneBy({ 200 | email: email, 201 | }) 202 | if (!user) { 203 | throw new UnauthorizedException('User does not exists'); 204 | } 205 | 206 | if (user.role !== Role.Admin) { 207 | throw new UnauthorizedException('User does not admin'); 208 | } 209 | 210 | const agency = await this.agenciesRepository.findOneBy({ 211 | ownerId: user.id, 212 | }) 213 | 214 | // TODO: let send invites for all admins, not only for owner 215 | if (agency.ownerId !== user.id) { 216 | throw new UnauthorizedException('User does not owner of this agency'); 217 | } 218 | 219 | if (agency) { 220 | const existingInvite = agency.invites.find((invite) => invite.id === id); 221 | if (existingInvite) { 222 | const updatedInvitesArr = agency.invites.filter((invite) => invite.id !== id); 223 | 224 | if (existingInvite.role === Role.Admin) { 225 | agency.admins = agency.admins.filter((itemId) => itemId !== id) 226 | } 227 | if (existingInvite.role === Role.Manager) { 228 | agency.managers = agency.managers.filter((itemId) => itemId !== id) 229 | } 230 | if (existingInvite.role === Role.Model) { 231 | agency.models = agency.models.filter((itemId) => itemId !== id) 232 | } 233 | 234 | agency.invites = updatedInvitesArr; 235 | return await this.agenciesRepository.save(agency).then( 236 | async (data) => { 237 | 238 | const user = await this.usersRepository.findOneBy({ 239 | id: id, 240 | }) 241 | user.invitedTo = null; 242 | user.agencyId = null; 243 | user.role = Role.Regular 244 | user.roles = [Role.Regular] 245 | await this.usersRepository.save(user); 246 | 247 | const profile = await this.profilesRepository.findOneBy({ 248 | id: user.profileId, 249 | }) 250 | profile.verified = false; 251 | await this.profilesRepository.save(profile); 252 | 253 | return data; 254 | } 255 | ); 256 | } else { 257 | throw new ConflictException('Invite for this user doesnt exist'); 258 | } 259 | } else { 260 | throw new ConflictException('Agency by this user dont exist'); 261 | } 262 | } catch (err) { 263 | const pgUniqueViolationErrorCode = '23505'; 264 | if (err.code === pgUniqueViolationErrorCode) { 265 | throw new ConflictException(); 266 | } 267 | throw err; 268 | } 269 | } 270 | 271 | async acceptInvite(token: string) { 272 | try { 273 | const refreshTokenData = jwt.decode(token) as { email: string }; 274 | const email = refreshTokenData.email; 275 | const user = await this.usersRepository.findOneBy({ 276 | email: email, 277 | }) 278 | if (!user) { 279 | throw new UnauthorizedException('User does not exists'); 280 | } 281 | 282 | const id = user.id; 283 | 284 | const agency = await this.agenciesRepository.findOneBy({ 285 | id: user.invitedTo, 286 | }) 287 | 288 | // TODO: check that this user exist invitation in agency.invites 289 | // if (agency.ownerId !== user.id) { 290 | // throw new UnauthorizedException('User does not owner of this agency'); 291 | // } 292 | 293 | if (agency) { 294 | const existingInvite = agency.invites.find((invite) => invite.id === id) as Invite; 295 | if (existingInvite) { 296 | 297 | if (!existingInvite.accepted) { 298 | const updatedInvitesArr = agency.invites.filter((invite) => invite.id !== id); 299 | 300 | updatedInvitesArr.push( 301 | { 302 | id: existingInvite.id, 303 | role: existingInvite.role, 304 | accepted: true 305 | } 306 | ) 307 | 308 | agency.invites = updatedInvitesArr; 309 | if (existingInvite.role === Role.Admin) { 310 | agency.admins.push(existingInvite.id) 311 | } 312 | if (existingInvite.role === Role.Manager) { 313 | agency.managers.push(existingInvite.id) 314 | } 315 | if (existingInvite.role === Role.Model) { 316 | agency.models.push(existingInvite.id) 317 | } 318 | 319 | return await this.agenciesRepository.save(agency).then( 320 | async (data) => { 321 | 322 | user.role = existingInvite.role; 323 | user.roles = [existingInvite.role] 324 | user.agencyId = agency.id; 325 | await this.usersRepository.save(user); 326 | await this.notificationService.OnAcceptInvite(user, existingInvite, agency) 327 | 328 | return data; 329 | } 330 | ); 331 | } else { 332 | throw new ConflictException('Invite for this user already accepted'); 333 | } 334 | } else { 335 | throw new ConflictException('Invite for this user doesnt exist'); 336 | } 337 | } else { 338 | throw new ConflictException('Agency by this user dont exist'); 339 | } 340 | 341 | } catch (err) { 342 | const pgUniqueViolationErrorCode = '23505'; 343 | if (err.code === pgUniqueViolationErrorCode) { 344 | throw new ConflictException(); 345 | } 346 | throw err; 347 | } 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /src/agency/dto/create-agency.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | 3 | export class CreateAgencyDto { 4 | @IsNotEmpty() 5 | name: string; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/agency/dto/invite-agency.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | import { Role } from '../../users/enums/role.enum'; 3 | 4 | export class InviteAgencyDto { 5 | @IsNotEmpty() 6 | id: string; 7 | 8 | @IsNotEmpty() 9 | role: Role; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/agency/dto/update-agency.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | import { Plan } from '../enums/agency.enum'; 3 | 4 | export class UpdateAgencyDto { 5 | @IsNotEmpty() 6 | name: string; 7 | @IsNotEmpty() 8 | stopWords: string; 9 | 10 | userTimeConstraint: { [key: string]: number }; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/agency/entities/agency.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { Invite, Plan, UserTimeConstraint } from '../enums/agency.enum'; 3 | 4 | @Entity() 5 | export class Agency { 6 | @PrimaryGeneratedColumn('uuid') 7 | id: string; 8 | 9 | @CreateDateColumn() 10 | created_at: Date; 11 | 12 | @Column({unique: true}) 13 | ownerId: string; 14 | 15 | @Column() 16 | name: string; 17 | 18 | @Column({ enum: Plan, default: Plan.Free }) 19 | plan: Plan; 20 | 21 | @Column({default: ''}) 22 | stopWords: string; 23 | 24 | @Column('jsonb', { default: [] }) 25 | invites: Invite[]; 26 | 27 | @Column('jsonb', { default: {} }) 28 | userTimeConstraint: UserTimeConstraint; 29 | 30 | @Column('text', { array: true, default: [] }) 31 | admins: string[]; 32 | 33 | @Column('text', { array: true, default: [] }) 34 | managers: string[]; 35 | 36 | @Column('text', { array: true, default: [] }) 37 | models: string[]; 38 | 39 | @Column({default: false}) 40 | verified: boolean; 41 | } -------------------------------------------------------------------------------- /src/agency/enums/agency.enum.ts: -------------------------------------------------------------------------------- 1 | import { Role } from '../../users/enums/role.enum'; 2 | 3 | export enum Plan { 4 | Free = 'free', 5 | Basic = 'basic', 6 | Pro = 'Pro', 7 | } 8 | 9 | export type Invite = { 10 | id: string; 11 | role: Role; 12 | accepted: boolean; 13 | }; 14 | 15 | export type UserTimeConstraint = { [key: string]: number }; -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { CoffeesModule } from './coffees/coffees.module'; 5 | import { UsersModule } from './users/users.module'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { IamModule } from './iam/iam.module'; 8 | import { ConfigModule } from '@nestjs/config'; 9 | import { OnlyFansModule } from './only-fans/only-fans.module'; 10 | import { ScrapperModule } from './scrapper/scrapper.module'; 11 | import { MessagesModule } from './messages/messages.module'; 12 | import { ScheduleModule } from '@nestjs/schedule'; 13 | import { ScheduleModule as SchedulerModule } from './schedule/schedule.module'; 14 | import { AgencyModule } from './agency/agency.module'; 15 | import { NotificationModule } from './notification/notification.module'; 16 | import { MailerModule } from '@nestjs-modules/mailer'; 17 | import { EmailModule } from './email/email.module'; 18 | import { TelegramModule } from './telegram/telegram.module'; 19 | import { IncidentModule } from './incident/incident.module'; 20 | import { ProfileModule } from './profile/profile.module'; 21 | 22 | @Module({ 23 | imports: [ 24 | ConfigModule.forRoot(), 25 | ScheduleModule.forRoot(), 26 | MailerModule.forRoot({ 27 | transport: 'smtps://user@domain.com:pass@smtp.domain.com', 28 | }), 29 | SchedulerModule, 30 | CoffeesModule, 31 | UsersModule, 32 | OnlyFansModule, 33 | ScrapperModule, 34 | TypeOrmModule.forRoot({ 35 | type: 'postgres', 36 | host: 'localhost', 37 | port: 5432, 38 | username: 'postgres', 39 | password: 'pass123', 40 | database: 'postgres', 41 | autoLoadEntities: true, 42 | synchronize: true, 43 | }), IamModule, MessagesModule, ScheduleModule, AgencyModule, NotificationModule, EmailModule, TelegramModule, IncidentModule, ProfileModule], 44 | controllers: [AppController], 45 | providers: [AppService], 46 | }) 47 | export class AppModule {} 48 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/coffees/coffees.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { CoffeesController } from './coffees.controller'; 3 | import { CoffeesService } from './coffees.service'; 4 | 5 | describe('CoffeesController', () => { 6 | let controller: CoffeesController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [CoffeesController], 11 | providers: [CoffeesService], 12 | }).compile(); 13 | 14 | controller = module.get(CoffeesController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/coffees/coffees.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'; 2 | import { CoffeesService } from './coffees.service'; 3 | import { CreateCoffeeDto } from './dto/create-coffee.dto'; 4 | import { UpdateCoffeeDto } from './dto/update-coffee.dto'; 5 | import { ActiveUser } from '../iam/decorators/active-user.decorator'; 6 | import { ActiveUserData } from '../iam/interfaces/active-user-data.interface'; 7 | import { Roles } from '../iam/authorization/decorators/roles.decorator'; 8 | import { Role } from '../users/enums/role.enum'; 9 | import { Auth } from '../iam/authentication/decorators/auth.decorator'; 10 | import { AuthType } from '../iam/authentication/enums/auth-type.enum'; 11 | import { Policies } from '../iam/authorization/decorators/policies.decorator'; 12 | import { FrameworkContributorPolicy } from '../iam/authorization/policies/framework-contributor.policy'; 13 | 14 | @Auth(AuthType.Bearer, AuthType.ApiKey) 15 | @Controller('coffees') 16 | export class CoffeesController { 17 | constructor(private readonly coffeesService: CoffeesService) {} 18 | 19 | // @Permissions(Permission.CreateCoffee) 20 | // @Policies(new FrameworkContributorPolicy(), /** new MinAgePolicy(18), new OnlyAdminPolicy() */) 21 | @Roles(Role.Admin) 22 | @Post() 23 | create(@Body() createCoffeeDto: CreateCoffeeDto) { 24 | return this.coffeesService.create(createCoffeeDto); 25 | } 26 | 27 | @Get() 28 | findAll(@ActiveUser() user: ActiveUserData) { 29 | console.log(user); 30 | return this.coffeesService.findAll(); 31 | } 32 | 33 | @Get(':id') 34 | findOne(@Param('id') id: string) { 35 | return this.coffeesService.findOne(+id); 36 | } 37 | 38 | @Roles(Role.Admin) 39 | @Patch(':id') 40 | update(@Param('id') id: string, @Body() updateCoffeeDto: UpdateCoffeeDto) { 41 | return this.coffeesService.update(+id, updateCoffeeDto); 42 | } 43 | 44 | @Roles(Role.Admin) 45 | @Delete(':id') 46 | remove(@Param('id') id: string) { 47 | return this.coffeesService.remove(+id); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/coffees/coffees.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CoffeesService } from './coffees.service'; 3 | import { CoffeesController } from './coffees.controller'; 4 | 5 | @Module({ 6 | controllers: [CoffeesController], 7 | providers: [CoffeesService], 8 | }) 9 | export class CoffeesModule {} 10 | -------------------------------------------------------------------------------- /src/coffees/coffees.permission.ts: -------------------------------------------------------------------------------- 1 | export enum CoffeesPermission { 2 | CreateCoffee = 'create_coffee', 3 | UpdateCoffee = 'update_coffee', 4 | DeleteCoffee = 'delete_coffee', 5 | } -------------------------------------------------------------------------------- /src/coffees/coffees.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { CoffeesService } from './coffees.service'; 3 | 4 | describe('CoffeesService', () => { 5 | let service: CoffeesService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [CoffeesService], 10 | }).compile(); 11 | 12 | service = module.get(CoffeesService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/coffees/coffees.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { CreateCoffeeDto } from './dto/create-coffee.dto'; 3 | import { UpdateCoffeeDto } from './dto/update-coffee.dto'; 4 | 5 | @Injectable() 6 | export class CoffeesService { 7 | create(createCoffeeDto: CreateCoffeeDto) { 8 | return 'This action adds a new coffee'; 9 | } 10 | 11 | findAll() { 12 | return `This action returns all coffees`; 13 | } 14 | 15 | findOne(id: number) { 16 | return `This action returns a #${id} coffee`; 17 | } 18 | 19 | update(id: number, updateCoffeeDto: UpdateCoffeeDto) { 20 | return `This action updates a #${id} coffee`; 21 | } 22 | 23 | remove(id: number) { 24 | return `This action removes a #${id} coffee`; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/coffees/dto/create-coffee.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateCoffeeDto {} 2 | -------------------------------------------------------------------------------- /src/coffees/dto/refresh-token.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | 3 | export class RefreshTokenDto { 4 | @IsNotEmpty() 5 | refreshToken: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/coffees/dto/update-coffee.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/mapped-types'; 2 | import { CreateCoffeeDto } from './create-coffee.dto'; 3 | 4 | export class UpdateCoffeeDto extends PartialType(CreateCoffeeDto) {} 5 | -------------------------------------------------------------------------------- /src/coffees/entities/coffee.entity.ts: -------------------------------------------------------------------------------- 1 | export class Coffee {} 2 | -------------------------------------------------------------------------------- /src/email/email.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { EmailController } from './email.controller'; 3 | 4 | describe('EmailController', () => { 5 | let controller: EmailController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [EmailController], 10 | }).compile(); 11 | 12 | controller = module.get(EmailController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/email/email.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { EmailService } from './email.service'; 3 | 4 | @Controller('email') 5 | export class EmailController { 6 | constructor( 7 | private readonly mailingService: EmailService 8 | ) {} 9 | 10 | @Get('send-mail') 11 | public sendMail() { 12 | return this.mailingService.sendMail(); 13 | } 14 | } -------------------------------------------------------------------------------- /src/email/email.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EmailService } from './email.service'; 3 | import { EmailController } from './email.controller'; 4 | import { ConfigService } from '@nestjs/config'; 5 | 6 | @Module({ 7 | providers: [EmailService, ConfigService], 8 | controllers: [EmailController] 9 | }) 10 | export class EmailModule {} 11 | -------------------------------------------------------------------------------- /src/email/email.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { EmailService } from './email.service'; 3 | 4 | describe('EmailService', () => { 5 | let service: EmailService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [EmailService], 10 | }).compile(); 11 | 12 | service = module.get(EmailService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/email/email.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { MailerService } from '@nestjs-modules/mailer'; 4 | import { google } from 'googleapis'; 5 | import { Options } from 'nodemailer/lib/smtp-transport'; 6 | import { User } from '../users/entities/user.entity'; 7 | import { NotificationType } from '../notification/enums/notification.enum'; 8 | 9 | @Injectable() 10 | export class EmailService { 11 | constructor( 12 | private readonly configService: ConfigService, 13 | private readonly mailerService: MailerService, 14 | ) {} 15 | 16 | private async setTransport() { 17 | const OAuth2 = google.auth.OAuth2; 18 | const oauth2Client = new OAuth2( 19 | this.configService.get('CLIENT_ID'), 20 | this.configService.get('CLIENT_SECRET'), 21 | 'https://developers.google.com/oauthplayground', 22 | ); 23 | 24 | oauth2Client.setCredentials({ 25 | refresh_token: process.env.REFRESH_TOKEN, 26 | }); 27 | 28 | const accessToken: string = await new Promise((resolve, reject) => { 29 | oauth2Client.getAccessToken((err, token) => { 30 | if (err) { 31 | reject('Failed to create access token'); 32 | } 33 | resolve(token); 34 | }); 35 | }); 36 | 37 | const config: Options = { 38 | service: 'gmail', 39 | auth: { 40 | type: 'OAuth2', 41 | user: this.configService.get('EMAIL'), 42 | clientId: this.configService.get('CLIENT_ID'), 43 | clientSecret: this.configService.get('CLIENT_SECRET'), 44 | accessToken, 45 | }, 46 | }; 47 | this.mailerService.addTransporter('gmail', config); 48 | } 49 | 50 | public async sendMail() { 51 | await this.setTransport(); 52 | const link = 'https://example.com/'; 53 | return this.mailerService 54 | .sendMail({ 55 | transporterName: 'gmail', 56 | // to: 'klim.barkalov@mail.ru', // list of receivers 57 | to: 'kliment.barkalov@gmail.com', // list of receivers 58 | from: 'noreply@nestjs.com', 59 | subject: 'Subject', // Subject line 60 | // template: 'action', 61 | // context: { 62 | // // Data to be sent to template engine.. 63 | // code: '38320', 64 | // }, 65 | // text: 'Welcome text', 66 | html: 67 | ` 68 |
69 |

Для активации перейдите по ссылке

70 | ${link} 71 |
72 | ` 73 | }) 74 | .then((success) => { 75 | console.log(success); 76 | return success 77 | }) 78 | .catch((err) => { 79 | console.log(err); 80 | return err 81 | }); 82 | } 83 | 84 | public async sendNotificationMail( 85 | user: User, 86 | subject: string, 87 | text: string, 88 | eventData: any = undefined, 89 | type: NotificationType = NotificationType.COMMON 90 | ) { 91 | const { settings: { notifications: notificationSettings } } = user; 92 | 93 | let allowSend = true; 94 | if (notificationSettings && notificationSettings.email) { 95 | switch (type) { 96 | case NotificationType.COMMON: 97 | allowSend = (notificationSettings.email.info !== undefined) ? !!notificationSettings.email.info : true; 98 | break; 99 | case NotificationType.EVENTS: 100 | allowSend = (notificationSettings.email.events !== undefined) ? !!notificationSettings.email.events : true; 101 | break; 102 | case NotificationType.CHAT: 103 | allowSend = (notificationSettings.email.comments !== undefined) ? !!notificationSettings.email.comments : true; 104 | break; 105 | } 106 | } 107 | 108 | if (allowSend && process.env.ALLOW_SEND === 'true') { 109 | 110 | // Todo: templating 111 | // const template = this.getTemplate(eventData); 112 | await this.setTransport(); 113 | const email = [user.email] 114 | return this.mailerService 115 | .sendMail({ 116 | transporterName: 'gmail', 117 | to: email, 118 | from: 'noreply@nestjs.com', 119 | subject: subject, 120 | html: 121 | ` 122 |
123 | ${text} 124 |
125 | ` 126 | }) 127 | .then((success) => { 128 | console.log(success); 129 | return success 130 | }) 131 | .catch((err) => { 132 | console.log(err); 133 | return err 134 | }); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/iam/authentication/api-key.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ApiKeyService } from './api-key.service'; 3 | 4 | describe('ApiKeyService', () => { 5 | let service: ApiKeyService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ApiKeyService], 10 | }).compile(); 11 | 12 | service = module.get(ApiKeyService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/iam/authentication/api-key.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { HashingService } from '../hashing/hashing.service'; 3 | import { randomUUID } from 'crypto'; 4 | 5 | export interface GeneratedApiKeyPayLoad { 6 | apiKey: string; 7 | hashedKey: string; 8 | } 9 | 10 | @Injectable() 11 | export class ApiKeysService { 12 | constructor(private readonly hashingService: HashingService) {} 13 | 14 | async createAndHash(id: number): Promise { 15 | const apiKey = this.generateApiKey(id); 16 | const hashedKey = await this.hashingService.hash(apiKey); 17 | return { apiKey, hashedKey }; 18 | } 19 | 20 | async validate(apiKey: string, hashedKey: string): Promise { 21 | return this.hashingService.compare(apiKey, hashedKey); 22 | } 23 | 24 | extractIdFromApiKey(apiKey: string): string { 25 | const [id] = Buffer.from(apiKey, 'base64').toString('ascii').split(' '); 26 | return id; 27 | } 28 | 29 | private generateApiKey(id: number): string { 30 | const apiKey = `${id} ${randomUUID()}`; 31 | return Buffer.from(apiKey).toString('base64'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/iam/authentication/authentication.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthenticationController } from './authentication.controller'; 3 | 4 | describe('AuthenticationController', () => { 5 | let controller: AuthenticationController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [AuthenticationController], 10 | }).compile(); 11 | 12 | controller = module.get(AuthenticationController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/iam/authentication/authentication.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, HttpCode, HttpStatus, Post, Req, Res } from '@nestjs/common'; 2 | import { AuthenticationService } from './authentication.service'; 3 | import { SignUpDto } from './dto/sign-up.dto/sign-up.dto'; 4 | import { SignInDto } from './dto/sign-in.dto/sign-in.dto'; 5 | import { Auth } from './decorators/auth.decorator'; 6 | import { AuthType } from './enums/auth-type.enum'; 7 | import { RefreshTokenDto } from '../../coffees/dto/refresh-token.dto'; 8 | import { ActiveUser } from '../decorators/active-user.decorator'; 9 | import { ActiveUserData } from '../interfaces/active-user-data.interface'; 10 | import { Response, Request } from 'express'; 11 | import { OtpAuthenticationService } from './opt-authentication/opt-authentication.service'; 12 | import { toFileStream } from 'qrcode'; 13 | 14 | @Auth(AuthType.None) 15 | @Controller('authentication') 16 | export class AuthenticationController { 17 | constructor( 18 | private readonly authService: AuthenticationService, 19 | private readonly otpAuthService: OtpAuthenticationService 20 | ) {} 21 | 22 | @Post('sign-up') 23 | signUp(@Body() signUpDto: SignUpDto) { 24 | return this.authService.signUp(signUpDto); 25 | } 26 | 27 | @HttpCode(HttpStatus.OK) 28 | @Post('sign-in') 29 | async signIn( 30 | @Res({ passthrough: true }) response: Response, 31 | @Body() signInDto: SignInDto 32 | ) { 33 | const tokens = await this.authService.signIn(signInDto); 34 | 35 | response.cookie('refreshToken', tokens.refreshToken, { 36 | httpOnly: true, 37 | sameSite: true, 38 | // TODO: fort postman 39 | // secure: true, 40 | secure: false, 41 | }) 42 | return tokens; 43 | } 44 | 45 | @HttpCode(HttpStatus.OK) 46 | @Post('refresh-tokens') 47 | async refreshTokens( 48 | @Res({ passthrough: true }) response: Response, 49 | @Req() request: Request 50 | ) { 51 | const refreshToken = request.cookies.refreshToken; 52 | const refreshTokenDto: RefreshTokenDto = { 53 | refreshToken: refreshToken 54 | } 55 | const refreshTokens = await this.authService.refreshTokens(refreshTokenDto); 56 | response.cookie('refreshToken', refreshTokens.refreshToken, { 57 | httpOnly: true, 58 | sameSite: true, 59 | // TODO: fort postman 60 | // secure: true, 61 | secure: false, 62 | }) 63 | 64 | return refreshTokens; 65 | } 66 | 67 | @Auth(AuthType.Bearer) 68 | @HttpCode(HttpStatus.OK) 69 | @Post('2fa/generate') 70 | async generateQrCode( 71 | @ActiveUser() activeUser: ActiveUserData, 72 | @Res() response: Response 73 | ) { 74 | const { secret, uri } = await this.otpAuthService.generateSecret( 75 | activeUser.email, 76 | ); 77 | await this.otpAuthService.enableTfaForUser(activeUser.email, secret); 78 | response.type( 'png'); 79 | return toFileStream(response, uri); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/iam/authentication/authentication.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthenticationService } from './authentication.service'; 3 | 4 | describe('AuthenticationService', () => { 5 | let service: AuthenticationService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AuthenticationService], 10 | }).compile(); 11 | 12 | service = module.get(AuthenticationService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/iam/authentication/authentication.service.ts: -------------------------------------------------------------------------------- 1 | import { ConflictException, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { User } from '../../users/entities/user.entity'; 4 | import { Repository } from 'typeorm'; 5 | import * as jwt from 'jsonwebtoken'; 6 | import { HashingService } from '../hashing/hashing.service'; 7 | import { SignUpDto, SignUpResponse } from './dto/sign-up.dto/sign-up.dto'; 8 | import { SignInDto, SignInResponse } from './dto/sign-in.dto/sign-in.dto'; 9 | import { JwtService } from '@nestjs/jwt'; 10 | import jwtConfig from '../config/jwt.config' 11 | import { ConfigType } from '@nestjs/config'; 12 | import { ActiveUserData } from '../interfaces/active-user-data.interface'; 13 | import { RefreshTokenDto } from '../../coffees/dto/refresh-token.dto'; 14 | import { randomUUID } from 'crypto'; 15 | import { 16 | InvalidatedRefreshTokenError, 17 | RefreshTokenIdsStorage, 18 | } from './refresh-token-ids.storage/refresh-token-ids.storage'; 19 | import { OtpAuthenticationService } from './opt-authentication/opt-authentication.service'; 20 | import { NotificationService } from '../../notification/notification.service'; 21 | import { Profile } from '../../profile/entities/profile.entities'; 22 | import { Agency } from '../../agency/entities/agency.entity'; 23 | import { Plan } from '../../agency/enums/agency.enum'; 24 | import { Role } from '../../users/enums/role.enum'; 25 | 26 | @Injectable() 27 | export class AuthenticationService { 28 | constructor( 29 | @InjectRepository(User) private readonly usersRepository: Repository, 30 | @InjectRepository(Profile) private readonly profilesRepository: Repository, 31 | @InjectRepository(Agency) private readonly agenciesRepository: Repository, 32 | private readonly hashingService: HashingService, 33 | private readonly jwtService: JwtService, 34 | @Inject(jwtConfig.KEY) 35 | private readonly jwtConfiguration: ConfigType, 36 | private readonly refreshTokenIdsStorage: RefreshTokenIdsStorage, 37 | private readonly otpAuthService: OtpAuthenticationService, 38 | private notificationService: NotificationService, 39 | ){} 40 | 41 | async signUp(signUpDto: SignUpDto): Promise { 42 | try { 43 | const user = new User(); 44 | user.email = signUpDto.email; 45 | user.password = await this.hashingService.hash(signUpDto.password); 46 | 47 | const profile = new Profile(); 48 | profile.userId = 'temporaryUserId' 49 | const profileSaved = await this.profilesRepository.save(profile); 50 | user.profileId = profileSaved.id; 51 | let savedUser = await this.usersRepository.save(user); 52 | 53 | profileSaved.userId = savedUser.id; 54 | await this.profilesRepository.save(profileSaved); 55 | 56 | const tokens = await this.generateTokens(savedUser); 57 | 58 | const signUpResponse: SignUpResponse = { 59 | accessToken: tokens.accessToken, 60 | refreshToken: tokens.refreshToken, 61 | id: savedUser.id, 62 | email: savedUser.email, 63 | features: savedUser.features, 64 | avatar: savedUser.avatar, 65 | jsonSettings: savedUser.jsonSettings, 66 | }; 67 | 68 | if (signUpDto.agencyName === undefined || signUpDto.agencyName === '') { 69 | } else { 70 | const agency = new Agency(); 71 | agency.ownerId = user.id; 72 | agency.name = signUpDto.agencyName; 73 | agency.plan = Plan.Free; 74 | 75 | await this.agenciesRepository.save(agency).then( 76 | (data) => { 77 | user.role = Role.Admin; 78 | user.roles = [Role.Admin]; 79 | user.agencyId = data.id; 80 | this.usersRepository.save(user); 81 | return data; 82 | } 83 | ) 84 | } 85 | 86 | await this.notificationService.OnSignUp(savedUser.id); 87 | 88 | return signUpResponse; 89 | } catch (err) { 90 | const pgUniqueViolationErrorCode = '23505'; 91 | if (err.code === pgUniqueViolationErrorCode) { 92 | throw new ConflictException(); 93 | } 94 | throw err; 95 | } 96 | } 97 | 98 | async signIn(signInDto: SignInDto): Promise { 99 | const user = await this.usersRepository.findOneBy({ 100 | email: signInDto.email, 101 | }) 102 | if (!user) { 103 | throw new UnauthorizedException('User does not exists'); 104 | } 105 | const isEqual = await this.hashingService.compare( 106 | signInDto.password, 107 | user.password 108 | ); 109 | if (!isEqual) { 110 | throw new UnauthorizedException('Password does not match'); 111 | } 112 | if (user.isTfaEnabled) { 113 | const isValid = this.otpAuthService.verifyCode( 114 | signInDto.tfaCode, 115 | user.tfaSecret 116 | ) 117 | if (!isValid) { 118 | throw new UnauthorizedException('Invalid 2FA code'); 119 | } 120 | } 121 | 122 | const tokens = await this.generateTokens(user); 123 | 124 | const signInResponse: SignInResponse = { 125 | accessToken: tokens.accessToken, 126 | refreshToken: tokens.refreshToken, 127 | id: user.id, 128 | email: user.email, 129 | features: user.features, 130 | avatar: user.avatar, 131 | jsonSettings: user.jsonSettings, 132 | }; 133 | 134 | return signInResponse; 135 | } 136 | 137 | async generateTokens(user: User, remainingTime?: number) { 138 | const refreshTokenId = randomUUID(); 139 | const [accessToken, refreshToken] = await Promise.all([ 140 | this.signToken>( 141 | user.id, 142 | this.jwtConfiguration.accessTokenTtl, 143 | { email: user.email, role: user.role }, 144 | // { 145 | // email: user.email, 146 | // role: user.role, 147 | // // Warning 148 | // permissions: user.permissions, 149 | // }, 150 | ), 151 | this.signToken(user.id, remainingTime ? remainingTime : this.jwtConfiguration.refreshTokenTtl, 152 | { 153 | refreshTokenId, 154 | }), 155 | ]); 156 | await this.refreshTokenIdsStorage.insert(user.id, refreshTokenId); 157 | return { 158 | accessToken, 159 | refreshToken, 160 | }; 161 | } 162 | 163 | async refreshTokens(refreshTokenDto: RefreshTokenDto) { 164 | try { 165 | const { sub, refreshTokenId } = await this.jwtService.verifyAsync< 166 | Pick & { refreshTokenId: string} 167 | >(refreshTokenDto.refreshToken, { 168 | secret: this.jwtConfiguration.secret, 169 | audience: this.jwtConfiguration.audience, 170 | issuer: this.jwtConfiguration.issuer, 171 | }); 172 | 173 | const user = await this.usersRepository.findOneByOrFail({ 174 | id: sub, 175 | }); 176 | const isValid = await this.refreshTokenIdsStorage.validate( 177 | user.id, 178 | refreshTokenId 179 | ); 180 | if (isValid) { 181 | await this.refreshTokenIdsStorage.invalidate(user.id); 182 | } else { 183 | throw new Error('Refresh token is invalid'); 184 | } 185 | 186 | const refreshTokenData = jwt.decode(refreshTokenDto.refreshToken) as { exp: number }; 187 | const remainingTime = refreshTokenData.exp - Math.floor(Date.now() / 1000); 188 | 189 | return this.generateTokens(user, remainingTime); 190 | } catch (err) { 191 | if (err instanceof InvalidatedRefreshTokenError) { 192 | // Take action: notify user that his refresh token might have been stolen? 193 | throw new UnauthorizedException('Access denied'); 194 | } 195 | throw new UnauthorizedException('Invalid token'); 196 | } 197 | } 198 | 199 | private async signToken(userId: string, expiresIn: number, payload?: T) { 200 | const accessToken = await this.jwtService.signAsync( 201 | { 202 | sub: userId, 203 | ...payload 204 | }, 205 | { 206 | audience: this.jwtConfiguration.audience, 207 | issuer: this.jwtConfiguration.issuer, 208 | secret: this.jwtConfiguration.secret, 209 | expiresIn, 210 | }, 211 | ); 212 | return accessToken; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/iam/authentication/decorators/auth.decorator.ts: -------------------------------------------------------------------------------- 1 | import { AuthType } from '../enums/auth-type.enum'; 2 | import { SetMetadata } from '@nestjs/common'; 3 | 4 | export const AUTH_TYPE_KEY='authType'; 5 | export const Auth = (...authTypes: AuthType[]) => 6 | SetMetadata(AUTH_TYPE_KEY, authTypes); -------------------------------------------------------------------------------- /src/iam/authentication/dto/google-token.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | 3 | export class GoogleTokenDto { 4 | @IsNotEmpty() 5 | token: string; 6 | } -------------------------------------------------------------------------------- /src/iam/authentication/dto/sign-in.dto/sign-in.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNumberString, IsOptional, MinLength } from 'class-validator'; 2 | import { TUserFeatures, TUserJsonSettings } from '../../../../users/enums/user.settings'; 3 | 4 | export class SignInDto { 5 | @IsEmail() 6 | email: string; 7 | 8 | @MinLength(10) 9 | password: string; 10 | 11 | @IsOptional() 12 | @IsNumberString() 13 | tfaCode?: string; 14 | } 15 | 16 | export interface SignInResponse { 17 | accessToken: string; 18 | refreshToken: string; 19 | id: string; 20 | email: string; 21 | features: TUserFeatures; 22 | avatar: string; 23 | jsonSettings: TUserJsonSettings; 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/iam/authentication/dto/sign-up.dto/sign-up.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsOptional, MinLength } from 'class-validator'; 2 | import { TUserFeatures, TUserJsonSettings } from '../../../../users/enums/user.settings'; 3 | 4 | export class SignUpDto { 5 | @IsEmail() 6 | email: string; 7 | 8 | @MinLength(10) 9 | password: string; 10 | 11 | @IsOptional() 12 | agencyName: string; 13 | } 14 | 15 | export interface SignUpResponse { 16 | accessToken: string; 17 | refreshToken: string; 18 | id: string; 19 | email: string; 20 | features: TUserFeatures; 21 | avatar: string; 22 | jsonSettings: TUserJsonSettings; 23 | } 24 | -------------------------------------------------------------------------------- /src/iam/authentication/enums/auth-type.enum.ts: -------------------------------------------------------------------------------- 1 | export enum AuthType { 2 | Bearer, 3 | ApiKey, 4 | None 5 | } -------------------------------------------------------------------------------- /src/iam/authentication/guards/access-token/access-token.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { AccessTokenGuard } from './access-token.guard'; 2 | 3 | describe('AccessTokenGuard', () => { 4 | it('should be defined', () => { 5 | expect(new AccessTokenGuard()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/iam/authentication/guards/access-token/access-token.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | import jwtConfig from '../../../config/jwt.config' 4 | import { JwtService } from '@nestjs/jwt'; 5 | import { ConfigType } from '@nestjs/config'; 6 | import { Request } from 'express'; 7 | import { REQUEST_USER_KEY } from '../../../iam.constants'; 8 | 9 | @Injectable() 10 | export class AccessTokenGuard implements CanActivate { 11 | constructor( 12 | private readonly jwtService: JwtService, 13 | @Inject(jwtConfig.KEY) 14 | private readonly jwtConfiguration: ConfigType, 15 | ) {} 16 | 17 | async canActivate(context: ExecutionContext): Promise { 18 | const request = context.switchToHttp().getRequest(); 19 | const token = this.extractTokenFromHeader(request); 20 | if (!token) { 21 | throw new UnauthorizedException(); 22 | } 23 | try { 24 | const payload = await this.jwtService.verifyAsync( 25 | token, 26 | this.jwtConfiguration, 27 | ) 28 | request[REQUEST_USER_KEY] = payload; 29 | } catch { 30 | throw new UnauthorizedException(); 31 | } 32 | return true; 33 | } 34 | 35 | private extractTokenFromHeader(request: Request): string | undefined { 36 | const [type, token] = request.headers.authorization?.split(' ') ?? []; 37 | return type === 'Bearer' ? token : undefined; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/iam/authentication/guards/api-key.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { Request } from 'express'; 3 | import { ApiKeysService } from '../api-key.service'; 4 | import { ApiKey } from '../../../users/api-keys/entities/api-key.entity/api-key.entity'; 5 | import { Repository } from 'typeorm'; 6 | import { REQUEST_USER_KEY } from '../../iam.constants'; 7 | import { ActiveUserData } from '../../interfaces/active-user-data.interface'; 8 | import { InjectRepository } from '@nestjs/typeorm'; 9 | 10 | @Injectable() 11 | export class ApiKeyGuard implements CanActivate { 12 | constructor( 13 | private readonly apiKeyService: ApiKeysService, 14 | @InjectRepository(ApiKey) 15 | private readonly apiKeysRepository: Repository, 16 | ) {} 17 | 18 | async canActivate( context: ExecutionContext ): Promise { 19 | const request = context.switchToHttp().getRequest(); 20 | const apiKey = this.extractKeyFromHeader(request) 21 | 22 | if (!apiKey) { 23 | throw new UnauthorizedException(); 24 | } 25 | const apiKeyEntityId = this.apiKeyService.extractIdFromApiKey(apiKey); 26 | try { 27 | const apiKeyEntity = await this.apiKeysRepository.findOne({ 28 | where: { uuid: apiKeyEntityId }, 29 | relations: { user: true } 30 | }) 31 | await this.apiKeyService.validate(apiKey, apiKeyEntity.key); 32 | request[REQUEST_USER_KEY] = { 33 | sub: apiKeyEntity.user.id, 34 | email: apiKeyEntity.user.email, 35 | role: apiKeyEntity.user.role, 36 | permissions: apiKeyEntity.user.permissions, 37 | } as ActiveUserData; 38 | } catch { 39 | throw new UnauthorizedException(); 40 | } 41 | return true; 42 | } 43 | 44 | private extractKeyFromHeader(request: Request): string | undefined { 45 | const [type, key] = request.headers.authorization?.split(' ') ?? []; 46 | return type === 'ApiKey' ? key : undefined; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/iam/authentication/guards/authentication/authentication.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticationGuard } from './authentication.guard'; 2 | 3 | describe('AuthenticationGuard', () => { 4 | it('should be defined', () => { 5 | expect(new AuthenticationGuard()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/iam/authentication/guards/authentication/authentication.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | import { AuthType } from '../../enums/auth-type.enum'; 4 | import { AccessTokenGuard } from '../access-token/access-token.guard'; 5 | import { Reflector } from '@nestjs/core'; 6 | import { AUTH_TYPE_KEY } from '../../decorators/auth.decorator'; 7 | import { ApiKeyGuard } from '../api-key.guard'; 8 | 9 | @Injectable() 10 | export class AuthenticationGuard implements CanActivate { 11 | private static readonly defaultAuthType = AuthType.Bearer; 12 | // private readonly authTypeGuardMap = Record< 13 | // AuthType, 14 | // CanActivate | CanActivate[] 15 | // > = { 16 | private readonly authTypeGuardMap: { 17 | [key in AuthType]: CanActivate | CanActivate[]; 18 | } = { 19 | [AuthType.Bearer]: this.accessTokenGuard, 20 | [AuthType.ApiKey]: this.apiKeyGuard, 21 | [AuthType.None]: { canActivate: () => true }, 22 | } 23 | 24 | constructor( 25 | private readonly reflector: Reflector, 26 | private readonly accessTokenGuard: AccessTokenGuard, 27 | private readonly apiKeyGuard: ApiKeyGuard, 28 | ) {} 29 | 30 | 31 | async canActivate(context: ExecutionContext): Promise { 32 | let authTypes = this.reflector.getAllAndOverride( 33 | AUTH_TYPE_KEY, 34 | [context.getHandler(), context.getClass()], 35 | ) ?? [AuthenticationGuard.defaultAuthType]; 36 | const guards = authTypes.map((type) => this.authTypeGuardMap[type]).flat(); 37 | let error = new UnauthorizedException(); 38 | 39 | for (const instance of guards) { 40 | const canActivate = await Promise.resolve( 41 | instance.canActivate(context), 42 | ).catch((err) => { 43 | error = err; 44 | }) 45 | 46 | // TODO: not activated for Auth type: apiKeys 47 | if (canActivate) { 48 | return true; 49 | } 50 | throw error; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/iam/authentication/opt-authentication/opt-authentication.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { OptAuthenticationService } from './opt-authentication.service'; 3 | 4 | describe('OptAuthenticationService', () => { 5 | let service: OptAuthenticationService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [OptAuthenticationService], 10 | }).compile(); 11 | 12 | service = module.get(OptAuthenticationService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/iam/authentication/opt-authentication/opt-authentication.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { Repository } from 'typeorm'; 4 | import { InjectRepository } from '@nestjs/typeorm'; 5 | import { User } from '../../../users/entities/user.entity'; 6 | import { authenticator } from 'otplib'; 7 | 8 | @Injectable() 9 | export class OtpAuthenticationService { 10 | constructor( 11 | private readonly configService: ConfigService, 12 | @InjectRepository(User) private readonly userRepository: Repository, 13 | ) { 14 | } 15 | 16 | async generateSecret(email: string) { 17 | const secret = authenticator.generateSecret(); 18 | const appName = this.configService.getOrThrow('TFA_APP_NAME'); 19 | const uri = authenticator.keyuri(email, appName, secret); 20 | return { 21 | uri, 22 | secret, 23 | }; 24 | } 25 | 26 | verifyCode(code: string, secret: string) { 27 | return authenticator.verify({ 28 | token: code, 29 | secret 30 | }) 31 | } 32 | 33 | async enableTfaForUser(email: string, secret: string) { 34 | const { id } = await this.userRepository.findOneOrFail({ 35 | where: { email }, 36 | select: { id: true }, 37 | }); 38 | await this.userRepository.update( 39 | { id }, 40 | // TIP: Ideally, we would want to encrypt the "secret", instead of 41 | //storing it in a plaintext. Note -we couldn't use hashing here as 42 | // the original secret is required to verify the user's provided code.I 43 | { tfaSecret: secret, isTfaEnabled: true }, 44 | ); 45 | } 46 | } -------------------------------------------------------------------------------- /src/iam/authentication/refresh-token-ids.storage/refresh-token-ids.storage.spec.ts: -------------------------------------------------------------------------------- 1 | import { RefreshTokenIdsStorage } from './refresh-token-ids.storage'; 2 | 3 | describe('RefreshTokenIdsStorage', () => { 4 | it('should be defined', () => { 5 | expect(new RefreshTokenIdsStorage()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/iam/authentication/refresh-token-ids.storage/refresh-token-ids.storage.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnApplicationBootstrap, OnApplicationShutdown } from '@nestjs/common'; 2 | import Redis from 'ioredis'; 3 | 4 | export class InvalidatedRefreshTokenError extends Error {} 5 | 6 | @Injectable() 7 | export class RefreshTokenIdsStorage 8 | implements OnApplicationBootstrap, OnApplicationShutdown 9 | { 10 | private redisClient: Redis; 11 | 12 | onApplicationBootstrap() { 13 | // TODO: Move this to the dedicated "RedisModule" 14 | this.redisClient = new Redis({ 15 | host: 'localhost', // NOTE: Use env variable here 16 | port: 6379 17 | // password + name 18 | }) 19 | } 20 | 21 | onApplicationShutdown(signal?: string) { 22 | return this.redisClient.quit(); 23 | } 24 | 25 | async insert(userId: string, tokenId: string): Promise { 26 | await this.redisClient.set(this.getKey(userId), tokenId); 27 | } 28 | 29 | async validate(userId: string, tokenId: string): Promise { 30 | const storeId = await this.redisClient.get(this.getKey(userId)); 31 | if (storeId !== tokenId) { 32 | throw new InvalidatedRefreshTokenError(); 33 | } 34 | return storeId === tokenId; 35 | } 36 | 37 | async invalidate(userId: string): Promise { 38 | await this.redisClient.del(this.getKey(userId)); 39 | } 40 | 41 | private getKey(userId: string): string { 42 | return `user-${userId}` 43 | } 44 | } -------------------------------------------------------------------------------- /src/iam/authentication/social/google-authentication.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { GoogleAuthenticationController } from './google-authentication.controller'; 3 | 4 | describe('GoogleAuthenticationController', () => { 5 | let controller: GoogleAuthenticationController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [GoogleAuthenticationController], 10 | }).compile(); 11 | 12 | controller = module.get(GoogleAuthenticationController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/iam/authentication/social/google-authentication.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post } from '@nestjs/common'; 2 | import { GoogleAuthenticationService } from './google-authentication.service'; 3 | import { GoogleTokenDto } from '../dto/google-token.dto'; 4 | import { AuthType } from '../enums/auth-type.enum'; 5 | import { Auth } from '../decorators/auth.decorator'; 6 | 7 | @Auth(AuthType.None) 8 | @Controller('authentication/google') 9 | export class GoogleAuthenticationController { 10 | constructor( 11 | private readonly googleAuthService: GoogleAuthenticationService, 12 | ) {} 13 | 14 | @Post() 15 | authenticate(@Body() tokenDto: GoogleTokenDto) { 16 | return this.googleAuthService.authenticate(tokenDto.token); 17 | } 18 | } -------------------------------------------------------------------------------- /src/iam/authentication/social/google-authentication.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { GoogleAuthenticationService } from './google-authentication.service'; 3 | 4 | describe('GoogleAuthenticationService', () => { 5 | let service: GoogleAuthenticationService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [GoogleAuthenticationService], 10 | }).compile(); 11 | 12 | service = module.get(GoogleAuthenticationService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/iam/authentication/social/google-authentication.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ConflictException, 3 | Injectable, 4 | UnauthorizedException, 5 | } from '@nestjs/common'; 6 | import { ConfigService } from '@nestjs/config'; 7 | import { OAuth2Client } from 'google-auth-library'; 8 | import { AuthenticationService } from '../authentication.service'; 9 | import { InjectRepository } from '@nestjs/typeorm'; 10 | import { Repository } from 'typeorm'; 11 | import { User } from 'src/users/entities/user.entity'; 12 | 13 | @Injectable() 14 | export class GoogleAuthenticationService { 15 | private oauthClient: OAuth2Client; 16 | 17 | constructor( 18 | private readonly configService: ConfigService, 19 | private readonly authService: AuthenticationService, 20 | @InjectRepository(User) private readonly userRepository: Repository, 21 | ) {} 22 | 23 | onModuleInit() { 24 | const clientId = this.configService.get('GOOGLE_CLIENT_ID'); 25 | const clientSecret = this.configService.get('GOOGLE_CLIENT_SECRET'); 26 | this.oauthClient = new OAuth2Client(clientId, clientSecret); 27 | } 28 | 29 | async authenticate(token: string) { 30 | try { 31 | const loginTicket = await this.oauthClient.verifyIdToken({ 32 | idToken: token, 33 | }); 34 | const { email, sub: googleId } = loginTicket.getPayload(); 35 | const user = await this.userRepository.findOneBy({ googleId }); 36 | if (user) { 37 | return this.authService.generateTokens(user); 38 | return 'user'; 39 | } else { 40 | const newUser = await this.userRepository.save({ email, googleId }); 41 | return this.authService.generateTokens(newUser); 42 | } 43 | } catch (err) { 44 | const pgUniqueViolationErrorCode = '23505'; 45 | if (err.code === pgUniqueViolationErrorCode) { 46 | throw new ConflictException(); 47 | } 48 | throw new UnauthorizedException(); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/iam/authorization/decorators/permissions.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { PermissionType } from '../permission.type'; 3 | 4 | export const PERMISSIONS_KEY = 'permissions'; 5 | export const Permissions = (...permissions: PermissionType[]) => 6 | SetMetadata(PERMISSIONS_KEY, permissions); -------------------------------------------------------------------------------- /src/iam/authorization/decorators/policies.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { Policy } from '../policies/interfaces/policy.interface'; 3 | 4 | export const POLICIES_KEY = 'policies'; 5 | export const Policies = (...policies: Policy []) => 6 | SetMetadata (POLICIES_KEY, policies); -------------------------------------------------------------------------------- /src/iam/authorization/decorators/roles.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Role } from '../../../users/enums/role.enum'; 2 | import { SetMetadata } from '@nestjs/common'; 3 | 4 | export const ROLES_KEY = 'roles'; 5 | export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles); -------------------------------------------------------------------------------- /src/iam/authorization/guards/permissions.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { PermissionsGuard } from './permissions.guard'; 2 | 3 | describe('RolesGuard', () => { 4 | it('should be defined', () => { 5 | expect(new PermissionsGuard()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/iam/authorization/guards/permissions.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | import { Reflector } from '@nestjs/core'; 4 | import { Role } from '../../../users/enums/role.enum'; 5 | import { PERMISSIONS_KEY } from '../decorators/permissions.decorator' 6 | import { ROLES_KEY } from '../decorators/roles.decorator'; 7 | import { ActiveUserData } from '../../interfaces/active-user-data.interface'; 8 | import { REQUEST_USER_KEY } from '../../iam.constants'; 9 | import { PermissionType } from '../permission.type'; 10 | 11 | @Injectable() 12 | export class PermissionsGuard implements CanActivate { 13 | constructor(private readonly reflector: Reflector) {} 14 | 15 | canActivate( 16 | context: ExecutionContext, 17 | ): boolean | Promise | Observable { 18 | const contextPermissions = this.reflector.getAllAndOverride< 19 | PermissionType[] 20 | >(PERMISSIONS_KEY, [context.getHandler(), context.getClass()]); 21 | if (!contextPermissions) { 22 | return true; 23 | } 24 | const user: ActiveUserData = context.switchToHttp().getRequest()[ 25 | REQUEST_USER_KEY 26 | ]; 27 | 28 | return contextPermissions.every((permission) => { 29 | user.permissions?.includes(permission) 30 | } 31 | ); 32 | } 33 | } -------------------------------------------------------------------------------- /src/iam/authorization/guards/policies.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, ForbiddenException, Injectable, Type } from '@nestjs/common'; 2 | import { Reflector } from '@nestjs/core'; 3 | import { PolicyHandlerStorage } from '../policies/policy-handlers.storage'; 4 | import { Policy } from '../policies/interfaces/policy.interface'; 5 | import { POLICIES_KEY } from '../decorators/policies.decorator'; 6 | import { REQUEST_USER_KEY } from '../../iam.constants'; 7 | import { ActiveUserData } from '../../interfaces/active-user-data.interface'; 8 | 9 | @Injectable() 10 | export class PoliciesGuard implements CanActivate { 11 | constructor ( 12 | private readonly reflector: Reflector, 13 | private readonly policyHandlerStorage: PolicyHandlerStorage, 14 | ) {} 15 | 16 | async canActivate(context: ExecutionContext): Promise { 17 | const policies = this.reflector.getAllAndOverride(POLICIES_KEY, [ 18 | context.getHandler(), 19 | context.getClass(), 20 | ]); 21 | if (policies) { 22 | const user: ActiveUserData = context.switchToHttp().getRequest()[ 23 | REQUEST_USER_KEY 24 | ]; 25 | await Promise.all( 26 | policies.map((policy) => { 27 | const policyHandler = this.policyHandlerStorage.get( 28 | policy.constructor as Type, 29 | ); 30 | return policyHandler.handle(policy, user); 31 | }), 32 | ).catch((err) => { 33 | throw new ForbiddenException(err.message); 34 | }) 35 | } 36 | return true; 37 | } 38 | } -------------------------------------------------------------------------------- /src/iam/authorization/guards/roles/roles.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { RolesGuard } from './roles.guard'; 2 | 3 | describe('RolesGuard', () => { 4 | it('should be defined', () => { 5 | expect(new RolesGuard()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/iam/authorization/guards/roles/roles.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | import { Reflector } from '@nestjs/core'; 4 | import { Role } from '../../../../users/enums/role.enum'; 5 | import { ROLES_KEY } from '../../decorators/roles.decorator'; 6 | import { ActiveUserData } from '../../../interfaces/active-user-data.interface'; 7 | import { REQUEST_USER_KEY } from '../../../iam.constants'; 8 | 9 | @Injectable() 10 | export class RolesGuard implements CanActivate { 11 | constructor(private readonly reflector: Reflector) {} 12 | 13 | canActivate( 14 | context: ExecutionContext 15 | ): boolean | Promise | Observable { 16 | const contextRoles = this.reflector.getAllAndOverride(ROLES_KEY, [ 17 | context.getHandler(), 18 | context.getClass(), 19 | ]); 20 | if (!contextRoles) { 21 | return true; 22 | } 23 | const user: ActiveUserData = context.switchToHttp().getRequest()[ 24 | REQUEST_USER_KEY 25 | ]; 26 | return contextRoles.some((role) => user.role === role); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/iam/authorization/permission.type.ts: -------------------------------------------------------------------------------- 1 | import { CoffeesPermission } from '../../coffees/coffees.permission'; 2 | 3 | export const Permission = { 4 | ...CoffeesPermission, 5 | } 6 | 7 | export type PermissionType = CoffeesPermission; // | ...other permission enums -------------------------------------------------------------------------------- /src/iam/authorization/policies/framework-contributor.policy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Policy } from './interfaces/policy.interface'; 3 | import { PolicyHandler } from './interfaces/policy-handler.interfaces'; 4 | import { PolicyHandlerStorage } from './policy-handlers.storage'; 5 | import { ActiveUserData } from '../../interfaces/active-user-data.interface'; 6 | 7 | @Injectable() 8 | export class FrameworkContributorPolicy implements Policy { 9 | name = 'FrameworkContributor'; 10 | } 11 | @Injectable() 12 | export class FrameworkContributorPolicyHandler 13 | implements PolicyHandler 14 | { 15 | constructor (private readonly policyHandlerStorage: PolicyHandlerStorage) { 16 | this.policyHandlerStorage.add(FrameworkContributorPolicy, this); 17 | } 18 | 19 | async handle( 20 | policy: FrameworkContributorPolicy, 21 | user: ActiveUserData, 22 | ): Promise { 23 | // const isContributor = user.email.endsWith('@nestjs.com'); 24 | const isContributor = user.email.endsWith('@trilon.io'); 25 | if (!isContributor) { 26 | throw new Error('User is not a contributor'); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/iam/authorization/policies/interfaces/policy-handler.interfaces.ts: -------------------------------------------------------------------------------- 1 | import { ActiveUserData } from '../../../interfaces/active-user-data.interface'; 2 | import { Policy } from './policy.interface'; 3 | 4 | export interface PolicyHandler { 5 | handle(policy: T, user: ActiveUserData): Promise; 6 | } -------------------------------------------------------------------------------- /src/iam/authorization/policies/interfaces/policy.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Policy { 2 | name: string; 3 | } -------------------------------------------------------------------------------- /src/iam/authorization/policies/policy-handlers.storage.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Type } from '@nestjs/common' 2 | import { PolicyHandler } from './interfaces/policy-handler.interfaces'; 3 | import { Policy } from './interfaces/policy.interface' 4 | 5 | @Injectable() 6 | export class PolicyHandlerStorage { 7 | private readonly collection = new Map, PolicyHandler> (); 8 | 9 | add (policyCls: Type, handler: PolicyHandler) { 10 | this.collection.set(policyCls, handler); 11 | } 12 | 13 | get(policyCls: Type): PolicyHandler | undefined { 14 | const handler = this.collection.get(policyCls); 15 | if (!handler) { 16 | throw new Error( 17 | `"${policyCls.name}" does not have the associated handler.`, 18 | ); 19 | } 20 | return handler; 21 | } 22 | } -------------------------------------------------------------------------------- /src/iam/config/jwt.config.ts: -------------------------------------------------------------------------------- 1 | import { registerAs } from '@nestjs/config'; 2 | 3 | export default registerAs('jwt', () => { 4 | return{ 5 | secret: process.env.JWT_SECRET, 6 | audience: process.env.JWT_TOKEN_AUDIENCE, 7 | issuer: process.env.JWT_TOKEN_ISSUER, 8 | accessTokenTtl: parseInt(process.env.JWT_ACCESS_TOKEN_TTL ?? '3600', 10), 9 | refreshTokenTtl: parseInt(process.env.JWT_REFRESH_TOKEN_TTL ?? '86400', 10), 10 | } 11 | }) -------------------------------------------------------------------------------- /src/iam/decorators/active-user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | import { REQUEST_USER_KEY } from '../iam.constants'; 3 | import { ActiveUserData } from '../interfaces/active-user-data.interface'; 4 | 5 | export const ActiveUser = createParamDecorator( 6 | (field: keyof ActiveUserData | undefined, ctx: ExecutionContext) => { 7 | const request = ctx.switchToHttp().getRequest(); 8 | const user: ActiveUserData | undefined = request[REQUEST_USER_KEY]; 9 | return field ? user?.[field] : user; 10 | } 11 | ) -------------------------------------------------------------------------------- /src/iam/hashing/bcrypt.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { BcryptService } from './bcrypt.service'; 3 | 4 | describe('BcryptService', () => { 5 | let service: BcryptService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [BcryptService], 10 | }).compile(); 11 | 12 | service = module.get(BcryptService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/iam/hashing/bcrypt.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { compare, genSalt, hash } from 'bcrypt'; 3 | import { HashingService } from './hashing.service'; 4 | 5 | @Injectable() 6 | export class BcryptService implements HashingService { 7 | async hash(data: string | Buffer): Promise { 8 | const salt = await genSalt(); 9 | return hash(data, salt); 10 | } 11 | 12 | async compare(data: string | Buffer, encrypted: string): Promise { 13 | return compare(data, encrypted); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/iam/hashing/hashing.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { HashingService } from './hashing.service'; 3 | 4 | describe('HashingService', () => { 5 | let service: HashingService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [HashingService], 10 | }).compile(); 11 | 12 | service = module.get(HashingService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/iam/hashing/hashing.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export abstract class HashingService { 5 | abstract hash(data: string | Buffer): Promise; 6 | abstract compare(data: string | Buffer, encrypted: string): Promise; 7 | } -------------------------------------------------------------------------------- /src/iam/iam.constants.ts: -------------------------------------------------------------------------------- 1 | export const REQUEST_USER_KEY='user'; -------------------------------------------------------------------------------- /src/iam/iam.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { HashingService } from './hashing/hashing.service'; 3 | import { BcryptService } from './hashing/bcrypt.service'; 4 | import { AuthenticationController } from './authentication/authentication.controller'; 5 | import { AuthenticationService } from './authentication/authentication.service'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { User } from '../users/entities/user.entity'; 8 | import { JwtModule } from '@nestjs/jwt'; 9 | import { ConfigModule } from '@nestjs/config'; 10 | import jwtConfig from './config/jwt.config' 11 | import { APP_GUARD } from '@nestjs/core'; 12 | import { AccessTokenGuard } from './authentication/guards/access-token/access-token.guard'; 13 | import { AuthenticationGuard } from './authentication/guards/authentication/authentication.guard'; 14 | import { RefreshTokenIdsStorage } from './authentication/refresh-token-ids.storage/refresh-token-ids.storage'; 15 | import { PermissionsGuard } from './authorization/guards/permissions.guard'; 16 | import { PoliciesGuard } from './authorization/guards/policies.guard'; 17 | import { PolicyHandlerStorage } from './authorization/policies/policy-handlers.storage'; 18 | import { 19 | FrameworkContributorPolicy, 20 | FrameworkContributorPolicyHandler, 21 | } from './authorization/policies/framework-contributor.policy'; 22 | import { RolesGuard } from './authorization/guards/roles/roles.guard'; 23 | import { ApiKeysService } from './authentication/api-key.service'; 24 | import { ApiKey } from '../users/api-keys/entities/api-key.entity/api-key.entity'; 25 | import { ApiKeyGuard } from './authentication/guards/api-key.guard'; 26 | import { GoogleAuthenticationService } from './authentication/social/google-authentication.service'; 27 | import { GoogleAuthenticationController } from './authentication/social/google-authentication.controller'; 28 | import { OtpAuthenticationService } from './authentication/opt-authentication/opt-authentication.service'; 29 | import { NotificationService } from '../notification/notification.service'; 30 | import { Notification } from '../notification/entities/notification.entity'; 31 | import { SUserNotifications } from '../notification/helpers/notification.suser'; 32 | import { AllNotifications } from '../notification/helpers/notification.all'; 33 | import { SendNotifications } from '../notification/helpers/notification.send'; 34 | import { AdminNotifications } from '../notification/helpers/notification.admin'; 35 | import { ManagerNotifications } from '../notification/helpers/notification.manager'; 36 | import { EmailService } from '../email/email.service'; 37 | import { TelegramService } from '../telegram/telegram.service'; 38 | import { Profile } from '../profile/entities/profile.entities'; 39 | import { Agency } from '../agency/entities/agency.entity'; 40 | 41 | @Module({ 42 | imports: [ 43 | TypeOrmModule.forFeature([User, ApiKey, Notification, Profile, Agency]), 44 | JwtModule.registerAsync(jwtConfig.asProvider()), 45 | ConfigModule.forFeature(jwtConfig), 46 | ], 47 | providers: [ 48 | { 49 | provide: HashingService, 50 | useClass: BcryptService 51 | }, 52 | { 53 | provide: APP_GUARD, 54 | useClass: AuthenticationGuard 55 | }, 56 | { 57 | provide: APP_GUARD, 58 | useClass: RolesGuard, // PermissionsGuard, // PoliciesGuard, 59 | }, 60 | AccessTokenGuard, 61 | ApiKeyGuard, 62 | RefreshTokenIdsStorage, 63 | AuthenticationService, 64 | ApiKeysService, 65 | GoogleAuthenticationService, 66 | OtpAuthenticationService, 67 | NotificationService, 68 | SUserNotifications, 69 | AllNotifications, 70 | SendNotifications, 71 | AdminNotifications, 72 | ManagerNotifications, 73 | EmailService, 74 | TelegramService, 75 | // PolicyHandlerStorage, 76 | // FrameworkContributorPolicyHandler, 77 | ], 78 | controllers: [AuthenticationController, GoogleAuthenticationController] 79 | }) 80 | export class IamModule {} 81 | -------------------------------------------------------------------------------- /src/iam/interfaces/active-user-data.interface.ts: -------------------------------------------------------------------------------- 1 | import { Role } from '../../users/enums/role.enum'; 2 | import { PermissionType } from '../authorization/permission.type'; 3 | 4 | export interface ActiveUserData { 5 | /** 6 | * The "subject" of the token. The value of this property it the user ID 7 | * that granted this token. 8 | */ 9 | sub: string; 10 | 11 | /** 12 | * The subject's (user) email. 13 | */ 14 | email: string; 15 | 16 | /** 17 | * The subject's (user) role. 18 | */ 19 | role: Role; 20 | 21 | /** 22 | * The subject's (user) permissions. 23 | * NOTE: Using this approach in combination with the "role-based" approach 24 | * does not make sense. We have those two properties here ("role" and "permissions") 25 | * just to showcase two alternative approaches. 26 | */ 27 | permissions: PermissionType[]; 28 | } -------------------------------------------------------------------------------- /src/incident/entities/incident.entities.ts: -------------------------------------------------------------------------------- 1 | import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { TIncident } from '../enums/incident.enum'; 3 | 4 | @Entity() 5 | export class Incident { 6 | @PrimaryGeneratedColumn() 7 | id: string; 8 | 9 | @Column() 10 | ofId: string; 11 | 12 | @CreateDateColumn({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) 13 | created_at: Date; 14 | 15 | @Column() 16 | incident_created_at: string; 17 | 18 | @Column({unique: false}) 19 | msgId: string | null; 20 | 21 | @Column({unique: false}) 22 | stopWords: string | null; 23 | 24 | @Column({ enum: TIncident, default: TIncident.Chat }) 25 | type: TIncident; 26 | 27 | @Column() 28 | agencyId: string; 29 | 30 | @Column() 31 | managerId: string; 32 | 33 | @Column({default: false}) 34 | isCounted: boolean; 35 | 36 | @Column({default: ''}) 37 | workShift?: string; 38 | } -------------------------------------------------------------------------------- /src/incident/enums/incident.enum.ts: -------------------------------------------------------------------------------- 1 | export enum TIncident { 2 | Message , 3 | Chat , 4 | } -------------------------------------------------------------------------------- /src/incident/incident.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { IncidentController } from './incident.controller'; 3 | 4 | describe('IncidentController', () => { 5 | let controller: IncidentController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [IncidentController], 10 | }).compile(); 11 | 12 | controller = module.get(IncidentController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/incident/incident.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Param, Post, Query, Req } from '@nestjs/common'; 2 | import { Request } from 'express'; 3 | import { IncidentService } from './incident.service'; 4 | import { TIncident } from './enums/incident.enum'; 5 | 6 | @Controller('incident') 7 | export class IncidentController { 8 | constructor(private readonly incidentService: IncidentService) {} 9 | 10 | @Get() 11 | incidentController() { 12 | return 'incident controller working'; 13 | } 14 | 15 | @Get('report') 16 | findAll( 17 | @Query('startDate') startDate: string, 18 | @Query('endDate') endDate: string, 19 | @Query('ofId') ofId: string, 20 | @Query('type') type: string, 21 | @Query('managerId') managerId: string, 22 | ) { 23 | const parsedStartDate = startDate ? new Date(startDate) : null; 24 | const parsedEndDate = endDate ? new Date(endDate) : null; 25 | 26 | return this.incidentService.findAll(ofId, managerId, type as unknown as TIncident, parsedStartDate, parsedEndDate); 27 | } 28 | 29 | @Post() 30 | create(@Req() request: Request, @Body() createIncident: any) { 31 | const tokenParts = request.headers.authorization.split(' '); 32 | 33 | return this.incidentService.createIncident(tokenParts[1], createIncident); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/incident/incident.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { IncidentService } from './incident.service'; 3 | import { IncidentController } from './incident.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Incident } from './entities/incident.entities'; 6 | import { User } from '../users/entities/user.entity'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([Incident, User])], 10 | providers: [IncidentService], 11 | controllers: [IncidentController] 12 | }) 13 | export class IncidentModule {} 14 | -------------------------------------------------------------------------------- /src/incident/incident.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { IncidentService } from './incident.service'; 3 | 4 | describe('IncidentService', () => { 5 | let service: IncidentService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [IncidentService], 10 | }).compile(); 11 | 12 | service = module.get(IncidentService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/incident/incident.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { Incident } from './entities/incident.entities'; 5 | import { TIncident } from './enums/incident.enum'; 6 | import * as jwt from 'jsonwebtoken'; 7 | import { User } from 'src/users/entities/user.entity'; 8 | 9 | function getTypeByTime(incidentCreatedAt: any) { 10 | const createdAtDate = new Date(incidentCreatedAt); 11 | const hours = createdAtDate.getHours(); 12 | 13 | if (hours >= 0 && hours < 8) { 14 | return '3'; 15 | } else if (hours >= 8 && hours < 16) { 16 | return '1'; 17 | } else { 18 | return '2'; 19 | } 20 | } 21 | 22 | 23 | @Injectable() 24 | export class IncidentService { 25 | @InjectRepository(Incident) private readonly incidentsRepository: Repository 26 | @InjectRepository(User) private readonly usersRepository: Repository 27 | 28 | async findAll(ofId: string, managerId: string, type: TIncident, startDate: Date, endDate: Date) { 29 | const findObj = {} 30 | if (ofId) { findObj['ofId'] = ofId } 31 | if (managerId) { findObj['managerId'] = managerId } 32 | if (type) { findObj['type'] = type } 33 | 34 | const incidents: Incident[] = await this.incidentsRepository.findBy(findObj); 35 | 36 | incidents.forEach((incident) => { 37 | incident.workShift = getTypeByTime(incident.incident_created_at); 38 | }); 39 | 40 | if (startDate === null || endDate === null) { 41 | return incidents; 42 | } 43 | 44 | const filteredIncidents: Incident[] = incidents.filter((incident) => { 45 | // const incidentsCreatedAt = new Date(incident.created_at); 46 | const incidentsCreatedAt = new Date(incident.incident_created_at); 47 | let startUtc = new Date(startDate); 48 | let endUtc = new Date(endDate); 49 | 50 | startUtc.setHours(startUtc.getHours()); 51 | endUtc.setHours(endUtc.getHours()); 52 | 53 | return ( 54 | incidentsCreatedAt >= startUtc && 55 | incidentsCreatedAt <= endUtc 56 | ); 57 | }); 58 | 59 | return filteredIncidents 60 | } 61 | 62 | async createIncident(token: string, data: any) { 63 | const refreshTokenData = jwt.decode(token) as { email: string }; 64 | const email = refreshTokenData.email; 65 | const user = await this.usersRepository.findOneBy({ 66 | email: email, 67 | }) 68 | 69 | const newIncident = new Incident(); 70 | newIncident.type = TIncident.Chat; 71 | newIncident.managerId = user.id; 72 | newIncident.agencyId = user.agencyId; 73 | newIncident.msgId = 'msgId'; 74 | newIncident.incident_created_at = new Date(Date.now()).toISOString(); 75 | newIncident.isCounted = true; 76 | // 77 | newIncident.stopWords = data.stopWords; 78 | newIncident.ofId = data.ofId; 79 | 80 | return await this.incidentsRepository.save(newIncident) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { ValidationPipe } from '@nestjs/common'; 4 | import * as cookieParser from 'cookie-parser'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | app.useGlobalPipes(new ValidationPipe()); 9 | app.enableCors(); 10 | app.use(cookieParser()); 11 | await app.listen(3000); 12 | } 13 | bootstrap(); 14 | -------------------------------------------------------------------------------- /src/messages/dto/create-message.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | 3 | export class CreateMessageDto { 4 | @IsNotEmpty() 5 | msg_created_at: string; 6 | 7 | @IsNotEmpty() 8 | msgId: string; 9 | 10 | @IsNotEmpty() 11 | chatId: string; 12 | 13 | @IsNotEmpty() 14 | fromUserId: string; 15 | 16 | @IsNotEmpty() 17 | text: string; 18 | 19 | @IsNotEmpty() 20 | agencyId: string; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/messages/dto/update-message.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | 3 | export class UpdateMessageDto { 4 | @IsNotEmpty() 5 | msg_created_at: string; 6 | 7 | @IsNotEmpty() 8 | msgId: string; 9 | 10 | @IsNotEmpty() 11 | chatId: string; 12 | 13 | @IsNotEmpty() 14 | fromUserId: string; 15 | 16 | @IsNotEmpty() 17 | text: string; 18 | 19 | @IsNotEmpty() 20 | isRead: boolean; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/messages/entities/message.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class Message { 5 | @PrimaryGeneratedColumn() 6 | id: string; 7 | 8 | // @Column({ type: 'date' }) 9 | // createdAt: string; 10 | // @Column({ type: 'timestamptz' }) 11 | // date_time_with_timezone: Date; 12 | 13 | @Column() 14 | ofId: string; 15 | 16 | @CreateDateColumn() 17 | created_at: Date; 18 | 19 | @Column() 20 | msg_created_at: string; 21 | 22 | // Onlyf 23 | @Column({unique: true}) 24 | msgId: string; 25 | 26 | // Onlyf 27 | @Column() 28 | fromUserId: string; 29 | 30 | // Onlyf 31 | @Column() 32 | chatId: string; 33 | 34 | @Column() 35 | managerId: string; 36 | 37 | @Column() 38 | agencyId: string; 39 | 40 | @Column({ nullable: true }) 41 | text: string; 42 | 43 | @Column({default: false}) 44 | isCounted: boolean; 45 | 46 | @Column({default: false}) 47 | isRead: boolean; 48 | } -------------------------------------------------------------------------------- /src/messages/enums/messages.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Status { 2 | Сounted , 3 | UnCounted , 4 | } -------------------------------------------------------------------------------- /src/messages/messages.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { MessagesController } from './messages.controller'; 3 | 4 | describe('MessagesController', () => { 5 | let controller: MessagesController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [MessagesController], 10 | }).compile(); 11 | 12 | controller = module.get(MessagesController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/messages/messages.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Delete, Get, Param, Patch, Post, Query, Req } from '@nestjs/common'; 2 | import { MessagesService } from './messages.service'; 3 | import { Auth } from '../iam/authentication/decorators/auth.decorator'; 4 | import { AuthType } from '../iam/authentication/enums/auth-type.enum'; 5 | import { CreateMessageDto } from './dto/create-message.dto'; 6 | import { Roles } from '../iam/authorization/decorators/roles.decorator'; 7 | import { Role } from '../users/enums/role.enum'; 8 | import { UpdateMessageDto } from './dto/update-message.dto'; 9 | import { Request } from 'express'; 10 | 11 | // TODO: authType 12 | @Auth(AuthType.None) 13 | @Controller('messages') 14 | export class MessagesController { 15 | constructor(private readonly messageService: MessagesService) {} 16 | 17 | @Get() 18 | messageController() { 19 | return 'message controller working'; 20 | } 21 | 22 | @Post(':id') 23 | create(@Param('id') id: string, @Body() createMessageDto: Array) { 24 | return this.messageService.createMany(id, createMessageDto); 25 | } 26 | 27 | @Post(':id/one') 28 | createOne(@Param('id') id: string, @Body() createMessageDto: CreateMessageDto) { 29 | return this.messageService.createOne(id, createMessageDto); 30 | } 31 | 32 | // TODO: 33 | // Get cookies here, and find user next check role 34 | // @Roles(Role.Admin) 35 | @Get('all/:id') 36 | findAll(@Param('id') id: string) { 37 | return this.messageService.findAll(id); 38 | } 39 | 40 | @Get('all/') 41 | findAllBy(@Query('managerId') managerId: string) { 42 | return this.messageService.findAllBy(managerId); 43 | } 44 | 45 | @Get('activeDialog/:id') 46 | findActiveDialogs( 47 | @Param('id') id: string, 48 | @Query('startDate') startDate: string, 49 | @Query('endDate') endDate: string, 50 | // FromModel constraint 51 | @Query('mc') mc: string, 52 | // FromUser constraint 53 | @Query('uc') uc: string, 54 | ) { 55 | const parsedStartDate = new Date(startDate); 56 | const parsedEndDate = new Date(endDate); 57 | 58 | return this.messageService.findActiveDialog(id, parsedStartDate, parsedEndDate, Number(mc), Number(uc)); 59 | } 60 | 61 | @Get('activeDialogs/:id') 62 | findActiveDialogs1( 63 | @Param('id') id: string, 64 | @Query('mc') mc: string, 65 | @Query('uc') uc: string, 66 | @Query('tz') tz: string, 67 | ) { 68 | return this.messageService.findActiveDialogs(id, Number(mc), Number(uc), Number(tz)); 69 | } 70 | 71 | @Patch(':id') 72 | update( 73 | @Req() request: Request, 74 | @Param('id') id: string, 75 | @Body() updateMessageDto: Array 76 | ) { 77 | const tokenParts = request.headers.authorization.split(' '); 78 | 79 | return this.messageService.update(tokenParts[1], id, updateMessageDto); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/messages/messages.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MessagesController } from './messages.controller'; 3 | import { MessagesService } from './messages.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Message } from './entities/message.entity'; 6 | import { User } from '../users/entities/user.entity'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([Message, User])], 10 | controllers: [MessagesController], 11 | providers: [MessagesService] 12 | }) 13 | export class MessagesModule {} 14 | -------------------------------------------------------------------------------- /src/messages/messages.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { MessagesService } from './messages.service'; 3 | 4 | describe('MessagesService', () => { 5 | let service: MessagesService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [MessagesService], 10 | }).compile(); 11 | 12 | service = module.get(MessagesService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/messages/messages.service.ts: -------------------------------------------------------------------------------- 1 | import { ConflictException, Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { Message } from './entities/message.entity'; 5 | import { CreateMessageDto } from './dto/create-message.dto'; 6 | import { UpdateMessageDto } from './dto/update-message.dto'; 7 | import * as jwt from 'jsonwebtoken'; 8 | import { User } from '../users/entities/user.entity'; 9 | import { subHours, format } from 'date-fns'; 10 | 11 | @Injectable() 12 | export class MessagesService { 13 | constructor( 14 | @InjectRepository(Message) private readonly messagesRepository: Repository, 15 | @InjectRepository(User) private readonly usersRepository: Repository, 16 | ){} 17 | 18 | async createOne(id: string, createMessageDto: CreateMessageDto) { 19 | try { 20 | const message = new Message(); 21 | message.ofId = id; 22 | message.msg_created_at = createMessageDto.msg_created_at; 23 | message.msgId = createMessageDto.msgId; 24 | message.chatId = createMessageDto.chatId; 25 | message.fromUserId = createMessageDto.fromUserId; 26 | message.text = createMessageDto.text.substring(0, 30); 27 | 28 | await this.messagesRepository.save(message); 29 | return 'create' 30 | } catch (err) { 31 | const pgUniqueViolationErrorCode = '23505'; 32 | if (err.code === pgUniqueViolationErrorCode) { 33 | throw new ConflictException(); 34 | } 35 | throw err; 36 | } 37 | } 38 | 39 | async createMany(id: string, createMessageDto: Array) { 40 | return 'create many messages'; 41 | } 42 | 43 | async update(token: string, id: string, updateMessageDto: Array) { 44 | try { 45 | const refreshTokenData = jwt.decode(token) as { email: string }; 46 | const email = refreshTokenData.email; 47 | const user = await this.usersRepository.findOneBy({ 48 | email: email, 49 | }) 50 | if (!user.agencyId) { 51 | throw new ConflictException('User have not agency!'); 52 | } 53 | 54 | for (const message of updateMessageDto) { 55 | const existingMessage = await this.messagesRepository.findOneBy({ 56 | msgId: message.msgId, 57 | ofId: id, 58 | }); 59 | 60 | if (!existingMessage) { 61 | const newMessage = new Message(); 62 | newMessage.ofId = id; 63 | newMessage.msg_created_at = message.msg_created_at; 64 | newMessage.msgId = message.msgId; 65 | newMessage.chatId = message.chatId; 66 | newMessage.fromUserId = message.fromUserId; 67 | newMessage.agencyId = user.agencyId; 68 | newMessage.text = message.text.substring(0, 30); 69 | newMessage.isRead = message.isRead 70 | newMessage.managerId = user.id; 71 | 72 | await this.messagesRepository.save(newMessage); 73 | } else { 74 | existingMessage.text = message.text.substring(0, 30); 75 | existingMessage.isRead = message.isRead; 76 | // TODO: remove. For testing 77 | existingMessage.msg_created_at = message.msg_created_at; 78 | existingMessage.fromUserId = message.fromUserId; 79 | 80 | await this.messagesRepository.save(existingMessage); 81 | } 82 | } 83 | } catch (err) { 84 | throw err; 85 | } 86 | return 'update successfully'; 87 | } 88 | 89 | async findAll(id: string) { 90 | const existingMessage = await this.messagesRepository.findBy({ 91 | ofId: id, 92 | }); 93 | 94 | return existingMessage; 95 | } 96 | 97 | async findAllBy(managerId: string) { 98 | const findObj = {} 99 | if (managerId) { findObj['managerId'] = managerId } 100 | 101 | const existingMessage = await this.messagesRepository.findBy(findObj); 102 | 103 | return existingMessage; 104 | } 105 | 106 | async findActiveDialog(id: string, startDate: Date, endDate: Date, mc: number, uc: number): Promise<{ totalFromModel: number, totalFromUser: number, totalActive: number, chats: any[] }> { 107 | const existingMessages: Message[] = await this.messagesRepository.findBy({ 108 | ofId: id, 109 | }); 110 | 111 | const filteredMessages: Message[] = existingMessages.filter((message) => { 112 | const msgCreatedAt = new Date(message.msg_created_at); 113 | return msgCreatedAt >= startDate && msgCreatedAt <= endDate; 114 | }); 115 | 116 | const groupedMessages: Record = filteredMessages.reduce((result, message) => { 117 | if (!result[message.chatId]) { 118 | result[message.chatId] = { 119 | chatId: message.chatId, 120 | fromModel: 0, 121 | fromUser: 0, 122 | isActive: false 123 | }; 124 | } 125 | 126 | 127 | if (message.fromUserId === id) { 128 | result[message.chatId].fromModel++; 129 | } else { 130 | result[message.chatId].fromUser++; 131 | } 132 | 133 | if (result[message.chatId].fromModel >= mc && result[message.chatId].fromUser >= uc) { 134 | result[message.chatId].isActive = true; 135 | } 136 | 137 | return result; 138 | }, {}); 139 | 140 | const chats: { chatId: string, fromModel: number, fromUser: number, isActive: boolean }[] = Object.values(groupedMessages); 141 | 142 | const sumObject = chats.reduce((result, chat) => { 143 | result.totalFromModel = (result.totalFromModel || 0) + chat.fromModel; 144 | result.totalFromUser = (result.totalFromUser || 0) + chat.fromUser; 145 | result.totalActive = chat.isActive ? result.totalActive + 1 : result.totalActive; 146 | return result; 147 | }, { totalFromModel: 0, totalFromUser: 0, totalActive: 0 }); 148 | 149 | return { 150 | totalFromModel: sumObject.totalFromModel, 151 | totalFromUser: sumObject.totalFromUser, 152 | totalActive: sumObject.totalActive, 153 | chats, 154 | }; 155 | } 156 | 157 | async findActiveDialogs(id: string, mc: number, uc: number, tz: number): Promise<{ date: string, dayOfYear: string, type: number, totalFromModel: number, totalFromUser: number, totalActive: number, chats: any[] }[]> { 158 | const existingMessages: Message[] = await this.messagesRepository.findBy({ 159 | ofId: id, 160 | }); 161 | 162 | const filteredMessages: Message[] = existingMessages; 163 | 164 | const groupedMessages: Record = filteredMessages.reduce((result, message) => { 165 | const messageDateInit = new Date(message.msg_created_at); 166 | const messageDate = subHours(messageDateInit, tz); 167 | 168 | const type = Math.ceil((messageDate.getHours() + 1) / 8); 169 | 170 | const startOfYear = new Date(messageDate.getFullYear(), 0, 1); 171 | const diffInDays = Math.floor((messageDate.getTime() - startOfYear.getTime()) / (24 * 60 * 60 * 1000)) + 1; 172 | const dayOfYear = diffInDays < 10 ? '0' + diffInDays : diffInDays.toString(); 173 | 174 | if (!result[messageDate.toISOString().split('T')[0]]) { 175 | result[messageDate.toISOString().split('T')[0]] = []; 176 | } 177 | 178 | const existingChat = result[messageDate.toISOString().split('T')[0]].find(chat => chat.chatId === message.chatId); 179 | 180 | if (!existingChat) { 181 | const newChat = { 182 | type, 183 | chatId: message.chatId, 184 | fromModel: 0, 185 | fromUser: 0, 186 | isActive: false, 187 | }; 188 | result[messageDate.toISOString().split('T')[0]].push({ dayOfYear, ...newChat }); 189 | } 190 | 191 | const chatToUpdate = result[messageDate.toISOString().split('T')[0]].find(chat => chat.chatId === message.chatId); 192 | 193 | chatToUpdate.dayOfYear = Number(dayOfYear); 194 | 195 | if (message.fromUserId === id) { 196 | chatToUpdate.fromModel++; 197 | } else { 198 | chatToUpdate.fromUser++; 199 | } 200 | 201 | if (chatToUpdate.fromModel >= mc && chatToUpdate.fromUser >= uc) { 202 | chatToUpdate.isActive = true; 203 | } 204 | 205 | return result; 206 | }, {}); 207 | 208 | const resultArray: { date: string, dayOfYear: string, type: number, totalFromModel: number, totalFromUser: number, totalActive: number, chats: any[] }[] = []; 209 | 210 | for (const [date, chats] of Object.entries(groupedMessages)) { 211 | const sumObject = chats.reduce((result, chat) => { 212 | result.totalFromModel = (result.totalFromModel || 0) + chat.fromModel; 213 | result.totalFromUser = (result.totalFromUser || 0) + chat.fromUser; 214 | result.totalActive = chat.isActive ? result.totalActive + 1 : result.totalActive; 215 | return result; 216 | }, { totalFromModel: 0, totalFromUser: 0, totalActive: 0 }); 217 | 218 | resultArray.push({ 219 | date, 220 | dayOfYear: chats[0].dayOfYear, 221 | type: chats[0].type, 222 | totalFromModel: sumObject.totalFromModel, 223 | totalFromUser: sumObject.totalFromUser, 224 | totalActive: sumObject.totalActive, 225 | chats, 226 | }); 227 | } 228 | 229 | return resultArray; 230 | } 231 | 232 | } 233 | -------------------------------------------------------------------------------- /src/notification/dto/get-count.dto.ts: -------------------------------------------------------------------------------- 1 | import { Notification } from '../entities/notification.entity'; 2 | 3 | export type GetNotificationsCountResponseData = { 4 | count: number; 5 | }; 6 | 7 | export type GetNotificationsResponseData = { 8 | notifications: Array 9 | }; 10 | -------------------------------------------------------------------------------- /src/notification/dto/update-notifications.dto.ts: -------------------------------------------------------------------------------- 1 | import { NotificationsSettings, NotificationsSource, NotificationsType } from '../../users/enums/user.settings'; 2 | 3 | export type UpdateNotificationsRequestItem = { 4 | type: NotificationsType, 5 | source: NotificationsSource, 6 | value: boolean, 7 | }; 8 | 9 | export type UpdateNotificationsRequestData = Array; 10 | 11 | export type UpdateNotificationsResponseData = { 12 | notifications: NotificationsSettings, 13 | }; 14 | 15 | export type UpdateVisibleRequestData = { 16 | isVisible: boolean, 17 | }; -------------------------------------------------------------------------------- /src/notification/entities/notification.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { NotificationType } from '../enums/notification.enum'; 3 | 4 | @Entity() 5 | export class Notification { 6 | @PrimaryGeneratedColumn() 7 | id: string; 8 | 9 | // date: Date; 10 | @CreateDateColumn() 11 | created_at: Date; 12 | 13 | @Column({ enum: NotificationType, default: NotificationType.COMMON }) 14 | type: NotificationType; 15 | 16 | @Column() 17 | dstUserId: string; 18 | 19 | @Column() 20 | title: string; 21 | 22 | @Column() 23 | text: string; 24 | 25 | @Column({default: false}) 26 | isRead: boolean; 27 | 28 | @Column({default: false}) 29 | isDelete: boolean; 30 | } -------------------------------------------------------------------------------- /src/notification/enums/notification.enum.ts: -------------------------------------------------------------------------------- 1 | export enum NotificationType { 2 | COMMON = 0, 3 | EVENTS = 1, 4 | BILLING = 2, 5 | CHAT = 3, 6 | } -------------------------------------------------------------------------------- /src/notification/helpers/notification.admin.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AdminNotifications {} -------------------------------------------------------------------------------- /src/notification/helpers/notification.all.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ManagerNotifications } from './notification.manager'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { User } from '../../users/entities/user.entity'; 5 | import { Repository } from 'typeorm'; 6 | import { NotificationType } from '../enums/notification.enum'; 7 | import { SendNotifications } from './notification.send'; 8 | import { ConfigService } from '@nestjs/config'; 9 | import { Invite } from '../../agency/enums/agency.enum'; 10 | import { Agency } from '../../agency/entities/agency.entity'; 11 | import { EmailService } from '../../email/email.service'; 12 | import { Incident } from '../../incident/entities/incident.entities'; 13 | import { TIncident } from '../../incident/enums/incident.enum'; 14 | 15 | @Injectable() 16 | export class AllNotifications { 17 | private eventData: any = {}; 18 | constructor( 19 | @InjectRepository(User) private readonly usersRepository: Repository, 20 | private readonly configService: ConfigService, 21 | private readonly managerNotifications: ManagerNotifications, 22 | private readonly sendNotifications: SendNotifications, 23 | private readonly emailService: EmailService 24 | ){} 25 | 26 | private GetServerUri() { 27 | return this.configService.get('SERVER_URI') as unknown as string; 28 | } 29 | 30 | async OnSignUp(user) { 31 | const tfaLink = `here `; 32 | const text = `Please do double factor authentication for safety. Click ${tfaLink}.`; 33 | 34 | await this.sendNotifications.createNotification(user.id, 'Welcome 🎉 🎉 🎉', text, NotificationType.COMMON); 35 | await this.emailService.sendNotificationMail(user, 'Onlyf - registration.', text, this.eventData, NotificationType.COMMON) 36 | 37 | // switch (user.role) { 38 | // case Role.Admin: 39 | // await this.managerNotifications.OnLogin(user); 40 | // break; 41 | // case Role.Manager: 42 | // break; 43 | // case Role.Model: 44 | // break; 45 | // case Role.Regular: 46 | // break; 47 | // } 48 | // await this.managerNotifications.OnLogin(a); 49 | } 50 | 51 | async OnSentInvite(user: User, dstUser: User, invite: Invite, agency: Agency) { 52 | for (const id of agency.admins) { 53 | const userLink = `${dstUser.email}`; 54 | const text = `You sent an invitation to user: ${userLink}, by role ${invite.role}. You can always cancel your decision. From Agency: ${agency.name}`; 55 | 56 | await this.sendNotifications.createNotification(id, 'Invitation sent', text, NotificationType.COMMON); 57 | await this.emailService.sendNotificationMail(user, 'Invitation sent', text, this.eventData, NotificationType.COMMON) 58 | } 59 | 60 | const ownerUser = await this.usersRepository.findOneBy({ 61 | id: agency.ownerId 62 | }) 63 | const userLink = `${dstUser.email}`; 64 | const text = `You sent an invitation to user: ${userLink}, by role ${invite.role}. You can always cancel your decision. From Agency: ${agency.name}`; 65 | await this.sendNotifications.createNotification(ownerUser.id, 'Invitation sent', text, NotificationType.COMMON); 66 | await this.emailService.sendNotificationMail(ownerUser, 'Invitation sent', text, this.eventData, NotificationType.COMMON) 67 | 68 | const dstUserLink = `${ownerUser.email}`; 69 | const acceptLink = `Accept`; 70 | const dstUserText = `You have received an invitation from a user: ${dstUserLink}, by role ${invite.role}. From Agency: ${agency.name}. 71 | \n${acceptLink} invitation.`; 72 | 73 | await this.sendNotifications.createNotification(dstUser.id, 'An invitation has arrived.', dstUserText, NotificationType.COMMON); 74 | await this.emailService.sendNotificationMail(dstUser, 'An invitation has arrived.', dstUserText, this.eventData, NotificationType.COMMON) 75 | } 76 | 77 | async OnAcceptInvite(user: User, invite: Invite, agency: Agency) { 78 | const text = `You accept an invitation by role ${invite.role}. To Agency: ${agency.name}`; 79 | 80 | await this.sendNotifications.createNotification(invite.id, 'Invitation accept', text, NotificationType.COMMON); 81 | await this.emailService.sendNotificationMail(user, 'Invitation accept.', text, this.eventData, NotificationType.COMMON) 82 | 83 | for (const id of agency.admins) { 84 | const dstUserLink = `${user.email}`; 85 | const adminUserText = `User: ${dstUserLink} has accept invitation by role ${invite.role}. To Agency: ${agency.name}`; 86 | 87 | await this.sendNotifications.createNotification(id, 'Invitation has accept.', adminUserText, NotificationType.COMMON); 88 | const userAdmin = await this.usersRepository.findOneBy({ 89 | id: id 90 | }) 91 | await this.emailService.sendNotificationMail(userAdmin, 'Invitation has accept.', adminUserText, this.eventData, NotificationType.COMMON) 92 | } 93 | 94 | const dstUserLink = `${user.email}`; 95 | const adminUserText = `User: ${dstUserLink} has accept invitation by role ${invite.role}. To Agency: ${agency.name}`; 96 | 97 | await this.sendNotifications.createNotification(agency.ownerId, 'Invitation has accept.', adminUserText, NotificationType.COMMON); 98 | const ownerUser = await this.usersRepository.findOneBy({ 99 | id: agency.ownerId 100 | }) 101 | await this.emailService.sendNotificationMail(ownerUser, 'Invitation has accept.', adminUserText, this.eventData, NotificationType.COMMON) 102 | } 103 | 104 | async OnIncident(agency: Agency, incident: Incident) { 105 | 106 | const type = incident.type; 107 | let incidentTypeText = '' 108 | if (type === TIncident.Chat) { 109 | incidentTypeText = `do stop words: ${incident.stopWords}`; 110 | } else { 111 | incidentTypeText = 'so late answering' 112 | } 113 | 114 | for (const id of agency.admins) { 115 | const adminUser = await this.usersRepository.findOneBy({ 116 | id: id 117 | }) 118 | const dstUserLink = `${adminUser.email}`; 119 | const adminUserText = `Hello, ${dstUserLink}. User by ${incident.ofId} ${incidentTypeText}.`; 120 | 121 | await this.sendNotifications.createNotification(id, 'Incident.', adminUserText, NotificationType.COMMON); 122 | await this.emailService.sendNotificationMail(adminUser, 'Incident.', adminUserText, this.eventData, NotificationType.COMMON) 123 | } 124 | 125 | const ownerUser = await this.usersRepository.findOneBy({ 126 | id: agency.ownerId 127 | }) 128 | const dstUserLink = `${ownerUser.email}`; 129 | const adminUserText = `Hello, ${dstUserLink}. User by ${incident.ofId} ${incidentTypeText}.`; 130 | await this.sendNotifications.createNotification(agency.ownerId, 'Incident.', adminUserText, NotificationType.COMMON); 131 | await this.emailService.sendNotificationMail(ownerUser, 'Incident.', adminUserText, this.eventData, NotificationType.COMMON) 132 | } 133 | } -------------------------------------------------------------------------------- /src/notification/helpers/notification.manager.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class ManagerNotifications {} -------------------------------------------------------------------------------- /src/notification/helpers/notification.send.ts: -------------------------------------------------------------------------------- 1 | import { ConflictException, Injectable } from '@nestjs/common'; 2 | import { NotificationType } from '../enums/notification.enum'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { Notification } from '../entities/notification.entity'; 5 | import { Repository } from 'typeorm'; 6 | import { User } from '../../users/entities/user.entity'; 7 | 8 | @Injectable() 9 | export class SendNotifications { 10 | constructor( 11 | @InjectRepository(Notification) private readonly notificationsRepository: Repository, 12 | @InjectRepository(User) private readonly usersRepository: Repository, 13 | ){} 14 | 15 | async createNotification(dstUserId: string, title: string, text: string, type: NotificationType = NotificationType.COMMON) { 16 | try { 17 | const dstUser = await this.usersRepository.findOneBy({ 18 | id: dstUserId, 19 | }) 20 | const notificationSettings = (dstUser.settings && dstUser.settings.notifications) ? dstUser.settings.notifications : null; 21 | let allowSend = true; 22 | 23 | if (notificationSettings && notificationSettings.push) { 24 | switch (type) { 25 | case NotificationType.COMMON: 26 | allowSend = (notificationSettings.push.info !== undefined) ? !!notificationSettings.push.info : true; 27 | break; 28 | case NotificationType.EVENTS: 29 | allowSend = (notificationSettings.push.events !== undefined) ? !!notificationSettings.push.events : true; 30 | break; 31 | case NotificationType.CHAT: 32 | allowSend = (notificationSettings.push.comments !== undefined) ? !!notificationSettings.push.comments : true; 33 | break; 34 | } 35 | } 36 | 37 | if (allowSend) { 38 | return await this.notificationsRepository.save({ dstUserId, title, text, type }); 39 | } 40 | return null; 41 | 42 | } catch (err) { 43 | const pgUniqueViolationErrorCode = '23505'; 44 | if (err.code === pgUniqueViolationErrorCode) { 45 | throw new ConflictException(); 46 | } 47 | throw err; 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/notification/helpers/notification.suser.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { User } from '../../users/entities/user.entity'; 4 | import { Repository } from 'typeorm'; 5 | import { SendNotifications } from './notification.send'; 6 | import { NotificationType } from '../enums/notification.enum'; 7 | import { Role } from '../../users/enums/role.enum'; 8 | import { ConfigService } from '@nestjs/config'; 9 | import { Invite } from '../../agency/enums/agency.enum'; 10 | import { Agency } from '../../agency/entities/agency.entity'; 11 | import { EmailService } from '../../email/email.service'; 12 | import { TelegramService } from '../../telegram/telegram.service'; 13 | import { Incident } from '../../incident/entities/incident.entities'; 14 | import { TIncident } from '../../incident/enums/incident.enum'; 15 | 16 | @Injectable() 17 | export class SUserNotifications { 18 | private eventData: any = {}; 19 | constructor( 20 | @InjectRepository(User) private readonly usersRepository: Repository, 21 | private readonly configService: ConfigService, 22 | private readonly sendNotifications: SendNotifications, 23 | private readonly emailService: EmailService, 24 | private readonly telegramService: TelegramService 25 | ){} 26 | 27 | private GetServerUri() { 28 | return this.configService.get('SERVER_URI') as unknown as string; 29 | } 30 | 31 | async OnSignUp(user) { 32 | const superUsers = await this.usersRepository.findBy({ 33 | role: Role.SuperUser, 34 | }) 35 | 36 | const emailLink = `${user.email}`; 37 | const text = `Some user registered with email: ${emailLink}.`; 38 | const telegramText = `Some user registered with email: ${user.email}.`; 39 | 40 | for (const superUser of superUsers) { 41 | await this.sendNotifications.createNotification(superUser.id, 'User signup', text, NotificationType.COMMON); 42 | } 43 | await this.telegramService.sendNotificationTelegram(`User signup \n\n${telegramText}`) 44 | } 45 | 46 | async OnSentInvite(user: User, dstUser: User, invite: Invite, agency: Agency) { 47 | const superUsers = await this.usersRepository.findBy({ 48 | role: Role.SuperUser, 49 | }) 50 | 51 | const userLink = `${user.email}`; 52 | const dstUserLink = `${dstUser.email}`; 53 | const text = `User: ${userLink}, sent invitation by role ${invite.role} to ${dstUserLink}. Agency name: ${agency.name}`; 54 | const telegramText = `User: ${user.email}, sent invitation by role ${invite.role} to ${dstUser.email}. Agency name: ${agency.name}`; 55 | 56 | for (const sUser of superUsers) { 57 | await this.sendNotifications.createNotification(sUser.id, 'Invitation user by user', text, NotificationType.COMMON); 58 | // await this.emailService.sendNotificationMail(sUser, 'Invitation user by user', text, this.eventData, NotificationType.COMMON) 59 | } 60 | await this.telegramService.sendNotificationTelegram(`Invitation user by user \n\n${telegramText}`) 61 | } 62 | 63 | async OnAcceptInvite(user: User, invite: Invite, agency: Agency) { 64 | const dstUserLink = `${user.email}`; 65 | const text = `User: ${dstUserLink} accept an invitation by role ${invite.role}. To Agency: ${agency.name}`; 66 | const telegramText = `User: ${user.email} accept an invitation by role ${invite.role}. To Agency: ${agency.name}`; 67 | 68 | const superUsers = await this.usersRepository.findBy({ 69 | role: Role.SuperUser, 70 | }) 71 | 72 | for (const sUser of superUsers) { 73 | await this.sendNotifications.createNotification(sUser.id, 'Invitation accept', text, NotificationType.COMMON); 74 | } 75 | await this.telegramService.sendNotificationTelegram(`Invitation accept \n\n${telegramText}`) 76 | } 77 | 78 | async OnIncident(agency: Agency, incident: Incident) { 79 | 80 | const type = incident.type; 81 | let incidentTypeText = '' 82 | if (type === TIncident.Chat) { 83 | incidentTypeText = `do stop words: ${incident.stopWords}`; 84 | } else { 85 | incidentTypeText = 'so late answering' 86 | } 87 | 88 | const ownerUser = await this.usersRepository.findOneBy({ 89 | id: agency.ownerId 90 | }) 91 | 92 | const dstUserLink = `${ownerUser.email}`; 93 | const text = `Hello, ${dstUserLink}. User by ${incident.ofId} ${incidentTypeText}. To Agency: ${agency.name}`; 94 | const telegramText = `User by ${incident.ofId} ${incidentTypeText}. \n\nOwner: ${ownerUser.email} of Agency: ${agency.name} get Incident. `; 95 | 96 | const superUsers = await this.usersRepository.findBy({ 97 | role: Role.SuperUser, 98 | }) 99 | 100 | for (const sUser of superUsers) { 101 | await this.sendNotifications.createNotification(sUser.id, 'Incident', text, NotificationType.COMMON); 102 | } 103 | await this.telegramService.sendNotificationTelegram(`Incident \n\n${telegramText}`) 104 | } 105 | } -------------------------------------------------------------------------------- /src/notification/notification.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { NotificationController } from './notification.controller'; 3 | 4 | describe('NotificationController', () => { 5 | let controller: NotificationController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [NotificationController], 10 | }).compile(); 11 | 12 | controller = module.get(NotificationController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/notification/notification.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Delete, Get, Param, Req } from '@nestjs/common'; 2 | import { Auth } from '../iam/authentication/decorators/auth.decorator'; 3 | import { AuthType } from '../iam/authentication/enums/auth-type.enum'; 4 | import { NotificationService } from './notification.service'; 5 | import { Request } from 'express'; 6 | import * as jwt from 'jsonwebtoken'; 7 | 8 | @Auth(AuthType.Bearer, AuthType.ApiKey) 9 | @Controller('notification') 10 | export class NotificationController { 11 | constructor(private readonly notificationService: NotificationService) {} 12 | 13 | @Get() 14 | getNotificationForUser(@Req() request: Request) { 15 | const tokenParts = request.headers.authorization.split(' '); 16 | const token = tokenParts[1] 17 | const refreshTokenData = jwt.decode(token); 18 | 19 | return this.notificationService.getNotificationForUser(refreshTokenData.sub as unknown as string); 20 | } 21 | 22 | @Get('count') 23 | getNotificationCountForUser(@Req() request: Request) { 24 | const tokenParts = request.headers.authorization.split(' '); 25 | const token = tokenParts[1] 26 | const refreshTokenData = jwt.decode(token); 27 | 28 | return this.notificationService.getNotificationCountForUser(refreshTokenData.sub as unknown as string); 29 | } 30 | 31 | @Delete(':notificationId') 32 | markAsDelete( 33 | @Param('notificationId') notificationId: string, 34 | @Req() request: Request) 35 | { 36 | const tokenParts = request.headers.authorization.split(' '); 37 | const token = tokenParts[1] 38 | const refreshTokenData = jwt.decode(token); 39 | 40 | return this.notificationService.markAsDelete(notificationId, refreshTokenData.sub as unknown as string); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/notification/notification.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { NotificationService } from './notification.service'; 3 | import { NotificationController } from './notification.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Notification } from './entities/notification.entity'; 6 | import { User } from '../users/entities/user.entity'; 7 | import { AdminNotifications } from './helpers/notification.admin'; 8 | import { ManagerNotifications } from './helpers/notification.manager'; 9 | import { AllNotifications } from './helpers/notification.all'; 10 | import { SendNotifications } from './helpers/notification.send'; 11 | import { SUserNotifications } from './helpers/notification.suser'; 12 | import { ConfigModule, ConfigService } from '@nestjs/config'; 13 | import { EmailService } from '../email/email.service'; 14 | import { TelegramService } from '../telegram/telegram.service'; 15 | 16 | @Module({ 17 | imports: [TypeOrmModule.forFeature([Notification, User]), ConfigModule.forRoot()], 18 | providers: [ 19 | NotificationService, 20 | SendNotifications, 21 | SUserNotifications, 22 | AllNotifications, 23 | AdminNotifications, 24 | ManagerNotifications, 25 | EmailService, 26 | TelegramService, 27 | ConfigService 28 | ], 29 | controllers: [NotificationController] 30 | }) 31 | export class NotificationModule {} 32 | -------------------------------------------------------------------------------- /src/notification/notification.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { NotificationService } from './notification.service'; 3 | 4 | describe('NotificationService', () => { 5 | let service: NotificationService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [NotificationService], 10 | }).compile(); 11 | 12 | service = module.get(NotificationService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/notification/notification.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { Notification } from './entities/notification.entity'; 5 | import { User } from '../users/entities/user.entity'; 6 | import { AllNotifications } from './helpers/notification.all'; 7 | import { SUserNotifications } from './helpers/notification.suser'; 8 | import { Invite } from '../agency/enums/agency.enum'; 9 | import { Agency } from '../agency/entities/agency.entity'; 10 | import { GetNotificationsCountResponseData } from './dto/get-count.dto'; 11 | import { Incident } from '../incident/entities/incident.entities'; 12 | 13 | @Injectable() 14 | export class NotificationService { 15 | constructor( 16 | @InjectRepository(Notification) private readonly notificationsRepository: Repository, 17 | @InjectRepository(User) private readonly usersRepository: Repository, 18 | private readonly sUserNotifications: SUserNotifications, 19 | private readonly allNotifications: AllNotifications, 20 | ){} 21 | 22 | async getNotificationForUser(dstUserId: string): Promise> { 23 | const notifications = await this.notificationsRepository.findBy({ 24 | dstUserId: dstUserId, 25 | isDelete: false 26 | }) 27 | 28 | notifications.sort((a, b) => b.created_at.getTime() - a.created_at.getTime()); 29 | 30 | notifications 31 | .filter(notifications => !notifications.isRead) 32 | .map(async notification => { 33 | notification.isRead = true; 34 | await this.notificationsRepository.save(notification); 35 | }) 36 | .slice(0, 9); 37 | 38 | return notifications; 39 | } 40 | 41 | async getNotificationCountForUser(dstUserId: string): Promise { 42 | const count = (await this.notificationsRepository.findBy({ 43 | dstUserId: dstUserId, 44 | isRead: false 45 | })).length 46 | 47 | return { count }; 48 | } 49 | 50 | async markAsRead(notificationId: string, dstUserId: string): Promise { 51 | const notification = await this.notificationsRepository.findOneBy({ 52 | id: notificationId, 53 | dstUserId: dstUserId 54 | }) 55 | notification.isRead = true; 56 | return await this.notificationsRepository.save(notification); 57 | } 58 | 59 | async markAsDelete(notificationId: string, dstUserId: string): Promise { 60 | const notification = await this.notificationsRepository.findOneBy({ 61 | id: notificationId, 62 | dstUserId: dstUserId 63 | }) 64 | notification.isDelete = true; 65 | return await this.notificationsRepository.save(notification); 66 | } 67 | 68 | async OnSignUp(dstUserId: string) { 69 | const user = await this.usersRepository.findOneBy({ 70 | id: dstUserId, 71 | }) 72 | await this.allNotifications.OnSignUp(user); 73 | await this.sUserNotifications.OnSignUp(user); 74 | } 75 | 76 | async OnSentInvite(user: User, invite: Invite, agency: Agency) { 77 | const dstUser = await this.usersRepository.findOneBy({ 78 | id: invite.id 79 | }) 80 | await this.allNotifications.OnSentInvite(user, dstUser, invite, agency); 81 | await this.sUserNotifications.OnSentInvite(user, dstUser, invite, agency); 82 | } 83 | 84 | async OnAcceptInvite(user: User, invite: Invite, agency: Agency) { 85 | await this.allNotifications.OnAcceptInvite(user, invite, agency); 86 | await this.sUserNotifications.OnAcceptInvite(user, invite, agency); 87 | } 88 | 89 | async OnIncident(agency: Agency, incident: Incident) { 90 | await this.allNotifications.OnIncident(agency, incident); 91 | await this.sUserNotifications.OnIncident(agency, incident); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/only-fans/only-fans.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { OnlyFansController } from './only-fans.controller'; 3 | 4 | describe('OnlyFansController', () => { 5 | let controller: OnlyFansController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [OnlyFansController], 10 | }).compile(); 11 | 12 | controller = module.get(OnlyFansController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/only-fans/only-fans.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param, Query } from '@nestjs/common'; 2 | import { OnlyFansService } from './only-fans.service'; 3 | import { Auth } from '../iam/authentication/decorators/auth.decorator'; 4 | import { AuthType } from '../iam/authentication/enums/auth-type.enum'; 5 | 6 | // TODO: 7 | // @Auth(AuthType.Bearer, AuthType.ApiKey) 8 | @Auth(AuthType.None) 9 | @Controller('only-fans') 10 | export class OnlyFansController { 11 | constructor(private readonly onlyFansService: OnlyFansService) {} 12 | 13 | @Get() 14 | scrapperController() { 15 | return 'onlyFans controller working'; 16 | } 17 | 18 | @Get('test') 19 | getTest() { 20 | return this.onlyFansService.getDataViaPuppeteer(); 21 | } 22 | 23 | @Get('products') 24 | getProducts(@Query('product') product: string) { 25 | return this.onlyFansService.getProducts(product); 26 | } 27 | 28 | // TODO: 29 | // @Roles(Role.Admin) 30 | @Get(':id/run') 31 | runScrapper(@Param('id') id: string) { 32 | return this.onlyFansService.runScrapper(id); 33 | } 34 | 35 | @Get(':id/stop') 36 | stopScrapper(@Param('id') id: string) { 37 | return this.onlyFansService.stopScrapper(id); 38 | } 39 | 40 | @Get('/stop_all') 41 | stopAllScrappers() { 42 | return this.onlyFansService.stopAllScrappers(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/only-fans/only-fans.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { OnlyFansController } from './only-fans.controller'; 3 | import { OnlyFansService } from './only-fans.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Scrapper } from '../scrapper/entities/scrapper.entity'; 6 | import { Message } from '../messages/entities/message.entity'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([Scrapper, Message])], 10 | controllers: [OnlyFansController], 11 | providers: [OnlyFansService] 12 | }) 13 | export class OnlyFansModule {} 14 | -------------------------------------------------------------------------------- /src/only-fans/only-fans.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { OnlyFansService } from './only-fans.service'; 3 | 4 | describe('OnlyFansService', () => { 5 | let service: OnlyFansService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [OnlyFansService], 10 | }).compile(); 11 | 12 | service = module.get(OnlyFansService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/only-fans/only-fans.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus, Injectable, NotFoundException } from '@nestjs/common'; 2 | import puppeteer from 'puppeteer'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { Column, Repository } from 'typeorm'; 5 | import { Scrapper } from '../scrapper/entities/scrapper.entity'; 6 | import { Status } from '../scrapper/enums/scrapper.enum'; 7 | import { Message } from '../messages/entities/message.entity'; 8 | 9 | @Injectable() 10 | export class OnlyFansService { 11 | constructor( 12 | @InjectRepository(Scrapper) private readonly scrappersRepository: Repository, 13 | @InjectRepository(Message) private readonly messagesRepository: Repository, 14 | ){} 15 | // private browserInstances: Map = new Map(); 16 | private browserInstances: Map = new Map(); 17 | private maxTimeout = 2*60*1000; 18 | // private delayTimeout = 5000; 19 | private delayTimeout = 25000; 20 | private typeTimeoutDelay = 500; // 1100 21 | 22 | async createBrowserInstance(id: string) { 23 | const browser = await puppeteer.launch({ 24 | headless: false 25 | }); 26 | // const browser = await puppeteer.launch({ 27 | // // devtools: true, 28 | // // args: [ 29 | // // '--disable-web-security', 30 | // // '--disable-features=IsolateOrigins', 31 | // // '--disable-site-isolation-trials', 32 | // // '--disable-features=BlockInsecurePrivateNetworkRequests', 33 | // // // '--no-sandbox', 34 | // // '--disable-setuid-sandbox' 35 | // // ], 36 | // headless: false 37 | // }); 38 | 39 | this.browserInstances.set(id, browser); 40 | } 41 | 42 | async closeBrowserInstance(id: string) { 43 | const browser = this.browserInstances.get(id); 44 | 45 | if (browser) { 46 | const scrapper = await this.scrappersRepository.findOneBy({ 47 | id: id, 48 | }) 49 | scrapper.status = Status.Offline; 50 | await this.scrappersRepository.save(scrapper); 51 | 52 | await browser.close(); 53 | this.browserInstances.delete(id); 54 | } 55 | } 56 | 57 | async getProducts(products: string) { 58 | // const browser = await puppeteer.connect({ 59 | // // browserWSEndpoint: this.configService.getOrThrow('SBR_WS_ENDPOINT') 60 | // // browserWSEndpoint: 'ws://localhost:3000' 61 | // browserURL: 'http://localhost:9222' 62 | // }); 63 | const browser = await puppeteer.launch(); 64 | try { 65 | const page = await browser.newPage(); 66 | page.setDefaultNavigationTimeout(2 * 60 * 1000); 67 | await Promise.all([ 68 | page.waitForNavigation(), 69 | page.goto('https://amazon.com') 70 | ]) 71 | await page.type('#twotabsearchtextbox', products) 72 | await Promise.all([ 73 | page.waitForNavigation(), 74 | page.click('#nav-search-submit-button') 75 | ]) 76 | return await page.$$eval('.s-search-results .s-card-container', 77 | (resultItems) => { 78 | return resultItems.map(resultItem => { 79 | const url = resultItem.querySelector('a').href; 80 | const title = resultItem.querySelector( 81 | '.s-title-instructions-style span', 82 | )?.textContent; 83 | const price = resultItem.querySelector( 84 | '.a-price .a-offscreen', 85 | ).textContent; 86 | return {url, title, price}; 87 | }) 88 | }); 89 | } finally { 90 | await browser.close(); 91 | } 92 | } 93 | 94 | async getDataViaPuppeteer() { 95 | 96 | const URL = 'https://example.com'; 97 | 98 | // createIncognitoBrowserContext 99 | const browser = await puppeteer.launch({ 100 | headless: false 101 | }); 102 | const page = await browser.newPage(); 103 | 104 | await page.goto(URL, { 105 | waitUntil: 'networkidle2' 106 | }); 107 | 108 | console.log('testing inside NEST console') 109 | 110 | const results = await page.evaluate(() => { 111 | const propertyList = []; 112 | console.log('testing inside HEADLESS BROWSER console') 113 | 114 | document.querySelectorAll('.List').forEach((item) => { 115 | let someList = [] 116 | 117 | item.querySelectorAll('.subList').forEach((sublistItem) => { 118 | if (sublistItem.querySelector('img').src) { 119 | // do something 120 | // item.querySelector('.detail-property > div > h2')?.textContent 121 | } 122 | }) 123 | propertyList.push('someData'); 124 | }) 125 | 126 | return propertyList; 127 | }) 128 | 129 | // await browser.close() 130 | // return 'someData'; 131 | } 132 | 133 | 134 | async stopAllScrappers() { 135 | const scrappers = await this.scrappersRepository.find(); 136 | 137 | try { 138 | await Promise.all(scrappers.map(async (scrapper) => { 139 | if (scrapper.status === Status.Online && this.browserInstances.has(scrapper.id.toString())) { 140 | await this.closeBrowserInstance(scrapper.id.toString()); 141 | } 142 | 143 | if (scrapper.status === Status.Online) { 144 | const scrapperBd = await this.scrappersRepository.findOneBy({ 145 | id: scrapper.id, 146 | }) 147 | scrapperBd.status = Status.Offline; 148 | await this.scrappersRepository.save(scrapperBd); 149 | } 150 | })); 151 | } catch (err) { 152 | throw new HttpException(err, HttpStatus.CONFLICT); 153 | } 154 | return 'success'; 155 | } 156 | 157 | async stopScrapper(id: string) { 158 | if (this.browserInstances.has(id)) { 159 | await this.closeBrowserInstance(id) 160 | return 'success' 161 | } else { 162 | throw new HttpException('Scrapper not running', HttpStatus.CONFLICT); 163 | } 164 | } 165 | 166 | async runScrapper(id: string) { 167 | const scrapper = await this.scrappersRepository.findOneBy({ 168 | id: id, 169 | }) 170 | 171 | if (!scrapper) { 172 | throw new NotFoundException('Scrapper not found'); 173 | } 174 | if (scrapper.status === Status.Online) { 175 | throw new HttpException('Scrapper already running', HttpStatus.CONFLICT); 176 | } 177 | 178 | if (!this.browserInstances.has(id)) { 179 | await this.createBrowserInstance(id); 180 | } 181 | 182 | scrapper.status = Status.Online; 183 | await this.scrappersRepository.save(scrapper); 184 | 185 | // const URL = 'https://main--sprightly-toffee-821aa4.netlify.app/' 186 | // const URL = 'https://example.com' 187 | // const URL = 'https://23oct.zetfix.online/' 188 | // const URL = 'https://vk.com' 189 | const URL = 'https://onlyfans.com/' 190 | 191 | const browser = this.browserInstances.get(id); 192 | const page = await browser.newPage(); 193 | await page.setRequestInterception(true); 194 | 195 | page.on('request', async interceptedRequest => { 196 | if (interceptedRequest.isNavigationRequest() && interceptedRequest.redirectChain().length === 0) { 197 | await interceptedRequest.continue(); 198 | } else { 199 | await interceptedRequest.continue(); 200 | } 201 | }); 202 | 203 | page.on('response', async response => { 204 | console.log('!!! Intercept response scrapper'); 205 | // console.log("response code: ", response.status()); 206 | 207 | const request = await response.request(); 208 | 209 | const URL = request.url(); 210 | const isMedia = URL.endsWith('.jpg') || URL.endsWith('.png') 211 | const isRecapcha = URL.includes('https://recaptcha.net') 212 | 213 | if (response.status() === 200 && response.headers()['content-length'] !== '0' && !isMedia && !isRecapcha) { 214 | console.log('Inside'); 215 | console.log(request.url()); 216 | // console.log(response.request()); 217 | // console.log(request.headers()); 218 | // console.log(response.request().method()); 219 | const contentType = response.headers()['content-type']; 220 | const isImage = contentType && contentType.includes('image') 221 | 222 | if (!isImage) { 223 | if (contentType && contentType.includes('application/json')) { 224 | // console.log('JSON Response:'); 225 | // console.log(response); 226 | console.log('contentType: application/json'); 227 | // https://onlyfans.com/api2/v2/users/list?a[]=15585607&a[]=108013309&a[]=234122322 228 | const jsonResponse = await response.json(); 229 | // console.log('JSON Response:', jsonResponse); 230 | 231 | if (URL.includes('https://onlyfans.com/api2/v2/chats')) { 232 | const unreadChats = this.getUnreadChats(jsonResponse); 233 | 234 | // console.log('!!! unreadChats'); 235 | // console.log(unreadChats); 236 | // console.log('!!!'); 237 | 238 | for (let unreadChat of unreadChats) { 239 | const chat = await this.messagesRepository.findOneBy({ 240 | chatId: unreadChat.chatId, 241 | }) 242 | const DateNow = new Date(); 243 | // const inputTime = new Date(unreadChat.createdAt); 244 | // TODO: get time from settings 245 | // const newTime = inputTime.setMinutes(inputTime.getMinutes() + 1); 246 | // inputTime.setMinutes(inputTime.getMinutes() + 1); 247 | // console.log(DateNow < inputTime); 248 | // console.log(unreadChat.createdAt); 249 | // console.log(DateNow); 250 | // console.log(inputTime); 251 | // console.log('!!!'); 252 | if (chat) { 253 | // console.log('!!! 1111'); 254 | // console.log(chat); 255 | // console.log(unreadChat); 256 | // console.log('!!!'); 257 | // await this.messagesRepository.update({ chatId: unreadChat.chatId }, { 258 | // createdAt: unreadChat.createdAt 259 | // }) 260 | } else { 261 | // console.log('!!! 2222'); 262 | // console.log(unreadChat); 263 | // console.log('!!!'); 264 | const newChat = new Message(); 265 | // newChat.userId = id; 266 | newChat.chatId = unreadChat.chatId; 267 | // newChat.created_at = unreadChat.created_at; 268 | newChat.text = unreadChat.text; 269 | newChat.isCounted = false; 270 | await this.messagesRepository.save(newChat); 271 | } 272 | // await this.unreadMessagesRepository.update({ chatId: unreadChat.chatId }, { 273 | // createdAt: unreadChat.createdAt 274 | // }); 275 | } 276 | } 277 | } 278 | 279 | // text/javascript text/html text/plain 280 | if (contentType && contentType.includes('text/plain')) { 281 | console.log('contentType: text/plain'); 282 | const textResponse = await response.text(); 283 | console.log('Text Response:', textResponse); 284 | } 285 | } 286 | } 287 | }); 288 | 289 | try { 290 | page.setDefaultNavigationTimeout(1 * 60 * 1000); 291 | page.goto(URL, {timeout: 30000}) 292 | 293 | await page.waitForSelector('[type="email"]'); 294 | await page.waitForSelector('[type="password"]'); 295 | 296 | await this.slowType(page, '[type="email"]', scrapper.email); 297 | await this.slowType(page, '[type="password"]', scrapper.password); 298 | 299 | await page.waitForSelector('[type="submit"]'); 300 | page.click('[type="submit"]') 301 | 302 | // await page.waitForSelector('[data-name="Chats"]', { visible: true, timeout: this.maxTimeout }); 303 | // await this.delay(this.delayTimeout); 304 | // page.click('[data-name="Chats"]') 305 | 306 | for (let i = 0; i < 100; i++) { 307 | await page.waitForSelector('[data-name="Chats"]', { visible: true, timeout: this.maxTimeout }); 308 | await this.delay(this.delayTimeout); 309 | page.click('[data-name="Chats"]') 310 | } 311 | 312 | // await page.waitForSelector('[data-name="Collections"]', { visible: true, timeout: this.maxTimeout }); 313 | // await this.delay(this.delayTimeout); 314 | // page.click('[data-name="Collections"]') 315 | 316 | } catch (error) { 317 | await this.closeBrowserInstance(id); 318 | console.error('Navigation failed:', error); 319 | throw new HttpException('Gateway timeout', HttpStatus.GATEWAY_TIMEOUT); 320 | } 321 | 322 | return id; 323 | } 324 | 325 | async slowType(page, selector, text) { 326 | await page.waitForSelector(selector); 327 | 328 | const inputElement = await page.$(selector); 329 | 330 | for (const char of text) { 331 | await inputElement.type(char, { delay: this.typeTimeoutDelay }); 332 | } 333 | } 334 | 335 | delay(ms) { 336 | return new Promise(resolve => setTimeout(resolve, ms)); 337 | } 338 | 339 | getUnreadChats(data): Array> { 340 | let unreadChats: Array> = []; 341 | data["list"]?.forEach(chat => { 342 | if (chat['messagesCount'] > 0 && !chat['isMutedNotifications']) { 343 | // unreadChats.push({ 344 | // chatId: chat['withUser']['id'], 345 | // createdAt: chat['lastMessage']['createdAt'], 346 | // text: chat['lastMessage']['text'] 347 | // }) 348 | } 349 | }) 350 | return unreadChats; 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /src/profile/dto/update-profile.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsOptional } from 'class-validator'; 2 | 3 | export class UpdateProfileDto { 4 | @IsOptional() 5 | avatar?: string; 6 | 7 | @IsOptional() 8 | username?: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/profile/entities/profile.entities.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class Profile { 5 | @PrimaryGeneratedColumn() 6 | id: string; 7 | 8 | @Column({unique: false}) 9 | userId: string; 10 | 11 | @Column({ default: 'username' }) 12 | username: string; 13 | 14 | @Column({ default: 'first' }) 15 | first: string; 16 | 17 | @Column({ default: 'lastname' }) 18 | lastname: string; 19 | 20 | @Column({ default: false }) 21 | verified: boolean; 22 | 23 | 24 | // TODO? 25 | @Column({ default: 0 }) 26 | countViolations: number; 27 | 28 | @Column({ default: 0 }) 29 | countViolationsPercentage: number; 30 | 31 | @Column({ default: 0 }) 32 | countViolationsTotal: number; 33 | 34 | @Column({ default: 0 }) 35 | countActiveDialogs: number; 36 | 37 | 38 | @Column({ default: 18 }) 39 | age: number; 40 | 41 | @Column({ default: 'EUR' }) 42 | currency: string; 43 | 44 | @Column({ default: 'Ukraine' }) 45 | country: string; 46 | 47 | @Column({ default: 'Kiev' }) 48 | city: string; 49 | 50 | @Column({ default: 'https://picsum.photos/800/600' }) 51 | avatar: string; 52 | 53 | @Column({ default: 'https://www.instagram.com/' }) 54 | instLink: string; 55 | 56 | @Column({ default: 'stop words' }) 57 | stopWords: string; 58 | } -------------------------------------------------------------------------------- /src/profile/profile.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ProfileController } from './profile.controller'; 3 | 4 | describe('ProfileController', () => { 5 | let controller: ProfileController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [ProfileController], 10 | }).compile(); 11 | 12 | controller = module.get(ProfileController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/profile/profile.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Param, Put } from '@nestjs/common'; 2 | import { ProfileService } from './profile.service'; 3 | import { UpdateProfileDto } from './dto/update-profile.dto'; 4 | 5 | @Controller('profiles') 6 | export class ProfileController { 7 | constructor(private readonly profileService: ProfileService) {} 8 | 9 | 10 | @Get(':id') 11 | findOne(@Param('id') id: string) { 12 | return this.profileService.findOne(id); 13 | } 14 | 15 | @Put(':id') 16 | update(@Param('id') id: string, @Body() updateProfileDto: UpdateProfileDto) { 17 | return this.profileService.update(id, updateProfileDto); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/profile/profile.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ProfileService } from './profile.service'; 3 | import { ProfileController } from './profile.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Profile } from './entities/profile.entities'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([Profile])], 9 | providers: [ProfileService], 10 | controllers: [ProfileController] 11 | }) 12 | export class ProfileModule {} 13 | -------------------------------------------------------------------------------- /src/profile/profile.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ProfileService } from './profile.service'; 3 | 4 | describe('ProfileService', () => { 5 | let service: ProfileService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ProfileService], 10 | }).compile(); 11 | 12 | service = module.get(ProfileService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/profile/profile.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Profile } from './entities/profile.entities'; 4 | import { Repository } from 'typeorm'; 5 | import { UpdateProfileDto } from './dto/update-profile.dto'; 6 | 7 | @Injectable() 8 | export class ProfileService { 9 | @InjectRepository(Profile) private readonly profilesRepository: Repository 10 | 11 | async findOne(id: string) { 12 | const profile = await this.profilesRepository.findOneBy({ 13 | id: id, 14 | }) 15 | return profile; 16 | } 17 | 18 | async update(id: string, updateProfileDto: UpdateProfileDto) { 19 | const profile = await this.profilesRepository.findOneBy({ 20 | id: id, 21 | }) 22 | 23 | if (profile.avatar) { 24 | profile.avatar = updateProfileDto.avatar; 25 | } 26 | if (profile.username) { 27 | profile.username = updateProfileDto.username; 28 | } 29 | 30 | return await this.profilesRepository.save(profile); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/repl.ts: -------------------------------------------------------------------------------- 1 | import { repl } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | await repl(AppModule); 6 | } 7 | bootstrap(); 8 | -------------------------------------------------------------------------------- /src/schedule/schedule.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ScheduleController } from './schedule.controller'; 3 | 4 | describe('ScheduleController', () => { 5 | let controller: ScheduleController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [ScheduleController], 10 | }).compile(); 11 | 12 | controller = module.get(ScheduleController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/schedule/schedule.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { ScheduleService } from './schedule.service'; 3 | 4 | @Controller('schedule') 5 | export class ScheduleController { 6 | constructor(private readonly scheduleService: ScheduleService) {} 7 | 8 | @Get() 9 | messageController() { 10 | return 'schedule controller working'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/schedule/schedule.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ScheduleService } from './schedule.service'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { Message } from '../messages/entities/message.entity'; 5 | import { ScheduleController } from './schedule.controller'; 6 | import { Agency } from '../agency/entities/agency.entity'; 7 | import { Incident } from '../incident/entities/incident.entities'; 8 | import { Notification } from '../notification/entities/notification.entity'; 9 | import { NotificationService } from '../notification/notification.service'; 10 | import { SendNotifications } from '../notification/helpers/notification.send'; 11 | import { AllNotifications } from '../notification/helpers/notification.all'; 12 | import { SUserNotifications } from '../notification/helpers/notification.suser'; 13 | import { AdminNotifications } from '../notification/helpers/notification.admin'; 14 | import { ManagerNotifications } from '../notification/helpers/notification.manager'; 15 | import { EmailService } from '../email/email.service'; 16 | import { TelegramService } from '../telegram/telegram.service'; 17 | import { User } from '../users/entities/user.entity'; 18 | import { ConfigService } from '@nestjs/config'; 19 | 20 | @Module({ 21 | imports: [ 22 | TypeOrmModule.forFeature([Message, Agency, Incident, Notification, User]) 23 | ], 24 | providers: [ 25 | ScheduleService, 26 | NotificationService, 27 | SendNotifications, 28 | AllNotifications, 29 | SUserNotifications, 30 | AdminNotifications, 31 | ManagerNotifications, 32 | EmailService, 33 | TelegramService, 34 | ConfigService, 35 | ], 36 | controllers: [ScheduleController] 37 | }) 38 | export class ScheduleModule {} 39 | -------------------------------------------------------------------------------- /src/schedule/schedule.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ScheduleService } from './schedule.service'; 3 | 4 | describe('ScheduleService', () => { 5 | let service: ScheduleService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ScheduleService], 10 | }).compile(); 11 | 12 | service = module.get(ScheduleService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/schedule/schedule.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Cron } from '@nestjs/schedule'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { Message } from '../messages/entities/message.entity'; 5 | import { Repository } from 'typeorm'; 6 | import { Agency } from '../agency/entities/agency.entity'; 7 | import { Incident } from '../incident/entities/incident.entities'; 8 | import { TIncident } from '../incident/enums/incident.enum'; 9 | import { NotificationService } from '../notification/notification.service'; 10 | 11 | @Injectable() 12 | export class ScheduleService { 13 | constructor( 14 | @InjectRepository(Message) private readonly messagesRepository: Repository, 15 | @InjectRepository(Agency) private readonly agenciesRepository: Repository, 16 | @InjectRepository(Incident) private readonly incidentsRepository: Repository, 17 | private readonly notificationService: NotificationService 18 | ){} 19 | 20 | @Cron('*/10 * * * * *') 21 | // @Cron('* * */10 * * *') 22 | async handleCron() { 23 | console.log('chron handleCron'); 24 | 25 | const currentTime = new Date(); 26 | const messages = await this.messagesRepository.findBy({ 27 | isRead: false, 28 | }); 29 | 30 | const filteredMessages = await Promise.all(messages.map(async (message) => { 31 | const msgCreatedAt = new Date(message.msg_created_at); 32 | const agency = await this.agenciesRepository.findOneBy({ 33 | id: message.agencyId, 34 | }); 35 | const timeDelay = agency.userTimeConstraint[message.ofId]; 36 | const delay = timeDelay === undefined ? 1000*60*10 : timeDelay; 37 | 38 | // console.log('message.id'); 39 | // console.log(message.id); 40 | // console.log(agency); 41 | // console.log(timeDelay); 42 | // console.log('Created:'); 43 | // console.log(message.msg_created_at); 44 | // console.log('When delayed:'); 45 | // console.log(new Date(currentTime.getTime() - timeDelay)); 46 | // console.log(msgCreatedAt < new Date(currentTime.getTime() - timeDelay)); 47 | // console.log(''); 48 | 49 | return msgCreatedAt < new Date(currentTime.getTime() - delay); 50 | })); 51 | 52 | const preparedArr = messages 53 | .filter((message, index) => filteredMessages[index] && !message.isCounted) 54 | .map((message) => ({ 55 | id: message.id, 56 | msg_created_at: message.msg_created_at, 57 | isCounted: message.isCounted 58 | })); 59 | 60 | for (const unreadMessage of preparedArr) { 61 | const message = await this.messagesRepository.findOneBy({ 62 | id: unreadMessage.id, 63 | }); 64 | message.isCounted = true; 65 | 66 | const incident = new Incident(); 67 | incident.ofId = message.ofId; 68 | incident.msgId = message.msgId; 69 | incident.type = TIncident.Message; 70 | incident.agencyId = message.agencyId; 71 | incident.stopWords = ''; 72 | incident.managerId = message.managerId; 73 | 74 | let createdDate = new Date(); 75 | createdDate.setHours(createdDate.getHours()); 76 | incident.incident_created_at = createdDate.toISOString(); 77 | 78 | await this.incidentsRepository.save(incident).then(async (data) => { 79 | await this.messagesRepository.save(message); 80 | }); 81 | } 82 | } 83 | 84 | @Cron('*/20 * * * * *') 85 | async handleIncidentsCron() { 86 | console.log('chron handleIncidentsCron'); 87 | const incidents = await this.incidentsRepository.findBy({ 88 | isCounted: false, 89 | }); 90 | 91 | for (const incident of incidents) { 92 | try { 93 | await this.incidentsRepository.update({ id: incident.id }, { 94 | isCounted: true 95 | }); 96 | 97 | const agency = await this.agenciesRepository.findOneBy({ 98 | id: incident.agencyId, 99 | }); 100 | 101 | await this.notificationService.OnIncident(agency, incident); 102 | } catch (error) { 103 | console.error(`Err incident: ${error}`); 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/scrapper/dto/create-scrapper.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | 3 | export class CreateScrapperDto { 4 | @IsNotEmpty() 5 | email: string; 6 | @IsNotEmpty() 7 | password: string; 8 | @IsNotEmpty() 9 | ofId: string; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/scrapper/dto/update-scrapper.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | 3 | export class UpdateScrapperDto { 4 | @IsNotEmpty() 5 | email: string; 6 | @IsNotEmpty() 7 | password: string; 8 | @IsNotEmpty() 9 | ofId: string; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/scrapper/entities/scrapper.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { Status } from '../enums/scrapper.enum'; 3 | 4 | @Entity() 5 | export class Scrapper { 6 | @PrimaryGeneratedColumn() 7 | id: string; 8 | 9 | @Column({unique: true}) 10 | email: string; 11 | 12 | @Column({ nullable: true }) 13 | password: string; 14 | 15 | @Column({unique: true}) 16 | ofId: string; 17 | 18 | @Column({ enum: Status, default: Status.Offline }) 19 | status: Status; 20 | } -------------------------------------------------------------------------------- /src/scrapper/enums/scrapper.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Status { 2 | Online = 'online', 3 | Offline = 'offline', 4 | } -------------------------------------------------------------------------------- /src/scrapper/scrapper.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ScrapperController } from './scrapper.controller'; 3 | 4 | describe('ScrapperController', () => { 5 | let controller: ScrapperController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [ScrapperController], 10 | }).compile(); 11 | 12 | controller = module.get(ScrapperController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/scrapper/scrapper.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'; 2 | import { Auth } from '../iam/authentication/decorators/auth.decorator'; 3 | import { AuthType } from '../iam/authentication/enums/auth-type.enum'; 4 | import { ScrapperService } from './scrapper.service'; 5 | import { CreateScrapperDto } from './dto/create-scrapper.dto'; 6 | import { UpdateScrapperDto } from './dto/update-scrapper.dto'; 7 | 8 | // TODO: authType 9 | @Auth(AuthType.None) 10 | @Controller('scrapper') 11 | export class ScrapperController { 12 | constructor(private readonly scrapperService: ScrapperService) {} 13 | 14 | @Get() 15 | scrapperController() { 16 | return 'scrapper controller working'; 17 | } 18 | 19 | @Get('test') 20 | getScrappers() { 21 | return this.scrapperService.getScrappers(); 22 | } 23 | 24 | @Post() 25 | create(@Body() createScrapperDto: CreateScrapperDto) { 26 | return this.scrapperService.create(createScrapperDto); 27 | } 28 | 29 | // TODO: 30 | // Get cookies here, and find user next check role 31 | // @Roles(Role.Admin) 32 | @Get('all') 33 | findAll() { 34 | return this.scrapperService.findAllScrappers(); 35 | } 36 | 37 | @Get(':id') 38 | findOne(@Param('id') id: string) { 39 | return this.scrapperService.findOne(id); 40 | } 41 | 42 | @Patch(':id') 43 | update(@Param('id') id: string, @Body() updateScrapperDto: UpdateScrapperDto) { 44 | return this.scrapperService.update(id, updateScrapperDto); 45 | } 46 | 47 | @Delete(':id') 48 | remove(@Param('id') id: string) { 49 | return this.scrapperService.remove(id); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/scrapper/scrapper.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ScrapperController } from './scrapper.controller'; 3 | import { ScrapperService } from './scrapper.service'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Scrapper } from './entities/scrapper.entity'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([Scrapper])], 9 | controllers: [ScrapperController], 10 | providers: [ScrapperService] 11 | }) 12 | export class ScrapperModule {} 13 | -------------------------------------------------------------------------------- /src/scrapper/scrapper.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ScrapperService } from './scrapper.service'; 3 | 4 | describe('ScrapperService', () => { 5 | let service: ScrapperService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ScrapperService], 10 | }).compile(); 11 | 12 | service = module.get(ScrapperService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/scrapper/scrapper.service.ts: -------------------------------------------------------------------------------- 1 | import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { CreateScrapperDto } from './dto/create-scrapper.dto'; 5 | import { Scrapper } from './entities/scrapper.entity'; 6 | import { UpdateScrapperDto } from './dto/update-scrapper.dto'; 7 | 8 | @Injectable() 9 | export class ScrapperService { 10 | constructor( 11 | @InjectRepository(Scrapper) private readonly scrappersRepository: Repository, 12 | ){} 13 | 14 | async getScrappers() { 15 | return 'scrappers'; 16 | } 17 | 18 | async create(createScrapperDto: CreateScrapperDto) { 19 | 20 | try { 21 | const scrapper = new Scrapper(); 22 | scrapper.email = createScrapperDto.email; 23 | // scrapper.password = await this.hashingService.hash(createScrapperDto.password); 24 | scrapper.password = createScrapperDto.password; 25 | scrapper.ofId = createScrapperDto.ofId; 26 | 27 | await this.scrappersRepository.save(scrapper); 28 | return 'create' 29 | } catch (err) { 30 | const pgUniqueViolationErrorCode = '23505'; 31 | if (err.code === pgUniqueViolationErrorCode) { 32 | throw new ConflictException(); 33 | } 34 | throw err; 35 | } 36 | } 37 | 38 | async findAllScrappers() { 39 | return this.scrappersRepository.find(); 40 | } 41 | 42 | async findOne(id: string) { 43 | const scrapper = await this.scrappersRepository.findOneBy({ 44 | id: id, 45 | }); 46 | 47 | if (!scrapper) { 48 | throw new NotFoundException('Scrapper not found'); 49 | } 50 | 51 | return scrapper; 52 | } 53 | 54 | async update(id: string, updateScrapperDto: UpdateScrapperDto) { 55 | const existingScrapper = await this.scrappersRepository.findOneBy({ 56 | id: id, 57 | }); 58 | 59 | if (!existingScrapper) { 60 | throw new NotFoundException('Scrapper not found'); 61 | } 62 | 63 | existingScrapper.email = updateScrapperDto.email; 64 | existingScrapper.password = updateScrapperDto.password; 65 | existingScrapper.ofId = updateScrapperDto.ofId; 66 | 67 | await this.scrappersRepository.save(existingScrapper); 68 | 69 | return existingScrapper; 70 | } 71 | 72 | async remove(id: string) { 73 | const existingScrapper = await this.scrappersRepository.findOneBy({ 74 | id: id, 75 | }); 76 | 77 | if (!existingScrapper) { 78 | throw new NotFoundException('Scrapper not found'); 79 | } 80 | 81 | await this.scrappersRepository.remove(existingScrapper); 82 | 83 | return 'Scrapper has been removed'; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/telegram/telegram.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { TelegramController } from './telegram.controller'; 3 | 4 | describe('TelegramController', () => { 5 | let controller: TelegramController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [TelegramController], 10 | }).compile(); 11 | 12 | controller = module.get(TelegramController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/telegram/telegram.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { TelegramService } from './telegram.service'; 3 | 4 | @Controller('telegram') 5 | export class TelegramController { 6 | constructor(private readonly telegramService: TelegramService) {} 7 | 8 | // @Get() 9 | // telegramController() { 10 | // this.telegramService.sendToChat(process.env.TELEGRAM_CHAT_ID,'hello') 11 | // return 'telegram controller working'; 12 | // } 13 | } 14 | -------------------------------------------------------------------------------- /src/telegram/telegram.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TelegramService } from './telegram.service'; 3 | import { TelegramController } from './telegram.controller'; 4 | 5 | @Module({ 6 | providers: [TelegramService], 7 | controllers: [TelegramController] 8 | }) 9 | export class TelegramModule {} 10 | -------------------------------------------------------------------------------- /src/telegram/telegram.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { TelegramService } from './telegram.service'; 3 | 4 | describe('TelegramService', () => { 5 | let service: TelegramService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [TelegramService], 10 | }).compile(); 11 | 12 | service = module.get(TelegramService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/telegram/telegram.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | const TelegramBot = require('node-telegram-bot-api'); 3 | 4 | @Injectable() 5 | export class TelegramService { 6 | private static bot: any; 7 | 8 | constructor() { 9 | if (!TelegramService.bot) { 10 | TelegramService.bot = new TelegramBot(process.env.TELEGRAM_API, { polling: true }); 11 | } 12 | } 13 | 14 | async sendToChat(chatId: string, message: string) { 15 | await TelegramService.bot.sendMessage(chatId, message); 16 | } 17 | 18 | async sendNotificationTelegram(message: string) { 19 | await TelegramService.bot.sendMessage(process.env.TELEGRAM_CHAT_ID, message); 20 | } 21 | } -------------------------------------------------------------------------------- /src/users/api-keys/entities/api-key.entity/api-key.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { User } from '../../../entities/user.entity'; 3 | 4 | @Entity() 5 | export class ApiKey { 6 | @PrimaryGeneratedColumn () 7 | id: number; 8 | 9 | @Column () 10 | key: string; 11 | 12 | @Column () 13 | uuid: string; 14 | 15 | @ManyToOne ( (type) => User, (user) => user.apiKeys) 16 | user: User; 17 | 18 | // NOTE: As an exercise, you could create a new entity called "Scope" and establish 19 | // a Many-to-Many relationship between API Keys and Scopes. With the scopes feature, users 20 | // could selectively grant specific permissions/scopes to given API Keys. For example, some 21 | // API Keys could only let 3-rd party applications "Read" data, but not modify it, etc. 22 | // @Column () 23 | // scopes: Scopell; 24 | } 25 | -------------------------------------------------------------------------------- /src/users/dto/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateUserDto {} 2 | -------------------------------------------------------------------------------- /src/users/dto/findAll.dto.ts: -------------------------------------------------------------------------------- 1 | export interface FindAllParams { 2 | _expand: string; 3 | _limit: string; 4 | _page: string; 5 | _sort: string; 6 | _order: string; 7 | q: string; 8 | roles: string; 9 | } -------------------------------------------------------------------------------- /src/users/dto/update-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsOptional } from 'class-validator'; 2 | import { TUserFeatures, TUserTheme } from '../enums/user.settings'; 3 | 4 | // export class UpdateUserDto extends PartialType(CreateUserDto) {} 5 | 6 | export class UpdateUserDto { 7 | @IsOptional() 8 | isAccountsPageWasOpened?: boolean; 9 | 10 | @IsOptional() 11 | isCookieDefined?: boolean; 12 | 13 | @IsOptional() 14 | isArticlesPageWasOpened?: boolean; 15 | 16 | @IsOptional() 17 | theme?: TUserTheme; 18 | 19 | @IsOptional() 20 | features?: TUserFeatures; 21 | 22 | @IsOptional() 23 | isVisible?: boolean; 24 | } 25 | -------------------------------------------------------------------------------- /src/users/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | CreateDateColumn, 4 | Entity, 5 | JoinColumn, 6 | JoinTable, 7 | OneToMany, 8 | OneToOne, 9 | PrimaryGeneratedColumn, 10 | } from 'typeorm'; 11 | import { Role } from '../enums/role.enum'; 12 | import { Permission, PermissionType } from '../../iam/authorization/permission.type'; 13 | import { ApiKey } from '../api-keys/entities/api-key.entity/api-key.entity'; 14 | import { 15 | NotificationsSettings, 16 | NotificationsSource, 17 | NotificationsType, 18 | TUserFeatures, TUserJsonSettings, 19 | TUserSettings, 20 | } from '../enums/user.settings'; 21 | import { Profile } from '../../profile/entities/profile.entities'; 22 | 23 | @Entity() 24 | export class User { 25 | @PrimaryGeneratedColumn('uuid') 26 | id: string; 27 | 28 | @CreateDateColumn() 29 | created_at: Date; 30 | 31 | @Column({unique: true}) 32 | email: string; 33 | 34 | @Column({ nullable: true }) 35 | password: string; 36 | 37 | @Column({ nullable: true , default: null}) 38 | agencyId: string | null; 39 | 40 | @Column({ nullable: true , default: null}) 41 | invitedTo: string | null; 42 | 43 | @Column({ enum: Role, default: Role.Regular }) 44 | role: Role; 45 | 46 | @Column({ default: false }) 47 | isTfaEnabled: boolean; 48 | 49 | @Column({ nullable: true }) 50 | tfaSecret: string; 51 | 52 | @Column({ nullable: true }) 53 | googleId: string; 54 | 55 | @JoinTable() 56 | @OneToMany ( (type) => ApiKey, (apiKey) => apiKey.user) 57 | apiKeys: ApiKey[]; 58 | 59 | // NOTE: Having the "permissions" column in combination with the "role" 60 | // likely does not make sense. We use both in this course just to showcase 61 | // two different approaches to authorization. 62 | @Column({ enum: Permission, default: [], type: 'json' }) 63 | permissions: PermissionType[]; 64 | 65 | @Column('jsonb', { 66 | default: { 67 | notifications: { 68 | [NotificationsType.EMAIL]: { 69 | [NotificationsSource.COMMENTS]: true, 70 | [NotificationsSource.EVENTS]: true, 71 | [NotificationsSource.INFO]: true, 72 | }, 73 | [NotificationsType.PUSH]: { 74 | [NotificationsSource.COMMENTS]: true, 75 | [NotificationsSource.EVENTS]: true, 76 | [NotificationsSource.INFO]: true, 77 | }, 78 | }, 79 | isVisible: false 80 | } as TUserSettings, 81 | }) 82 | settings: TUserSettings; 83 | 84 | @Column({ nullable: false, default: '1' }) 85 | profileId: string; 86 | 87 | @Column({ default: false }) 88 | online: boolean; 89 | 90 | @Column({ nullable: true, default: '' }) 91 | username: string | null; 92 | 93 | @Column('enum', { array: true, enum: Role, default: [Role.Regular] }) 94 | roles: Role[]; 95 | 96 | @Column('jsonb', { 97 | default: { 98 | "isArticleRatingEnabled": true, 99 | "isCounterEnabled": true, 100 | "isAppRedesigned": false, 101 | } as TUserFeatures, 102 | }) 103 | features: TUserFeatures; 104 | 105 | @Column({ nullable: false, default: 'https://picsum.photos/800/600' }) 106 | avatar: string; 107 | 108 | @Column('jsonb', { 109 | default: { 110 | "isArticlesPageWasOpened": false, 111 | "theme": 'app_orange_theme', 112 | "isAccountsPageWasOpened": false, 113 | "isCookieDefined": false 114 | } as TUserJsonSettings, 115 | }) 116 | jsonSettings: TUserJsonSettings; 117 | 118 | @OneToOne(type => Profile) 119 | @JoinColumn() 120 | profile?: Profile; 121 | } -------------------------------------------------------------------------------- /src/users/enums/role.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Role { 2 | Regular = 'regular', 3 | Admin = 'admin', 4 | Manager = 'manager', 5 | Model = 'model', 6 | SuperUser = 'superuser' 7 | } -------------------------------------------------------------------------------- /src/users/enums/user.settings.ts: -------------------------------------------------------------------------------- 1 | export enum NotificationsType { 2 | PUSH = 'push', 3 | EMAIL = 'email', 4 | } 5 | 6 | export enum NotificationsSource { 7 | COMMENTS = 'comments', 8 | EVENTS = 'events', 9 | INFO = 'info', 10 | } 11 | 12 | export type NotificationsSettings = { 13 | [key in NotificationsType]: NotificationsSettingsItem 14 | }; 15 | 16 | export type NotificationsSettingsItem = { 17 | [key in NotificationsSource]: boolean 18 | }; 19 | 20 | 21 | export type TUserSettings = { 22 | notifications: NotificationsSettings, 23 | isVisible: boolean 24 | }; 25 | 26 | export type TUserFeatures = { 27 | [key: string]: boolean; 28 | }; 29 | 30 | export type TUserJsonSettings = { 31 | [key: string]: boolean | string; 32 | }; 33 | 34 | export type TUserTheme = 'app_orange_theme' | 'app_dark_theme' | 'app_light_theme'; -------------------------------------------------------------------------------- /src/users/users.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UsersController } from './users.controller'; 3 | import { UsersService } from './users.service'; 4 | 5 | describe('UsersController', () => { 6 | let controller: UsersController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [UsersController], 11 | providers: [UsersService], 12 | }).compile(); 13 | 14 | controller = module.get(UsersController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post, Body, Patch, Param, Delete, Req, Query } from '@nestjs/common'; 2 | import { UsersService } from './users.service'; 3 | import { CreateUserDto } from './dto/create-user.dto'; 4 | import { UpdateUserDto } from './dto/update-user.dto'; 5 | import { Auth } from '../iam/authentication/decorators/auth.decorator'; 6 | import { AuthType } from '../iam/authentication/enums/auth-type.enum'; 7 | import { Request } from 'express'; 8 | import { UpdateNotificationsRequestData } from '../notification/dto/update-notifications.dto'; 9 | 10 | @Auth(AuthType.Bearer, AuthType.ApiKey) 11 | @Controller('users') 12 | export class UsersController { 13 | constructor(private readonly usersService: UsersService) {} 14 | 15 | @Post() 16 | create(@Body() createUserDto: CreateUserDto) { 17 | return this.usersService.create(createUserDto); 18 | } 19 | 20 | @Get() 21 | findAll( 22 | @Query('_expand') _expand: string, 23 | @Query('_limit') _limit: string, 24 | @Query('_page') _page: string, 25 | @Query('_sort') _sort: string, 26 | @Query('_order') _order: string, 27 | @Query('q') q: string, 28 | @Query('roles') roles: string, 29 | ) { 30 | const params = { _expand, _limit, _page, _sort, _order, q, roles } 31 | 32 | return this.usersService.findAll(params); 33 | } 34 | 35 | @Get('me') 36 | findMe(@Req() request: Request) { 37 | const tokenParts = request.headers.authorization.split(' '); 38 | 39 | return this.usersService.findMe(tokenParts[1]); 40 | } 41 | 42 | @Post('settings/notifications') 43 | setNotificationsSettings(@Req() request: Request, @Body() settingsData: UpdateNotificationsRequestData) { 44 | const tokenParts = request.headers.authorization.split(' '); 45 | 46 | return this.usersService.setNotificationsSettings(tokenParts[1], settingsData); 47 | } 48 | 49 | @Get(':id') 50 | findOne( 51 | @Param('id') id: string, 52 | @Query('_expand') _expand: string 53 | ) { 54 | return this.usersService.findOne(id, _expand); 55 | } 56 | 57 | @Patch(':id') 58 | update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) { 59 | return this.usersService.update(id, updateUserDto); 60 | } 61 | 62 | @Delete('') 63 | remove(@Req() request: Request) { 64 | const tokenParts = request.headers.authorization.split(' '); 65 | 66 | return this.usersService.remove(tokenParts[1]); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UsersService } from './users.service'; 3 | import { UsersController } from './users.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { User } from './entities/user.entity'; 6 | import { ApiKey } from './api-keys/entities/api-key.entity/api-key.entity'; 7 | import { AdminNotifications } from '../notification/helpers/notification.admin'; 8 | import { NotificationService } from '../notification/notification.service'; 9 | import { Notification } from '../notification/entities/notification.entity'; 10 | import { ManagerNotifications } from '../notification/helpers/notification.manager'; 11 | import { AllNotifications } from '../notification/helpers/notification.all'; 12 | import { SendNotifications } from '../notification/helpers/notification.send'; 13 | import { SUserNotifications } from '../notification/helpers/notification.suser'; 14 | import { ConfigModule, ConfigService } from '@nestjs/config'; 15 | import { EmailService } from '../email/email.service'; 16 | import { TelegramService } from '../telegram/telegram.service'; 17 | import { Profile } from '../profile/entities/profile.entities'; 18 | 19 | @Module({ 20 | imports: [TypeOrmModule.forFeature([User, ApiKey, Notification, Profile]), ConfigModule.forRoot()], 21 | controllers: [UsersController], 22 | providers: [ 23 | UsersService, 24 | NotificationService, 25 | SendNotifications, 26 | AllNotifications, 27 | SUserNotifications, 28 | AdminNotifications, 29 | ManagerNotifications, 30 | EmailService, 31 | TelegramService, 32 | ConfigService 33 | ], 34 | }) 35 | export class UsersModule {} 36 | -------------------------------------------------------------------------------- /src/users/users.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UsersService } from './users.service'; 3 | 4 | describe('UsersService', () => { 5 | let service: UsersService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [UsersService], 10 | }).compile(); 11 | 12 | service = module.get(UsersService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { CreateUserDto } from './dto/create-user.dto'; 3 | import { UpdateUserDto } from './dto/update-user.dto'; 4 | import { InjectRepository } from '@nestjs/typeorm'; 5 | import { User } from './entities/user.entity'; 6 | import { Repository } from 'typeorm'; 7 | import jwtConfig from '../iam/config/jwt.config'; 8 | import { ConfigType } from '@nestjs/config'; 9 | import * as jwt from 'jsonwebtoken'; 10 | import { NotificationsSource, NotificationsType, TUserSettings } from './enums/user.settings'; 11 | import { UpdateNotificationsRequestData } from '../notification/dto/update-notifications.dto'; 12 | import { FindAllParams } from './dto/findAll.dto'; 13 | import { Profile } from '../profile/entities/profile.entities'; 14 | 15 | @Injectable() 16 | export class UsersService { 17 | constructor( 18 | @InjectRepository(User) private readonly usersRepository: Repository, 19 | @InjectRepository(Profile) private readonly profilesRepository: Repository 20 | ){} 21 | 22 | create(createUserDto: CreateUserDto) { 23 | return 'This action adds a new user'; 24 | } 25 | 26 | async findAll(params: FindAllParams) { 27 | const { _expand, _limit, _page, _sort, _order, q, roles } = params; 28 | const query = this.usersRepository.createQueryBuilder('user'); 29 | 30 | if (_expand && _sort !== 'username') { 31 | query.leftJoinAndSelect('user.profile', 'profile'); 32 | } 33 | 34 | if (_limit && _page) { 35 | const limit = parseInt(_limit, 10); 36 | const page = parseInt(_page, 10); 37 | query.skip((page - 1) * limit).take(limit); 38 | } 39 | 40 | if (_sort) { 41 | const order = _order === 'desc' ? 'DESC' : 'ASC'; 42 | 43 | if (_sort === 'username') { 44 | query.addSelect('profile.username'); 45 | query.leftJoin('user.profile', 'profile'); 46 | query.addOrderBy('profile.username', order); 47 | } else if (_sort === 'createdAt') { 48 | query.addOrderBy('user.created_at', order); 49 | } else if (_sort === 'online') { 50 | query.addOrderBy('user.online', order); 51 | } 52 | } 53 | 54 | if (q) { 55 | query 56 | .innerJoin('user.profile', 'profile') 57 | .where('profile.username LIKE :q', { q: `%${q}%` }); 58 | } 59 | 60 | if (roles) { 61 | const rolesArray = roles.toLowerCase().split(','); 62 | query.andWhere('user.roles @> :roles', { roles: rolesArray }); 63 | } 64 | 65 | // query.andWhere('profile.isVisible = :isVisible', { isVisible: true }); 66 | query.andWhere("user.settings @> :settings", { settings: { isVisible: true } }); 67 | 68 | const users = await query.getMany(); 69 | 70 | if (_expand) { 71 | const userProfiles = await Promise.all(users.map(async (user) => { 72 | const profile = await this.profilesRepository.findOneBy({ 73 | id: user.profileId, 74 | }); 75 | 76 | return { 77 | ...user, 78 | profile, 79 | }; 80 | })); 81 | 82 | return userProfiles; 83 | } 84 | 85 | return users; 86 | } 87 | 88 | async findMe(token: string) { 89 | const refreshTokenData = jwt.decode(token) as any; 90 | const email = refreshTokenData.email; 91 | const user = await this.usersRepository.findOneBy({ 92 | email: email, 93 | }); 94 | return user; 95 | } 96 | 97 | async setNotificationsSettings(token: string, settingsData: UpdateNotificationsRequestData) { 98 | const refreshTokenData = jwt.decode(token) as any; 99 | const email = refreshTokenData.email; 100 | const user = await this.usersRepository.findOneBy({ 101 | email: email, 102 | }); 103 | 104 | let settings = user.settings || {} as TUserSettings; 105 | settingsData.map((settingsItem) => { 106 | const { type, source, value } = settingsItem; 107 | if (settings?.notifications === undefined) { 108 | settings.notifications = { 109 | [NotificationsType.EMAIL]: { 110 | [NotificationsSource.COMMENTS]: true, 111 | [NotificationsSource.EVENTS]: true, 112 | [NotificationsSource.INFO]: true, 113 | }, 114 | [NotificationsType.PUSH]: { 115 | [NotificationsSource.COMMENTS]: true, 116 | [NotificationsSource.EVENTS]: true, 117 | [NotificationsSource.INFO]: true, 118 | } 119 | }; 120 | } 121 | if (settings?.notifications[type]) { 122 | settings.notifications[type][source] = !!value; 123 | } 124 | }); 125 | 126 | await this.usersRepository.save(user); 127 | 128 | return user; 129 | // return user.settings; 130 | } 131 | 132 | async findOne(id: string, _expand: string) { 133 | const user = await this.usersRepository.findOneBy({ 134 | id: id, 135 | }) 136 | 137 | if (_expand) { 138 | const profile = await this.profilesRepository.findOneBy({ 139 | id: user.profileId, 140 | }); 141 | 142 | return { 143 | ...user, 144 | profile, 145 | }; 146 | } 147 | 148 | return user; 149 | } 150 | 151 | async update(id: string, updateUserDto: UpdateUserDto) { 152 | const user = await this.usersRepository.findOneBy({ 153 | id: id, 154 | }) 155 | 156 | if (updateUserDto.isVisible !== undefined) { 157 | user.settings.isVisible = updateUserDto.isVisible; 158 | } 159 | 160 | if (updateUserDto.isAccountsPageWasOpened !== undefined) { 161 | user.jsonSettings['isAccountsPageWasOpened'] = updateUserDto.isAccountsPageWasOpened; 162 | } 163 | if (updateUserDto.isCookieDefined !== undefined) { 164 | user.jsonSettings['isCookieDefined'] = updateUserDto.isCookieDefined; 165 | } 166 | if (updateUserDto.theme !== undefined) { 167 | user.jsonSettings['theme'] = updateUserDto.theme; 168 | } 169 | if (updateUserDto.isArticlesPageWasOpened !== undefined) { 170 | user.jsonSettings['isArticlesPageWasOpened'] = updateUserDto.isArticlesPageWasOpened; 171 | } 172 | if (updateUserDto.theme !== undefined) { 173 | user.jsonSettings['theme'] = updateUserDto.theme; 174 | } 175 | 176 | if (updateUserDto.features) { 177 | if (updateUserDto.features['isArticleRatingEnabled'] !== undefined) { 178 | user.features['isArticleRatingEnabled'] = updateUserDto.features['isArticleRatingEnabled'] 179 | } 180 | if (updateUserDto.features['isCounterEnabled'] !== undefined) { 181 | user.features['isCounterEnabled'] = updateUserDto.features['isCounterEnabled'] 182 | } 183 | if (updateUserDto.features['isAppRedesigned'] !== undefined) { 184 | user.features['isAppRedesigned'] = updateUserDto.features['isAppRedesigned'] 185 | } 186 | } 187 | 188 | return await this.usersRepository.save(user); 189 | } 190 | 191 | async remove(token: string) { 192 | const refreshTokenData = jwt.decode(token) as any; 193 | const email = refreshTokenData.email; 194 | const user = await this.usersRepository.findOneBy({ 195 | email: email, 196 | }); 197 | 198 | const profile = await this.profilesRepository.findOneBy({ 199 | id: user.profileId, 200 | }); 201 | 202 | user ? this.usersRepository.remove(user) : null; 203 | profile ? this.profilesRepository.remove(profile) : null; 204 | 205 | return user; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false, 20 | "lib": [ 21 | "es2021.full", 22 | "dom" 23 | ] 24 | } 25 | } 26 | --------------------------------------------------------------------------------