Add field management module with database schema and validation

- Introduced `FieldModule` to manage cycles and plans, including read and write controllers.
- Created database migrations for `company_settings`, `cycles`, `cycle_weekdays`, `cycle_destinations`, `plans`, `plan_destinations`, `plan_invoices`, and `plan_packing_slips` tables, including constraints and unique indexes.
- Developed service and repository layers for handling cycle and plan data operations.
- Added unit tests for the cycles and plans services, repositories, and controllers to ensure functionality and correctness.
- Updated application module to include the new `FieldModule` for better organization.
This commit is contained in:
shancheas
2026-08-25 18:20:19 +07:00
parent 34d4f2c120
commit c9f9b31abf
53 changed files with 5990 additions and 5 deletions
@@ -0,0 +1,92 @@
import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import type { AuthUser } from '../../../common/auth/auth-user';
import type { PrivilegeAction } from '../../privileges/privilege-action';
import { PrivilegesService } from '../../privileges/privileges.service';
import {
fieldPrivilegeKey,
isFieldPurpose,
type FieldPurpose,
type FieldResource,
} from './field-purpose';
import {
REQUIRE_FIELD_PRIVILEGE_KEY,
type RequireFieldPrivilegeMeta,
} from './field-privilege.decorator';
@Injectable()
export class FieldPrivilegeGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly privilegesService: PrivilegesService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const required = this.reflector.getAllAndOverride<
RequireFieldPrivilegeMeta | undefined
>(REQUIRE_FIELD_PRIVILEGE_KEY, [context.getHandler(), context.getClass()]);
if (!required) {
return true;
}
const request = context.switchToHttp().getRequest<{
user?: AuthUser;
body?: { purpose?: string };
query?: { purpose?: string };
}>();
const user = request.user;
if (!user) {
throw new UnauthorizedException();
}
if (user.isSuperadmin) {
return true;
}
const purposeRaw = request.body?.purpose ?? request.query?.purpose;
const allowed = await this.allowedPurposes(
user.id,
required.resource,
required.action,
);
if (purposeRaw !== undefined && purposeRaw !== '') {
if (!isFieldPurpose(purposeRaw)) {
throw new ForbiddenException('Insufficient privilege');
}
if (!allowed.includes(purposeRaw)) {
throw new ForbiddenException('Insufficient privilege');
}
return true;
}
if (allowed.length === 0) {
throw new ForbiddenException('Insufficient privilege');
}
return true;
}
async allowedPurposes(
userId: string,
resource: FieldResource,
action: PrivilegeAction,
): Promise<FieldPurpose[]> {
const purposes: FieldPurpose[] = ['sales', 'logistics'];
const matches: FieldPurpose[] = [];
for (const purpose of purposes) {
const ok = await this.privilegesService.checkPermission(
userId,
fieldPrivilegeKey(resource, purpose),
action,
);
if (ok) {
matches.push(purpose);
}
}
return matches;
}
}