├── .editorconfig
├── .github
├── dependabot.yml
└── workflows
│ └── dependabot-merge.yml
├── .gitignore
├── README.md
├── apps
├── backend
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── .prettierrc
│ ├── README.md
│ ├── nest-cli.json
│ ├── package.json
│ ├── public
│ ├── src
│ │ ├── app.controller.spec.ts
│ │ ├── app.controller.ts
│ │ ├── app.module.ts
│ │ ├── app.service.ts
│ │ ├── auth
│ │ │ ├── auth.controller.spec.ts
│ │ │ ├── auth.controller.ts
│ │ │ ├── auth.decorator.ts
│ │ │ ├── auth.dto.ts
│ │ │ ├── auth.guard.ts
│ │ │ ├── auth.module.ts
│ │ │ └── auth.service.ts
│ │ ├── config
│ │ │ ├── app.ts
│ │ │ └── config.schema.ts
│ │ ├── env.d.ts
│ │ ├── event
│ │ │ ├── event.controller.spec.ts
│ │ │ ├── event.controller.ts
│ │ │ └── event.module.ts
│ │ ├── main.ts
│ │ ├── message
│ │ │ ├── message.controller.spec.ts
│ │ │ ├── message.controller.ts
│ │ │ ├── message.dto.ts
│ │ │ ├── message.module.ts
│ │ │ ├── message.service.spec.ts
│ │ │ └── message.service.ts
│ │ ├── session
│ │ │ └── session.store.ts
│ │ ├── system
│ │ │ ├── system.controller.spec.ts
│ │ │ ├── system.controller.ts
│ │ │ ├── system.module.ts
│ │ │ ├── system.service.spec.ts
│ │ │ └── system.service.ts
│ │ └── whatsapp
│ │ │ ├── whatsapp.controller.spec.ts
│ │ │ ├── whatsapp.controller.ts
│ │ │ ├── whatsapp.module.ts
│ │ │ ├── whatsapp.service.spec.ts
│ │ │ └── whatsapp.service.ts
│ ├── test
│ │ ├── app.e2e-spec.ts
│ │ └── jest-e2e.json
│ ├── tsconfig.build.json
│ └── tsconfig.json
└── frontend
│ ├── .gitignore
│ ├── .vscode
│ └── extensions.json
│ ├── README.md
│ ├── index.html
│ ├── package.json
│ ├── public
│ ├── 000m.jpg
│ ├── login-bg.jpg
│ ├── logo.svg
│ └── vite.svg
│ ├── src
│ ├── App.vue
│ ├── layouts
│ │ └── Main.vue
│ ├── main.ts
│ ├── pages
│ │ ├── Home
│ │ │ ├── Home.vue
│ │ │ └── Widgets
│ │ │ │ ├── MemoryUsage.vue
│ │ │ │ ├── RecentMessage.vue
│ │ │ │ ├── StorageUsage.vue
│ │ │ │ ├── UptimeConnection.vue
│ │ │ │ ├── UptimeProcess.vue
│ │ │ │ ├── WAConnection.vue
│ │ │ │ └── WAProcess.vue
│ │ ├── Login.vue
│ │ └── NotFound.vue
│ ├── router.ts
│ ├── stores
│ │ ├── useProcess.ts
│ │ └── useToasts.ts
│ ├── style.css
│ └── vite-env.d.ts
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
├── libraries
├── database
│ ├── package.json
│ ├── prisma
│ │ └── schema.prisma
│ ├── src
│ │ ├── client.ts
│ │ ├── index.ts
│ │ └── seed.ts
│ └── tsconfig.json
└── whatsapp
│ ├── package.json
│ ├── src
│ ├── commands
│ │ ├── logout.ts
│ │ ├── memoryUsage.ts
│ │ ├── restart.ts
│ │ ├── sendMessage.ts
│ │ ├── start.ts
│ │ └── stop.ts
│ ├── main.ts
│ └── whatsapp
│ │ ├── index.ts
│ │ ├── whatsapp.helper.ts
│ │ ├── whatsapp.interface.ts
│ │ ├── whatsapp.logger.ts
│ │ ├── whatsapp.storage.ts
│ │ └── whatsapp.ts
│ └── tsconfig.json
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── screenshots
└── home.jpg
└── turbo.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | trim_trailing_whitespace = true
8 | indent_style = space
9 | indent_size = 2
10 | root = true
11 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: 'github-actions'
4 | directory: '/'
5 | schedule:
6 | interval: 'daily'
7 | open-pull-requests-limit: 30
8 |
9 | - package-ecosystem: 'npm'
10 | directory: '/'
11 | schedule:
12 | interval: 'daily'
13 | open-pull-requests-limit: 50
14 |
--------------------------------------------------------------------------------
/.github/workflows/dependabot-merge.yml:
--------------------------------------------------------------------------------
1 | name: Dependabot Auto Merge Pull Request
2 | on: pull_request_target
3 |
4 | permissions:
5 | pull-requests: write
6 | contents: write
7 |
8 | jobs:
9 | dependabot:
10 | runs-on: ubuntu-latest
11 |
12 | permissions:
13 | contents: write
14 | issues: write
15 | pull-requests: write
16 |
17 | if: ${{ github.actor == 'dependabot[bot]' }}
18 | steps:
19 | - name: Dependabot metadata
20 | id: metadata
21 | uses: dependabot/fetch-metadata@v1.6.0
22 | with:
23 | github-token: '${{ secrets.GITHUB_TOKEN }}'
24 |
25 | - name: Auto-merge Dependabot PRs for semver-minor updates
26 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}}
27 | run: gh pr merge --auto --merge "$PR_URL"
28 | env:
29 | PR_URL: ${{github.event.pull_request.html_url}}
30 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
31 |
32 | - name: Auto-merge Dependabot PRs for semver-patch updates
33 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
34 | run: gh pr merge --auto --merge "$PR_URL"
35 | env:
36 | PR_URL: ${{github.event.pull_request.html_url}}
37 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .turbo
2 | node_modules
3 | dist
4 | *.pem
5 | .env
6 | test.js
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
Whatsapp Gateway
3 | (Under Development)
4 |
5 |
6 | ## Preview
7 | 
8 |
--------------------------------------------------------------------------------
/apps/backend/.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 | '@typescript-eslint/no-unused-vars': 'off',
25 | "@typescript-eslint/ban-types": "error"
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/apps/backend/.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
38 | .env.local
39 |
--------------------------------------------------------------------------------
/apps/backend/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/apps/backend/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 | $ pnpm install
33 | ```
34 |
35 | ## Running the app
36 |
37 | ```bash
38 | # development
39 | $ pnpm run start
40 |
41 | # watch mode
42 | $ pnpm run start:dev
43 |
44 | # production mode
45 | $ pnpm run start:prod
46 | ```
47 |
48 | ## Test
49 |
50 | ```bash
51 | # unit tests
52 | $ pnpm run test
53 |
54 | # e2e tests
55 | $ pnpm run test:e2e
56 |
57 | # test coverage
58 | $ pnpm 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 |
--------------------------------------------------------------------------------
/apps/backend/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 |
--------------------------------------------------------------------------------
/apps/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backend",
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 | "dev": "nest start --watch",
13 | "debug": "nest start --debug --watch",
14 | "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 | "prepare": "ts-patch install",
22 | "keygen": "node -p \"require('crypto').randomBytes(16).toString('hex')\""
23 | },
24 | "dependencies": {
25 | "@fastify/static": "^6.12.0",
26 | "@nestia/core": "^2.3.4",
27 | "@nestjs/axios": "^3.0.0",
28 | "@nestjs/common": "^10.0.0",
29 | "@nestjs/config": "^3.1.1",
30 | "@nestjs/core": "^10.0.0",
31 | "@nestjs/event-emitter": "^2.0.2",
32 | "@nestjs/platform-express": "^10.0.0",
33 | "@nestjs/platform-fastify": "^10.2.8",
34 | "@nestjs/serve-static": "^4.0.0",
35 | "argon2": "^0.31.2",
36 | "axios": "^1.5.1",
37 | "database": "workspace:^",
38 | "diskusage": "^1.2.0",
39 | "express-session": "^1.17.3",
40 | "fastify": "^4.24.3",
41 | "joi": "^17.11.0",
42 | "reflect-metadata": "^0.1.13",
43 | "rxjs": "^7.8.1",
44 | "whatsapp": "workspace:^"
45 | },
46 | "devDependencies": {
47 | "@nestia/e2e": "^0.3.6",
48 | "@nestia/sdk": "^2.3.4",
49 | "@nestjs/cli": "^10.0.0",
50 | "@nestjs/schematics": "^10.0.0",
51 | "@nestjs/testing": "^10.0.0",
52 | "@types/express": "^4.17.17",
53 | "@types/express-session": "^1.17.10",
54 | "@types/jest": "^29.5.2",
55 | "@types/node": "^20.3.1",
56 | "@types/supertest": "^2.0.12",
57 | "@typescript-eslint/eslint-plugin": "^6.0.0",
58 | "@typescript-eslint/parser": "^6.0.0",
59 | "eslint": "^8.42.0",
60 | "eslint-config-prettier": "^9.0.0",
61 | "eslint-plugin-prettier": "^5.0.0",
62 | "jest": "^29.5.0",
63 | "nestia": "^5.0.3",
64 | "prettier": "^3.0.0",
65 | "source-map-support": "^0.5.21",
66 | "supertest": "^6.3.3",
67 | "ts-jest": "^29.1.0",
68 | "ts-loader": "^9.4.3",
69 | "ts-node": "^10.9.1",
70 | "ts-patch": "^3.0.2",
71 | "tsconfig-paths": "^4.2.0",
72 | "typescript": "^5.2.2",
73 | "typia": "^5.2.4"
74 | },
75 | "jest": {
76 | "moduleFileExtensions": [
77 | "js",
78 | "json",
79 | "ts"
80 | ],
81 | "rootDir": "src",
82 | "testRegex": ".*\\.spec\\.ts$",
83 | "transform": {
84 | "^.+\\.(t|j)s$": "ts-jest"
85 | },
86 | "collectCoverageFrom": [
87 | "**/*.(t|j)s"
88 | ],
89 | "coverageDirectory": "../coverage",
90 | "testEnvironment": "node"
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/apps/backend/public:
--------------------------------------------------------------------------------
1 | ../frontend/dist
--------------------------------------------------------------------------------
/apps/backend/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 |
--------------------------------------------------------------------------------
/apps/backend/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get, Req } from '@nestjs/common';
2 | import { AppService } from './app.service';
3 | import { Request } from 'express';
4 |
5 | @Controller()
6 | export class AppController {
7 | constructor(private readonly appService: AppService) {}
8 |
9 | @Get()
10 | getHello(@Req() req: Request): string {
11 | return this.appService.getHello();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/apps/backend/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 | import { WhatsappModule } from './whatsapp/whatsapp.module';
5 | import { MessageModule } from './message/message.module';
6 | import { EventEmitterModule } from '@nestjs/event-emitter';
7 | import { EventModule } from './event/event.module';
8 | import { join } from 'path';
9 | import { ServeStaticModule } from '@nestjs/serve-static';
10 | import { ConfigModule } from '@nestjs/config';
11 | import { SystemModule } from './system/system.module';
12 | import { AuthModule } from './auth/auth.module';
13 | import AppConfig from './config/app';
14 | import ConfigSchema from './config/config.schema';
15 | import { APP_GUARD } from '@nestjs/core';
16 | import { AuthGuard } from './auth/auth.guard';
17 |
18 | @Module({
19 | imports: [
20 | ConfigModule.forRoot({
21 | cache: true,
22 | envFilePath: ['.env.local', '.env'],
23 | isGlobal: true,
24 | load: [AppConfig],
25 | validationSchema: ConfigSchema,
26 | validationOptions: {
27 | allowUnknown: true,
28 | abortEarly: true,
29 | },
30 | }),
31 | ServeStaticModule.forRoot({
32 | rootPath: join(__dirname, '..', 'public'),
33 | }),
34 | EventEmitterModule.forRoot(),
35 | WhatsappModule,
36 | MessageModule,
37 | EventModule,
38 | SystemModule,
39 | AuthModule,
40 | ],
41 | controllers: [AppController],
42 | providers: [
43 | AppService,
44 | {
45 | provide: APP_GUARD,
46 | useClass: AuthGuard,
47 | },
48 | ],
49 | })
50 | export class AppModule {}
51 |
--------------------------------------------------------------------------------
/apps/backend/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 |
--------------------------------------------------------------------------------
/apps/backend/src/auth/auth.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AuthController } from './auth.controller';
3 |
4 | describe('AuthController', () => {
5 | let controller: AuthController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [AuthController],
10 | }).compile();
11 |
12 | controller = module.get(AuthController);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(controller).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/apps/backend/src/auth/auth.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Res,
4 | Req,
5 | ForbiddenException,
6 | Logger,
7 | HttpStatus,
8 | } from '@nestjs/common';
9 | import { TypedRoute, TypedBody } from '@nestia/core';
10 | import { SignInDto } from './auth.dto';
11 | import { Request, Response } from 'express';
12 | import { AuthService } from './auth.service';
13 | import { Auth } from './auth.decorator';
14 |
15 | @Controller('auth')
16 | export class AuthController {
17 | private readonly logger = new Logger(AuthController.name);
18 |
19 | constructor(private readonly auth: AuthService) {}
20 |
21 | @TypedRoute.Post('/login')
22 | /**
23 | * Signs in a user.
24 | *
25 | * @param {SignInDto} body - The sign-in data.
26 | * @param {Response} res - The response object.
27 | * @param {Request} req - The request object.
28 | * @return {Promise} - A promise that resolves when the sign-in process is complete.
29 | */
30 | async signIn(
31 | @TypedBody() body: SignInDto,
32 | @Res() res: Response,
33 | @Req() req: Request,
34 | ) {
35 | try {
36 | const user = await this.auth.signIn({
37 | username: body.username,
38 | password: body.password,
39 | });
40 |
41 | req.session.user = {
42 | uuid: user.id,
43 | };
44 |
45 | res.status(HttpStatus.OK).send({
46 | message: 'Successfully signed in',
47 | });
48 | } catch (error) {
49 | throw new ForbiddenException(error.message ?? 'Failed to sign in');
50 | }
51 | }
52 |
53 | @Auth('session')
54 | @TypedRoute.Post('/isLoggedIn')
55 | /**
56 | * Checks if the user is logged in.
57 | *
58 | * @param {@Req()} req - The request object.
59 | * @param {@Res()} res - The response object.
60 | * @return {Promise} - The response with the isLoggedIn status.
61 | */
62 | async isLoggedIn(@Req() req: Request, @Res() res: Response) {
63 | return res.status(HttpStatus.OK).send({
64 | isLoggedIn: !!req.session.user,
65 | });
66 | }
67 |
68 | @Auth('session')
69 | @TypedRoute.Post('/logout')
70 | /**
71 | * Destroys the session asynchronously.
72 | *
73 | * @param {@Req()} req - The request object.
74 | * @param {@Res()} res - The response object.
75 | * @return {Promise} A promise that resolves when the session is destroyed.
76 | */
77 | async logout(@Req() req: Request, @Res() res: Response) {
78 | /**
79 | * Destroys the session asynchronously.
80 | *
81 | * @return {Promise} A promise that resolves when the session is destroyed.
82 | */
83 | const destroy = () => {
84 | return new Promise((resolve, reject) => {
85 | req.session.destroy((err) => {
86 | resolve();
87 | });
88 |
89 | setTimeout(() => {
90 | this.logger.error('Failed to destroy session');
91 | }, 15_000);
92 | });
93 | };
94 |
95 | await destroy().catch(() => {});
96 |
97 | res.status(HttpStatus.OK).send({
98 | message: 'Successfully logged out',
99 | });
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/apps/backend/src/auth/auth.decorator.ts:
--------------------------------------------------------------------------------
1 | import { SetMetadata } from '@nestjs/common';
2 |
3 | export type AuthType = 'session' | 'token' | 'all' | undefined | null;
4 | export const AUTH_TYPE: AuthType = 'session';
5 | export const Auth = (authType: AuthType = 'session') =>
6 | SetMetadata(AUTH_TYPE, authType);
7 |
--------------------------------------------------------------------------------
/apps/backend/src/auth/auth.dto.ts:
--------------------------------------------------------------------------------
1 | import { tags } from 'typia';
2 |
3 | export class SignInDto {
4 | /**
5 | * The username of the user to sign in.
6 | *
7 | */
8 | username: string & tags.Pattern<'^[a-zA-Z0-9-_.]+$'> & tags.MinLength<4>;
9 |
10 | /**
11 | * The password of the user to sign in.
12 | *
13 | */
14 | password: string & tags.MinLength<6>;
15 | }
16 |
--------------------------------------------------------------------------------
/apps/backend/src/auth/auth.guard.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CanActivate,
3 | ExecutionContext,
4 | Injectable,
5 | UnauthorizedException,
6 | } from '@nestjs/common';
7 | import { Observable } from 'rxjs';
8 | import { Request } from 'express';
9 | import { Reflector } from '@nestjs/core';
10 | import { AUTH_TYPE, AuthType } from './auth.decorator';
11 |
12 | @Injectable()
13 | export class AuthGuard implements CanActivate {
14 | constructor(private reflector: Reflector) {}
15 |
16 | /**
17 | * Determines whether the user can activate the current route.
18 | *
19 | * @param {ExecutionContext} context - The execution context.
20 | * @return {boolean | Promise | Observable} - Returns a boolean, a promise that resolves to a boolean, or an observable that emits a boolean.
21 | */
22 | canActivate(
23 | context: ExecutionContext,
24 | ): boolean | Promise | Observable {
25 | const authType = this.reflector.getAllAndOverride(AUTH_TYPE, [
26 | context.getHandler(),
27 | context.getClass(),
28 | ]);
29 |
30 | if (!authType) {
31 | return true;
32 | }
33 |
34 | const request = context.switchToHttp().getRequest();
35 |
36 | const isSessionValid = () => (request.session.user?.uuid ? true : false);
37 | const isTokenValid = () => false;
38 |
39 | switch (authType) {
40 | case 'session':
41 | if (!isSessionValid())
42 | throw new UnauthorizedException('User must be logged in');
43 | break;
44 |
45 | case 'token':
46 | if (!isTokenValid())
47 | throw new UnauthorizedException('Token must be provided and valid');
48 | break;
49 |
50 | case 'all':
51 | const validSession = isSessionValid();
52 | const token = isTokenValid();
53 | if (!(validSession || token))
54 | throw new UnauthorizedException(
55 | 'User must be logged in or Token must be provided and valid',
56 | );
57 |
58 | break;
59 | }
60 |
61 | if (authType === 'session') {
62 | return isSessionValid();
63 | } else if (authType === 'token') {
64 | return isTokenValid();
65 | } else if (authType === 'all') {
66 | return isSessionValid() || isTokenValid();
67 | }
68 |
69 | return true;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/apps/backend/src/auth/auth.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AuthService } from './auth.service';
3 | import { AuthController } from './auth.controller';
4 |
5 | @Module({
6 | providers: [AuthService],
7 | controllers: [AuthController],
8 | })
9 | export class AuthModule {}
10 |
--------------------------------------------------------------------------------
/apps/backend/src/auth/auth.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { SignInDto } from './auth.dto';
3 | import { prisma } from 'database';
4 | import { verify } from 'argon2';
5 |
6 | @Injectable()
7 | export class AuthService {
8 | /**
9 | * Sign in a user.
10 | *
11 | * @param {SignInDto} params - The sign-in parameters.
12 | * @return {Promise} The signed-in user.
13 | */
14 | async signIn(params: SignInDto) {
15 | const user = await prisma.user.findFirst({
16 | where: {
17 | username: params.username,
18 | },
19 | });
20 |
21 | if (user === null) {
22 | throw new Error(`User ${params.username} not found`);
23 | } else if ((await verify(user.password, params.password)) === false) {
24 | throw new Error('Wrong password');
25 | }
26 |
27 | return user;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/apps/backend/src/config/app.ts:
--------------------------------------------------------------------------------
1 | export default () => ({
2 | port: parseInt(process.env.PORT ?? '3000', 10),
3 | hostname: process.env.HOSTNAME ?? '0.0.0.0',
4 | encryption: process.env.ENCRYPTION_KEY ?? '',
5 | frontend: {
6 | url: process.env.FRONTEND_URL?.split(',') ?? null,
7 | },
8 | });
9 |
--------------------------------------------------------------------------------
/apps/backend/src/config/config.schema.ts:
--------------------------------------------------------------------------------
1 | import * as Joi from 'joi';
2 |
3 | export default Joi.object({
4 | PORT: Joi.number().default(3000),
5 | HOST: Joi.string().ip().default('0.0.0.0'),
6 | ENCRYPTION_KEY: Joi.string().required(),
7 | FRONTEND_URL: Joi.string().required(),
8 | });
9 |
--------------------------------------------------------------------------------
/apps/backend/src/env.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'express-session' {
2 | interface SessionData {
3 | user?: {
4 | uuid: string;
5 | } | null;
6 | }
7 | }
8 |
9 | export {};
10 |
--------------------------------------------------------------------------------
/apps/backend/src/event/event.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { EventController } from './event.controller';
3 |
4 | describe('EventController', () => {
5 | let controller: EventController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [EventController],
10 | }).compile();
11 |
12 | controller = module.get(EventController);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(controller).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/apps/backend/src/event/event.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Sse, MessageEvent } from '@nestjs/common';
2 | import { EventEmitter2 } from '@nestjs/event-emitter';
3 | import { Observable, fromEvent, map, merge } from 'rxjs';
4 | import { WhatsappService } from '../whatsapp/whatsapp.service';
5 |
6 | @Controller('event')
7 | export class EventController {
8 | constructor(
9 | private readonly event: EventEmitter2,
10 | private readonly whatsapp: WhatsappService,
11 | ) {}
12 |
13 | @Sse('qr')
14 | getQr(): Observable {
15 | return fromEvent(this.event, 'qr.update').pipe(
16 | map((data: string) => {
17 | return new MessageEvent('message', { data });
18 | }),
19 | );
20 | }
21 |
22 | @Sse('connection-state')
23 | getConnectionState(): Observable {
24 | const connectionUpdate = fromEvent(this.event, 'connection.update');
25 | const processState = fromEvent(this.event, 'process.state');
26 | return merge(connectionUpdate, processState).pipe(
27 | map(() => {
28 | const {
29 | workerStartedAt,
30 | connectedAt,
31 | workerState,
32 | connectionState,
33 | hasSession,
34 | } = this.whatsapp;
35 |
36 | return new MessageEvent('message', {
37 | data: {
38 | connectionState,
39 | connectedAt,
40 | workerState,
41 | workerStartedAt,
42 | hasSession,
43 | },
44 | });
45 | }),
46 | );
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/apps/backend/src/event/event.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { EventController } from './event.controller';
3 |
4 | @Module({
5 | controllers: [EventController],
6 | })
7 | export class EventModule {}
8 |
--------------------------------------------------------------------------------
/apps/backend/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import { ConfigService } from '@nestjs/config';
4 | import * as session from 'express-session';
5 | import { NestExpressApplication } from '@nestjs/platform-express';
6 | import { SessionStore } from './session/session.store';
7 |
8 | async function bootstrap() {
9 | const app = await NestFactory.create(AppModule);
10 | const config = app.get(ConfigService);
11 |
12 | app.set('trust proxy', 1);
13 |
14 | app.use(
15 | session({
16 | secret: config.getOrThrow('encryption'),
17 | resave: false,
18 | rolling: true,
19 | saveUninitialized: false,
20 | cookie: {
21 | httpOnly: true,
22 | secure: 'auto',
23 | maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days
24 | },
25 | name: 'wases',
26 | store: new SessionStore(),
27 | }),
28 | );
29 |
30 | app.enableCors({
31 | origin: config.get('frontend.url') ?? true,
32 | credentials: true,
33 | });
34 |
35 | app.enableShutdownHooks();
36 |
37 | app.setGlobalPrefix('api');
38 |
39 | await app.listen(3000, '0.0.0.0');
40 | }
41 | bootstrap();
42 |
--------------------------------------------------------------------------------
/apps/backend/src/message/message.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { MessageController } from './message.controller';
3 |
4 | describe('MessageController', () => {
5 | let controller: MessageController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [MessageController],
10 | }).compile();
11 |
12 | controller = module.get(MessageController);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(controller).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/apps/backend/src/message/message.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, HttpException, Res } from '@nestjs/common';
2 | import { TypedRoute, TypedBody } from '@nestia/core';
3 | import { MessageService } from './message.service';
4 | import type { Response } from 'express';
5 | import { ISendMediaMessage, ISendTextMessage } from './message.dto';
6 |
7 | @Controller('message')
8 | export class MessageController {
9 | constructor(private readonly message: MessageService) {}
10 |
11 | @TypedRoute.Post('/sendTextMessage')
12 | /**
13 | * Send a text message.
14 | *
15 | * @param {Response} res - The response object.
16 | * @param {ISendTextMessage} body - The body of the message.
17 | * @return {Promise} - A promise that resolves to void.
18 | */
19 | async sendTextMessage(
20 | @Res() res: Response,
21 | @TypedBody() body: ISendTextMessage,
22 | ) {
23 | try {
24 | const result = await this.message.sendTextMessage({
25 | to: body.to,
26 | message: body.message,
27 | });
28 |
29 | return res.send(result);
30 | } catch (error) {
31 | throw new HttpException(
32 | error.message ?? error ?? 'Failed to send message',
33 | 500,
34 | );
35 | }
36 | }
37 |
38 | @TypedRoute.Post('/sendMediaMessage')
39 | async sendMediaMessage(
40 | @Res() res: Response,
41 | @TypedBody() body: ISendMediaMessage,
42 | ) {
43 | try {
44 | const result = await this.message.sendMediaMessage(body);
45 | return res.send({
46 | message: result,
47 | });
48 | } catch (error) {
49 | throw new HttpException(
50 | error.message ?? error ?? 'Failed to send message',
51 | 500,
52 | );
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/apps/backend/src/message/message.dto.ts:
--------------------------------------------------------------------------------
1 | export interface ISendTextMessage {
2 | /**
3 | * The phone number of the recipient.
4 | */
5 | to: string;
6 |
7 | /**
8 | * The message to be sent.
9 | */
10 | message: string;
11 | }
12 |
13 | export interface ISendMediaMessage {
14 | /**
15 | * The phone number of the recipient.
16 | */
17 | to: string;
18 |
19 | /**
20 | * The message to be sent.
21 | */
22 | message: string;
23 |
24 | /**
25 | * The media to be sent.
26 | */
27 | mediaUrl: string;
28 |
29 | /**
30 | * The media type.
31 | *
32 | * @default 'auto' auto detect mime-types and set to document for fallback
33 | */
34 | mediaType?: 'audio' | 'image' | 'video' | 'document' | 'auto';
35 |
36 | /**
37 | * The name of the file
38 | * only for document type
39 | */
40 | fileName?: string;
41 | }
42 |
--------------------------------------------------------------------------------
/apps/backend/src/message/message.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { MessageService } from './message.service';
3 | import { MessageController } from './message.controller';
4 | import { HttpModule } from '@nestjs/axios';
5 |
6 | @Module({
7 | imports: [HttpModule],
8 | providers: [MessageService],
9 | controllers: [MessageController],
10 | })
11 | export class MessageModule {}
12 |
--------------------------------------------------------------------------------
/apps/backend/src/message/message.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { MessageService } from './message.service';
3 |
4 | describe('MessageService', () => {
5 | let service: MessageService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [MessageService],
10 | }).compile();
11 |
12 | service = module.get(MessageService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/apps/backend/src/message/message.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { WhatsappService } from '../whatsapp/whatsapp.service';
3 | import { ISendMediaMessage, ISendTextMessage } from './message.dto';
4 | import { HttpService } from '@nestjs/axios';
5 | import { writeFileSync, mkdirSync, existsSync, rmSync } from 'fs';
6 | import { resolve as pathResolve } from 'path';
7 |
8 | @Injectable()
9 | export class MessageService {
10 | constructor(
11 | private readonly whatsapp: WhatsappService,
12 | private readonly http: HttpService,
13 | ) {}
14 |
15 | /**
16 | * Sends a text message.
17 | *
18 | * @param {ISendTextMessage} to - The recipient of the message.
19 | * @return {Promise} - A promise that resolves to the response of the send command.
20 | */
21 | async sendTextMessage({ to, message }: ISendTextMessage): Promise {
22 | try {
23 | // Limit the message to 4096 characters
24 | message = message.substring(0, 4096 - 1);
25 | const res = await this.whatsapp.sendCommand('SEND_MESSAGE', {
26 | to,
27 | data: {
28 | text: message,
29 | },
30 | });
31 |
32 | return res;
33 | } catch (error) {
34 | throw new Error(error.message ?? error ?? 'Failed to send message');
35 | }
36 | }
37 |
38 | /**
39 | * Sends a media message.
40 | *
41 | * @param {ISendMediaMessage} sendMediaMessage - Object containing the parameters for the media message.
42 | * @param {string} sendMediaMessage.to - The recipient of the media message.
43 | * @param {string} sendMediaMessage.mediaUrl - The URL of the media file to be sent.
44 | * @param {string} sendMediaMessage.message - The message to be sent along with the media file.
45 | * @param {string} sendMediaMessage.fileName - The name of the media file.
46 | * @param {string} sendMediaMessage.mediaType - The type of media (audio, image, video, document).
47 | * @return {Promise} Resolves with the response data if the message is sent successfully.
48 | * @throws {Error} Throws an error if the message fails to send.
49 | */
50 | async sendMediaMessage({
51 | to,
52 | mediaUrl,
53 | message,
54 | fileName,
55 | mediaType,
56 | }: ISendMediaMessage): Promise {
57 | const res = await this.detectMimeTypes(mediaUrl);
58 | const mimeTypes = res.mimeTypes ?? null;
59 | if (!mediaType || mediaType == 'auto') {
60 | mediaType = res.type as any;
61 | }
62 |
63 | let data: any = {};
64 | const mediaPath = new URL(mediaUrl);
65 |
66 | if (mediaType == 'audio') {
67 | data = {
68 | audio: {
69 | url: mediaPath,
70 | mimetype: mimeTypes,
71 | },
72 | };
73 | } else if (mediaType == 'image') {
74 | data = {
75 | image: {
76 | url: mediaPath,
77 | mimetype: mimeTypes,
78 | },
79 | caption: message,
80 | };
81 | } else if (mediaType == 'video') {
82 | data = {
83 | video: {
84 | url: mediaPath,
85 | mimetype: mimeTypes,
86 | },
87 | caption: message,
88 | };
89 | } else {
90 | data = {
91 | document: {
92 | url: mediaPath,
93 | mimetype: mimeTypes,
94 | },
95 | caption: message,
96 | mimetype: mimeTypes,
97 | fileName,
98 | };
99 | }
100 |
101 | try {
102 | // Limit the message to 4096 characters
103 | message = message.substring(0, 4096 - 1);
104 | const res = await this.whatsapp.sendCommand('SEND_MESSAGE', {
105 | to,
106 | data,
107 | });
108 |
109 | if (res.status) {
110 | return res.data;
111 | }
112 | throw new Error(res.data);
113 | } catch (error) {
114 | rmSync(mediaPath);
115 | throw new Error(error.message ?? error ?? 'Failed to send message');
116 | }
117 | }
118 |
119 | /**
120 | * Detects the MIME type of a given URL.
121 | *
122 | * @param {string} url - The URL to detect the MIME type for.
123 | * @return {Promise<{ type: string, mimeTypes: string }>} - The detected MIME type and its corresponding type.
124 | */
125 | async detectMimeTypes(
126 | url: string,
127 | ): Promise<{ type: string; mimeTypes: string }> {
128 | const header = (await this.http.axiosRef.head(url)).headers;
129 | const fileType =
130 | header?.['Content-Type']?.toString() ||
131 | header?.['content-type']?.toString();
132 |
133 | const imageMimeType = ['image/gif', 'image/jpeg', 'image/png'];
134 | const videoMimeType = [
135 | 'video/mpeg',
136 | 'video/mp4',
137 | 'video/quicktime',
138 | 'video/x-ms-wmv',
139 | 'video/x-msvideo',
140 | 'video/x-flv',
141 | 'video/webm',
142 | ];
143 | const audioMimeType = [
144 | 'audio/mpeg',
145 | 'audio/x-ms-wma',
146 | 'audio/vnd.rn-realaudio',
147 | 'audio/x-wav',
148 | ];
149 |
150 | if (!fileType) {
151 | return {
152 | type: 'document',
153 | mimeTypes: fileType,
154 | };
155 | } else if (imageMimeType.includes(fileType)) {
156 | return {
157 | type: 'image',
158 | mimeTypes: fileType,
159 | };
160 | } else if (videoMimeType.includes(fileType)) {
161 | return {
162 | type: 'video',
163 | mimeTypes: fileType,
164 | };
165 | } else if (audioMimeType.includes(fileType)) {
166 | return {
167 | type: 'audio',
168 | mimeTypes: fileType,
169 | };
170 | }
171 |
172 | return {
173 | type: 'document',
174 | mimeTypes: fileType,
175 | };
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/apps/backend/src/session/session.store.ts:
--------------------------------------------------------------------------------
1 | import { SessionData, Store } from 'express-session';
2 | import { prisma, Prisma } from 'database';
3 |
4 | /**
5 | * An object containing a list of sessions where the key is the
6 | * ID of the session and the value is the SessionData
7 | */
8 | export interface ISessions {
9 | [key: string]: SessionData;
10 | }
11 |
12 | /**
13 | * Runs a callback with a number of arguments on the next tick
14 | *
15 | * @param callback the function to run in the future
16 | * @param args the arguments for the `callback` function when it is run
17 | */
18 | export const defer = void, A extends unknown[]>(
19 | callback: T,
20 | ...args: A
21 | ) => {
22 | setImmediate(() => {
23 | callback(...args);
24 | });
25 | };
26 |
27 | export class SessionStore extends Store {
28 | private readonly model = prisma.userSession;
29 | private readonly logger = console;
30 | private checkInterval?: NodeJS.Timeout;
31 |
32 | constructor() {
33 | super();
34 | this.startInterval();
35 | }
36 |
37 | /**
38 | * Starts an interval for running a function periodically.
39 | *
40 | * @param {Function} onIntervalError - Optional callback function to handle errors that occur during the interval.
41 | * @return {void} This function does not return a value.
42 | */
43 | public startInterval(onIntervalError?: (err: unknown) => void): void {
44 | if (this.checkInterval) return;
45 |
46 | const ms = 1000 * 60 * 60 * 24; // 1 day
47 | this.stopInterval();
48 | this.checkInterval = setInterval(async () => {
49 | try {
50 | await this.prune();
51 | } catch (err: unknown) {
52 | if (onIntervalError !== undefined) onIntervalError(err);
53 | }
54 | }, Math.floor(ms));
55 | }
56 |
57 | /**
58 | * Stops the interval.
59 | *
60 | * @return {void} No return value.
61 | */
62 | public stopInterval(): void {
63 | if (this.checkInterval) clearInterval(this.checkInterval);
64 | }
65 |
66 | async prune() {
67 | this.logger.log('Checking for any expired sessions...');
68 | const sessions = await this.model.findMany({
69 | select: {
70 | expiresAt: true,
71 | sid: true,
72 | },
73 | });
74 |
75 | const p = this.model;
76 | for (const session of sessions) {
77 | const now = new Date();
78 | const remainingSec = (session.expiresAt.valueOf() - now.valueOf()) / 1000;
79 | this.logger.log(`session:${session.sid} expires in ${remainingSec}sec`);
80 | if (now.valueOf() >= session.expiresAt.valueOf()) {
81 | const sid = session.sid;
82 | this.logger.log(`Deleting session with sid: ${sid}`);
83 | const foundSession = await p.findUnique({ where: { sid } });
84 | if (foundSession !== null) await p.delete({ where: { sid } });
85 | }
86 | }
87 | }
88 |
89 | async all(
90 | callback?: (err?: unknown, all?: ISessions) => void,
91 | ): Promise {
92 | try {
93 | const sessions = await this.model.findMany({
94 | select: { sid: true, data: true },
95 | });
96 |
97 | const result = sessions
98 | .map(({ sid, data }) => [sid, data as unknown as SessionData] as const)
99 | .reduce(
100 | (prev, [sid, data]) => ({ ...prev, [sid]: data }),
101 | {},
102 | );
103 |
104 | if (callback) defer(callback, undefined, result);
105 |
106 | return result;
107 | } catch (e: unknown) {
108 | this.logger.error(`all(): ${String(e)}`);
109 | if (callback) defer(callback, e);
110 | }
111 | }
112 |
113 | /**
114 | * Clears the data in the model.
115 | *
116 | * @param {Function} [callback] - An optional callback function to be called when the operation is complete or an error occurs.
117 | * @return {Promise} - A promise that resolves when the data is cleared.
118 | */
119 | async clear(callback?: (err?: unknown) => void): Promise {
120 | try {
121 | await this.model.deleteMany();
122 |
123 | if (callback) defer(callback);
124 | } catch (e: unknown) {
125 | if (callback) defer(callback, e);
126 | }
127 | }
128 |
129 | /**
130 | * Destroys the session(s) identified by the given session ID(s).
131 | *
132 | * @param {string|string[]} sid - The session ID(s) to destroy.
133 | * @param {(err?: unknown) => void} [callback] - An optional callback function to be called after the session(s) have been destroyed.
134 | * @return {Promise} A promise that resolves when the session(s) have been destroyed.
135 | */
136 | async destroy(
137 | sid: string | string[],
138 | callback?: (err?: unknown) => void,
139 | ): Promise {
140 | try {
141 | if (Array.isArray(sid)) {
142 | await Promise.all(sid.map(async (id) => this.destroy(id, callback)));
143 | } else {
144 | // Calling deleteMany to prevent an error from being thrown. Fix for issue 91
145 | await this.model.deleteMany({
146 | where: { sid },
147 | });
148 | }
149 | } catch (e: unknown) {
150 | // NOTE: Attempts to delete non-existent sessions land here
151 | if (callback) defer(callback, e);
152 |
153 | return;
154 | }
155 |
156 | if (callback) defer(callback);
157 | }
158 |
159 | /**
160 | * Retrieves a session data object based on the session ID.
161 | *
162 | * @param {string} sid - The session ID.
163 | * @param {(err?: unknown, val?: SessionData) => void} [callback] - Optional callback function.
164 | * @return {Promise} A Promise that resolves to the session data object, or undefined if the session does not exist.
165 | */
166 | async get(
167 | sid: string,
168 | callback?: (err?: unknown, val?: SessionData) => void,
169 | ): Promise {
170 | const p = this.model;
171 |
172 | const session = await p
173 | .findUnique({
174 | where: { sid },
175 | })
176 | .catch(() => null);
177 |
178 | if (session === null) {
179 | callback?.();
180 | return undefined;
181 | }
182 |
183 | try {
184 | // If session has has expired (allowing for missing 'expiresAt' and 'sid' fields)
185 | if (
186 | session.sid &&
187 | session.expiresAt &&
188 | new Date().valueOf() >= session.expiresAt.valueOf()
189 | ) {
190 | this.logger.log(`Session with sid: ${sid} expired; deleting.`);
191 | await p.delete({ where: { sid } });
192 | callback?.();
193 | return undefined;
194 | }
195 |
196 | const result = session.data as unknown as SessionData;
197 | if (callback) defer(callback, undefined, result);
198 |
199 | return result;
200 | } catch (e: unknown) {
201 | this.logger.error(`get(): ${String(e)}`);
202 | if (callback) defer(callback, e);
203 | }
204 | }
205 |
206 | /**
207 | * Retrieves the length of the array and optionally calls the provided callback with the result.
208 | *
209 | * @param {function} callback - An optional callback function that will be called with the result or an error.
210 | * @param {unknown} err - The error object passed to the callback function if an error occurred.
211 | * @param {number} length - The length of the array.
212 | * @return {Promise} - A promise that resolves to the length of the array or undefined.
213 | */
214 | async length(
215 | callback?: (err: unknown, length: number) => void,
216 | ): Promise {
217 | try {
218 | const sessions = await this.model.findMany({
219 | select: { sid: true }, // Limit what gets sent back; can't be empty.
220 | });
221 |
222 | const itemCount = sessions.length;
223 | if (callback) defer(callback, undefined, itemCount);
224 |
225 | return itemCount;
226 | } catch (e: unknown) {
227 | if (callback) defer(callback, e, 0);
228 | }
229 | }
230 |
231 | /**
232 | * Sets a session data for a given session ID.
233 | *
234 | * @param {string} sid - The session ID.
235 | * @param {SessionData} session - The session data.
236 | * @param {(err?: unknown) => void} [callback] - An optional callback function.
237 | * @return {Promise} - A promise that resolves when the session data is set.
238 | */
239 | async set(
240 | sid: string,
241 | session: SessionData,
242 | callback?: (err?: unknown) => void,
243 | ): Promise {
244 | const expiresAt = session.cookie.expires ?? new Date();
245 |
246 | const data: Prisma.UserSessionCreateInput = {
247 | sid,
248 | expiresAt,
249 | data: session as unknown as Prisma.InputJsonObject,
250 | };
251 |
252 | try {
253 | await this.model.upsert({
254 | where: {
255 | sid,
256 | },
257 | update: data,
258 | create: data,
259 | });
260 | } catch (error) {
261 | this.logger.error(`set(): ${String(error)}`);
262 | if (callback) defer(callback, error);
263 | }
264 |
265 | if (callback) defer(callback);
266 | }
267 |
268 | /**
269 | * Updates the session data and expiresAt timestamp for a given session ID.
270 | *
271 | * @param {string} sid - The session ID.
272 | * @param {SessionData} session - The session data.
273 | * @param {(err?: unknown) => void} [callback] - An optional callback function.
274 | * @return {Promise} - A Promise that resolves when the update is complete.
275 | */
276 | async touch(
277 | sid: string,
278 | session: SessionData,
279 | callback?: (err?: unknown) => void,
280 | ): Promise {
281 | const expiresAt = session.cookie.expires ?? new Date();
282 |
283 | const data: Prisma.UserSessionCreateInput = {
284 | sid,
285 | expiresAt,
286 | data: session as unknown as Prisma.InputJsonObject,
287 | };
288 |
289 | try {
290 | await this.model.upsert({
291 | where: {
292 | sid,
293 | },
294 | update: data,
295 | create: data,
296 | });
297 | } catch (error) {
298 | this.logger.error(`touch(): ${String(error)}`);
299 | if (callback) defer(callback, error);
300 | }
301 |
302 | if (callback) defer(callback);
303 | }
304 | }
305 |
--------------------------------------------------------------------------------
/apps/backend/src/system/system.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { SystemController } from './system.controller';
3 |
4 | describe('SystemController', () => {
5 | let controller: SystemController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [SystemController],
10 | }).compile();
11 |
12 | controller = module.get(SystemController);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(controller).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/apps/backend/src/system/system.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Res } from '@nestjs/common';
2 | import { TypedRoute } from '@nestia/core';
3 | import { Response } from 'express';
4 | import { SystemService } from './system.service';
5 |
6 | @Controller('system')
7 | export class SystemController {
8 | constructor(private readonly system: SystemService) {}
9 |
10 | @TypedRoute.Get('/disk')
11 | async diskUsage(@Res() res: Response) {
12 | const data = await this.system.diskUsage();
13 | res.send(data);
14 | }
15 |
16 | @TypedRoute.Get('/memory')
17 | async memoryUsage(@Res() res: Response) {
18 | const data = await this.system.memoryUsage();
19 | res.send(data);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/apps/backend/src/system/system.module.ts:
--------------------------------------------------------------------------------
1 | import { Module, Global } from '@nestjs/common';
2 | import { SystemService } from './system.service';
3 | import { SystemController } from './system.controller';
4 |
5 | @Global()
6 | @Module({
7 | providers: [SystemService],
8 | controllers: [SystemController],
9 | })
10 | export class SystemModule {}
11 |
--------------------------------------------------------------------------------
/apps/backend/src/system/system.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { SystemService } from './system.service';
3 |
4 | describe('SystemService', () => {
5 | let service: SystemService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [SystemService],
10 | }).compile();
11 |
12 | service = module.get(SystemService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/apps/backend/src/system/system.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { WhatsappService } from '../whatsapp/whatsapp.service';
3 | import { check, DiskUsage } from 'diskusage';
4 | import { totalmem, freemem } from 'os';
5 | import { memoryUsage } from 'process';
6 |
7 | @Injectable()
8 | export class SystemService {
9 | constructor(private readonly whatsapp: WhatsappService) {}
10 |
11 | /**
12 | * Calculates the disk usage of the system.
13 | *
14 | * @return {Promise} The total disk usage in bytes.
15 | */
16 | async diskUsage(): Promise {
17 | return check('/');
18 | }
19 |
20 | /**
21 | * Retrieves the memory usage of the application and server.
22 | *
23 | * @returns {object} The memory usage information.
24 | * - `process`: The memory usage of the application process.
25 | * - `server`: The memory usage of the server.
26 | * - `other`: The memory usage of other resources.
27 | * - `total`: The total memory available.
28 | */
29 | async memoryUsage() {
30 | const processUsage = (
31 | await this.whatsapp.sendCommand('MEMORY_USAGE')
32 | ).data;
33 | const serverUsage = memoryUsage.rss();
34 | const total = totalmem();
35 | const other = freemem() - serverUsage - serverUsage;
36 |
37 | return {
38 | process: processUsage,
39 | server: serverUsage,
40 | other: other,
41 | total: total,
42 | };
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/apps/backend/src/whatsapp/whatsapp.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { WhatsappController } from './whatsapp.controller';
3 |
4 | describe('WhatsappController', () => {
5 | let controller: WhatsappController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [WhatsappController],
10 | }).compile();
11 |
12 | controller = module.get(WhatsappController);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(controller).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/apps/backend/src/whatsapp/whatsapp.controller.ts:
--------------------------------------------------------------------------------
1 | import { HttpException, Controller, Res } from '@nestjs/common';
2 | import { TypedRoute } from '@nestia/core';
3 | import { WhatsappService } from './whatsapp.service';
4 | import { Response } from 'express';
5 |
6 | @Controller('whatsapp')
7 | export class WhatsappController {
8 | constructor(private whatsapp: WhatsappService) {}
9 |
10 | @TypedRoute.Get('/')
11 | async index(@Res() res: Response) {
12 | const {
13 | workerStartedAt,
14 | connectedAt,
15 | workerState,
16 | connectionState,
17 | hasSession,
18 | qr,
19 | } = this.whatsapp;
20 |
21 | return res.send({
22 | connectionState,
23 | connectedAt,
24 | workerState,
25 | workerStartedAt,
26 | hasSession,
27 | qr,
28 | });
29 | }
30 |
31 | @TypedRoute.Get('/start')
32 | /**
33 | * Starts the asynchronous operation.
34 | *
35 | * @return {Promise<{ status: boolean }>} A promise that resolves with an object containing the status.
36 | * @throws {HttpException} If an error occurs during the operation.
37 | */
38 | async start() {
39 | try {
40 | await this.whatsapp.start();
41 |
42 | return {
43 | status: true,
44 | };
45 | } catch (error) {
46 | throw new HttpException(
47 | {
48 | message: error?.message ?? 'Failed to start connection',
49 | },
50 | 500,
51 | );
52 | }
53 | }
54 |
55 | @TypedRoute.Get('/stop')
56 | /**
57 | * Stops the execution of the function.
58 | *
59 | * @return {Promise<{ status: boolean }>} An object indicating the status of the function execution.
60 | * @throws {HttpException} If there was an error stopping the connection.
61 | */
62 | async stop() {
63 | try {
64 | await this.whatsapp.stop();
65 |
66 | return {
67 | status: true,
68 | };
69 | } catch (error) {
70 | throw new HttpException(
71 | {
72 | message: error?.message ?? 'Failed to stop connection',
73 | },
74 | 500,
75 | );
76 | }
77 | }
78 |
79 | @TypedRoute.Get('restart')
80 | /**
81 | * Restarts the function by stopping and then starting it.
82 | *
83 | * @throws {HttpException} If there is an error during restart, a 500 HTTP exception is thrown with a message.
84 | */
85 | async restart() {
86 | try {
87 | await this.stop();
88 | await this.start();
89 | } catch (error) {
90 | throw new HttpException(
91 | {
92 | message: error?.message ?? 'Failed to restart connection',
93 | },
94 | 500,
95 | );
96 | }
97 | }
98 |
99 | @TypedRoute.Get('connect')
100 | /**
101 | * Asynchronously connects to the worker.
102 | *
103 | * @return {Promise<{ status: boolean }>} An object with the status of the connection.
104 | * @throws {HttpException} If failed to connect to the worker.
105 | */
106 | async connect() {
107 | try {
108 | await this.whatsapp.connect();
109 |
110 | return {
111 | status: true,
112 | };
113 | } catch (error) {
114 | throw new HttpException(
115 | {
116 | message: error?.message ?? 'Failed to connect to the worker',
117 | },
118 | 500,
119 | );
120 | }
121 | }
122 |
123 | @TypedRoute.Get('disconnect')
124 | /**
125 | * Disconnects from the worker.
126 | *
127 | * @return {Promise<{ status: boolean }>} The status of the disconnection.
128 | * @throws {HttpException} If there was an error disconnecting from the worker.
129 | */
130 | async disconnect() {
131 | try {
132 | await this.whatsapp.disconnect();
133 |
134 | return {
135 | status: true,
136 | };
137 | } catch (error) {
138 | throw new HttpException(
139 | {
140 | message: error?.message ?? 'Failed to disconnect from the worker',
141 | },
142 | 500,
143 | );
144 | }
145 | }
146 |
147 | @TypedRoute.Get('reconnect')
148 | /**
149 | * Reconnects to the worker.
150 | *
151 | * @return {Promise} A promise that resolves when the reconnection is successful.
152 | * @throws {HttpException} If there is an error during reconnection.
153 | */
154 | async reconnect() {
155 | try {
156 | await this.disconnect();
157 | await this.connect();
158 | } catch (error) {
159 | throw new HttpException(
160 | {
161 | message: error?.message ?? 'Failed to reconnect to the worker',
162 | },
163 | 500,
164 | );
165 | }
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/apps/backend/src/whatsapp/whatsapp.module.ts:
--------------------------------------------------------------------------------
1 | import { Module, Global } from '@nestjs/common';
2 | import { WhatsappService } from './whatsapp.service';
3 | import { WhatsappController } from './whatsapp.controller';
4 |
5 | @Global()
6 | @Module({
7 | providers: [WhatsappService],
8 | controllers: [WhatsappController],
9 | exports: [WhatsappService],
10 | })
11 | export class WhatsappModule {}
12 |
--------------------------------------------------------------------------------
/apps/backend/src/whatsapp/whatsapp.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { WhatsappService } from './whatsapp.service';
3 |
4 | describe('WhatsappService', () => {
5 | let service: WhatsappService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [WhatsappService],
10 | }).compile();
11 |
12 | service = module.get(WhatsappService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/apps/backend/src/whatsapp/whatsapp.service.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Logger,
3 | Injectable,
4 | OnModuleInit,
5 | OnModuleDestroy,
6 | } from '@nestjs/common';
7 | import { fork, type ChildProcess } from 'child_process';
8 | import { resolve as pathResolve } from 'path';
9 | import { EventEmitter2 } from '@nestjs/event-emitter';
10 | import { prisma } from 'database';
11 |
12 | interface ISendCommand {
13 | command: string;
14 | status: boolean;
15 | data: K;
16 | }
17 |
18 | @Injectable()
19 | export class WhatsappService implements OnModuleInit, OnModuleDestroy {
20 | private _worker: ChildProcess | null = null;
21 | private workerPath: string;
22 | private logger = new Logger(WhatsappService.name);
23 | private _connectionState: 'open' | 'connecting' | 'close' = 'close';
24 | private _workerStartedAt: Date | null = null;
25 | private _connectedAt: Date | null = null;
26 | private _hasSession = false;
27 | private _qr: string | null = null;
28 |
29 | /**
30 | * Constructor for the class.
31 | *
32 | */
33 | constructor(private readonly event: EventEmitter2) {
34 | const home = pathResolve(process.cwd(), '..', '..');
35 | this.workerPath = pathResolve(home, 'libraries/whatsapp/dist/main.js');
36 | }
37 |
38 | /**
39 | * Executes the necessary cleanup tasks when the module is destroyed.
40 | *
41 | * No parameters are required.
42 | *
43 | * Does not return any value.
44 | */
45 | onModuleDestroy() {
46 | try {
47 | this.stop().then(() => {
48 | this.disconnect();
49 | });
50 | } catch (error) {
51 | this.logger.error(error);
52 | }
53 | }
54 |
55 | /**
56 | * Called when the module is initialized.
57 | *
58 | * @return {Promise} Returns a promise that resolves when the function finishes executing.
59 | */
60 | async onModuleInit(): Promise {
61 | await this.connect();
62 | }
63 |
64 | /**
65 | * Sends a command to the worker and returns a promise that resolves with the response.
66 | *
67 | * @param {string} command - The command to be sent to the worker.
68 | * @param {any} data - Optional data to be sent along with the command.
69 | * @return {Promise} A promise that resolves with the response from the worker.
70 | */
71 | async sendCommand(
72 | command: string,
73 | data: any = null,
74 | ): Promise> {
75 | return new Promise((resolve, reject) => {
76 | if (!this._worker || this._worker.killed) {
77 | reject(new Error('Worker is not running'));
78 | return;
79 | }
80 |
81 | const res = this._worker.send({
82 | command,
83 | data,
84 | });
85 |
86 | if (res) {
87 | /**
88 | * A callback function that handles the response from the worker.
89 | *
90 | * @param {any} response - The response object from the worker.
91 | */
92 | const callback = (response: any) => {
93 | if (response.command === command) {
94 | this._worker?.removeListener('message', callback);
95 | resolve(response);
96 | }
97 | };
98 |
99 | this._worker.on('message', callback);
100 |
101 | setTimeout(() => {
102 | reject(new Error('Failed to send command, timeout'));
103 | }, 10_000);
104 |
105 | return;
106 | }
107 |
108 | reject(new Error('Failed to send command'));
109 | });
110 | }
111 |
112 | /**
113 | * Starts the function asynchronously.
114 | *
115 | * @return {Promise} A Promise that resolves once the function has started.
116 | */
117 | async start(): Promise {
118 | return new Promise((resolve, reject) => {
119 | if (!this._worker || this._worker.killed) {
120 | reject(new Error('Worker is not running'));
121 | return;
122 | }
123 |
124 | const res = this._worker?.send({
125 | command: 'START',
126 | });
127 |
128 | if (res) {
129 | const callback = (response: any) => {
130 | if (
131 | !(
132 | response.command === 'CONNECTION_UPDATED' &&
133 | response.data.status !== 'close'
134 | )
135 | ) {
136 | return;
137 | }
138 | this._worker?.removeListener('message', callback);
139 | resolve();
140 | return;
141 | };
142 |
143 | this._worker.on('message', callback);
144 | setTimeout(() => {
145 | this._worker?.removeListener('message', callback);
146 | reject(new Error('Failed to start worker, timeout'));
147 | }, 10_000);
148 | return;
149 | }
150 |
151 | reject(new Error('Failed to start worker'));
152 | });
153 | }
154 |
155 | /**
156 | * Stops the execution of the function and returns a promise that resolves when the function is stopped or rejects with an error message.
157 | *
158 | * @return {Promise} A promise that resolves when the function is stopped or rejects with an error message.
159 | */
160 | async stop(): Promise {
161 | return new Promise((resolve, reject) => {
162 | if (!this._worker || this._worker.killed) {
163 | resolve();
164 | return;
165 | }
166 |
167 | const res = this._worker?.send({
168 | command: 'STOP',
169 | });
170 |
171 | if (res) {
172 | const callback = (response: any) => {
173 | if (
174 | !(
175 | response.command === 'CONNECTION_UPDATED' &&
176 | response.data.status == 'close'
177 | )
178 | ) {
179 | return;
180 | }
181 | this._worker?.removeListener('message', callback);
182 | resolve();
183 | return;
184 | };
185 |
186 | this._worker.on('message', callback);
187 | setTimeout(() => {
188 | this._worker?.removeListener('message', callback);
189 | reject(new Error('Failed to stop whatsapp server, timeout'));
190 | }, 10_000);
191 | return;
192 | }
193 |
194 | reject(new Error('Failed to stop to whatsapp server'));
195 | });
196 | }
197 |
198 | /**
199 | * Asynchronously restarts the function.
200 | *
201 | * @return {Promise} Returns a Promise that resolves to true if the function is successfully restarted, or false if an error occurs.
202 | */
203 | async restart() {
204 | try {
205 | await this.start();
206 | await this.stop();
207 | return true;
208 | } catch (error) {
209 | this.logger.error(error);
210 | return false;
211 | }
212 | }
213 |
214 | /**
215 | * Connects to the worker process.
216 | *
217 | * @return {Promise} Returns a promise that resolves to true if the connection is successful, or false if there is already an existing connection.
218 | */
219 | async connect(): Promise {
220 | if (this._worker) {
221 | return false;
222 | }
223 |
224 | this._worker = fork(this.workerPath, {
225 | detached: true,
226 | stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
227 | });
228 |
229 | this._worker?.unref();
230 | this._worker.stdout?.on('data', (res) => {
231 | console.log(res.toString());
232 | });
233 |
234 | this._worker.stderr?.on('data', (res) => {
235 | console.log(res.toString());
236 | });
237 |
238 | this._worker.once('spawn', () => {
239 | this._workerStartedAt = new Date();
240 | this.event.emit('process.state', 'connected');
241 | });
242 |
243 | this._worker.on('error', (err) => {
244 | this.logger.fatal(
245 | `Failed to spawn whatsapp process: ${err.message}`,
246 | err.stack,
247 | );
248 | });
249 |
250 | this._worker.on('exit', () => {
251 | this._worker?.removeAllListeners();
252 | this._worker?.stderr?.removeAllListeners();
253 | this._worker?.stdin?.removeAllListeners();
254 | this._worker?.stdout?.removeAllListeners();
255 | this._worker?.kill('SIGKILL');
256 | this._workerStartedAt = null;
257 | this._connectedAt = null;
258 | this._connectionState = 'close';
259 | this._qr = null;
260 | this._worker = null;
261 | this.event.emit('process.state', 'disconnected');
262 | });
263 |
264 | this._worker.on('message', async (res: any) => {
265 | switch (res.command) {
266 | case 'CONNECTION_UPDATED':
267 | this._connectionState = res.data.status;
268 | this._connectedAt =
269 | this._connectionState === 'open' ? new Date() : null;
270 | this._hasSession = (await prisma.session.count()) > 0;
271 | if (this._connectionState !== 'connecting') {
272 | this._qr = null;
273 | }
274 | this.event.emit('connection.update', this._connectionState);
275 | break;
276 | case 'QR_UPDATED':
277 | this._qr = res.data;
278 | this.event.emit('qr.update', res.data);
279 | break;
280 | }
281 | });
282 |
283 | return true;
284 | }
285 |
286 | /**
287 | * Disconnects the worker process.
288 | *
289 | * @return {Promise} A promise that resolves to true if the worker was successfully disconnected, or rejects with an error message if there was an issue disconnecting the worker.
290 | */
291 | async disconnect(): Promise {
292 | return new Promise((resolve, reject) => {
293 | if (!this._worker || this._worker?.killed) {
294 | resolve(true);
295 | return;
296 | }
297 |
298 | const res = this._worker.kill('SIGTERM');
299 | if (res) {
300 | const callback = () => {
301 | this._worker?.stdin?.removeAllListeners();
302 | this._worker?.stdout?.removeAllListeners();
303 | this._worker?.stderr?.removeAllListeners();
304 | this._worker?.removeAllListeners();
305 | this._workerStartedAt = null;
306 | this._connectedAt = null;
307 | this._connectionState = 'close';
308 | this._qr = null;
309 | this.event.emit('process.state', 'disconnected');
310 | resolve(true);
311 | };
312 | this._worker.once('exit', callback);
313 | setTimeout(() => {
314 | reject(new Error('Failed to kill worker, timeout'));
315 | }, 10_000);
316 | return;
317 | }
318 |
319 | reject(new Error('Failed to kill worker'));
320 | });
321 | }
322 |
323 | /**
324 | * Reconnects to the worker process.
325 | *
326 | * @return {Promise} A promise that resolves to true if the reconnection is successful, otherwise false.
327 | */
328 | async reconnect(): Promise {
329 | try {
330 | await this.disconnect();
331 | await this.connect();
332 | return true;
333 | } catch (error) {
334 | this.logger.error(error);
335 | return false;
336 | }
337 | }
338 |
339 | /**
340 | * Returns the worker.
341 | *
342 | * @return {ChildProcess | null} the worker
343 | */
344 | get worker(): ChildProcess | null {
345 | return this._worker;
346 | }
347 |
348 | /**
349 | * Retrieves the current connection state.
350 | *
351 | * @return {"open" | "connecting" | "close"} The current connection state.
352 | */
353 | get connectionState(): 'open' | 'connecting' | 'close' {
354 | return this._connectionState ?? 'close';
355 | }
356 |
357 | /**
358 | * Retrieves the current worker state.
359 | *
360 | * @return {"connected" | "disconnected"} The current worker state.
361 | */
362 | get workerState(): 'connected' | 'disconnected' {
363 | if (!this._worker) {
364 | return 'disconnected';
365 | }
366 | return this._worker?.connected ? 'connected' : 'disconnected';
367 | }
368 |
369 | /**
370 | * Get the date when the object was connected.
371 | *
372 | * @return {Date | null} The date when the object was connected.
373 | */
374 | get connectedAt(): Date | null {
375 | return this._connectedAt;
376 | }
377 |
378 | /**
379 | * Get the value of the workerStartedAt property.
380 | *
381 | * @return {Date | null} The value of the workerStartedAt property.
382 | */
383 | get workerStartedAt(): Date | null {
384 | return this._workerStartedAt;
385 | }
386 |
387 | /**
388 | * Returns a boolean value indicating whether a session exists.
389 | *
390 | * @return {boolean} The value indicating whether a session exists.
391 | */
392 | get hasSession(): boolean {
393 | return this._hasSession;
394 | }
395 |
396 | /**
397 | * Get the value of qr.
398 | *
399 | * @return {boolean} The value of qr.
400 | */
401 | get qr(): string | null {
402 | return this._qr;
403 | }
404 | }
405 |
--------------------------------------------------------------------------------
/apps/backend/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 |
--------------------------------------------------------------------------------
/apps/backend/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 |
--------------------------------------------------------------------------------
/apps/backend/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/apps/backend/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": true,
16 | "noImplicitAny": true,
17 | "strictBindCallApply": true,
18 | "forceConsistentCasingInFileNames": true,
19 | "noFallthroughCasesInSwitch": true,
20 | "plugins": [
21 | {
22 | "transform": "@nestia/core/lib/transform",
23 | /**
24 | * Validate request body.
25 | *
26 | * - "assert": Use typia.assert() function
27 | * - "is": Use typia.is() function
28 | * - "validate": Use typia.validate() function
29 | * - "assertEquals": Use typia.assertEquals() function
30 | * - "equals": Use typia.equals() function
31 | * - "validateEquals": Use typia.validateEquals() function
32 | */
33 | "validate": "assert",
34 | /**
35 | * Validate JSON typed response body.
36 | *
37 | * - "assert": Use typia.assertStringify() function
38 | * - "is": Use typia.isStringify() function
39 | * - "validate": Use typia.validateStringify() function
40 | * - "stringify": Use typia.stringify() function, but dangerous
41 | * - null: Just use JSON.stringify() function, without boosting
42 | */
43 | "stringify": "assert"
44 | },
45 | {
46 | "transform": "typia/lib/transform"
47 | }
48 | ]
49 | }
50 | }
--------------------------------------------------------------------------------
/apps/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | !.env
27 | .env.local
28 |
--------------------------------------------------------------------------------
/apps/frontend/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
3 | }
4 |
--------------------------------------------------------------------------------
/apps/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Vue 3 + TypeScript + Vite
2 |
3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `
15 |