Refactor privilege management to support hierarchical privilege keys
- Updated privilege key structure to use a 3- or 4-part dotted hierarchy (e.g., `GROUP.PARENT.MODULE`). - Modified the `RequirePrivilege` decorator to accept multiple keys, allowing for OR logic in privilege checks. - Enhanced `PrivilegesGuard` to validate against multiple privilege keys, improving access control logic. - Created migration scripts to update existing privilege keys in the database to the new format. - Updated related services, controllers, and tests to accommodate the new privilege key structure and validation logic.
This commit is contained in:
@@ -11,7 +11,7 @@ alwaysApply: false
|
|||||||
Every **non-public** controller handler on a primary (CRUD) resource MUST use:
|
Every **non-public** controller handler on a primary (CRUD) resource MUST use:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@RequirePrivilege('MODULE.RESOURCE', 'view' | 'create' | 'update' | 'delete' | 'import')
|
@RequirePrivilege('GROUP.PARENT.MODULE' | ['ADMIN.SALES.ACTIVITIES.PLAN', 'MOBILE.SALES.PLAN'], 'view' | 'create' | 'update' | 'delete' | 'import')
|
||||||
```
|
```
|
||||||
|
|
||||||
Map HTTP verbs to actions:
|
Map HTTP verbs to actions:
|
||||||
@@ -24,13 +24,13 @@ Map HTTP verbs to actions:
|
|||||||
| `DELETE /:id`, bulk-delete | `delete` |
|
| `DELETE /:id`, bulk-delete | `delete` |
|
||||||
| `POST /import` | `import` |
|
| `POST /import` | `import` |
|
||||||
|
|
||||||
Key codes use dotted uppercase module levels (`PRIVILEGES`, `SALES.INVOICE`). New modules add a `privilege_keys` seed row via migration — do not invent a parallel permission helper.
|
Key codes use 3- or 4-part dotted uppercase hierarchy: `Group.Parent.Module` or `Group.Parent.Module.Submodule` (e.g. `ADMIN.SALES.ACTIVITIES.INVOICE`, `MOBILE.SALES.PLAN`). Pass a string or string array to `@RequirePrivilege`; arrays use OR semantics. New modules add `privilege_keys` rows via migration — do not invent a parallel permission helper.
|
||||||
|
|
||||||
Seed an Administrator privilege only via SQL/ops after the first user exists (`created_by` requires a user). Documented bootstrap: insert privilege + details, then `UPDATE users SET privilege_id = …`. Do not auto-grant on register.
|
Seed an Administrator privilege only via SQL/ops after the first user exists (`created_by` requires a user). Documented bootstrap: insert privilege + details, then `UPDATE users SET privilege_id = …`. Do not auto-grant on register.
|
||||||
|
|
||||||
## Guard behavior
|
## Guard behavior
|
||||||
|
|
||||||
`PrivilegesGuard` (global) allows when there is no metadata. When metadata is present, `users.is_superadmin === true` skips the matrix check. Otherwise the user’s assigned privilege must be **status `active`** and the matrix cell must be `value === true`, or the request is `403 Forbidden`. Missing privilege / draft / archived / missing cell / `false` → deny.
|
`PrivilegesGuard` (global) allows when there is no metadata. When metadata is present, `users.is_superadmin === true` skips the matrix check. Otherwise the user’s assigned privilege must be **status `active`** and at least one matrix cell in the required key list must be `value === true`, or the request is `403 Forbidden`. Missing privilege / draft / archived / missing cell / `false` → deny.
|
||||||
|
|
||||||
Do not set `is_superadmin` via register/login. Default is `false`; promote via SQL/ops (`UPDATE users SET is_superadmin = true`). The flag is loaded from the database on each JWT validation (not from JWT claims).
|
Do not set `is_superadmin` via register/login. Default is `false`; promote via SQL/ops (`UPDATE users SET is_superadmin = true`). The flag is loaded from the database on each JWT validation (not from JWT claims).
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
-- Rename existing privilege_keys to Group.Parent.Module[.Submodule] hierarchy
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'ADMIN.SETTINGS.USER.PRIVILEGES', "label" = 'Privileges', "sort_order" = 101 WHERE "code" = 'PRIVILEGES';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'ADMIN.SETTINGS.USER.USERS', "label" = 'Users', "sort_order" = 102 WHERE "code" = 'USERS';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'ADMIN.SETTINGS.DATA.DIVISION', "label" = 'Divisions', "sort_order" = 103 WHERE "code" = 'CONFIGURATION.DIVISION';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'ADMIN.SETTINGS.DATA.BRANCH', "label" = 'Branches', "sort_order" = 104 WHERE "code" = 'CONFIGURATION.BRANCH';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'ADMIN.SETTINGS.DATA.CUSTOMER', "label" = 'Customers', "sort_order" = 105 WHERE "code" = 'CONFIGURATION.CUSTOMER';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'ADMIN.SETTINGS.DATA.PRODUCT', "label" = 'Products', "sort_order" = 106 WHERE "code" = 'CONFIGURATION.PRODUCT';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'ADMIN.SETTINGS.DATA.SETTING', "label" = 'Company settings', "sort_order" = 107 WHERE "code" = 'CONFIGURATION.SETTING';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'ADMIN.SALES.DATA.EMPLOYEE', "label" = 'Employees', "sort_order" = 111 WHERE "code" = 'CONFIGURATION.EMPLOYEE';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'ADMIN.SALES.DATA.CYCLE', "label" = 'Sales cycles', "sort_order" = 112 WHERE "code" = 'SALES.CYCLE';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'ADMIN.SALES.ACTIVITIES.REQUEST', "label" = 'Sales requests', "sort_order" = 113 WHERE "code" = 'SALES.REQUEST';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'ADMIN.SALES.ACTIVITIES.ORDER', "label" = 'Sales orders', "sort_order" = 114 WHERE "code" = 'SALES.ORDER';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'ADMIN.SALES.ACTIVITIES.INVOICE', "label" = 'Sales invoices', "sort_order" = 115 WHERE "code" = 'SALES.INVOICE';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'ADMIN.SALES.ACTIVITIES.PAYMENT', "label" = 'Sales payments', "sort_order" = 116 WHERE "code" = 'SALES.PAYMENT';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'ADMIN.SALES.ACTIVITIES.PLAN', "label" = 'Sales plans', "sort_order" = 117 WHERE "code" = 'SALES.PLAN';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'ADMIN.SALES.REPORT', "label" = 'Sales reports', "sort_order" = 118 WHERE "code" = 'SALES.REPORT';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'ADMIN.LOGISTICS.ACTIVITIES.PACKING_SLIP', "label" = 'Packing slips', "sort_order" = 121 WHERE "code" = 'SALES.PACKING_SLIP';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'ADMIN.LOGISTICS.DATA.CYCLE', "label" = 'Logistics cycles', "sort_order" = 122 WHERE "code" = 'LOGISTICS.CYCLE';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'ADMIN.LOGISTICS.ACTIVITIES.PLAN', "label" = 'Logistics plans', "sort_order" = 123 WHERE "code" = 'LOGISTICS.PLAN';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'ADMIN.LOGISTICS.REPORT', "label" = 'Logistics reports', "sort_order" = 124 WHERE "code" = 'LOGISTICS.REPORT';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'MOBILE.SALES.PLAN.ATTENDANCE', "label" = 'Branch attendance', "sort_order" = 201 WHERE "code" = 'FIELD.ATTENDANCE';
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "privilege_keys" SET "code" = 'MOBILE.SALES.VISIT', "label" = 'Customer visits', "sort_order" = 202 WHERE "code" = 'FIELD.VISIT';
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO "privilege_keys" ("code", "label", "sort_order") VALUES
|
||||||
|
('MOBILE.SALES.PLAN', 'Sales plans (mobile)', 203),
|
||||||
|
('MOBILE.SALES.REQUEST', 'Sales requests (mobile)', 204),
|
||||||
|
('MOBILE.SALES.ORDER', 'Sales orders (mobile)', 205),
|
||||||
|
('MOBILE.SALES.INVOICE', 'Sales invoices (mobile)', 206),
|
||||||
|
('MOBILE.SALES.PAYMENT', 'Sales payments (mobile)', 207),
|
||||||
|
('MOBILE.SALES.CUSTOMER', 'Customers (mobile)', 208),
|
||||||
|
('MOBILE.LOGISTICS.PLAN', 'Logistics plans (mobile)', 209);
|
||||||
@@ -106,6 +106,20 @@
|
|||||||
"when": 1787562000000,
|
"when": 1787562000000,
|
||||||
"tag": "0014_sales_invoice_location",
|
"tag": "0014_sales_invoice_location",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 15,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1787563000000,
|
||||||
|
"tag": "0015_field_check_in",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 16,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1787564000000,
|
||||||
|
"tag": "0016_privilege_key_hierarchy",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -4,13 +4,22 @@ import type { PrivilegeAction } from '../../modules/privileges/privilege-action'
|
|||||||
export const REQUIRE_PRIVILEGE_KEY = 'requirePrivilege';
|
export const REQUIRE_PRIVILEGE_KEY = 'requirePrivilege';
|
||||||
|
|
||||||
export type RequirePrivilegeMeta = {
|
export type RequirePrivilegeMeta = {
|
||||||
readonly key: string;
|
readonly keys: readonly string[];
|
||||||
readonly action: PrivilegeAction;
|
readonly action: PrivilegeAction;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Marks a handler as requiring a privilege matrix cell to be true. */
|
function normalizePrivilegeKeys(
|
||||||
export const RequirePrivilege = (key: string, action: PrivilegeAction) =>
|
keys: string | readonly string[],
|
||||||
|
): readonly string[] {
|
||||||
|
return typeof keys === 'string' ? [keys] : keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Marks a handler as requiring one or more privilege matrix cells (OR). */
|
||||||
|
export const RequirePrivilege = (
|
||||||
|
keys: string | readonly string[],
|
||||||
|
action: PrivilegeAction,
|
||||||
|
) =>
|
||||||
SetMetadata(REQUIRE_PRIVILEGE_KEY, {
|
SetMetadata(REQUIRE_PRIVILEGE_KEY, {
|
||||||
key,
|
keys: normalizePrivilegeKeys(keys),
|
||||||
action,
|
action,
|
||||||
} satisfies RequirePrivilegeMeta);
|
} satisfies RequirePrivilegeMeta);
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ import {
|
|||||||
import { PrivilegesGuard } from './privileges.guard';
|
import { PrivilegesGuard } from './privileges.guard';
|
||||||
|
|
||||||
describe('PrivilegesGuard', () => {
|
describe('PrivilegesGuard', () => {
|
||||||
const checkPermission = jest.fn();
|
const checkAnyPermission = jest.fn();
|
||||||
const getAllAndOverride = jest.fn();
|
const getAllAndOverride = jest.fn();
|
||||||
const reflector = {
|
const reflector = {
|
||||||
getAllAndOverride,
|
getAllAndOverride,
|
||||||
} as unknown as Reflector;
|
} as unknown as Reflector;
|
||||||
|
|
||||||
const guard = new PrivilegesGuard(reflector, {
|
const guard = new PrivilegesGuard(reflector, {
|
||||||
checkPermission,
|
checkAnyPermission,
|
||||||
} as never);
|
} as never);
|
||||||
|
|
||||||
const user: AuthUser = {
|
const user: AuthUser = {
|
||||||
@@ -46,26 +46,48 @@ describe('PrivilegesGuard', () => {
|
|||||||
it('allows when no RequirePrivilege metadata', async () => {
|
it('allows when no RequirePrivilege metadata', async () => {
|
||||||
getAllAndOverride.mockReturnValue(undefined);
|
getAllAndOverride.mockReturnValue(undefined);
|
||||||
await expect(guard.canActivate(createContext(user))).resolves.toBe(true);
|
await expect(guard.canActivate(createContext(user))).resolves.toBe(true);
|
||||||
expect(checkPermission).not.toHaveBeenCalled();
|
expect(checkAnyPermission).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows when permission value is true', async () => {
|
it('allows when permission value is true for a single key', async () => {
|
||||||
const meta: RequirePrivilegeMeta = { key: 'PRIVILEGES', action: 'view' };
|
const meta: RequirePrivilegeMeta = {
|
||||||
|
keys: ['ADMIN.SETTINGS.USER.PRIVILEGES'],
|
||||||
|
action: 'view',
|
||||||
|
};
|
||||||
getAllAndOverride.mockReturnValue(meta);
|
getAllAndOverride.mockReturnValue(meta);
|
||||||
checkPermission.mockResolvedValue(true);
|
checkAnyPermission.mockResolvedValue(true);
|
||||||
|
|
||||||
await expect(guard.canActivate(createContext(user))).resolves.toBe(true);
|
await expect(guard.canActivate(createContext(user))).resolves.toBe(true);
|
||||||
expect(checkPermission).toHaveBeenCalledWith(
|
expect(checkAnyPermission).toHaveBeenCalledWith(
|
||||||
'user-1',
|
'user-1',
|
||||||
'PRIVILEGES',
|
['ADMIN.SETTINGS.USER.PRIVILEGES'],
|
||||||
|
'view',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows when any key in the list is granted', async () => {
|
||||||
|
const meta: RequirePrivilegeMeta = {
|
||||||
|
keys: ['ADMIN.SALES.ACTIVITIES.PLAN', 'MOBILE.SALES.PLAN'],
|
||||||
|
action: 'view',
|
||||||
|
};
|
||||||
|
getAllAndOverride.mockReturnValue(meta);
|
||||||
|
checkAnyPermission.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await expect(guard.canActivate(createContext(user))).resolves.toBe(true);
|
||||||
|
expect(checkAnyPermission).toHaveBeenCalledWith(
|
||||||
|
'user-1',
|
||||||
|
['ADMIN.SALES.ACTIVITIES.PLAN', 'MOBILE.SALES.PLAN'],
|
||||||
'view',
|
'view',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('forbids when permission is false or missing', async () => {
|
it('forbids when permission is false or missing', async () => {
|
||||||
const meta: RequirePrivilegeMeta = { key: 'PRIVILEGES', action: 'delete' };
|
const meta: RequirePrivilegeMeta = {
|
||||||
|
keys: ['ADMIN.SETTINGS.USER.PRIVILEGES'],
|
||||||
|
action: 'delete',
|
||||||
|
};
|
||||||
getAllAndOverride.mockReturnValue(meta);
|
getAllAndOverride.mockReturnValue(meta);
|
||||||
checkPermission.mockResolvedValue(false);
|
checkAnyPermission.mockResolvedValue(false);
|
||||||
|
|
||||||
await expect(guard.canActivate(createContext(user))).rejects.toBeInstanceOf(
|
await expect(guard.canActivate(createContext(user))).rejects.toBeInstanceOf(
|
||||||
ForbiddenException,
|
ForbiddenException,
|
||||||
@@ -73,17 +95,23 @@ describe('PrivilegesGuard', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('skips privilege lookup when user is superadmin', async () => {
|
it('skips privilege lookup when user is superadmin', async () => {
|
||||||
const meta: RequirePrivilegeMeta = { key: 'PRIVILEGES', action: 'delete' };
|
const meta: RequirePrivilegeMeta = {
|
||||||
|
keys: ['ADMIN.SETTINGS.USER.PRIVILEGES'],
|
||||||
|
action: 'delete',
|
||||||
|
};
|
||||||
getAllAndOverride.mockReturnValue(meta);
|
getAllAndOverride.mockReturnValue(meta);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
guard.canActivate(createContext({ ...user, isSuperadmin: true })),
|
guard.canActivate(createContext({ ...user, isSuperadmin: true })),
|
||||||
).resolves.toBe(true);
|
).resolves.toBe(true);
|
||||||
expect(checkPermission).not.toHaveBeenCalled();
|
expect(checkAnyPermission).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('unauthorized when metadata present but no user', async () => {
|
it('unauthorized when metadata present but no user', async () => {
|
||||||
const meta: RequirePrivilegeMeta = { key: 'PRIVILEGES', action: 'view' };
|
const meta: RequirePrivilegeMeta = {
|
||||||
|
keys: ['ADMIN.SETTINGS.USER.PRIVILEGES'],
|
||||||
|
action: 'view',
|
||||||
|
};
|
||||||
getAllAndOverride.mockReturnValue(meta);
|
getAllAndOverride.mockReturnValue(meta);
|
||||||
|
|
||||||
await expect(guard.canActivate(createContext())).rejects.toBeInstanceOf(
|
await expect(guard.canActivate(createContext())).rejects.toBeInstanceOf(
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ export class PrivilegesGuard implements CanActivate {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowed = await this.privilegesService.checkPermission(
|
const allowed = await this.privilegesService.checkAnyPermission(
|
||||||
user.id,
|
user.id,
|
||||||
required.key,
|
required.keys,
|
||||||
required.action,
|
required.action,
|
||||||
);
|
);
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ describe('AuthController', () => {
|
|||||||
status: 'active',
|
status: 'active',
|
||||||
});
|
});
|
||||||
privilegesService.getPermissionsMap.mockResolvedValue({
|
privilegesService.getPermissionsMap.mockResolvedValue({
|
||||||
PRIVILEGES: {
|
'ADMIN.SETTINGS.USER.PRIVILEGES': {
|
||||||
view: true,
|
view: true,
|
||||||
create: true,
|
create: true,
|
||||||
update: true,
|
update: true,
|
||||||
@@ -137,7 +137,7 @@ describe('AuthController', () => {
|
|||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
privilege: { id: 'priv-1', code: 'ADMIN' },
|
privilege: { id: 'priv-1', code: 'ADMIN' },
|
||||||
permissions: {
|
permissions: {
|
||||||
PRIVILEGES: expect.objectContaining({ view: true }),
|
'ADMIN.SETTINGS.USER.PRIVILEGES': expect.objectContaining({ view: true }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export class MeResponseDto {
|
|||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'Permission matrix keyed by privilege key code',
|
description: 'Permission matrix keyed by privilege key code',
|
||||||
example: {
|
example: {
|
||||||
PRIVILEGES: {
|
'ADMIN.SETTINGS.USER.PRIVILEGES': {
|
||||||
view: true,
|
view: true,
|
||||||
create: false,
|
create: false,
|
||||||
update: false,
|
update: false,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { BEARER_AUTH_NAME } from '../../../common/swagger/setup-swagger';
|
|||||||
import { BranchDto, ListBranchesQueryDto } from './dto/branch.dto';
|
import { BranchDto, ListBranchesQueryDto } from './dto/branch.dto';
|
||||||
import { BranchesService } from './branches.service';
|
import { BranchesService } from './branches.service';
|
||||||
|
|
||||||
export const BRANCH_PRIVILEGE_KEY = 'CONFIGURATION.BRANCH';
|
export const BRANCH_PRIVILEGE_KEY = 'ADMIN.SETTINGS.DATA.BRANCH';
|
||||||
|
|
||||||
@ApiTags('branches')
|
@ApiTags('branches')
|
||||||
@ApiBearerAuth(BEARER_AUTH_NAME)
|
@ApiBearerAuth(BEARER_AUTH_NAME)
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ import { BEARER_AUTH_NAME } from '../../../common/swagger/setup-swagger';
|
|||||||
import { CustomerDto, ListCustomersQueryDto } from './dto/customer.dto';
|
import { CustomerDto, ListCustomersQueryDto } from './dto/customer.dto';
|
||||||
import { CustomersService } from './customers.service';
|
import { CustomersService } from './customers.service';
|
||||||
|
|
||||||
export const CUSTOMER_PRIVILEGE_KEY = 'CONFIGURATION.CUSTOMER';
|
export const CUSTOMER_PRIVILEGE_KEYS = [
|
||||||
|
'ADMIN.SETTINGS.DATA.CUSTOMER',
|
||||||
|
'MOBILE.SALES.CUSTOMER',
|
||||||
|
] as const;
|
||||||
|
|
||||||
@ApiTags('customers')
|
@ApiTags('customers')
|
||||||
@ApiBearerAuth(BEARER_AUTH_NAME)
|
@ApiBearerAuth(BEARER_AUTH_NAME)
|
||||||
@@ -28,7 +31,7 @@ export class CustomersReadController {
|
|||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@Pagination()
|
@Pagination()
|
||||||
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'view')
|
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEYS, 'view')
|
||||||
@ApiOperation({ summary: 'List customers' })
|
@ApiOperation({ summary: 'List customers' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
schema: {
|
schema: {
|
||||||
@@ -50,7 +53,7 @@ export class CustomersReadController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'view')
|
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEYS, 'view')
|
||||||
@ApiOperation({ summary: 'Get customer detail' })
|
@ApiOperation({ summary: 'Get customer detail' })
|
||||||
@ApiOkResponse({ type: CustomerDto })
|
@ApiOkResponse({ type: CustomerDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { CurrentUser } from '../../../common/decorators/current-user.decorator';
|
|||||||
import { RequirePrivilege } from '../../../common/decorators/require-privilege.decorator';
|
import { RequirePrivilege } from '../../../common/decorators/require-privilege.decorator';
|
||||||
import { BEARER_AUTH_NAME } from '../../../common/swagger/setup-swagger';
|
import { BEARER_AUTH_NAME } from '../../../common/swagger/setup-swagger';
|
||||||
import { isAllowedCsvUpload } from './customer-fields';
|
import { isAllowedCsvUpload } from './customer-fields';
|
||||||
import { CUSTOMER_PRIVILEGE_KEY } from './customers-read.controller';
|
import { CUSTOMER_PRIVILEGE_KEYS } from './customers-read.controller';
|
||||||
import { CustomersService } from './customers.service';
|
import { CustomersService } from './customers.service';
|
||||||
import {
|
import {
|
||||||
BulkIdsDto,
|
BulkIdsDto,
|
||||||
@@ -49,7 +49,7 @@ export class CustomersWriteController {
|
|||||||
constructor(private readonly customersService: CustomersService) {}
|
constructor(private readonly customersService: CustomersService) {}
|
||||||
|
|
||||||
@Post('import')
|
@Post('import')
|
||||||
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'import')
|
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEYS, 'import')
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor('file', {
|
FileInterceptor('file', {
|
||||||
limits: { fileSize: 1_048_576 },
|
limits: { fileSize: 1_048_576 },
|
||||||
@@ -88,7 +88,7 @@ export class CustomersWriteController {
|
|||||||
|
|
||||||
@Post('bulk-delete')
|
@Post('bulk-delete')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'delete')
|
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEYS, 'delete')
|
||||||
@ApiOperation({ summary: 'Bulk delete customers' })
|
@ApiOperation({ summary: 'Bulk delete customers' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
schema: { properties: { deleted: { type: 'number' } } },
|
schema: { properties: { deleted: { type: 'number' } } },
|
||||||
@@ -101,7 +101,7 @@ export class CustomersWriteController {
|
|||||||
|
|
||||||
@Post('bulk-status')
|
@Post('bulk-status')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'update')
|
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEYS, 'update')
|
||||||
@ApiOperation({ summary: 'Bulk update customer status' })
|
@ApiOperation({ summary: 'Bulk update customer status' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
schema: { properties: { updated: { type: 'number' } } },
|
schema: { properties: { updated: { type: 'number' } } },
|
||||||
@@ -116,7 +116,7 @@ export class CustomersWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'create')
|
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEYS, 'create')
|
||||||
@ApiOperation({ summary: 'Create customer' })
|
@ApiOperation({ summary: 'Create customer' })
|
||||||
@ApiCreatedResponse({ type: CustomerDto })
|
@ApiCreatedResponse({ type: CustomerDto })
|
||||||
@ApiUnauthorizedResponse()
|
@ApiUnauthorizedResponse()
|
||||||
@@ -132,7 +132,7 @@ export class CustomersWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/contacts')
|
@Post(':id/contacts')
|
||||||
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'update')
|
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEYS, 'update')
|
||||||
@ApiOperation({ summary: 'Add a customer contact' })
|
@ApiOperation({ summary: 'Add a customer contact' })
|
||||||
@ApiOkResponse({ type: CustomerDto })
|
@ApiOkResponse({ type: CustomerDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
@@ -147,7 +147,7 @@ export class CustomersWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id/contacts/:contactId')
|
@Patch(':id/contacts/:contactId')
|
||||||
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'update')
|
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEYS, 'update')
|
||||||
@ApiOperation({ summary: 'Update a customer contact' })
|
@ApiOperation({ summary: 'Update a customer contact' })
|
||||||
@ApiOkResponse({ type: CustomerDto })
|
@ApiOkResponse({ type: CustomerDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
@@ -167,7 +167,7 @@ export class CustomersWriteController {
|
|||||||
|
|
||||||
@Delete(':id/contacts/:contactId')
|
@Delete(':id/contacts/:contactId')
|
||||||
@HttpCode(204)
|
@HttpCode(204)
|
||||||
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'update')
|
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEYS, 'update')
|
||||||
@ApiOperation({ summary: 'Delete a customer contact' })
|
@ApiOperation({ summary: 'Delete a customer contact' })
|
||||||
@ApiNoContentResponse()
|
@ApiNoContentResponse()
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
@@ -181,7 +181,7 @@ export class CustomersWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id/status')
|
@Patch(':id/status')
|
||||||
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'update')
|
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEYS, 'update')
|
||||||
@ApiOperation({ summary: 'Update customer status' })
|
@ApiOperation({ summary: 'Update customer status' })
|
||||||
@ApiOkResponse({ type: CustomerDto })
|
@ApiOkResponse({ type: CustomerDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
@@ -196,7 +196,7 @@ export class CustomersWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'update')
|
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEYS, 'update')
|
||||||
@ApiOperation({ summary: 'Update customer (not status)' })
|
@ApiOperation({ summary: 'Update customer (not status)' })
|
||||||
@ApiOkResponse({ type: CustomerDto })
|
@ApiOkResponse({ type: CustomerDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
@@ -215,7 +215,7 @@ export class CustomersWriteController {
|
|||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@HttpCode(204)
|
@HttpCode(204)
|
||||||
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEY, 'delete')
|
@RequirePrivilege(CUSTOMER_PRIVILEGE_KEYS, 'delete')
|
||||||
@ApiOperation({ summary: 'Delete customer' })
|
@ApiOperation({ summary: 'Delete customer' })
|
||||||
@ApiNoContentResponse()
|
@ApiNoContentResponse()
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { BEARER_AUTH_NAME } from '../../../common/swagger/setup-swagger';
|
|||||||
import { DivisionDto, ListDivisionsQueryDto } from './dto/division.dto';
|
import { DivisionDto, ListDivisionsQueryDto } from './dto/division.dto';
|
||||||
import { DivisionsService } from './divisions.service';
|
import { DivisionsService } from './divisions.service';
|
||||||
|
|
||||||
export const DIVISION_PRIVILEGE_KEY = 'CONFIGURATION.DIVISION';
|
export const DIVISION_PRIVILEGE_KEY = 'ADMIN.SETTINGS.DATA.DIVISION';
|
||||||
|
|
||||||
@ApiTags('divisions')
|
@ApiTags('divisions')
|
||||||
@ApiBearerAuth(BEARER_AUTH_NAME)
|
@ApiBearerAuth(BEARER_AUTH_NAME)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { BEARER_AUTH_NAME } from '../../../common/swagger/setup-swagger';
|
|||||||
import { EmployeeDto, ListEmployeesQueryDto } from './dto/employee.dto';
|
import { EmployeeDto, ListEmployeesQueryDto } from './dto/employee.dto';
|
||||||
import { EmployeesService } from './employees.service';
|
import { EmployeesService } from './employees.service';
|
||||||
|
|
||||||
export const EMPLOYEE_PRIVILEGE_KEY = 'CONFIGURATION.EMPLOYEE';
|
export const EMPLOYEE_PRIVILEGE_KEY = 'ADMIN.SALES.DATA.EMPLOYEE';
|
||||||
|
|
||||||
@ApiTags('employees')
|
@ApiTags('employees')
|
||||||
@ApiBearerAuth(BEARER_AUTH_NAME)
|
@ApiBearerAuth(BEARER_AUTH_NAME)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { BEARER_AUTH_NAME } from '../../../common/swagger/setup-swagger';
|
|||||||
import { ProductDto, ListProductsQueryDto } from './dto/product.dto';
|
import { ProductDto, ListProductsQueryDto } from './dto/product.dto';
|
||||||
import { ProductsService } from './products.service';
|
import { ProductsService } from './products.service';
|
||||||
|
|
||||||
export const PRODUCT_PRIVILEGE_KEY = 'CONFIGURATION.PRODUCT';
|
export const PRODUCT_PRIVILEGE_KEY = 'ADMIN.SETTINGS.DATA.PRODUCT';
|
||||||
|
|
||||||
@ApiTags('products')
|
@ApiTags('products')
|
||||||
@ApiBearerAuth(BEARER_AUTH_NAME)
|
@ApiBearerAuth(BEARER_AUTH_NAME)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ describe('CyclesService', () => {
|
|||||||
const employeesService = { findById: jest.fn(), findByCode: jest.fn() };
|
const employeesService = { findById: jest.fn(), findByCode: jest.fn() };
|
||||||
const branchesService = { findById: jest.fn(), findByCode: jest.fn() };
|
const branchesService = { findById: jest.fn(), findByCode: jest.fn() };
|
||||||
const customersService = { findById: jest.fn(), findByCode: jest.fn() };
|
const customersService = { findById: jest.fn(), findByCode: jest.fn() };
|
||||||
const privilegesService = { checkPermission: jest.fn() };
|
const privilegesService = { checkPermission: jest.fn(), checkAnyPermission: jest.fn() };
|
||||||
|
|
||||||
const now = DateTime.fromUnixMs(1_700_000_000_000);
|
const now = DateTime.fromUnixMs(1_700_000_000_000);
|
||||||
const user: AuthUser = {
|
const user: AuthUser = {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
type WeekdaysInput,
|
type WeekdaysInput,
|
||||||
} from '../shared/field-fields';
|
} from '../shared/field-fields';
|
||||||
import {
|
import {
|
||||||
fieldPrivilegeKey,
|
fieldPrivilegeKeys,
|
||||||
isFieldPurpose,
|
isFieldPurpose,
|
||||||
WEEKDAY_NAMES,
|
WEEKDAY_NAMES,
|
||||||
type FieldPurpose,
|
type FieldPurpose,
|
||||||
@@ -485,9 +485,9 @@ export class CyclesService {
|
|||||||
}
|
}
|
||||||
const allowed: FieldPurpose[] = [];
|
const allowed: FieldPurpose[] = [];
|
||||||
for (const purpose of ['sales', 'logistics'] as const) {
|
for (const purpose of ['sales', 'logistics'] as const) {
|
||||||
const ok = await this.privilegesService.checkPermission(
|
const ok = await this.privilegesService.checkAnyPermission(
|
||||||
user.id,
|
user.id,
|
||||||
fieldPrivilegeKey('cycle', purpose),
|
fieldPrivilegeKeys('cycle', purpose),
|
||||||
action,
|
action,
|
||||||
);
|
);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
@@ -505,9 +505,9 @@ export class CyclesService {
|
|||||||
if (user.isSuperadmin) {
|
if (user.isSuperadmin) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ok = await this.privilegesService.checkPermission(
|
const ok = await this.privilegesService.checkAnyPermission(
|
||||||
user.id,
|
user.id,
|
||||||
fieldPrivilegeKey('cycle', purpose),
|
fieldPrivilegeKeys('cycle', purpose),
|
||||||
action,
|
action,
|
||||||
);
|
);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ describe('PlansService', () => {
|
|||||||
markDraftsProcessed: jest.fn(),
|
markDraftsProcessed: jest.fn(),
|
||||||
};
|
};
|
||||||
const packingSlipsService = { findById: jest.fn() };
|
const packingSlipsService = { findById: jest.fn() };
|
||||||
const privilegesService = { checkPermission: jest.fn() };
|
const privilegesService = { checkPermission: jest.fn(), checkAnyPermission: jest.fn() };
|
||||||
|
|
||||||
const user: AuthUser = {
|
const user: AuthUser = {
|
||||||
id: 'user-1',
|
id: 'user-1',
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { CyclesRepository } from '../cycles/cycles.repository';
|
|||||||
import { CompanySettingsService } from '../settings/company-settings.service';
|
import { CompanySettingsService } from '../settings/company-settings.service';
|
||||||
import {
|
import {
|
||||||
cycleNumberForDate,
|
cycleNumberForDate,
|
||||||
fieldPrivilegeKey,
|
fieldPrivilegeKeys,
|
||||||
isFieldPurpose,
|
isFieldPurpose,
|
||||||
noCycleMessage,
|
noCycleMessage,
|
||||||
type FieldPurpose,
|
type FieldPurpose,
|
||||||
@@ -615,9 +615,9 @@ export class PlansService {
|
|||||||
}
|
}
|
||||||
const allowed: FieldPurpose[] = [];
|
const allowed: FieldPurpose[] = [];
|
||||||
for (const purpose of ['sales', 'logistics'] as const) {
|
for (const purpose of ['sales', 'logistics'] as const) {
|
||||||
const ok = await this.privilegesService.checkPermission(
|
const ok = await this.privilegesService.checkAnyPermission(
|
||||||
user.id,
|
user.id,
|
||||||
fieldPrivilegeKey('plan', purpose),
|
fieldPrivilegeKeys('plan', purpose),
|
||||||
action,
|
action,
|
||||||
);
|
);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
@@ -635,9 +635,9 @@ export class PlansService {
|
|||||||
if (user.isSuperadmin) {
|
if (user.isSuperadmin) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ok = await this.privilegesService.checkPermission(
|
const ok = await this.privilegesService.checkAnyPermission(
|
||||||
user.id,
|
user.id,
|
||||||
fieldPrivilegeKey('plan', purpose),
|
fieldPrivilegeKeys('plan', purpose),
|
||||||
action,
|
action,
|
||||||
);
|
);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import { REQUIRE_FIELD_PRIVILEGE_KEY } from './field-privilege.decorator';
|
|||||||
import { FieldPrivilegeGuard } from './field-privilege.guard';
|
import { FieldPrivilegeGuard } from './field-privilege.guard';
|
||||||
|
|
||||||
describe('FieldPrivilegeGuard', () => {
|
describe('FieldPrivilegeGuard', () => {
|
||||||
const checkPermission = jest.fn();
|
const checkAnyPermission = jest.fn();
|
||||||
const getAllAndOverride = jest.fn();
|
const getAllAndOverride = jest.fn();
|
||||||
const reflector = {
|
const reflector = {
|
||||||
getAllAndOverride,
|
getAllAndOverride,
|
||||||
} as unknown as Reflector;
|
} as unknown as Reflector;
|
||||||
|
|
||||||
const guard = new FieldPrivilegeGuard(reflector, {
|
const guard = new FieldPrivilegeGuard(reflector, {
|
||||||
checkPermission,
|
checkAnyPermission,
|
||||||
} as never);
|
} as never);
|
||||||
|
|
||||||
const user: AuthUser = {
|
const user: AuthUser = {
|
||||||
@@ -48,7 +48,7 @@ describe('FieldPrivilegeGuard', () => {
|
|||||||
it('allows when no field privilege metadata', async () => {
|
it('allows when no field privilege metadata', async () => {
|
||||||
getAllAndOverride.mockReturnValue(undefined);
|
getAllAndOverride.mockReturnValue(undefined);
|
||||||
await expect(guard.canActivate(createContext(user))).resolves.toBe(true);
|
await expect(guard.canActivate(createContext(user))).resolves.toBe(true);
|
||||||
expect(checkPermission).not.toHaveBeenCalled();
|
expect(checkAnyPermission).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows sales purpose when sales key is granted', async () => {
|
it('allows sales purpose when sales key is granted', async () => {
|
||||||
@@ -57,8 +57,9 @@ describe('FieldPrivilegeGuard', () => {
|
|||||||
action: 'create',
|
action: 'create',
|
||||||
};
|
};
|
||||||
getAllAndOverride.mockReturnValue(meta);
|
getAllAndOverride.mockReturnValue(meta);
|
||||||
checkPermission.mockImplementation((_id: string, key: string) =>
|
checkAnyPermission.mockImplementation(
|
||||||
Promise.resolve(key === 'SALES.CYCLE'),
|
(_id: string, keys: readonly string[]) =>
|
||||||
|
Promise.resolve(keys.includes('ADMIN.SALES.DATA.CYCLE')),
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -72,8 +73,9 @@ describe('FieldPrivilegeGuard', () => {
|
|||||||
action: 'update',
|
action: 'update',
|
||||||
};
|
};
|
||||||
getAllAndOverride.mockReturnValue(meta);
|
getAllAndOverride.mockReturnValue(meta);
|
||||||
checkPermission.mockImplementation((_id: string, key: string) =>
|
checkAnyPermission.mockImplementation(
|
||||||
Promise.resolve(key === 'SALES.PLAN'),
|
(_id: string, keys: readonly string[]) =>
|
||||||
|
Promise.resolve(keys.includes('MOBILE.SALES.PLAN')),
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@@ -87,8 +89,9 @@ describe('FieldPrivilegeGuard', () => {
|
|||||||
action: 'view',
|
action: 'view',
|
||||||
};
|
};
|
||||||
getAllAndOverride.mockReturnValue(meta);
|
getAllAndOverride.mockReturnValue(meta);
|
||||||
checkPermission.mockImplementation((_id: string, key: string) =>
|
checkAnyPermission.mockImplementation(
|
||||||
Promise.resolve(key === 'LOGISTICS.CYCLE'),
|
(_id: string, keys: readonly string[]) =>
|
||||||
|
Promise.resolve(keys.includes('ADMIN.LOGISTICS.DATA.CYCLE')),
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(guard.canActivate(createContext(user, {}, {}))).resolves.toBe(
|
await expect(guard.canActivate(createContext(user, {}, {}))).resolves.toBe(
|
||||||
@@ -106,7 +109,7 @@ describe('FieldPrivilegeGuard', () => {
|
|||||||
await expect(
|
await expect(
|
||||||
guard.canActivate(createContext({ ...user, isSuperadmin: true })),
|
guard.canActivate(createContext({ ...user, isSuperadmin: true })),
|
||||||
).resolves.toBe(true);
|
).resolves.toBe(true);
|
||||||
expect(checkPermission).not.toHaveBeenCalled();
|
expect(checkAnyPermission).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('unauthorized when metadata present but no user', async () => {
|
it('unauthorized when metadata present but no user', async () => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type { AuthUser } from '../../../common/auth/auth-user';
|
|||||||
import type { PrivilegeAction } from '../../privileges/privilege-action';
|
import type { PrivilegeAction } from '../../privileges/privilege-action';
|
||||||
import { PrivilegesService } from '../../privileges/privileges.service';
|
import { PrivilegesService } from '../../privileges/privileges.service';
|
||||||
import {
|
import {
|
||||||
fieldPrivilegeKey,
|
fieldPrivilegeKeys,
|
||||||
isFieldPurpose,
|
isFieldPurpose,
|
||||||
type FieldPurpose,
|
type FieldPurpose,
|
||||||
type FieldResource,
|
type FieldResource,
|
||||||
@@ -78,9 +78,9 @@ export class FieldPrivilegeGuard implements CanActivate {
|
|||||||
const purposes: FieldPurpose[] = ['sales', 'logistics'];
|
const purposes: FieldPurpose[] = ['sales', 'logistics'];
|
||||||
const matches: FieldPurpose[] = [];
|
const matches: FieldPurpose[] = [];
|
||||||
for (const purpose of purposes) {
|
for (const purpose of purposes) {
|
||||||
const ok = await this.privilegesService.checkPermission(
|
const ok = await this.privilegesService.checkAnyPermission(
|
||||||
userId,
|
userId,
|
||||||
fieldPrivilegeKey(resource, purpose),
|
fieldPrivilegeKeys(resource, purpose),
|
||||||
action,
|
action,
|
||||||
);
|
);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
cycleNumberForDate,
|
cycleNumberForDate,
|
||||||
fieldPrivilegeKey,
|
fieldPrivilegeKeys,
|
||||||
isFieldPurpose,
|
isFieldPurpose,
|
||||||
isWeekdayName,
|
isWeekdayName,
|
||||||
noCycleMessage,
|
noCycleMessage,
|
||||||
@@ -19,8 +19,13 @@ describe('field purpose helpers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('maps resource and purpose to privilege keys', () => {
|
it('maps resource and purpose to privilege keys', () => {
|
||||||
expect(fieldPrivilegeKey('cycle', 'sales')).toBe('SALES.CYCLE');
|
expect(fieldPrivilegeKeys('cycle', 'sales')).toEqual([
|
||||||
expect(fieldPrivilegeKey('plan', 'logistics')).toBe('LOGISTICS.PLAN');
|
'ADMIN.SALES.DATA.CYCLE',
|
||||||
|
]);
|
||||||
|
expect(fieldPrivilegeKeys('plan', 'logistics')).toEqual([
|
||||||
|
'ADMIN.LOGISTICS.ACTIVITIES.PLAN',
|
||||||
|
'MOBILE.LOGISTICS.PLAN',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns purpose-specific missing-cycle messages', () => {
|
it('returns purpose-specific missing-cycle messages', () => {
|
||||||
|
|||||||
@@ -22,28 +22,31 @@ export function isWeekdayName(raw: string): raw is WeekdayName {
|
|||||||
return (WEEKDAY_NAMES as readonly string[]).includes(raw);
|
return (WEEKDAY_NAMES as readonly string[]).includes(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SALES_CYCLE_PRIVILEGE_KEY = 'SALES.CYCLE';
|
export const ADMIN_SALES_CYCLE_PRIVILEGE_KEY = 'ADMIN.SALES.DATA.CYCLE';
|
||||||
export const SALES_PLAN_PRIVILEGE_KEY = 'SALES.PLAN';
|
export const ADMIN_SALES_PLAN_PRIVILEGE_KEY = 'ADMIN.SALES.ACTIVITIES.PLAN';
|
||||||
export const LOGISTICS_CYCLE_PRIVILEGE_KEY = 'LOGISTICS.CYCLE';
|
export const ADMIN_LOGISTICS_CYCLE_PRIVILEGE_KEY = 'ADMIN.LOGISTICS.DATA.CYCLE';
|
||||||
export const LOGISTICS_PLAN_PRIVILEGE_KEY = 'LOGISTICS.PLAN';
|
export const ADMIN_LOGISTICS_PLAN_PRIVILEGE_KEY =
|
||||||
export const SETTINGS_PRIVILEGE_KEY = 'CONFIGURATION.SETTING';
|
'ADMIN.LOGISTICS.ACTIVITIES.PLAN';
|
||||||
export const FIELD_ATTENDANCE_PRIVILEGE_KEY = 'FIELD.ATTENDANCE';
|
export const MOBILE_SALES_PLAN_PRIVILEGE_KEY = 'MOBILE.SALES.PLAN';
|
||||||
export const FIELD_VISIT_PRIVILEGE_KEY = 'FIELD.VISIT';
|
export const MOBILE_LOGISTICS_PLAN_PRIVILEGE_KEY = 'MOBILE.LOGISTICS.PLAN';
|
||||||
|
export const SETTINGS_PRIVILEGE_KEY = 'ADMIN.SETTINGS.DATA.SETTING';
|
||||||
|
export const FIELD_ATTENDANCE_PRIVILEGE_KEY = 'MOBILE.SALES.PLAN.ATTENDANCE';
|
||||||
|
export const FIELD_VISIT_PRIVILEGE_KEY = 'MOBILE.SALES.VISIT';
|
||||||
|
|
||||||
export type FieldResource = 'cycle' | 'plan';
|
export type FieldResource = 'cycle' | 'plan';
|
||||||
|
|
||||||
export function fieldPrivilegeKey(
|
export function fieldPrivilegeKeys(
|
||||||
resource: FieldResource,
|
resource: FieldResource,
|
||||||
purpose: FieldPurpose,
|
purpose: FieldPurpose,
|
||||||
): string {
|
): readonly string[] {
|
||||||
if (resource === 'cycle') {
|
if (resource === 'cycle') {
|
||||||
return purpose === 'sales'
|
return purpose === 'sales'
|
||||||
? SALES_CYCLE_PRIVILEGE_KEY
|
? [ADMIN_SALES_CYCLE_PRIVILEGE_KEY]
|
||||||
: LOGISTICS_CYCLE_PRIVILEGE_KEY;
|
: [ADMIN_LOGISTICS_CYCLE_PRIVILEGE_KEY];
|
||||||
}
|
}
|
||||||
return purpose === 'sales'
|
return purpose === 'sales'
|
||||||
? SALES_PLAN_PRIVILEGE_KEY
|
? [ADMIN_SALES_PLAN_PRIVILEGE_KEY, MOBILE_SALES_PLAN_PRIVILEGE_KEY]
|
||||||
: LOGISTICS_PLAN_PRIVILEGE_KEY;
|
: [ADMIN_LOGISTICS_PLAN_PRIVILEGE_KEY, MOBILE_LOGISTICS_PLAN_PRIVILEGE_KEY];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function noCycleMessage(purpose: FieldPurpose): string {
|
export function noCycleMessage(purpose: FieldPurpose): string {
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export class PrivilegeDetailDto {
|
|||||||
@ApiProperty({ format: 'uuid' })
|
@ApiProperty({ format: 'uuid' })
|
||||||
privilegeKeyId!: string;
|
privilegeKeyId!: string;
|
||||||
|
|
||||||
@ApiProperty({ example: 'SALES.INVOICE' })
|
@ApiProperty({ example: 'ADMIN.SALES.ACTIVITIES.INVOICE' })
|
||||||
keyCode!: string;
|
keyCode!: string;
|
||||||
|
|
||||||
@ApiProperty({ example: 'Sales Invoice' })
|
@ApiProperty({ example: 'Sales Invoice' })
|
||||||
@@ -197,7 +197,7 @@ export class PrivilegeKeyDto {
|
|||||||
@ApiProperty({ format: 'uuid' })
|
@ApiProperty({ format: 'uuid' })
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@ApiProperty({ example: 'PRIVILEGES' })
|
@ApiProperty({ example: 'ADMIN.SETTINGS.USER.PRIVILEGES' })
|
||||||
code!: string;
|
code!: string;
|
||||||
|
|
||||||
@ApiProperty({ example: 'Privileges' })
|
@ApiProperty({ example: 'Privileges' })
|
||||||
|
|||||||
@@ -17,14 +17,15 @@ describe('privilege-action', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('privilege-key-code', () => {
|
describe('privilege-key-code', () => {
|
||||||
it('accepts dotted uppercase module levels', () => {
|
it('accepts 3- and 4-part dotted uppercase codes', () => {
|
||||||
expect(isValidPrivilegeKeyCode('PRIVILEGES')).toBe(true);
|
expect(isValidPrivilegeKeyCode('ADMIN.SETTINGS.USER.PRIVILEGES')).toBe(true);
|
||||||
expect(isValidPrivilegeKeyCode('SALES.INVOICE')).toBe(true);
|
expect(isValidPrivilegeKeyCode('ADMIN.SALES.ACTIVITIES.INVOICE')).toBe(true);
|
||||||
expect(isValidPrivilegeKeyCode('SALES.INVOICE.LINE')).toBe(true);
|
expect(isValidPrivilegeKeyCode('MOBILE.SALES.PLAN')).toBe(true);
|
||||||
expect(assertPrivilegeKeyCode('USERS')).toBe('USERS');
|
expect(assertPrivilegeKeyCode('MOBILE.SALES.PLAN')).toBe('MOBILE.SALES.PLAN');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects invalid codes', () => {
|
it('rejects invalid codes', () => {
|
||||||
|
expect(isValidPrivilegeKeyCode('PRIVILEGES')).toBe(false);
|
||||||
expect(isValidPrivilegeKeyCode('sales.invoice')).toBe(false);
|
expect(isValidPrivilegeKeyCode('sales.invoice')).toBe(false);
|
||||||
expect(isValidPrivilegeKeyCode('SALES.')).toBe(false);
|
expect(isValidPrivilegeKeyCode('SALES.')).toBe(false);
|
||||||
expect(isValidPrivilegeKeyCode('.SALES')).toBe(false);
|
expect(isValidPrivilegeKeyCode('.SALES')).toBe(false);
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
assertPrivilegeKeyCode,
|
||||||
|
isValidPrivilegeKeyCode,
|
||||||
|
parsePrivilegeKeyCode,
|
||||||
|
} from './privilege-key-code';
|
||||||
|
|
||||||
|
describe('privilege-key-code', () => {
|
||||||
|
describe('isValidPrivilegeKeyCode', () => {
|
||||||
|
it('accepts 3-part keys', () => {
|
||||||
|
expect(isValidPrivilegeKeyCode('MOBILE.SALES.PLAN')).toBe(true);
|
||||||
|
expect(isValidPrivilegeKeyCode('ADMIN.SALES.REPORT')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts 4-part keys', () => {
|
||||||
|
expect(isValidPrivilegeKeyCode('MOBILE.SALES.PLAN.ATTENDANCE')).toBe(true);
|
||||||
|
expect(isValidPrivilegeKeyCode('ADMIN.SALES.ACTIVITIES.PLAN')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects fewer than 3 segments', () => {
|
||||||
|
expect(isValidPrivilegeKeyCode('PRIVILEGES')).toBe(false);
|
||||||
|
expect(isValidPrivilegeKeyCode('SALES.PLAN')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects more than 4 segments', () => {
|
||||||
|
expect(isValidPrivilegeKeyCode('A.B.C.D.E')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects invalid characters', () => {
|
||||||
|
expect(isValidPrivilegeKeyCode('mobile.sales.plan')).toBe(false);
|
||||||
|
expect(isValidPrivilegeKeyCode('MOBILE..PLAN')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('assertPrivilegeKeyCode', () => {
|
||||||
|
it('returns the code when valid', () => {
|
||||||
|
expect(assertPrivilegeKeyCode('MOBILE.SALES.PLAN')).toBe('MOBILE.SALES.PLAN');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when invalid', () => {
|
||||||
|
expect(() => assertPrivilegeKeyCode('PRIVILEGES')).toThrow(TypeError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parsePrivilegeKeyCode', () => {
|
||||||
|
it('parses a 3-part key with null submodule', () => {
|
||||||
|
expect(parsePrivilegeKeyCode('MOBILE.SALES.PLAN')).toEqual({
|
||||||
|
group: 'MOBILE',
|
||||||
|
parent: 'SALES',
|
||||||
|
module: 'PLAN',
|
||||||
|
submodule: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses a 4-part key', () => {
|
||||||
|
expect(parsePrivilegeKeyCode('MOBILE.SALES.PLAN.ATTENDANCE')).toEqual({
|
||||||
|
group: 'MOBILE',
|
||||||
|
parent: 'SALES',
|
||||||
|
module: 'PLAN',
|
||||||
|
submodule: 'ATTENDANCE',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws for invalid codes', () => {
|
||||||
|
expect(() => parsePrivilegeKeyCode('PRIVILEGES')).toThrow(TypeError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
/** Dotted uppercase module levels: MODULE / MODULE.RESOURCE / MODULE.RESOURCE.SUB */
|
/** Dotted uppercase: Group.Parent.Module or Group.Parent.Module.Submodule */
|
||||||
export const PRIVILEGE_KEY_CODE_PATTERN =
|
export const PRIVILEGE_KEY_CODE_PATTERN =
|
||||||
/^[A-Z][A-Z0-9_]*(\.[A-Z][A-Z0-9_]*)*$/;
|
/^[A-Z][A-Z0-9_]*\.[A-Z][A-Z0-9_]*\.[A-Z][A-Z0-9_]*(?:\.[A-Z][A-Z0-9_]*)?$/;
|
||||||
|
|
||||||
|
export type ParsedPrivilegeKeyCode = {
|
||||||
|
readonly group: string;
|
||||||
|
readonly parent: string;
|
||||||
|
readonly module: string;
|
||||||
|
readonly submodule: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export function isValidPrivilegeKeyCode(code: string): boolean {
|
export function isValidPrivilegeKeyCode(code: string): boolean {
|
||||||
return typeof code === 'string' && PRIVILEGE_KEY_CODE_PATTERN.test(code);
|
return typeof code === 'string' && PRIVILEGE_KEY_CODE_PATTERN.test(code);
|
||||||
@@ -12,3 +19,14 @@ export function assertPrivilegeKeyCode(code: string): string {
|
|||||||
}
|
}
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parsePrivilegeKeyCode(code: string): ParsedPrivilegeKeyCode {
|
||||||
|
assertPrivilegeKeyCode(code);
|
||||||
|
const parts = code.split('.');
|
||||||
|
if (parts.length === 3) {
|
||||||
|
const [group, parent, module] = parts;
|
||||||
|
return { group, parent, module, submodule: null };
|
||||||
|
}
|
||||||
|
const [group, parent, module, submodule] = parts;
|
||||||
|
return { group, parent, module, submodule };
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class PrivilegeKeysController {
|
|||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@Pagination()
|
@Pagination()
|
||||||
@RequirePrivilege('PRIVILEGES', 'view')
|
@RequirePrivilege('ADMIN.SETTINGS.USER.PRIVILEGES', 'view')
|
||||||
@ApiOperation({ summary: 'List privilege keys catalog' })
|
@ApiOperation({ summary: 'List privilege keys catalog' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
schema: {
|
schema: {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export class PrivilegesReadController {
|
|||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@Pagination()
|
@Pagination()
|
||||||
@RequirePrivilege('PRIVILEGES', 'view')
|
@RequirePrivilege('ADMIN.SETTINGS.USER.PRIVILEGES', 'view')
|
||||||
@ApiOperation({ summary: 'List privileges' })
|
@ApiOperation({ summary: 'List privileges' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
schema: {
|
schema: {
|
||||||
@@ -52,7 +52,7 @@ export class PrivilegesReadController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@RequirePrivilege('PRIVILEGES', 'view')
|
@RequirePrivilege('ADMIN.SETTINGS.USER.PRIVILEGES', 'view')
|
||||||
@ApiOperation({ summary: 'Get privilege detail with matrix' })
|
@ApiOperation({ summary: 'Get privilege detail with matrix' })
|
||||||
@ApiOkResponse({ type: PrivilegeDetailResponseDto })
|
@ApiOkResponse({ type: PrivilegeDetailResponseDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export class PrivilegesWriteController {
|
|||||||
constructor(private readonly privilegesService: PrivilegesService) {}
|
constructor(private readonly privilegesService: PrivilegesService) {}
|
||||||
|
|
||||||
@Post('import')
|
@Post('import')
|
||||||
@RequirePrivilege('PRIVILEGES', 'import')
|
@RequirePrivilege('ADMIN.SETTINGS.USER.PRIVILEGES', 'import')
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor('file', {
|
FileInterceptor('file', {
|
||||||
limits: { fileSize: 1_048_576 },
|
limits: { fileSize: 1_048_576 },
|
||||||
@@ -90,7 +90,7 @@ export class PrivilegesWriteController {
|
|||||||
|
|
||||||
@Post('bulk-delete')
|
@Post('bulk-delete')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@RequirePrivilege('PRIVILEGES', 'delete')
|
@RequirePrivilege('ADMIN.SETTINGS.USER.PRIVILEGES', 'delete')
|
||||||
@ApiOperation({ summary: 'Bulk delete privileges' })
|
@ApiOperation({ summary: 'Bulk delete privileges' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
schema: { properties: { deleted: { type: 'number' } } },
|
schema: { properties: { deleted: { type: 'number' } } },
|
||||||
@@ -103,7 +103,7 @@ export class PrivilegesWriteController {
|
|||||||
|
|
||||||
@Post('bulk-status')
|
@Post('bulk-status')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@RequirePrivilege('PRIVILEGES', 'update')
|
@RequirePrivilege('ADMIN.SETTINGS.USER.PRIVILEGES', 'update')
|
||||||
@ApiOperation({ summary: 'Bulk update privilege status' })
|
@ApiOperation({ summary: 'Bulk update privilege status' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
schema: { properties: { updated: { type: 'number' } } },
|
schema: { properties: { updated: { type: 'number' } } },
|
||||||
@@ -118,7 +118,7 @@ export class PrivilegesWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@RequirePrivilege('PRIVILEGES', 'create')
|
@RequirePrivilege('ADMIN.SETTINGS.USER.PRIVILEGES', 'create')
|
||||||
@ApiOperation({ summary: 'Create privilege' })
|
@ApiOperation({ summary: 'Create privilege' })
|
||||||
@ApiCreatedResponse({ type: PrivilegeDetailResponseDto })
|
@ApiCreatedResponse({ type: PrivilegeDetailResponseDto })
|
||||||
@ApiUnauthorizedResponse()
|
@ApiUnauthorizedResponse()
|
||||||
@@ -137,7 +137,7 @@ export class PrivilegesWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id/status')
|
@Patch(':id/status')
|
||||||
@RequirePrivilege('PRIVILEGES', 'update')
|
@RequirePrivilege('ADMIN.SETTINGS.USER.PRIVILEGES', 'update')
|
||||||
@ApiOperation({ summary: 'Update privilege status' })
|
@ApiOperation({ summary: 'Update privilege status' })
|
||||||
@ApiOkResponse({ type: PrivilegeDto })
|
@ApiOkResponse({ type: PrivilegeDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
@@ -152,7 +152,7 @@ export class PrivilegesWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@RequirePrivilege('PRIVILEGES', 'update')
|
@RequirePrivilege('ADMIN.SETTINGS.USER.PRIVILEGES', 'update')
|
||||||
@ApiOperation({ summary: 'Update privilege (not status)' })
|
@ApiOperation({ summary: 'Update privilege (not status)' })
|
||||||
@ApiOkResponse({ type: PrivilegeDetailResponseDto })
|
@ApiOkResponse({ type: PrivilegeDetailResponseDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
@@ -173,7 +173,7 @@ export class PrivilegesWriteController {
|
|||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@HttpCode(204)
|
@HttpCode(204)
|
||||||
@RequirePrivilege('PRIVILEGES', 'delete')
|
@RequirePrivilege('ADMIN.SETTINGS.USER.PRIVILEGES', 'delete')
|
||||||
@ApiOperation({ summary: 'Delete privilege' })
|
@ApiOperation({ summary: 'Delete privilege' })
|
||||||
@ApiNoContentResponse()
|
@ApiNoContentResponse()
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
|
|||||||
@@ -389,6 +389,39 @@ export class PrivilegesRepository {
|
|||||||
return row?.value === true;
|
return row?.value === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkAnyPermission(
|
||||||
|
userId: string,
|
||||||
|
keyCodes: readonly string[],
|
||||||
|
action: PrivilegeAction,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (keyCodes.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const [row] = await this.db
|
||||||
|
.select({ value: privilegeDetails.value })
|
||||||
|
.from(users)
|
||||||
|
.innerJoin(privileges, eq(users.privilegeId, privileges.id))
|
||||||
|
.innerJoin(
|
||||||
|
privilegeDetails,
|
||||||
|
eq(privilegeDetails.privilegeId, privileges.id),
|
||||||
|
)
|
||||||
|
.innerJoin(
|
||||||
|
privilegeKeys,
|
||||||
|
eq(privilegeDetails.privilegeKeyId, privilegeKeys.id),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(users.id, userId),
|
||||||
|
eq(privileges.status, 'active'),
|
||||||
|
inArray(privilegeKeys.code, [...keyCodes]),
|
||||||
|
eq(privilegeDetails.action, action),
|
||||||
|
eq(privilegeDetails.value, true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
return row?.value === true;
|
||||||
|
}
|
||||||
|
|
||||||
async getPermissionsMap(
|
async getPermissionsMap(
|
||||||
privilegeId: string,
|
privilegeId: string,
|
||||||
): Promise<Record<string, Record<PrivilegeAction, boolean>>> {
|
): Promise<Record<string, Record<PrivilegeAction, boolean>>> {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ describe('PrivilegesService', () => {
|
|||||||
| 'listKeys'
|
| 'listKeys'
|
||||||
| 'findKeyById'
|
| 'findKeyById'
|
||||||
| 'checkPermission'
|
| 'checkPermission'
|
||||||
|
| 'checkAnyPermission'
|
||||||
| 'getPermissionsMap'
|
| 'getPermissionsMap'
|
||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
@@ -56,6 +57,7 @@ describe('PrivilegesService', () => {
|
|||||||
listKeys: jest.fn(),
|
listKeys: jest.fn(),
|
||||||
findKeyById: jest.fn(),
|
findKeyById: jest.fn(),
|
||||||
checkPermission: jest.fn(),
|
checkPermission: jest.fn(),
|
||||||
|
checkAnyPermission: jest.fn(),
|
||||||
getPermissionsMap: jest.fn(),
|
getPermissionsMap: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -140,7 +142,44 @@ describe('PrivilegesService', () => {
|
|||||||
it('checkPermission delegates', async () => {
|
it('checkPermission delegates', async () => {
|
||||||
repository.checkPermission.mockResolvedValue(true);
|
repository.checkPermission.mockResolvedValue(true);
|
||||||
await expect(
|
await expect(
|
||||||
service.checkPermission('user-1', 'PRIVILEGES', 'view'),
|
service.checkPermission(
|
||||||
|
'user-1',
|
||||||
|
'ADMIN.SETTINGS.USER.PRIVILEGES',
|
||||||
|
'view',
|
||||||
|
),
|
||||||
).resolves.toBe(true);
|
).resolves.toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('checkAnyPermission delegates for multiple keys', async () => {
|
||||||
|
repository.checkAnyPermission.mockResolvedValue(true);
|
||||||
|
await expect(
|
||||||
|
service.checkAnyPermission(
|
||||||
|
'user-1',
|
||||||
|
['ADMIN.SALES.ACTIVITIES.PLAN', 'MOBILE.SALES.PLAN'],
|
||||||
|
'view',
|
||||||
|
),
|
||||||
|
).resolves.toBe(true);
|
||||||
|
expect(repository.checkAnyPermission).toHaveBeenCalledWith(
|
||||||
|
'user-1',
|
||||||
|
['ADMIN.SALES.ACTIVITIES.PLAN', 'MOBILE.SALES.PLAN'],
|
||||||
|
'view',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('checkAnyPermission uses checkPermission for a single key', async () => {
|
||||||
|
repository.checkPermission.mockResolvedValue(true);
|
||||||
|
await expect(
|
||||||
|
service.checkAnyPermission(
|
||||||
|
'user-1',
|
||||||
|
['MOBILE.SALES.PLAN'],
|
||||||
|
'view',
|
||||||
|
),
|
||||||
|
).resolves.toBe(true);
|
||||||
|
expect(repository.checkPermission).toHaveBeenCalledWith(
|
||||||
|
'user-1',
|
||||||
|
'MOBILE.SALES.PLAN',
|
||||||
|
'view',
|
||||||
|
);
|
||||||
|
expect(repository.checkAnyPermission).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -262,6 +262,24 @@ export class PrivilegesService {
|
|||||||
return this.privilegesRepository.checkPermission(userId, keyCode, action);
|
return this.privilegesRepository.checkPermission(userId, keyCode, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkAnyPermission(
|
||||||
|
userId: string,
|
||||||
|
keyCodes: readonly string[],
|
||||||
|
action: PrivilegeAction,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (keyCodes.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (keyCodes.length === 1) {
|
||||||
|
return this.checkPermission(userId, keyCodes[0], action);
|
||||||
|
}
|
||||||
|
return this.privilegesRepository.checkAnyPermission(
|
||||||
|
userId,
|
||||||
|
keyCodes,
|
||||||
|
action,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async getPermissionsMap(
|
async getPermissionsMap(
|
||||||
privilegeId: string,
|
privilegeId: string,
|
||||||
): Promise<Record<string, Record<PrivilegeAction, boolean>>> {
|
): Promise<Record<string, Record<PrivilegeAction, boolean>>> {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const LOGISTICS_PREFIX = `${REPORT_GROUP.LOGISTICS_REPORT}__`;
|
|||||||
export const packingSlipReport: ReportConfigEntity = {
|
export const packingSlipReport: ReportConfigEntity = {
|
||||||
groupName: REPORT_GROUP.LOGISTICS_REPORT,
|
groupName: REPORT_GROUP.LOGISTICS_REPORT,
|
||||||
uniqueName: `${LOGISTICS_PREFIX}packing_slip`,
|
uniqueName: `${LOGISTICS_PREFIX}packing_slip`,
|
||||||
privilegeKey: 'LOGISTICS.REPORT',
|
privilegeKey: 'ADMIN.LOGISTICS.REPORT',
|
||||||
label: 'Report Packing Slip',
|
label: 'Report Packing Slip',
|
||||||
tableSchema: `packing_slips main
|
tableSchema: `packing_slips main
|
||||||
JOIN customers cust ON cust.id = main.customer_id`,
|
JOIN customers cust ON cust.id = main.customer_id`,
|
||||||
@@ -89,7 +89,7 @@ export const packingSlipReport: ReportConfigEntity = {
|
|||||||
export const deliveryPlanReport: ReportConfigEntity = {
|
export const deliveryPlanReport: ReportConfigEntity = {
|
||||||
groupName: REPORT_GROUP.LOGISTICS_REPORT,
|
groupName: REPORT_GROUP.LOGISTICS_REPORT,
|
||||||
uniqueName: `${LOGISTICS_PREFIX}delivery_plan`,
|
uniqueName: `${LOGISTICS_PREFIX}delivery_plan`,
|
||||||
privilegeKey: 'LOGISTICS.REPORT',
|
privilegeKey: 'ADMIN.LOGISTICS.REPORT',
|
||||||
label: 'Report Delivery Plan',
|
label: 'Report Delivery Plan',
|
||||||
tableSchema: `plans main
|
tableSchema: `plans main
|
||||||
JOIN employees emp ON emp.id = main.employee_id
|
JOIN employees emp ON emp.id = main.employee_id
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const LOGISTICS_PREFIX = `${REPORT_GROUP.LOGISTICS_REPORT}__`;
|
|||||||
export const salesOrderReport: ReportConfigEntity = {
|
export const salesOrderReport: ReportConfigEntity = {
|
||||||
groupName: REPORT_GROUP.SALES_REPORT,
|
groupName: REPORT_GROUP.SALES_REPORT,
|
||||||
uniqueName: `${SALES_PREFIX}sales_order`,
|
uniqueName: `${SALES_PREFIX}sales_order`,
|
||||||
privilegeKey: 'SALES.REPORT',
|
privilegeKey: 'ADMIN.SALES.REPORT',
|
||||||
label: 'Report Sales Order',
|
label: 'Report Sales Order',
|
||||||
tableSchema: `sales_orders main
|
tableSchema: `sales_orders main
|
||||||
JOIN customers cust ON cust.id = main.customer_id
|
JOIN customers cust ON cust.id = main.customer_id
|
||||||
@@ -138,7 +138,7 @@ export const salesOrderReport: ReportConfigEntity = {
|
|||||||
export const salesRequestReport: ReportConfigEntity = {
|
export const salesRequestReport: ReportConfigEntity = {
|
||||||
groupName: REPORT_GROUP.SALES_REPORT,
|
groupName: REPORT_GROUP.SALES_REPORT,
|
||||||
uniqueName: `${SALES_PREFIX}sales_request`,
|
uniqueName: `${SALES_PREFIX}sales_request`,
|
||||||
privilegeKey: 'SALES.REPORT',
|
privilegeKey: 'ADMIN.SALES.REPORT',
|
||||||
label: 'Report Request Order',
|
label: 'Report Request Order',
|
||||||
tableSchema: `sales_requests main
|
tableSchema: `sales_requests main
|
||||||
JOIN customers cust ON cust.id = main.customer_id
|
JOIN customers cust ON cust.id = main.customer_id
|
||||||
@@ -250,7 +250,7 @@ export const salesRequestReport: ReportConfigEntity = {
|
|||||||
export const salesInvoiceReport: ReportConfigEntity = {
|
export const salesInvoiceReport: ReportConfigEntity = {
|
||||||
groupName: REPORT_GROUP.SALES_REPORT,
|
groupName: REPORT_GROUP.SALES_REPORT,
|
||||||
uniqueName: `${SALES_PREFIX}sales_invoice`,
|
uniqueName: `${SALES_PREFIX}sales_invoice`,
|
||||||
privilegeKey: 'SALES.REPORT',
|
privilegeKey: 'ADMIN.SALES.REPORT',
|
||||||
label: 'Report Invoice',
|
label: 'Report Invoice',
|
||||||
tableSchema: `sales_invoices main
|
tableSchema: `sales_invoices main
|
||||||
JOIN customers cust ON cust.id = main.customer_id
|
JOIN customers cust ON cust.id = main.customer_id
|
||||||
@@ -365,7 +365,7 @@ export const salesInvoiceReport: ReportConfigEntity = {
|
|||||||
export const salesPaymentReport: ReportConfigEntity = {
|
export const salesPaymentReport: ReportConfigEntity = {
|
||||||
groupName: REPORT_GROUP.SALES_REPORT,
|
groupName: REPORT_GROUP.SALES_REPORT,
|
||||||
uniqueName: `${SALES_PREFIX}sales_payment`,
|
uniqueName: `${SALES_PREFIX}sales_payment`,
|
||||||
privilegeKey: 'SALES.REPORT',
|
privilegeKey: 'ADMIN.SALES.REPORT',
|
||||||
label: 'Report Payment',
|
label: 'Report Payment',
|
||||||
tableSchema: `sales_payments main
|
tableSchema: `sales_payments main
|
||||||
JOIN sales_payment_invoices spi ON spi.sales_payment_id = main.id
|
JOIN sales_payment_invoices spi ON spi.sales_payment_id = main.id
|
||||||
@@ -476,7 +476,7 @@ export const salesPaymentReport: ReportConfigEntity = {
|
|||||||
export const visitPlanReport: ReportConfigEntity = {
|
export const visitPlanReport: ReportConfigEntity = {
|
||||||
groupName: REPORT_GROUP.SALES_REPORT,
|
groupName: REPORT_GROUP.SALES_REPORT,
|
||||||
uniqueName: `${SALES_PREFIX}visit_plan`,
|
uniqueName: `${SALES_PREFIX}visit_plan`,
|
||||||
privilegeKey: 'SALES.REPORT',
|
privilegeKey: 'ADMIN.SALES.REPORT',
|
||||||
label: 'Report Visit Plan',
|
label: 'Report Visit Plan',
|
||||||
tableSchema: `plans main
|
tableSchema: `plans main
|
||||||
JOIN employees emp ON emp.id = main.employee_id
|
JOIN employees emp ON emp.id = main.employee_id
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
} from './dto/packing-slip.dto';
|
} from './dto/packing-slip.dto';
|
||||||
import { PackingSlipsService } from './packing-slips.service';
|
import { PackingSlipsService } from './packing-slips.service';
|
||||||
|
|
||||||
export const PACKING_SLIP_PRIVILEGE_KEY = 'SALES.PACKING_SLIP';
|
export const PACKING_SLIP_PRIVILEGE_KEY = 'ADMIN.LOGISTICS.ACTIVITIES.PACKING_SLIP';
|
||||||
|
|
||||||
@ApiTags('packing-slips')
|
@ApiTags('packing-slips')
|
||||||
@ApiBearerAuth(BEARER_AUTH_NAME)
|
@ApiBearerAuth(BEARER_AUTH_NAME)
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ import {
|
|||||||
} from './dto/sales-invoice.dto';
|
} from './dto/sales-invoice.dto';
|
||||||
import { SalesInvoicesService } from './sales-invoices.service';
|
import { SalesInvoicesService } from './sales-invoices.service';
|
||||||
|
|
||||||
export const SALES_INVOICE_PRIVILEGE_KEY = 'SALES.INVOICE';
|
export const SALES_INVOICE_PRIVILEGE_KEYS = [
|
||||||
|
'ADMIN.SALES.ACTIVITIES.INVOICE',
|
||||||
|
'MOBILE.SALES.INVOICE',
|
||||||
|
] as const;
|
||||||
|
|
||||||
@ApiTags('sales-invoices')
|
@ApiTags('sales-invoices')
|
||||||
@ApiBearerAuth(BEARER_AUTH_NAME)
|
@ApiBearerAuth(BEARER_AUTH_NAME)
|
||||||
@@ -31,7 +34,7 @@ export class SalesInvoicesReadController {
|
|||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@Pagination()
|
@Pagination()
|
||||||
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEY, 'view')
|
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEYS, 'view')
|
||||||
@ApiOperation({ summary: 'List sales invoices' })
|
@ApiOperation({ summary: 'List sales invoices' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
schema: {
|
schema: {
|
||||||
@@ -53,7 +56,7 @@ export class SalesInvoicesReadController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEY, 'view')
|
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEYS, 'view')
|
||||||
@ApiOperation({ summary: 'Get sales invoice detail' })
|
@ApiOperation({ summary: 'Get sales invoice detail' })
|
||||||
@ApiOkResponse({ type: SalesInvoiceDto })
|
@ApiOkResponse({ type: SalesInvoiceDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { CurrentUser } from '../../../common/decorators/current-user.decorator';
|
|||||||
import { RequirePrivilege } from '../../../common/decorators/require-privilege.decorator';
|
import { RequirePrivilege } from '../../../common/decorators/require-privilege.decorator';
|
||||||
import { BEARER_AUTH_NAME } from '../../../common/swagger/setup-swagger';
|
import { BEARER_AUTH_NAME } from '../../../common/swagger/setup-swagger';
|
||||||
import { isAllowedCsvUpload } from '../shared/sales-fields';
|
import { isAllowedCsvUpload } from '../shared/sales-fields';
|
||||||
import { SALES_INVOICE_PRIVILEGE_KEY } from './sales-invoices-read.controller';
|
import { SALES_INVOICE_PRIVILEGE_KEYS } from './sales-invoices-read.controller';
|
||||||
import { SalesInvoicesService } from './sales-invoices.service';
|
import { SalesInvoicesService } from './sales-invoices.service';
|
||||||
import {
|
import {
|
||||||
BulkIdsDto,
|
BulkIdsDto,
|
||||||
@@ -47,7 +47,7 @@ export class SalesInvoicesWriteController {
|
|||||||
constructor(private readonly salesInvoicesService: SalesInvoicesService) {}
|
constructor(private readonly salesInvoicesService: SalesInvoicesService) {}
|
||||||
|
|
||||||
@Post('import')
|
@Post('import')
|
||||||
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEY, 'import')
|
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEYS, 'import')
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor('file', {
|
FileInterceptor('file', {
|
||||||
limits: { fileSize: 1_048_576 },
|
limits: { fileSize: 1_048_576 },
|
||||||
@@ -86,7 +86,7 @@ export class SalesInvoicesWriteController {
|
|||||||
|
|
||||||
@Post('bulk-delete')
|
@Post('bulk-delete')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEY, 'delete')
|
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEYS, 'delete')
|
||||||
@ApiOperation({ summary: 'Bulk delete sales invoices' })
|
@ApiOperation({ summary: 'Bulk delete sales invoices' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
schema: { properties: { deleted: { type: 'number' } } },
|
schema: { properties: { deleted: { type: 'number' } } },
|
||||||
@@ -99,7 +99,7 @@ export class SalesInvoicesWriteController {
|
|||||||
|
|
||||||
@Post('bulk-status')
|
@Post('bulk-status')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEY, 'update')
|
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEYS, 'update')
|
||||||
@ApiOperation({ summary: 'Bulk update sales invoice status' })
|
@ApiOperation({ summary: 'Bulk update sales invoice status' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
schema: { properties: { updated: { type: 'number' } } },
|
schema: { properties: { updated: { type: 'number' } } },
|
||||||
@@ -118,7 +118,7 @@ export class SalesInvoicesWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEY, 'create')
|
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEYS, 'create')
|
||||||
@ApiOperation({ summary: 'Create sales invoice' })
|
@ApiOperation({ summary: 'Create sales invoice' })
|
||||||
@ApiCreatedResponse({ type: SalesInvoiceDto })
|
@ApiCreatedResponse({ type: SalesInvoiceDto })
|
||||||
@ApiUnauthorizedResponse()
|
@ApiUnauthorizedResponse()
|
||||||
@@ -147,7 +147,7 @@ export class SalesInvoicesWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id/status')
|
@Patch(':id/status')
|
||||||
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEY, 'update')
|
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEYS, 'update')
|
||||||
@ApiOperation({ summary: 'Update sales invoice status' })
|
@ApiOperation({ summary: 'Update sales invoice status' })
|
||||||
@ApiOkResponse({ type: SalesInvoiceDto })
|
@ApiOkResponse({ type: SalesInvoiceDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
@@ -162,7 +162,7 @@ export class SalesInvoicesWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEY, 'update')
|
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEYS, 'update')
|
||||||
@ApiOperation({ summary: 'Update sales invoice (not status)' })
|
@ApiOperation({ summary: 'Update sales invoice (not status)' })
|
||||||
@ApiOkResponse({ type: SalesInvoiceDto })
|
@ApiOkResponse({ type: SalesInvoiceDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
@@ -193,7 +193,7 @@ export class SalesInvoicesWriteController {
|
|||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@HttpCode(204)
|
@HttpCode(204)
|
||||||
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEY, 'delete')
|
@RequirePrivilege(SALES_INVOICE_PRIVILEGE_KEYS, 'delete')
|
||||||
@ApiOperation({ summary: 'Delete sales invoice' })
|
@ApiOperation({ summary: 'Delete sales invoice' })
|
||||||
@ApiNoContentResponse()
|
@ApiNoContentResponse()
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ import { BEARER_AUTH_NAME } from '../../../common/swagger/setup-swagger';
|
|||||||
import { SalesOrderDto, ListSalesOrdersQueryDto } from './dto/sales-order.dto';
|
import { SalesOrderDto, ListSalesOrdersQueryDto } from './dto/sales-order.dto';
|
||||||
import { SalesOrdersService } from './sales-orders.service';
|
import { SalesOrdersService } from './sales-orders.service';
|
||||||
|
|
||||||
export const SALES_ORDER_PRIVILEGE_KEY = 'SALES.ORDER';
|
export const SALES_ORDER_PRIVILEGE_KEYS = [
|
||||||
|
'ADMIN.SALES.ACTIVITIES.ORDER',
|
||||||
|
'MOBILE.SALES.ORDER',
|
||||||
|
] as const;
|
||||||
|
|
||||||
@ApiTags('sales-orders')
|
@ApiTags('sales-orders')
|
||||||
@ApiBearerAuth(BEARER_AUTH_NAME)
|
@ApiBearerAuth(BEARER_AUTH_NAME)
|
||||||
@@ -28,7 +31,7 @@ export class SalesOrdersReadController {
|
|||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@Pagination()
|
@Pagination()
|
||||||
@RequirePrivilege(SALES_ORDER_PRIVILEGE_KEY, 'view')
|
@RequirePrivilege(SALES_ORDER_PRIVILEGE_KEYS, 'view')
|
||||||
@ApiOperation({ summary: 'List sales orders' })
|
@ApiOperation({ summary: 'List sales orders' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
schema: {
|
schema: {
|
||||||
@@ -50,7 +53,7 @@ export class SalesOrdersReadController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@RequirePrivilege(SALES_ORDER_PRIVILEGE_KEY, 'view')
|
@RequirePrivilege(SALES_ORDER_PRIVILEGE_KEYS, 'view')
|
||||||
@ApiOperation({ summary: 'Get sales order detail' })
|
@ApiOperation({ summary: 'Get sales order detail' })
|
||||||
@ApiOkResponse({ type: SalesOrderDto })
|
@ApiOkResponse({ type: SalesOrderDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { CurrentUser } from '../../../common/decorators/current-user.decorator';
|
|||||||
import { RequirePrivilege } from '../../../common/decorators/require-privilege.decorator';
|
import { RequirePrivilege } from '../../../common/decorators/require-privilege.decorator';
|
||||||
import { BEARER_AUTH_NAME } from '../../../common/swagger/setup-swagger';
|
import { BEARER_AUTH_NAME } from '../../../common/swagger/setup-swagger';
|
||||||
import { isAllowedCsvUpload } from '../shared/sales-fields';
|
import { isAllowedCsvUpload } from '../shared/sales-fields';
|
||||||
import { SALES_ORDER_PRIVILEGE_KEY } from './sales-orders-read.controller';
|
import { SALES_ORDER_PRIVILEGE_KEYS } from './sales-orders-read.controller';
|
||||||
import { SalesOrdersService } from './sales-orders.service';
|
import { SalesOrdersService } from './sales-orders.service';
|
||||||
import {
|
import {
|
||||||
BulkIdsDto,
|
BulkIdsDto,
|
||||||
@@ -47,7 +47,7 @@ export class SalesOrdersWriteController {
|
|||||||
constructor(private readonly salesOrdersService: SalesOrdersService) {}
|
constructor(private readonly salesOrdersService: SalesOrdersService) {}
|
||||||
|
|
||||||
@Post('import')
|
@Post('import')
|
||||||
@RequirePrivilege(SALES_ORDER_PRIVILEGE_KEY, 'import')
|
@RequirePrivilege(SALES_ORDER_PRIVILEGE_KEYS, 'import')
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor('file', {
|
FileInterceptor('file', {
|
||||||
limits: { fileSize: 1_048_576 },
|
limits: { fileSize: 1_048_576 },
|
||||||
@@ -86,7 +86,7 @@ export class SalesOrdersWriteController {
|
|||||||
|
|
||||||
@Post('bulk-delete')
|
@Post('bulk-delete')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@RequirePrivilege(SALES_ORDER_PRIVILEGE_KEY, 'delete')
|
@RequirePrivilege(SALES_ORDER_PRIVILEGE_KEYS, 'delete')
|
||||||
@ApiOperation({ summary: 'Bulk delete sales orders' })
|
@ApiOperation({ summary: 'Bulk delete sales orders' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
schema: { properties: { deleted: { type: 'number' } } },
|
schema: { properties: { deleted: { type: 'number' } } },
|
||||||
@@ -99,7 +99,7 @@ export class SalesOrdersWriteController {
|
|||||||
|
|
||||||
@Post('bulk-status')
|
@Post('bulk-status')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@RequirePrivilege(SALES_ORDER_PRIVILEGE_KEY, 'update')
|
@RequirePrivilege(SALES_ORDER_PRIVILEGE_KEYS, 'update')
|
||||||
@ApiOperation({ summary: 'Bulk update sales order status' })
|
@ApiOperation({ summary: 'Bulk update sales order status' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
schema: { properties: { updated: { type: 'number' } } },
|
schema: { properties: { updated: { type: 'number' } } },
|
||||||
@@ -119,7 +119,7 @@ export class SalesOrdersWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@RequirePrivilege(SALES_ORDER_PRIVILEGE_KEY, 'create')
|
@RequirePrivilege(SALES_ORDER_PRIVILEGE_KEYS, 'create')
|
||||||
@ApiOperation({ summary: 'Create sales order' })
|
@ApiOperation({ summary: 'Create sales order' })
|
||||||
@ApiCreatedResponse({ type: SalesOrderDto })
|
@ApiCreatedResponse({ type: SalesOrderDto })
|
||||||
@ApiUnauthorizedResponse()
|
@ApiUnauthorizedResponse()
|
||||||
@@ -148,7 +148,7 @@ export class SalesOrdersWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id/status')
|
@Patch(':id/status')
|
||||||
@RequirePrivilege(SALES_ORDER_PRIVILEGE_KEY, 'update')
|
@RequirePrivilege(SALES_ORDER_PRIVILEGE_KEYS, 'update')
|
||||||
@ApiOperation({ summary: 'Update sales order status' })
|
@ApiOperation({ summary: 'Update sales order status' })
|
||||||
@ApiOkResponse({ type: SalesOrderDto })
|
@ApiOkResponse({ type: SalesOrderDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
@@ -165,7 +165,7 @@ export class SalesOrdersWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@RequirePrivilege(SALES_ORDER_PRIVILEGE_KEY, 'update')
|
@RequirePrivilege(SALES_ORDER_PRIVILEGE_KEYS, 'update')
|
||||||
@ApiOperation({ summary: 'Update sales order (not status)' })
|
@ApiOperation({ summary: 'Update sales order (not status)' })
|
||||||
@ApiOkResponse({ type: SalesOrderDto })
|
@ApiOkResponse({ type: SalesOrderDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
@@ -195,7 +195,7 @@ export class SalesOrdersWriteController {
|
|||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@HttpCode(204)
|
@HttpCode(204)
|
||||||
@RequirePrivilege(SALES_ORDER_PRIVILEGE_KEY, 'delete')
|
@RequirePrivilege(SALES_ORDER_PRIVILEGE_KEYS, 'delete')
|
||||||
@ApiOperation({ summary: 'Delete sales order' })
|
@ApiOperation({ summary: 'Delete sales order' })
|
||||||
@ApiNoContentResponse()
|
@ApiNoContentResponse()
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ import {
|
|||||||
} from './dto/sales-payment.dto';
|
} from './dto/sales-payment.dto';
|
||||||
import { SalesPaymentsService } from './sales-payments.service';
|
import { SalesPaymentsService } from './sales-payments.service';
|
||||||
|
|
||||||
export const SALES_PAYMENT_PRIVILEGE_KEY = 'SALES.PAYMENT';
|
export const SALES_PAYMENT_PRIVILEGE_KEYS = [
|
||||||
|
'ADMIN.SALES.ACTIVITIES.PAYMENT',
|
||||||
|
'MOBILE.SALES.PAYMENT',
|
||||||
|
] as const;
|
||||||
|
|
||||||
@ApiTags('sales-payments')
|
@ApiTags('sales-payments')
|
||||||
@ApiBearerAuth(BEARER_AUTH_NAME)
|
@ApiBearerAuth(BEARER_AUTH_NAME)
|
||||||
@@ -31,7 +34,7 @@ export class SalesPaymentsReadController {
|
|||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@Pagination()
|
@Pagination()
|
||||||
@RequirePrivilege(SALES_PAYMENT_PRIVILEGE_KEY, 'view')
|
@RequirePrivilege(SALES_PAYMENT_PRIVILEGE_KEYS, 'view')
|
||||||
@ApiOperation({ summary: 'List sales payments' })
|
@ApiOperation({ summary: 'List sales payments' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
schema: {
|
schema: {
|
||||||
@@ -53,7 +56,7 @@ export class SalesPaymentsReadController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@RequirePrivilege(SALES_PAYMENT_PRIVILEGE_KEY, 'view')
|
@RequirePrivilege(SALES_PAYMENT_PRIVILEGE_KEYS, 'view')
|
||||||
@ApiOperation({ summary: 'Get sales payment detail' })
|
@ApiOperation({ summary: 'Get sales payment detail' })
|
||||||
@ApiOkResponse({ type: SalesPaymentDto })
|
@ApiOkResponse({ type: SalesPaymentDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { CurrentUser } from '../../../common/decorators/current-user.decorator';
|
|||||||
import { RequirePrivilege } from '../../../common/decorators/require-privilege.decorator';
|
import { RequirePrivilege } from '../../../common/decorators/require-privilege.decorator';
|
||||||
import { BEARER_AUTH_NAME } from '../../../common/swagger/setup-swagger';
|
import { BEARER_AUTH_NAME } from '../../../common/swagger/setup-swagger';
|
||||||
import { isAllowedCsvUpload } from '../shared/sales-fields';
|
import { isAllowedCsvUpload } from '../shared/sales-fields';
|
||||||
import { SALES_PAYMENT_PRIVILEGE_KEY } from './sales-payments-read.controller';
|
import { SALES_PAYMENT_PRIVILEGE_KEYS } from './sales-payments-read.controller';
|
||||||
import { SalesPaymentsService } from './sales-payments.service';
|
import { SalesPaymentsService } from './sales-payments.service';
|
||||||
import {
|
import {
|
||||||
BulkIdsDto,
|
BulkIdsDto,
|
||||||
@@ -47,7 +47,7 @@ export class SalesPaymentsWriteController {
|
|||||||
constructor(private readonly salesPaymentsService: SalesPaymentsService) {}
|
constructor(private readonly salesPaymentsService: SalesPaymentsService) {}
|
||||||
|
|
||||||
@Post('import')
|
@Post('import')
|
||||||
@RequirePrivilege(SALES_PAYMENT_PRIVILEGE_KEY, 'import')
|
@RequirePrivilege(SALES_PAYMENT_PRIVILEGE_KEYS, 'import')
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor('file', {
|
FileInterceptor('file', {
|
||||||
limits: { fileSize: 1_048_576 },
|
limits: { fileSize: 1_048_576 },
|
||||||
@@ -86,7 +86,7 @@ export class SalesPaymentsWriteController {
|
|||||||
|
|
||||||
@Post('bulk-delete')
|
@Post('bulk-delete')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@RequirePrivilege(SALES_PAYMENT_PRIVILEGE_KEY, 'delete')
|
@RequirePrivilege(SALES_PAYMENT_PRIVILEGE_KEYS, 'delete')
|
||||||
@ApiOperation({ summary: 'Bulk delete sales payments' })
|
@ApiOperation({ summary: 'Bulk delete sales payments' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
schema: { properties: { deleted: { type: 'number' } } },
|
schema: { properties: { deleted: { type: 'number' } } },
|
||||||
@@ -99,7 +99,7 @@ export class SalesPaymentsWriteController {
|
|||||||
|
|
||||||
@Post('bulk-status')
|
@Post('bulk-status')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@RequirePrivilege(SALES_PAYMENT_PRIVILEGE_KEY, 'update')
|
@RequirePrivilege(SALES_PAYMENT_PRIVILEGE_KEYS, 'update')
|
||||||
@ApiOperation({ summary: 'Bulk update sales payment status' })
|
@ApiOperation({ summary: 'Bulk update sales payment status' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
schema: { properties: { updated: { type: 'number' } } },
|
schema: { properties: { updated: { type: 'number' } } },
|
||||||
@@ -118,7 +118,7 @@ export class SalesPaymentsWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@RequirePrivilege(SALES_PAYMENT_PRIVILEGE_KEY, 'create')
|
@RequirePrivilege(SALES_PAYMENT_PRIVILEGE_KEYS, 'create')
|
||||||
@ApiOperation({ summary: 'Create sales payment' })
|
@ApiOperation({ summary: 'Create sales payment' })
|
||||||
@ApiCreatedResponse({ type: SalesPaymentDto })
|
@ApiCreatedResponse({ type: SalesPaymentDto })
|
||||||
@ApiUnauthorizedResponse()
|
@ApiUnauthorizedResponse()
|
||||||
@@ -139,7 +139,7 @@ export class SalesPaymentsWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id/status')
|
@Patch(':id/status')
|
||||||
@RequirePrivilege(SALES_PAYMENT_PRIVILEGE_KEY, 'update')
|
@RequirePrivilege(SALES_PAYMENT_PRIVILEGE_KEYS, 'update')
|
||||||
@ApiOperation({ summary: 'Update sales payment status' })
|
@ApiOperation({ summary: 'Update sales payment status' })
|
||||||
@ApiOkResponse({ type: SalesPaymentDto })
|
@ApiOkResponse({ type: SalesPaymentDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
@@ -154,7 +154,7 @@ export class SalesPaymentsWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@RequirePrivilege(SALES_PAYMENT_PRIVILEGE_KEY, 'update')
|
@RequirePrivilege(SALES_PAYMENT_PRIVILEGE_KEYS, 'update')
|
||||||
@ApiOperation({ summary: 'Update sales payment (not status)' })
|
@ApiOperation({ summary: 'Update sales payment (not status)' })
|
||||||
@ApiOkResponse({ type: SalesPaymentDto })
|
@ApiOkResponse({ type: SalesPaymentDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
@@ -177,7 +177,7 @@ export class SalesPaymentsWriteController {
|
|||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@HttpCode(204)
|
@HttpCode(204)
|
||||||
@RequirePrivilege(SALES_PAYMENT_PRIVILEGE_KEY, 'delete')
|
@RequirePrivilege(SALES_PAYMENT_PRIVILEGE_KEYS, 'delete')
|
||||||
@ApiOperation({ summary: 'Delete sales payment' })
|
@ApiOperation({ summary: 'Delete sales payment' })
|
||||||
@ApiNoContentResponse()
|
@ApiNoContentResponse()
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ import {
|
|||||||
} from './dto/sales-request.dto';
|
} from './dto/sales-request.dto';
|
||||||
import { SalesRequestsService } from './sales-requests.service';
|
import { SalesRequestsService } from './sales-requests.service';
|
||||||
|
|
||||||
export const SALES_REQUEST_PRIVILEGE_KEY = 'SALES.REQUEST';
|
export const SALES_REQUEST_PRIVILEGE_KEYS = [
|
||||||
|
'ADMIN.SALES.ACTIVITIES.REQUEST',
|
||||||
|
'MOBILE.SALES.REQUEST',
|
||||||
|
] as const;
|
||||||
|
|
||||||
@ApiTags('sales-requests')
|
@ApiTags('sales-requests')
|
||||||
@ApiBearerAuth(BEARER_AUTH_NAME)
|
@ApiBearerAuth(BEARER_AUTH_NAME)
|
||||||
@@ -31,7 +34,7 @@ export class SalesRequestsReadController {
|
|||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@Pagination()
|
@Pagination()
|
||||||
@RequirePrivilege(SALES_REQUEST_PRIVILEGE_KEY, 'view')
|
@RequirePrivilege(SALES_REQUEST_PRIVILEGE_KEYS, 'view')
|
||||||
@ApiOperation({ summary: 'List sales requests' })
|
@ApiOperation({ summary: 'List sales requests' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
schema: {
|
schema: {
|
||||||
@@ -53,7 +56,7 @@ export class SalesRequestsReadController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@RequirePrivilege(SALES_REQUEST_PRIVILEGE_KEY, 'view')
|
@RequirePrivilege(SALES_REQUEST_PRIVILEGE_KEYS, 'view')
|
||||||
@ApiOperation({ summary: 'Get sales request detail' })
|
@ApiOperation({ summary: 'Get sales request detail' })
|
||||||
@ApiOkResponse({ type: SalesRequestDto })
|
@ApiOkResponse({ type: SalesRequestDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { CurrentUser } from '../../../common/decorators/current-user.decorator';
|
|||||||
import { RequirePrivilege } from '../../../common/decorators/require-privilege.decorator';
|
import { RequirePrivilege } from '../../../common/decorators/require-privilege.decorator';
|
||||||
import { BEARER_AUTH_NAME } from '../../../common/swagger/setup-swagger';
|
import { BEARER_AUTH_NAME } from '../../../common/swagger/setup-swagger';
|
||||||
import { isAllowedCsvUpload } from '../shared/sales-fields';
|
import { isAllowedCsvUpload } from '../shared/sales-fields';
|
||||||
import { SALES_REQUEST_PRIVILEGE_KEY } from './sales-requests-read.controller';
|
import { SALES_REQUEST_PRIVILEGE_KEYS } from './sales-requests-read.controller';
|
||||||
import { SalesRequestsService } from './sales-requests.service';
|
import { SalesRequestsService } from './sales-requests.service';
|
||||||
import {
|
import {
|
||||||
BulkIdsDto,
|
BulkIdsDto,
|
||||||
@@ -47,7 +47,7 @@ export class SalesRequestsWriteController {
|
|||||||
constructor(private readonly salesRequestsService: SalesRequestsService) {}
|
constructor(private readonly salesRequestsService: SalesRequestsService) {}
|
||||||
|
|
||||||
@Post('import')
|
@Post('import')
|
||||||
@RequirePrivilege(SALES_REQUEST_PRIVILEGE_KEY, 'import')
|
@RequirePrivilege(SALES_REQUEST_PRIVILEGE_KEYS, 'import')
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor('file', {
|
FileInterceptor('file', {
|
||||||
limits: { fileSize: 1_048_576 },
|
limits: { fileSize: 1_048_576 },
|
||||||
@@ -86,7 +86,7 @@ export class SalesRequestsWriteController {
|
|||||||
|
|
||||||
@Post('bulk-delete')
|
@Post('bulk-delete')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@RequirePrivilege(SALES_REQUEST_PRIVILEGE_KEY, 'delete')
|
@RequirePrivilege(SALES_REQUEST_PRIVILEGE_KEYS, 'delete')
|
||||||
@ApiOperation({ summary: 'Bulk delete sales requests' })
|
@ApiOperation({ summary: 'Bulk delete sales requests' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
schema: { properties: { deleted: { type: 'number' } } },
|
schema: { properties: { deleted: { type: 'number' } } },
|
||||||
@@ -99,7 +99,7 @@ export class SalesRequestsWriteController {
|
|||||||
|
|
||||||
@Post('bulk-status')
|
@Post('bulk-status')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@RequirePrivilege(SALES_REQUEST_PRIVILEGE_KEY, 'update')
|
@RequirePrivilege(SALES_REQUEST_PRIVILEGE_KEYS, 'update')
|
||||||
@ApiOperation({ summary: 'Bulk update sales request status' })
|
@ApiOperation({ summary: 'Bulk update sales request status' })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
schema: { properties: { updated: { type: 'number' } } },
|
schema: { properties: { updated: { type: 'number' } } },
|
||||||
@@ -118,7 +118,7 @@ export class SalesRequestsWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@RequirePrivilege(SALES_REQUEST_PRIVILEGE_KEY, 'create')
|
@RequirePrivilege(SALES_REQUEST_PRIVILEGE_KEYS, 'create')
|
||||||
@ApiOperation({ summary: 'Create sales request' })
|
@ApiOperation({ summary: 'Create sales request' })
|
||||||
@ApiCreatedResponse({ type: SalesRequestDto })
|
@ApiCreatedResponse({ type: SalesRequestDto })
|
||||||
@ApiUnauthorizedResponse()
|
@ApiUnauthorizedResponse()
|
||||||
@@ -146,7 +146,7 @@ export class SalesRequestsWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id/status')
|
@Patch(':id/status')
|
||||||
@RequirePrivilege(SALES_REQUEST_PRIVILEGE_KEY, 'update')
|
@RequirePrivilege(SALES_REQUEST_PRIVILEGE_KEYS, 'update')
|
||||||
@ApiOperation({ summary: 'Update sales request status' })
|
@ApiOperation({ summary: 'Update sales request status' })
|
||||||
@ApiOkResponse({ type: SalesRequestDto })
|
@ApiOkResponse({ type: SalesRequestDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
@@ -161,7 +161,7 @@ export class SalesRequestsWriteController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@RequirePrivilege(SALES_REQUEST_PRIVILEGE_KEY, 'update')
|
@RequirePrivilege(SALES_REQUEST_PRIVILEGE_KEYS, 'update')
|
||||||
@ApiOperation({ summary: 'Update sales request (not status)' })
|
@ApiOperation({ summary: 'Update sales request (not status)' })
|
||||||
@ApiOkResponse({ type: SalesRequestDto })
|
@ApiOkResponse({ type: SalesRequestDto })
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
@@ -191,7 +191,7 @@ export class SalesRequestsWriteController {
|
|||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@HttpCode(204)
|
@HttpCode(204)
|
||||||
@RequirePrivilege(SALES_REQUEST_PRIVILEGE_KEY, 'delete')
|
@RequirePrivilege(SALES_REQUEST_PRIVILEGE_KEYS, 'delete')
|
||||||
@ApiOperation({ summary: 'Delete sales request' })
|
@ApiOperation({ summary: 'Delete sales request' })
|
||||||
@ApiNoContentResponse()
|
@ApiNoContentResponse()
|
||||||
@ApiNotFoundResponse()
|
@ApiNotFoundResponse()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { BEARER_AUTH_NAME } from '../../common/swagger/setup-swagger';
|
|||||||
import { ListUsersQueryDto, UserDto } from './dto/user.dto';
|
import { ListUsersQueryDto, UserDto } from './dto/user.dto';
|
||||||
import { UsersService } from './users.service';
|
import { UsersService } from './users.service';
|
||||||
|
|
||||||
export const USERS_PRIVILEGE_KEY = 'USERS';
|
export const USERS_PRIVILEGE_KEY = 'ADMIN.SETTINGS.USER.USERS';
|
||||||
|
|
||||||
@ApiTags('users')
|
@ApiTags('users')
|
||||||
@ApiBearerAuth(BEARER_AUTH_NAME)
|
@ApiBearerAuth(BEARER_AUTH_NAME)
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ describe('Privileges (e2e)', () => {
|
|||||||
|
|
||||||
expect(res.body.data).toEqual(
|
expect(res.body.data).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({ code: 'PRIVILEGES' }),
|
expect.objectContaining({ code: 'ADMIN.SETTINGS.USER.PRIVILEGES' }),
|
||||||
expect.objectContaining({ code: 'USERS' }),
|
expect.objectContaining({ code: 'ADMIN.SETTINGS.USER.USERS' }),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
expect(res.body.meta).toMatchObject({
|
expect(res.body.meta).toMatchObject({
|
||||||
@@ -131,7 +131,7 @@ describe('Privileges (e2e)', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
const privilegesKey = (
|
const privilegesKey = (
|
||||||
keysRes.body.data as { id: string; code: string }[]
|
keysRes.body.data as { id: string; code: string }[]
|
||||||
).find((k) => k.code === 'PRIVILEGES');
|
).find((k) => k.code === 'ADMIN.SETTINGS.USER.PRIVILEGES');
|
||||||
expect(privilegesKey).toBeDefined();
|
expect(privilegesKey).toBeDefined();
|
||||||
|
|
||||||
const created = await request(app.getHttpServer())
|
const created = await request(app.getHttpServer())
|
||||||
@@ -156,7 +156,7 @@ describe('Privileges (e2e)', () => {
|
|||||||
createdBy: adminUserId,
|
createdBy: adminUserId,
|
||||||
details: [
|
details: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
keyCode: 'PRIVILEGES',
|
keyCode: 'ADMIN.SETTINGS.USER.PRIVILEGES',
|
||||||
action: 'view',
|
action: 'view',
|
||||||
value: true,
|
value: true,
|
||||||
}),
|
}),
|
||||||
@@ -202,7 +202,7 @@ describe('Privileges (e2e)', () => {
|
|||||||
expect(me.body.privilege).toMatchObject({
|
expect(me.body.privilege).toMatchObject({
|
||||||
id: adminPrivilegeId,
|
id: adminPrivilegeId,
|
||||||
});
|
});
|
||||||
expect(me.body.permissions.PRIVILEGES.view).toBe(true);
|
expect(me.body.permissions['ADMIN.SETTINGS.USER.PRIVILEGES'].view).toBe(true);
|
||||||
|
|
||||||
await request(app.getHttpServer())
|
await request(app.getHttpServer())
|
||||||
.patch(`/users/${otherUserId}/privilege`)
|
.patch(`/users/${otherUserId}/privilege`)
|
||||||
|
|||||||
Reference in New Issue
Block a user