refactor: resolve remaining field audit issues

This commit is contained in:
2026-07-13 15:12:36 +08:00
parent 0533c30ece
commit aa1ed7db56
34 changed files with 953 additions and 263 deletions

View File

@@ -0,0 +1,48 @@
import { ValidationPipe } from '@nestjs/common';
import {
DingTalkThirdConfigDto,
IntegrationType,
SaveIntegrationConfigDto,
WeComThirdConfigDto,
} from './config.dto';
const pipe = new ValidationPipe({ transform: true, whitelist: true });
const transform = (value: unknown) =>
pipe.transform(value, { type: 'body', metatype: SaveIntegrationConfigDto });
describe('integration config request DTO', () => {
it('validates and transforms DingTalk configuration', async () => {
const result = await transform({
type: 'DINGTALK',
config: {
agentId: 'app-key',
corpId: 'corp-id',
appSecret: '',
appId: 'app-id',
ignored: 'value',
},
});
expect(result.type).toBe(IntegrationType.DINGTALK);
expect(result.config).toBeInstanceOf(DingTalkThirdConfigDto);
expect(result.config).toMatchObject({ agentId: 'app-key', corpId: 'corp-id', appSecret: '' });
expect(result.config).not.toHaveProperty('ignored');
});
it('uses the WeCom nested DTO and removes DingTalk-only fields', async () => {
const result = await transform({
type: 'WECOM',
config: { agentId: 'agent', corpId: 'corp', appId: 'not-supported' },
});
expect(result.config).toBeInstanceOf(WeComThirdConfigDto);
expect(result.config).not.toHaveProperty('appId');
});
it('rejects invalid platform types and incomplete nested config', async () => {
await expect(transform({ type: 'UNKNOWN', config: {} })).rejects.toThrow();
await expect(transform({ type: 'DINGTALK', config: { corpId: 'corp' } })).rejects.toThrow();
await expect(transform({ type: 'DINGTALK' })).rejects.toThrow();
});
});

View File

@@ -1,27 +1,71 @@
/** 钉钉配置 */
export interface DingTalkThirdConfig {
agentId: string; // AppKey
appSecret?: string; // AppSecret更新已有配置时可留空保留旧值
corpId: string; // CorpId
appId?: string; // 内部应用ID用于消息推送可选
import { Type } from 'class-transformer';
import {
IsDefined,
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
export enum IntegrationType {
WECOM = 'WECOM',
DINGTALK = 'DINGTALK',
}
/** 企微配置 */
export interface WeComThirdConfig {
export class DingTalkThirdConfigDto {
@IsString()
@IsNotEmpty()
agentId: string;
@IsOptional()
@IsString()
appSecret?: string;
@IsString()
@IsNotEmpty()
corpId: string;
@IsOptional()
@IsString()
appId?: string;
}
export class WeComThirdConfigDto {
@IsString()
@IsNotEmpty()
agentId: string;
@IsOptional()
@IsString()
appSecret?: string;
@IsString()
@IsNotEmpty()
corpId: string;
}
export class IntegrationConfigRequestDto {
@IsEnum(IntegrationType)
type: IntegrationType;
@IsDefined()
@ValidateNested()
@Type((options) =>
options?.object?.type === IntegrationType.DINGTALK
? DingTalkThirdConfigDto
: WeComThirdConfigDto,
)
config: DingTalkThirdConfigDto | WeComThirdConfigDto;
}
export class SaveIntegrationConfigDto extends IntegrationConfigRequestDto {}
export class TestIntegrationConfigDto extends IntegrationConfigRequestDto {}
/** 对外返回的配置(脱敏后,不含 appSecret */
export interface ThirdConfigBaseDTO<T = unknown> {
type: string;
verify?: boolean;
config: T;
}
/** 保存配置的请求体 */
export interface SaveConfigRequest {
type: 'WECOM' | 'DINGTALK';
config: DingTalkThirdConfig | WeComThirdConfig;
}

View File

@@ -2,7 +2,7 @@ import { Controller, Get, Post, Body, Param, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { RequirePermission } from '../../auth/decorators/permission.decorator';
import { IntegrationConfigService } from './integration-config.service';
import type { SaveConfigRequest } from './dto/config.dto';
import { SaveIntegrationConfigDto, TestIntegrationConfigDto } from './dto/config.dto';
@Controller('integration/config')
@UseGuards(JwtAuthGuard)
@@ -31,7 +31,7 @@ export class IntegrationConfigController {
/** 保存配置 */
@Post()
@RequirePermission('integration:trigger')
async saveConfig(@Body() body: SaveConfigRequest) {
async saveConfig(@Body() body: SaveIntegrationConfigDto) {
await this.service.saveConfig(body);
return { success: true, message: '配置已保存' };
}
@@ -39,7 +39,7 @@ export class IntegrationConfigController {
/** 测试连接 */
@Post('test')
@RequirePermission('integration:read')
async testConnection(@Body() body: SaveConfigRequest) {
async testConnection(@Body() body: TestIntegrationConfigDto) {
const success = await this.service.testConnection(body.type, body.config);
return { success, message: success ? '连接成功' : '连接失败,请检查配置信息' };
}

View File

@@ -1,15 +1,13 @@
import { Injectable, Logger, BadRequestException, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
IntegrationConfig,
IntegrationConfigDetail,
} from '../entities/integration-config.entity';
import { IntegrationConfig, IntegrationConfigDetail } from '../entities/integration-config.entity';
import {
ThirdConfigBaseDTO,
DingTalkThirdConfig,
WeComThirdConfig,
SaveConfigRequest,
DingTalkThirdConfigDto,
WeComThirdConfigDto,
IntegrationType,
SaveIntegrationConfigDto,
} from './dto/config.dto';
@Injectable()
@@ -67,7 +65,7 @@ export class IntegrationConfigService {
}
/** 保存/更新配置 */
async saveConfig(request: SaveConfigRequest): Promise<void> {
async saveConfig(request: SaveIntegrationConfigDto): Promise<void> {
const config = await this.ensureConfig();
const detailType = this.getDetailType(request.type);
@@ -121,8 +119,8 @@ export class IntegrationConfigService {
/** 测试连接 */
async testConnection(
type: string,
config: DingTalkThirdConfig | WeComThirdConfig,
type: IntegrationType,
config: DingTalkThirdConfigDto | WeComThirdConfigDto,
): Promise<boolean> {
try {
const finalConfig = { ...config } as Record<string, unknown>;