15 Commits

Author SHA1 Message Date
morteza 7f78691f91 update national code
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-26 14:29:25 +03:30
morteza aef712ca1d workspace users
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-26 10:06:36 +03:30
morteza 77783549fa new perms and fix email
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-26 09:31:08 +03:30
morteza be6e4a8d32 ticket master 2026-07-26 08:44:56 +03:30
morteza af606ce9a7 task manager
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-25 15:48:33 +03:30
morteza 7996617cf0 fix superadmin bug
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-25 10:34:06 +03:30
morteza 9e28dfd628 new perms for ticket category
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-23 12:06:34 +03:30
morteza c20e0ba684 sort customers
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-22 16:17:55 +03:30
morteza ced46e500d sort customers
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-22 15:43:08 +03:30
morteza c925d2e539 multi role
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-22 12:19:37 +03:30
morteza 10035f3f25 update users
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-22 11:24:46 +03:30
morteza 19c396e9d1 populate user
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-21 14:32:54 +03:30
morteza a13097bbd6 task : populate
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-20 19:34:14 +03:30
morteza 8aaac51b12 add : ticket comment
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-20 12:07:20 +03:30
morteza b855d81817 add : ticket to task 2026-07-20 11:54:39 +03:30
49 changed files with 1454 additions and 180 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ SMS_PATTERN_NEW_TICKET_GLOBAL=536596
SMS_PATTERN_NEW_CRITICISM=983724 SMS_PATTERN_NEW_CRITICISM=983724
SMS_PATTERN_USER_PASSWORD=486237 SMS_PATTERN_USER_PASSWORD=486237
SMTP_HOST=smtps.danakcorp.com SMTP_HOST=smtps.danakcorp.com
SMTP_PORT=465 SMTP_PORT=587
SMTP_USER=no-reply-danakcorp.com SMTP_USER=no-reply-danakcorp.com
SMTP_PASS=Mahyar.SR4@ SMTP_PASS=Mahyar.SR4@
MAIL_FROM=no-reply <no-reply@danakcorp.com> 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"`,
);
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"folders": [
{
"path": "."
},
{
"path": "../danak-admin"
}
],
"settings": {}
}
+10
View File
@@ -330,6 +330,12 @@ export const enum TicketMessageEnum {
INVALID_TICKET_LIMIT = "محدودیت تعداد تیکت در پلن پشتیبانی شما نامعتبر است", INVALID_TICKET_LIMIT = "محدودیت تعداد تیکت در پلن پشتیبانی شما نامعتبر است",
TICKET_LIMIT_EXCEEDED = "شما به محدودیت تعداد تیکت در پلن پشتیبانی خود رسیده‌اید", TICKET_LIMIT_EXCEEDED = "شما به محدودیت تعداد تیکت در پلن پشتیبانی خود رسیده‌اید",
OPENED = "تیکت با موفقیت باز شد", 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 { export const enum WalletMessage {
@@ -963,3 +969,7 @@ export const enum timeMessage {
TIME_ALREADY_STARTED = "برای این تسک یک زمان در حال اجرا وجود دارد!", TIME_ALREADY_STARTED = "برای این تسک یک زمان در حال اجرا وجود دارد!",
NO_RUNNING_TIME = "زمان در حال اجرایی برای این تسک پیدا نشد!" NO_RUNNING_TIME = "زمان در حال اجرایی برای این تسک پیدا نشد!"
} }
export const enum taskCommentMessage {
TASK_COMMENT_NOT_FOUND = "نظر مورد نظر پیدا نشد!"
}
+11 -5
View File
@@ -5,11 +5,17 @@ import { MailerAsyncOptions } from "@nestjs-modules/mailer/dist/interfaces/maile
export function mailerConfig(): MailerAsyncOptions { export function mailerConfig(): MailerAsyncOptions {
return { return {
inject: [ConfigService], inject: [ConfigService],
useFactory: (configService: ConfigService) => ({ 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;
return {
transport: { transport: {
host: configService.getOrThrow<string>("SMTP_HOST"), host: configService.getOrThrow<string>("SMTP_HOST"),
port: configService.getOrThrow<number>("SMTP_PORT"), port,
secure: true, secure,
requireTLS: !secure,
auth: { auth: {
user: configService.getOrThrow<string>("SMTP_USER"), user: configService.getOrThrow<string>("SMTP_USER"),
pass: configService.getOrThrow<string>("SMTP_PASS"), pass: configService.getOrThrow<string>("SMTP_PASS"),
@@ -18,7 +24,6 @@ export function mailerConfig(): MailerAsyncOptions {
defaults: { defaults: {
from: configService.getOrThrow<string>("MAIL_FROM"), from: configService.getOrThrow<string>("MAIL_FROM"),
}, },
template: { template: {
dir: process.cwd() + "/src/modules/templates/email", dir: process.cwd() + "/src/modules/templates/email",
adapter: new HandlebarsAdapter(), adapter: new HandlebarsAdapter(),
@@ -26,6 +31,7 @@ export function mailerConfig(): MailerAsyncOptions {
strict: true, strict: true,
}, },
}, },
}), };
},
}; };
} }
+3 -2
View File
@@ -7,6 +7,7 @@ import { SSOClientService } from "./sso-client.service";
import { TokensService } from "./tokens.service"; import { TokensService } from "./tokens.service";
import { AuthMessage } from "../../../common/enums/message.enum"; import { AuthMessage } from "../../../common/enums/message.enum";
import { User } from "../../users/entities/user.entity"; import { User } from "../../users/entities/user.entity";
import { getPermissionsFromRoles } from "../../users/utils/role-permissions.util";
import { ITokenPayload } from "../interfaces/IToken-payload"; import { ITokenPayload } from "../interfaces/IToken-payload";
@Injectable() @Injectable()
@@ -46,7 +47,7 @@ export class SSOService {
const payload: ITokenPayload = { const payload: ITokenPayload = {
id: user.id, id: user.id,
isAdmin: user.roles.some((r) => r.isAdmin), 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, audience: clientId,
}; };
@@ -105,7 +106,7 @@ export class SSOService {
const payload: ITokenPayload = { const payload: ITokenPayload = {
id: user.id, id: user.id,
isAdmin: user.roles.some((r) => r.isAdmin), 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, audience: clientId,
}; };
+2 -1
View File
@@ -9,6 +9,7 @@ import { DataSource, QueryRunner } from "typeorm";
import { AuthMessage } from "../../../common/enums/message.enum"; import { AuthMessage } from "../../../common/enums/message.enum";
import { User } from "../../users/entities/user.entity"; import { User } from "../../users/entities/user.entity";
import { RefreshTokensRepository } from "../../users/repositories/refresh-token.repository"; import { RefreshTokensRepository } from "../../users/repositories/refresh-token.repository";
import { getPermissionsFromRoles } from "../../users/utils/role-permissions.util";
import { ITokenPayload } from "../interfaces/IToken-payload"; import { ITokenPayload } from "../interfaces/IToken-payload";
import { ResellerType } from "../DTO/verify-reseller-otp.dto"; import { ResellerType } from "../DTO/verify-reseller-otp.dto";
@@ -28,7 +29,7 @@ export class TokensService {
{ {
id: user.id, id: user.id,
isAdmin: user.roles.some((r) => r.isAdmin), 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, queryRunner,
); );
+7 -7
View File
@@ -149,7 +149,7 @@ export class BlogsController {
@ApiOperation({ summary: "Get all comments for a blog (admin route)" }) @ApiOperation({ summary: "Get all comments for a blog (admin route)" })
@AdminRoute() @AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_READ) @PermissionsDec(PermissionEnum.BLOG_COMMENTS_READ)
@Get(":id/comments") @Get(":id/comments")
getBlogCommentsForAdmin(@Param() paramDto: ParamDto) { getBlogCommentsForAdmin(@Param() paramDto: ParamDto) {
return this.blogCommentService.getBlogCommentsForAdmin(paramDto.id); return this.blogCommentService.getBlogCommentsForAdmin(paramDto.id);
@@ -157,7 +157,7 @@ export class BlogsController {
@ApiOperation({ summary: "Get all comments for a blog (admin route)" }) @ApiOperation({ summary: "Get all comments for a blog (admin route)" })
@AdminRoute() @AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_READ) @PermissionsDec(PermissionEnum.BLOG_COMMENTS_READ)
@Get("comments") @Get("comments")
getAllCommentForAdmin(@Query() queryDto: BlogCommentQueryDto) { getAllCommentForAdmin(@Query() queryDto: BlogCommentQueryDto) {
return this.blogCommentService.getAllCommentForAdmin(queryDto); return this.blogCommentService.getAllCommentForAdmin(queryDto);
@@ -165,7 +165,7 @@ export class BlogsController {
@ApiOperation({ summary: "Update the status of a comment (admin route)" }) @ApiOperation({ summary: "Update the status of a comment (admin route)" })
@AdminRoute() @AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_UPDATE) @PermissionsDec(PermissionEnum.BLOG_COMMENTS_UPDATE)
@Patch("comments/:id/status") @Patch("comments/:id/status")
updateCommentStatus(@Param() paramDto: ParamDto, @Body() updateCommentStatusDto: UpdateCommentStatusDto) { updateCommentStatus(@Param() paramDto: ParamDto, @Body() updateCommentStatusDto: UpdateCommentStatusDto) {
return this.blogCommentService.updateCommentStatus(paramDto.id, updateCommentStatusDto); return this.blogCommentService.updateCommentStatus(paramDto.id, updateCommentStatusDto);
@@ -196,7 +196,7 @@ export class BlogsController {
@ApiOperation({ summary: "Create a new category (admin route)" }) @ApiOperation({ summary: "Create a new category (admin route)" })
@Post("categories") @Post("categories")
@AdminRoute() @AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_CREATE) @PermissionsDec(PermissionEnum.BLOG_CATEGORIES_CREATE)
createCategory(@Body() createCategoryDto: CreateBlogCategoryDto) { createCategory(@Body() createCategoryDto: CreateBlogCategoryDto) {
return this.blogsService.createCategory(createCategoryDto); return this.blogsService.createCategory(createCategoryDto);
} }
@@ -204,7 +204,7 @@ export class BlogsController {
@ApiOperation({ summary: "Update a category (admin route)" }) @ApiOperation({ summary: "Update a category (admin route)" })
@Patch("categories/:id") @Patch("categories/:id")
@AdminRoute() @AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_UPDATE) @PermissionsDec(PermissionEnum.BLOG_CATEGORIES_UPDATE)
updateCategory(@Param() paramDto: ParamDto, @Body() updateCategoryDto: UpdateBlogCategoryDto) { updateCategory(@Param() paramDto: ParamDto, @Body() updateCategoryDto: UpdateBlogCategoryDto) {
return this.blogsService.updateCategory(paramDto.id, updateCategoryDto); return this.blogsService.updateCategory(paramDto.id, updateCategoryDto);
} }
@@ -212,13 +212,13 @@ export class BlogsController {
@ApiOperation({ summary: "Delete a category (admin route)" }) @ApiOperation({ summary: "Delete a category (admin route)" })
@Delete("categories/:id") @Delete("categories/:id")
@AdminRoute() @AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_DELETE) @PermissionsDec(PermissionEnum.BLOG_CATEGORIES_DELETE)
deleteCategory(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { deleteCategory(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
return this.blogsService.deleteCategory(paramDto.id, userIpAndHeaders); return this.blogsService.deleteCategory(paramDto.id, userIpAndHeaders);
} }
@ApiOperation({ summary: "toggle status of categories (admin route)" }) @ApiOperation({ summary: "toggle status of categories (admin route)" })
@PermissionsDec(PermissionEnum.BLOGS_CREATE) @PermissionsDec(PermissionEnum.BLOG_CATEGORIES_UPDATE)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post("categories/toggle-status/:id") @Post("categories/toggle-status/:id")
toggleCategoryStatus(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { toggleCategoryStatus(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
+5 -5
View File
@@ -18,7 +18,7 @@ export class SlidersController {
@ApiOperation({ summary: "create slider (admin route)" }) @ApiOperation({ summary: "create slider (admin route)" })
@AdminRoute() @AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_CREATE) @PermissionsDec(PermissionEnum.SLIDERS_CREATE)
@Post() @Post()
create(@Body() createSliderDto: CreateSliderDto) { create(@Body() createSliderDto: CreateSliderDto) {
return this.slidersService.create(createSliderDto); return this.slidersService.create(createSliderDto);
@@ -26,7 +26,7 @@ export class SlidersController {
@ApiOperation({ summary: "Get all sliders (admin route)" }) @ApiOperation({ summary: "Get all sliders (admin route)" })
@AdminRoute() @AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_READ) @PermissionsDec(PermissionEnum.SLIDERS_READ)
@Get() @Get()
getSlidersList(@Query() queryDto: PaginationDto) { getSlidersList(@Query() queryDto: PaginationDto) {
return this.slidersService.getSlidersList(queryDto); return this.slidersService.getSlidersList(queryDto);
@@ -34,7 +34,7 @@ export class SlidersController {
@ApiOperation({ summary: "Get a slider by id (admin route)" }) @ApiOperation({ summary: "Get a slider by id (admin route)" })
@AdminRoute() @AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_READ) @PermissionsDec(PermissionEnum.SLIDERS_READ)
@Get(":id") @Get(":id")
getSliderById(@Param() paramDto: ParamDto) { getSliderById(@Param() paramDto: ParamDto) {
return this.slidersService.getSliderById(paramDto.id); return this.slidersService.getSliderById(paramDto.id);
@@ -42,7 +42,7 @@ export class SlidersController {
@ApiOperation({ summary: "Update a slider (admin route)" }) @ApiOperation({ summary: "Update a slider (admin route)" })
@AdminRoute() @AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_UPDATE) @PermissionsDec(PermissionEnum.SLIDERS_UPDATE)
@Patch(":id") @Patch(":id")
update(@Param() paramDto: ParamDto, @Body() updateSliderDto: UpdateSliderDto) { update(@Param() paramDto: ParamDto, @Body() updateSliderDto: UpdateSliderDto) {
return this.slidersService.update(paramDto.id, updateSliderDto); return this.slidersService.update(paramDto.id, updateSliderDto);
@@ -50,7 +50,7 @@ export class SlidersController {
@ApiOperation({ summary: "Delete a slider (admin route)" }) @ApiOperation({ summary: "Delete a slider (admin route)" })
@AdminRoute() @AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_DELETE) @PermissionsDec(PermissionEnum.SLIDERS_DELETE)
@Delete(":id") @Delete(":id")
remove(@Param() paramDto: ParamDto) { remove(@Param() paramDto: ParamDto) {
return this.slidersService.remove(paramDto.id); 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 { AuthGuards } from "../../../common/decorators/auth-guard.decorator";
import { ParamDto } from "../../../common/DTO/param.dto"; import { ParamDto } from "../../../common/DTO/param.dto";
import { ITokenPayload } from "../../auth/interfaces/IToken-payload"; import { ITokenPayload } from "../../auth/interfaces/IToken-payload";
import { User } from "../../users/entities/user.entity";
@AuthGuards() @AuthGuards()
@AdminRoute() @AdminRoute()
@@ -80,4 +81,11 @@ export class ProjectController {
getProjectDetail(@Param() paramDto: ParamDto, @UserDec() user: ITokenPayload): Promise<TMProject | null> { getProjectDetail(@Param() paramDto: ParamDto, @UserDec() user: ITokenPayload): Promise<TMProject | null> {
return this.projectService.getProjectDetail(paramDto.id, user.id, user.permissions); 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 { UserDec } from '../../../common/decorators/user.decorator';
import { AuthGuards } from '../../../common/decorators/auth-guard.decorator'; import { AuthGuards } from '../../../common/decorators/auth-guard.decorator';
import { ITokenPayload } from '../../auth/interfaces/IToken-payload'; import { ITokenPayload } from '../../auth/interfaces/IToken-payload';
import { ParamDto } from '../../../common/DTO/param.dto';
import { User } from '../../users/entities/user.entity';
@AuthGuards() @AuthGuards()
@AdminRoute() @AdminRoute()
@@ -28,6 +30,14 @@ export class WorkspaceController {
return this.workspaceService.findOneOrFail(id); 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() @Post()
@PermissionsDec(PermissionEnum.WORKSPACE_CREATE) @PermissionsDec(PermissionEnum.WORKSPACE_CREATE)
@ApiOperation({ summary: 'Create a new workspace' }) @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() @IsNotEmpty()
taskPhaseId: string; taskPhaseId: string;
@ApiProperty({
description: "ID of the related ticket",
required: false,
example: "c3d4e5f6-a7b8-9012-cdef-123456789012",
})
@IsUUID()
@IsOptional()
ticketId?: string;
@ApiProperty({ @ApiProperty({
description: "IDs of users to assign to the task", description: "IDs of users to assign to the task",
required: false, 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 { TMAttachment } from "./attachment.entity";
import { TMCheckListItem } from "./check-list-item.entity"; import { TMCheckListItem } from "./check-list-item.entity";
import { TMTime } from "./time.entity"; import { TMTime } from "./time.entity";
import { TMTaskComment } from "./task-comment.entity";
import { User } from "../../users/entities/user.entity"; import { User } from "../../users/entities/user.entity";
import { Ticket } from "../../tickets/entities/ticket.entity";
@Entity("tm_tasks") @Entity("tm_tasks")
export class TMTask extends BaseEntity { export class TMTask extends BaseEntity {
@@ -27,6 +29,13 @@ export class TMTask extends BaseEntity {
@Column({type: 'int', default: 1}) @Column({type: 'int', default: 1})
priority: number; priority: number;
@ManyToOne(() => Ticket, { nullable: true, onDelete: "SET NULL" })
@JoinColumn({ name: "ticketId" })
ticket: Ticket | null;
@Column({ type: "uuid", nullable: true })
ticketId: string | null;
// --- Relations --- // --- Relations ---
@ManyToOne(() => TMTaskPhase, (taskPhase) => taskPhase.tasks, { @ManyToOne(() => TMTaskPhase, (taskPhase) => taskPhase.tasks, {
@@ -59,6 +68,11 @@ export class TMTask extends BaseEntity {
}) })
times: TMTime[]; times: TMTime[];
@OneToMany(() => TMTaskComment, (comment) => comment.task, {
cascade: ["insert", "update"],
})
comments: TMTaskComment[];
@ManyToMany(() => User, (user) => user.tasks) @ManyToMany(() => User, (user) => user.tasks)
@JoinTable({ @JoinTable({
name: "tm_task_users", name: "tm_task_users",
@@ -31,7 +31,8 @@ export class ProjectService {
users: { users: {
id: true, id: true,
firstName: true, firstName: true,
lastName: true lastName: true,
profilePic: true,
}, },
taskPhases: { taskPhases: {
id: true, id: true,
@@ -165,4 +166,9 @@ export class ProjectService {
return projectDetail; 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 { UpdateTaskDto } from "../dto/task/update-task.dto";
import { ChangeTaskPhaseDto } from "../dto/task/change-task-phase.dto"; import { ChangeTaskPhaseDto } from "../dto/task/change-task-phase.dto";
import { User } from "../../users/entities/user.entity"; 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 { TaskPhaseRepository } from "../repositories/task-phase.repository";
import { TMTaskPhase } from "../entities/task-phase.entity"; import { TMTaskPhase } from "../entities/task-phase.entity";
import { PermissionEnum } from "../../users/enums/permission.enum"; import { PermissionEnum } from "../../users/enums/permission.enum";
@@ -21,6 +22,8 @@ export class TaskService {
private readonly taskPhaseRepository: TaskPhaseRepository, private readonly taskPhaseRepository: TaskPhaseRepository,
@InjectRepository(User) @InjectRepository(User)
private readonly userRepository: Repository<User>, private readonly userRepository: Repository<User>,
@InjectRepository(Ticket)
private readonly ticketRepository: Repository<Ticket>,
) {} ) {}
async findOneOrFail(id: string): Promise<TMTask> { async findOneOrFail(id: string): Promise<TMTask> {
@@ -29,12 +32,19 @@ export class TaskService {
relations: { relations: {
taskPhase: true, taskPhase: true,
users: true, users: true,
ticket: true,
}, },
}); });
if (!task) throw new NotFoundException(TaskMessage.TASK_NOT_FOUND); if (!task) throw new NotFoundException(TaskMessage.TASK_NOT_FOUND);
return task; 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> { async findTaskPhaseOrFail(taskPhaseId: string): Promise<TMTaskPhase> {
const taskPhase = await this.taskPhaseRepository.findOne({ const taskPhase = await this.taskPhaseRepository.findOne({
where: { id: taskPhaseId }, where: { id: taskPhaseId },
@@ -50,10 +60,14 @@ export class TaskService {
} }
async create(dto: CreateTaskDto): Promise<TMTask> { 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); const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId);
if (ticketId) {
await this.findTicketOrFail(ticketId);
}
if (userIds?.length) { if (userIds?.length) {
const isUsersValid = userIds.every((userId) => taskPhase.project.users.some((user) => user.id === userId)); 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); 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({ const task = this.taskRepository.create({
...rest, ...rest,
ticketId,
startDate: startDate ? new Date(startDate) : undefined, startDate: startDate ? new Date(startDate) : undefined,
endDate: endDate ? new Date(endDate) : undefined, endDate: endDate ? new Date(endDate) : undefined,
}); });
@@ -74,7 +89,11 @@ export class TaskService {
async update(id: string, dto: UpdateTaskDto): Promise<TMTask> { async update(id: string, dto: UpdateTaskDto): Promise<TMTask> {
const task = await this.findOneOrFail(id); 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) { if (dto.taskPhaseId) {
const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId); const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId);
@@ -87,6 +106,7 @@ export class TaskService {
Object.assign(task, { Object.assign(task, {
...rest, ...rest,
...(ticketId !== undefined && { ticketId }),
startDate: startDate ? new Date(startDate) : task.startDate, startDate: startDate ? new Date(startDate) : task.startDate,
endDate: endDate ? new Date(endDate) : task.endDate, endDate: endDate ? new Date(endDate) : task.endDate,
}); });
@@ -71,4 +71,9 @@ export class WorkspaceService {
} }
return await this.workspaceRepository.findByUser(userId); 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.checkListItems", "checkListItem")
.leftJoinAndSelect("task.users", "user") .leftJoinAndSelect("task.users", "user")
.leftJoinAndSelect("task.times", "time") .leftJoinAndSelect("task.times", "time")
.leftJoinAndSelect("task.ticket", "ticket")
.leftJoinAndSelect("time.admin", "timeAdmin") .leftJoinAndSelect("time.admin", "timeAdmin")
.leftJoinAndSelect("task.comments", "comment")
.leftJoinAndSelect("comment.user", "commentUser")
.loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems") .loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems")
@@ -44,6 +47,12 @@ export class TaskRepository extends Repository<TMTask> {
"user.id", "user.id",
"user.firstName", "user.firstName",
"user.lastName", "user.lastName",
"user.profilePic",
"ticket.id",
"ticket.subject",
"ticket.numericId",
"ticket.status",
"time.id", "time.id",
"time.startedAt", "time.startedAt",
@@ -53,6 +62,14 @@ export class TaskRepository extends Repository<TMTask> {
"timeAdmin.id", "timeAdmin.id",
"timeAdmin.firstName", "timeAdmin.firstName",
"timeAdmin.lastName", "timeAdmin.lastName",
"comment.id",
"comment.content",
"comment.attachments",
"comment.createdAt",
"commentUser.firstName",
"commentUser.lastName",
"commentUser.profilePic",
]) ])
.where("task.id = :taskId", { taskId }) .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. // response needs. IN (:...ids) doesn't preserve order, so re-order to match seekQuery.
const detailRows = await this.createQueryBuilder("task") const detailRows = await this.createQueryBuilder("task")
.leftJoinAndSelect("task.remarks", "remark") .leftJoinAndSelect("task.remarks", "remark")
.leftJoinAndSelect("task.ticket", "ticket")
.leftJoinAndSelect("task.users", "user")
.loadRelationCountAndMap("task.attachmentCount", "task.attachments") .loadRelationCountAndMap("task.attachmentCount", "task.attachments")
.loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems") .loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems")
.loadRelationCountAndMap("task.completedCheckListItemCount", "task.checkListItems", "completedCheckListItems", (qb) => .loadRelationCountAndMap("task.completedCheckListItemCount", "task.checkListItems", "completedCheckListItems", (qb) =>
qb.where("completedCheckListItems.isDone = :isDone", { isDone: true }), 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 }) .where("task.id IN (:...ids)", { ids })
.getMany(); .getMany();
@@ -16,7 +16,8 @@ export class WorkspaceRepository extends Repository<TMWorkspace> {
'workspace', 'workspace',
'user.id', 'user.id',
'user.firstName', 'user.firstName',
'user.lastName' 'user.lastName',
'user.profilePic',
]).where('workspace.id = :workspaceId', {workspaceId}) ]).where('workspace.id = :workspaceId', {workspaceId})
.getOne(); .getOne();
} }
@@ -9,10 +9,12 @@ import { TMRemark } from "./entities/remark.entity";
import { TMAttachment } from "./entities/attachment.entity"; import { TMAttachment } from "./entities/attachment.entity";
import { TMCheckListItem } from "./entities/check-list-item.entity"; import { TMCheckListItem } from "./entities/check-list-item.entity";
import { TMTime } from "./entities/time.entity"; import { TMTime } from "./entities/time.entity";
import { TMTaskComment } from "./entities/task-comment.entity";
import { WorkspaceRepository } from "./repositories/workspace.repository"; import { WorkspaceRepository } from "./repositories/workspace.repository";
import { WorkspaceController } from "./controllers/workspace.controller"; import { WorkspaceController } from "./controllers/workspace.controller";
import { WorkspaceService } from "./providers/workspace.service"; import { WorkspaceService } from "./providers/workspace.service";
import { User } from "../users/entities/user.entity"; import { User } from "../users/entities/user.entity";
import { Ticket } from "../tickets/entities/ticket.entity";
import { ProjectController } from "./controllers/project.controller"; import { ProjectController } from "./controllers/project.controller";
import { ProjectRepository } from "./repositories/project.repository"; import { ProjectRepository } from "./repositories/project.repository";
import { ProjectService } from "./providers/project.service"; import { ProjectService } from "./providers/project.service";
@@ -34,10 +36,13 @@ import { AttachmentService } from "./providers/attachment.service";
import { TimeController } from "./controllers/time.controller"; import { TimeController } from "./controllers/time.controller";
import { TimeRepository } from "./repositories/time.repository"; import { TimeRepository } from "./repositories/time.repository";
import { TimeService } from "./providers/time.service"; 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({ @Module({
imports: [ 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: [ controllers: [
WorkspaceController, WorkspaceController,
@@ -48,6 +53,7 @@ import { TimeService } from "./providers/time.service";
RemarkController, RemarkController,
AttachmentController, AttachmentController,
TimeController, TimeController,
TaskCommentController,
], ],
providers: [ providers: [
WorkspaceRepository, WorkspaceRepository,
@@ -66,6 +72,8 @@ import { TimeService } from "./providers/time.service";
AttachmentService, AttachmentService,
TimeRepository, TimeRepository,
TimeService, TimeService,
] TaskCommentRepository,
TaskCommentService,
],
}) })
export class TaskManagerModule {} 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 { AdminsService } from "../../users/providers/admins.service";
import { UsersService } from "../../users/providers/users.service"; import { UsersService } from "../../users/providers/users.service";
import { CreateTicketCategoryDto } from "../DTO/create-ticket-category.dto"; 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 { CreateTicketMessageDto } from "../DTO/create-ticket-message.dto";
import { CreateTicketByAdminDto, CreateTicketDto } from "../DTO/create-ticket.dto"; import { CreateTicketByAdminDto, CreateTicketDto } from "../DTO/create-ticket.dto";
import { ReferTicketDto } from "../DTO/refer-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 { TicketMessage } from "../entities/ticket-message.entity";
import { Ticket } from "../entities/ticket.entity"; import { Ticket } from "../entities/ticket.entity";
import { TicketStatus } from "../enums/ticket-status.enum"; import { TicketStatus } from "../enums/ticket-status.enum";
import { TicketMasterRepository } from "../repositories/ticket-master.repository";
import { TicketCategoryRepository } from "../repositories/tickets-category.repository"; import { TicketCategoryRepository } from "../repositories/tickets-category.repository";
import { TicketMessagesRepository } from "../repositories/tickets-message.repository"; import { TicketMessagesRepository } from "../repositories/tickets-message.repository";
import { TicketsRepository } from "../repositories/tickets.repository"; import { TicketsRepository } from "../repositories/tickets.repository";
import { PermissionEnum } from "../../users/enums/permission.enum";
@Injectable() @Injectable()
export class TicketsService implements OnModuleInit { export class TicketsService implements OnModuleInit {
@@ -42,6 +45,7 @@ export class TicketsService implements OnModuleInit {
@InjectQueue(TICKET.QUEUE_NAME) private readonly ticketQueue: Queue, @InjectQueue(TICKET.QUEUE_NAME) private readonly ticketQueue: Queue,
private readonly ticketsRepository: TicketsRepository, private readonly ticketsRepository: TicketsRepository,
private readonly ticketsCategoryRepository: TicketCategoryRepository, private readonly ticketsCategoryRepository: TicketCategoryRepository,
private readonly ticketMasterRepository: TicketMasterRepository,
private readonly ticketMessagesRepository: TicketMessagesRepository, private readonly ticketMessagesRepository: TicketMessagesRepository,
private readonly usersService: UsersService, private readonly usersService: UsersService,
private readonly adminsService: AdminsService, 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> { private async checkTicketLimit(userId: string, queryRunner: QueryRunner): Promise<void> {
@@ -326,10 +403,10 @@ export class TicketsService implements OnModuleInit {
return { tickets, count, paginate: true }; return { tickets, count, paginate: true };
} }
//******************************** */ //******************************** */
async getTicketForAdmin(queryDto: SearchTicketQueryDto, adminId: string) { async getTicketForAdmin(queryDto: SearchTicketQueryDto, adminPermissions: PermissionEnum[],adminId: string) {
const isSuperAdmin = await this.usersService.isSuperAdmin(adminId); 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 }; 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) { 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; ticket.assignedTo = assignedUser;
await queryRunner.manager.save(Ticket, ticket); await queryRunner.manager.save(Ticket, ticket);
@@ -561,6 +638,22 @@ export class TicketsService implements OnModuleInit {
}); });
await this.notifySuperAdminsForTicket(queryRunner, ticket); 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> { 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 },
});
}
}
+44 -8
View File
@@ -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 { ApiOperation, ApiTags } from "@nestjs/swagger";
import { CreateTicketCategoryDto } from "./DTO/create-ticket-category.dto"; 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 { CreateTicketMessageDto } from "./DTO/create-ticket-message.dto";
import { CreateTicketByAdminDto, CreateTicketDto } from "./DTO/create-ticket.dto"; import { CreateTicketByAdminDto, CreateTicketDto } from "./DTO/create-ticket.dto";
import { ReferTicketDto } from "./DTO/refer-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 { IUserIpAndHeaders, UserDec, UserIpAndHeaders } from "../../common/decorators/user.decorator";
import { ParamDto } from "../../common/DTO/param.dto"; import { ParamDto } from "../../common/DTO/param.dto";
import { PermissionEnum } from "../users/enums/permission.enum"; import { PermissionEnum } from "../users/enums/permission.enum";
import { ITokenPayload } from "../auth/interfaces/IToken-payload";
@Controller("tickets") @Controller("tickets")
@ApiTags("Tickets") @ApiTags("Tickets")
@AuthGuards() @AuthGuards()
@@ -24,14 +27,14 @@ export class TicketsController {
//----------------- ticket categories ------------------- //----------------- ticket categories -------------------
@ApiOperation({ summary: "Create ticket category => admin route" }) @ApiOperation({ summary: "Create ticket category => admin route" })
@PermissionsDec(PermissionEnum.TICKETS_CREATE) @PermissionsDec(PermissionEnum.TICKET_CATEGORIES_CREATE)
@Post("category") @Post("category")
createTicketCategory(@Body() createDto: CreateTicketCategoryDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { createTicketCategory(@Body() createDto: CreateTicketCategoryDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
return this.ticketsService.createTicketCategory(createDto, userIpAndHeaders); return this.ticketsService.createTicketCategory(createDto, userIpAndHeaders);
} }
@ApiOperation({ summary: "Update ticket category => admin route" }) @ApiOperation({ summary: "Update ticket category => admin route" })
@PermissionsDec(PermissionEnum.TICKETS_UPDATE) @PermissionsDec(PermissionEnum.TICKET_CATEGORIES_UPDATE)
@Patch("category/:id") @Patch("category/:id")
updateCategory( updateCategory(
@Param() paramDto: ParamDto, @Param() paramDto: ParamDto,
@@ -49,7 +52,7 @@ export class TicketsController {
@ApiOperation({ summary: "Get ticket categories list => admin route" }) @ApiOperation({ summary: "Get ticket categories list => admin route" })
@AdminRoute() @AdminRoute()
@PermissionsDec(PermissionEnum.TICKETS_READ) @PermissionsDec(PermissionEnum.TICKET_CATEGORIES_READ)
@Get("categories-list") @Get("categories-list")
getCategoriesList(@Query() queryDto: SearchTicketCategoryDto) { getCategoriesList(@Query() queryDto: SearchTicketCategoryDto) {
return this.ticketsService.getCategoriesList(queryDto); return this.ticketsService.getCategoriesList(queryDto);
@@ -58,12 +61,46 @@ export class TicketsController {
@ApiOperation({ summary: "toggle status of categories => admin route" }) @ApiOperation({ summary: "toggle status of categories => admin route" })
@AdminRoute() @AdminRoute()
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@PermissionsDec(PermissionEnum.TICKETS_UPDATE) @PermissionsDec(PermissionEnum.TICKET_CATEGORIES_UPDATE)
@Post("category/toggle-status/:id") @Post("category/toggle-status/:id")
toggleCategoryStatus(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { toggleCategoryStatus(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
return this.ticketsService.toggleCategoryStatus(paramDto, userIpAndHeaders); 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 ------------------- //----------------- ticket and ticket messages -------------------
@ApiOperation({ summary: "create ticket ==> user route" }) @ApiOperation({ summary: "create ticket ==> user route" })
@@ -88,10 +125,9 @@ export class TicketsController {
@ApiOperation({ summary: "Get all tickets ==> admin route" }) @ApiOperation({ summary: "Get all tickets ==> admin route" })
@AdminRoute() @AdminRoute()
@PermissionsDec(PermissionEnum.TICKETS_READ)
@Get("admin") @Get("admin")
getAllTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") adminId: string) { getAllTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec() admin: ITokenPayload) {
return this.ticketsService.getTicketForAdmin(queryDto, adminId); return this.ticketsService.getTicketForAdmin(queryDto, admin.permissions, admin.id);
} }
@ApiOperation({ summary: "create ticket messages" }) @ApiOperation({ summary: "create ticket messages" })
+4 -1
View File
@@ -4,10 +4,12 @@ import { TypeOrmModule } from "@nestjs/typeorm";
import { TICKET } from "./constant"; import { TICKET } from "./constant";
import { TicketCategory } from "./entities/ticket-category.entity"; import { TicketCategory } from "./entities/ticket-category.entity";
import { TicketMaster } from "./entities/ticket-master.entity";
import { TicketMessage } from "./entities/ticket-message.entity"; import { TicketMessage } from "./entities/ticket-message.entity";
import { Ticket } from "./entities/ticket.entity"; import { Ticket } from "./entities/ticket.entity";
import { ReferralStrategyFactory } from "./providers/referral-strategy.factory"; import { ReferralStrategyFactory } from "./providers/referral-strategy.factory";
import { TicketsService } from "./providers/tickets.service"; import { TicketsService } from "./providers/tickets.service";
import { TicketMasterRepository } from "./repositories/ticket-master.repository";
import { TicketCategoryRepository } from "./repositories/tickets-category.repository"; import { TicketCategoryRepository } from "./repositories/tickets-category.repository";
import { TicketMessagesRepository } from "./repositories/tickets-message.repository"; import { TicketMessagesRepository } from "./repositories/tickets-message.repository";
import { TicketsRepository } from "./repositories/tickets.repository"; import { TicketsRepository } from "./repositories/tickets.repository";
@@ -27,7 +29,7 @@ import { LoggerModule } from "../logger/logger.module";
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Ticket, TicketCategory, TicketMessageAttachment, TicketMessage]), TypeOrmModule.forFeature([Ticket, TicketCategory, TicketMaster, TicketMessageAttachment, TicketMessage]),
AccessLogsModule, AccessLogsModule,
BullModule.registerQueue({ name: TICKET.QUEUE_NAME }), BullModule.registerQueue({ name: TICKET.QUEUE_NAME }),
UsersModule, UsersModule,
@@ -48,6 +50,7 @@ import { LoggerModule } from "../logger/logger.module";
TicketsService, TicketsService,
TicketsRepository, TicketsRepository,
TicketCategoryRepository, TicketCategoryRepository,
TicketMasterRepository,
TicketMessagesRepository, TicketMessagesRepository,
TicketMessageAttachmentsRepository, TicketMessageAttachmentsRepository,
TicketProcessor, TicketProcessor,
+6 -4
View File
@@ -1,5 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; 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"; import { AdminMessage, AuthMessage } from "../../../common/enums/message.enum";
@@ -22,9 +22,11 @@ export class CreateAdminDto {
phone: string; phone: string;
@IsNotEmpty({ message: AdminMessage.ROLE_REQUIRED }) @IsNotEmpty({ message: AdminMessage.ROLE_REQUIRED })
@IsUUID("4", { message: AdminMessage.ROLE_ID_SHOULD_BE_UUID }) @IsArray({ message: AdminMessage.ROLE_REQUIRED })
@ApiProperty({ description: "Role id", example: "123e4567-e89b-12d3-a456-426614174000" }) @ArrayNotEmpty({ message: AdminMessage.ROLE_REQUIRED })
roleId: string; @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 }) @IsNotEmpty({ message: AuthMessage.FIRST_NAME_NOT_EMPTY })
@IsString({ 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_UPDATE]: { title: "ویرایش تیکت‌ها", group: PermissionGroupEnum.SUPPORT },
[PermissionEnum.TICKETS_DELETE]: { 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_CREATE]: { title: "ایجاد انتقادات", group: PermissionGroupEnum.OTHER },
[PermissionEnum.CRITICISMS_READ]: { title: "مشاهده انتقادات", group: PermissionGroupEnum.OTHER }, [PermissionEnum.CRITICISMS_READ]: { title: "مشاهده انتقادات", group: PermissionGroupEnum.OTHER },
[PermissionEnum.CRITICISMS_UPDATE]: { 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_UPDATE]: { title: "ویرایش بلاگ‌ها", group: PermissionGroupEnum.CONTENT },
[PermissionEnum.BLOGS_DELETE]: { 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_CREATE]: { title: "ایجاد آموزش‌ها", group: PermissionGroupEnum.CONTENT },
[PermissionEnum.LEARNINGS_READ]: { title: "مشاهده آموزش‌ها", group: PermissionGroupEnum.CONTENT }, [PermissionEnum.LEARNINGS_READ]: { title: "مشاهده آموزش‌ها", group: PermissionGroupEnum.CONTENT },
[PermissionEnum.LEARNINGS_UPDATE]: { title: "ویرایش آموزش‌ها", group: PermissionGroupEnum.CONTENT }, [PermissionEnum.LEARNINGS_UPDATE]: { title: "ویرایش آموزش‌ها", group: PermissionGroupEnum.CONTENT },
@@ -53,6 +53,18 @@ export enum PermissionEnum {
TICKETS_UPDATE = "tickets_update", TICKETS_UPDATE = "tickets_update",
TICKETS_DELETE = "tickets_delete", 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
CRITICISMS_CREATE = "criticisms_create", CRITICISMS_CREATE = "criticisms_create",
CRITICISMS_READ = "criticisms_read", CRITICISMS_READ = "criticisms_read",
@@ -83,6 +95,24 @@ export enum PermissionEnum {
BLOGS_UPDATE = "blogs_update", BLOGS_UPDATE = "blogs_update",
BLOGS_DELETE = "blogs_delete", 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
LEARNINGS_CREATE = "learnings_create", LEARNINGS_CREATE = "learnings_create",
LEARNINGS_READ = "learnings_read", LEARNINGS_READ = "learnings_read",
+23 -19
View File
@@ -18,6 +18,7 @@ import { RoleEnum } from "../enums/role.enum";
import { PermissionsRepository } from "../repositories/permissions.repository"; import { PermissionsRepository } from "../repositories/permissions.repository";
import { RoleRepository } from "../repositories/roles.repository"; import { RoleRepository } from "../repositories/roles.repository";
import { UserRepository } from "../repositories/users.repository"; import { UserRepository } from "../repositories/users.repository";
import { getPermissionEntitiesFromRoles } from "../utils/role-permissions.util";
@Injectable() @Injectable()
export class AdminsService { export class AdminsService {
@@ -43,22 +44,22 @@ export class AdminsService {
const existPhone = await queryRunner.manager.findOneBy(this.userRepository.target, { phone: createDto.phone }); const existPhone = await queryRunner.manager.findOneBy(this.userRepository.target, { phone: createDto.phone });
if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST); if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST);
const role = await queryRunner.manager.findOne(this.rolesRepository.target, { const roles = await queryRunner.manager.find(this.rolesRepository.target, {
where: { id: createDto.roleId }, where: { id: In(createDto.roleIds), isAdmin: true },
relations: { permissions: 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); if (createDto.password !== createDto.repeatPassword) throw new BadRequestException(AdminMessage.PASSWORD_NOT_MATCH);
const hashedPassword = await this.passwordService.hashPassword(createDto.password); const hashedPassword = await this.passwordService.hashPassword(createDto.password);
const userName = slugify(`${createDto.firstName} ${Date.now().toString().slice(-5)}`, { lower: true, trim: true }); 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, { const adminUser = queryRunner.manager.create(this.userRepository.target, {
...createDto, ...adminData,
roles: [role], roles,
password: hashedPassword, password: hashedPassword,
userName, userName,
}); });
@@ -75,7 +76,7 @@ export class AdminsService {
userAgent: userIpAndHeaders.headers["user-agent"], userAgent: userIpAndHeaders.headers["user-agent"],
actionDescription: `Admin created new admin: ${createDto.email}`, actionDescription: `Admin created new admin: ${createDto.email}`,
metadata: { metadata: {
roleId: createDto.roleId, roleIds: createDto.roleIds,
createdBy: "admin", createdBy: "admin",
email: createDto.email, email: createDto.email,
phone: createDto.phone, phone: createDto.phone,
@@ -118,16 +119,14 @@ export class AdminsService {
if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST); if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST);
} }
if (updateDto.roleId) { if (updateDto.roleIds?.length) {
const role = await queryRunner.manager.findOne(this.rolesRepository.target, { const roles = await queryRunner.manager.find(this.rolesRepository.target, {
where: { id: updateDto.roleId }, where: { id: In(updateDto.roleIds), isAdmin: true },
relations: { permissions: 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 = roles;
admin.roles = [role];
await queryRunner.manager.save(this.userRepository.target, admin); await queryRunner.manager.save(this.userRepository.target, admin);
} }
@@ -138,7 +137,8 @@ export class AdminsService {
admin.password = await this.passwordService.hashPassword(updateDto.password); 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 queryRunner.commitTransaction();
await this.accessLogService.logUpdate("User", admin.id, UserType.ADMIN, { await this.accessLogService.logUpdate("User", admin.id, UserType.ADMIN, {
@@ -150,7 +150,7 @@ export class AdminsService {
userType: UserType.ADMIN, userType: UserType.ADMIN,
actionDescription: `Admin updated admin: ${admin.email}`, actionDescription: `Admin updated admin: ${admin.email}`,
metadata: { metadata: {
roleId: updateDto.roleId, roleIds: updateDto.roleIds,
}, },
oldValues: JSON.stringify({ ...admin }), oldValues: JSON.stringify({ ...admin }),
newValues: JSON.stringify({ ...updateDto }), newValues: JSON.stringify({ ...updateDto }),
@@ -169,7 +169,7 @@ export class AdminsService {
/************************************************************ */ /************************************************************ */
async deleteAdmin(id: string, userIpAndHeaders: IUserIpAndHeaders) { 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); if (!admin) throw new BadRequestException(AdminMessage.ADMIN_NOT_FOUND);
await this.userRepository.softDelete(id); await this.userRepository.softDelete(id);
@@ -182,7 +182,7 @@ export class AdminsService {
userAgent: userIpAndHeaders.headers["user-agent"], userAgent: userIpAndHeaders.headers["user-agent"],
actionDescription: `Admin deleted admin: ${admin.email}`, actionDescription: `Admin deleted admin: ${admin.email}`,
metadata: { metadata: {
roleId: admin.roles[0].id, roleIds: admin.roles.map((role) => role.id),
email: admin.email, email: admin.email,
phone: admin.phone, phone: admin.phone,
}, },
@@ -277,7 +277,11 @@ export class AdminsService {
paginate: true, paginate: true,
}; };
} }
/************************************************************ */
async getAllAdmins() {
return this.userRepository.find({ where: { roles: { isAdmin: true } } });
}
/************************************************************ */ /************************************************************ */
async getAdminById(id: string) { async getAdminById(id: string) {
@@ -303,7 +307,7 @@ export class AdminsService {
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
return { 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 { DataSource, Not } from "typeorm";
import { IUserIpAndHeaders } from "../../../common/decorators/user.decorator"; 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 { UserType } from "../../access-logs/enums/user-type.enum";
import { AccessLogService } from "../../access-logs/providers/access-log.service"; import { AccessLogService } from "../../access-logs/providers/access-log.service";
import { Address } from "../../address/entities/address.entity"; import { Address } from "../../address/entities/address.entity";
@@ -135,6 +135,7 @@ export class CustomersService {
password, password,
phone, phone,
birthDate, birthDate,
nationalCode,
}; };
// const addressUpdateData = { // const addressUpdateData = {
@@ -169,7 +170,7 @@ export class CustomersService {
if (nationalCode) { if (nationalCode) {
const existingNationalCode = await queryRunner.manager.findOne(this.realUserRepository.target, { 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); if (existingNationalCode) throw new BadRequestException(UserMessage.NATIONAL_CODE_EXIST);
} }
@@ -180,13 +181,56 @@ export class CustomersService {
await queryRunner.manager.update(this.userRepository.target, { id: customerId }, { password: hashPassword }); await queryRunner.manager.update(this.userRepository.target, { id: customerId }, { password: hashPassword });
} }
if (nationalCode) {
const realUser = await queryRunner.manager.findOne(this.realUserRepository.target, {
where: { user: { id: customerId } },
});
if (realUser) {
await queryRunner.manager.update(this.realUserRepository.target, { id: realUser.id }, { nationalCode });
}
}
const hasLegalUpdateData = Boolean(
registrationName || economicCode || nationalIdentity || registrationCode || postalCode || address || cityId,
);
if (hasLegalUpdateData) {
const legalUser = await queryRunner.manager.findOne(this.legalUserRepository.target, { const legalUser = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { user: { id: customerId } }, where: { user: { id: customerId } },
relations: { address: true }, relations: { address: true },
}); });
if (!legalUser) throw new BadRequestException(UserMessage.LEGAL_DATA_NOT_FOUND); 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) { if (economicCode) {
const existingEconomicCode = await queryRunner.manager.findOne(this.legalUserRepository.target, { const existingEconomicCode = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { economicCode, id: Not(legalUser.id) }, where: { economicCode, id: Not(legalUser.id) },
@@ -217,14 +261,11 @@ export class CustomersService {
await queryRunner.manager.update(this.legalUserRepository.target, { id: legalUser.id }, legalUserUpdateData); await queryRunner.manager.update(this.legalUserRepository.target, { id: legalUser.id }, legalUserUpdateData);
// Update address if needed
if (cityId || address || postalCode) { if (cityId || address || postalCode) {
// Get the address entity
const addressEntity = legalUser.address; const addressEntity = legalUser.address;
if (!addressEntity) throw new BadRequestException(UserMessage.ADDRESS_NOT_FOUND); if (!addressEntity) throw new BadRequestException(UserMessage.ADDRESS_NOT_FOUND);
// Prepare address updates
const addressUpdates: Record<string, unknown> = {}; const addressUpdates: Record<string, unknown> = {};
if (cityId) { if (cityId) {
@@ -242,6 +283,8 @@ export class CustomersService {
await queryRunner.manager.update(Address, { id: addressEntity.id }, addressUpdates); await queryRunner.manager.update(Address, { id: addressEntity.id }, addressUpdates);
} }
}
}
await queryRunner.commitTransaction(); await queryRunner.commitTransaction();
@@ -359,6 +359,7 @@ export class UsersService {
async isSuperAdmin(adminId: string) { async isSuperAdmin(adminId: string) {
const user = await this.userRepository.findOne({ where: { id: adminId }, relations: { roles: true } }); const user = await this.userRepository.findOne({ where: { id: adminId }, relations: { roles: true } });
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); 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); 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"); .loadRelationCountAndMap("user.ticketsCount", "user.tickets");
if (queryDto.q) { if (queryDto.q) {
queryBuilder queryBuilder.andWhere(
.orWhere("user.firstName ILIKE :search", { search: `%${queryDto.q}%` }) `(
.orWhere("user.lastName ILIKE :search", { search: `%${queryDto.q}%` }) user.firstName ILIKE :search
.orWhere("user.userName ILIKE :search", { search: `%${queryDto.q}%` }); 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(); return queryBuilder.getMany();
@@ -166,13 +177,28 @@ export class UserRepository extends Repository<User> {
.loadRelationCountAndMap("user.ticketsCount", "user.tickets"); .loadRelationCountAndMap("user.ticketsCount", "user.tickets");
if (queryDto.q) { if (queryDto.q) {
queryBuilder queryBuilder.andWhere(
.orWhere("user.firstName ILIKE :search", { search: `%${queryDto.q}%` }) `(
.orWhere("user.lastName ILIKE :search", { search: `%${queryDto.q}%` }) user.firstName ILIKE :search
.orWhere("user.userName ILIKE :search", { search: `%${queryDto.q}%` }); 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();
} }
//************* */ //************* */
+7 -1
View File
@@ -171,12 +171,18 @@ export class UsersController {
@ApiOperation({ summary: "get all admins ==> admin route" }) @ApiOperation({ summary: "get all admins ==> admin route" })
@AdminRoute() @AdminRoute()
// @PermissionsDec(PermissionEnum.ADMINS_READ, PermissionEnum.CUSTOMERS_READ)
@Get("admins") @Get("admins")
getAdmins(@Query() queryDto: SearchAdminQueryDto) { getAdmins(@Query() queryDto: SearchAdminQueryDto) {
return this.adminsService.getAdmins(queryDto); 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" }) @ApiOperation({ summary: "get admin by id ==> admin route" })
@PermissionsDec(PermissionEnum.ADMINS_READ, PermissionEnum.CUSTOMERS_READ) @PermissionsDec(PermissionEnum.ADMINS_READ, PermissionEnum.CUSTOMERS_READ)
@Get("admins/:id") @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()];
}
-22
View File
@@ -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!");
});
});
-9
View File
@@ -1,9 +0,0 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}