= [
11 | {
12 | title: 'Activate Account',
13 | slug: 'activate-account',
14 | sender: 'noreply@truthy.com',
15 | subject: 'Activate Account',
16 | isDefault: true,
17 | body: "Hi {{username}},
A new account has been created using your email . Click below button to activate your account.
{{link}}
If you haven't requested the code please ignore the email.
Thank you!.
"
18 | },
19 | {
20 | title: 'Two Factor Authentication',
21 | slug: 'two-factor-authentication',
22 | sender: 'noreply@truthy.com',
23 | subject: 'Activate Two Factor Authentication',
24 | isDefault: true,
25 | body: "Hi {{username}},
This mail is sent because you requested to enable two factor authentication. To configure authentication via TOTP on multiple devices, during setup, scan the QR code using each device at the same time.

A time-based one-time password (TOTP) application automatically generates an authentication code that changes after a certain period of time. We recommend using cloud-based TOTP apps such as:
If you haven't requested the code please ignore the email.
Thank you!.
"
26 | },
27 | {
28 | title: 'Reset Password',
29 | slug: 'reset-password',
30 | sender: 'noreply@truthy.com',
31 | subject: 'Reset Password',
32 | isDefault: true,
33 | body: "Hi {{username}},
You have requested to reset a password. Please use following link to complete the action. Please note this link is only valid for the next hour.
{{link}}
If you haven't requested the code please ignore the email.
Thank you!.
"
34 | },
35 | {
36 | title: 'New User Set Password',
37 | slug: 'new-user-set-password',
38 | sender: 'noreply@truthy.com',
39 | subject: 'Set Password',
40 | isDefault: true,
41 | body: "Hi {{username}},
A new account has been created using your email. Please use following link to set password for your account. Please note this link is only valid for the next hour.
{{link}}
If you haven't requested the code please ignore the email.
Thank you!.
"
42 | }
43 | ];
44 |
45 | export = templates;
46 |
--------------------------------------------------------------------------------
/src/config/ormconfig.ts:
--------------------------------------------------------------------------------
1 | import { ConnectionOptions } from 'typeorm';
2 | import * as config from 'config';
3 |
4 | const dbConfig = config.get('db');
5 | const ormConfig: ConnectionOptions = {
6 | type: process.env.DB_TYPE || dbConfig.type,
7 | host: process.env.DB_HOST || dbConfig.host,
8 | port: process.env.DB_PORT || dbConfig.port,
9 | username: process.env.DB_USERNAME || dbConfig.username,
10 | password: process.env.DB_PASSWORD || dbConfig.password,
11 | database: process.env.DB_DATABASE_NAME || dbConfig.database,
12 | migrationsTransactionMode: 'each',
13 | entities: [__dirname + '/../**/*.entity.{js,ts}'],
14 | logging: false,
15 | synchronize: false,
16 | migrationsRun: process.env.NODE_ENV === 'test',
17 | dropSchema: process.env.NODE_ENV === 'test',
18 | migrationsTableName: 'migrations',
19 | migrations: [__dirname + '/../database/migrations/**/*{.ts,.js}'],
20 | cli: {
21 | migrationsDir: 'src/database/migrations'
22 | }
23 | };
24 |
25 | export = ormConfig;
26 |
--------------------------------------------------------------------------------
/src/config/throttle-config.ts:
--------------------------------------------------------------------------------
1 | import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis';
2 | import { ThrottlerModuleOptions } from '@nestjs/throttler';
3 | import * as config from 'config';
4 |
5 | const throttleConfigVariables = config.get('throttle.global');
6 | const redisConfig = config.get('queue');
7 | const throttleConfig: ThrottlerModuleOptions = {
8 | ttl: process.env.THROTTLE_TTL || throttleConfigVariables.get('ttl'),
9 | limit: process.env.THROTTLE_LIMIT || throttleConfigVariables.get('limit'),
10 | storage: new ThrottlerStorageRedisService({
11 | host: process.env.REDIS_HOST || redisConfig.host,
12 | port: process.env.REDIS_PORT || redisConfig.port,
13 | password: process.env.REDIS_PASSWORD || redisConfig.password
14 | })
15 | };
16 |
17 | export = throttleConfig;
18 |
--------------------------------------------------------------------------------
/src/config/winston.ts:
--------------------------------------------------------------------------------
1 | import * as winston from 'winston';
2 | import { utilities as nestWinstonModuleUtilities } from 'nest-winston';
3 | import { WinstonModuleOptions } from 'nest-winston';
4 | import * as WinstonCloudWatch from 'winston-cloudwatch';
5 | import * as config from 'config';
6 |
7 | const isProduction = process.env.NODE_ENV === 'production';
8 | const winstonConfig = config.get('winston');
9 |
10 | export default {
11 | format: winston.format.colorize(),
12 | exitOnError: false,
13 | transports: isProduction
14 | ? new WinstonCloudWatch({
15 | name: 'Truthy CMS',
16 | awsOptions: {
17 | credentials: {
18 | accessKeyId:
19 | process.env.AWS_ACCESS_KEY || winstonConfig.awsAccessKeyId,
20 | secretAccessKey:
21 | process.env.AWS_KEY_SECRET || winstonConfig.awsSecretAccessKey
22 | }
23 | },
24 | logGroupName:
25 | process.env.CLOUDWATCH_GROUP_NAME || winstonConfig.groupName,
26 | logStreamName:
27 | process.env.CLOUDWATCH_STREAM_NAME || winstonConfig.streamName,
28 | awsRegion: process.env.CLOUDWATCH_AWS_REGION || winstonConfig.awsRegion,
29 | messageFormatter: function (item) {
30 | return (
31 | item.level + ': ' + item.message + ' ' + JSON.stringify(item.meta)
32 | );
33 | }
34 | })
35 | : new winston.transports.Console({
36 | format: winston.format.combine(
37 | winston.format.timestamp(),
38 | winston.format.ms(),
39 | nestWinstonModuleUtilities.format.nestLike('Truthy Logger', {
40 | prettyPrint: true
41 | })
42 | )
43 | })
44 | } as WinstonModuleOptions;
45 |
--------------------------------------------------------------------------------
/src/dashboard/dashboard.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get, UseGuards } from '@nestjs/common';
2 | import { ApiTags } from '@nestjs/swagger';
3 |
4 | import JwtTwoFactorGuard from 'src/common/guard/jwt-two-factor.guard';
5 | import { PermissionGuard } from 'src/common/guard/permission.guard';
6 | import { DashboardService } from 'src/dashboard/dashboard.service';
7 | import { OsStatsInterface } from 'src/dashboard/interface/os-stats.interface';
8 | import { UsersStatsInterface } from 'src/dashboard/interface/user-stats.interface';
9 | import { BrowserStatsInterface } from 'src/dashboard/interface/browser-stats.interface';
10 |
11 | @ApiTags('dashboard')
12 | @UseGuards(JwtTwoFactorGuard, PermissionGuard)
13 | @Controller('dashboard')
14 | export class DashboardController {
15 | constructor(private readonly dashboardService: DashboardService) {}
16 |
17 | @Get('/users')
18 | userStat(): Promise {
19 | return this.dashboardService.getUserStat();
20 | }
21 |
22 | @Get('/os')
23 | osStat(): Promise> {
24 | return this.dashboardService.getOsData();
25 | }
26 |
27 | @Get('/browser')
28 | browserStat(): Promise> {
29 | return this.dashboardService.getBrowserData();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/dashboard/dashboard.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { DashboardService } from 'src/dashboard/dashboard.service';
3 | import { DashboardController } from 'src/dashboard/dashboard.controller';
4 | import { AuthModule } from 'src/auth/auth.module';
5 |
6 | @Module({
7 | controllers: [DashboardController],
8 | imports: [AuthModule],
9 | providers: [DashboardService]
10 | })
11 | export class DashboardModule {}
12 |
--------------------------------------------------------------------------------
/src/dashboard/dashboard.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | import { UserStatusEnum } from 'src/auth/user-status.enum';
4 | import { AuthService } from 'src/auth/auth.service';
5 | import { UsersStatsInterface } from 'src/dashboard/interface/user-stats.interface';
6 | import { BrowserStatsInterface } from 'src/dashboard/interface/browser-stats.interface';
7 | import { OsStatsInterface } from 'src/dashboard/interface/os-stats.interface';
8 |
9 | @Injectable()
10 | export class DashboardService {
11 | constructor(private readonly authService: AuthService) {}
12 |
13 | async getUserStat(): Promise {
14 | const totalUserPromise = this.authService.countByCondition({});
15 | const totalActiveUserPromise = this.authService.countByCondition({
16 | status: UserStatusEnum.ACTIVE
17 | });
18 | const totalInActiveUserPromise = this.authService.countByCondition({
19 | status: UserStatusEnum.INACTIVE
20 | });
21 | const [total, active, inactive] = await Promise.all([
22 | totalUserPromise,
23 | totalActiveUserPromise,
24 | totalInActiveUserPromise
25 | ]);
26 | return {
27 | total,
28 | active,
29 | inactive
30 | };
31 | }
32 |
33 | getOsData(): Promise> {
34 | return this.authService.getRefreshTokenGroupedData('os');
35 | }
36 |
37 | getBrowserData(): Promise> {
38 | return this.authService.getRefreshTokenGroupedData('browser');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/dashboard/dto/create-dashboard.dto.ts:
--------------------------------------------------------------------------------
1 | export class CreateDashboardDto {}
2 |
--------------------------------------------------------------------------------
/src/dashboard/dto/update-dashboard.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/swagger';
2 |
3 | import { CreateDashboardDto } from 'src/dashboard/dto/create-dashboard.dto';
4 |
5 | export class UpdateDashboardDto extends PartialType(CreateDashboardDto) {}
6 |
--------------------------------------------------------------------------------
/src/dashboard/entities/dashboard.entity.ts:
--------------------------------------------------------------------------------
1 | export class Dashboard {}
2 |
--------------------------------------------------------------------------------
/src/dashboard/interface/browser-stats.interface.ts:
--------------------------------------------------------------------------------
1 | export interface BrowserStatsInterface {
2 | token_browser: string;
3 | count: number;
4 | }
5 |
--------------------------------------------------------------------------------
/src/dashboard/interface/os-stats.interface.ts:
--------------------------------------------------------------------------------
1 | export interface OsStatsInterface {
2 | token_os: string;
3 | count: number;
4 | }
5 |
--------------------------------------------------------------------------------
/src/dashboard/interface/user-stats.interface.ts:
--------------------------------------------------------------------------------
1 | export interface UsersStatsInterface {
2 | total: number;
3 | active: number;
4 | inactive: number;
5 | }
6 |
--------------------------------------------------------------------------------
/src/database/migrations/1614275766942-RoleTable.ts:
--------------------------------------------------------------------------------
1 | import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
2 |
3 | export class RoleTable1614275766942 implements MigrationInterface {
4 | tableName = 'role';
5 |
6 | public async up(queryRunner: QueryRunner): Promise {
7 | await queryRunner.createTable(
8 | new Table({
9 | name: this.tableName,
10 | columns: [
11 | {
12 | name: 'id',
13 | type: 'int',
14 | isPrimary: true,
15 | isGenerated: true,
16 | generationStrategy: 'increment'
17 | },
18 | {
19 | name: 'name',
20 | type: 'varchar',
21 | isNullable: false,
22 | isUnique: true,
23 | length: '100'
24 | },
25 | {
26 | name: 'description',
27 | type: 'text',
28 | isNullable: true
29 | },
30 | {
31 | name: 'createdAt',
32 | type: 'timestamp',
33 | default: 'now()'
34 | },
35 | {
36 | name: 'updatedAt',
37 | type: 'timestamp',
38 | default: 'now()'
39 | }
40 | ]
41 | }),
42 | false
43 | );
44 |
45 | await queryRunner.createIndex(
46 | this.tableName,
47 | new TableIndex({
48 | name: `IDX_ROLE_NAME`,
49 | columnNames: ['name']
50 | })
51 | );
52 | }
53 |
54 | public async down(queryRunner: QueryRunner): Promise {
55 | const table = await queryRunner.getTable(this.tableName);
56 | const index = `IDX_ROLE_NAME`;
57 | const nameIndex = table.indices.find((fk) => fk.name.indexOf(index) !== -1);
58 | if (nameIndex) {
59 | await queryRunner.dropIndex(this.tableName, nameIndex);
60 | }
61 | await queryRunner.dropTable(this.tableName);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/database/migrations/1614275788549-PermissionTable.ts:
--------------------------------------------------------------------------------
1 | import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
2 |
3 | export class PermissionTable1614275788549 implements MigrationInterface {
4 | tableName = 'permission';
5 | indexFields = ['resource', 'description'];
6 |
7 | public async up(queryRunner: QueryRunner): Promise {
8 | await queryRunner.createTable(
9 | new Table({
10 | name: this.tableName,
11 | columns: [
12 | {
13 | name: 'id',
14 | type: 'int',
15 | isPrimary: true,
16 | isGenerated: true,
17 | generationStrategy: 'increment'
18 | },
19 | {
20 | name: 'resource',
21 | type: 'varchar',
22 | length: '100'
23 | },
24 | {
25 | name: 'path',
26 | type: 'varchar',
27 | isNullable: false
28 | },
29 | {
30 | name: 'description',
31 | type: 'text',
32 | isNullable: true,
33 | isUnique: true
34 | },
35 | {
36 | name: 'method',
37 | type: 'varchar',
38 | default: `'get'`,
39 | length: '20'
40 | },
41 | {
42 | name: 'isDefault',
43 | type: 'boolean',
44 | default: false
45 | },
46 | {
47 | name: 'createdAt',
48 | type: 'timestamp',
49 | default: 'now()'
50 | },
51 | {
52 | name: 'updatedAt',
53 | type: 'timestamp',
54 | default: 'now()'
55 | }
56 | ]
57 | }),
58 | false
59 | );
60 |
61 | for (const field of this.indexFields) {
62 | await queryRunner.createIndex(
63 | this.tableName,
64 | new TableIndex({
65 | name: `IDX_PERMISSION_${field.toUpperCase()}`,
66 | columnNames: [field]
67 | })
68 | );
69 | }
70 | }
71 |
72 | public async down(queryRunner: QueryRunner): Promise {
73 | const table = await queryRunner.getTable(this.tableName);
74 | for (const field of this.indexFields) {
75 | const index = `IDX_PERMISSION_${field.toUpperCase()}`;
76 | const keyIndex = table.indices.find(
77 | (fk) => fk.name.indexOf(index) !== -1
78 | );
79 | if (keyIndex) {
80 | await queryRunner.dropIndex(this.tableName, keyIndex);
81 | }
82 | }
83 | await queryRunner.dropTable(this.tableName);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/database/migrations/1614275796207-PermissionRoleTable.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MigrationInterface,
3 | QueryRunner,
4 | Table,
5 | TableColumn,
6 | TableForeignKey
7 | } from 'typeorm';
8 |
9 | export class PermissionRoleTable1614275796207 implements MigrationInterface {
10 | foreignKeysArray = [
11 | {
12 | table: 'role',
13 | field: 'roleId',
14 | reference: 'id'
15 | },
16 | {
17 | table: 'permission',
18 | field: 'permissionId',
19 | reference: 'id'
20 | }
21 | ];
22 | tableName = 'role_permission';
23 |
24 | public async up(queryRunner: QueryRunner): Promise {
25 | await queryRunner.createTable(
26 | new Table({
27 | name: this.tableName,
28 | columns: [
29 | // {
30 | // name: 'id',
31 | // type: 'int',
32 | // isPrimary: true,
33 | // isGenerated: true,
34 | // generationStrategy: 'increment'
35 | // }
36 | ]
37 | }),
38 | false
39 | );
40 | for (const foreignKey of this.foreignKeysArray) {
41 | await queryRunner.addColumn(
42 | this.tableName,
43 | new TableColumn({
44 | name: foreignKey.field,
45 | type: 'int'
46 | })
47 | );
48 |
49 | await queryRunner.createForeignKey(
50 | this.tableName,
51 | new TableForeignKey({
52 | columnNames: [foreignKey.field],
53 | referencedColumnNames: [foreignKey.reference],
54 | referencedTableName: foreignKey.table,
55 | onDelete: 'CASCADE'
56 | })
57 | );
58 | }
59 | }
60 |
61 | public async down(queryRunner: QueryRunner): Promise {
62 | const table = await queryRunner.getTable(this.tableName);
63 | for (const key of this.foreignKeysArray) {
64 | const foreignKey = table.foreignKeys.find(
65 | (fk) => fk.columnNames.indexOf(key.field) !== -1
66 | );
67 | await queryRunner.dropForeignKey(this.tableName, foreignKey);
68 | await queryRunner.dropColumn(this.tableName, key.field);
69 | }
70 | await queryRunner.dropTable(this.tableName);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/database/migrations/1614275816426-UserTable.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MigrationInterface,
3 | QueryRunner,
4 | Table,
5 | TableColumn,
6 | TableForeignKey,
7 | TableIndex
8 | } from 'typeorm';
9 |
10 | export class UserTable1614275816426 implements MigrationInterface {
11 | indexFields = ['name', 'email', 'username'];
12 | tableName = 'user';
13 |
14 | public async up(queryRunner: QueryRunner): Promise {
15 | await queryRunner.createTable(
16 | new Table({
17 | name: this.tableName,
18 | columns: [
19 | {
20 | name: 'id',
21 | type: 'int',
22 | isPrimary: true,
23 | isGenerated: true,
24 | generationStrategy: 'increment'
25 | },
26 | {
27 | name: 'username',
28 | type: 'varchar',
29 | isNullable: false,
30 | isUnique: true,
31 | length: '100'
32 | },
33 | {
34 | name: 'email',
35 | type: 'varchar',
36 | isNullable: false,
37 | isUnique: true,
38 | length: '100'
39 | },
40 | {
41 | name: 'password',
42 | type: 'varchar',
43 | isNullable: true
44 | },
45 | {
46 | name: 'name',
47 | type: 'varchar',
48 | isNullable: true
49 | },
50 | {
51 | name: 'address',
52 | type: 'varchar',
53 | isNullable: true
54 | },
55 | {
56 | name: 'contact',
57 | type: 'varchar',
58 | isNullable: true
59 | },
60 | {
61 | name: 'salt',
62 | type: 'varchar',
63 | isNullable: true
64 | },
65 | {
66 | name: 'token',
67 | type: 'varchar',
68 | isNullable: true
69 | },
70 | {
71 | name: 'status',
72 | type: 'varchar',
73 | default: `'active'`
74 | },
75 | {
76 | name: 'createdAt',
77 | type: 'timestamp',
78 | default: 'now()'
79 | },
80 | {
81 | name: 'updatedAt',
82 | type: 'timestamp',
83 | default: 'now()'
84 | }
85 | ]
86 | }),
87 | false
88 | );
89 |
90 | for (const field of this.indexFields) {
91 | await queryRunner.createIndex(
92 | this.tableName,
93 | new TableIndex({
94 | name: `IDX_USER_${field.toUpperCase()}`,
95 | columnNames: [field]
96 | })
97 | );
98 | }
99 |
100 | await queryRunner.addColumn(
101 | this.tableName,
102 | new TableColumn({
103 | name: 'roleId',
104 | type: 'int'
105 | })
106 | );
107 |
108 | await queryRunner.createForeignKey(
109 | this.tableName,
110 | new TableForeignKey({
111 | columnNames: ['roleId'],
112 | referencedColumnNames: ['id'],
113 | referencedTableName: 'role',
114 | onDelete: 'CASCADE'
115 | })
116 | );
117 | }
118 |
119 | public async down(queryRunner: QueryRunner): Promise {
120 | const table = await queryRunner.getTable(this.tableName);
121 |
122 | const foreignKey = await table.foreignKeys.find(
123 | (fk) => fk.columnNames.indexOf('roleId') !== -1
124 | );
125 | await queryRunner.dropForeignKey(this.tableName, foreignKey);
126 | await queryRunner.dropColumn(this.tableName, 'roleId');
127 |
128 | for (const field of this.indexFields) {
129 | const index = `IDX_USER_${field.toUpperCase()}`;
130 | const keyIndex = await table.indices.find(
131 | (fk) => fk.name.indexOf(index) !== -1
132 | );
133 | await queryRunner.dropIndex(this.tableName, keyIndex);
134 | }
135 | await queryRunner.dropTable(this.tableName);
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/database/migrations/1617559216655-addTokenValidityDateInUserEntity.ts:
--------------------------------------------------------------------------------
1 | import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
2 |
3 | export class addTokenValidityDateInUserEntity1617559216655
4 | implements MigrationInterface
5 | {
6 | tableName = 'user';
7 |
8 | public async up(queryRunner: QueryRunner): Promise {
9 | await queryRunner.addColumn(
10 | this.tableName,
11 | new TableColumn({
12 | name: 'tokenValidityDate',
13 | type: 'timestamp',
14 | default: 'now()'
15 | })
16 | );
17 | }
18 |
19 | public async down(queryRunner: QueryRunner): Promise {
20 | await queryRunner.dropColumn(
21 | this.tableName,
22 | new TableColumn({
23 | name: 'tokenValidityDate',
24 | type: 'timestamp',
25 | default: 'now()'
26 | })
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/database/migrations/1622305543735-EmailTemplate.ts:
--------------------------------------------------------------------------------
1 | import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
2 |
3 | export class EmailTemplate1622305543735 implements MigrationInterface {
4 | tableName = 'email_templates';
5 | index = 'IDX_EMAIL_TEMPLATES_TITLE';
6 |
7 | public async up(queryRunner: QueryRunner): Promise {
8 | await queryRunner.createTable(
9 | new Table({
10 | name: this.tableName,
11 | columns: [
12 | {
13 | name: 'id',
14 | type: 'int',
15 | isPrimary: true,
16 | isGenerated: true,
17 | generationStrategy: 'increment'
18 | },
19 | {
20 | name: 'title',
21 | type: 'varchar',
22 | isNullable: false,
23 | isUnique: true,
24 | length: '200'
25 | },
26 | {
27 | name: 'slug',
28 | type: 'varchar',
29 | isNullable: false,
30 | isUnique: true,
31 | length: '200'
32 | },
33 | {
34 | name: 'sender',
35 | type: 'varchar',
36 | isNullable: false,
37 | length: '200'
38 | },
39 | {
40 | name: 'subject',
41 | type: 'text',
42 | isNullable: true
43 | },
44 | {
45 | name: 'body',
46 | type: 'text',
47 | isNullable: true
48 | },
49 | {
50 | name: 'isDefault',
51 | type: 'boolean',
52 | default: false
53 | },
54 | {
55 | name: 'createdAt',
56 | type: 'timestamp',
57 | default: 'now()'
58 | },
59 | {
60 | name: 'updatedAt',
61 | type: 'timestamp',
62 | default: 'now()'
63 | }
64 | ]
65 | }),
66 | false
67 | );
68 |
69 | await queryRunner.createIndex(
70 | this.tableName,
71 | new TableIndex({
72 | name: `${this.index}`,
73 | columnNames: ['title']
74 | })
75 | );
76 | }
77 |
78 | public async down(queryRunner: QueryRunner): Promise {
79 | const table = await queryRunner.getTable(this.tableName);
80 | const nameIndex = table.indices.find(
81 | (ik) => ik.name.indexOf(this.index) !== -1
82 | );
83 | if (nameIndex) {
84 | await queryRunner.dropIndex(this.tableName, nameIndex);
85 | }
86 | await queryRunner.dropTable(this.tableName);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/database/migrations/1623601947397-CreateRefreshTokenTable.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MigrationInterface,
3 | QueryRunner,
4 | Table,
5 | TableColumn,
6 | TableForeignKey
7 | } from 'typeorm';
8 |
9 | export class CreateRefreshTokenTable1623601947397
10 | implements MigrationInterface
11 | {
12 | foreignKeysArray = [
13 | {
14 | table: 'user',
15 | field: 'userId',
16 | reference: 'id'
17 | }
18 | ];
19 | tableName = 'refresh_token';
20 |
21 | public async up(queryRunner: QueryRunner): Promise {
22 | await queryRunner.createTable(
23 | new Table({
24 | name: this.tableName,
25 | columns: [
26 | {
27 | name: 'id',
28 | type: 'int',
29 | isPrimary: true,
30 | isGenerated: true,
31 | generationStrategy: 'increment'
32 | },
33 | {
34 | name: 'isRevoked',
35 | type: 'boolean',
36 | default: false
37 | },
38 | {
39 | name: 'expires',
40 | type: 'timestamp',
41 | default: 'now()'
42 | }
43 | ]
44 | }),
45 | false
46 | );
47 |
48 | for (const foreignKey of this.foreignKeysArray) {
49 | await queryRunner.addColumn(
50 | this.tableName,
51 | new TableColumn({
52 | name: foreignKey.field,
53 | type: 'int'
54 | })
55 | );
56 |
57 | await queryRunner.createForeignKey(
58 | this.tableName,
59 | new TableForeignKey({
60 | columnNames: [foreignKey.field],
61 | referencedColumnNames: [foreignKey.reference],
62 | referencedTableName: foreignKey.table,
63 | onDelete: 'CASCADE'
64 | })
65 | );
66 | }
67 | }
68 |
69 | public async down(queryRunner: QueryRunner): Promise {
70 | const table = await queryRunner.getTable(this.tableName);
71 | for (const key of this.foreignKeysArray) {
72 | const foreignKey = table.foreignKeys.find(
73 | (fk) => fk.columnNames.indexOf(key.field) !== -1
74 | );
75 | await queryRunner.dropForeignKey(this.tableName, foreignKey);
76 | await queryRunner.dropColumn(this.tableName, key.field);
77 | }
78 | await queryRunner.dropTable(this.tableName);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/database/migrations/1623777103308-AddUserAgentRefreshTokenTable.ts:
--------------------------------------------------------------------------------
1 | import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
2 |
3 | export class AddUserAgentRefreshTokenTable1623777103308
4 | implements MigrationInterface
5 | {
6 | tableName = 'refresh_token';
7 | columns = [
8 | new TableColumn({
9 | name: 'ip',
10 | type: 'varchar',
11 | isNullable: true,
12 | length: '50'
13 | }),
14 | new TableColumn({
15 | name: 'userAgent',
16 | type: 'text',
17 | isNullable: true
18 | })
19 | ];
20 | public async up(queryRunner: QueryRunner): Promise {
21 | await queryRunner.addColumns(this.tableName, this.columns);
22 | }
23 |
24 | public async down(queryRunner: QueryRunner): Promise {
25 | await queryRunner.dropColumns(this.tableName, this.columns);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/database/migrations/1626924978575-AddAvatarColumnUserTable.ts:
--------------------------------------------------------------------------------
1 | import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
2 |
3 | export class AddAvatarColumnUserTable1626924978575
4 | implements MigrationInterface
5 | {
6 | tableName = 'user';
7 | columns = [
8 | new TableColumn({
9 | name: 'avatar',
10 | type: 'varchar',
11 | isNullable: true,
12 | length: '200'
13 | })
14 | ];
15 | public async up(queryRunner: QueryRunner): Promise {
16 | await queryRunner.addColumns(this.tableName, this.columns);
17 | }
18 |
19 | public async down(queryRunner: QueryRunner): Promise {
20 | await queryRunner.dropColumns(this.tableName, this.columns);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/database/migrations/1627278359782-Add2faColumnsUserTable.ts:
--------------------------------------------------------------------------------
1 | import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
2 |
3 | export class Add2faColumnsUserTable1627278359782 implements MigrationInterface {
4 | tableName = 'user';
5 | columns = [
6 | new TableColumn({
7 | name: 'twoFASecret',
8 | type: 'varchar',
9 | isNullable: true
10 | }),
11 | new TableColumn({
12 | name: 'isTwoFAEnabled',
13 | type: 'boolean',
14 | default: false
15 | })
16 | ];
17 | public async up(queryRunner: QueryRunner): Promise {
18 | await queryRunner.addColumns(this.tableName, this.columns);
19 | }
20 |
21 | public async down(queryRunner: QueryRunner): Promise {
22 | await queryRunner.dropColumns(this.tableName, this.columns);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/database/migrations/1627736950484-AddTwoSecretGenerateThrottleTime.ts:
--------------------------------------------------------------------------------
1 | import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
2 |
3 | export class AddTwoSecretGenerateThrottleTime1627736950484
4 | implements MigrationInterface
5 | {
6 | tableName = 'user';
7 | columns = [
8 | new TableColumn({
9 | name: 'twoFAThrottleTime',
10 | type: 'timestamp',
11 | default: 'now()'
12 | })
13 | ];
14 | public async up(queryRunner: QueryRunner): Promise {
15 | await queryRunner.addColumns(this.tableName, this.columns);
16 | }
17 |
18 | public async down(queryRunner: QueryRunner): Promise {
19 | await queryRunner.dropColumns(this.tableName, this.columns);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/database/migrations/1629136129718-AddBrowserAndOsColumnRefreshTokenTable.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MigrationInterface,
3 | QueryRunner,
4 | TableColumn,
5 | TableIndex
6 | } from 'typeorm';
7 |
8 | export class AddBrowserAndOsColumnRefreshTokenTable1629136129718
9 | implements MigrationInterface
10 | {
11 | tableName = 'refresh_token';
12 | indexFields = ['browser', 'os'];
13 | columns = [
14 | new TableColumn({
15 | name: 'browser',
16 | type: 'varchar',
17 | isNullable: true,
18 | length: '200'
19 | }),
20 | new TableColumn({
21 | name: 'os',
22 | type: 'varchar',
23 | isNullable: true,
24 | length: '200'
25 | })
26 | ];
27 | public async up(queryRunner: QueryRunner): Promise {
28 | await queryRunner.addColumns(this.tableName, this.columns);
29 | for (const field of this.indexFields) {
30 | await queryRunner.createIndex(
31 | this.tableName,
32 | new TableIndex({
33 | name: `IDX_REFRESH_TOKEN_${field.toUpperCase()}`,
34 | columnNames: [field]
35 | })
36 | );
37 | }
38 | }
39 |
40 | public async down(queryRunner: QueryRunner): Promise {
41 | const table = await queryRunner.getTable(this.tableName);
42 | for (const field of this.indexFields) {
43 | const index = `IDX_REFRESH_TOKEN_${field.toUpperCase()}`;
44 | const keyIndex = await table.indices.find(
45 | (fk) => fk.name.indexOf(index) !== -1
46 | );
47 | await queryRunner.dropIndex(this.tableName, keyIndex);
48 | }
49 | await queryRunner.dropColumns(this.tableName, this.columns);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/database/seeds/create-email-template.seed.ts:
--------------------------------------------------------------------------------
1 | import { Factory } from 'typeorm-seeding';
2 | import { Connection } from 'typeorm';
3 |
4 | import * as templates from 'src/config/email-template';
5 | import { EmailTemplateEntity } from 'src/email-template/entities/email-template.entity';
6 |
7 | export default class CreateEmailTemplateSeed {
8 | public async run(factory: Factory, connection: Connection): Promise {
9 | await connection
10 | .createQueryBuilder()
11 | .insert()
12 | .into(EmailTemplateEntity)
13 | .values(templates)
14 | .orIgnore()
15 | .execute();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/database/seeds/create-permission.seed.ts:
--------------------------------------------------------------------------------
1 | import { Factory } from 'typeorm-seeding';
2 | import { Connection } from 'typeorm';
3 |
4 | import {
5 | ModulesPayloadInterface,
6 | PermissionConfiguration,
7 | PermissionPayload,
8 | RoutePayloadInterface,
9 | SubModulePayloadInterface
10 | } from 'src/config/permission-config';
11 | import { PermissionEntity } from 'src/permission/entities/permission.entity';
12 |
13 | export default class CreatePermissionSeed {
14 | permissions: RoutePayloadInterface[] = [];
15 |
16 | public async run(factory: Factory, connection: Connection): Promise {
17 | const modules = PermissionConfiguration.modules;
18 | for (const moduleData of modules) {
19 | let resource = moduleData.resource;
20 | this.assignResourceAndConcatPermission(moduleData, resource);
21 |
22 | if (moduleData.hasSubmodules) {
23 | for (const submodule of moduleData.submodules) {
24 | resource = submodule.resource || resource;
25 | this.assignResourceAndConcatPermission(submodule, resource);
26 | }
27 | }
28 | }
29 |
30 | if (this.permissions && this.permissions.length > 0) {
31 | await connection
32 | .createQueryBuilder()
33 | .insert()
34 | .into(PermissionEntity)
35 | .values(this.permissions)
36 | .orIgnore()
37 | .execute();
38 | }
39 | }
40 |
41 | assignResourceAndConcatPermission(
42 | modules: ModulesPayloadInterface | SubModulePayloadInterface,
43 | resource: string,
44 | isDefault?: false
45 | ) {
46 | if (modules.permissions) {
47 | for (const permission of modules.permissions) {
48 | this.concatPermissions(permission, resource, isDefault);
49 | }
50 | }
51 | }
52 |
53 | concatPermissions(
54 | permission: PermissionPayload,
55 | resource: string,
56 | isDefault: boolean
57 | ) {
58 | const description = permission.name;
59 | for (const data of permission.route) {
60 | data.resource = data.resource || resource;
61 | data.description = data.description || description;
62 | data.isDefault = isDefault;
63 | }
64 | this.permissions = this.permissions.concat(permission.route);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/database/seeds/create-role.seed.ts:
--------------------------------------------------------------------------------
1 | import { Factory } from 'typeorm-seeding';
2 | import { Connection } from 'typeorm';
3 |
4 | import { RoleEntity } from 'src/role/entities/role.entity';
5 | import { PermissionConfiguration } from 'src/config/permission-config';
6 | import { PermissionEntity } from 'src/permission/entities/permission.entity';
7 |
8 | export default class CreateRoleSeed {
9 | public async run(factory: Factory, connection: Connection): Promise {
10 | const roles = PermissionConfiguration.roles;
11 | await connection
12 | .createQueryBuilder()
13 | .insert()
14 | .into(RoleEntity)
15 | .values(roles)
16 | .orIgnore()
17 | .execute();
18 |
19 | // Assign all permission to superUser
20 | const role = await connection
21 | .getRepository(RoleEntity)
22 | .createQueryBuilder('role')
23 | .where('role.name = :name', {
24 | name: 'superuser'
25 | })
26 | .getOne();
27 |
28 | if (role) {
29 | role.permission = await connection
30 | .getRepository(PermissionEntity)
31 | .createQueryBuilder('permission')
32 | .getMany();
33 | await role.save();
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/database/seeds/create-user.seed.ts:
--------------------------------------------------------------------------------
1 | import { Factory } from 'typeorm-seeding';
2 | import { Connection } from 'typeorm';
3 |
4 | import { UserEntity } from 'src/auth/entity/user.entity';
5 | import { UserStatusEnum } from 'src/auth/user-status.enum';
6 | import { RoleEntity } from 'src/role/entities/role.entity';
7 |
8 | export default class CreateUserSeed {
9 | public async run(factory: Factory, connection: Connection): Promise {
10 | const role = await connection
11 | .getRepository(RoleEntity)
12 | .createQueryBuilder('role')
13 | .where('role.name = :name', {
14 | name: 'superuser'
15 | })
16 | .getOne();
17 |
18 | if (!role) {
19 | return;
20 | }
21 | await connection
22 | .createQueryBuilder()
23 | .insert()
24 | .into(UserEntity)
25 | .values([
26 | {
27 | username: 'admin',
28 | email: 'admin@truthy.com',
29 | password:
30 | '$2b$10$O9BWip02GuE14bDPfBomQebCjwKQyuUfkulhvBB1UoizOeKxGG8Fu', // Truthy@123
31 | salt: '$2b$10$O9BWip02GuE14bDPfBomQe',
32 | name: 'truthy',
33 | status: UserStatusEnum.ACTIVE,
34 | roleId: role.id
35 | }
36 | ])
37 | .orIgnore()
38 | .execute();
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/email-template/dto/create-email-template.dto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IsBoolean,
3 | IsEmail,
4 | IsNotEmpty,
5 | IsOptional,
6 | IsString,
7 | MaxLength,
8 | MinLength,
9 | Validate
10 | } from 'class-validator';
11 | import { UniqueValidatorPipe } from 'src/common/pipes/unique-validator.pipe';
12 | import { EmailTemplateEntity } from 'src/email-template/entities/email-template.entity';
13 |
14 | export class CreateEmailTemplateDto {
15 | @IsNotEmpty()
16 | @IsString()
17 | @MaxLength(100, {
18 | message: 'maxLength-{"ln":100,"count":100}'
19 | })
20 | @Validate(UniqueValidatorPipe, [EmailTemplateEntity], {
21 | message: 'already taken'
22 | })
23 | title: string;
24 |
25 | @IsNotEmpty()
26 | @IsString()
27 | @IsEmail()
28 | sender: string;
29 |
30 | @IsNotEmpty()
31 | @IsString()
32 | subject: string;
33 |
34 | @IsNotEmpty()
35 | @IsString()
36 | @MinLength(50, {
37 | message: 'minLength-{"ln":50,"count":50}'
38 | })
39 | body: string;
40 |
41 | @IsOptional()
42 | @IsBoolean()
43 | isDefault: boolean;
44 | }
45 |
--------------------------------------------------------------------------------
/src/email-template/dto/email-templates-search-filter.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/swagger';
2 |
3 | import { CommonSearchFieldDto } from 'src/common/extra/common-search-field.dto';
4 |
5 | export class EmailTemplatesSearchFilterDto extends PartialType(
6 | CommonSearchFieldDto
7 | ) {}
8 |
--------------------------------------------------------------------------------
/src/email-template/dto/update-email-template.dto.ts:
--------------------------------------------------------------------------------
1 | import { CreateEmailTemplateDto } from 'src/email-template/dto/create-email-template.dto';
2 | import { ApiPropertyOptional, PartialType } from '@nestjs/swagger';
3 | import { Optional } from '@nestjs/common';
4 | import { IsString } from 'class-validator';
5 |
6 | export class UpdateEmailTemplateDto extends PartialType(
7 | CreateEmailTemplateDto
8 | ) {
9 | @ApiPropertyOptional()
10 | @Optional()
11 | @IsString()
12 | title: string;
13 | }
14 |
--------------------------------------------------------------------------------
/src/email-template/email-template.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Delete,
5 | Get,
6 | HttpCode,
7 | HttpStatus,
8 | Param,
9 | Post,
10 | Put,
11 | Query,
12 | UseGuards
13 | } from '@nestjs/common';
14 | import { ApiTags } from '@nestjs/swagger';
15 |
16 | import { EmailTemplateService } from 'src/email-template/email-template.service';
17 | import { CreateEmailTemplateDto } from 'src/email-template/dto/create-email-template.dto';
18 | import { UpdateEmailTemplateDto } from 'src/email-template/dto/update-email-template.dto';
19 | import { PermissionGuard } from 'src/common/guard/permission.guard';
20 | import { Pagination } from 'src/paginate';
21 | import { EmailTemplate } from 'src/email-template/serializer/email-template.serializer';
22 | import { EmailTemplatesSearchFilterDto } from 'src/email-template/dto/email-templates-search-filter.dto';
23 | import JwtTwoFactorGuard from 'src/common/guard/jwt-two-factor.guard';
24 |
25 | @ApiTags('email-templates')
26 | @UseGuards(JwtTwoFactorGuard, PermissionGuard)
27 | @Controller('email-templates')
28 | export class EmailTemplateController {
29 | constructor(private readonly emailTemplateService: EmailTemplateService) {}
30 |
31 | @Post()
32 | create(
33 | @Body()
34 | createEmailTemplateDto: CreateEmailTemplateDto
35 | ): Promise {
36 | return this.emailTemplateService.create(createEmailTemplateDto);
37 | }
38 |
39 | @Get()
40 | findAll(
41 | @Query()
42 | filter: EmailTemplatesSearchFilterDto
43 | ): Promise> {
44 | return this.emailTemplateService.findAll(filter);
45 | }
46 |
47 | @Get(':id')
48 | findOne(
49 | @Param('id')
50 | id: string
51 | ): Promise {
52 | return this.emailTemplateService.findOne(+id);
53 | }
54 |
55 | @Put(':id')
56 | update(
57 | @Param('id')
58 | id: string,
59 | @Body()
60 | updateEmailTemplateDto: UpdateEmailTemplateDto
61 | ): Promise {
62 | return this.emailTemplateService.update(+id, updateEmailTemplateDto);
63 | }
64 |
65 | @Delete(':id')
66 | @HttpCode(HttpStatus.NO_CONTENT)
67 | remove(
68 | @Param('id')
69 | id: string
70 | ): Promise {
71 | return this.emailTemplateService.remove(+id);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/email-template/email-template.module.ts:
--------------------------------------------------------------------------------
1 | import { forwardRef, Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 |
4 | import { EmailTemplateService } from 'src/email-template/email-template.service';
5 | import { EmailTemplateController } from 'src/email-template/email-template.controller';
6 | import { AuthModule } from 'src/auth/auth.module';
7 | import { UniqueValidatorPipe } from 'src/common/pipes/unique-validator.pipe';
8 | import { EmailTemplateRepository } from 'src/email-template/email-template.repository';
9 |
10 | @Module({
11 | imports: [
12 | TypeOrmModule.forFeature([EmailTemplateRepository]),
13 | forwardRef(() => AuthModule)
14 | ],
15 | exports: [EmailTemplateService],
16 | controllers: [EmailTemplateController],
17 | providers: [EmailTemplateService, UniqueValidatorPipe]
18 | })
19 | export class EmailTemplateModule {}
20 |
--------------------------------------------------------------------------------
/src/email-template/email-template.repository.ts:
--------------------------------------------------------------------------------
1 | import { EntityRepository } from 'typeorm';
2 | import { classToPlain, plainToClass } from 'class-transformer';
3 |
4 | import { BaseRepository } from 'src/common/repository/base.repository';
5 | import { EmailTemplateEntity } from 'src/email-template/entities/email-template.entity';
6 | import { EmailTemplate } from 'src/email-template/serializer/email-template.serializer';
7 |
8 | @EntityRepository(EmailTemplateEntity)
9 | export class EmailTemplateRepository extends BaseRepository<
10 | EmailTemplateEntity,
11 | EmailTemplate
12 | > {
13 | transform(model: EmailTemplateEntity, transformOption = {}): EmailTemplate {
14 | return plainToClass(
15 | EmailTemplate,
16 | classToPlain(model, transformOption),
17 | transformOption
18 | );
19 | }
20 |
21 | transformMany(
22 | models: EmailTemplateEntity[],
23 | transformOption = {}
24 | ): EmailTemplate[] {
25 | return models.map((model) => this.transform(model, transformOption));
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/email-template/entities/email-template.entity.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, Index } from 'typeorm';
2 |
3 | import { CustomBaseEntity } from 'src/common/entity/custom-base.entity';
4 |
5 | @Entity({
6 | name: 'email_templates'
7 | })
8 | export class EmailTemplateEntity extends CustomBaseEntity {
9 | @Column()
10 | @Index({
11 | unique: true
12 | })
13 | title: string;
14 |
15 | @Column()
16 | slug: string;
17 |
18 | @Column()
19 | sender: string;
20 |
21 | @Column()
22 | subject: string;
23 |
24 | @Column()
25 | body: string;
26 |
27 | @Column()
28 | isDefault: boolean;
29 | }
30 |
--------------------------------------------------------------------------------
/src/email-template/serializer/email-template.serializer.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2 |
3 | import { ModelSerializer } from 'src/common/serializer/model.serializer';
4 |
5 | export class EmailTemplate extends ModelSerializer {
6 | id: number;
7 |
8 | @ApiProperty()
9 | title: string;
10 |
11 | @ApiProperty()
12 | slug: string;
13 |
14 | @ApiProperty()
15 | sender: string;
16 |
17 | @ApiProperty()
18 | subject: string;
19 |
20 | @ApiProperty()
21 | body: string;
22 |
23 | @ApiProperty()
24 | isDefault: boolean;
25 |
26 | @ApiPropertyOptional()
27 | createdAt: Date;
28 |
29 | @ApiPropertyOptional()
30 | updatedAt: Date;
31 | }
32 |
--------------------------------------------------------------------------------
/src/exception/custom-http.exception.ts:
--------------------------------------------------------------------------------
1 | import { HttpException, HttpStatus } from '@nestjs/common';
2 | import { ExceptionTitleList } from 'src/common/constants/exception-title-list.constants';
3 | import { StatusCodesList } from 'src/common/constants/status-codes-list.constants';
4 |
5 | export class CustomHttpException extends HttpException {
6 | constructor(message?: string, statusCode?: number, code?: number) {
7 | super(
8 | {
9 | message: message || ExceptionTitleList.BadRequest,
10 | code: code || StatusCodesList.BadRequest,
11 | statusCode: statusCode || HttpStatus.BAD_REQUEST,
12 | error: true
13 | },
14 | statusCode || HttpStatus.BAD_REQUEST
15 | );
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/exception/forbidden.exception.ts:
--------------------------------------------------------------------------------
1 | import { HttpException, HttpStatus } from '@nestjs/common';
2 | import { ExceptionTitleList } from 'src/common/constants/exception-title-list.constants';
3 | import { StatusCodesList } from 'src/common/constants/status-codes-list.constants';
4 |
5 | export class ForbiddenException extends HttpException {
6 | constructor(message?: string, code?: number) {
7 | super(
8 | {
9 | message: message || ExceptionTitleList.Forbidden,
10 | code: code || StatusCodesList.Forbidden,
11 | statusCode: HttpStatus.FORBIDDEN,
12 | error: true
13 | },
14 | HttpStatus.FORBIDDEN
15 | );
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/exception/not-found.exception.ts:
--------------------------------------------------------------------------------
1 | import { HttpException, HttpStatus } from '@nestjs/common';
2 |
3 | import { ExceptionTitleList } from 'src/common/constants/exception-title-list.constants';
4 | import { StatusCodesList } from 'src/common/constants/status-codes-list.constants';
5 |
6 | export class NotFoundException extends HttpException {
7 | constructor(message?: string, code?: number) {
8 | super(
9 | {
10 | message: message || ExceptionTitleList.NotFound,
11 | code: code || StatusCodesList.NotFound,
12 | statusCode: HttpStatus.NOT_FOUND,
13 | error: true
14 | },
15 | HttpStatus.NOT_FOUND
16 | );
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/exception/unauthorized.exception.ts:
--------------------------------------------------------------------------------
1 | import { HttpException, HttpStatus } from '@nestjs/common';
2 | import { ExceptionTitleList } from 'src/common/constants/exception-title-list.constants';
3 | import { StatusCodesList } from 'src/common/constants/status-codes-list.constants';
4 |
5 | export class UnauthorizedException extends HttpException {
6 | constructor(message?: string, code?: number) {
7 | super(
8 | {
9 | message: message || ExceptionTitleList.Unauthorized,
10 | code: code || StatusCodesList.UnauthorizedAccess,
11 | statusCode: HttpStatus.UNAUTHORIZED,
12 | error: true
13 | },
14 | HttpStatus.UNAUTHORIZED
15 | );
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/i18n/en/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "permissions": "permissions",
3 | "method": "method",
4 | "path": "path",
5 | "description": "description",
6 | "resource": "resource",
7 | "token": "token",
8 | "page": "page",
9 | "limit": "limit",
10 | "keyword": "keyword",
11 | "sender": "sender",
12 | "subject": "subject",
13 | "body": "body",
14 | "isDefault": "default",
15 | "title": "title",
16 | "password": "password",
17 | "oldPassword": "old password",
18 | "username": "username",
19 | "confirmPassword": "confirm password",
20 | "status": "status",
21 | "email": "email",
22 | "name": "name",
23 | "roleId": "role"
24 | }
25 |
--------------------------------------------------------------------------------
/src/i18n/en/exception.json:
--------------------------------------------------------------------------------
1 | {
2 | "sec": {
3 | "one": "second",
4 | "other": "seconds",
5 | "zero": "second"
6 | },
7 | "Unauthorized": "Unauthorized",
8 | "internalError": "Internal Server Error!",
9 | "otpRequired": "OTP required",
10 | "invalidOTP": "Invalid OTP",
11 | "inactiveUser": "You account has not been activated yet!",
12 | "invalidCredentials": "Credential didn't match!",
13 | "Forbidden": "Access to the requested resource is forbidden!",
14 | "userInactive": "User is inactive please contact your administrator for further help!",
15 | "Not Found": "Data not found!",
16 | "tokenExpired": "Your session has expired!",
17 | "unsupportedFileType": "Uploaded file type is not supported!",
18 | "incorrectOldPassword": "Incorrect old password!",
19 | "badRequest": "Bad Request, There was some error please try again later!",
20 | "tooManyRequest": "Too many tries, retry after {second} $t(exception.sec, {{\"count\": {second} }})!",
21 | "invalidRefreshToken": "Invalid refresh token!",
22 | "deleteDefaultError": "Cannot delete default item!",
23 | "refreshTokenExpired": "Refresh token is expired!",
24 | "tooManyTries": "Too many tries!"
25 | }
26 |
--------------------------------------------------------------------------------
/src/i18n/en/validation.json:
--------------------------------------------------------------------------------
1 | {
2 | "character": {
3 | "one": "character",
4 | "other": "characters",
5 | "zero": "character"
6 | },
7 | "Unauthorized": "Invalid email/password",
8 | "isNotEmpty": "$t(app.{property}) should not be empty",
9 | "maxLength": "$t(app.{property}) can only have maximum length of {ln} $t(validation.character, {{\"count\": {count} }})",
10 | "minLength": "$t(app.{property}) must have minimum length of {ln} $t(validation.character, {{\"count\": {count} }})",
11 | "max": "$t(app.{property}) can be less than or equal to {ln}",
12 | "min": "$t(app.{property}) must have greater than or equal to {ln}",
13 | "password should contain at least one lowercase letter, one uppercase letter, one numeric digit, and one special character": "password should contain at least one lowercase letter, one uppercase letter, one numeric digit, and one special character",
14 | "isIn": "$t(app.{property}) must be one of {items}",
15 | "isEnum": "$t(app.{property}) must be one of {items}",
16 | "isNumberString": "$t(app.{property}) must be number",
17 | "isNumber": "$t(app.{property}) must be number",
18 | "isEqualTo": "$t(app.{property}) must be same as $t(app.{field})",
19 | "isLowercase": "$t(app.{property}) should be in lowercase",
20 | "isEmail": "$t(app.{property}) must be valid email",
21 | "isString": "$t(app.{property}) must be string",
22 | "isBoolean": "field must be selected between true or false",
23 | "already taken": "$t(app.{property}) is already taken",
24 | "unique": "$t(app.{property}) is already taken"
25 | }
26 |
--------------------------------------------------------------------------------
/src/i18n/ne/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "permissions": "अनुमतिहरू",
3 | "method": "विधि",
4 | "path": "लिंक",
5 | "description": "वर्णन",
6 | "resource": "स्रोत नाम",
7 | "page": "पृष्ठ",
8 | "limit": "सीमा",
9 | "keyword": "कुञ्जी शब्द",
10 | "title": "शीर्षक",
11 | "sender": "प्रेषक",
12 | "subject": "विषय",
13 | "token": "टोकन",
14 | "body": "सामग्री",
15 | "isDefault": "पूर्वनिर्धारित",
16 | "password": "पासवर्ड",
17 | "username": "प्रयोगकर्ता नाम",
18 | "oldPassword": "पुरानो पासवर्ड",
19 | "confirmPassword": "सुनिश्चित गरिएको पासवर्ड",
20 | "status": "स्थिति",
21 | "email": "ईमेल",
22 | "name": "नाम",
23 | "roleId": "भूमिका"
24 | }
25 |
--------------------------------------------------------------------------------
/src/i18n/ne/exception.json:
--------------------------------------------------------------------------------
1 | {
2 | "Unauthorized": "अनधिकृत पहुँच अस्वीकृत!",
3 | "internalError": "आन्तरिक सर्वर त्रुटि!",
4 | "inactiveUser": "तपाइँको खाता अझै सक्रिय गरिएको छैन।",
5 | "Not Found": "डाटा भेटिएन!",
6 | "tokenExpired": "तपाईको सत्रको समयावधि सकियो!",
7 | "otpRequired": "OTP आवश्यक छ",
8 | "invalidOTP": "अवैध OTP",
9 | "invalidCredentials": "अमान्य ईमेल/पासवर्ड विवरण",
10 | "Forbidden": "अनुरोध गरिएको संसाधनमा पहुँच निषेध छ!",
11 | "userInactive": "प्रयोगकर्ता निष्क्रिय छ कृपया थप मद्दत को लागी तपाइँको प्रशासक लाई सम्पर्क गर्नुहोस्!",
12 | "incorrectOldPassword": "पुरानो पासवर्ड गलत छ!",
13 | "unsupportedFileType": "अपलोड गरिएको फाइल प्रकार समर्थित छैन!",
14 | "badRequest": "खराब अनुरोध, त्यहाँ केहि त्रुटि थियो कृपया पछि फेरि प्रयास गर्नुहोस्!",
15 | "tooManyRequest": "धेरै प्रयत्नहरू, {second} सेकेन्ड पछि पुनःप्रयास गर्नुहोस्!",
16 | "invalidRefreshToken": "अवैध टोकन!",
17 | "deleteDefaultError": "पूर्वनिर्धारित आइटम मेटाउन मिल्दैन!",
18 | "refreshTokenExpired": "रिफ्रेस टोकनको म्याद सकिएको छ!",
19 | "tooManyTries": "धेरै अनुरोध निषेध!"
20 | }
21 |
--------------------------------------------------------------------------------
/src/i18n/ne/validation.json:
--------------------------------------------------------------------------------
1 | {
2 | "character": {
3 | "one": "character",
4 | "other": "characters",
5 | "zero": "character"
6 | },
7 | "Unauthorized": "अवैध ईमेल/पासवर्ड",
8 | "Not Found": "डाटा भेटिएन",
9 | "isNotEmpty": "$t(app.{property}) खाली हुनु हुँदैन",
10 | "maxLength": "$t(app.{property})को अधिकतम शब्द गणना {ln} मात्र हुनुपर्छ",
11 | "minLength": "$t(app.{property})क न्यूनतम शब्द गणना {ln} हुनुपर्छ",
12 | "max": "$t(app.{property}) {ln} भन्दा कम वा बराबर हुन सक्छ",
13 | "min": "$t(app.{property}) {ln} भन्दा ठूलो वा बराबर हुनै पर्छ",
14 | "password should contain at least one lowercase letter, one uppercase letter, one numeric digit, and one special character": "पासवर्ड कम्तिमा एउटा सानो अक्षर, एक ठूलो अक्षर, एक संख्यात्मक अंक, र एक विशेष वर्ण हुनु पर्छ",
15 | "isEqualTo": "$t(app.{property}) $t(app.{field}) जस्तै हुनु पर्छ",
16 | "isLowercase": "$t(app.{property}) लोअरकेसमा हुनुपर्छ",
17 | "isIn": "$t(app.{property}) {items} मध्ये एक हुनुपर्दछ",
18 | "isEnum": "$t(app.{property}) {items} मध्ये एक हुनुपर्दछ",
19 | "isNumberString": "$t(app.{property}) नम्बर हुनुपर्दछ",
20 | "isNumber": "$t(app.{property}) नम्बर हुनुपर्दछ",
21 | "isEmail": "क्षेत्र मान्य ईमेल हुनुपर्दछ",
22 | "isString": "क्षेत्र शब्दहरू हुनुपर्दछ",
23 | "isBoolean": "क्षेत्र सही वा गलत बीचमा चयन गरिएको हुनुपर्दछ",
24 | "already taken": "$t(app.{property}) पहिले नै अर्को प्रयोगकर्ता द्वारा प्रयोग गरीएको छ",
25 | "unique": "$t(app.{property}) पहिले नै अर्को प्रयोगकर्ता द्वारा प्रयोग गरीएको छ"
26 | }
27 |
--------------------------------------------------------------------------------
/src/mail/interface/mail-job.interface.ts:
--------------------------------------------------------------------------------
1 | export interface MailJobInterface {
2 | to: string;
3 | slug: string;
4 | subject: string;
5 | context: any;
6 | attachments?: any;
7 | }
8 |
--------------------------------------------------------------------------------
/src/mail/mail.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { BullModule } from '@nestjs/bull';
3 | import { MailerModule } from '@nestjs-modules/mailer';
4 | import { PugAdapter } from '@nestjs-modules/mailer/dist/adapters/pug.adapter';
5 | import * as config from 'config';
6 |
7 | import { MailService } from 'src/mail/mail.service';
8 | import { MailProcessor } from 'src/mail/mail.processor';
9 | import { EmailTemplateModule } from 'src/email-template/email-template.module';
10 |
11 | const mailConfig = config.get('mail');
12 | const queueConfig = config.get('queue');
13 |
14 | @Module({
15 | imports: [
16 | EmailTemplateModule,
17 | BullModule.registerQueueAsync({
18 | name: config.get('mail.queueName'),
19 | useFactory: () => ({
20 | redis: {
21 | host: process.env.REDIS_HOST || queueConfig.host,
22 | port: process.env.REDIS_PORT || queueConfig.port,
23 | password: process.env.REDIS_PASSWORD || queueConfig.password,
24 | retryStrategy(times) {
25 | return Math.min(times * 50, 2000);
26 | }
27 | }
28 | })
29 | }),
30 | MailerModule.forRootAsync({
31 | useFactory: () => ({
32 | transport: {
33 | host: process.env.MAIL_HOST || mailConfig.host,
34 | port: process.env.MAIL_PORT || mailConfig.port,
35 | secure: mailConfig.secure,
36 | ignoreTLS: mailConfig.ignoreTLS,
37 | auth: {
38 | user: process.env.MAIL_USER || mailConfig.user,
39 | pass: process.env.MAIL_PASS || mailConfig.pass
40 | }
41 | },
42 | defaults: {
43 | from: `"${process.env.MAIL_FROM || mailConfig.from}" <${
44 | process.env.MAIL_FROM || mailConfig.fromMail
45 | }>`
46 | },
47 | preview: mailConfig.preview,
48 | template: {
49 | dir: __dirname + '/templates/email/layouts/',
50 | adapter: new PugAdapter(),
51 | options: {
52 | strict: true
53 | }
54 | }
55 | })
56 | })
57 | ],
58 | controllers: [],
59 | providers: [MailService, MailProcessor],
60 | exports: [MailService]
61 | })
62 | export class MailModule {}
63 |
--------------------------------------------------------------------------------
/src/mail/mail.processor.ts:
--------------------------------------------------------------------------------
1 | import { Logger } from '@nestjs/common';
2 | import * as config from 'config';
3 | import { MailerService } from '@nestjs-modules/mailer';
4 | import {
5 | OnQueueActive,
6 | OnQueueCompleted,
7 | OnQueueFailed,
8 | Process,
9 | Processor
10 | } from '@nestjs/bull';
11 | import { Job } from 'bull';
12 |
13 | import { MailJobInterface } from 'src/mail/interface/mail-job.interface';
14 |
15 | @Processor(config.get('mail.queueName'))
16 | export class MailProcessor {
17 | private readonly logger = new Logger(this.constructor.name);
18 |
19 | constructor(private readonly mailerService: MailerService) {}
20 |
21 | @OnQueueActive()
22 | onActive(job: Job) {
23 | this.logger.debug(
24 | `Processing job ${job.id} of type ${job.name}. Data: ${JSON.stringify(
25 | job.data
26 | )}`
27 | );
28 | }
29 |
30 | @OnQueueCompleted()
31 | onComplete(job: Job, result: any) {
32 | this.logger.debug(
33 | `Completed job ${job.id} of type ${job.name}. Result: ${JSON.stringify(
34 | result
35 | )}`
36 | );
37 | }
38 |
39 | @OnQueueFailed()
40 | onError(job: Job, error: any) {
41 | this.logger.error(
42 | `Failed job ${job.id} of type ${job.name}: ${error.message}`,
43 | error.stack
44 | );
45 | }
46 |
47 | @Process('system-mail')
48 | async sendEmail(
49 | job: Job<{
50 | payload: MailJobInterface;
51 | type: string;
52 | }>
53 | ): Promise {
54 | this.logger.log(`Sending email to '${job.data.payload.to}'`);
55 | const mailConfig = config.get('mail');
56 | try {
57 | const options: Record = {
58 | to: job.data.payload.to,
59 | from: process.env.MAIL_FROM || mailConfig.fromMail,
60 | subject: job.data.payload.subject,
61 | template: 'email-layout',
62 | context: job.data.payload.context,
63 | attachments: job.data.payload.attachments
64 | };
65 | return await this.mailerService.sendMail({ ...options });
66 | } catch (error) {
67 | this.logger.error(
68 | `Failed to send email to '${job.data.payload.to}'`,
69 | error.stack
70 | );
71 | throw error;
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/mail/mail.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { Queue } from 'bull';
3 | import * as config from 'config';
4 | import { InjectQueue } from '@nestjs/bull';
5 |
6 | import { MailJobInterface } from 'src/mail/interface/mail-job.interface';
7 | import { EmailTemplateService } from 'src/email-template/email-template.service';
8 |
9 | @Injectable()
10 | export class MailService {
11 | constructor(
12 | @InjectQueue(config.get('mail.queueName'))
13 | private mailQueue: Queue,
14 | private readonly emailTemplateService: EmailTemplateService
15 | ) {}
16 |
17 | /**
18 | * Replace place holder
19 | * @param str
20 | * @param obj
21 | */
22 | stringInject(str = '', obj = {}) {
23 | let newStr = str;
24 | Object.keys(obj).forEach((key) => {
25 | const placeHolder = `{{${key}}}`;
26 | if (newStr.includes(placeHolder)) {
27 | newStr = newStr.replace(placeHolder, obj[key] || ' ');
28 | }
29 | });
30 | return newStr;
31 | }
32 |
33 | async sendMail(payload: MailJobInterface, type: string): Promise {
34 | const mailBody = await this.emailTemplateService.findBySlug(payload.slug);
35 | payload.context.content = this.stringInject(mailBody.body, payload.context);
36 | if (mailBody) {
37 | try {
38 | await this.mailQueue.add(type, {
39 | payload
40 | });
41 | return true;
42 | } catch (error) {
43 | return false;
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/mail/templates/email/activate-account.pug:
--------------------------------------------------------------------------------
1 | extends layouts/email-layout
2 | include mixins/_button
3 | block preHeader
4 | - let previewText = subject
5 |
6 | block content
7 | table.main(role='presentation')
8 | tbody
9 | tr
10 | td.wrapper
11 | table(role='presentation' border='0' cellpadding='0' cellspacing='0')
12 | tbody
13 | tr
14 | td
15 | p Hi #{username},
16 | p
17 | | A new account has been created using your email #{email}. Click below button to activate your account.
18 | +button(link, 'Activate Account →')
19 | p
20 | | If you haven't requested the code please ignore the email.
21 | p Thank you!.
22 |
--------------------------------------------------------------------------------
/src/mail/templates/email/assets/css/style.css:
--------------------------------------------------------------------------------
1 | img {
2 | border: none;
3 | -ms-interpolation-mode: bicubic;
4 | max-width: 100%;
5 | }
6 |
7 | body {
8 | background-color: #f6f6f6;
9 | font-family: sans-serif;
10 | -webkit-font-smoothing: antialiased;
11 | font-size: 14px;
12 | line-height: 1.4;
13 | margin: 0;
14 | padding: 0;
15 | -ms-text-size-adjust: 100%;
16 | -webkit-text-size-adjust: 100%;
17 | }
18 |
19 | .body {
20 | background-color: #f6f6f6;
21 | width: 100%;
22 | }
23 |
24 | /* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
25 | .container {
26 | display: block;
27 | margin: 0 auto !important;
28 | /* makes it centered */
29 | max-width: 580px;
30 | padding: 10px;
31 | width: 580px;
32 | }
33 |
34 | /* This should also be a block element, so that it will fill 100% of the .container */
35 | .content {
36 | box-sizing: border-box;
37 | display: block;
38 | margin: 0 auto;
39 | max-width: 580px;
40 | padding: 10px;
41 | }
42 |
43 | /* -------------------------------------
44 | HEADER, FOOTER, MAIN
45 | ------------------------------------- */
46 | .main {
47 | background: #ffffff;
48 | border-radius: 3px;
49 | width: 100%;
50 | }
51 |
52 | .wrapper {
53 | box-sizing: border-box;
54 | padding: 20px;
55 | }
56 |
57 | .content-block {
58 | padding-bottom: 10px;
59 | padding-top: 10px;
60 | }
61 |
62 | .footer {
63 | clear: both;
64 | margin-top: 10px;
65 | text-align: center;
66 | width: 100%;
67 | }
68 | .footer td,
69 | .footer p,
70 | .footer span,
71 | .footer a {
72 | color: #999999;
73 | font-size: 12px;
74 | text-align: center;
75 | }
76 |
77 | /* -------------------------------------
78 | TYPOGRAPHY
79 | ------------------------------------- */
80 | h1,
81 | h2,
82 | h3,
83 | h4 {
84 | color: #000000;
85 | font-family: sans-serif;
86 | font-weight: 400;
87 | line-height: 1.4;
88 | margin: 0;
89 | margin-bottom: 30px;
90 | }
91 |
92 | h1 {
93 | font-size: 35px;
94 | font-weight: 300;
95 | text-align: center;
96 | text-transform: capitalize;
97 | }
98 |
99 | p,
100 | ul,
101 | ol {
102 | font-family: sans-serif;
103 | font-size: 14px;
104 | font-weight: normal;
105 | margin: 0;
106 | margin-bottom: 15px;
107 | }
108 | p li,
109 | ul li,
110 | ol li {
111 | list-style-position: inside;
112 | margin-left: 5px;
113 | }
114 |
115 | a {
116 | color: #3498db;
117 | text-decoration: underline;
118 | }
119 |
120 | /* -------------------------------------
121 | OTHER STYLES THAT MIGHT BE USEFUL
122 | ------------------------------------- */
123 | .last {
124 | margin-bottom: 0;
125 | }
126 |
127 | .first {
128 | margin-top: 0;
129 | }
130 |
131 | .align-center {
132 | text-align: center;
133 | }
134 |
135 | .align-right {
136 | text-align: right;
137 | }
138 |
139 | .align-left {
140 | text-align: left;
141 | }
142 |
143 | .clear {
144 | clear: both;
145 | }
146 |
147 | .mt0 {
148 | margin-top: 0;
149 | }
150 |
151 | .mb0 {
152 | margin-bottom: 0;
153 | }
154 |
155 | .preheader {
156 | color: transparent;
157 | display: none;
158 | height: 0;
159 | max-height: 0;
160 | max-width: 0;
161 | opacity: 0;
162 | overflow: hidden;
163 | mso-hide: all;
164 | visibility: hidden;
165 | width: 0;
166 | }
167 |
168 | .powered-by a {
169 | text-decoration: none;
170 | }
171 |
172 | hr {
173 | border: 0;
174 | border-bottom: 1px solid #f6f6f6;
175 | margin: 20px 0;
176 | }
177 |
178 | .content a {
179 | background-color: #3498db;
180 | border-color: #3498db;
181 | color: #ffffff;
182 | border-radius: 5px;
183 | box-sizing: border-box;
184 | cursor: pointer;
185 | display: inline-block;
186 | font-size: 14px;
187 | font-weight: bold;
188 | margin: 0;
189 | padding: 12px 25px;
190 | text-decoration: none;
191 | text-transform: capitalize;
192 | }
193 |
--------------------------------------------------------------------------------
/src/mail/templates/email/layouts/email-layout.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html(lang='en')
3 | head
4 | meta(name='viewport' content='width=device-width')
5 | meta(http-equiv='Content-Type' content='text/html; charset=UTF-8')
6 | style
7 | include ../assets/css/style.css
8 | title Truthy CMS
9 | body
10 | span.preheader #{subject}
11 | table.body(role='presentation' border='0' cellpadding='0' cellspacing='0')
12 | tbody
13 | tr
14 | td
15 | td.container
16 | .content !{content}
17 | include ../partials/footer
18 |
--------------------------------------------------------------------------------
/src/mail/templates/email/mixins/_button.pug:
--------------------------------------------------------------------------------
1 | mixin button(url, text)
2 | table.btn.btn-primary(role='presentation' border='0' cellpadding='0' cellspacing='0')
3 | tbody
4 | tr
5 | td(align='left')
6 | table(role='presentation' border='0' cellpadding='0' cellspacing='0')
7 | tbody
8 | tr
9 | td
10 | a(href=url, target='_blank')= text
11 |
--------------------------------------------------------------------------------
/src/mail/templates/email/partials/footer.pug:
--------------------------------------------------------------------------------
1 | .footer
2 | table(role='presentation' border='0' cellpadding='0' cellspacing='0')
3 | tbody
4 | //tr
5 | // td.content-block
6 | // span.Truthy CMS
7 | // br
8 | // | Don't like these emails?
9 | // a(href='https://github.com/gobeam/truthy') Unsubscribe
10 | // | .
11 | tr
12 | td.content-block.powered-by
13 | | Powered by
14 | a(href='https://github.com/gobeam/truthy') Truthy CMS
15 | | .
16 |
--------------------------------------------------------------------------------
/src/mail/templates/email/password-reset.pug:
--------------------------------------------------------------------------------
1 | extends layouts/email-layout
2 | include mixins/_button
3 | block preHeader
4 | - let previewText = subject
5 |
6 | block content
7 | table.main(role='presentation')
8 | tbody
9 | tr
10 | td.wrapper
11 | table(role='presentation' border='0' cellpadding='0' cellspacing='0')
12 | tbody
13 | tr
14 | td
15 | p Hi #{username},
16 | p
17 | | You have requested a #{subject.toLowerCase()}. Please use following code to complete the action. Please note this link is only valid for the next hour.
18 | +button(link, `${subject} →`)
19 | p
20 | | If you haven't requested the code please ignore the email.
21 | p Thank you!.
22 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { ValidationPipe } from '@nestjs/common';
3 | import { useContainer } from 'class-validator';
4 | import * as config from 'config';
5 | import helmet from 'helmet';
6 | import {
7 | DocumentBuilder,
8 | SwaggerCustomOptions,
9 | SwaggerModule
10 | } from '@nestjs/swagger';
11 | import * as cookieParser from 'cookie-parser';
12 | import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
13 |
14 | import { AppModule } from 'src/app.module';
15 |
16 | async function bootstrap() {
17 | const serverConfig = config.get('server');
18 | const port = process.env.PORT || serverConfig.port;
19 | const app = await NestFactory.create(AppModule);
20 | app.use(helmet());
21 | app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));
22 | const apiConfig = config.get('app');
23 | if (process.env.NODE_ENV === 'development') {
24 | app.enableCors({
25 | origin: true,
26 | methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
27 | credentials: true
28 | });
29 | const swaggerConfig = new DocumentBuilder()
30 | .setTitle(apiConfig.name)
31 | .setDescription(apiConfig.description)
32 | .setVersion(apiConfig.version)
33 | .addBearerAuth()
34 | .build();
35 | const customOptions: SwaggerCustomOptions = {
36 | swaggerOptions: {
37 | persistAuthorization: true
38 | },
39 | customSiteTitle: apiConfig.description
40 | };
41 | const document = SwaggerModule.createDocument(app, swaggerConfig);
42 | SwaggerModule.setup('api-docs', app, document, customOptions);
43 | } else {
44 | const whitelist = [apiConfig.get('frontendUrl')];
45 | app.enableCors({
46 | origin: function (origin, callback) {
47 | if (!origin || whitelist.indexOf(origin) !== -1) {
48 | callback(null, true);
49 | } else {
50 | callback(new Error('Not allowed by CORS'));
51 | }
52 | },
53 | credentials: true
54 | });
55 | }
56 | useContainer(app.select(AppModule), {
57 | fallbackOnErrors: true
58 | });
59 | app.useGlobalPipes(
60 | new ValidationPipe({
61 | transform: true,
62 | whitelist: true,
63 | forbidNonWhitelisted: true
64 | })
65 | );
66 |
67 | app.use(cookieParser());
68 | await app.listen(port);
69 | console.log(`Application listening in port: ${port}`);
70 | }
71 |
72 | bootstrap();
73 |
--------------------------------------------------------------------------------
/src/paginate/index.ts:
--------------------------------------------------------------------------------
1 | export * from 'src/paginate/pagination.results.interface';
2 | export * from 'src/paginate/pagination';
3 |
--------------------------------------------------------------------------------
/src/paginate/pagination-info.interface.ts:
--------------------------------------------------------------------------------
1 | export interface PaginationInfoInterface {
2 | skip: number;
3 | limit: number;
4 | page: number;
5 | }
6 |
--------------------------------------------------------------------------------
/src/paginate/pagination.results.interface.ts:
--------------------------------------------------------------------------------
1 | export interface PaginationResultInterface {
2 | results: PaginationEntity[];
3 | currentPage: number;
4 | pageSize: number;
5 | totalItems: number;
6 | next: number;
7 | previous: number;
8 | }
9 |
--------------------------------------------------------------------------------
/src/paginate/pagination.ts:
--------------------------------------------------------------------------------
1 | import { PaginationResultInterface } from 'src/paginate/pagination.results.interface';
2 | export class Pagination {
3 | public results: PaginationEntity[];
4 | public currentPage: number;
5 | public pageSize: number;
6 | public totalItems: number;
7 | public next: number;
8 | public previous: number;
9 |
10 | constructor(paginationResults: PaginationResultInterface) {
11 | this.results = paginationResults.results;
12 | this.currentPage = paginationResults.currentPage;
13 | this.pageSize = paginationResults.pageSize;
14 | this.totalItems = paginationResults.totalItems;
15 | this.next = paginationResults.next;
16 | this.previous = paginationResults.previous;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/permission/dto/create-permission.dto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IsIn,
3 | IsNotEmpty,
4 | IsString,
5 | MaxLength,
6 | Validate
7 | } from 'class-validator';
8 |
9 | import { MethodList } from 'src/config/permission-config';
10 | import { UniqueValidatorPipe } from 'src/common/pipes/unique-validator.pipe';
11 | import { PermissionEntity } from 'src/permission/entities/permission.entity';
12 |
13 | const methodListArray = [
14 | MethodList.GET,
15 | MethodList.POST,
16 | MethodList.ANY,
17 | MethodList.DELETE,
18 | MethodList.OPTIONS,
19 | MethodList.OPTIONS
20 | ];
21 |
22 | export class CreatePermissionDto {
23 | @IsNotEmpty()
24 | @IsString()
25 | @MaxLength(50, {
26 | message: 'maxLength-{"ln":50,"count":50}'
27 | })
28 | resource: string;
29 |
30 | @IsNotEmpty()
31 | @IsString()
32 | @Validate(UniqueValidatorPipe, [PermissionEntity], {
33 | message: 'already taken'
34 | })
35 | description: string;
36 |
37 | @IsNotEmpty()
38 | @IsString()
39 | @MaxLength(50, {
40 | message: 'maxLength-{"ln":50,"count":50}'
41 | })
42 | path: string;
43 |
44 | @IsNotEmpty()
45 | @IsIn(methodListArray, {
46 | message: `isIn-{"items":"${methodListArray.join(',')}"}`
47 | })
48 | method: MethodList;
49 | }
50 |
--------------------------------------------------------------------------------
/src/permission/dto/permission-filter.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/swagger';
2 |
3 | import { CommonSearchFieldDto } from 'src/common/extra/common-search-field.dto';
4 |
5 | export class PermissionFilterDto extends PartialType(CommonSearchFieldDto) {}
6 |
--------------------------------------------------------------------------------
/src/permission/dto/update-permission.dto.ts:
--------------------------------------------------------------------------------
1 | import { Optional } from '@nestjs/common';
2 | import { ApiPropertyOptional, PartialType } from '@nestjs/swagger';
3 | import { IsString } from 'class-validator';
4 |
5 | import { CreatePermissionDto } from 'src/permission/dto/create-permission.dto';
6 |
7 | export class UpdatePermissionDto extends PartialType(CreatePermissionDto) {
8 | @ApiPropertyOptional()
9 | @Optional()
10 | @IsString()
11 | description: string;
12 | }
13 |
--------------------------------------------------------------------------------
/src/permission/entities/permission.entity.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, Index, ManyToMany, Unique } from 'typeorm';
2 |
3 | import { CustomBaseEntity } from 'src/common/entity/custom-base.entity';
4 | import { RoleEntity } from 'src/role/entities/role.entity';
5 |
6 | @Entity({
7 | name: 'permission'
8 | })
9 | @Unique(['description'])
10 | export class PermissionEntity extends CustomBaseEntity {
11 | @Column('varchar', { length: 100 })
12 | resource: string;
13 |
14 | @Column()
15 | @Index({
16 | unique: true
17 | })
18 | description: string;
19 |
20 | @Column()
21 | path: string;
22 |
23 | @Column('varchar', {
24 | default: 'get',
25 | length: 20
26 | })
27 | method: string;
28 |
29 | @Column()
30 | isDefault: boolean;
31 |
32 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
33 | @ManyToMany((type) => RoleEntity, (role) => role.permission)
34 | role: RoleEntity[];
35 |
36 | constructor(data?: Partial) {
37 | super();
38 | if (data) {
39 | Object.assign(this, data);
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/permission/misc/load-permission.misc.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ModulesPayloadInterface,
3 | PermissionPayload,
4 | RoutePayloadInterface,
5 | SubModulePayloadInterface
6 | } from 'src/config/permission-config';
7 |
8 | export class LoadPermissionMisc {
9 | assignResourceAndConcatPermission(
10 | modules: ModulesPayloadInterface | SubModulePayloadInterface,
11 | permissionsList: RoutePayloadInterface[],
12 | resource: string,
13 | isDefault?: false
14 | ) {
15 | if (modules.permissions) {
16 | for (const permission of modules.permissions) {
17 | permissionsList = this.concatPermissions(
18 | permission,
19 | permissionsList,
20 | resource,
21 | isDefault
22 | );
23 | }
24 | }
25 | return permissionsList;
26 | }
27 |
28 | concatPermissions(
29 | permission: PermissionPayload,
30 | permissionsList: RoutePayloadInterface[],
31 | resource: string,
32 | isDefault: boolean
33 | ) {
34 | const description = permission.name;
35 | for (const data of permission.route) {
36 | data.resource = data.resource || resource;
37 | data.description = data.description || description;
38 | data.isDefault = isDefault;
39 | }
40 | return permissionsList.concat(permission.route);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/permission/permission.repository.ts:
--------------------------------------------------------------------------------
1 | import { EntityRepository } from 'typeorm';
2 | import { classToPlain, plainToClass } from 'class-transformer';
3 |
4 | import { PermissionEntity } from 'src/permission/entities/permission.entity';
5 | import { BaseRepository } from 'src/common/repository/base.repository';
6 | import { Permission } from 'src/permission/serializer/permission.serializer';
7 | import { RoutePayloadInterface } from 'src/config/permission-config';
8 |
9 | @EntityRepository(PermissionEntity)
10 | export class PermissionRepository extends BaseRepository<
11 | PermissionEntity,
12 | Permission
13 | > {
14 | async syncPermission(
15 | permissionsList: RoutePayloadInterface[]
16 | ): Promise {
17 | await this.createQueryBuilder('permission')
18 | .insert()
19 | .into(PermissionEntity)
20 | .values(permissionsList)
21 | .orIgnore()
22 | .execute();
23 | }
24 |
25 | transform(model: PermissionEntity, transformOption = {}): Permission {
26 | return plainToClass(
27 | Permission,
28 | classToPlain(model, transformOption),
29 | transformOption
30 | );
31 | }
32 |
33 | transformMany(
34 | models: PermissionEntity[],
35 | transformOption = {}
36 | ): Permission[] {
37 | return models.map((model) => this.transform(model, transformOption));
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/permission/permissions.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Delete,
5 | Get,
6 | HttpCode,
7 | HttpStatus,
8 | Param,
9 | Post,
10 | Put,
11 | Query,
12 | UseGuards
13 | } from '@nestjs/common';
14 | import { ApiBearerAuth, ApiQuery, ApiTags } from '@nestjs/swagger';
15 |
16 | import { PermissionsService } from 'src/permission/permissions.service';
17 | import { CreatePermissionDto } from 'src/permission/dto/create-permission.dto';
18 | import { UpdatePermissionDto } from 'src/permission/dto/update-permission.dto';
19 | import { PermissionFilterDto } from 'src/permission/dto/permission-filter.dto';
20 | import { Permission } from 'src/permission/serializer/permission.serializer';
21 | import { PermissionGuard } from 'src/common/guard/permission.guard';
22 | import { Pagination } from 'src/paginate';
23 | import JwtTwoFactorGuard from 'src/common/guard/jwt-two-factor.guard';
24 |
25 | @ApiTags('permissions')
26 | @UseGuards(JwtTwoFactorGuard, PermissionGuard)
27 | @Controller('permissions')
28 | @ApiBearerAuth()
29 | export class PermissionsController {
30 | constructor(private readonly permissionsService: PermissionsService) {}
31 |
32 | @Post()
33 | create(
34 | @Body()
35 | createPermissionDto: CreatePermissionDto
36 | ): Promise {
37 | return this.permissionsService.create(createPermissionDto);
38 | }
39 |
40 | @Get()
41 | @ApiQuery({
42 | type: PermissionFilterDto
43 | })
44 | findAll(
45 | @Query()
46 | permissionFilterDto: PermissionFilterDto
47 | ): Promise> {
48 | return this.permissionsService.findAll(permissionFilterDto);
49 | }
50 |
51 | @Get(':id')
52 | findOne(
53 | @Param('id')
54 | id: string
55 | ): Promise {
56 | return this.permissionsService.findOne(+id);
57 | }
58 |
59 | @Put(':id')
60 | update(
61 | @Param('id')
62 | id: string,
63 | @Body()
64 | updatePermissionDto: UpdatePermissionDto
65 | ): Promise {
66 | return this.permissionsService.update(+id, updatePermissionDto);
67 | }
68 |
69 | @Delete(':id')
70 | @HttpCode(HttpStatus.NO_CONTENT)
71 | remove(
72 | @Param('id')
73 | id: string
74 | ): Promise {
75 | return this.permissionsService.remove(+id);
76 | }
77 |
78 | @Post('/sync')
79 | @HttpCode(HttpStatus.NO_CONTENT)
80 | syncPermission(): Promise {
81 | return this.permissionsService.syncPermission();
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/permission/permissions.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 |
4 | import { PermissionsService } from 'src/permission/permissions.service';
5 | import { PermissionsController } from 'src/permission/permissions.controller';
6 | import { PermissionRepository } from 'src/permission/permission.repository';
7 | import { UniqueValidatorPipe } from 'src/common/pipes/unique-validator.pipe';
8 | import { AuthModule } from 'src/auth/auth.module';
9 |
10 | @Module({
11 | imports: [TypeOrmModule.forFeature([PermissionRepository]), AuthModule],
12 | exports: [PermissionsService],
13 | controllers: [PermissionsController],
14 | providers: [PermissionsService, UniqueValidatorPipe]
15 | })
16 | export class PermissionsModule {}
17 |
--------------------------------------------------------------------------------
/src/permission/serializer/permission.serializer.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2 | import { Expose } from 'class-transformer';
3 |
4 | import { ModelSerializer } from 'src/common/serializer/model.serializer';
5 |
6 | export const basicFieldGroupsForSerializing: string[] = ['basic'];
7 |
8 | export class Permission extends ModelSerializer {
9 | @Expose({
10 | groups: basicFieldGroupsForSerializing
11 | })
12 | id: number;
13 |
14 | @ApiProperty()
15 | resource: string;
16 |
17 | @ApiProperty()
18 | @Expose({
19 | groups: basicFieldGroupsForSerializing
20 | })
21 | description: string;
22 |
23 | @ApiProperty()
24 | path: string;
25 |
26 | @ApiProperty()
27 | method: string;
28 |
29 | @ApiProperty()
30 | @Expose({
31 | groups: basicFieldGroupsForSerializing
32 | })
33 | isDefault: boolean;
34 |
35 | @ApiPropertyOptional()
36 | @Expose({
37 | groups: basicFieldGroupsForSerializing
38 | })
39 | createdAt: Date;
40 |
41 | @ApiPropertyOptional()
42 | @Expose({
43 | groups: basicFieldGroupsForSerializing
44 | })
45 | updatedAt: Date;
46 | }
47 |
--------------------------------------------------------------------------------
/src/refresh-token/dto/refresh-paginate-filter.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/swagger';
2 | import { CommonSearchFieldDto } from 'src/common/extra/common-search-field.dto';
3 |
4 | export class RefreshPaginateFilterDto extends PartialType(
5 | CommonSearchFieldDto
6 | ) {}
7 |
--------------------------------------------------------------------------------
/src/refresh-token/entities/refresh-token.entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BaseEntity,
3 | Column,
4 | Entity,
5 | Index,
6 | PrimaryGeneratedColumn
7 | } from 'typeorm';
8 |
9 | @Entity({
10 | name: 'refresh_token'
11 | })
12 | export class RefreshToken extends BaseEntity {
13 | @PrimaryGeneratedColumn()
14 | id: number;
15 |
16 | @Column()
17 | userId: number;
18 |
19 | @Column()
20 | ip: string;
21 |
22 | @Column()
23 | userAgent: string;
24 |
25 | @Index()
26 | @Column({
27 | nullable: true
28 | })
29 | browser: string;
30 |
31 | @Index()
32 | @Column({
33 | nullable: true
34 | })
35 | os: string;
36 |
37 | @Column()
38 | isRevoked: boolean;
39 |
40 | @Column()
41 | expires: Date;
42 | }
43 |
--------------------------------------------------------------------------------
/src/refresh-token/interface/refresh-token.interface.ts:
--------------------------------------------------------------------------------
1 | export interface RefreshTokenInterface {
2 | jwtid: number;
3 | subject: number;
4 | }
5 |
--------------------------------------------------------------------------------
/src/refresh-token/refresh-token.module.ts:
--------------------------------------------------------------------------------
1 | import { forwardRef, Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 |
4 | import { RefreshTokenService } from 'src/refresh-token/refresh-token.service';
5 | import { AuthModule } from 'src/auth/auth.module';
6 | import { RefreshTokenRepository } from 'src/refresh-token/refresh-token.repository';
7 |
8 | @Module({
9 | imports: [
10 | forwardRef(() => AuthModule),
11 | TypeOrmModule.forFeature([RefreshTokenRepository])
12 | ],
13 | providers: [RefreshTokenService],
14 | exports: [RefreshTokenService],
15 | controllers: []
16 | })
17 | export class RefreshTokenModule {}
18 |
--------------------------------------------------------------------------------
/src/refresh-token/refresh-token.repository.ts:
--------------------------------------------------------------------------------
1 | import { EntityRepository } from 'typeorm';
2 | import * as config from 'config';
3 |
4 | import { RefreshToken } from 'src/refresh-token/entities/refresh-token.entity';
5 | import { UserSerializer } from 'src/auth/serializer/user.serializer';
6 | import { BaseRepository } from 'src/common/repository/base.repository';
7 | import { RefreshTokenSerializer } from 'src/refresh-token/serializer/refresh-token.serializer';
8 |
9 | const tokenConfig = config.get('jwt');
10 | @EntityRepository(RefreshToken)
11 | export class RefreshTokenRepository extends BaseRepository<
12 | RefreshToken,
13 | RefreshTokenSerializer
14 | > {
15 | /**
16 | * Create refresh token
17 | * @param user
18 | * @param tokenPayload
19 | */
20 | public async createRefreshToken(
21 | user: UserSerializer,
22 | tokenPayload: Partial
23 | ): Promise {
24 | const token = this.create();
25 | token.userId = user.id;
26 | token.isRevoked = false;
27 | token.ip = tokenPayload.ip;
28 | token.userAgent = tokenPayload.userAgent;
29 | token.browser = tokenPayload.browser;
30 | token.os = tokenPayload.os;
31 | const expiration = new Date();
32 | expiration.setSeconds(
33 | expiration.getSeconds() + tokenConfig.refreshExpiresIn
34 | );
35 | token.expires = expiration;
36 | return token.save();
37 | }
38 |
39 | /**
40 | * find token by id
41 | * @param id
42 | */
43 | public async findTokenById(id: number): Promise {
44 | return this.findOne({
45 | where: {
46 | id
47 | }
48 | });
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/refresh-token/serializer/refresh-token.serializer.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty } from '@nestjs/swagger';
2 | import { ModelSerializer } from 'src/common/serializer/model.serializer';
3 |
4 | export class RefreshTokenSerializer extends ModelSerializer {
5 | id: number;
6 |
7 | @ApiProperty()
8 | userId: number;
9 |
10 | @ApiProperty()
11 | ip: string;
12 |
13 | @ApiProperty()
14 | userAgent: string;
15 |
16 | @ApiProperty()
17 | browser: string;
18 |
19 | @ApiProperty()
20 | os: string;
21 |
22 | @ApiProperty()
23 | isRevoked: boolean;
24 |
25 | @ApiProperty()
26 | expires: Date;
27 | }
28 |
--------------------------------------------------------------------------------
/src/role/dto/create-role.dto.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IsNotEmpty,
3 | IsNumber,
4 | IsString,
5 | MaxLength,
6 | MinLength,
7 | Validate,
8 | ValidateIf
9 | } from 'class-validator';
10 |
11 | import { UniqueValidatorPipe } from 'src/common/pipes/unique-validator.pipe';
12 | import { RoleEntity } from 'src/role/entities/role.entity';
13 |
14 | export class CreateRoleDto {
15 | @IsNotEmpty()
16 | @IsString()
17 | @MinLength(2, {
18 | message: 'minLength-{"ln":2,"count":2}'
19 | })
20 | @MaxLength(100, {
21 | message: 'maxLength-{"ln":100,"count":100}'
22 | })
23 | @Validate(UniqueValidatorPipe, [RoleEntity], {
24 | message: 'already taken'
25 | })
26 | name: string;
27 |
28 | @ValidateIf((object, value) => value)
29 | @IsString()
30 | description: string;
31 |
32 | @ValidateIf((object, value) => value)
33 | @IsNumber(
34 | {},
35 | {
36 | each: true,
37 | message: 'should be array of numbers'
38 | }
39 | )
40 | permissions: number[];
41 | }
42 |
--------------------------------------------------------------------------------
/src/role/dto/role-filter.dto.ts:
--------------------------------------------------------------------------------
1 | import { PartialType } from '@nestjs/swagger';
2 |
3 | import { CommonSearchFieldDto } from 'src/common/extra/common-search-field.dto';
4 |
5 | export class RoleFilterDto extends PartialType(CommonSearchFieldDto) {}
6 |
--------------------------------------------------------------------------------
/src/role/dto/update-role.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, MaxLength, MinLength, ValidateIf } from 'class-validator';
2 | import { ApiPropertyOptional, PartialType } from '@nestjs/swagger';
3 |
4 | import { CreateRoleDto } from 'src/role/dto/create-role.dto';
5 |
6 | export class UpdateRoleDto extends PartialType(CreateRoleDto) {
7 | @ApiPropertyOptional()
8 | @ValidateIf((object, value) => value)
9 | @IsString()
10 | @MinLength(2)
11 | @MaxLength(100)
12 | name: string;
13 | }
14 |
--------------------------------------------------------------------------------
/src/role/entities/role.entity.ts:
--------------------------------------------------------------------------------
1 | import { Column, Entity, Index, JoinTable, ManyToMany, Unique } from 'typeorm';
2 |
3 | import { CustomBaseEntity } from 'src/common/entity/custom-base.entity';
4 | import { PermissionEntity } from 'src/permission/entities/permission.entity';
5 |
6 | @Entity({
7 | name: 'role'
8 | })
9 | @Unique(['name'])
10 | export class RoleEntity extends CustomBaseEntity {
11 | @Column('varchar', { length: 100 })
12 | @Index({
13 | unique: true
14 | })
15 | name: string;
16 |
17 | @Column('text')
18 | description: string;
19 |
20 | @ManyToMany(() => PermissionEntity, (permission) => permission.role)
21 | @JoinTable({
22 | name: 'role_permission',
23 | joinColumn: {
24 | name: 'roleId',
25 | referencedColumnName: 'id'
26 | },
27 | inverseJoinColumn: {
28 | name: 'permissionId',
29 | referencedColumnName: 'id'
30 | }
31 | })
32 | permission: PermissionEntity[];
33 |
34 | constructor(data?: Partial) {
35 | super();
36 | if (data) {
37 | Object.assign(this, data);
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/role/role.repository.ts:
--------------------------------------------------------------------------------
1 | import { EntityRepository } from 'typeorm';
2 | import { classToPlain, plainToClass } from 'class-transformer';
3 |
4 | import { RoleEntity } from 'src/role/entities/role.entity';
5 | import { RoleSerializer } from 'src/role/serializer/role.serializer';
6 | import { BaseRepository } from 'src/common/repository/base.repository';
7 | import { CreateRoleDto } from 'src/role/dto/create-role.dto';
8 | import { PermissionEntity } from 'src/permission/entities/permission.entity';
9 | import { UpdateRoleDto } from 'src/role/dto/update-role.dto';
10 |
11 | @EntityRepository(RoleEntity)
12 | export class RoleRepository extends BaseRepository {
13 | async store(
14 | createRoleDto: CreateRoleDto,
15 | permissions: PermissionEntity[]
16 | ): Promise {
17 | const { name, description } = createRoleDto;
18 | const role = this.create();
19 | role.name = name;
20 | role.description = description;
21 | role.permission = permissions;
22 | await role.save();
23 | return this.transform(role);
24 | }
25 |
26 | async updateItem(
27 | role: RoleEntity,
28 | updateRoleDto: UpdateRoleDto,
29 | permission: PermissionEntity[]
30 | ): Promise {
31 | const fields = ['name', 'description'];
32 | for (const field of fields) {
33 | if (updateRoleDto[field]) {
34 | role[field] = updateRoleDto[field];
35 | }
36 | }
37 | if (permission && permission.length > 0) {
38 | role.permission = permission;
39 | }
40 | await role.save();
41 | return this.transform(role);
42 | }
43 |
44 | /**
45 | * transform single role
46 | * @param model
47 | * @param transformOption
48 | */
49 | transform(model: RoleEntity, transformOption = {}): RoleSerializer {
50 | return plainToClass(
51 | RoleSerializer,
52 | classToPlain(model, transformOption),
53 | transformOption
54 | );
55 | }
56 |
57 | /**
58 | * transform array of roles
59 | * @param models
60 | * @param transformOption
61 | */
62 | transformMany(models: RoleEntity[], transformOption = {}): RoleSerializer[] {
63 | return models.map((model) => this.transform(model, transformOption));
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/role/roles.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | Delete,
5 | Get,
6 | HttpCode,
7 | HttpStatus,
8 | Param,
9 | Post,
10 | Put,
11 | Query,
12 | UseGuards
13 | } from '@nestjs/common';
14 | import { ApiBearerAuth, ApiQuery, ApiTags } from '@nestjs/swagger';
15 |
16 | import { RolesService } from 'src/role/roles.service';
17 | import { CreateRoleDto } from 'src/role/dto/create-role.dto';
18 | import { UpdateRoleDto } from 'src/role/dto/update-role.dto';
19 | import { RoleFilterDto } from 'src/role/dto/role-filter.dto';
20 | import { RoleSerializer } from 'src/role/serializer/role.serializer';
21 | import { Pagination } from 'src/paginate';
22 | import { PermissionGuard } from 'src/common/guard/permission.guard';
23 | import JwtTwoFactorGuard from 'src/common/guard/jwt-two-factor.guard';
24 |
25 | @ApiTags('roles')
26 | @UseGuards(JwtTwoFactorGuard, PermissionGuard)
27 | @Controller('roles')
28 | @ApiBearerAuth()
29 | export class RolesController {
30 | constructor(private readonly rolesService: RolesService) {}
31 |
32 | @Post()
33 | create(
34 | @Body()
35 | createRoleDto: CreateRoleDto
36 | ): Promise {
37 | return this.rolesService.create(createRoleDto);
38 | }
39 |
40 | @Get()
41 | @ApiQuery({
42 | type: RoleFilterDto
43 | })
44 | findAll(
45 | @Query()
46 | roleFilterDto: RoleFilterDto
47 | ): Promise> {
48 | return this.rolesService.findAll(roleFilterDto);
49 | }
50 |
51 | @Get(':id')
52 | findOne(
53 | @Param('id')
54 | id: string
55 | ): Promise {
56 | return this.rolesService.findOne(+id);
57 | }
58 |
59 | @Put(':id')
60 | update(
61 | @Param('id')
62 | id: string,
63 | @Body()
64 | updateRoleDto: UpdateRoleDto
65 | ): Promise {
66 | return this.rolesService.update(+id, updateRoleDto);
67 | }
68 |
69 | @Delete(':id')
70 | @HttpCode(HttpStatus.NO_CONTENT)
71 | remove(
72 | @Param('id')
73 | id: string
74 | ): Promise {
75 | return this.rolesService.remove(+id);
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/role/roles.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TypeOrmModule } from '@nestjs/typeorm';
3 |
4 | import { RolesService } from 'src/role/roles.service';
5 | import { RolesController } from 'src/role/roles.controller';
6 | import { RoleRepository } from 'src/role/role.repository';
7 | import { UniqueValidatorPipe } from 'src/common/pipes/unique-validator.pipe';
8 | import { AuthModule } from 'src/auth/auth.module';
9 | import { PermissionsModule } from 'src/permission/permissions.module';
10 |
11 | @Module({
12 | imports: [
13 | TypeOrmModule.forFeature([RoleRepository]),
14 | AuthModule,
15 | PermissionsModule
16 | ],
17 | exports: [],
18 | controllers: [RolesController],
19 | providers: [RolesService, UniqueValidatorPipe]
20 | })
21 | export class RolesModule {}
22 |
--------------------------------------------------------------------------------
/src/role/roles.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, UnprocessableEntityException } from '@nestjs/common';
2 | import { InjectRepository } from '@nestjs/typeorm';
3 | import { Not, ObjectLiteral } from 'typeorm';
4 |
5 | import { NotFoundException } from 'src/exception/not-found.exception';
6 | import { CreateRoleDto } from 'src/role/dto/create-role.dto';
7 | import { UpdateRoleDto } from 'src/role/dto/update-role.dto';
8 | import { RoleRepository } from 'src/role/role.repository';
9 | import { RoleFilterDto } from 'src/role/dto/role-filter.dto';
10 | import {
11 | adminUserGroupsForSerializing,
12 | basicFieldGroupsForSerializing,
13 | RoleSerializer
14 | } from 'src/role/serializer/role.serializer';
15 | import { CommonServiceInterface } from 'src/common/interfaces/common-service.interface';
16 | import { PermissionsService } from 'src/permission/permissions.service';
17 | import { Pagination } from 'src/paginate';
18 |
19 | @Injectable()
20 | export class RolesService implements CommonServiceInterface {
21 | constructor(
22 | @InjectRepository(RoleRepository)
23 | private repository: RoleRepository,
24 | private readonly permissionsService: PermissionsService
25 | ) {}
26 |
27 | /**
28 | * Get Permission Id array
29 | * @param ids
30 | */
31 | async getPermissionByIds(ids) {
32 | if (ids && ids.length > 0) {
33 | return await this.permissionsService.whereInIds(ids);
34 | }
35 | return [];
36 | }
37 |
38 | /**
39 | * Find by name
40 | * @param name
41 | */
42 | async findByName(name) {
43 | return await this.repository.findOne({ name });
44 | }
45 |
46 | /**
47 | * create new role
48 | * @param createRoleDto
49 | */
50 | async create(createRoleDto: CreateRoleDto): Promise {
51 | const { permissions } = createRoleDto;
52 | const permission = await this.getPermissionByIds(permissions);
53 | return this.repository.store(createRoleDto, permission);
54 | }
55 |
56 | /**
57 | * find and return collection of roles
58 | * @param roleFilterDto
59 | */
60 | async findAll(
61 | roleFilterDto: RoleFilterDto
62 | ): Promise> {
63 | return this.repository.paginate(
64 | roleFilterDto,
65 | [],
66 | ['name', 'description'],
67 | {
68 | groups: [
69 | ...adminUserGroupsForSerializing,
70 | ...basicFieldGroupsForSerializing
71 | ]
72 | }
73 | );
74 | }
75 |
76 | /**
77 | * find role by id
78 | * @param id
79 | */
80 | async findOne(id: number): Promise {
81 | return this.repository.get(id, ['permission'], {
82 | groups: [
83 | ...adminUserGroupsForSerializing,
84 | ...basicFieldGroupsForSerializing
85 | ]
86 | });
87 | }
88 |
89 | /**
90 | * update role by id
91 | * @param id
92 | * @param updateRoleDto
93 | */
94 | async update(
95 | id: number,
96 | updateRoleDto: UpdateRoleDto
97 | ): Promise {
98 | const role = await this.repository.findOne(id);
99 | if (!role) {
100 | throw new NotFoundException();
101 | }
102 | const condition: ObjectLiteral = {
103 | name: updateRoleDto.name
104 | };
105 | condition.id = Not(id);
106 | const checkUniqueTitle = await this.repository.countEntityByCondition(
107 | condition
108 | );
109 | if (checkUniqueTitle > 0) {
110 | throw new UnprocessableEntityException({
111 | property: 'name',
112 | constraints: {
113 | unique: 'already taken'
114 | }
115 | });
116 | }
117 | const { permissions } = updateRoleDto;
118 | const permission = await this.getPermissionByIds(permissions);
119 | return this.repository.updateItem(role, updateRoleDto, permission);
120 | }
121 |
122 | /**
123 | * remove role by id
124 | * @param id
125 | */
126 | async remove(id: number): Promise {
127 | await this.findOne(id);
128 | await this.repository.delete({ id });
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/role/serializer/role.serializer.ts:
--------------------------------------------------------------------------------
1 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2 | import { Expose, Type } from 'class-transformer';
3 |
4 | import { ModelSerializer } from 'src/common/serializer/model.serializer';
5 | import { Permission } from 'src/permission/serializer/permission.serializer';
6 |
7 | export const adminUserGroupsForSerializing: string[] = ['admin'];
8 | export const basicFieldGroupsForSerializing: string[] = ['basic'];
9 |
10 | export class RoleSerializer extends ModelSerializer {
11 | id: number;
12 |
13 | @ApiProperty()
14 | name: string;
15 |
16 | @ApiPropertyOptional()
17 | @Expose({
18 | groups: basicFieldGroupsForSerializing
19 | })
20 | description: string;
21 |
22 | @Type(() => Permission)
23 | permission: Permission[];
24 |
25 | @ApiPropertyOptional()
26 | @Expose({
27 | groups: basicFieldGroupsForSerializing
28 | })
29 | createdAt: Date;
30 |
31 | @ApiPropertyOptional()
32 | @Expose({
33 | groups: basicFieldGroupsForSerializing
34 | })
35 | updatedAt: Date;
36 | }
37 |
--------------------------------------------------------------------------------
/src/twofa/dto/twofa-code.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsNotEmpty } from 'class-validator';
2 |
3 | export class TwofaCodeDto {
4 | @IsNotEmpty()
5 | code: string;
6 | }
7 |
--------------------------------------------------------------------------------
/src/twofa/dto/twofa-status-update.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsBoolean } from 'class-validator';
2 |
3 | export class TwoFaStatusUpdateDto {
4 | @IsBoolean()
5 | isTwoFAEnabled: boolean;
6 | }
7 |
--------------------------------------------------------------------------------
/src/twofa/twofa.controller.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Controller,
4 | HttpCode,
5 | HttpStatus,
6 | Post,
7 | Put,
8 | Req,
9 | Res,
10 | UnauthorizedException,
11 | UseGuards
12 | } from '@nestjs/common';
13 | import { Request, Response } from 'express';
14 |
15 | import { AuthService } from 'src/auth/auth.service';
16 | import { UserEntity } from 'src/auth/entity/user.entity';
17 | import { GetUser } from 'src/common/decorators/get-user.decorator';
18 | import { JwtAuthGuard } from 'src/common/guard/jwt-auth.guard';
19 | import { TwofaCodeDto } from 'src/twofa/dto/twofa-code.dto';
20 | import { TwoFaStatusUpdateDto } from 'src/twofa/dto/twofa-status-update.dto';
21 | import { TwofaService } from 'src/twofa/twofa.service';
22 |
23 | @Controller('twofa')
24 | export class TwofaController {
25 | constructor(
26 | private readonly twofaService: TwofaService,
27 | private readonly usersService: AuthService
28 | ) {}
29 |
30 | @Post('authenticate')
31 | @HttpCode(200)
32 | @UseGuards(JwtAuthGuard)
33 | async authenticate(
34 | @Req()
35 | req: Request,
36 | @Res()
37 | response: Response,
38 | @GetUser()
39 | user: UserEntity,
40 | @Body()
41 | twofaCodeDto: TwofaCodeDto
42 | ) {
43 | const isCodeValid = this.twofaService.isTwoFACodeValid(
44 | twofaCodeDto.code,
45 | user
46 | );
47 | if (!isCodeValid) {
48 | throw new UnauthorizedException('invalidOTP');
49 | }
50 | const accessToken = await this.usersService.generateAccessToken(user, true);
51 | const cookiePayload = this.usersService.buildResponsePayload(accessToken);
52 | response.setHeader('Set-Cookie', cookiePayload);
53 | return response.status(HttpStatus.NO_CONTENT).json({});
54 | }
55 |
56 | @Put()
57 | @HttpCode(HttpStatus.NO_CONTENT)
58 | @UseGuards(JwtAuthGuard)
59 | async toggleTwoFa(
60 | @Body()
61 | twofaStatusUpdateDto: TwoFaStatusUpdateDto,
62 | @GetUser()
63 | user: UserEntity
64 | ) {
65 | let qrDataUri = null;
66 | if (twofaStatusUpdateDto.isTwoFAEnabled) {
67 | const { otpauthUrl } = await this.twofaService.generateTwoFASecret(user);
68 | qrDataUri = await this.twofaService.qrDataToUrl(otpauthUrl);
69 | }
70 | return this.usersService.turnOnTwoFactorAuthentication(
71 | user,
72 | twofaStatusUpdateDto.isTwoFAEnabled,
73 | qrDataUri
74 | );
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/twofa/twofa.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 |
3 | import { TwofaService } from 'src/twofa/twofa.service';
4 | import { AuthModule } from 'src/auth/auth.module';
5 | import { TwofaController } from 'src/twofa/twofa.controller';
6 |
7 | @Module({
8 | providers: [TwofaService],
9 | imports: [AuthModule],
10 | exports: [TwofaService],
11 | controllers: [TwofaController]
12 | })
13 | export class TwofaModule {}
14 |
--------------------------------------------------------------------------------
/src/twofa/twofa.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpStatus, Injectable } from '@nestjs/common';
2 | import * as config from 'config';
3 | import { Response } from 'express';
4 | import { authenticator } from 'otplib';
5 | import { toFileStream, toDataURL } from 'qrcode';
6 |
7 | import { StatusCodesList } from 'src/common/constants/status-codes-list.constants';
8 | import { AuthService } from 'src/auth/auth.service';
9 | import { UserEntity } from 'src/auth/entity/user.entity';
10 | import { CustomHttpException } from 'src/exception/custom-http.exception';
11 |
12 | const TwofaConfig = config.get('twofa');
13 |
14 | @Injectable()
15 | export class TwofaService {
16 | constructor(private readonly usersService: AuthService) {}
17 |
18 | async generateTwoFASecret(user: UserEntity) {
19 | if (user.twoFAThrottleTime > new Date()) {
20 | throw new CustomHttpException(
21 | `tooManyRequest-{"second":"${this.differentBetweenDatesInSec(
22 | user.twoFAThrottleTime,
23 | new Date()
24 | )}"}`,
25 | HttpStatus.TOO_MANY_REQUESTS,
26 | StatusCodesList.TooManyTries
27 | );
28 | }
29 | const secret = authenticator.generateSecret();
30 | const otpauthUrl = authenticator.keyuri(
31 | user.email,
32 | TwofaConfig.authenticationAppNAme,
33 | secret
34 | );
35 | await this.usersService.setTwoFactorAuthenticationSecret(secret, user.id);
36 | return {
37 | secret,
38 | otpauthUrl
39 | };
40 | }
41 |
42 | isTwoFACodeValid(twoFASecret: string, user: UserEntity) {
43 | return authenticator.verify({
44 | token: twoFASecret,
45 | secret: user.twoFASecret
46 | });
47 | }
48 |
49 | async pipeQrCodeStream(stream: Response, otpauthUrl: string) {
50 | return toFileStream(stream, otpauthUrl);
51 | }
52 |
53 | async qrDataToUrl(otpauthUrl: string): Promise {
54 | return toDataURL(otpauthUrl);
55 | }
56 |
57 | differentBetweenDatesInSec(initialDate: Date, endDate: Date): number {
58 | const diffInSeconds = Math.abs(initialDate.getTime() - endDate.getTime());
59 | return Math.round(diffInSeconds / 1000);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/test/e2e/app/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import * as request from 'supertest';
2 |
3 | import { AppFactory } from 'test/factories/app';
4 |
5 | describe('AppController (e2e)', () => {
6 | let app: AppFactory;
7 |
8 | beforeAll(async () => {
9 | await AppFactory.dropTables();
10 | app = await AppFactory.new();
11 | });
12 |
13 | beforeEach(async () => {
14 | await AppFactory.cleanupDB();
15 | });
16 |
17 | it('/ (GET)', () => {
18 | return request(app.instance.getHttpServer())
19 | .get('/')
20 | .expect(200)
21 | .expect({ message: 'hello world' });
22 | });
23 |
24 | afterAll(async () => {
25 | await app.close();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/test/e2e/auth/auth.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { HttpStatus } from '@nestjs/common';
2 | import * as request from 'supertest';
3 |
4 | import { AppFactory } from 'test/factories/app';
5 | import { RoleFactory } from 'test/factories/role.factory';
6 | import { UserFactory } from 'test/factories/user.factory';
7 | import { extractCookies } from 'test/utility/extract-cookie';
8 |
9 | describe('AuthController (e2e)', () => {
10 | let app: AppFactory;
11 |
12 | beforeAll(async () => {
13 | await AppFactory.dropTables();
14 | app = await AppFactory.new();
15 | });
16 |
17 | beforeEach(async () => {
18 | await AppFactory.cleanupDB();
19 | });
20 |
21 | it('POST /auth/login requires valid username and password', async () => {
22 | await request(app.instance.getHttpServer())
23 | .post(`/auth/login`)
24 | .send({
25 | username: 'email'
26 | })
27 | .expect(HttpStatus.UNPROCESSABLE_ENTITY);
28 | });
29 |
30 | it('POST /auth/login should throw unauthorized error if wrong username and password provided', async () => {
31 | await request(app.instance.getHttpServer())
32 | .post(`/auth/login`)
33 | .send({
34 | username: 'john@example.com',
35 | password: 'wrongPassword',
36 | remember: true
37 | })
38 | .expect(HttpStatus.UNAUTHORIZED);
39 | });
40 |
41 | it('POST /auth/login should login if provided with valid username and password', async () => {
42 | let cookie;
43 | const role = await RoleFactory.new().create();
44 | const user = await UserFactory.new()
45 | .withRole(role)
46 | .create({ password: 'password' });
47 |
48 | await request(app.instance.getHttpServer())
49 | .post(`/auth/login`)
50 | .send({
51 | username: user.email,
52 | password: 'password',
53 | remember: true
54 | })
55 | .expect(HttpStatus.NO_CONTENT)
56 | .then((res) => {
57 | cookie = extractCookies(res.headers);
58 | });
59 | expect(cookie).toBeDefined();
60 | expect(cookie).toHaveProperty('Authentication');
61 | expect(cookie).toHaveProperty('Refresh');
62 | expect(cookie).toHaveProperty('ExpiresIn');
63 | });
64 |
65 | afterAll(async () => {
66 | await app.close();
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/test/e2e/example.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { AppFactory } from 'test/factories/app';
2 |
3 | describe('Example Test (e2e)', () => {
4 | let app: AppFactory;
5 |
6 | beforeAll(async () => {
7 | await AppFactory.dropTables();
8 | app = await AppFactory.new();
9 | });
10 |
11 | beforeEach(async () => {
12 | await AppFactory.cleanupDB();
13 | });
14 |
15 | it('it passes', async () => {
16 | expect(true).toBe(true);
17 | });
18 |
19 | afterAll(async () => {
20 | await app.close();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/test/e2e/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 | "modulePaths": ["."]
10 | }
11 |
--------------------------------------------------------------------------------
/test/factories/app.ts:
--------------------------------------------------------------------------------
1 | import { ThrottlerModule } from '@nestjs/throttler';
2 | import { INestApplication } from '@nestjs/common';
3 | import { Test } from '@nestjs/testing';
4 | import { createConnection, getConnection } from 'typeorm';
5 | import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis';
6 | import { RateLimiterRedis } from 'rate-limiter-flexible';
7 | import * as Redis from 'ioredis';
8 | import * as config from 'config';
9 |
10 | import { AppModule } from 'src/app.module';
11 |
12 | const dbConfig = config.get('db');
13 |
14 | export class AppFactory {
15 | private constructor(
16 | private readonly appInstance: INestApplication,
17 | private readonly redis: Redis.Redis
18 | ) {}
19 |
20 | get instance() {
21 | return this.appInstance;
22 | }
23 |
24 | static async new() {
25 | const redis = await setupRedis();
26 | const moduleBuilder = Test.createTestingModule({
27 | imports: [
28 | AppModule,
29 | ThrottlerModule.forRootAsync({
30 | useFactory: () => {
31 | return {
32 | ttl: 60,
33 | limit: 60,
34 | storage: new ThrottlerStorageRedisService(redis)
35 | };
36 | }
37 | })
38 | ]
39 | })
40 | .overrideProvider('LOGIN_THROTTLE')
41 | .useFactory({
42 | factory: () => {
43 | return new RateLimiterRedis({
44 | storeClient: redis,
45 | keyPrefix: 'login',
46 | points: 5,
47 | duration: 60 * 60 * 24 * 30, // Store number for 30 days since first fail
48 | blockDuration: 3000
49 | });
50 | }
51 | });
52 |
53 | const module = await moduleBuilder.compile();
54 |
55 | const app = module.createNestApplication(undefined, {
56 | logger: false
57 | });
58 |
59 | await app.init();
60 |
61 | return new AppFactory(app, redis);
62 | }
63 |
64 | async close() {
65 | await getConnection().dropDatabase();
66 | if (this.redis) await this.teardown(this.redis);
67 | if (this.appInstance) await this.appInstance.close();
68 | }
69 |
70 | static async cleanupDB() {
71 | const connection = getConnection();
72 | const tables = connection.entityMetadatas.map(
73 | (entity) => `"${entity.tableName}"`
74 | );
75 |
76 | for (const table of tables) {
77 | await connection.query(`DELETE FROM ${table};`);
78 | }
79 | }
80 |
81 | static async dropTables() {
82 | const connection = await createConnection({
83 | type: dbConfig.type || 'postgres',
84 | host: process.env.DB_HOST || dbConfig.host,
85 | port: parseInt(process.env.DB_PORT) || dbConfig.port,
86 | database: process.env.DB_DATABASE_NAME || dbConfig.database,
87 | username: process.env.DB_USERNAME || dbConfig.username,
88 | password: process.env.DB_PASSWORD || dbConfig.password
89 | });
90 |
91 | await connection.query(`SET session_replication_role = 'replica';`);
92 | const tables = connection.entityMetadatas.map(
93 | (entity) => `"${entity.tableName}"`
94 | );
95 | for (const tableName of tables) {
96 | await connection.query(`DROP TABLE IF EXISTS ${tableName};`);
97 | }
98 |
99 | await connection.close();
100 | }
101 |
102 | async teardown(redis: Redis.Redis) {
103 | return new Promise((resolve) => {
104 | redis.quit();
105 | redis.on('end', () => {
106 | resolve();
107 | });
108 | });
109 | }
110 | }
111 |
112 | const setupRedis = async () => {
113 | const redis = new Redis({
114 | host: process.env.REDIS_HOST || 'localhost',
115 | port: parseInt(process.env.REDIS_PORT) || 6379
116 | });
117 | await redis.flushall();
118 | return redis;
119 | };
120 |
--------------------------------------------------------------------------------
/test/factories/role.factory.ts:
--------------------------------------------------------------------------------
1 | import { getRepository } from 'typeorm';
2 | import { faker } from '@faker-js/faker';
3 |
4 | import { RoleEntity } from 'src/role/entities/role.entity';
5 |
6 | export class RoleFactory {
7 | static new() {
8 | return new RoleFactory();
9 | }
10 |
11 | async create(role: Partial = {}) {
12 | const roleRepository = getRepository(RoleEntity);
13 | return roleRepository.save({
14 | name: faker.name.jobTitle(),
15 | description: faker.lorem.sentence(),
16 | ...role
17 | });
18 | }
19 |
20 | async createMany(roles: Partial[]) {
21 | return Promise.all([roles.map((role) => this.create(role))]);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/test/factories/throttle.ts:
--------------------------------------------------------------------------------
1 | import { Global, Module } from '@nestjs/common';
2 | import { ThrottlerModule } from '@nestjs/throttler';
3 |
4 | @Global()
5 | @Module({
6 | providers: [
7 | {
8 | provide: ThrottlerModule,
9 | useValue: {
10 | limit: 10,
11 | ttl: 1000,
12 | storage: jest.fn()
13 | } // mock
14 | }
15 | ]
16 | })
17 | export default class Throttle {}
18 |
--------------------------------------------------------------------------------
/test/factories/user.factory.ts:
--------------------------------------------------------------------------------
1 | import { getRepository } from 'typeorm';
2 | import { faker } from '@faker-js/faker';
3 | import * as bcrypt from 'bcrypt';
4 |
5 | import { UserEntity } from 'src/auth/entity/user.entity';
6 | import { UserStatusEnum } from 'src/auth/user-status.enum';
7 | import { RoleEntity } from 'src/role/entities/role.entity';
8 |
9 | export class UserFactory {
10 | private role: RoleEntity;
11 |
12 | static new() {
13 | return new UserFactory();
14 | }
15 |
16 | withRole(role: RoleEntity) {
17 | this.role = role;
18 | return this;
19 | }
20 |
21 | async create(user: Partial = {}) {
22 | const userRepository = getRepository(UserEntity);
23 | const salt = await bcrypt.genSalt();
24 | const password = await this.hashPassword(
25 | user.password || faker.internet.password(),
26 | salt
27 | );
28 | const payload = {
29 | username: faker.internet.userName().toLowerCase(),
30 | email: faker.internet.email().toLowerCase(),
31 | name: `${faker.name.firstName()} ${faker.name.lastName()}`,
32 | address: faker.address.streetAddress(),
33 | contact: faker.phone.phoneNumber(),
34 | avatar: faker.image.avatar(),
35 | salt,
36 | token: faker.datatype.uuid(),
37 | status: UserStatusEnum.ACTIVE,
38 | isTwoFAEnabled: false,
39 | ...user,
40 | password
41 | };
42 |
43 | if (this.role) payload.role = this.role;
44 | return userRepository.save(payload);
45 | }
46 |
47 | async createMany(users: Partial[]) {
48 | return Promise.all([users.map((user) => this.create(user))]);
49 | }
50 |
51 | private hashPassword(password, salt) {
52 | return bcrypt.hash(password, salt);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/test/unit/auth/entity/user.entity.unit-spec.ts:
--------------------------------------------------------------------------------
1 | import * as bcrypt from 'bcrypt';
2 |
3 | import { UserEntity } from 'src/auth/entity/user.entity';
4 |
5 | describe('test validate password', () => {
6 | let user: UserEntity;
7 | beforeEach(async () => {
8 | user = new UserEntity();
9 | user.password = 'testPassword';
10 | user.salt = 'testSalt';
11 | bcrypt.hash = jest.fn();
12 | });
13 | it('validate password if password matches', async () => {
14 | bcrypt.hash.mockResolvedValue('testPassword');
15 | const check = await user.validatePassword('123456');
16 | expect(bcrypt.hash).toHaveBeenCalledWith('123456', 'testSalt');
17 | expect(check).toBeTruthy();
18 | });
19 | it('validate password if password dont matches', async () => {
20 | bcrypt.hash.mockResolvedValue('anotherPassword');
21 | const check = await user.validatePassword('123456');
22 | expect(bcrypt.hash).toHaveBeenCalledWith('123456', 'testSalt');
23 | expect(check).toBeFalsy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/test/unit/auth/pipes/username-unique-validation.pipes.unit-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 |
3 | import { AuthService } from 'src/auth/auth.service';
4 | import { IsUsernameAlreadyExist } from 'src/auth/pipes/username-unique-validation.pipes';
5 |
6 | const mockAuthService = () => ({
7 | findBy: jest.fn()
8 | });
9 | describe('IsUsernameAlreadyExist', () => {
10 | let authService: AuthService, isUsernameAlreadyExist: IsUsernameAlreadyExist;
11 | beforeEach(async () => {
12 | const module = await Test.createTestingModule({
13 | providers: [
14 | IsUsernameAlreadyExist,
15 | {
16 | provide: AuthService,
17 | useFactory: mockAuthService
18 | }
19 | ]
20 | }).compile();
21 | isUsernameAlreadyExist = await module.get(
22 | IsUsernameAlreadyExist
23 | );
24 | authService = await module.get(AuthService);
25 | });
26 |
27 | describe('username unique validation', () => {
28 | it('check for same username', async () => {
29 | expect(authService.findBy).not.toHaveBeenCalled();
30 | const result = await isUsernameAlreadyExist.validate('tester');
31 | expect(authService.findBy).toHaveBeenCalledWith('username', 'tester');
32 | expect(result).toBe(true);
33 | });
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/test/unit/auth/user.repository.unit-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 |
3 | import { UserRepository } from 'src/auth/user.repository';
4 | import { CreateUserDto } from 'src/auth/dto/create-user.dto';
5 | import { UserLoginDto } from 'src/auth/dto/user-login.dto';
6 | import { UserEntity } from 'src/auth/entity/user.entity';
7 | import { UserStatusEnum } from 'src/auth/user-status.enum';
8 | import { StatusCodesList } from 'src/common/constants/status-codes-list.constants';
9 | import { ExceptionTitleList } from 'src/common/constants/exception-title-list.constants';
10 |
11 | const mockUser = {
12 | roleId: 1,
13 | email: 'test@email.com',
14 | username: 'tester',
15 | name: 'test',
16 | status: UserStatusEnum.ACTIVE,
17 | password: 'pwd'
18 | };
19 | describe('User Repository', () => {
20 | let userRepository;
21 | beforeEach(async () => {
22 | const module = await Test.createTestingModule({
23 | providers: [UserRepository]
24 | }).compile();
25 | userRepository = await module.get(UserRepository);
26 | });
27 |
28 | describe('store', () => {
29 | let save;
30 | beforeEach(async () => {
31 | save = jest.fn().mockResolvedValue(undefined);
32 | userRepository.create = jest.fn().mockReturnValue({ save });
33 | });
34 | it('store new user', async () => {
35 | const createUserDto: CreateUserDto = {
36 | ...mockUser
37 | };
38 | await expect(userRepository.store(createUserDto)).resolves.not.toThrow();
39 | });
40 | });
41 |
42 | describe('login', () => {
43 | let user, userLoginDto: UserLoginDto;
44 | beforeEach(async () => {
45 | userRepository.findOne = jest.fn();
46 | user = new UserEntity();
47 | user.status = UserStatusEnum.ACTIVE;
48 | user.username = mockUser.username;
49 | user.password = mockUser.password;
50 | user.validatePassword = jest.fn();
51 | userLoginDto = {
52 | ...mockUser,
53 | remember: true
54 | };
55 | });
56 | it('check if username and password matches and return user', async () => {
57 | userRepository.findOne.mockResolvedValue(user);
58 | user.validatePassword.mockResolvedValue(true);
59 | const result = await userRepository.login(userLoginDto);
60 | expect(userRepository.findOne).toHaveBeenCalledWith({
61 | where: [
62 | {
63 | username: userLoginDto.username
64 | },
65 | {
66 | email: userLoginDto.username
67 | }
68 | ]
69 | });
70 | expect(result).toEqual([user, null, null]);
71 | });
72 |
73 | it('throw error if username and password does not matches', async () => {
74 | userRepository.findOne.mockResolvedValue(user);
75 | user.validatePassword.mockResolvedValue(false);
76 | const result = await userRepository.login(userLoginDto);
77 | expect(result).toEqual([
78 | null,
79 | ExceptionTitleList.InvalidCredentials,
80 | StatusCodesList.InvalidCredentials
81 | ]);
82 | });
83 |
84 | it('check if user is null', async () => {
85 | userRepository.findOne.mockResolvedValue(null);
86 | const result = await userRepository.login(userLoginDto);
87 | expect(result).toEqual([
88 | null,
89 | ExceptionTitleList.InvalidCredentials,
90 | StatusCodesList.InvalidCredentials
91 | ]);
92 | });
93 | });
94 | });
95 |
--------------------------------------------------------------------------------
/test/unit/common/guard/permission.guard.unit-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 |
3 | import { PermissionGuard } from 'src/common/guard/permission.guard';
4 | import { UserEntity } from 'src/auth/entity/user.entity';
5 | import { RoleEntity } from 'src/role/entities/role.entity';
6 | import { PermissionEntity } from 'src/permission/entities/permission.entity';
7 |
8 | describe('Permission guard test', () => {
9 | let permissionGuard;
10 | beforeEach(async () => {
11 | const module: TestingModule = await Test.createTestingModule({
12 | providers: [PermissionGuard]
13 | }).compile();
14 | permissionGuard = module.get(PermissionGuard);
15 | jest.clearAllMocks();
16 | });
17 |
18 | describe('check if user have necessary permission', () => {
19 | let context, mockPermission, mockRole, mockUser, mockSwitchToHttp;
20 | beforeEach(() => {
21 | context = {
22 | getArgByIndex: jest.fn(),
23 | getArgs: jest.fn(),
24 | getClass: jest.fn(),
25 | getHandler: jest.fn(),
26 | getType: jest.fn(),
27 | switchToRpc: jest.fn(),
28 | switchToWs: jest.fn(),
29 | switchToHttp: jest.fn()
30 | };
31 |
32 | mockPermission = new PermissionEntity();
33 | mockPermission.method = 'get';
34 | mockPermission.path = '/roles/:id';
35 |
36 | mockRole = new RoleEntity();
37 | mockRole.name = 'tester';
38 | mockRole.permission = [mockPermission];
39 |
40 | mockUser = new UserEntity();
41 | mockUser.name = 'truthy';
42 | mockUser.username = 'truthy';
43 | mockUser.role = mockRole;
44 |
45 | mockSwitchToHttp = {
46 | getRequest: jest.fn(),
47 | getNext: jest.fn(),
48 | getResponse: jest.fn()
49 | };
50 | });
51 | it('check if permission is granted if user have valid permission', async () => {
52 | const requestMockData = {
53 | route: {
54 | path: '/roles/:id'
55 | },
56 | method: 'GET',
57 | user: mockUser
58 | };
59 | mockSwitchToHttp.getRequest.mockReturnValue(requestMockData);
60 | context.switchToHttp.mockReturnValue(mockSwitchToHttp);
61 | permissionGuard.checkIfDefaultRoute = jest.fn();
62 | const result = await permissionGuard.canActivate(context);
63 | expect(permissionGuard.checkIfDefaultRoute).toHaveBeenCalledTimes(1);
64 | expect(result).toBeTruthy();
65 | });
66 |
67 | it("check if permission is granted if user don't have valid permission", async () => {
68 | const requestMockData = {
69 | route: {
70 | path: '/permission/:id'
71 | },
72 | method: 'GET',
73 | user: mockUser
74 | };
75 | mockSwitchToHttp.getRequest.mockReturnValue(requestMockData);
76 | context.switchToHttp.mockReturnValue(mockSwitchToHttp);
77 | const result = await permissionGuard.canActivate(context);
78 | expect(result).toBeFalsy();
79 | });
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/test/unit/common/pipes/unique-validator.pipe.unit-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { getConnectionToken } from '@nestjs/typeorm';
3 | import { Connection } from 'typeorm';
4 |
5 | import { UniqueValidatorPipe } from 'src/common/pipes/unique-validator.pipe';
6 | import { UserEntity } from 'src/auth/entity/user.entity';
7 | import { UniqueValidationArguments } from 'src/common/pipes/abstract-unique-validator';
8 |
9 | const mockConnection = () => ({
10 | getRepository: jest.fn(() => ({
11 | count: jest.fn().mockResolvedValue(0)
12 | }))
13 | });
14 |
15 | describe('UniqueValidatorPipe', () => {
16 | let isUnique: UniqueValidatorPipe, connection;
17 | beforeEach(async () => {
18 | const module = await Test.createTestingModule({
19 | providers: [
20 | UniqueValidatorPipe,
21 | {
22 | provide: getConnectionToken(),
23 | useFactory: mockConnection
24 | }
25 | ]
26 | }).compile();
27 | isUnique = await module.get(UniqueValidatorPipe);
28 | connection = await module.get(Connection);
29 | });
30 |
31 | describe('check unique validation', () => {
32 | it('check if there is no duplicate', async () => {
33 | const username = 'tester';
34 | const args: UniqueValidationArguments = {
35 | constraints: [UserEntity, ({ object: {} }) => ({})],
36 | value: username,
37 | targetName: '',
38 | object: {
39 | username
40 | },
41 | property: 'username'
42 | };
43 | const result = await isUnique.validate('username', args);
44 | expect(connection.getRepository).toHaveBeenCalledWith(UserEntity);
45 | expect(result).toBe(true);
46 | });
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/test/unit/common/strategy/jwt.strategy.unit-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 |
3 | import { JwtStrategy } from 'src/common/strategy/jwt.strategy';
4 | import { UserRepository } from 'src/auth/user.repository';
5 | import { UserEntity } from 'src/auth/entity/user.entity';
6 | import { JwtPayloadDto } from 'src/auth/dto/jwt-payload.dto';
7 | import { UnauthorizedException } from 'src/exception/unauthorized.exception';
8 |
9 | const mockUserRepository = () => ({
10 | findOne: jest.fn()
11 | });
12 |
13 | describe('Test JWT strategy', () => {
14 | let userRepository, jwtStrategy: JwtStrategy;
15 | beforeEach(async () => {
16 | jest.mock('config', () => ({
17 | default: {
18 | get: () => jest.fn().mockImplementation(() => 'hello')
19 | }
20 | }));
21 | const module = await Test.createTestingModule({
22 | providers: [
23 | JwtStrategy,
24 | {
25 | provide: UserRepository,
26 | useFactory: mockUserRepository
27 | }
28 | ]
29 | }).compile();
30 | jwtStrategy = await module.get(JwtStrategy);
31 | userRepository = await module.get(UserRepository);
32 | });
33 |
34 | describe('validate user', () => {
35 | it('should return user if username is found on database', async () => {
36 | const user = new UserEntity();
37 | user.name = 'test';
38 | user.username = 'tester';
39 | const payload: JwtPayloadDto = {
40 | subject: '1'
41 | };
42 | userRepository.findOne.mockResolvedValue(user);
43 | const result = await jwtStrategy.validate(payload);
44 | expect(userRepository.findOne).toHaveBeenCalledWith(
45 | Number(payload.subject),
46 | {
47 | relations: ['role', 'role.permission']
48 | }
49 | );
50 | expect(result).toEqual(user);
51 | });
52 |
53 | it('should throw error if subject is not found on database', async () => {
54 | const payload: JwtPayloadDto = {
55 | subject: '1'
56 | };
57 | userRepository.findOne.mockResolvedValue(null);
58 | await expect(jwtStrategy.validate(payload)).rejects.toThrow(
59 | UnauthorizedException
60 | );
61 | });
62 | });
63 | });
64 |
--------------------------------------------------------------------------------
/test/unit/dashboard/dashboard.service.unit-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AuthService } from 'src/auth/auth.service';
3 | import { DashboardService } from 'src/dashboard/dashboard.service';
4 |
5 | const authServiceMock = () => ({
6 | countByCondition: jest.fn(),
7 | getRefreshTokenGroupedData: jest.fn()
8 | });
9 |
10 | describe('DashboardService', () => {
11 | let service: DashboardService, authService;
12 |
13 | beforeEach(async () => {
14 | const module: TestingModule = await Test.createTestingModule({
15 | providers: [
16 | DashboardService,
17 | {
18 | provide: AuthService,
19 | useFactory: authServiceMock
20 | }
21 | ]
22 | }).compile();
23 |
24 | service = module.get(DashboardService);
25 | authService = module.get(AuthService);
26 | });
27 |
28 | it('should get User Stat', () => {
29 | const result = service.getUserStat();
30 | expect(authService.countByCondition).toHaveBeenCalledTimes(3);
31 | expect(result).resolves.not.toThrow();
32 | });
33 |
34 | it('should get browser Stat', () => {
35 | service.getBrowserData();
36 | expect(authService.getRefreshTokenGroupedData).toHaveBeenCalledTimes(1);
37 | expect(authService.getRefreshTokenGroupedData).toHaveBeenCalledWith(
38 | 'browser'
39 | );
40 | });
41 |
42 | it('should get os Stat', () => {
43 | service.getOsData();
44 | expect(authService.getRefreshTokenGroupedData).toHaveBeenCalledTimes(1);
45 | expect(authService.getRefreshTokenGroupedData).toHaveBeenCalledWith('os');
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/test/unit/jest-unit.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": "../../",
4 | "testEnvironment": "node",
5 | "testRegex": ".unit-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | },
9 | "modulePaths": ["."]
10 | }
11 |
--------------------------------------------------------------------------------
/test/unit/refresh-token/refresh-token.repository.unit-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 |
3 | import { RefreshTokenRepository } from 'src/refresh-token/refresh-token.repository';
4 | import { UserSerializer } from 'src/auth/serializer/user.serializer';
5 |
6 | const mockRefreshToken = {
7 | id: 1,
8 | userId: 1,
9 | expires: new Date(),
10 | isRevoked: false,
11 | save: jest.fn()
12 | };
13 |
14 | describe('Refresh token repository', () => {
15 | let repository, user;
16 | beforeEach(async () => {
17 | const module = await Test.createTestingModule({
18 | providers: [RefreshTokenRepository]
19 | }).compile();
20 | repository = await module.get(
21 | RefreshTokenRepository
22 | );
23 | user = new UserSerializer();
24 | user.id = 1;
25 | user.email = 'test@mail.com';
26 | });
27 |
28 | it('create new refresh token', async () => {
29 | jest.spyOn(repository, 'create').mockReturnValue(mockRefreshToken);
30 | await repository.createRefreshToken(user, 60 * 60);
31 | expect(repository.create).toHaveBeenCalledTimes(1);
32 | expect(repository.create().save).toHaveBeenCalledTimes(1);
33 | });
34 |
35 | it('findTokenById', async () => {
36 | jest.spyOn(repository, 'findOne').mockReturnValue(mockRefreshToken);
37 | await repository.findTokenById(1);
38 | expect(repository.findOne).toHaveBeenCalledTimes(1);
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/test/unit/twofa/twofa.service.unit-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { authenticator } from 'otplib';
3 |
4 | import { TwofaService } from 'src/twofa/twofa.service';
5 | import { AuthService } from 'src/auth/auth.service';
6 | import { UserEntity } from 'src/auth/entity/user.entity';
7 | import { CustomHttpException } from 'src/exception/custom-http.exception';
8 |
9 | const authServiceMock = () => ({
10 | setTwoFactorAuthenticationSecret: jest.fn()
11 | });
12 |
13 | describe('TwofaService', () => {
14 | let service: TwofaService, authService, user: UserEntity;
15 |
16 | beforeEach(async () => {
17 | const module: TestingModule = await Test.createTestingModule({
18 | providers: [
19 | TwofaService,
20 | {
21 | provide: AuthService,
22 | useFactory: authServiceMock
23 | }
24 | ]
25 | }).compile();
26 |
27 | service = module.get(TwofaService);
28 | authService = await module.get(AuthService);
29 | user = new UserEntity();
30 | user.email = 'test@mail.com';
31 | user.username = 'tester';
32 | user.password = 'pwd';
33 | user.salt = 'result';
34 | });
35 |
36 | afterEach(() => {
37 | jest.clearAllMocks();
38 | });
39 |
40 | describe('#generateSecret', () => {
41 | it('should throw error if user tries to spam generate OTP', async () => {
42 | const twoFAThrottleTime = new Date();
43 | twoFAThrottleTime.setSeconds(twoFAThrottleTime.getSeconds() + 60);
44 | user.twoFAThrottleTime = twoFAThrottleTime;
45 | await expect(service.generateTwoFASecret(user)).rejects.toThrowError(
46 | CustomHttpException
47 | );
48 | });
49 | it('should generate 2fa secret', async () => {
50 | jest.spyOn(authenticator, 'generateSecret').mockReturnValue('result');
51 | jest.spyOn(authenticator, 'keyuri').mockReturnValue('result');
52 | await service.generateTwoFASecret(user);
53 | expect(authenticator.generateSecret).toHaveBeenCalledTimes(1);
54 | expect(authenticator.keyuri).toHaveBeenCalledTimes(1);
55 | expect(
56 | authService.setTwoFactorAuthenticationSecret
57 | ).toHaveBeenCalledTimes(1);
58 | });
59 | });
60 |
61 | it('isTwoFACodeValid', async () => {
62 | jest.spyOn(authenticator, 'verify').mockReturnValue(true);
63 | service.isTwoFACodeValid('secret', user);
64 | expect(authenticator.verify).toHaveBeenCalledTimes(1);
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/test/utility/create-mock.ts:
--------------------------------------------------------------------------------
1 | import { DynamicModule, Provider } from '@nestjs/common';
2 |
3 | export const createMockModule = (providers: Provider[]): DynamicModule => {
4 | const exports = providers.map(
5 | (provider) => (provider as any).provide || provider
6 | );
7 | return {
8 | module: class MockModule {},
9 | providers,
10 | exports,
11 | global: true
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/test/utility/extract-cookie.ts:
--------------------------------------------------------------------------------
1 | const shapeFlags = (flags) =>
2 | flags.reduce((shapedFlags, flag) => {
3 | const [flagName, rawValue] = flag.split('=');
4 | const value = rawValue ? rawValue.replace(';', '') : true;
5 | return { ...shapedFlags, [flagName]: value };
6 | }, {});
7 |
8 | export const extractCookies = (headers) => {
9 | const cookies = headers['set-cookie'];
10 |
11 | return cookies.reduce((shapedCookies, cookieString) => {
12 | const [rawCookie, ...flags] = cookieString.split('; ');
13 | const [cookieName, value] = rawCookie.split('=');
14 | return {
15 | ...shapedCookies,
16 | [cookieName]: { value, flags: shapeFlags(flags) }
17 | };
18 | }, {});
19 | };
20 |
--------------------------------------------------------------------------------
/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 | "target": "es2017",
9 | "sourceMap": true,
10 | "outDir": "./dist",
11 | "baseUrl": "./",
12 | "incremental": true
13 | },
14 | "exclude": ["node_modules", "dist"]
15 | }
16 |
--------------------------------------------------------------------------------