├── .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 |
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 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
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 |
--------------------------------------------------------------------------------