690329:1621 Fixing superadmin by GPT-5.3 #01
This commit is contained in:
@@ -15,9 +15,15 @@ import { UpdateCirculationRoutingDto } from './dto/update-circulation-routing.dt
|
||||
import { SearchCirculationDto } from './dto/search-circulation.dto';
|
||||
import { DocumentNumberingService } from '../document-numbering/services/document-numbering.service';
|
||||
import { UuidResolverService } from '../../common/services/uuid-resolver.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
|
||||
@Injectable()
|
||||
export class CirculationService {
|
||||
private async hasSystemManageAllPermission(userId: number): Promise<boolean> {
|
||||
const permissions = await this.userService.getUserPermissions(userId);
|
||||
return permissions.includes('system.manage_all');
|
||||
}
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Circulation)
|
||||
private circulationRepo: Repository<Circulation>,
|
||||
@@ -25,11 +31,36 @@ export class CirculationService {
|
||||
private routingRepo: Repository<CirculationRouting>,
|
||||
private numberingService: DocumentNumberingService,
|
||||
private dataSource: DataSource,
|
||||
private uuidResolver: UuidResolverService
|
||||
private uuidResolver: UuidResolverService,
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
async create(createDto: CreateCirculationDto, user: User) {
|
||||
if (!user.primaryOrganizationId) {
|
||||
let userOrgId = user.primaryOrganizationId;
|
||||
if (!userOrgId) {
|
||||
const fullUser = await this.userService.findOne(user.user_id);
|
||||
if (fullUser) {
|
||||
userOrgId = fullUser.primaryOrganizationId;
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedOriginatorId = createDto.originatorId
|
||||
? await this.uuidResolver.resolveOrganizationId(createDto.originatorId)
|
||||
: undefined;
|
||||
|
||||
if (resolvedOriginatorId && resolvedOriginatorId !== userOrgId) {
|
||||
const canManageAll = await this.hasSystemManageAllPermission(
|
||||
user.user_id
|
||||
);
|
||||
if (!canManageAll) {
|
||||
throw new ForbiddenException(
|
||||
'You do not have permission to create documents on behalf of other organizations.'
|
||||
);
|
||||
}
|
||||
userOrgId = resolvedOriginatorId;
|
||||
}
|
||||
|
||||
if (!userOrgId) {
|
||||
throw new BadRequestException('User must belong to an organization');
|
||||
}
|
||||
|
||||
@@ -52,7 +83,7 @@ export class CirculationService {
|
||||
// Generate No. using DocumentNumberingService (Type 900 - Circulation)
|
||||
const result = await this.numberingService.generateNextNumber({
|
||||
projectId: resolvedProjectId,
|
||||
originatorOrganizationId: user.primaryOrganizationId,
|
||||
originatorOrganizationId: userOrgId,
|
||||
typeId: 900, // Fixed Type ID for Circulation
|
||||
year: new Date().getFullYear(),
|
||||
customTokens: {
|
||||
@@ -62,7 +93,7 @@ export class CirculationService {
|
||||
});
|
||||
|
||||
const circulation = queryRunner.manager.create(Circulation, {
|
||||
organizationId: user.primaryOrganizationId,
|
||||
organizationId: userOrgId,
|
||||
correspondenceId: resolvedCorrId,
|
||||
circulationNo: result.number,
|
||||
subject: createDto.subject,
|
||||
@@ -76,7 +107,7 @@ export class CirculationService {
|
||||
queryRunner.manager.create(CirculationRouting, {
|
||||
circulationId: savedCirculation.id,
|
||||
stepNumber: index + 1,
|
||||
organizationId: user.primaryOrganizationId,
|
||||
organizationId: userOrgId,
|
||||
assignedTo: assigneeId,
|
||||
status: 'PENDING',
|
||||
})
|
||||
|
||||
@@ -13,6 +13,9 @@ export class CreateCirculationDto {
|
||||
@IsOptional()
|
||||
projectId?: number | string; // Project ID or UUID for Numbering
|
||||
|
||||
@IsOptional()
|
||||
originatorId?: number | string; // ระบุองค์กรเจ้าของเอกสาร (ต้องใช้ร่วมกับสิทธิ system.manage_all)
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
subject!: string; // หัวข้อเรื่อง (Subject)
|
||||
|
||||
@@ -19,6 +19,7 @@ import { FileStorageService } from '../../common/file-storage/file-storage.servi
|
||||
import { UuidResolverService } from '../../common/services/uuid-resolver.service';
|
||||
import { NotificationService } from '../notification/notification.service';
|
||||
import { UpdateCorrespondenceDto } from './dto/update-correspondence.dto';
|
||||
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto';
|
||||
import { User } from '../user/entities/user.entity';
|
||||
|
||||
describe('CorrespondenceService', () => {
|
||||
@@ -336,4 +337,95 @@ describe('CorrespondenceService', () => {
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should allow system.manage_all user without primaryOrganizationId when originatorId is provided', async () => {
|
||||
const mockUser = {
|
||||
user_id: 1,
|
||||
primaryOrganizationId: null,
|
||||
} as unknown as User;
|
||||
|
||||
const createDto: CreateCorrespondenceDto = {
|
||||
projectId: 'project-uuid',
|
||||
typeId: 1,
|
||||
subject: 'Test Subject',
|
||||
originatorId: 'originator-uuid',
|
||||
recipients: [{ organizationId: 'recipient-uuid', type: 'TO' }],
|
||||
};
|
||||
|
||||
const userService = testingModule.get<UserService>(UserService);
|
||||
const typeRepo = testingModule.get<Repository<CorrespondenceType>>(
|
||||
getRepositoryToken(CorrespondenceType)
|
||||
);
|
||||
const statusRepo = testingModule.get<Repository<CorrespondenceStatus>>(
|
||||
getRepositoryToken(CorrespondenceStatus)
|
||||
);
|
||||
const uuidResolver =
|
||||
testingModule.get<UuidResolverService>(UuidResolverService);
|
||||
|
||||
(userService.findOne as jest.Mock).mockResolvedValue({
|
||||
user_id: 1,
|
||||
primaryOrganizationId: null,
|
||||
});
|
||||
(userService.getUserPermissions as jest.Mock).mockResolvedValue([
|
||||
'system.manage_all',
|
||||
]);
|
||||
|
||||
(uuidResolver.resolveProjectId as jest.Mock).mockResolvedValue(100);
|
||||
(uuidResolver.resolveOrganizationId as jest.Mock).mockImplementation(
|
||||
(value: number | string) => {
|
||||
if (value === 'originator-uuid') return 10;
|
||||
if (value === 'recipient-uuid') return 20;
|
||||
return 0;
|
||||
}
|
||||
);
|
||||
|
||||
(typeRepo.findOne as jest.Mock).mockResolvedValue({
|
||||
id: 1,
|
||||
typeCode: 'LTR',
|
||||
});
|
||||
(statusRepo.findOne as jest.Mock).mockResolvedValue({
|
||||
id: 1,
|
||||
statusCode: 'DRAFT',
|
||||
});
|
||||
|
||||
(numberingService.generateNextNumber as jest.Mock).mockResolvedValue({
|
||||
number: 'DOC-001',
|
||||
});
|
||||
|
||||
mockDataSource.manager.findOne
|
||||
.mockResolvedValueOnce({ id: 10, organizationCode: 'ORG' })
|
||||
.mockResolvedValueOnce({ id: 20, organizationCode: 'REC' });
|
||||
|
||||
const queryRunner = {
|
||||
connect: jest.fn(),
|
||||
startTransaction: jest.fn(),
|
||||
commitTransaction: jest.fn(),
|
||||
rollbackTransaction: jest.fn(),
|
||||
release: jest.fn(),
|
||||
manager: {
|
||||
create: jest.fn(
|
||||
(_entity: unknown, payload: Record<string, unknown>) => payload
|
||||
),
|
||||
save: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ id: 999, publicId: 'corr-uuid' })
|
||||
.mockResolvedValueOnce({ id: 1000 })
|
||||
.mockResolvedValueOnce([]),
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
(mockDataSource.createQueryRunner as jest.Mock).mockReturnValue(
|
||||
queryRunner
|
||||
);
|
||||
|
||||
await service.create(createDto, mockUser);
|
||||
|
||||
expect(queryRunner.manager.create).toHaveBeenCalledWith(
|
||||
Correspondence,
|
||||
expect.objectContaining({ originatorId: 10 })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,6 +50,11 @@ interface ResolvedRecipient {
|
||||
export class CorrespondenceService {
|
||||
private readonly logger = new Logger(CorrespondenceService.name);
|
||||
|
||||
private async hasSystemManageAllPermission(userId: number): Promise<boolean> {
|
||||
const permissions = await this.userService.getUserPermissions(userId);
|
||||
return permissions.includes('system.manage_all');
|
||||
}
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Correspondence)
|
||||
private correspondenceRepo: Repository<Correspondence>,
|
||||
@@ -92,9 +97,22 @@ export class CorrespondenceService {
|
||||
}
|
||||
|
||||
if (!userOrgId) {
|
||||
throw new BadRequestException(
|
||||
'User must belong to an organization to create documents'
|
||||
);
|
||||
if (createDto.originatorId) {
|
||||
const canManageAll = await this.hasSystemManageAllPermission(
|
||||
user.user_id
|
||||
);
|
||||
if (canManageAll) {
|
||||
userOrgId = await this.uuidResolver.resolveOrganizationId(
|
||||
createDto.originatorId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!userOrgId) {
|
||||
throw new BadRequestException(
|
||||
'User must belong to an organization to create documents'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// For impersonation, use the specified originator
|
||||
@@ -187,10 +205,10 @@ export class CorrespondenceService {
|
||||
|
||||
// Impersonation Logic
|
||||
if (resolvedOriginatorId && resolvedOriginatorId !== userOrgId) {
|
||||
const permissions = await this.userService.getUserPermissions(
|
||||
const canManageAll = await this.hasSystemManageAllPermission(
|
||||
user.user_id
|
||||
);
|
||||
if (!permissions.includes('system.manage_all')) {
|
||||
if (!canManageAll) {
|
||||
throw new ForbiddenException(
|
||||
'You do not have permission to create documents on behalf of other organizations.'
|
||||
);
|
||||
|
||||
@@ -25,6 +25,14 @@ export class CreateRfaDto {
|
||||
@IsNotEmpty()
|
||||
toOrganizationId!: number | string;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Originator Organization ID or UUID (for users with system.manage_all)',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
originatorId?: number | string;
|
||||
|
||||
@ApiProperty({ description: 'ID ของประเภท RFA', example: 1 })
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -63,6 +63,11 @@ import { UuidResolverService } from '../../common/services/uuid-resolver.service
|
||||
export class RfaService {
|
||||
private readonly logger = new Logger(RfaService.name);
|
||||
|
||||
private async hasSystemManageAllPermission(userId: number): Promise<boolean> {
|
||||
const permissions = await this.userService.getUserPermissions(userId);
|
||||
return permissions.includes('system.manage_all');
|
||||
}
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Rfa)
|
||||
private rfaRepo: Repository<Rfa>,
|
||||
@@ -226,12 +231,29 @@ export class RfaService {
|
||||
);
|
||||
}
|
||||
|
||||
const resolvedOriginatorId = createDto.originatorId
|
||||
? await this.uuidResolver.resolveOrganizationId(createDto.originatorId)
|
||||
: undefined;
|
||||
|
||||
// Determine User Organization
|
||||
let userOrgId = user.primaryOrganizationId;
|
||||
if (!userOrgId) {
|
||||
const fullUser = await this.userService.findOne(user.user_id);
|
||||
if (fullUser) userOrgId = fullUser.primaryOrganizationId;
|
||||
}
|
||||
|
||||
if (resolvedOriginatorId && resolvedOriginatorId !== userOrgId) {
|
||||
const canManageAll = await this.hasSystemManageAllPermission(
|
||||
user.user_id
|
||||
);
|
||||
if (!canManageAll) {
|
||||
throw new ForbiddenException(
|
||||
'You do not have permission to create documents on behalf of other organizations.'
|
||||
);
|
||||
}
|
||||
userOrgId = resolvedOriginatorId;
|
||||
}
|
||||
|
||||
if (!userOrgId) {
|
||||
throw new BadRequestException('User must belong to an organization');
|
||||
}
|
||||
|
||||
@@ -54,6 +54,14 @@ export class CreateTransmittalDto {
|
||||
@IsNotEmpty()
|
||||
recipientOrganizationId!: number | string;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'ผู้ส่ง Organization ID หรือ UUID (สำหรับผู้ใช้ที่มีสิทธิ system.manage_all)',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
originatorId?: number | string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Correspondence ID หรือ UUID (ADR-019)',
|
||||
required: false,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
NotFoundException,
|
||||
InternalServerErrorException,
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
@@ -22,11 +23,17 @@ import { CorrespondenceType } from '../correspondence/entities/correspondence-ty
|
||||
import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity';
|
||||
import { UuidResolverService } from '../../common/services/uuid-resolver.service';
|
||||
import { CorrespondenceRecipient } from '../correspondence/entities/correspondence-recipient.entity';
|
||||
import { UserService } from '../user/user.service';
|
||||
|
||||
@Injectable()
|
||||
export class TransmittalService {
|
||||
private readonly logger = new Logger(TransmittalService.name);
|
||||
|
||||
private async hasSystemManageAllPermission(userId: number): Promise<boolean> {
|
||||
const permissions = await this.userService.getUserPermissions(userId);
|
||||
return permissions.includes('system.manage_all');
|
||||
}
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Transmittal)
|
||||
private transmittalRepo: Repository<Transmittal>,
|
||||
@@ -38,7 +45,8 @@ export class TransmittalService {
|
||||
private statusRepo: Repository<CorrespondenceStatus>,
|
||||
private numberingService: DocumentNumberingService,
|
||||
private dataSource: DataSource,
|
||||
private uuidResolver: UuidResolverService
|
||||
private uuidResolver: UuidResolverService,
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
async create(
|
||||
@@ -61,7 +69,31 @@ export class TransmittalService {
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
if (!user.primaryOrganizationId) {
|
||||
let userOrgId = user.primaryOrganizationId;
|
||||
if (!userOrgId) {
|
||||
const fullUser = await this.userService.findOne(user.user_id);
|
||||
if (fullUser) {
|
||||
userOrgId = fullUser.primaryOrganizationId;
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedOriginatorId = createDto.originatorId
|
||||
? await this.uuidResolver.resolveOrganizationId(createDto.originatorId)
|
||||
: undefined;
|
||||
|
||||
if (resolvedOriginatorId && resolvedOriginatorId !== userOrgId) {
|
||||
const canManageAll = await this.hasSystemManageAllPermission(
|
||||
user.user_id
|
||||
);
|
||||
if (!canManageAll) {
|
||||
throw new ForbiddenException(
|
||||
'You do not have permission to create documents on behalf of other organizations.'
|
||||
);
|
||||
}
|
||||
userOrgId = resolvedOriginatorId;
|
||||
}
|
||||
|
||||
if (!userOrgId) {
|
||||
throw new BadRequestException(
|
||||
'User must belong to an organization to create a transmittal'
|
||||
);
|
||||
@@ -76,7 +108,7 @@ export class TransmittalService {
|
||||
// 2. Generate Number
|
||||
const docNumber = await this.numberingService.generateNextNumber({
|
||||
projectId: internalProjectId,
|
||||
originatorOrganizationId: user.primaryOrganizationId,
|
||||
originatorOrganizationId: userOrgId,
|
||||
typeId: type.id,
|
||||
year: new Date().getFullYear(),
|
||||
customTokens: {
|
||||
@@ -90,7 +122,7 @@ export class TransmittalService {
|
||||
correspondenceNumber: docNumber.number,
|
||||
correspondenceTypeId: type.id,
|
||||
projectId: internalProjectId,
|
||||
originatorId: user.primaryOrganizationId,
|
||||
originatorId: userOrgId,
|
||||
isInternal: false,
|
||||
createdBy: user.user_id,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user