Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f78691f91 | |||
| aef712ca1d | |||
| 77783549fa | |||
| be6e4a8d32 | |||
| af606ce9a7 | |||
| 7996617cf0 | |||
| 9e28dfd628 | |||
| c20e0ba684 | |||
| ced46e500d | |||
| c925d2e539 | |||
| 10035f3f25 | |||
| 19c396e9d1 | |||
| a13097bbd6 | |||
| 8aaac51b12 | |||
| b855d81817 |
@@ -33,7 +33,7 @@ SMS_PATTERN_NEW_TICKET_GLOBAL=536596
|
||||
SMS_PATTERN_NEW_CRITICISM=983724
|
||||
SMS_PATTERN_USER_PASSWORD=486237
|
||||
SMTP_HOST=smtps.danakcorp.com
|
||||
SMTP_PORT=465
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=no-reply-danakcorp.com
|
||||
SMTP_PASS=Mahyar.SR4@
|
||||
MAIL_FROM=no-reply <no-reply@danakcorp.com>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddTicketToTaskEntity1784600000000 implements MigrationInterface {
|
||||
name = "AddTicketToTaskEntity1784600000000";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "tm_tasks" ADD "ticketId" uuid`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "tm_tasks" ADD CONSTRAINT "FK_tm_tasks_ticket" FOREIGN KEY ("ticketId") REFERENCES "ticket"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "tm_tasks" DROP CONSTRAINT "FK_tm_tasks_ticket"`);
|
||||
await queryRunner.query(`ALTER TABLE "tm_tasks" DROP COLUMN "ticketId"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddTaskCommentEntity1784700000000 implements MigrationInterface {
|
||||
name = "AddTaskCommentEntity1784700000000";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "tm_task_comments" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "content" text NOT NULL, "userId" uuid NOT NULL, "taskId" uuid NOT NULL, CONSTRAINT "PK_tm_task_comments" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "tm_task_comments" ADD CONSTRAINT "FK_tm_task_comments_user" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "tm_task_comments" ADD CONSTRAINT "FK_tm_task_comments_task" FOREIGN KEY ("taskId") REFERENCES "tm_tasks"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "tm_task_comments" DROP CONSTRAINT "FK_tm_task_comments_task"`);
|
||||
await queryRunner.query(`ALTER TABLE "tm_task_comments" DROP CONSTRAINT "FK_tm_task_comments_user"`);
|
||||
await queryRunner.query(`DROP TABLE "tm_task_comments"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
const NEW_PERMISSIONS = [
|
||||
{ name: "ticket_categories_create", title: "ایجاد دستهبندی تیکت", source: "tickets_create" },
|
||||
{ name: "ticket_categories_read", title: "مشاهده دستهبندی تیکت", source: "tickets_read" },
|
||||
{ name: "ticket_categories_update", title: "ویرایش دستهبندی تیکت", source: "tickets_update" },
|
||||
{ name: "ticket_categories_delete", title: "حذف دستهبندی تیکت", source: "tickets_delete" },
|
||||
] as const;
|
||||
|
||||
const CURRENT_ENUM_VALUES = [
|
||||
"services_create",
|
||||
"services_read",
|
||||
"services_update",
|
||||
"services_delete",
|
||||
"customers_create",
|
||||
"customers_read",
|
||||
"customers_update",
|
||||
"customers_delete",
|
||||
"agents_create",
|
||||
"agents_read",
|
||||
"agents_update",
|
||||
"agents_delete",
|
||||
"developers_create",
|
||||
"developers_read",
|
||||
"developers_update",
|
||||
"developers_delete",
|
||||
"invoices_create",
|
||||
"invoices_read",
|
||||
"invoices_update",
|
||||
"invoices_delete",
|
||||
"transactions_create",
|
||||
"transactions_read",
|
||||
"transactions_update",
|
||||
"transactions_delete",
|
||||
"discounts_create",
|
||||
"discounts_read",
|
||||
"discounts_update",
|
||||
"discounts_delete",
|
||||
"admins_create",
|
||||
"admins_read",
|
||||
"admins_update",
|
||||
"admins_delete",
|
||||
"tickets_create",
|
||||
"tickets_read",
|
||||
"tickets_update",
|
||||
"tickets_delete",
|
||||
"criticisms_create",
|
||||
"criticisms_read",
|
||||
"criticisms_update",
|
||||
"criticisms_delete",
|
||||
"contacts_us_create",
|
||||
"contacts_us_read",
|
||||
"contacts_us_update",
|
||||
"contacts_us_delete",
|
||||
"advertisements_create",
|
||||
"advertisements_read",
|
||||
"advertisements_update",
|
||||
"advertisements_delete",
|
||||
"announcements_create",
|
||||
"announcements_read",
|
||||
"announcements_update",
|
||||
"announcements_delete",
|
||||
"blogs_create",
|
||||
"blogs_read",
|
||||
"blogs_update",
|
||||
"blogs_delete",
|
||||
"learnings_create",
|
||||
"learnings_read",
|
||||
"learnings_update",
|
||||
"learnings_delete",
|
||||
"settings_create",
|
||||
"settings_read",
|
||||
"settings_update",
|
||||
"settings_delete",
|
||||
"bank_accounts_create",
|
||||
"bank_accounts_read",
|
||||
"bank_accounts_update",
|
||||
"bank_accounts_delete",
|
||||
"payments_create",
|
||||
"payments_read",
|
||||
"payments_update",
|
||||
"payments_delete",
|
||||
"support_plan_create",
|
||||
"support_plan_read",
|
||||
"support_plan_update",
|
||||
"support_plan_delete",
|
||||
"reseller_create",
|
||||
"reseller_read",
|
||||
"reseller_update",
|
||||
"reseller_delete",
|
||||
"workspace_create",
|
||||
"workspace_read",
|
||||
"workspace_update",
|
||||
"workspace_delete",
|
||||
"project_create",
|
||||
"project_read",
|
||||
"project_update",
|
||||
"project_delete",
|
||||
"task_create",
|
||||
"task_read",
|
||||
"task_update",
|
||||
"task_delete",
|
||||
"task_phase_create",
|
||||
"task_phase_read",
|
||||
"task_phase_update",
|
||||
"task_phase_delete",
|
||||
"logs",
|
||||
"manage_sso_clients",
|
||||
"dmenu",
|
||||
"dkala",
|
||||
"dpage",
|
||||
"dmail",
|
||||
"view_all_workspaces",
|
||||
"view_all_projects",
|
||||
];
|
||||
|
||||
const UPDATED_ENUM_VALUES = [
|
||||
...CURRENT_ENUM_VALUES.slice(0, CURRENT_ENUM_VALUES.indexOf("tickets_delete") + 1),
|
||||
...NEW_PERMISSIONS.map((p) => p.name),
|
||||
...CURRENT_ENUM_VALUES.slice(CURRENT_ENUM_VALUES.indexOf("tickets_delete") + 1),
|
||||
];
|
||||
|
||||
export class AddTicketCategoriesPermissions1784800000000 implements MigrationInterface {
|
||||
name = "AddTicketCategoriesPermissions1784800000000";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE text USING "name"::text`);
|
||||
await queryRunner.query(`DROP TYPE "public"."permission_name_enum"`);
|
||||
|
||||
const enumList = UPDATED_ENUM_VALUES.map((v) => `'${v}'`).join(", ");
|
||||
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM(${enumList})`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum" USING "name"::"public"."permission_name_enum"`,
|
||||
);
|
||||
|
||||
for (const perm of NEW_PERMISSIONS) {
|
||||
const inserted = await queryRunner.query(
|
||||
`INSERT INTO "permission" ("id", "name", "title", "group", "createdAt", "updatedAt")
|
||||
VALUES (uuid_generate_v4(), $1, $2, 'support', NOW(), NOW())
|
||||
RETURNING id`,
|
||||
[perm.name, perm.title],
|
||||
);
|
||||
const newId = inserted[0].id as string;
|
||||
|
||||
const sourcePerm = await queryRunner.query(`SELECT id FROM "permission" WHERE "name" = $1`, [perm.source]);
|
||||
if (!sourcePerm.length) continue;
|
||||
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "role_permission_relation" ("roleId", "permissionId")
|
||||
SELECT "roleId", $1 FROM "role_permission_relation" WHERE "permissionId" = $2
|
||||
ON CONFLICT ("roleId", "permissionId") DO NOTHING`,
|
||||
[newId, sourcePerm[0].id],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
for (const perm of NEW_PERMISSIONS) {
|
||||
const rows = await queryRunner.query(`SELECT id FROM "permission" WHERE "name" = $1`, [perm.name]);
|
||||
if (!rows.length) continue;
|
||||
|
||||
await queryRunner.query(`DELETE FROM "role_permission_relation" WHERE "permissionId" = $1`, [rows[0].id]);
|
||||
await queryRunner.query(`DELETE FROM "permission" WHERE "id" = $1`, [rows[0].id]);
|
||||
}
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE text USING "name"::text`);
|
||||
await queryRunner.query(`DROP TYPE "public"."permission_name_enum"`);
|
||||
|
||||
const enumList = CURRENT_ENUM_VALUES.map((v) => `'${v}'`).join(", ");
|
||||
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM(${enumList})`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum" USING "name"::"public"."permission_name_enum"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddAttachmentsToTaskComment1784900000000 implements MigrationInterface {
|
||||
name = "AddAttachmentsToTaskComment1784900000000";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "tm_task_comments" ADD "attachments" json NOT NULL DEFAULT '[]'`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "tm_task_comments" DROP COLUMN "attachments"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddTicketMasterEntity1785000000000 implements MigrationInterface {
|
||||
name = "AddTicketMasterEntity1785000000000";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "ticket_master" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "userId" uuid NOT NULL, CONSTRAINT "UQ_ticket_master_user" UNIQUE ("userId"), CONSTRAINT "PK_ticket_master" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "ticket_master" ADD CONSTRAINT "FK_ticket_master_user" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "ticket_master" DROP CONSTRAINT "FK_ticket_master_user"`);
|
||||
await queryRunner.query(`DROP TABLE "ticket_master"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
const NEW_PERMISSIONS = [
|
||||
{ name: "ticket_masters_create", title: "ایجاد مستر تیکت", source: "tickets_create" },
|
||||
{ name: "ticket_masters_read", title: "مشاهده مستر تیکت", source: "tickets_read" },
|
||||
{ name: "ticket_masters_update", title: "ویرایش مستر تیکت", source: "tickets_update" },
|
||||
{ name: "ticket_masters_delete", title: "حذف مستر تیکت", source: "tickets_delete" },
|
||||
] as const;
|
||||
|
||||
const CURRENT_ENUM_VALUES = [
|
||||
"services_create",
|
||||
"services_read",
|
||||
"services_update",
|
||||
"services_delete",
|
||||
"customers_create",
|
||||
"customers_read",
|
||||
"customers_update",
|
||||
"customers_delete",
|
||||
"agents_create",
|
||||
"agents_read",
|
||||
"agents_update",
|
||||
"agents_delete",
|
||||
"developers_create",
|
||||
"developers_read",
|
||||
"developers_update",
|
||||
"developers_delete",
|
||||
"invoices_create",
|
||||
"invoices_read",
|
||||
"invoices_update",
|
||||
"invoices_delete",
|
||||
"transactions_create",
|
||||
"transactions_read",
|
||||
"transactions_update",
|
||||
"transactions_delete",
|
||||
"discounts_create",
|
||||
"discounts_read",
|
||||
"discounts_update",
|
||||
"discounts_delete",
|
||||
"admins_create",
|
||||
"admins_read",
|
||||
"admins_update",
|
||||
"admins_delete",
|
||||
"tickets_create",
|
||||
"tickets_read",
|
||||
"tickets_update",
|
||||
"tickets_delete",
|
||||
"ticket_categories_create",
|
||||
"ticket_categories_read",
|
||||
"ticket_categories_update",
|
||||
"ticket_categories_delete",
|
||||
"criticisms_create",
|
||||
"criticisms_read",
|
||||
"criticisms_update",
|
||||
"criticisms_delete",
|
||||
"contacts_us_create",
|
||||
"contacts_us_read",
|
||||
"contacts_us_update",
|
||||
"contacts_us_delete",
|
||||
"advertisements_create",
|
||||
"advertisements_read",
|
||||
"advertisements_update",
|
||||
"advertisements_delete",
|
||||
"announcements_create",
|
||||
"announcements_read",
|
||||
"announcements_update",
|
||||
"announcements_delete",
|
||||
"blogs_create",
|
||||
"blogs_read",
|
||||
"blogs_update",
|
||||
"blogs_delete",
|
||||
"learnings_create",
|
||||
"learnings_read",
|
||||
"learnings_update",
|
||||
"learnings_delete",
|
||||
"settings_create",
|
||||
"settings_read",
|
||||
"settings_update",
|
||||
"settings_delete",
|
||||
"bank_accounts_create",
|
||||
"bank_accounts_read",
|
||||
"bank_accounts_update",
|
||||
"bank_accounts_delete",
|
||||
"payments_create",
|
||||
"payments_read",
|
||||
"payments_update",
|
||||
"payments_delete",
|
||||
"support_plan_create",
|
||||
"support_plan_read",
|
||||
"support_plan_update",
|
||||
"support_plan_delete",
|
||||
"reseller_create",
|
||||
"reseller_read",
|
||||
"reseller_update",
|
||||
"reseller_delete",
|
||||
"workspace_create",
|
||||
"workspace_read",
|
||||
"workspace_update",
|
||||
"workspace_delete",
|
||||
"project_create",
|
||||
"project_read",
|
||||
"project_update",
|
||||
"project_delete",
|
||||
"task_create",
|
||||
"task_read",
|
||||
"task_update",
|
||||
"task_delete",
|
||||
"task_phase_create",
|
||||
"task_phase_read",
|
||||
"task_phase_update",
|
||||
"task_phase_delete",
|
||||
"logs",
|
||||
"manage_sso_clients",
|
||||
"dmenu",
|
||||
"dkala",
|
||||
"dpage",
|
||||
"dmail",
|
||||
"view_all_workspaces",
|
||||
"view_all_projects",
|
||||
];
|
||||
|
||||
const UPDATED_ENUM_VALUES = [
|
||||
...CURRENT_ENUM_VALUES.slice(0, CURRENT_ENUM_VALUES.indexOf("ticket_categories_delete") + 1),
|
||||
...NEW_PERMISSIONS.map((p) => p.name),
|
||||
...CURRENT_ENUM_VALUES.slice(CURRENT_ENUM_VALUES.indexOf("ticket_categories_delete") + 1),
|
||||
];
|
||||
|
||||
export class AddTicketMastersPermissions1785100000000 implements MigrationInterface {
|
||||
name = "AddTicketMastersPermissions1785100000000";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE text USING "name"::text`);
|
||||
await queryRunner.query(`DROP TYPE "public"."permission_name_enum"`);
|
||||
|
||||
const enumList = UPDATED_ENUM_VALUES.map((v) => `'${v}'`).join(", ");
|
||||
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM(${enumList})`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum" USING "name"::"public"."permission_name_enum"`,
|
||||
);
|
||||
|
||||
for (const perm of NEW_PERMISSIONS) {
|
||||
const inserted = await queryRunner.query(
|
||||
`INSERT INTO "permission" ("id", "name", "title", "group", "createdAt", "updatedAt")
|
||||
VALUES (uuid_generate_v4(), $1, $2, 'support', NOW(), NOW())
|
||||
RETURNING id`,
|
||||
[perm.name, perm.title],
|
||||
);
|
||||
const newId = inserted[0].id as string;
|
||||
|
||||
const sourcePerm = await queryRunner.query(`SELECT id FROM "permission" WHERE "name" = $1`, [perm.source]);
|
||||
if (!sourcePerm.length) continue;
|
||||
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "role_permission_relation" ("roleId", "permissionId")
|
||||
SELECT "roleId", $1 FROM "role_permission_relation" WHERE "permissionId" = $2
|
||||
ON CONFLICT ("roleId", "permissionId") DO NOTHING`,
|
||||
[newId, sourcePerm[0].id],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
for (const perm of NEW_PERMISSIONS) {
|
||||
const rows = await queryRunner.query(`SELECT id FROM "permission" WHERE "name" = $1`, [perm.name]);
|
||||
if (!rows.length) continue;
|
||||
|
||||
await queryRunner.query(`DELETE FROM "role_permission_relation" WHERE "permissionId" = $1`, [rows[0].id]);
|
||||
await queryRunner.query(`DELETE FROM "permission" WHERE "id" = $1`, [rows[0].id]);
|
||||
}
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE text USING "name"::text`);
|
||||
await queryRunner.query(`DROP TYPE "public"."permission_name_enum"`);
|
||||
|
||||
const enumList = CURRENT_ENUM_VALUES.map((v) => `'${v}'`).join(", ");
|
||||
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM(${enumList})`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum" USING "name"::"public"."permission_name_enum"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
const NEW_PERMISSIONS = [
|
||||
{ name: "blog_categories_create", title: "ایجاد دستهبندی بلاگ", source: "blogs_create" },
|
||||
{ name: "blog_categories_read", title: "مشاهده دستهبندی بلاگ", source: "blogs_read" },
|
||||
{ name: "blog_categories_update", title: "ویرایش دستهبندی بلاگ", source: "blogs_update" },
|
||||
{ name: "blog_categories_delete", title: "حذف دستهبندی بلاگ", source: "blogs_delete" },
|
||||
{ name: "blog_comments_create", title: "ایجاد نظرات بلاگ", source: "blogs_create" },
|
||||
{ name: "blog_comments_read", title: "مشاهده نظرات بلاگ", source: "blogs_read" },
|
||||
{ name: "blog_comments_update", title: "ویرایش نظرات بلاگ", source: "blogs_update" },
|
||||
{ name: "blog_comments_delete", title: "حذف نظرات بلاگ", source: "blogs_delete" },
|
||||
{ name: "sliders_create", title: "ایجاد اسلایدر", source: "blogs_create" },
|
||||
{ name: "sliders_read", title: "مشاهده اسلایدر", source: "blogs_read" },
|
||||
{ name: "sliders_update", title: "ویرایش اسلایدر", source: "blogs_update" },
|
||||
{ name: "sliders_delete", title: "حذف اسلایدر", source: "blogs_delete" },
|
||||
] as const;
|
||||
|
||||
const CURRENT_ENUM_VALUES = [
|
||||
"services_create",
|
||||
"services_read",
|
||||
"services_update",
|
||||
"services_delete",
|
||||
"customers_create",
|
||||
"customers_read",
|
||||
"customers_update",
|
||||
"customers_delete",
|
||||
"agents_create",
|
||||
"agents_read",
|
||||
"agents_update",
|
||||
"agents_delete",
|
||||
"developers_create",
|
||||
"developers_read",
|
||||
"developers_update",
|
||||
"developers_delete",
|
||||
"invoices_create",
|
||||
"invoices_read",
|
||||
"invoices_update",
|
||||
"invoices_delete",
|
||||
"transactions_create",
|
||||
"transactions_read",
|
||||
"transactions_update",
|
||||
"transactions_delete",
|
||||
"discounts_create",
|
||||
"discounts_read",
|
||||
"discounts_update",
|
||||
"discounts_delete",
|
||||
"admins_create",
|
||||
"admins_read",
|
||||
"admins_update",
|
||||
"admins_delete",
|
||||
"tickets_create",
|
||||
"tickets_read",
|
||||
"tickets_update",
|
||||
"tickets_delete",
|
||||
"ticket_categories_create",
|
||||
"ticket_categories_read",
|
||||
"ticket_categories_update",
|
||||
"ticket_categories_delete",
|
||||
"ticket_masters_create",
|
||||
"ticket_masters_read",
|
||||
"ticket_masters_update",
|
||||
"ticket_masters_delete",
|
||||
"criticisms_create",
|
||||
"criticisms_read",
|
||||
"criticisms_update",
|
||||
"criticisms_delete",
|
||||
"contacts_us_create",
|
||||
"contacts_us_read",
|
||||
"contacts_us_update",
|
||||
"contacts_us_delete",
|
||||
"advertisements_create",
|
||||
"advertisements_read",
|
||||
"advertisements_update",
|
||||
"advertisements_delete",
|
||||
"announcements_create",
|
||||
"announcements_read",
|
||||
"announcements_update",
|
||||
"announcements_delete",
|
||||
"blogs_create",
|
||||
"blogs_read",
|
||||
"blogs_update",
|
||||
"blogs_delete",
|
||||
"learnings_create",
|
||||
"learnings_read",
|
||||
"learnings_update",
|
||||
"learnings_delete",
|
||||
"settings_create",
|
||||
"settings_read",
|
||||
"settings_update",
|
||||
"settings_delete",
|
||||
"bank_accounts_create",
|
||||
"bank_accounts_read",
|
||||
"bank_accounts_update",
|
||||
"bank_accounts_delete",
|
||||
"payments_create",
|
||||
"payments_read",
|
||||
"payments_update",
|
||||
"payments_delete",
|
||||
"support_plan_create",
|
||||
"support_plan_read",
|
||||
"support_plan_update",
|
||||
"support_plan_delete",
|
||||
"reseller_create",
|
||||
"reseller_read",
|
||||
"reseller_update",
|
||||
"reseller_delete",
|
||||
"workspace_create",
|
||||
"workspace_read",
|
||||
"workspace_update",
|
||||
"workspace_delete",
|
||||
"project_create",
|
||||
"project_read",
|
||||
"project_update",
|
||||
"project_delete",
|
||||
"task_create",
|
||||
"task_read",
|
||||
"task_update",
|
||||
"task_delete",
|
||||
"task_phase_create",
|
||||
"task_phase_read",
|
||||
"task_phase_update",
|
||||
"task_phase_delete",
|
||||
"logs",
|
||||
"manage_sso_clients",
|
||||
"dmenu",
|
||||
"dkala",
|
||||
"dpage",
|
||||
"dmail",
|
||||
"view_all_workspaces",
|
||||
"view_all_projects",
|
||||
];
|
||||
|
||||
const UPDATED_ENUM_VALUES = [
|
||||
...CURRENT_ENUM_VALUES.slice(0, CURRENT_ENUM_VALUES.indexOf("blogs_delete") + 1),
|
||||
...NEW_PERMISSIONS.map((p) => p.name),
|
||||
...CURRENT_ENUM_VALUES.slice(CURRENT_ENUM_VALUES.indexOf("blogs_delete") + 1),
|
||||
];
|
||||
|
||||
export class AddBlogCategoriesCommentsSlidersPermissions1785200000000 implements MigrationInterface {
|
||||
name = "AddBlogCategoriesCommentsSlidersPermissions1785200000000";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE text USING "name"::text`);
|
||||
await queryRunner.query(`DROP TYPE "public"."permission_name_enum"`);
|
||||
|
||||
const enumList = UPDATED_ENUM_VALUES.map((v) => `'${v}'`).join(", ");
|
||||
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM(${enumList})`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum" USING "name"::"public"."permission_name_enum"`,
|
||||
);
|
||||
|
||||
for (const perm of NEW_PERMISSIONS) {
|
||||
const inserted = await queryRunner.query(
|
||||
`INSERT INTO "permission" ("id", "name", "title", "group", "createdAt", "updatedAt")
|
||||
VALUES (uuid_generate_v4(), $1, $2, 'content', NOW(), NOW())
|
||||
RETURNING id`,
|
||||
[perm.name, perm.title],
|
||||
);
|
||||
const newId = inserted[0].id as string;
|
||||
|
||||
const sourcePerm = await queryRunner.query(`SELECT id FROM "permission" WHERE "name" = $1`, [perm.source]);
|
||||
if (!sourcePerm.length) continue;
|
||||
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "role_permission_relation" ("roleId", "permissionId")
|
||||
SELECT "roleId", $1 FROM "role_permission_relation" WHERE "permissionId" = $2
|
||||
ON CONFLICT ("roleId", "permissionId") DO NOTHING`,
|
||||
[newId, sourcePerm[0].id],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
for (const perm of NEW_PERMISSIONS) {
|
||||
const rows = await queryRunner.query(`SELECT id FROM "permission" WHERE "name" = $1`, [perm.name]);
|
||||
if (!rows.length) continue;
|
||||
|
||||
await queryRunner.query(`DELETE FROM "role_permission_relation" WHERE "permissionId" = $1`, [rows[0].id]);
|
||||
await queryRunner.query(`DELETE FROM "permission" WHERE "id" = $1`, [rows[0].id]);
|
||||
}
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE text USING "name"::text`);
|
||||
await queryRunner.query(`DROP TYPE "public"."permission_name_enum"`);
|
||||
|
||||
const enumList = CURRENT_ENUM_VALUES.map((v) => `'${v}'`).join(", ");
|
||||
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM(${enumList})`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum" USING "name"::"public"."permission_name_enum"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../danak-admin"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
@@ -330,6 +330,12 @@ export const enum TicketMessageEnum {
|
||||
INVALID_TICKET_LIMIT = "محدودیت تعداد تیکت در پلن پشتیبانی شما نامعتبر است",
|
||||
TICKET_LIMIT_EXCEEDED = "شما به محدودیت تعداد تیکت در پلن پشتیبانی خود رسیدهاید",
|
||||
OPENED = "تیکت با موفقیت باز شد",
|
||||
MASTER_USER_ID_REQUIRED = "شناسه کاربر مستر تیکت اجباری است",
|
||||
MASTER_USER_ID_SHOULD_BE_UUID = "شناسه کاربر مستر تیکت باید یک UUID معتبر باشد",
|
||||
MASTER_CREATED = "مستر تیکت با موفقیت ایجاد شد",
|
||||
MASTER_NOT_FOUND = "مستر تیکت یافت نشد",
|
||||
MASTER_EXIST = "این کاربر قبلاً به عنوان مستر تیکت ثبت شده است",
|
||||
MASTER_DELETED = "مستر تیکت با موفقیت حذف شد",
|
||||
}
|
||||
|
||||
export const enum WalletMessage {
|
||||
@@ -963,3 +969,7 @@ export const enum timeMessage {
|
||||
TIME_ALREADY_STARTED = "برای این تسک یک زمان در حال اجرا وجود دارد!",
|
||||
NO_RUNNING_TIME = "زمان در حال اجرایی برای این تسک پیدا نشد!"
|
||||
}
|
||||
|
||||
export const enum taskCommentMessage {
|
||||
TASK_COMMENT_NOT_FOUND = "نظر مورد نظر پیدا نشد!"
|
||||
}
|
||||
@@ -5,27 +5,33 @@ import { MailerAsyncOptions } from "@nestjs-modules/mailer/dist/interfaces/maile
|
||||
export function mailerConfig(): MailerAsyncOptions {
|
||||
return {
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
transport: {
|
||||
host: configService.getOrThrow<string>("SMTP_HOST"),
|
||||
port: configService.getOrThrow<number>("SMTP_PORT"),
|
||||
secure: true,
|
||||
auth: {
|
||||
user: configService.getOrThrow<string>("SMTP_USER"),
|
||||
pass: configService.getOrThrow<string>("SMTP_PASS"),
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
from: configService.getOrThrow<string>("MAIL_FROM"),
|
||||
},
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const port = Number(configService.getOrThrow<number>("SMTP_PORT"));
|
||||
// 465 = implicit TLS; 587 = STARTTLS (required by zone-mta behind Traefik)
|
||||
const secure = port === 465;
|
||||
|
||||
template: {
|
||||
dir: process.cwd() + "/src/modules/templates/email",
|
||||
adapter: new HandlebarsAdapter(),
|
||||
options: {
|
||||
strict: true,
|
||||
return {
|
||||
transport: {
|
||||
host: configService.getOrThrow<string>("SMTP_HOST"),
|
||||
port,
|
||||
secure,
|
||||
requireTLS: !secure,
|
||||
auth: {
|
||||
user: configService.getOrThrow<string>("SMTP_USER"),
|
||||
pass: configService.getOrThrow<string>("SMTP_PASS"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
defaults: {
|
||||
from: configService.getOrThrow<string>("MAIL_FROM"),
|
||||
},
|
||||
template: {
|
||||
dir: process.cwd() + "/src/modules/templates/email",
|
||||
adapter: new HandlebarsAdapter(),
|
||||
options: {
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { SSOClientService } from "./sso-client.service";
|
||||
import { TokensService } from "./tokens.service";
|
||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { getPermissionsFromRoles } from "../../users/utils/role-permissions.util";
|
||||
import { ITokenPayload } from "../interfaces/IToken-payload";
|
||||
|
||||
@Injectable()
|
||||
@@ -46,7 +47,7 @@ export class SSOService {
|
||||
const payload: ITokenPayload = {
|
||||
id: user.id,
|
||||
isAdmin: user.roles.some((r) => r.isAdmin),
|
||||
permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])),
|
||||
permissions: getPermissionsFromRoles(user.roles),
|
||||
audience: clientId,
|
||||
};
|
||||
|
||||
@@ -105,7 +106,7 @@ export class SSOService {
|
||||
const payload: ITokenPayload = {
|
||||
id: user.id,
|
||||
isAdmin: user.roles.some((r) => r.isAdmin),
|
||||
permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])),
|
||||
permissions: getPermissionsFromRoles(user.roles),
|
||||
audience: clientId,
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { DataSource, QueryRunner } from "typeorm";
|
||||
import { AuthMessage } from "../../../common/enums/message.enum";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { RefreshTokensRepository } from "../../users/repositories/refresh-token.repository";
|
||||
import { getPermissionsFromRoles } from "../../users/utils/role-permissions.util";
|
||||
import { ITokenPayload } from "../interfaces/IToken-payload";
|
||||
import { ResellerType } from "../DTO/verify-reseller-otp.dto";
|
||||
|
||||
@@ -28,7 +29,7 @@ export class TokensService {
|
||||
{
|
||||
id: user.id,
|
||||
isAdmin: user.roles.some((r) => r.isAdmin),
|
||||
permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])),
|
||||
permissions: getPermissionsFromRoles(user.roles),
|
||||
},
|
||||
queryRunner,
|
||||
);
|
||||
|
||||
@@ -149,7 +149,7 @@ export class BlogsController {
|
||||
|
||||
@ApiOperation({ summary: "Get all comments for a blog (admin route)" })
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS_READ)
|
||||
@PermissionsDec(PermissionEnum.BLOG_COMMENTS_READ)
|
||||
@Get(":id/comments")
|
||||
getBlogCommentsForAdmin(@Param() paramDto: ParamDto) {
|
||||
return this.blogCommentService.getBlogCommentsForAdmin(paramDto.id);
|
||||
@@ -157,7 +157,7 @@ export class BlogsController {
|
||||
|
||||
@ApiOperation({ summary: "Get all comments for a blog (admin route)" })
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS_READ)
|
||||
@PermissionsDec(PermissionEnum.BLOG_COMMENTS_READ)
|
||||
@Get("comments")
|
||||
getAllCommentForAdmin(@Query() queryDto: BlogCommentQueryDto) {
|
||||
return this.blogCommentService.getAllCommentForAdmin(queryDto);
|
||||
@@ -165,7 +165,7 @@ export class BlogsController {
|
||||
|
||||
@ApiOperation({ summary: "Update the status of a comment (admin route)" })
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS_UPDATE)
|
||||
@PermissionsDec(PermissionEnum.BLOG_COMMENTS_UPDATE)
|
||||
@Patch("comments/:id/status")
|
||||
updateCommentStatus(@Param() paramDto: ParamDto, @Body() updateCommentStatusDto: UpdateCommentStatusDto) {
|
||||
return this.blogCommentService.updateCommentStatus(paramDto.id, updateCommentStatusDto);
|
||||
@@ -196,7 +196,7 @@ export class BlogsController {
|
||||
@ApiOperation({ summary: "Create a new category (admin route)" })
|
||||
@Post("categories")
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS_CREATE)
|
||||
@PermissionsDec(PermissionEnum.BLOG_CATEGORIES_CREATE)
|
||||
createCategory(@Body() createCategoryDto: CreateBlogCategoryDto) {
|
||||
return this.blogsService.createCategory(createCategoryDto);
|
||||
}
|
||||
@@ -204,7 +204,7 @@ export class BlogsController {
|
||||
@ApiOperation({ summary: "Update a category (admin route)" })
|
||||
@Patch("categories/:id")
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS_UPDATE)
|
||||
@PermissionsDec(PermissionEnum.BLOG_CATEGORIES_UPDATE)
|
||||
updateCategory(@Param() paramDto: ParamDto, @Body() updateCategoryDto: UpdateBlogCategoryDto) {
|
||||
return this.blogsService.updateCategory(paramDto.id, updateCategoryDto);
|
||||
}
|
||||
@@ -212,13 +212,13 @@ export class BlogsController {
|
||||
@ApiOperation({ summary: "Delete a category (admin route)" })
|
||||
@Delete("categories/:id")
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS_DELETE)
|
||||
@PermissionsDec(PermissionEnum.BLOG_CATEGORIES_DELETE)
|
||||
deleteCategory(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
|
||||
return this.blogsService.deleteCategory(paramDto.id, userIpAndHeaders);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "toggle status of categories (admin route)" })
|
||||
@PermissionsDec(PermissionEnum.BLOGS_CREATE)
|
||||
@PermissionsDec(PermissionEnum.BLOG_CATEGORIES_UPDATE)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("categories/toggle-status/:id")
|
||||
toggleCategoryStatus(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
|
||||
|
||||
@@ -18,7 +18,7 @@ export class SlidersController {
|
||||
|
||||
@ApiOperation({ summary: "create slider (admin route)" })
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS_CREATE)
|
||||
@PermissionsDec(PermissionEnum.SLIDERS_CREATE)
|
||||
@Post()
|
||||
create(@Body() createSliderDto: CreateSliderDto) {
|
||||
return this.slidersService.create(createSliderDto);
|
||||
@@ -26,7 +26,7 @@ export class SlidersController {
|
||||
|
||||
@ApiOperation({ summary: "Get all sliders (admin route)" })
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS_READ)
|
||||
@PermissionsDec(PermissionEnum.SLIDERS_READ)
|
||||
@Get()
|
||||
getSlidersList(@Query() queryDto: PaginationDto) {
|
||||
return this.slidersService.getSlidersList(queryDto);
|
||||
@@ -34,7 +34,7 @@ export class SlidersController {
|
||||
|
||||
@ApiOperation({ summary: "Get a slider by id (admin route)" })
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS_READ)
|
||||
@PermissionsDec(PermissionEnum.SLIDERS_READ)
|
||||
@Get(":id")
|
||||
getSliderById(@Param() paramDto: ParamDto) {
|
||||
return this.slidersService.getSliderById(paramDto.id);
|
||||
@@ -42,7 +42,7 @@ export class SlidersController {
|
||||
|
||||
@ApiOperation({ summary: "Update a slider (admin route)" })
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS_UPDATE)
|
||||
@PermissionsDec(PermissionEnum.SLIDERS_UPDATE)
|
||||
@Patch(":id")
|
||||
update(@Param() paramDto: ParamDto, @Body() updateSliderDto: UpdateSliderDto) {
|
||||
return this.slidersService.update(paramDto.id, updateSliderDto);
|
||||
@@ -50,7 +50,7 @@ export class SlidersController {
|
||||
|
||||
@ApiOperation({ summary: "Delete a slider (admin route)" })
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.BLOGS_DELETE)
|
||||
@PermissionsDec(PermissionEnum.SLIDERS_DELETE)
|
||||
@Delete(":id")
|
||||
remove(@Param() paramDto: ParamDto) {
|
||||
return this.slidersService.remove(paramDto.id);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { UserDec } from "../../../common/decorators/user.decorator";
|
||||
import { AuthGuards } from "../../../common/decorators/auth-guard.decorator";
|
||||
import { ParamDto } from "../../../common/DTO/param.dto";
|
||||
import { ITokenPayload } from "../../auth/interfaces/IToken-payload";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
|
||||
@AuthGuards()
|
||||
@AdminRoute()
|
||||
@@ -80,4 +81,11 @@ export class ProjectController {
|
||||
getProjectDetail(@Param() paramDto: ParamDto, @UserDec() user: ITokenPayload): Promise<TMProject | null> {
|
||||
return this.projectService.getProjectDetail(paramDto.id, user.id, user.permissions);
|
||||
}
|
||||
|
||||
@Get(":id/users")
|
||||
@ApiOperation({ summary: "Get project users" })
|
||||
@ApiResponse({ status: 200, description: "Got Project Users Successfully!" })
|
||||
getProjectUsers(@Param() paramDto: ParamDto): Promise<User[]> {
|
||||
return this.projectService.getProjectUsers(paramDto.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Controller, Post, Patch, Delete, Param, Body, Get } from "@nestjs/common";
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger";
|
||||
import { TaskCommentService } from "../providers/task-comment.service";
|
||||
import { CreateTaskCommentDto } from "../dto/task-comment/create-task-comment.dto";
|
||||
import { UpdateTaskCommentDto } from "../dto/task-comment/update-task-comment.dto";
|
||||
import { TMTaskComment } from "../entities/task-comment.entity";
|
||||
import { AuthGuards } from "../../../common/decorators/auth-guard.decorator";
|
||||
import { AdminRoute } from "../../../common/decorators/admin.decorator";
|
||||
import { UserDec } from "../../../common/decorators/user.decorator";
|
||||
|
||||
@AuthGuards()
|
||||
@AdminRoute()
|
||||
@ApiTags("Task Manager - Comments")
|
||||
@Controller("task-manager/comments")
|
||||
export class TaskCommentController {
|
||||
constructor(private readonly taskCommentService: TaskCommentService) {}
|
||||
|
||||
@Get("task/:taskId")
|
||||
@ApiOperation({ summary: "Get all comments for a task" })
|
||||
@ApiParam({ name: "taskId", description: "Task ID" })
|
||||
getTaskComments(@Param("taskId") taskId: string): Promise<TMTaskComment[]> {
|
||||
return this.taskCommentService.getTaskComments(taskId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: "Create a new comment" })
|
||||
@ApiResponse({ status: 201, description: "Comment created", type: TMTaskComment })
|
||||
@ApiResponse({ status: 404, description: "Task not found" })
|
||||
create(@UserDec("id") userId: string, @Body() body: CreateTaskCommentDto): Promise<TMTaskComment> {
|
||||
return this.taskCommentService.create(userId, body);
|
||||
}
|
||||
|
||||
@Patch(":id")
|
||||
@ApiOperation({ summary: "Update a comment" })
|
||||
@ApiParam({ name: "id", description: "Comment ID" })
|
||||
@ApiResponse({ status: 200, description: "Comment updated", type: TMTaskComment })
|
||||
@ApiResponse({ status: 404, description: "Comment or Task not found" })
|
||||
update(@Param("id") id: string, @Body() body: UpdateTaskCommentDto): Promise<TMTaskComment> {
|
||||
return this.taskCommentService.update(id, body);
|
||||
}
|
||||
|
||||
@Delete(":id")
|
||||
@ApiOperation({ summary: "Delete a comment" })
|
||||
@ApiParam({ name: "id", description: "Comment ID" })
|
||||
@ApiResponse({ status: 200, description: "Comment deleted" })
|
||||
@ApiResponse({ status: 404, description: "Comment not found" })
|
||||
remove(@Param("id") id: string): Promise<void> {
|
||||
return this.taskCommentService.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import { PermissionEnum } from '../../users/enums/permission.enum';
|
||||
import { UserDec } from '../../../common/decorators/user.decorator';
|
||||
import { AuthGuards } from '../../../common/decorators/auth-guard.decorator';
|
||||
import { ITokenPayload } from '../../auth/interfaces/IToken-payload';
|
||||
import { ParamDto } from '../../../common/DTO/param.dto';
|
||||
import { User } from '../../users/entities/user.entity';
|
||||
|
||||
@AuthGuards()
|
||||
@AdminRoute()
|
||||
@@ -28,6 +30,14 @@ export class WorkspaceController {
|
||||
return this.workspaceService.findOneOrFail(id);
|
||||
}
|
||||
|
||||
@Get(':id/users')
|
||||
@ApiOperation({ summary: 'Get workspace users' })
|
||||
@ApiResponse({ status: 200, description: 'Got Workspace Users Successfully!' })
|
||||
@ApiResponse({ status: 404, description: 'Workspace Not Found!' })
|
||||
getWorkspaceUsers(@Param() paramDto: ParamDto): Promise<User[]> {
|
||||
return this.workspaceService.getWorkspaceUsers(paramDto.id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@PermissionsDec(PermissionEnum.WORKSPACE_CREATE)
|
||||
@ApiOperation({ summary: 'Create a new workspace' })
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsString, IsNotEmpty, IsUUID, IsOptional, IsArray } from "class-validator";
|
||||
|
||||
export class CreateTaskCommentDto {
|
||||
@ApiProperty({ description: "Comment content", example: "Please review this task before closing." })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
content: string;
|
||||
|
||||
@ApiProperty({ description: "ID of the task this comment belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" })
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
taskId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: "Attachment file URLs or paths",
|
||||
type: [String],
|
||||
required: false,
|
||||
example: ["https://cdn.example.com/files/screenshot.png"],
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
attachments?: string[];
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from "@nestjs/swagger";
|
||||
import { CreateTaskCommentDto } from "./create-task-comment.dto";
|
||||
|
||||
export class UpdateTaskCommentDto extends PartialType(CreateTaskCommentDto) {}
|
||||
@@ -34,6 +34,15 @@ export class CreateTaskDto {
|
||||
@IsNotEmpty()
|
||||
taskPhaseId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: "ID of the related ticket",
|
||||
required: false,
|
||||
example: "c3d4e5f6-a7b8-9012-cdef-123456789012",
|
||||
})
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
ticketId?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: "IDs of users to assign to the task",
|
||||
required: false,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { TMTask } from "./task.entity";
|
||||
|
||||
@Entity("tm_task_comments")
|
||||
export class TMTaskComment extends BaseEntity {
|
||||
@Column({ type: "text" })
|
||||
content: string;
|
||||
|
||||
@Column({ type: "json", default: [] })
|
||||
attachments: string[];
|
||||
|
||||
// --- Relations ---
|
||||
|
||||
@ManyToOne(() => User, {
|
||||
nullable: false,
|
||||
onDelete: "RESTRICT",
|
||||
})
|
||||
@JoinColumn({ name: "userId" })
|
||||
user: User;
|
||||
|
||||
@Column({ type: "uuid" })
|
||||
userId: string;
|
||||
|
||||
@ManyToOne(() => TMTask, (task) => task.comments, {
|
||||
nullable: false,
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
@JoinColumn({ name: "taskId" })
|
||||
task: TMTask;
|
||||
|
||||
@Column({ type: "uuid" })
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import { TMRemark } from "./remark.entity";
|
||||
import { TMAttachment } from "./attachment.entity";
|
||||
import { TMCheckListItem } from "./check-list-item.entity";
|
||||
import { TMTime } from "./time.entity";
|
||||
import { TMTaskComment } from "./task-comment.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { Ticket } from "../../tickets/entities/ticket.entity";
|
||||
|
||||
@Entity("tm_tasks")
|
||||
export class TMTask extends BaseEntity {
|
||||
@@ -27,6 +29,13 @@ export class TMTask extends BaseEntity {
|
||||
@Column({type: 'int', default: 1})
|
||||
priority: number;
|
||||
|
||||
@ManyToOne(() => Ticket, { nullable: true, onDelete: "SET NULL" })
|
||||
@JoinColumn({ name: "ticketId" })
|
||||
ticket: Ticket | null;
|
||||
|
||||
@Column({ type: "uuid", nullable: true })
|
||||
ticketId: string | null;
|
||||
|
||||
// --- Relations ---
|
||||
|
||||
@ManyToOne(() => TMTaskPhase, (taskPhase) => taskPhase.tasks, {
|
||||
@@ -59,6 +68,11 @@ export class TMTask extends BaseEntity {
|
||||
})
|
||||
times: TMTime[];
|
||||
|
||||
@OneToMany(() => TMTaskComment, (comment) => comment.task, {
|
||||
cascade: ["insert", "update"],
|
||||
})
|
||||
comments: TMTaskComment[];
|
||||
|
||||
@ManyToMany(() => User, (user) => user.tasks)
|
||||
@JoinTable({
|
||||
name: "tm_task_users",
|
||||
|
||||
@@ -31,7 +31,8 @@ export class ProjectService {
|
||||
users: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true
|
||||
lastName: true,
|
||||
profilePic: true,
|
||||
},
|
||||
taskPhases: {
|
||||
id: true,
|
||||
@@ -165,4 +166,9 @@ export class ProjectService {
|
||||
|
||||
return projectDetail;
|
||||
}
|
||||
|
||||
async getProjectUsers(projectId: string): Promise<User[]> {
|
||||
const project = await this.findOneOrFail(projectId);
|
||||
return project.users;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { TaskCommentRepository } from "../repositories/task-comment.repository";
|
||||
import { TMTaskComment } from "../entities/task-comment.entity";
|
||||
import { CreateTaskCommentDto } from "../dto/task-comment/create-task-comment.dto";
|
||||
import { UpdateTaskCommentDto } from "../dto/task-comment/update-task-comment.dto";
|
||||
import { taskCommentMessage } from "../../../common/enums/message.enum";
|
||||
import { TaskService } from "./task.service";
|
||||
|
||||
@Injectable()
|
||||
export class TaskCommentService {
|
||||
constructor(
|
||||
private readonly taskCommentRepository: TaskCommentRepository,
|
||||
private readonly taskService: TaskService,
|
||||
) {}
|
||||
|
||||
async findOneOrFail(id: string): Promise<TMTaskComment> {
|
||||
const comment = await this.taskCommentRepository.findOne({
|
||||
where: { id },
|
||||
relations: { user: true, task: true },
|
||||
});
|
||||
if (!comment) throw new NotFoundException(taskCommentMessage.TASK_COMMENT_NOT_FOUND);
|
||||
return comment;
|
||||
}
|
||||
|
||||
async getTaskComments(taskId: string): Promise<TMTaskComment[]> {
|
||||
await this.taskService.findOneOrFail(taskId);
|
||||
|
||||
return this.taskCommentRepository.find({
|
||||
where: { taskId },
|
||||
relations: { user: true },
|
||||
order: { createdAt: "DESC" },
|
||||
});
|
||||
}
|
||||
|
||||
async create(userId: string, dto: CreateTaskCommentDto): Promise<TMTaskComment> {
|
||||
await this.taskService.findOneOrFail(dto.taskId);
|
||||
|
||||
const comment = this.taskCommentRepository.create({
|
||||
...dto,
|
||||
attachments: dto.attachments ?? [],
|
||||
userId,
|
||||
});
|
||||
return this.taskCommentRepository.save(comment);
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateTaskCommentDto): Promise<TMTaskComment> {
|
||||
const comment = await this.findOneOrFail(id);
|
||||
|
||||
if (dto.taskId) {
|
||||
await this.taskService.findOneOrFail(dto.taskId);
|
||||
}
|
||||
|
||||
Object.assign(comment, dto);
|
||||
return this.taskCommentRepository.save(comment);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.findOneOrFail(id);
|
||||
await this.taskCommentRepository.delete(id);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,8 @@ import { CreateTaskDto } from "../dto/task/create-task.dto";
|
||||
import { UpdateTaskDto } from "../dto/task/update-task.dto";
|
||||
import { ChangeTaskPhaseDto } from "../dto/task/change-task-phase.dto";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { TaskMessage, TaskPhaseMessage } from "../../../common/enums/message.enum";
|
||||
import { Ticket } from "../../tickets/entities/ticket.entity";
|
||||
import { TaskMessage, TaskPhaseMessage, TicketMessageEnum } from "../../../common/enums/message.enum";
|
||||
import { TaskPhaseRepository } from "../repositories/task-phase.repository";
|
||||
import { TMTaskPhase } from "../entities/task-phase.entity";
|
||||
import { PermissionEnum } from "../../users/enums/permission.enum";
|
||||
@@ -21,6 +22,8 @@ export class TaskService {
|
||||
private readonly taskPhaseRepository: TaskPhaseRepository,
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
@InjectRepository(Ticket)
|
||||
private readonly ticketRepository: Repository<Ticket>,
|
||||
) {}
|
||||
|
||||
async findOneOrFail(id: string): Promise<TMTask> {
|
||||
@@ -29,12 +32,19 @@ export class TaskService {
|
||||
relations: {
|
||||
taskPhase: true,
|
||||
users: true,
|
||||
ticket: true,
|
||||
},
|
||||
});
|
||||
if (!task) throw new NotFoundException(TaskMessage.TASK_NOT_FOUND);
|
||||
return task;
|
||||
}
|
||||
|
||||
private async findTicketOrFail(ticketId: string): Promise<Ticket> {
|
||||
const ticket = await this.ticketRepository.findOne({ where: { id: ticketId } });
|
||||
if (!ticket) throw new NotFoundException(TicketMessageEnum.TICKET_NOT_FOUND);
|
||||
return ticket;
|
||||
}
|
||||
|
||||
async findTaskPhaseOrFail(taskPhaseId: string): Promise<TMTaskPhase> {
|
||||
const taskPhase = await this.taskPhaseRepository.findOne({
|
||||
where: { id: taskPhaseId },
|
||||
@@ -50,10 +60,14 @@ export class TaskService {
|
||||
}
|
||||
|
||||
async create(dto: CreateTaskDto): Promise<TMTask> {
|
||||
const { userIds, startDate, endDate, ...rest } = dto;
|
||||
const { userIds, startDate, endDate, ticketId, ...rest } = dto;
|
||||
|
||||
const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId);
|
||||
|
||||
if (ticketId) {
|
||||
await this.findTicketOrFail(ticketId);
|
||||
}
|
||||
|
||||
if (userIds?.length) {
|
||||
const isUsersValid = userIds.every((userId) => taskPhase.project.users.some((user) => user.id === userId));
|
||||
if (!isUsersValid) throw new BadRequestException(TaskMessage.USERS_ASSIGNED_TO_TASK_NOT_PROJECT_MEMBER);
|
||||
@@ -61,6 +75,7 @@ export class TaskService {
|
||||
|
||||
const task = this.taskRepository.create({
|
||||
...rest,
|
||||
ticketId,
|
||||
startDate: startDate ? new Date(startDate) : undefined,
|
||||
endDate: endDate ? new Date(endDate) : undefined,
|
||||
});
|
||||
@@ -74,7 +89,11 @@ export class TaskService {
|
||||
|
||||
async update(id: string, dto: UpdateTaskDto): Promise<TMTask> {
|
||||
const task = await this.findOneOrFail(id);
|
||||
const { userIds, startDate, endDate, ...rest } = dto;
|
||||
const { userIds, startDate, endDate, ticketId, ...rest } = dto;
|
||||
|
||||
if (ticketId) {
|
||||
await this.findTicketOrFail(ticketId);
|
||||
}
|
||||
|
||||
if (dto.taskPhaseId) {
|
||||
const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId);
|
||||
@@ -87,6 +106,7 @@ export class TaskService {
|
||||
|
||||
Object.assign(task, {
|
||||
...rest,
|
||||
...(ticketId !== undefined && { ticketId }),
|
||||
startDate: startDate ? new Date(startDate) : task.startDate,
|
||||
endDate: endDate ? new Date(endDate) : task.endDate,
|
||||
});
|
||||
|
||||
@@ -71,4 +71,9 @@ export class WorkspaceService {
|
||||
}
|
||||
return await this.workspaceRepository.findByUser(userId);
|
||||
}
|
||||
|
||||
async getWorkspaceUsers(workspaceId: string): Promise<User[]> {
|
||||
const workspace = await this.findOneOrFail(workspaceId);
|
||||
return workspace.users;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { TMTaskComment } from "../entities/task-comment.entity";
|
||||
|
||||
@Injectable()
|
||||
export class TaskCommentRepository extends Repository<TMTaskComment> {
|
||||
constructor(
|
||||
@InjectRepository(TMTaskComment) taskCommentRepository: Repository<TMTaskComment>,
|
||||
) {
|
||||
super(taskCommentRepository.target, taskCommentRepository.manager, taskCommentRepository.queryRunner);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,10 @@ export class TaskRepository extends Repository<TMTask> {
|
||||
.leftJoinAndSelect("task.checkListItems", "checkListItem")
|
||||
.leftJoinAndSelect("task.users", "user")
|
||||
.leftJoinAndSelect("task.times", "time")
|
||||
.leftJoinAndSelect("task.ticket", "ticket")
|
||||
.leftJoinAndSelect("time.admin", "timeAdmin")
|
||||
.leftJoinAndSelect("task.comments", "comment")
|
||||
.leftJoinAndSelect("comment.user", "commentUser")
|
||||
|
||||
.loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems")
|
||||
|
||||
@@ -44,6 +47,12 @@ export class TaskRepository extends Repository<TMTask> {
|
||||
"user.id",
|
||||
"user.firstName",
|
||||
"user.lastName",
|
||||
"user.profilePic",
|
||||
|
||||
"ticket.id",
|
||||
"ticket.subject",
|
||||
"ticket.numericId",
|
||||
"ticket.status",
|
||||
|
||||
"time.id",
|
||||
"time.startedAt",
|
||||
@@ -53,6 +62,14 @@ export class TaskRepository extends Repository<TMTask> {
|
||||
"timeAdmin.id",
|
||||
"timeAdmin.firstName",
|
||||
"timeAdmin.lastName",
|
||||
|
||||
"comment.id",
|
||||
"comment.content",
|
||||
"comment.attachments",
|
||||
"comment.createdAt",
|
||||
"commentUser.firstName",
|
||||
"commentUser.lastName",
|
||||
"commentUser.profilePic",
|
||||
])
|
||||
.where("task.id = :taskId", { taskId })
|
||||
|
||||
@@ -113,12 +130,27 @@ export class TaskRepository extends Repository<TMTask> {
|
||||
// response needs. IN (:...ids) doesn't preserve order, so re-order to match seekQuery.
|
||||
const detailRows = await this.createQueryBuilder("task")
|
||||
.leftJoinAndSelect("task.remarks", "remark")
|
||||
.leftJoinAndSelect("task.ticket", "ticket")
|
||||
.leftJoinAndSelect("task.users", "user")
|
||||
.loadRelationCountAndMap("task.attachmentCount", "task.attachments")
|
||||
.loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems")
|
||||
.loadRelationCountAndMap("task.completedCheckListItemCount", "task.checkListItems", "completedCheckListItems", (qb) =>
|
||||
qb.where("completedCheckListItems.isDone = :isDone", { isDone: true }),
|
||||
)
|
||||
.select(["task", "remark.id", "remark.title", "remark.color"])
|
||||
.select([
|
||||
"task",
|
||||
"remark.id",
|
||||
"remark.title",
|
||||
"remark.color",
|
||||
"user.id",
|
||||
"user.firstName",
|
||||
"user.lastName",
|
||||
"user.profilePic",
|
||||
"ticket.id",
|
||||
"ticket.subject",
|
||||
"ticket.numericId",
|
||||
"ticket.status",
|
||||
])
|
||||
.where("task.id IN (:...ids)", { ids })
|
||||
.getMany();
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ export class WorkspaceRepository extends Repository<TMWorkspace> {
|
||||
'workspace',
|
||||
'user.id',
|
||||
'user.firstName',
|
||||
'user.lastName'
|
||||
'user.lastName',
|
||||
'user.profilePic',
|
||||
]).where('workspace.id = :workspaceId', {workspaceId})
|
||||
.getOne();
|
||||
}
|
||||
|
||||
@@ -9,10 +9,12 @@ import { TMRemark } from "./entities/remark.entity";
|
||||
import { TMAttachment } from "./entities/attachment.entity";
|
||||
import { TMCheckListItem } from "./entities/check-list-item.entity";
|
||||
import { TMTime } from "./entities/time.entity";
|
||||
import { TMTaskComment } from "./entities/task-comment.entity";
|
||||
import { WorkspaceRepository } from "./repositories/workspace.repository";
|
||||
import { WorkspaceController } from "./controllers/workspace.controller";
|
||||
import { WorkspaceService } from "./providers/workspace.service";
|
||||
import { User } from "../users/entities/user.entity";
|
||||
import { Ticket } from "../tickets/entities/ticket.entity";
|
||||
import { ProjectController } from "./controllers/project.controller";
|
||||
import { ProjectRepository } from "./repositories/project.repository";
|
||||
import { ProjectService } from "./providers/project.service";
|
||||
@@ -34,10 +36,13 @@ import { AttachmentService } from "./providers/attachment.service";
|
||||
import { TimeController } from "./controllers/time.controller";
|
||||
import { TimeRepository } from "./repositories/time.repository";
|
||||
import { TimeService } from "./providers/time.service";
|
||||
import { TaskCommentController } from "./controllers/task-comment.controller";
|
||||
import { TaskCommentRepository } from "./repositories/task-comment.repository";
|
||||
import { TaskCommentService } from "./providers/task-comment.service";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, TMTime, User]),
|
||||
TypeOrmModule.forFeature([TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, TMTime, TMTaskComment, User, Ticket]),
|
||||
],
|
||||
controllers: [
|
||||
WorkspaceController,
|
||||
@@ -48,6 +53,7 @@ import { TimeService } from "./providers/time.service";
|
||||
RemarkController,
|
||||
AttachmentController,
|
||||
TimeController,
|
||||
TaskCommentController,
|
||||
],
|
||||
providers: [
|
||||
WorkspaceRepository,
|
||||
@@ -66,6 +72,8 @@ import { TimeService } from "./providers/time.service";
|
||||
AttachmentService,
|
||||
TimeRepository,
|
||||
TimeService,
|
||||
]
|
||||
TaskCommentRepository,
|
||||
TaskCommentService,
|
||||
],
|
||||
})
|
||||
export class TaskManagerModule {}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsUUID } from "class-validator";
|
||||
|
||||
import { TicketMessageEnum } from "../../../common/enums/message.enum";
|
||||
|
||||
export class CreateTicketMasterDto {
|
||||
@IsNotEmpty({ message: TicketMessageEnum.MASTER_USER_ID_REQUIRED })
|
||||
@IsUUID("4", { message: TicketMessageEnum.MASTER_USER_ID_SHOULD_BE_UUID })
|
||||
@ApiProperty({ description: "User ID of the ticket master", example: "123e4567-e89b-12d3-a456-426614174000" })
|
||||
userId: string;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsUUID } from "class-validator";
|
||||
|
||||
import { TicketMessageEnum } from "../../../common/enums/message.enum";
|
||||
|
||||
export class UpdateTicketMasterDto {
|
||||
@IsNotEmpty({ message: TicketMessageEnum.MASTER_USER_ID_REQUIRED })
|
||||
@IsUUID("4", { message: TicketMessageEnum.MASTER_USER_ID_SHOULD_BE_UUID })
|
||||
@ApiProperty({ description: "User ID of the ticket master", example: "123e4567-e89b-12d3-a456-426614174000" })
|
||||
userId: string;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Entity, JoinColumn, ManyToOne, Unique } from "typeorm";
|
||||
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
|
||||
@Entity("ticket_master")
|
||||
@Unique(["user"])
|
||||
export class TicketMaster extends BaseEntity {
|
||||
@ManyToOne(() => User, { nullable: false, onDelete: "CASCADE" })
|
||||
@JoinColumn()
|
||||
user: User;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { User } from "../../users/entities/user.entity";
|
||||
import { AdminsService } from "../../users/providers/admins.service";
|
||||
import { UsersService } from "../../users/providers/users.service";
|
||||
import { CreateTicketCategoryDto } from "../DTO/create-ticket-category.dto";
|
||||
import { CreateTicketMasterDto } from "../DTO/create-ticket-master.dto";
|
||||
import { CreateTicketMessageDto } from "../DTO/create-ticket-message.dto";
|
||||
import { CreateTicketByAdminDto, CreateTicketDto } from "../DTO/create-ticket.dto";
|
||||
import { ReferTicketDto } from "../DTO/refer-ticket.dto";
|
||||
@@ -30,9 +31,11 @@ import { TicketMessageAttachment } from "../entities/ticket-message-attachment";
|
||||
import { TicketMessage } from "../entities/ticket-message.entity";
|
||||
import { Ticket } from "../entities/ticket.entity";
|
||||
import { TicketStatus } from "../enums/ticket-status.enum";
|
||||
import { TicketMasterRepository } from "../repositories/ticket-master.repository";
|
||||
import { TicketCategoryRepository } from "../repositories/tickets-category.repository";
|
||||
import { TicketMessagesRepository } from "../repositories/tickets-message.repository";
|
||||
import { TicketsRepository } from "../repositories/tickets.repository";
|
||||
import { PermissionEnum } from "../../users/enums/permission.enum";
|
||||
|
||||
@Injectable()
|
||||
export class TicketsService implements OnModuleInit {
|
||||
@@ -42,6 +45,7 @@ export class TicketsService implements OnModuleInit {
|
||||
@InjectQueue(TICKET.QUEUE_NAME) private readonly ticketQueue: Queue,
|
||||
private readonly ticketsRepository: TicketsRepository,
|
||||
private readonly ticketsCategoryRepository: TicketCategoryRepository,
|
||||
private readonly ticketMasterRepository: TicketMasterRepository,
|
||||
private readonly ticketMessagesRepository: TicketMessagesRepository,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly adminsService: AdminsService,
|
||||
@@ -166,6 +170,79 @@ export class TicketsService implements OnModuleInit {
|
||||
};
|
||||
}
|
||||
|
||||
//******************************** */
|
||||
// ticket masters
|
||||
//******************************** */
|
||||
|
||||
async createTicketMaster(createDto: CreateTicketMasterDto, userIpAndHeaders: IUserIpAndHeaders) {
|
||||
const { user } = await this.usersService.findOneById(createDto.userId);
|
||||
|
||||
const existingMaster = await this.ticketMasterRepository.findByUserId(createDto.userId);
|
||||
if (existingMaster) throw new BadRequestException(TicketMessageEnum.MASTER_EXIST);
|
||||
|
||||
const ticketMaster = this.ticketMasterRepository.create({ user });
|
||||
await this.ticketMasterRepository.save(ticketMaster);
|
||||
|
||||
await this.accessLogService.logCreate("TicketMaster", UserType.ADMIN, {
|
||||
user: { id: userIpAndHeaders.userId },
|
||||
endpoint: "/tickets/masters",
|
||||
requestId: userIpAndHeaders.headers["x-request-id"],
|
||||
ipAddress: userIpAndHeaders.ip,
|
||||
userAgent: userIpAndHeaders.headers["user-agent"],
|
||||
actionDescription: `Admin created ticket master for user: ${user.id}`,
|
||||
metadata: {
|
||||
ticketMasterId: ticketMaster.id,
|
||||
masterUserId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
message: TicketMessageEnum.MASTER_CREATED,
|
||||
ticketMaster: await this.ticketMasterRepository.findByIdWithUser(ticketMaster.id),
|
||||
};
|
||||
}
|
||||
|
||||
//******************************** */
|
||||
|
||||
async getTicketMasters() {
|
||||
const ticketMasters = await this.ticketMasterRepository.findAllWithUser();
|
||||
return { ticketMasters };
|
||||
}
|
||||
|
||||
//******************************** */
|
||||
|
||||
async getTicketMaster(paramDto: ParamDto) {
|
||||
const ticketMaster = await this.ticketMasterRepository.findByIdWithUser(paramDto.id);
|
||||
if (!ticketMaster) throw new BadRequestException(TicketMessageEnum.MASTER_NOT_FOUND);
|
||||
return { ticketMaster };
|
||||
}
|
||||
|
||||
//******************************** */
|
||||
|
||||
async deleteTicketMaster(paramDto: ParamDto, userIpAndHeaders: IUserIpAndHeaders) {
|
||||
const ticketMaster = await this.ticketMasterRepository.findByIdWithUser(paramDto.id);
|
||||
if (!ticketMaster) throw new BadRequestException(TicketMessageEnum.MASTER_NOT_FOUND);
|
||||
|
||||
await this.ticketMasterRepository.delete(paramDto.id);
|
||||
|
||||
await this.accessLogService.logDelete("TicketMaster", paramDto.id, UserType.ADMIN, {
|
||||
user: { id: userIpAndHeaders.userId },
|
||||
endpoint: "/tickets/masters",
|
||||
requestId: userIpAndHeaders.headers["x-request-id"],
|
||||
ipAddress: userIpAndHeaders.ip,
|
||||
userAgent: userIpAndHeaders.headers["user-agent"],
|
||||
actionDescription: `Admin deleted ticket master: ${paramDto.id}`,
|
||||
metadata: {
|
||||
ticketMasterId: ticketMaster.id,
|
||||
masterUserId: ticketMaster.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
message: TicketMessageEnum.MASTER_DELETED,
|
||||
};
|
||||
}
|
||||
|
||||
//******************************** */
|
||||
|
||||
private async checkTicketLimit(userId: string, queryRunner: QueryRunner): Promise<void> {
|
||||
@@ -326,10 +403,10 @@ export class TicketsService implements OnModuleInit {
|
||||
return { tickets, count, paginate: true };
|
||||
}
|
||||
//******************************** */
|
||||
async getTicketForAdmin(queryDto: SearchTicketQueryDto, adminId: string) {
|
||||
const isSuperAdmin = await this.usersService.isSuperAdmin(adminId);
|
||||
async getTicketForAdmin(queryDto: SearchTicketQueryDto, adminPermissions: PermissionEnum[],adminId: string) {
|
||||
const canSeeAllTickets = adminPermissions.includes(PermissionEnum.TICKETS_READ);
|
||||
|
||||
const [tickets, count] = await this.ticketsRepository.findTicketsListForAdmin(queryDto, adminId, isSuperAdmin);
|
||||
const [tickets, count] = await this.ticketsRepository.findTicketsListForAdmin(queryDto, adminId, canSeeAllTickets);
|
||||
|
||||
return { tickets, count, paginate: true };
|
||||
}
|
||||
@@ -538,7 +615,7 @@ export class TicketsService implements OnModuleInit {
|
||||
|
||||
//***************************** */
|
||||
private async assignTicketAndNotify(ticket: Ticket, ticketCategory: TicketCategory, user: User, queryRunner: QueryRunner) {
|
||||
const assignedUser = await this.referralService.assignAdmin(ticketCategory.group);
|
||||
const assignedUser = (await this.assignFromTicketMasters()) ?? (await this.referralService.assignAdmin(ticketCategory.group));
|
||||
//
|
||||
ticket.assignedTo = assignedUser;
|
||||
await queryRunner.manager.save(Ticket, ticket);
|
||||
@@ -561,6 +638,22 @@ export class TicketsService implements OnModuleInit {
|
||||
});
|
||||
await this.notifySuperAdminsForTicket(queryRunner, ticket);
|
||||
}
|
||||
|
||||
//***************************** */
|
||||
|
||||
private async assignFromTicketMasters(): Promise<User | null> {
|
||||
const masters = await this.ticketMasterRepository.findAllWithUser();
|
||||
if (!masters.length) return null;
|
||||
|
||||
const mastersWithLoad = await Promise.all(
|
||||
masters.map(async (master) => ({
|
||||
user: master.user,
|
||||
assignedCount: await this.ticketsRepository.count({ where: { assignedTo: { id: master.user.id } } }),
|
||||
})),
|
||||
);
|
||||
|
||||
return mastersWithLoad.reduce((prev, curr) => (prev.assignedCount < curr.assignedCount ? prev : curr)).user;
|
||||
}
|
||||
//***************************** */
|
||||
|
||||
private async findTicket(queryRunner: QueryRunner, ticketId: string, userId: string, isAdmin: boolean): Promise<Ticket> {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
|
||||
import { TicketMaster } from "../entities/ticket-master.entity";
|
||||
|
||||
@Injectable()
|
||||
export class TicketMasterRepository extends Repository<TicketMaster> {
|
||||
constructor(@InjectRepository(TicketMaster) ticketMasterRepository: Repository<TicketMaster>) {
|
||||
super(ticketMasterRepository.target, ticketMasterRepository.manager, ticketMasterRepository.queryRunner);
|
||||
}
|
||||
|
||||
findAllWithUser(): Promise<TicketMaster[]> {
|
||||
return this.find({
|
||||
relations: { user: true },
|
||||
order: { createdAt: "ASC" },
|
||||
});
|
||||
}
|
||||
|
||||
findByIdWithUser(id: string): Promise<TicketMaster | null> {
|
||||
return this.findOne({
|
||||
where: { id },
|
||||
relations: { user: true },
|
||||
});
|
||||
}
|
||||
|
||||
findByUserId(userId: string): Promise<TicketMaster | null> {
|
||||
return this.findOne({
|
||||
where: { user: { id: userId } },
|
||||
relations: { user: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, Post, Query } from "@nestjs/common";
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Query } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
|
||||
import { CreateTicketCategoryDto } from "./DTO/create-ticket-category.dto";
|
||||
import { CreateTicketMasterDto } from "./DTO/create-ticket-master.dto";
|
||||
import { CreateTicketMessageDto } from "./DTO/create-ticket-message.dto";
|
||||
import { CreateTicketByAdminDto, CreateTicketDto } from "./DTO/create-ticket.dto";
|
||||
import { ReferTicketDto } from "./DTO/refer-ticket.dto";
|
||||
@@ -15,6 +16,8 @@ import { PermissionsDec } from "../../common/decorators/permission.decorator";
|
||||
import { IUserIpAndHeaders, UserDec, UserIpAndHeaders } from "../../common/decorators/user.decorator";
|
||||
import { ParamDto } from "../../common/DTO/param.dto";
|
||||
import { PermissionEnum } from "../users/enums/permission.enum";
|
||||
import { ITokenPayload } from "../auth/interfaces/IToken-payload";
|
||||
|
||||
@Controller("tickets")
|
||||
@ApiTags("Tickets")
|
||||
@AuthGuards()
|
||||
@@ -24,14 +27,14 @@ export class TicketsController {
|
||||
//----------------- ticket categories -------------------
|
||||
|
||||
@ApiOperation({ summary: "Create ticket category => admin route" })
|
||||
@PermissionsDec(PermissionEnum.TICKETS_CREATE)
|
||||
@PermissionsDec(PermissionEnum.TICKET_CATEGORIES_CREATE)
|
||||
@Post("category")
|
||||
createTicketCategory(@Body() createDto: CreateTicketCategoryDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
|
||||
return this.ticketsService.createTicketCategory(createDto, userIpAndHeaders);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Update ticket category => admin route" })
|
||||
@PermissionsDec(PermissionEnum.TICKETS_UPDATE)
|
||||
@PermissionsDec(PermissionEnum.TICKET_CATEGORIES_UPDATE)
|
||||
@Patch("category/:id")
|
||||
updateCategory(
|
||||
@Param() paramDto: ParamDto,
|
||||
@@ -49,7 +52,7 @@ export class TicketsController {
|
||||
|
||||
@ApiOperation({ summary: "Get ticket categories list => admin route" })
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.TICKETS_READ)
|
||||
@PermissionsDec(PermissionEnum.TICKET_CATEGORIES_READ)
|
||||
@Get("categories-list")
|
||||
getCategoriesList(@Query() queryDto: SearchTicketCategoryDto) {
|
||||
return this.ticketsService.getCategoriesList(queryDto);
|
||||
@@ -58,12 +61,46 @@ export class TicketsController {
|
||||
@ApiOperation({ summary: "toggle status of categories => admin route" })
|
||||
@AdminRoute()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@PermissionsDec(PermissionEnum.TICKETS_UPDATE)
|
||||
@PermissionsDec(PermissionEnum.TICKET_CATEGORIES_UPDATE)
|
||||
@Post("category/toggle-status/:id")
|
||||
toggleCategoryStatus(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
|
||||
return this.ticketsService.toggleCategoryStatus(paramDto, userIpAndHeaders);
|
||||
}
|
||||
|
||||
//----------------- ticket masters -------------------
|
||||
|
||||
@ApiOperation({ summary: "Create ticket master => admin route" })
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.TICKET_MASTERS_CREATE)
|
||||
@Post("masters")
|
||||
createTicketMaster(@Body() createDto: CreateTicketMasterDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
|
||||
return this.ticketsService.createTicketMaster(createDto, userIpAndHeaders);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get all ticket masters => admin route" })
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.TICKET_MASTERS_READ)
|
||||
@Get("masters")
|
||||
getTicketMasters() {
|
||||
return this.ticketsService.getTicketMasters();
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get ticket master by id => admin route" })
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.TICKET_MASTERS_READ)
|
||||
@Get("masters/:id")
|
||||
getTicketMaster(@Param() paramDto: ParamDto) {
|
||||
return this.ticketsService.getTicketMaster(paramDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Delete ticket master => admin route" })
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.TICKET_MASTERS_DELETE)
|
||||
@Delete("masters/:id")
|
||||
deleteTicketMaster(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
|
||||
return this.ticketsService.deleteTicketMaster(paramDto, userIpAndHeaders);
|
||||
}
|
||||
|
||||
//----------------- ticket and ticket messages -------------------
|
||||
|
||||
@ApiOperation({ summary: "create ticket ==> user route" })
|
||||
@@ -88,10 +125,9 @@ export class TicketsController {
|
||||
|
||||
@ApiOperation({ summary: "Get all tickets ==> admin route" })
|
||||
@AdminRoute()
|
||||
@PermissionsDec(PermissionEnum.TICKETS_READ)
|
||||
@Get("admin")
|
||||
getAllTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") adminId: string) {
|
||||
return this.ticketsService.getTicketForAdmin(queryDto, adminId);
|
||||
getAllTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec() admin: ITokenPayload) {
|
||||
return this.ticketsService.getTicketForAdmin(queryDto, admin.permissions, admin.id);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "create ticket messages" })
|
||||
|
||||
@@ -4,10 +4,12 @@ import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
|
||||
import { TICKET } from "./constant";
|
||||
import { TicketCategory } from "./entities/ticket-category.entity";
|
||||
import { TicketMaster } from "./entities/ticket-master.entity";
|
||||
import { TicketMessage } from "./entities/ticket-message.entity";
|
||||
import { Ticket } from "./entities/ticket.entity";
|
||||
import { ReferralStrategyFactory } from "./providers/referral-strategy.factory";
|
||||
import { TicketsService } from "./providers/tickets.service";
|
||||
import { TicketMasterRepository } from "./repositories/ticket-master.repository";
|
||||
import { TicketCategoryRepository } from "./repositories/tickets-category.repository";
|
||||
import { TicketMessagesRepository } from "./repositories/tickets-message.repository";
|
||||
import { TicketsRepository } from "./repositories/tickets.repository";
|
||||
@@ -27,7 +29,7 @@ import { LoggerModule } from "../logger/logger.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Ticket, TicketCategory, TicketMessageAttachment, TicketMessage]),
|
||||
TypeOrmModule.forFeature([Ticket, TicketCategory, TicketMaster, TicketMessageAttachment, TicketMessage]),
|
||||
AccessLogsModule,
|
||||
BullModule.registerQueue({ name: TICKET.QUEUE_NAME }),
|
||||
UsersModule,
|
||||
@@ -48,6 +50,7 @@ import { LoggerModule } from "../logger/logger.module";
|
||||
TicketsService,
|
||||
TicketsRepository,
|
||||
TicketCategoryRepository,
|
||||
TicketMasterRepository,
|
||||
TicketMessagesRepository,
|
||||
TicketMessageAttachmentsRepository,
|
||||
TicketProcessor,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsEmail, IsMobilePhone, IsNotEmpty, IsOptional, IsString, IsUUID, IsUrl, Length, MinLength } from "class-validator";
|
||||
import { ArrayNotEmpty, IsArray, IsEmail, IsMobilePhone, IsNotEmpty, IsOptional, IsString, IsUUID, IsUrl, Length, MinLength } from "class-validator";
|
||||
|
||||
import { AdminMessage, AuthMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
@@ -22,9 +22,11 @@ export class CreateAdminDto {
|
||||
phone: string;
|
||||
|
||||
@IsNotEmpty({ message: AdminMessage.ROLE_REQUIRED })
|
||||
@IsUUID("4", { message: AdminMessage.ROLE_ID_SHOULD_BE_UUID })
|
||||
@ApiProperty({ description: "Role id", example: "123e4567-e89b-12d3-a456-426614174000" })
|
||||
roleId: string;
|
||||
@IsArray({ message: AdminMessage.ROLE_REQUIRED })
|
||||
@ArrayNotEmpty({ message: AdminMessage.ROLE_REQUIRED })
|
||||
@IsUUID("4", { each: true, message: AdminMessage.ROLE_ID_SHOULD_BE_UUID })
|
||||
@ApiProperty({ description: "Role ids", example: ["123e4567-e89b-12d3-a456-426614174000"], type: [String] })
|
||||
roleIds: string[];
|
||||
|
||||
@IsNotEmpty({ message: AuthMessage.FIRST_NAME_NOT_EMPTY })
|
||||
@IsString({ message: AuthMessage.FIRST_NAME_NOT_EMPTY })
|
||||
|
||||
@@ -52,6 +52,16 @@ export const permissionMeta: Record<PermissionEnum, PermissionMeta> = {
|
||||
[PermissionEnum.TICKETS_UPDATE]: { title: "ویرایش تیکتها", group: PermissionGroupEnum.SUPPORT },
|
||||
[PermissionEnum.TICKETS_DELETE]: { title: "حذف تیکتها", group: PermissionGroupEnum.SUPPORT },
|
||||
|
||||
[PermissionEnum.TICKET_CATEGORIES_CREATE]: { title: "ایجاد دستهبندی تیکت", group: PermissionGroupEnum.SUPPORT },
|
||||
[PermissionEnum.TICKET_CATEGORIES_READ]: { title: "مشاهده دستهبندی تیکت", group: PermissionGroupEnum.SUPPORT },
|
||||
[PermissionEnum.TICKET_CATEGORIES_UPDATE]: { title: "ویرایش دستهبندی تیکت", group: PermissionGroupEnum.SUPPORT },
|
||||
[PermissionEnum.TICKET_CATEGORIES_DELETE]: { title: "حذف دستهبندی تیکت", group: PermissionGroupEnum.SUPPORT },
|
||||
|
||||
[PermissionEnum.TICKET_MASTERS_CREATE]: { title: "ایجاد مستر تیکت", group: PermissionGroupEnum.SUPPORT },
|
||||
[PermissionEnum.TICKET_MASTERS_READ]: { title: "مشاهده مستر تیکت", group: PermissionGroupEnum.SUPPORT },
|
||||
[PermissionEnum.TICKET_MASTERS_UPDATE]: { title: "ویرایش مستر تیکت", group: PermissionGroupEnum.SUPPORT },
|
||||
[PermissionEnum.TICKET_MASTERS_DELETE]: { title: "حذف مستر تیکت", group: PermissionGroupEnum.SUPPORT },
|
||||
|
||||
[PermissionEnum.CRITICISMS_CREATE]: { title: "ایجاد انتقادات", group: PermissionGroupEnum.OTHER },
|
||||
[PermissionEnum.CRITICISMS_READ]: { title: "مشاهده انتقادات", group: PermissionGroupEnum.OTHER },
|
||||
[PermissionEnum.CRITICISMS_UPDATE]: { title: "ویرایش انتقادات", group: PermissionGroupEnum.OTHER },
|
||||
@@ -77,6 +87,21 @@ export const permissionMeta: Record<PermissionEnum, PermissionMeta> = {
|
||||
[PermissionEnum.BLOGS_UPDATE]: { title: "ویرایش بلاگها", group: PermissionGroupEnum.CONTENT },
|
||||
[PermissionEnum.BLOGS_DELETE]: { title: "حذف بلاگها", group: PermissionGroupEnum.CONTENT },
|
||||
|
||||
[PermissionEnum.BLOG_CATEGORIES_CREATE]: { title: "ایجاد دستهبندی بلاگ", group: PermissionGroupEnum.CONTENT },
|
||||
[PermissionEnum.BLOG_CATEGORIES_READ]: { title: "مشاهده دستهبندی بلاگ", group: PermissionGroupEnum.CONTENT },
|
||||
[PermissionEnum.BLOG_CATEGORIES_UPDATE]: { title: "ویرایش دستهبندی بلاگ", group: PermissionGroupEnum.CONTENT },
|
||||
[PermissionEnum.BLOG_CATEGORIES_DELETE]: { title: "حذف دستهبندی بلاگ", group: PermissionGroupEnum.CONTENT },
|
||||
|
||||
[PermissionEnum.BLOG_COMMENTS_CREATE]: { title: "ایجاد نظرات بلاگ", group: PermissionGroupEnum.CONTENT },
|
||||
[PermissionEnum.BLOG_COMMENTS_READ]: { title: "مشاهده نظرات بلاگ", group: PermissionGroupEnum.CONTENT },
|
||||
[PermissionEnum.BLOG_COMMENTS_UPDATE]: { title: "ویرایش نظرات بلاگ", group: PermissionGroupEnum.CONTENT },
|
||||
[PermissionEnum.BLOG_COMMENTS_DELETE]: { title: "حذف نظرات بلاگ", group: PermissionGroupEnum.CONTENT },
|
||||
|
||||
[PermissionEnum.SLIDERS_CREATE]: { title: "ایجاد اسلایدر", group: PermissionGroupEnum.CONTENT },
|
||||
[PermissionEnum.SLIDERS_READ]: { title: "مشاهده اسلایدر", group: PermissionGroupEnum.CONTENT },
|
||||
[PermissionEnum.SLIDERS_UPDATE]: { title: "ویرایش اسلایدر", group: PermissionGroupEnum.CONTENT },
|
||||
[PermissionEnum.SLIDERS_DELETE]: { title: "حذف اسلایدر", group: PermissionGroupEnum.CONTENT },
|
||||
|
||||
[PermissionEnum.LEARNINGS_CREATE]: { title: "ایجاد آموزشها", group: PermissionGroupEnum.CONTENT },
|
||||
[PermissionEnum.LEARNINGS_READ]: { title: "مشاهده آموزشها", group: PermissionGroupEnum.CONTENT },
|
||||
[PermissionEnum.LEARNINGS_UPDATE]: { title: "ویرایش آموزشها", group: PermissionGroupEnum.CONTENT },
|
||||
|
||||
@@ -53,6 +53,18 @@ export enum PermissionEnum {
|
||||
TICKETS_UPDATE = "tickets_update",
|
||||
TICKETS_DELETE = "tickets_delete",
|
||||
|
||||
// ticket_categories
|
||||
TICKET_CATEGORIES_CREATE = "ticket_categories_create",
|
||||
TICKET_CATEGORIES_READ = "ticket_categories_read",
|
||||
TICKET_CATEGORIES_UPDATE = "ticket_categories_update",
|
||||
TICKET_CATEGORIES_DELETE = "ticket_categories_delete",
|
||||
|
||||
// ticket_masters
|
||||
TICKET_MASTERS_CREATE = "ticket_masters_create",
|
||||
TICKET_MASTERS_READ = "ticket_masters_read",
|
||||
TICKET_MASTERS_UPDATE = "ticket_masters_update",
|
||||
TICKET_MASTERS_DELETE = "ticket_masters_delete",
|
||||
|
||||
// criticisms
|
||||
CRITICISMS_CREATE = "criticisms_create",
|
||||
CRITICISMS_READ = "criticisms_read",
|
||||
@@ -83,6 +95,24 @@ export enum PermissionEnum {
|
||||
BLOGS_UPDATE = "blogs_update",
|
||||
BLOGS_DELETE = "blogs_delete",
|
||||
|
||||
// blog_categories
|
||||
BLOG_CATEGORIES_CREATE = "blog_categories_create",
|
||||
BLOG_CATEGORIES_READ = "blog_categories_read",
|
||||
BLOG_CATEGORIES_UPDATE = "blog_categories_update",
|
||||
BLOG_CATEGORIES_DELETE = "blog_categories_delete",
|
||||
|
||||
// blog_comments
|
||||
BLOG_COMMENTS_CREATE = "blog_comments_create",
|
||||
BLOG_COMMENTS_READ = "blog_comments_read",
|
||||
BLOG_COMMENTS_UPDATE = "blog_comments_update",
|
||||
BLOG_COMMENTS_DELETE = "blog_comments_delete",
|
||||
|
||||
// sliders
|
||||
SLIDERS_CREATE = "sliders_create",
|
||||
SLIDERS_READ = "sliders_read",
|
||||
SLIDERS_UPDATE = "sliders_update",
|
||||
SLIDERS_DELETE = "sliders_delete",
|
||||
|
||||
// learnings
|
||||
LEARNINGS_CREATE = "learnings_create",
|
||||
LEARNINGS_READ = "learnings_read",
|
||||
|
||||
@@ -18,6 +18,7 @@ import { RoleEnum } from "../enums/role.enum";
|
||||
import { PermissionsRepository } from "../repositories/permissions.repository";
|
||||
import { RoleRepository } from "../repositories/roles.repository";
|
||||
import { UserRepository } from "../repositories/users.repository";
|
||||
import { getPermissionEntitiesFromRoles } from "../utils/role-permissions.util";
|
||||
|
||||
@Injectable()
|
||||
export class AdminsService {
|
||||
@@ -43,22 +44,22 @@ export class AdminsService {
|
||||
const existPhone = await queryRunner.manager.findOneBy(this.userRepository.target, { phone: createDto.phone });
|
||||
if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST);
|
||||
|
||||
const role = await queryRunner.manager.findOne(this.rolesRepository.target, {
|
||||
where: { id: createDto.roleId },
|
||||
const roles = await queryRunner.manager.find(this.rolesRepository.target, {
|
||||
where: { id: In(createDto.roleIds), isAdmin: true },
|
||||
relations: { permissions: true },
|
||||
});
|
||||
if (!role) throw new BadRequestException(AdsMessage.ROLE_NOT_FOUND);
|
||||
if (roles.length !== createDto.roleIds.length) throw new BadRequestException(AdsMessage.ROLE_NOT_FOUND);
|
||||
|
||||
if (createDto.password !== createDto.repeatPassword) throw new BadRequestException(AdminMessage.PASSWORD_NOT_MATCH);
|
||||
|
||||
const hashedPassword = await this.passwordService.hashPassword(createDto.password);
|
||||
const userName = slugify(`${createDto.firstName} ${Date.now().toString().slice(-5)}`, { lower: true, trim: true });
|
||||
|
||||
await queryRunner.manager.save(this.rolesRepository.target, role);
|
||||
const { roleIds: _roleIds, repeatPassword: _repeatPassword, ...adminData } = createDto;
|
||||
|
||||
const adminUser = queryRunner.manager.create(this.userRepository.target, {
|
||||
...createDto,
|
||||
roles: [role],
|
||||
...adminData,
|
||||
roles,
|
||||
password: hashedPassword,
|
||||
userName,
|
||||
});
|
||||
@@ -75,7 +76,7 @@ export class AdminsService {
|
||||
userAgent: userIpAndHeaders.headers["user-agent"],
|
||||
actionDescription: `Admin created new admin: ${createDto.email}`,
|
||||
metadata: {
|
||||
roleId: createDto.roleId,
|
||||
roleIds: createDto.roleIds,
|
||||
createdBy: "admin",
|
||||
email: createDto.email,
|
||||
phone: createDto.phone,
|
||||
@@ -118,16 +119,14 @@ export class AdminsService {
|
||||
if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST);
|
||||
}
|
||||
|
||||
if (updateDto.roleId) {
|
||||
const role = await queryRunner.manager.findOne(this.rolesRepository.target, {
|
||||
where: { id: updateDto.roleId },
|
||||
if (updateDto.roleIds?.length) {
|
||||
const roles = await queryRunner.manager.find(this.rolesRepository.target, {
|
||||
where: { id: In(updateDto.roleIds), isAdmin: true },
|
||||
relations: { permissions: true },
|
||||
});
|
||||
if (!role) throw new BadRequestException(AdsMessage.ROLE_NOT_FOUND);
|
||||
if (roles.length !== updateDto.roleIds.length) throw new BadRequestException(AdsMessage.ROLE_NOT_FOUND);
|
||||
|
||||
await queryRunner.manager.save(this.rolesRepository.target, role);
|
||||
|
||||
admin.roles = [role];
|
||||
admin.roles = roles;
|
||||
|
||||
await queryRunner.manager.save(this.userRepository.target, admin);
|
||||
}
|
||||
@@ -138,7 +137,8 @@ export class AdminsService {
|
||||
admin.password = await this.passwordService.hashPassword(updateDto.password);
|
||||
}
|
||||
|
||||
await queryRunner.manager.save(this.userRepository.target, { ...admin, ...updateDto, password: admin.password });
|
||||
const { roleIds: _roleIds, repeatPassword: _repeatPassword, ...updateData } = updateDto;
|
||||
await queryRunner.manager.save(this.userRepository.target, { ...admin, ...updateData, password: admin.password });
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
await this.accessLogService.logUpdate("User", admin.id, UserType.ADMIN, {
|
||||
@@ -150,7 +150,7 @@ export class AdminsService {
|
||||
userType: UserType.ADMIN,
|
||||
actionDescription: `Admin updated admin: ${admin.email}`,
|
||||
metadata: {
|
||||
roleId: updateDto.roleId,
|
||||
roleIds: updateDto.roleIds,
|
||||
},
|
||||
oldValues: JSON.stringify({ ...admin }),
|
||||
newValues: JSON.stringify({ ...updateDto }),
|
||||
@@ -169,7 +169,7 @@ export class AdminsService {
|
||||
/************************************************************ */
|
||||
|
||||
async deleteAdmin(id: string, userIpAndHeaders: IUserIpAndHeaders) {
|
||||
const admin = await this.userRepository.findOne({ where: { id, roles: { isAdmin: true } } });
|
||||
const admin = await this.userRepository.findOne({ where: { id, roles: { isAdmin: true } }, relations: { roles: true } });
|
||||
if (!admin) throw new BadRequestException(AdminMessage.ADMIN_NOT_FOUND);
|
||||
|
||||
await this.userRepository.softDelete(id);
|
||||
@@ -182,7 +182,7 @@ export class AdminsService {
|
||||
userAgent: userIpAndHeaders.headers["user-agent"],
|
||||
actionDescription: `Admin deleted admin: ${admin.email}`,
|
||||
metadata: {
|
||||
roleId: admin.roles[0].id,
|
||||
roleIds: admin.roles.map((role) => role.id),
|
||||
email: admin.email,
|
||||
phone: admin.phone,
|
||||
},
|
||||
@@ -277,7 +277,11 @@ export class AdminsService {
|
||||
paginate: true,
|
||||
};
|
||||
}
|
||||
/************************************************************ */
|
||||
|
||||
async getAllAdmins() {
|
||||
return this.userRepository.find({ where: { roles: { isAdmin: true } } });
|
||||
}
|
||||
/************************************************************ */
|
||||
|
||||
async getAdminById(id: string) {
|
||||
@@ -303,7 +307,7 @@ export class AdminsService {
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
|
||||
return {
|
||||
permissions: user.roles[0].permissions,
|
||||
permissions: getPermissionEntitiesFromRoles(user.roles),
|
||||
};
|
||||
}
|
||||
/************************************************************ */
|
||||
|
||||
@@ -2,7 +2,7 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||
import { DataSource, Not } from "typeorm";
|
||||
|
||||
import { IUserIpAndHeaders } from "../../../common/decorators/user.decorator";
|
||||
import { CommonMessage, UserMessage } from "../../../common/enums/message.enum";
|
||||
import { AuthMessage, CommonMessage, FinancialMessage, UserMessage } from "../../../common/enums/message.enum";
|
||||
import { UserType } from "../../access-logs/enums/user-type.enum";
|
||||
import { AccessLogService } from "../../access-logs/providers/access-log.service";
|
||||
import { Address } from "../../address/entities/address.entity";
|
||||
@@ -135,6 +135,7 @@ export class CustomersService {
|
||||
password,
|
||||
phone,
|
||||
birthDate,
|
||||
nationalCode,
|
||||
};
|
||||
|
||||
// const addressUpdateData = {
|
||||
@@ -169,7 +170,7 @@ export class CustomersService {
|
||||
|
||||
if (nationalCode) {
|
||||
const existingNationalCode = await queryRunner.manager.findOne(this.realUserRepository.target, {
|
||||
where: { nationalCode: nationalCode, id: Not(user.id) },
|
||||
where: { nationalCode: nationalCode, user: { id: Not(user.id) } },
|
||||
});
|
||||
if (existingNationalCode) throw new BadRequestException(UserMessage.NATIONAL_CODE_EXIST);
|
||||
}
|
||||
@@ -180,67 +181,109 @@ export class CustomersService {
|
||||
await queryRunner.manager.update(this.userRepository.target, { id: customerId }, { password: hashPassword });
|
||||
}
|
||||
|
||||
const legalUser = await queryRunner.manager.findOne(this.legalUserRepository.target, {
|
||||
where: { user: { id: customerId } },
|
||||
relations: { address: true },
|
||||
});
|
||||
|
||||
if (!legalUser) throw new BadRequestException(UserMessage.LEGAL_DATA_NOT_FOUND);
|
||||
|
||||
if (economicCode) {
|
||||
const existingEconomicCode = await queryRunner.manager.findOne(this.legalUserRepository.target, {
|
||||
where: { economicCode, id: Not(legalUser.id) },
|
||||
if (nationalCode) {
|
||||
const realUser = await queryRunner.manager.findOne(this.realUserRepository.target, {
|
||||
where: { user: { id: customerId } },
|
||||
});
|
||||
if (existingEconomicCode) throw new BadRequestException(UserMessage.ECONOMIC_CODE_EXIST);
|
||||
}
|
||||
|
||||
if (registrationCode) {
|
||||
const existingRegistrationCode = await queryRunner.manager.findOne(this.legalUserRepository.target, {
|
||||
where: { registrationCode, id: Not(legalUser.id) },
|
||||
});
|
||||
if (existingRegistrationCode) throw new BadRequestException(UserMessage.REGISTRATION_CODE_EXIST);
|
||||
}
|
||||
|
||||
if (nationalIdentity) {
|
||||
const existingNationalIdentity = await queryRunner.manager.findOne(this.legalUserRepository.target, {
|
||||
where: { nationalIdentity, id: Not(legalUser.id) },
|
||||
});
|
||||
if (existingNationalIdentity) throw new BadRequestException(UserMessage.NATIONAL_IDENTITY_EXIST);
|
||||
}
|
||||
|
||||
if (registrationName) {
|
||||
const existingRegistrationName = await queryRunner.manager.findOne(this.legalUserRepository.target, {
|
||||
where: { registrationName, id: Not(legalUser.id) },
|
||||
});
|
||||
if (existingRegistrationName) throw new BadRequestException(UserMessage.REGISTRATION_NAME_EXIST);
|
||||
}
|
||||
|
||||
await queryRunner.manager.update(this.legalUserRepository.target, { id: legalUser.id }, legalUserUpdateData);
|
||||
|
||||
// Update address if needed
|
||||
if (cityId || address || postalCode) {
|
||||
// Get the address entity
|
||||
const addressEntity = legalUser.address;
|
||||
|
||||
if (!addressEntity) throw new BadRequestException(UserMessage.ADDRESS_NOT_FOUND);
|
||||
|
||||
// Prepare address updates
|
||||
const addressUpdates: Record<string, unknown> = {};
|
||||
|
||||
if (cityId) {
|
||||
const { city } = await this.addressService.getCity(+cityId);
|
||||
addressUpdates.city = city;
|
||||
if (realUser) {
|
||||
await queryRunner.manager.update(this.realUserRepository.target, { id: realUser.id }, { nationalCode });
|
||||
}
|
||||
}
|
||||
|
||||
if (address) {
|
||||
addressUpdates.fullAddress = address;
|
||||
const hasLegalUpdateData = Boolean(
|
||||
registrationName || economicCode || nationalIdentity || registrationCode || postalCode || address || cityId,
|
||||
);
|
||||
|
||||
if (hasLegalUpdateData) {
|
||||
const legalUser = await queryRunner.manager.findOne(this.legalUserRepository.target, {
|
||||
where: { user: { id: customerId } },
|
||||
relations: { address: true },
|
||||
});
|
||||
|
||||
if (!legalUser) {
|
||||
if (user.financialType === FinancialType.REAL) {
|
||||
throw new BadRequestException(UserMessage.FINANCIAL_TYPE_IS_REAL);
|
||||
}
|
||||
|
||||
const legalPhone = phone ?? user.phone;
|
||||
|
||||
if (!economicCode) throw new BadRequestException(FinancialMessage.ECONOMIC_CODE_REQUIRED);
|
||||
if (!registrationCode) throw new BadRequestException(FinancialMessage.REGISTRATION_CODE_REQUIRED);
|
||||
if (!registrationName) throw new BadRequestException(FinancialMessage.REGISTRATION_NAME_REQUIRED);
|
||||
if (!nationalIdentity) throw new BadRequestException(FinancialMessage.NATIONAL_ID_REQUIRED);
|
||||
if (!legalPhone) throw new BadRequestException(AuthMessage.PHONE_NOT_EMPTY);
|
||||
if (!address) throw new BadRequestException(FinancialMessage.ADDRESS_REQUIRED);
|
||||
if (!postalCode) throw new BadRequestException(FinancialMessage.POSTAL_CODE_REQUIRED);
|
||||
if (!cityId) throw new BadRequestException(FinancialMessage.CITY_ID_REQUIRED);
|
||||
|
||||
await this.createLegalUserData(
|
||||
{
|
||||
economicCode,
|
||||
registrationCode,
|
||||
registrationName,
|
||||
nationalIdentity,
|
||||
phone: legalPhone,
|
||||
address,
|
||||
postalCode,
|
||||
cityId: +cityId,
|
||||
},
|
||||
customerId,
|
||||
queryRunner,
|
||||
);
|
||||
} else {
|
||||
if (economicCode) {
|
||||
const existingEconomicCode = await queryRunner.manager.findOne(this.legalUserRepository.target, {
|
||||
where: { economicCode, id: Not(legalUser.id) },
|
||||
});
|
||||
if (existingEconomicCode) throw new BadRequestException(UserMessage.ECONOMIC_CODE_EXIST);
|
||||
}
|
||||
|
||||
if (registrationCode) {
|
||||
const existingRegistrationCode = await queryRunner.manager.findOne(this.legalUserRepository.target, {
|
||||
where: { registrationCode, id: Not(legalUser.id) },
|
||||
});
|
||||
if (existingRegistrationCode) throw new BadRequestException(UserMessage.REGISTRATION_CODE_EXIST);
|
||||
}
|
||||
|
||||
if (nationalIdentity) {
|
||||
const existingNationalIdentity = await queryRunner.manager.findOne(this.legalUserRepository.target, {
|
||||
where: { nationalIdentity, id: Not(legalUser.id) },
|
||||
});
|
||||
if (existingNationalIdentity) throw new BadRequestException(UserMessage.NATIONAL_IDENTITY_EXIST);
|
||||
}
|
||||
|
||||
if (registrationName) {
|
||||
const existingRegistrationName = await queryRunner.manager.findOne(this.legalUserRepository.target, {
|
||||
where: { registrationName, id: Not(legalUser.id) },
|
||||
});
|
||||
if (existingRegistrationName) throw new BadRequestException(UserMessage.REGISTRATION_NAME_EXIST);
|
||||
}
|
||||
|
||||
await queryRunner.manager.update(this.legalUserRepository.target, { id: legalUser.id }, legalUserUpdateData);
|
||||
|
||||
if (cityId || address || postalCode) {
|
||||
const addressEntity = legalUser.address;
|
||||
|
||||
if (!addressEntity) throw new BadRequestException(UserMessage.ADDRESS_NOT_FOUND);
|
||||
|
||||
const addressUpdates: Record<string, unknown> = {};
|
||||
|
||||
if (cityId) {
|
||||
const { city } = await this.addressService.getCity(+cityId);
|
||||
addressUpdates.city = city;
|
||||
}
|
||||
|
||||
if (address) {
|
||||
addressUpdates.fullAddress = address;
|
||||
}
|
||||
|
||||
if (postalCode) {
|
||||
addressUpdates.postalCode = postalCode;
|
||||
}
|
||||
|
||||
await queryRunner.manager.update(Address, { id: addressEntity.id }, addressUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
if (postalCode) {
|
||||
addressUpdates.postalCode = postalCode;
|
||||
}
|
||||
|
||||
await queryRunner.manager.update(Address, { id: addressEntity.id }, addressUpdates);
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
@@ -359,6 +359,7 @@ export class UsersService {
|
||||
async isSuperAdmin(adminId: string) {
|
||||
const user = await this.userRepository.findOne({ where: { id: adminId }, relations: { roles: true } });
|
||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||
console.log("user.roles", user.roles.map((role) => role.name));
|
||||
return user.roles.some((role) => role.name === RoleEnum.SUPER_ADMIN);
|
||||
}
|
||||
|
||||
|
||||
@@ -119,10 +119,21 @@ export class UserRepository extends Repository<User> {
|
||||
.loadRelationCountAndMap("user.ticketsCount", "user.tickets");
|
||||
|
||||
if (queryDto.q) {
|
||||
queryBuilder
|
||||
.orWhere("user.firstName ILIKE :search", { search: `%${queryDto.q}%` })
|
||||
.orWhere("user.lastName ILIKE :search", { search: `%${queryDto.q}%` })
|
||||
.orWhere("user.userName ILIKE :search", { search: `%${queryDto.q}%` });
|
||||
queryBuilder.andWhere(
|
||||
`(
|
||||
user.firstName ILIKE :search
|
||||
OR user.lastName ILIKE :search
|
||||
OR CONCAT(user.firstName, ' ', user.lastName) ILIKE :search
|
||||
OR user.userName ILIKE :search
|
||||
OR user.phone ILIKE :search
|
||||
OR legalUser.registrationName ILIKE :search
|
||||
OR legalUser.phone ILIKE :search
|
||||
OR realUser.firstName ILIKE :search
|
||||
OR realUser.lastName ILIKE :search
|
||||
OR realUser.phone ILIKE :search
|
||||
)`,
|
||||
{ search: `%${queryDto.q}%` },
|
||||
);
|
||||
}
|
||||
|
||||
return queryBuilder.getMany();
|
||||
@@ -166,13 +177,28 @@ export class UserRepository extends Repository<User> {
|
||||
.loadRelationCountAndMap("user.ticketsCount", "user.tickets");
|
||||
|
||||
if (queryDto.q) {
|
||||
queryBuilder
|
||||
.orWhere("user.firstName ILIKE :search", { search: `%${queryDto.q}%` })
|
||||
.orWhere("user.lastName ILIKE :search", { search: `%${queryDto.q}%` })
|
||||
.orWhere("user.userName ILIKE :search", { search: `%${queryDto.q}%` });
|
||||
queryBuilder.andWhere(
|
||||
`(
|
||||
user.firstName ILIKE :search
|
||||
OR user.lastName ILIKE :search
|
||||
OR CONCAT(user.firstName, ' ', user.lastName) ILIKE :search
|
||||
OR user.userName ILIKE :search
|
||||
OR user.phone ILIKE :search
|
||||
OR legalUser.registrationName ILIKE :search
|
||||
OR legalUser.phone ILIKE :search
|
||||
OR realUser.firstName ILIKE :search
|
||||
OR realUser.lastName ILIKE :search
|
||||
OR realUser.phone ILIKE :search
|
||||
)`,
|
||||
{ search: `%${queryDto.q}%` },
|
||||
);
|
||||
}
|
||||
|
||||
return await queryBuilder.skip(skip).take(limit).getManyAndCount();
|
||||
return await queryBuilder
|
||||
.orderBy("user.createdAt", "DESC")
|
||||
.skip(skip)
|
||||
.take(limit)
|
||||
.getManyAndCount();
|
||||
}
|
||||
|
||||
//************* */
|
||||
|
||||
@@ -171,12 +171,18 @@ export class UsersController {
|
||||
|
||||
@ApiOperation({ summary: "get all admins ==> admin route" })
|
||||
@AdminRoute()
|
||||
// @PermissionsDec(PermissionEnum.ADMINS_READ, PermissionEnum.CUSTOMERS_READ)
|
||||
@Get("admins")
|
||||
getAdmins(@Query() queryDto: SearchAdminQueryDto) {
|
||||
return this.adminsService.getAdmins(queryDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "get all admins without pagination==> admin route" })
|
||||
@AdminRoute()
|
||||
@Get("admins-list/all")
|
||||
getAllAdmins() {
|
||||
return this.adminsService.getAllAdmins();
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "get admin by id ==> admin route" })
|
||||
@PermissionsDec(PermissionEnum.ADMINS_READ, PermissionEnum.CUSTOMERS_READ)
|
||||
@Get("admins/:id")
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { PermissionEnum } from "../enums/permission.enum";
|
||||
import { Role } from "../entities/role.entity";
|
||||
import { Permission } from "../entities/permission.entity";
|
||||
|
||||
export function getPermissionsFromRoles(roles: Role[]): PermissionEnum[] {
|
||||
return [...new Set(roles.flatMap((role) => (role?.permissions?.length ? role.permissions.map((permission) => permission.name) : [])))];
|
||||
}
|
||||
|
||||
export function getPermissionEntitiesFromRoles(roles: Role[]): Permission[] {
|
||||
const permissionsById = new Map<string, Permission>();
|
||||
|
||||
for (const role of roles) {
|
||||
for (const permission of role?.permissions ?? []) {
|
||||
permissionsById.set(permission.id, permission);
|
||||
}
|
||||
}
|
||||
|
||||
return [...permissionsById.values()];
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import request from "supertest";
|
||||
|
||||
import { AppModule } from "./../src/app.module";
|
||||
|
||||
describe("AppController (e2e)", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("/ (GET)", () => {
|
||||
return request(app.getHttpServer()).get("/").expect(200).expect("Hello World!");
|
||||
});
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user