13 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
41 changed files with 1176 additions and 177 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ SMS_PATTERN_NEW_TICKET_GLOBAL=536596
SMS_PATTERN_NEW_CRITICISM=983724
SMS_PATTERN_USER_PASSWORD=486237
SMTP_HOST=smtps.danakcorp.com
SMTP_PORT=465
SMTP_PORT=587
SMTP_USER=no-reply-danakcorp.com
SMTP_PASS=Mahyar.SR4@
MAIL_FROM=no-reply <no-reply@danakcorp.com>
@@ -0,0 +1,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": {}
}
+6
View File
@@ -330,6 +330,12 @@ export const enum TicketMessageEnum {
INVALID_TICKET_LIMIT = "محدودیت تعداد تیکت در پلن پشتیبانی شما نامعتبر است",
TICKET_LIMIT_EXCEEDED = "شما به محدودیت تعداد تیکت در پلن پشتیبانی خود رسیده‌اید",
OPENED = "تیکت با موفقیت باز شد",
MASTER_USER_ID_REQUIRED = "شناسه کاربر مستر تیکت اجباری است",
MASTER_USER_ID_SHOULD_BE_UUID = "شناسه کاربر مستر تیکت باید یک UUID معتبر باشد",
MASTER_CREATED = "مستر تیکت با موفقیت ایجاد شد",
MASTER_NOT_FOUND = "مستر تیکت یافت نشد",
MASTER_EXIST = "این کاربر قبلاً به عنوان مستر تیکت ثبت شده است",
MASTER_DELETED = "مستر تیکت با موفقیت حذف شد",
}
export const enum WalletMessage {
+26 -20
View File
@@ -5,27 +5,33 @@ import { MailerAsyncOptions } from "@nestjs-modules/mailer/dist/interfaces/maile
export function mailerConfig(): MailerAsyncOptions {
return {
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
transport: {
host: configService.getOrThrow<string>("SMTP_HOST"),
port: configService.getOrThrow<number>("SMTP_PORT"),
secure: true,
auth: {
user: configService.getOrThrow<string>("SMTP_USER"),
pass: configService.getOrThrow<string>("SMTP_PASS"),
},
},
defaults: {
from: configService.getOrThrow<string>("MAIL_FROM"),
},
useFactory: (configService: ConfigService) => {
const port = Number(configService.getOrThrow<number>("SMTP_PORT"));
// 465 = implicit TLS; 587 = STARTTLS (required by zone-mta behind Traefik)
const secure = port === 465;
template: {
dir: process.cwd() + "/src/modules/templates/email",
adapter: new HandlebarsAdapter(),
options: {
strict: true,
return {
transport: {
host: configService.getOrThrow<string>("SMTP_HOST"),
port,
secure,
requireTLS: !secure,
auth: {
user: configService.getOrThrow<string>("SMTP_USER"),
pass: configService.getOrThrow<string>("SMTP_PASS"),
},
},
},
}),
defaults: {
from: configService.getOrThrow<string>("MAIL_FROM"),
},
template: {
dir: process.cwd() + "/src/modules/templates/email",
adapter: new HandlebarsAdapter(),
options: {
strict: true,
},
},
};
},
};
}
+3 -2
View File
@@ -7,6 +7,7 @@ import { SSOClientService } from "./sso-client.service";
import { TokensService } from "./tokens.service";
import { AuthMessage } from "../../../common/enums/message.enum";
import { User } from "../../users/entities/user.entity";
import { getPermissionsFromRoles } from "../../users/utils/role-permissions.util";
import { ITokenPayload } from "../interfaces/IToken-payload";
@Injectable()
@@ -46,7 +47,7 @@ export class SSOService {
const payload: ITokenPayload = {
id: user.id,
isAdmin: user.roles.some((r) => r.isAdmin),
permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])),
permissions: getPermissionsFromRoles(user.roles),
audience: clientId,
};
@@ -105,7 +106,7 @@ export class SSOService {
const payload: ITokenPayload = {
id: user.id,
isAdmin: user.roles.some((r) => r.isAdmin),
permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])),
permissions: getPermissionsFromRoles(user.roles),
audience: clientId,
};
+2 -1
View File
@@ -9,6 +9,7 @@ import { DataSource, QueryRunner } from "typeorm";
import { AuthMessage } from "../../../common/enums/message.enum";
import { User } from "../../users/entities/user.entity";
import { RefreshTokensRepository } from "../../users/repositories/refresh-token.repository";
import { getPermissionsFromRoles } from "../../users/utils/role-permissions.util";
import { ITokenPayload } from "../interfaces/IToken-payload";
import { ResellerType } from "../DTO/verify-reseller-otp.dto";
@@ -28,7 +29,7 @@ export class TokensService {
{
id: user.id,
isAdmin: user.roles.some((r) => r.isAdmin),
permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])),
permissions: getPermissionsFromRoles(user.roles),
},
queryRunner,
);
+7 -7
View File
@@ -149,7 +149,7 @@ export class BlogsController {
@ApiOperation({ summary: "Get all comments for a blog (admin route)" })
@AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_READ)
@PermissionsDec(PermissionEnum.BLOG_COMMENTS_READ)
@Get(":id/comments")
getBlogCommentsForAdmin(@Param() paramDto: ParamDto) {
return this.blogCommentService.getBlogCommentsForAdmin(paramDto.id);
@@ -157,7 +157,7 @@ export class BlogsController {
@ApiOperation({ summary: "Get all comments for a blog (admin route)" })
@AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_READ)
@PermissionsDec(PermissionEnum.BLOG_COMMENTS_READ)
@Get("comments")
getAllCommentForAdmin(@Query() queryDto: BlogCommentQueryDto) {
return this.blogCommentService.getAllCommentForAdmin(queryDto);
@@ -165,7 +165,7 @@ export class BlogsController {
@ApiOperation({ summary: "Update the status of a comment (admin route)" })
@AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_UPDATE)
@PermissionsDec(PermissionEnum.BLOG_COMMENTS_UPDATE)
@Patch("comments/:id/status")
updateCommentStatus(@Param() paramDto: ParamDto, @Body() updateCommentStatusDto: UpdateCommentStatusDto) {
return this.blogCommentService.updateCommentStatus(paramDto.id, updateCommentStatusDto);
@@ -196,7 +196,7 @@ export class BlogsController {
@ApiOperation({ summary: "Create a new category (admin route)" })
@Post("categories")
@AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_CREATE)
@PermissionsDec(PermissionEnum.BLOG_CATEGORIES_CREATE)
createCategory(@Body() createCategoryDto: CreateBlogCategoryDto) {
return this.blogsService.createCategory(createCategoryDto);
}
@@ -204,7 +204,7 @@ export class BlogsController {
@ApiOperation({ summary: "Update a category (admin route)" })
@Patch("categories/:id")
@AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_UPDATE)
@PermissionsDec(PermissionEnum.BLOG_CATEGORIES_UPDATE)
updateCategory(@Param() paramDto: ParamDto, @Body() updateCategoryDto: UpdateBlogCategoryDto) {
return this.blogsService.updateCategory(paramDto.id, updateCategoryDto);
}
@@ -212,13 +212,13 @@ export class BlogsController {
@ApiOperation({ summary: "Delete a category (admin route)" })
@Delete("categories/:id")
@AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_DELETE)
@PermissionsDec(PermissionEnum.BLOG_CATEGORIES_DELETE)
deleteCategory(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
return this.blogsService.deleteCategory(paramDto.id, userIpAndHeaders);
}
@ApiOperation({ summary: "toggle status of categories (admin route)" })
@PermissionsDec(PermissionEnum.BLOGS_CREATE)
@PermissionsDec(PermissionEnum.BLOG_CATEGORIES_UPDATE)
@HttpCode(HttpStatus.OK)
@Post("categories/toggle-status/:id")
toggleCategoryStatus(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
+5 -5
View File
@@ -18,7 +18,7 @@ export class SlidersController {
@ApiOperation({ summary: "create slider (admin route)" })
@AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_CREATE)
@PermissionsDec(PermissionEnum.SLIDERS_CREATE)
@Post()
create(@Body() createSliderDto: CreateSliderDto) {
return this.slidersService.create(createSliderDto);
@@ -26,7 +26,7 @@ export class SlidersController {
@ApiOperation({ summary: "Get all sliders (admin route)" })
@AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_READ)
@PermissionsDec(PermissionEnum.SLIDERS_READ)
@Get()
getSlidersList(@Query() queryDto: PaginationDto) {
return this.slidersService.getSlidersList(queryDto);
@@ -34,7 +34,7 @@ export class SlidersController {
@ApiOperation({ summary: "Get a slider by id (admin route)" })
@AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_READ)
@PermissionsDec(PermissionEnum.SLIDERS_READ)
@Get(":id")
getSliderById(@Param() paramDto: ParamDto) {
return this.slidersService.getSliderById(paramDto.id);
@@ -42,7 +42,7 @@ export class SlidersController {
@ApiOperation({ summary: "Update a slider (admin route)" })
@AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_UPDATE)
@PermissionsDec(PermissionEnum.SLIDERS_UPDATE)
@Patch(":id")
update(@Param() paramDto: ParamDto, @Body() updateSliderDto: UpdateSliderDto) {
return this.slidersService.update(paramDto.id, updateSliderDto);
@@ -50,7 +50,7 @@ export class SlidersController {
@ApiOperation({ summary: "Delete a slider (admin route)" })
@AdminRoute()
@PermissionsDec(PermissionEnum.BLOGS_DELETE)
@PermissionsDec(PermissionEnum.SLIDERS_DELETE)
@Delete(":id")
remove(@Param() paramDto: ParamDto) {
return this.slidersService.remove(paramDto.id);
@@ -12,6 +12,7 @@ import { UserDec } from "../../../common/decorators/user.decorator";
import { AuthGuards } from "../../../common/decorators/auth-guard.decorator";
import { ParamDto } from "../../../common/DTO/param.dto";
import { ITokenPayload } from "../../auth/interfaces/IToken-payload";
import { User } from "../../users/entities/user.entity";
@AuthGuards()
@AdminRoute()
@@ -80,4 +81,11 @@ export class ProjectController {
getProjectDetail(@Param() paramDto: ParamDto, @UserDec() user: ITokenPayload): Promise<TMProject | null> {
return this.projectService.getProjectDetail(paramDto.id, user.id, user.permissions);
}
@Get(":id/users")
@ApiOperation({ summary: "Get project users" })
@ApiResponse({ status: 200, description: "Got Project Users Successfully!" })
getProjectUsers(@Param() paramDto: ParamDto): Promise<User[]> {
return this.projectService.getProjectUsers(paramDto.id);
}
}
@@ -10,6 +10,8 @@ import { PermissionEnum } from '../../users/enums/permission.enum';
import { UserDec } from '../../../common/decorators/user.decorator';
import { AuthGuards } from '../../../common/decorators/auth-guard.decorator';
import { ITokenPayload } from '../../auth/interfaces/IToken-payload';
import { ParamDto } from '../../../common/DTO/param.dto';
import { User } from '../../users/entities/user.entity';
@AuthGuards()
@AdminRoute()
@@ -28,6 +30,14 @@ export class WorkspaceController {
return this.workspaceService.findOneOrFail(id);
}
@Get(':id/users')
@ApiOperation({ summary: 'Get workspace users' })
@ApiResponse({ status: 200, description: 'Got Workspace Users Successfully!' })
@ApiResponse({ status: 404, description: 'Workspace Not Found!' })
getWorkspaceUsers(@Param() paramDto: ParamDto): Promise<User[]> {
return this.workspaceService.getWorkspaceUsers(paramDto.id);
}
@Post()
@PermissionsDec(PermissionEnum.WORKSPACE_CREATE)
@ApiOperation({ summary: 'Create a new workspace' })
@@ -1,5 +1,5 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsString, IsNotEmpty, IsUUID } from "class-validator";
import { IsString, IsNotEmpty, IsUUID, IsOptional, IsArray } from "class-validator";
export class CreateTaskCommentDto {
@ApiProperty({ description: "Comment content", example: "Please review this task before closing." })
@@ -11,4 +11,15 @@ export class CreateTaskCommentDto {
@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[];
}
@@ -8,6 +8,9 @@ export class TMTaskComment extends BaseEntity {
@Column({ type: "text" })
content: string;
@Column({ type: "json", default: [] })
attachments: string[];
// --- Relations ---
@ManyToOne(() => User, {
@@ -30,3 +33,4 @@ export class TMTaskComment extends BaseEntity {
@Column({ type: "uuid" })
taskId: string;
}
@@ -31,7 +31,8 @@ export class ProjectService {
users: {
id: true,
firstName: true,
lastName: true
lastName: true,
profilePic: true,
},
taskPhases: {
id: true,
@@ -165,4 +166,9 @@ export class ProjectService {
return projectDetail;
}
async getProjectUsers(projectId: string): Promise<User[]> {
const project = await this.findOneOrFail(projectId);
return project.users;
}
}
@@ -37,6 +37,7 @@ export class TaskCommentService {
const comment = this.taskCommentRepository.create({
...dto,
attachments: dto.attachments ?? [],
userId,
});
return this.taskCommentRepository.save(comment);
@@ -71,4 +71,9 @@ export class WorkspaceService {
}
return await this.workspaceRepository.findByUser(userId);
}
async getWorkspaceUsers(workspaceId: string): Promise<User[]> {
const workspace = await this.findOneOrFail(workspaceId);
return workspace.users;
}
}
@@ -21,6 +21,8 @@ export class TaskRepository extends Repository<TMTask> {
.leftJoinAndSelect("task.times", "time")
.leftJoinAndSelect("task.ticket", "ticket")
.leftJoinAndSelect("time.admin", "timeAdmin")
.leftJoinAndSelect("task.comments", "comment")
.leftJoinAndSelect("comment.user", "commentUser")
.loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems")
@@ -45,6 +47,7 @@ export class TaskRepository extends Repository<TMTask> {
"user.id",
"user.firstName",
"user.lastName",
"user.profilePic",
"ticket.id",
"ticket.subject",
@@ -59,6 +62,14 @@ export class TaskRepository extends Repository<TMTask> {
"timeAdmin.id",
"timeAdmin.firstName",
"timeAdmin.lastName",
"comment.id",
"comment.content",
"comment.attachments",
"comment.createdAt",
"commentUser.firstName",
"commentUser.lastName",
"commentUser.profilePic",
])
.where("task.id = :taskId", { taskId })
@@ -120,12 +131,26 @@ export class TaskRepository extends Repository<TMTask> {
const detailRows = await this.createQueryBuilder("task")
.leftJoinAndSelect("task.remarks", "remark")
.leftJoinAndSelect("task.ticket", "ticket")
.leftJoinAndSelect("task.users", "user")
.loadRelationCountAndMap("task.attachmentCount", "task.attachments")
.loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems")
.loadRelationCountAndMap("task.completedCheckListItemCount", "task.checkListItems", "completedCheckListItems", (qb) =>
qb.where("completedCheckListItems.isDone = :isDone", { isDone: true }),
)
.select(["task", "remark.id", "remark.title", "remark.color", "ticket.id", "ticket.subject", "ticket.numericId", "ticket.status"])
.select([
"task",
"remark.id",
"remark.title",
"remark.color",
"user.id",
"user.firstName",
"user.lastName",
"user.profilePic",
"ticket.id",
"ticket.subject",
"ticket.numericId",
"ticket.status",
])
.where("task.id IN (:...ids)", { ids })
.getMany();
@@ -16,7 +16,8 @@ export class WorkspaceRepository extends Repository<TMWorkspace> {
'workspace',
'user.id',
'user.firstName',
'user.lastName'
'user.lastName',
'user.profilePic',
]).where('workspace.id = :workspaceId', {workspaceId})
.getOne();
}
@@ -74,6 +74,6 @@ import { TaskCommentService } from "./providers/task-comment.service";
TimeService,
TaskCommentRepository,
TaskCommentService,
]
],
})
export class TaskManagerModule {}
@@ -0,0 +1,11 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsUUID } from "class-validator";
import { TicketMessageEnum } from "../../../common/enums/message.enum";
export class CreateTicketMasterDto {
@IsNotEmpty({ message: TicketMessageEnum.MASTER_USER_ID_REQUIRED })
@IsUUID("4", { message: TicketMessageEnum.MASTER_USER_ID_SHOULD_BE_UUID })
@ApiProperty({ description: "User ID of the ticket master", example: "123e4567-e89b-12d3-a456-426614174000" })
userId: string;
}
@@ -0,0 +1,11 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsUUID } from "class-validator";
import { TicketMessageEnum } from "../../../common/enums/message.enum";
export class UpdateTicketMasterDto {
@IsNotEmpty({ message: TicketMessageEnum.MASTER_USER_ID_REQUIRED })
@IsUUID("4", { message: TicketMessageEnum.MASTER_USER_ID_SHOULD_BE_UUID })
@ApiProperty({ description: "User ID of the ticket master", example: "123e4567-e89b-12d3-a456-426614174000" })
userId: string;
}
@@ -0,0 +1,12 @@
import { Entity, JoinColumn, ManyToOne, Unique } from "typeorm";
import { BaseEntity } from "../../../common/entities/base.entity";
import { User } from "../../users/entities/user.entity";
@Entity("ticket_master")
@Unique(["user"])
export class TicketMaster extends BaseEntity {
@ManyToOne(() => User, { nullable: false, onDelete: "CASCADE" })
@JoinColumn()
user: User;
}
@@ -19,6 +19,7 @@ import { User } from "../../users/entities/user.entity";
import { AdminsService } from "../../users/providers/admins.service";
import { UsersService } from "../../users/providers/users.service";
import { CreateTicketCategoryDto } from "../DTO/create-ticket-category.dto";
import { CreateTicketMasterDto } from "../DTO/create-ticket-master.dto";
import { CreateTicketMessageDto } from "../DTO/create-ticket-message.dto";
import { CreateTicketByAdminDto, CreateTicketDto } from "../DTO/create-ticket.dto";
import { ReferTicketDto } from "../DTO/refer-ticket.dto";
@@ -30,9 +31,11 @@ import { TicketMessageAttachment } from "../entities/ticket-message-attachment";
import { TicketMessage } from "../entities/ticket-message.entity";
import { Ticket } from "../entities/ticket.entity";
import { TicketStatus } from "../enums/ticket-status.enum";
import { TicketMasterRepository } from "../repositories/ticket-master.repository";
import { TicketCategoryRepository } from "../repositories/tickets-category.repository";
import { TicketMessagesRepository } from "../repositories/tickets-message.repository";
import { TicketsRepository } from "../repositories/tickets.repository";
import { PermissionEnum } from "../../users/enums/permission.enum";
@Injectable()
export class TicketsService implements OnModuleInit {
@@ -42,6 +45,7 @@ export class TicketsService implements OnModuleInit {
@InjectQueue(TICKET.QUEUE_NAME) private readonly ticketQueue: Queue,
private readonly ticketsRepository: TicketsRepository,
private readonly ticketsCategoryRepository: TicketCategoryRepository,
private readonly ticketMasterRepository: TicketMasterRepository,
private readonly ticketMessagesRepository: TicketMessagesRepository,
private readonly usersService: UsersService,
private readonly adminsService: AdminsService,
@@ -166,6 +170,79 @@ export class TicketsService implements OnModuleInit {
};
}
//******************************** */
// ticket masters
//******************************** */
async createTicketMaster(createDto: CreateTicketMasterDto, userIpAndHeaders: IUserIpAndHeaders) {
const { user } = await this.usersService.findOneById(createDto.userId);
const existingMaster = await this.ticketMasterRepository.findByUserId(createDto.userId);
if (existingMaster) throw new BadRequestException(TicketMessageEnum.MASTER_EXIST);
const ticketMaster = this.ticketMasterRepository.create({ user });
await this.ticketMasterRepository.save(ticketMaster);
await this.accessLogService.logCreate("TicketMaster", UserType.ADMIN, {
user: { id: userIpAndHeaders.userId },
endpoint: "/tickets/masters",
requestId: userIpAndHeaders.headers["x-request-id"],
ipAddress: userIpAndHeaders.ip,
userAgent: userIpAndHeaders.headers["user-agent"],
actionDescription: `Admin created ticket master for user: ${user.id}`,
metadata: {
ticketMasterId: ticketMaster.id,
masterUserId: user.id,
},
});
return {
message: TicketMessageEnum.MASTER_CREATED,
ticketMaster: await this.ticketMasterRepository.findByIdWithUser(ticketMaster.id),
};
}
//******************************** */
async getTicketMasters() {
const ticketMasters = await this.ticketMasterRepository.findAllWithUser();
return { ticketMasters };
}
//******************************** */
async getTicketMaster(paramDto: ParamDto) {
const ticketMaster = await this.ticketMasterRepository.findByIdWithUser(paramDto.id);
if (!ticketMaster) throw new BadRequestException(TicketMessageEnum.MASTER_NOT_FOUND);
return { ticketMaster };
}
//******************************** */
async deleteTicketMaster(paramDto: ParamDto, userIpAndHeaders: IUserIpAndHeaders) {
const ticketMaster = await this.ticketMasterRepository.findByIdWithUser(paramDto.id);
if (!ticketMaster) throw new BadRequestException(TicketMessageEnum.MASTER_NOT_FOUND);
await this.ticketMasterRepository.delete(paramDto.id);
await this.accessLogService.logDelete("TicketMaster", paramDto.id, UserType.ADMIN, {
user: { id: userIpAndHeaders.userId },
endpoint: "/tickets/masters",
requestId: userIpAndHeaders.headers["x-request-id"],
ipAddress: userIpAndHeaders.ip,
userAgent: userIpAndHeaders.headers["user-agent"],
actionDescription: `Admin deleted ticket master: ${paramDto.id}`,
metadata: {
ticketMasterId: ticketMaster.id,
masterUserId: ticketMaster.user.id,
},
});
return {
message: TicketMessageEnum.MASTER_DELETED,
};
}
//******************************** */
private async checkTicketLimit(userId: string, queryRunner: QueryRunner): Promise<void> {
@@ -326,10 +403,10 @@ export class TicketsService implements OnModuleInit {
return { tickets, count, paginate: true };
}
//******************************** */
async getTicketForAdmin(queryDto: SearchTicketQueryDto, adminId: string) {
const isSuperAdmin = await this.usersService.isSuperAdmin(adminId);
async getTicketForAdmin(queryDto: SearchTicketQueryDto, adminPermissions: PermissionEnum[],adminId: string) {
const canSeeAllTickets = adminPermissions.includes(PermissionEnum.TICKETS_READ);
const [tickets, count] = await this.ticketsRepository.findTicketsListForAdmin(queryDto, adminId, isSuperAdmin);
const [tickets, count] = await this.ticketsRepository.findTicketsListForAdmin(queryDto, adminId, canSeeAllTickets);
return { tickets, count, paginate: true };
}
@@ -538,7 +615,7 @@ export class TicketsService implements OnModuleInit {
//***************************** */
private async assignTicketAndNotify(ticket: Ticket, ticketCategory: TicketCategory, user: User, queryRunner: QueryRunner) {
const assignedUser = await this.referralService.assignAdmin(ticketCategory.group);
const assignedUser = (await this.assignFromTicketMasters()) ?? (await this.referralService.assignAdmin(ticketCategory.group));
//
ticket.assignedTo = assignedUser;
await queryRunner.manager.save(Ticket, ticket);
@@ -561,6 +638,22 @@ export class TicketsService implements OnModuleInit {
});
await this.notifySuperAdminsForTicket(queryRunner, ticket);
}
//***************************** */
private async assignFromTicketMasters(): Promise<User | null> {
const masters = await this.ticketMasterRepository.findAllWithUser();
if (!masters.length) return null;
const mastersWithLoad = await Promise.all(
masters.map(async (master) => ({
user: master.user,
assignedCount: await this.ticketsRepository.count({ where: { assignedTo: { id: master.user.id } } }),
})),
);
return mastersWithLoad.reduce((prev, curr) => (prev.assignedCount < curr.assignedCount ? prev : curr)).user;
}
//***************************** */
private async findTicket(queryRunner: QueryRunner, ticketId: string, userId: string, isAdmin: boolean): Promise<Ticket> {
@@ -0,0 +1,33 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { TicketMaster } from "../entities/ticket-master.entity";
@Injectable()
export class TicketMasterRepository extends Repository<TicketMaster> {
constructor(@InjectRepository(TicketMaster) ticketMasterRepository: Repository<TicketMaster>) {
super(ticketMasterRepository.target, ticketMasterRepository.manager, ticketMasterRepository.queryRunner);
}
findAllWithUser(): Promise<TicketMaster[]> {
return this.find({
relations: { user: true },
order: { createdAt: "ASC" },
});
}
findByIdWithUser(id: string): Promise<TicketMaster | null> {
return this.findOne({
where: { id },
relations: { user: true },
});
}
findByUserId(userId: string): Promise<TicketMaster | null> {
return this.findOne({
where: { user: { id: userId } },
relations: { user: true },
});
}
}
+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 { CreateTicketCategoryDto } from "./DTO/create-ticket-category.dto";
import { CreateTicketMasterDto } from "./DTO/create-ticket-master.dto";
import { CreateTicketMessageDto } from "./DTO/create-ticket-message.dto";
import { CreateTicketByAdminDto, CreateTicketDto } from "./DTO/create-ticket.dto";
import { ReferTicketDto } from "./DTO/refer-ticket.dto";
@@ -15,6 +16,8 @@ import { PermissionsDec } from "../../common/decorators/permission.decorator";
import { IUserIpAndHeaders, UserDec, UserIpAndHeaders } from "../../common/decorators/user.decorator";
import { ParamDto } from "../../common/DTO/param.dto";
import { PermissionEnum } from "../users/enums/permission.enum";
import { ITokenPayload } from "../auth/interfaces/IToken-payload";
@Controller("tickets")
@ApiTags("Tickets")
@AuthGuards()
@@ -24,14 +27,14 @@ export class TicketsController {
//----------------- ticket categories -------------------
@ApiOperation({ summary: "Create ticket category => admin route" })
@PermissionsDec(PermissionEnum.TICKETS_CREATE)
@PermissionsDec(PermissionEnum.TICKET_CATEGORIES_CREATE)
@Post("category")
createTicketCategory(@Body() createDto: CreateTicketCategoryDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
return this.ticketsService.createTicketCategory(createDto, userIpAndHeaders);
}
@ApiOperation({ summary: "Update ticket category => admin route" })
@PermissionsDec(PermissionEnum.TICKETS_UPDATE)
@PermissionsDec(PermissionEnum.TICKET_CATEGORIES_UPDATE)
@Patch("category/:id")
updateCategory(
@Param() paramDto: ParamDto,
@@ -49,7 +52,7 @@ export class TicketsController {
@ApiOperation({ summary: "Get ticket categories list => admin route" })
@AdminRoute()
@PermissionsDec(PermissionEnum.TICKETS_READ)
@PermissionsDec(PermissionEnum.TICKET_CATEGORIES_READ)
@Get("categories-list")
getCategoriesList(@Query() queryDto: SearchTicketCategoryDto) {
return this.ticketsService.getCategoriesList(queryDto);
@@ -58,12 +61,46 @@ export class TicketsController {
@ApiOperation({ summary: "toggle status of categories => admin route" })
@AdminRoute()
@HttpCode(HttpStatus.OK)
@PermissionsDec(PermissionEnum.TICKETS_UPDATE)
@PermissionsDec(PermissionEnum.TICKET_CATEGORIES_UPDATE)
@Post("category/toggle-status/:id")
toggleCategoryStatus(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
return this.ticketsService.toggleCategoryStatus(paramDto, userIpAndHeaders);
}
//----------------- ticket masters -------------------
@ApiOperation({ summary: "Create ticket master => admin route" })
@AdminRoute()
@PermissionsDec(PermissionEnum.TICKET_MASTERS_CREATE)
@Post("masters")
createTicketMaster(@Body() createDto: CreateTicketMasterDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
return this.ticketsService.createTicketMaster(createDto, userIpAndHeaders);
}
@ApiOperation({ summary: "Get all ticket masters => admin route" })
@AdminRoute()
@PermissionsDec(PermissionEnum.TICKET_MASTERS_READ)
@Get("masters")
getTicketMasters() {
return this.ticketsService.getTicketMasters();
}
@ApiOperation({ summary: "Get ticket master by id => admin route" })
@AdminRoute()
@PermissionsDec(PermissionEnum.TICKET_MASTERS_READ)
@Get("masters/:id")
getTicketMaster(@Param() paramDto: ParamDto) {
return this.ticketsService.getTicketMaster(paramDto);
}
@ApiOperation({ summary: "Delete ticket master => admin route" })
@AdminRoute()
@PermissionsDec(PermissionEnum.TICKET_MASTERS_DELETE)
@Delete("masters/:id")
deleteTicketMaster(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
return this.ticketsService.deleteTicketMaster(paramDto, userIpAndHeaders);
}
//----------------- ticket and ticket messages -------------------
@ApiOperation({ summary: "create ticket ==> user route" })
@@ -88,10 +125,9 @@ export class TicketsController {
@ApiOperation({ summary: "Get all tickets ==> admin route" })
@AdminRoute()
@PermissionsDec(PermissionEnum.TICKETS_READ)
@Get("admin")
getAllTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") adminId: string) {
return this.ticketsService.getTicketForAdmin(queryDto, adminId);
getAllTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec() admin: ITokenPayload) {
return this.ticketsService.getTicketForAdmin(queryDto, admin.permissions, admin.id);
}
@ApiOperation({ summary: "create ticket messages" })
+4 -1
View File
@@ -4,10 +4,12 @@ import { TypeOrmModule } from "@nestjs/typeorm";
import { TICKET } from "./constant";
import { TicketCategory } from "./entities/ticket-category.entity";
import { TicketMaster } from "./entities/ticket-master.entity";
import { TicketMessage } from "./entities/ticket-message.entity";
import { Ticket } from "./entities/ticket.entity";
import { ReferralStrategyFactory } from "./providers/referral-strategy.factory";
import { TicketsService } from "./providers/tickets.service";
import { TicketMasterRepository } from "./repositories/ticket-master.repository";
import { TicketCategoryRepository } from "./repositories/tickets-category.repository";
import { TicketMessagesRepository } from "./repositories/tickets-message.repository";
import { TicketsRepository } from "./repositories/tickets.repository";
@@ -27,7 +29,7 @@ import { LoggerModule } from "../logger/logger.module";
@Module({
imports: [
TypeOrmModule.forFeature([Ticket, TicketCategory, TicketMessageAttachment, TicketMessage]),
TypeOrmModule.forFeature([Ticket, TicketCategory, TicketMaster, TicketMessageAttachment, TicketMessage]),
AccessLogsModule,
BullModule.registerQueue({ name: TICKET.QUEUE_NAME }),
UsersModule,
@@ -48,6 +50,7 @@ import { LoggerModule } from "../logger/logger.module";
TicketsService,
TicketsRepository,
TicketCategoryRepository,
TicketMasterRepository,
TicketMessagesRepository,
TicketMessageAttachmentsRepository,
TicketProcessor,
+6 -4
View File
@@ -1,5 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsEmail, IsMobilePhone, IsNotEmpty, IsOptional, IsString, IsUUID, IsUrl, Length, MinLength } from "class-validator";
import { ArrayNotEmpty, IsArray, IsEmail, IsMobilePhone, IsNotEmpty, IsOptional, IsString, IsUUID, IsUrl, Length, MinLength } from "class-validator";
import { AdminMessage, AuthMessage } from "../../../common/enums/message.enum";
@@ -22,9 +22,11 @@ export class CreateAdminDto {
phone: string;
@IsNotEmpty({ message: AdminMessage.ROLE_REQUIRED })
@IsUUID("4", { message: AdminMessage.ROLE_ID_SHOULD_BE_UUID })
@ApiProperty({ description: "Role id", example: "123e4567-e89b-12d3-a456-426614174000" })
roleId: string;
@IsArray({ message: AdminMessage.ROLE_REQUIRED })
@ArrayNotEmpty({ message: AdminMessage.ROLE_REQUIRED })
@IsUUID("4", { each: true, message: AdminMessage.ROLE_ID_SHOULD_BE_UUID })
@ApiProperty({ description: "Role ids", example: ["123e4567-e89b-12d3-a456-426614174000"], type: [String] })
roleIds: string[];
@IsNotEmpty({ message: AuthMessage.FIRST_NAME_NOT_EMPTY })
@IsString({ message: AuthMessage.FIRST_NAME_NOT_EMPTY })
@@ -52,6 +52,16 @@ export const permissionMeta: Record<PermissionEnum, PermissionMeta> = {
[PermissionEnum.TICKETS_UPDATE]: { title: "ویرایش تیکت‌ها", group: PermissionGroupEnum.SUPPORT },
[PermissionEnum.TICKETS_DELETE]: { title: "حذف تیکت‌ها", group: PermissionGroupEnum.SUPPORT },
[PermissionEnum.TICKET_CATEGORIES_CREATE]: { title: "ایجاد دسته‌بندی تیکت", group: PermissionGroupEnum.SUPPORT },
[PermissionEnum.TICKET_CATEGORIES_READ]: { title: "مشاهده دسته‌بندی تیکت", group: PermissionGroupEnum.SUPPORT },
[PermissionEnum.TICKET_CATEGORIES_UPDATE]: { title: "ویرایش دسته‌بندی تیکت", group: PermissionGroupEnum.SUPPORT },
[PermissionEnum.TICKET_CATEGORIES_DELETE]: { title: "حذف دسته‌بندی تیکت", group: PermissionGroupEnum.SUPPORT },
[PermissionEnum.TICKET_MASTERS_CREATE]: { title: "ایجاد مستر تیکت", group: PermissionGroupEnum.SUPPORT },
[PermissionEnum.TICKET_MASTERS_READ]: { title: "مشاهده مستر تیکت", group: PermissionGroupEnum.SUPPORT },
[PermissionEnum.TICKET_MASTERS_UPDATE]: { title: "ویرایش مستر تیکت", group: PermissionGroupEnum.SUPPORT },
[PermissionEnum.TICKET_MASTERS_DELETE]: { title: "حذف مستر تیکت", group: PermissionGroupEnum.SUPPORT },
[PermissionEnum.CRITICISMS_CREATE]: { title: "ایجاد انتقادات", group: PermissionGroupEnum.OTHER },
[PermissionEnum.CRITICISMS_READ]: { title: "مشاهده انتقادات", group: PermissionGroupEnum.OTHER },
[PermissionEnum.CRITICISMS_UPDATE]: { title: "ویرایش انتقادات", group: PermissionGroupEnum.OTHER },
@@ -77,6 +87,21 @@ export const permissionMeta: Record<PermissionEnum, PermissionMeta> = {
[PermissionEnum.BLOGS_UPDATE]: { title: "ویرایش بلاگ‌ها", group: PermissionGroupEnum.CONTENT },
[PermissionEnum.BLOGS_DELETE]: { title: "حذف بلاگ‌ها", group: PermissionGroupEnum.CONTENT },
[PermissionEnum.BLOG_CATEGORIES_CREATE]: { title: "ایجاد دسته‌بندی بلاگ", group: PermissionGroupEnum.CONTENT },
[PermissionEnum.BLOG_CATEGORIES_READ]: { title: "مشاهده دسته‌بندی بلاگ", group: PermissionGroupEnum.CONTENT },
[PermissionEnum.BLOG_CATEGORIES_UPDATE]: { title: "ویرایش دسته‌بندی بلاگ", group: PermissionGroupEnum.CONTENT },
[PermissionEnum.BLOG_CATEGORIES_DELETE]: { title: "حذف دسته‌بندی بلاگ", group: PermissionGroupEnum.CONTENT },
[PermissionEnum.BLOG_COMMENTS_CREATE]: { title: "ایجاد نظرات بلاگ", group: PermissionGroupEnum.CONTENT },
[PermissionEnum.BLOG_COMMENTS_READ]: { title: "مشاهده نظرات بلاگ", group: PermissionGroupEnum.CONTENT },
[PermissionEnum.BLOG_COMMENTS_UPDATE]: { title: "ویرایش نظرات بلاگ", group: PermissionGroupEnum.CONTENT },
[PermissionEnum.BLOG_COMMENTS_DELETE]: { title: "حذف نظرات بلاگ", group: PermissionGroupEnum.CONTENT },
[PermissionEnum.SLIDERS_CREATE]: { title: "ایجاد اسلایدر", group: PermissionGroupEnum.CONTENT },
[PermissionEnum.SLIDERS_READ]: { title: "مشاهده اسلایدر", group: PermissionGroupEnum.CONTENT },
[PermissionEnum.SLIDERS_UPDATE]: { title: "ویرایش اسلایدر", group: PermissionGroupEnum.CONTENT },
[PermissionEnum.SLIDERS_DELETE]: { title: "حذف اسلایدر", group: PermissionGroupEnum.CONTENT },
[PermissionEnum.LEARNINGS_CREATE]: { title: "ایجاد آموزش‌ها", group: PermissionGroupEnum.CONTENT },
[PermissionEnum.LEARNINGS_READ]: { title: "مشاهده آموزش‌ها", group: PermissionGroupEnum.CONTENT },
[PermissionEnum.LEARNINGS_UPDATE]: { title: "ویرایش آموزش‌ها", group: PermissionGroupEnum.CONTENT },
@@ -53,6 +53,18 @@ export enum PermissionEnum {
TICKETS_UPDATE = "tickets_update",
TICKETS_DELETE = "tickets_delete",
// ticket_categories
TICKET_CATEGORIES_CREATE = "ticket_categories_create",
TICKET_CATEGORIES_READ = "ticket_categories_read",
TICKET_CATEGORIES_UPDATE = "ticket_categories_update",
TICKET_CATEGORIES_DELETE = "ticket_categories_delete",
// ticket_masters
TICKET_MASTERS_CREATE = "ticket_masters_create",
TICKET_MASTERS_READ = "ticket_masters_read",
TICKET_MASTERS_UPDATE = "ticket_masters_update",
TICKET_MASTERS_DELETE = "ticket_masters_delete",
// criticisms
CRITICISMS_CREATE = "criticisms_create",
CRITICISMS_READ = "criticisms_read",
@@ -83,6 +95,24 @@ export enum PermissionEnum {
BLOGS_UPDATE = "blogs_update",
BLOGS_DELETE = "blogs_delete",
// blog_categories
BLOG_CATEGORIES_CREATE = "blog_categories_create",
BLOG_CATEGORIES_READ = "blog_categories_read",
BLOG_CATEGORIES_UPDATE = "blog_categories_update",
BLOG_CATEGORIES_DELETE = "blog_categories_delete",
// blog_comments
BLOG_COMMENTS_CREATE = "blog_comments_create",
BLOG_COMMENTS_READ = "blog_comments_read",
BLOG_COMMENTS_UPDATE = "blog_comments_update",
BLOG_COMMENTS_DELETE = "blog_comments_delete",
// sliders
SLIDERS_CREATE = "sliders_create",
SLIDERS_READ = "sliders_read",
SLIDERS_UPDATE = "sliders_update",
SLIDERS_DELETE = "sliders_delete",
// learnings
LEARNINGS_CREATE = "learnings_create",
LEARNINGS_READ = "learnings_read",
+23 -19
View File
@@ -18,6 +18,7 @@ import { RoleEnum } from "../enums/role.enum";
import { PermissionsRepository } from "../repositories/permissions.repository";
import { RoleRepository } from "../repositories/roles.repository";
import { UserRepository } from "../repositories/users.repository";
import { getPermissionEntitiesFromRoles } from "../utils/role-permissions.util";
@Injectable()
export class AdminsService {
@@ -43,22 +44,22 @@ export class AdminsService {
const existPhone = await queryRunner.manager.findOneBy(this.userRepository.target, { phone: createDto.phone });
if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST);
const role = await queryRunner.manager.findOne(this.rolesRepository.target, {
where: { id: createDto.roleId },
const roles = await queryRunner.manager.find(this.rolesRepository.target, {
where: { id: In(createDto.roleIds), isAdmin: true },
relations: { permissions: true },
});
if (!role) throw new BadRequestException(AdsMessage.ROLE_NOT_FOUND);
if (roles.length !== createDto.roleIds.length) throw new BadRequestException(AdsMessage.ROLE_NOT_FOUND);
if (createDto.password !== createDto.repeatPassword) throw new BadRequestException(AdminMessage.PASSWORD_NOT_MATCH);
const hashedPassword = await this.passwordService.hashPassword(createDto.password);
const userName = slugify(`${createDto.firstName} ${Date.now().toString().slice(-5)}`, { lower: true, trim: true });
await queryRunner.manager.save(this.rolesRepository.target, role);
const { roleIds: _roleIds, repeatPassword: _repeatPassword, ...adminData } = createDto;
const adminUser = queryRunner.manager.create(this.userRepository.target, {
...createDto,
roles: [role],
...adminData,
roles,
password: hashedPassword,
userName,
});
@@ -75,7 +76,7 @@ export class AdminsService {
userAgent: userIpAndHeaders.headers["user-agent"],
actionDescription: `Admin created new admin: ${createDto.email}`,
metadata: {
roleId: createDto.roleId,
roleIds: createDto.roleIds,
createdBy: "admin",
email: createDto.email,
phone: createDto.phone,
@@ -118,16 +119,14 @@ export class AdminsService {
if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST);
}
if (updateDto.roleId) {
const role = await queryRunner.manager.findOne(this.rolesRepository.target, {
where: { id: updateDto.roleId },
if (updateDto.roleIds?.length) {
const roles = await queryRunner.manager.find(this.rolesRepository.target, {
where: { id: In(updateDto.roleIds), isAdmin: true },
relations: { permissions: true },
});
if (!role) throw new BadRequestException(AdsMessage.ROLE_NOT_FOUND);
if (roles.length !== updateDto.roleIds.length) throw new BadRequestException(AdsMessage.ROLE_NOT_FOUND);
await queryRunner.manager.save(this.rolesRepository.target, role);
admin.roles = [role];
admin.roles = roles;
await queryRunner.manager.save(this.userRepository.target, admin);
}
@@ -138,7 +137,8 @@ export class AdminsService {
admin.password = await this.passwordService.hashPassword(updateDto.password);
}
await queryRunner.manager.save(this.userRepository.target, { ...admin, ...updateDto, password: admin.password });
const { roleIds: _roleIds, repeatPassword: _repeatPassword, ...updateData } = updateDto;
await queryRunner.manager.save(this.userRepository.target, { ...admin, ...updateData, password: admin.password });
await queryRunner.commitTransaction();
await this.accessLogService.logUpdate("User", admin.id, UserType.ADMIN, {
@@ -150,7 +150,7 @@ export class AdminsService {
userType: UserType.ADMIN,
actionDescription: `Admin updated admin: ${admin.email}`,
metadata: {
roleId: updateDto.roleId,
roleIds: updateDto.roleIds,
},
oldValues: JSON.stringify({ ...admin }),
newValues: JSON.stringify({ ...updateDto }),
@@ -169,7 +169,7 @@ export class AdminsService {
/************************************************************ */
async deleteAdmin(id: string, userIpAndHeaders: IUserIpAndHeaders) {
const admin = await this.userRepository.findOne({ where: { id, roles: { isAdmin: true } } });
const admin = await this.userRepository.findOne({ where: { id, roles: { isAdmin: true } }, relations: { roles: true } });
if (!admin) throw new BadRequestException(AdminMessage.ADMIN_NOT_FOUND);
await this.userRepository.softDelete(id);
@@ -182,7 +182,7 @@ export class AdminsService {
userAgent: userIpAndHeaders.headers["user-agent"],
actionDescription: `Admin deleted admin: ${admin.email}`,
metadata: {
roleId: admin.roles[0].id,
roleIds: admin.roles.map((role) => role.id),
email: admin.email,
phone: admin.phone,
},
@@ -277,7 +277,11 @@ export class AdminsService {
paginate: true,
};
}
/************************************************************ */
async getAllAdmins() {
return this.userRepository.find({ where: { roles: { isAdmin: true } } });
}
/************************************************************ */
async getAdminById(id: string) {
@@ -303,7 +307,7 @@ export class AdminsService {
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
return {
permissions: user.roles[0].permissions,
permissions: getPermissionEntitiesFromRoles(user.roles),
};
}
/************************************************************ */
+102 -59
View File
@@ -2,7 +2,7 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { DataSource, Not } from "typeorm";
import { IUserIpAndHeaders } from "../../../common/decorators/user.decorator";
import { CommonMessage, UserMessage } from "../../../common/enums/message.enum";
import { AuthMessage, CommonMessage, FinancialMessage, UserMessage } from "../../../common/enums/message.enum";
import { UserType } from "../../access-logs/enums/user-type.enum";
import { AccessLogService } from "../../access-logs/providers/access-log.service";
import { Address } from "../../address/entities/address.entity";
@@ -135,6 +135,7 @@ export class CustomersService {
password,
phone,
birthDate,
nationalCode,
};
// const addressUpdateData = {
@@ -169,7 +170,7 @@ export class CustomersService {
if (nationalCode) {
const existingNationalCode = await queryRunner.manager.findOne(this.realUserRepository.target, {
where: { nationalCode: nationalCode, id: Not(user.id) },
where: { nationalCode: nationalCode, user: { id: Not(user.id) } },
});
if (existingNationalCode) throw new BadRequestException(UserMessage.NATIONAL_CODE_EXIST);
}
@@ -180,67 +181,109 @@ export class CustomersService {
await queryRunner.manager.update(this.userRepository.target, { id: customerId }, { password: hashPassword });
}
const legalUser = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { user: { id: customerId } },
relations: { address: true },
});
if (!legalUser) throw new BadRequestException(UserMessage.LEGAL_DATA_NOT_FOUND);
if (economicCode) {
const existingEconomicCode = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { economicCode, id: Not(legalUser.id) },
if (nationalCode) {
const realUser = await queryRunner.manager.findOne(this.realUserRepository.target, {
where: { user: { id: customerId } },
});
if (existingEconomicCode) throw new BadRequestException(UserMessage.ECONOMIC_CODE_EXIST);
}
if (registrationCode) {
const existingRegistrationCode = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { registrationCode, id: Not(legalUser.id) },
});
if (existingRegistrationCode) throw new BadRequestException(UserMessage.REGISTRATION_CODE_EXIST);
}
if (nationalIdentity) {
const existingNationalIdentity = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { nationalIdentity, id: Not(legalUser.id) },
});
if (existingNationalIdentity) throw new BadRequestException(UserMessage.NATIONAL_IDENTITY_EXIST);
}
if (registrationName) {
const existingRegistrationName = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { registrationName, id: Not(legalUser.id) },
});
if (existingRegistrationName) throw new BadRequestException(UserMessage.REGISTRATION_NAME_EXIST);
}
await queryRunner.manager.update(this.legalUserRepository.target, { id: legalUser.id }, legalUserUpdateData);
// Update address if needed
if (cityId || address || postalCode) {
// Get the address entity
const addressEntity = legalUser.address;
if (!addressEntity) throw new BadRequestException(UserMessage.ADDRESS_NOT_FOUND);
// Prepare address updates
const addressUpdates: Record<string, unknown> = {};
if (cityId) {
const { city } = await this.addressService.getCity(+cityId);
addressUpdates.city = city;
if (realUser) {
await queryRunner.manager.update(this.realUserRepository.target, { id: realUser.id }, { nationalCode });
}
}
if (address) {
addressUpdates.fullAddress = address;
const hasLegalUpdateData = Boolean(
registrationName || economicCode || nationalIdentity || registrationCode || postalCode || address || cityId,
);
if (hasLegalUpdateData) {
const legalUser = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { user: { id: customerId } },
relations: { address: true },
});
if (!legalUser) {
if (user.financialType === FinancialType.REAL) {
throw new BadRequestException(UserMessage.FINANCIAL_TYPE_IS_REAL);
}
const legalPhone = phone ?? user.phone;
if (!economicCode) throw new BadRequestException(FinancialMessage.ECONOMIC_CODE_REQUIRED);
if (!registrationCode) throw new BadRequestException(FinancialMessage.REGISTRATION_CODE_REQUIRED);
if (!registrationName) throw new BadRequestException(FinancialMessage.REGISTRATION_NAME_REQUIRED);
if (!nationalIdentity) throw new BadRequestException(FinancialMessage.NATIONAL_ID_REQUIRED);
if (!legalPhone) throw new BadRequestException(AuthMessage.PHONE_NOT_EMPTY);
if (!address) throw new BadRequestException(FinancialMessage.ADDRESS_REQUIRED);
if (!postalCode) throw new BadRequestException(FinancialMessage.POSTAL_CODE_REQUIRED);
if (!cityId) throw new BadRequestException(FinancialMessage.CITY_ID_REQUIRED);
await this.createLegalUserData(
{
economicCode,
registrationCode,
registrationName,
nationalIdentity,
phone: legalPhone,
address,
postalCode,
cityId: +cityId,
},
customerId,
queryRunner,
);
} else {
if (economicCode) {
const existingEconomicCode = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { economicCode, id: Not(legalUser.id) },
});
if (existingEconomicCode) throw new BadRequestException(UserMessage.ECONOMIC_CODE_EXIST);
}
if (registrationCode) {
const existingRegistrationCode = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { registrationCode, id: Not(legalUser.id) },
});
if (existingRegistrationCode) throw new BadRequestException(UserMessage.REGISTRATION_CODE_EXIST);
}
if (nationalIdentity) {
const existingNationalIdentity = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { nationalIdentity, id: Not(legalUser.id) },
});
if (existingNationalIdentity) throw new BadRequestException(UserMessage.NATIONAL_IDENTITY_EXIST);
}
if (registrationName) {
const existingRegistrationName = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { registrationName, id: Not(legalUser.id) },
});
if (existingRegistrationName) throw new BadRequestException(UserMessage.REGISTRATION_NAME_EXIST);
}
await queryRunner.manager.update(this.legalUserRepository.target, { id: legalUser.id }, legalUserUpdateData);
if (cityId || address || postalCode) {
const addressEntity = legalUser.address;
if (!addressEntity) throw new BadRequestException(UserMessage.ADDRESS_NOT_FOUND);
const addressUpdates: Record<string, unknown> = {};
if (cityId) {
const { city } = await this.addressService.getCity(+cityId);
addressUpdates.city = city;
}
if (address) {
addressUpdates.fullAddress = address;
}
if (postalCode) {
addressUpdates.postalCode = postalCode;
}
await queryRunner.manager.update(Address, { id: addressEntity.id }, addressUpdates);
}
}
if (postalCode) {
addressUpdates.postalCode = postalCode;
}
await queryRunner.manager.update(Address, { id: addressEntity.id }, addressUpdates);
}
await queryRunner.commitTransaction();
@@ -359,6 +359,7 @@ export class UsersService {
async isSuperAdmin(adminId: string) {
const user = await this.userRepository.findOne({ where: { id: adminId }, relations: { roles: true } });
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
console.log("user.roles", user.roles.map((role) => role.name));
return user.roles.some((role) => role.name === RoleEnum.SUPER_ADMIN);
}
@@ -119,10 +119,21 @@ export class UserRepository extends Repository<User> {
.loadRelationCountAndMap("user.ticketsCount", "user.tickets");
if (queryDto.q) {
queryBuilder
.orWhere("user.firstName ILIKE :search", { search: `%${queryDto.q}%` })
.orWhere("user.lastName ILIKE :search", { search: `%${queryDto.q}%` })
.orWhere("user.userName ILIKE :search", { search: `%${queryDto.q}%` });
queryBuilder.andWhere(
`(
user.firstName ILIKE :search
OR user.lastName ILIKE :search
OR CONCAT(user.firstName, ' ', user.lastName) ILIKE :search
OR user.userName ILIKE :search
OR user.phone ILIKE :search
OR legalUser.registrationName ILIKE :search
OR legalUser.phone ILIKE :search
OR realUser.firstName ILIKE :search
OR realUser.lastName ILIKE :search
OR realUser.phone ILIKE :search
)`,
{ search: `%${queryDto.q}%` },
);
}
return queryBuilder.getMany();
@@ -166,13 +177,28 @@ export class UserRepository extends Repository<User> {
.loadRelationCountAndMap("user.ticketsCount", "user.tickets");
if (queryDto.q) {
queryBuilder
.orWhere("user.firstName ILIKE :search", { search: `%${queryDto.q}%` })
.orWhere("user.lastName ILIKE :search", { search: `%${queryDto.q}%` })
.orWhere("user.userName ILIKE :search", { search: `%${queryDto.q}%` });
queryBuilder.andWhere(
`(
user.firstName ILIKE :search
OR user.lastName ILIKE :search
OR CONCAT(user.firstName, ' ', user.lastName) ILIKE :search
OR user.userName ILIKE :search
OR user.phone ILIKE :search
OR legalUser.registrationName ILIKE :search
OR legalUser.phone ILIKE :search
OR realUser.firstName ILIKE :search
OR realUser.lastName ILIKE :search
OR realUser.phone ILIKE :search
)`,
{ search: `%${queryDto.q}%` },
);
}
return await queryBuilder.skip(skip).take(limit).getManyAndCount();
return await queryBuilder
.orderBy("user.createdAt", "DESC")
.skip(skip)
.take(limit)
.getManyAndCount();
}
//************* */
+7 -1
View File
@@ -171,12 +171,18 @@ export class UsersController {
@ApiOperation({ summary: "get all admins ==> admin route" })
@AdminRoute()
// @PermissionsDec(PermissionEnum.ADMINS_READ, PermissionEnum.CUSTOMERS_READ)
@Get("admins")
getAdmins(@Query() queryDto: SearchAdminQueryDto) {
return this.adminsService.getAdmins(queryDto);
}
@ApiOperation({ summary: "get all admins without pagination==> admin route" })
@AdminRoute()
@Get("admins-list/all")
getAllAdmins() {
return this.adminsService.getAllAdmins();
}
@ApiOperation({ summary: "get admin by id ==> admin route" })
@PermissionsDec(PermissionEnum.ADMINS_READ, PermissionEnum.CUSTOMERS_READ)
@Get("admins/:id")
@@ -0,0 +1,19 @@
import { PermissionEnum } from "../enums/permission.enum";
import { Role } from "../entities/role.entity";
import { Permission } from "../entities/permission.entity";
export function getPermissionsFromRoles(roles: Role[]): PermissionEnum[] {
return [...new Set(roles.flatMap((role) => (role?.permissions?.length ? role.permissions.map((permission) => permission.name) : [])))];
}
export function getPermissionEntitiesFromRoles(roles: Role[]): Permission[] {
const permissionsById = new Map<string, Permission>();
for (const role of roles) {
for (const permission of role?.permissions ?? []) {
permissionsById.set(permission.id, permission);
}
}
return [...permissionsById.values()];
}
-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"
}
}