fix: 白名单问题调整 (#349)

* feat: 注册entity、出参调整

* fix: 白名单默认值处理

* feat: 修改白名单验证逻辑

* feat: 新增获取空间及下面成员的接口
This commit is contained in:
Stahsf 2024-07-16 21:13:37 +08:00 committed by GitHub
parent d31f6178c0
commit 9aec0fb1ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 154 additions and 217 deletions

View File

@ -41,7 +41,6 @@ import { PluginManagerProvider } from './securityPlugin/pluginManager.provider';
import { LogRequestMiddleware } from './middlewares/logRequest.middleware';
import { XiaojuSurveyPluginManager } from './securityPlugin/pluginManager';
import { Logger } from './logger';
import { WhitelistVerify } from './models/whitelistVerify.entity';
@Module({
imports: [
@ -82,7 +81,6 @@ import { WhitelistVerify } from './models/whitelistVerify.entity';
Workspace,
WorkspaceMember,
Collaborator,
WhitelistVerify,
],
};
},

View File

@ -1,8 +0,0 @@
import { Entity, Column } from 'typeorm';
import { BaseEntity } from './base.entity';
@Entity({ name: 'whitelistVerify' })
export class WhitelistVerify extends BaseEntity {
@Column()
surveyPath: string;
}

View File

@ -1,76 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { MongoRepository } from 'typeorm';
import { ObjectId } from 'mongodb';
import { getRepositoryToken } from '@nestjs/typeorm';
import { WhitelistService } from '../services/whitelist.service';
import { WhitelistVerify } from 'src/models/whitelistVerify.entity';
describe('WhitelistService', () => {
let service: WhitelistService;
let whitelistRepository: MongoRepository<WhitelistVerify>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
WhitelistService,
{
provide: getRepositoryToken(WhitelistVerify),
useValue: {
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
},
},
],
}).compile();
service = module.get<WhitelistService>(WhitelistService);
whitelistRepository = module.get<MongoRepository<WhitelistVerify>>(
getRepositoryToken(WhitelistVerify),
);
});
describe('create a verifyId', () => {
it('should create a verifyId successfully', async () => {
const surveyPath = 'GiWfCGPb';
jest.spyOn(whitelistRepository, 'create').mockImplementation((data) => {
return {
...data,
} as WhitelistVerify;
});
jest.spyOn(whitelistRepository, 'save').mockImplementation((data) => {
return Promise.resolve({
_id: new ObjectId(),
...data,
} as WhitelistVerify);
});
const result = await service.create(surveyPath);
expect(result.surveyPath).toBe(surveyPath);
expect(whitelistRepository.create).toHaveBeenCalledWith({
surveyPath,
});
});
});
describe('check if verifyId is correct ', () => {
it('should check if verifyId is correct successfully', async () => {
const mockId = new ObjectId();
const mockWhitelist = new WhitelistVerify();
const surveyPath = 'GiWfCGPb';
mockWhitelist._id = mockId;
mockWhitelist.surveyPath = surveyPath;
jest
.spyOn(whitelistRepository, 'findOne')
.mockImplementation(() => Promise.resolve(mockWhitelist));
const result = await service.match(surveyPath, mockId.toString());
expect(result).not.toBeNull();
expect(whitelistRepository.findOne).toHaveBeenCalledWith({
where: { _id: mockId, surveyPath },
});
});
});
});

View File

@ -11,16 +11,11 @@ import { Captcha } from 'src/models/captcha.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { WhitelistService } from './services/whitelist.service';
import { WhitelistVerify } from 'src/models/whitelistVerify.entity';
@Module({
imports: [
TypeOrmModule.forFeature([User, Captcha, WhitelistVerify]),
ConfigModule,
],
imports: [TypeOrmModule.forFeature([User, Captcha]), ConfigModule],
controllers: [AuthController, UserController],
providers: [UserService, AuthService, CaptchaService, WhitelistService],
exports: [UserService, AuthService, WhitelistService],
providers: [UserService, AuthService, CaptchaService],
exports: [UserService, AuthService],
})
export class AuthModule {}

View File

@ -1,31 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { MongoRepository } from 'typeorm';
import { WhitelistVerify } from 'src/models/whitelistVerify.entity';
import { ObjectId } from 'mongodb';
@Injectable()
export class WhitelistService {
constructor(
@InjectRepository(WhitelistVerify)
private readonly whitelistVerifyRepo: MongoRepository<WhitelistVerify>,
) {}
// 创建
async create(surveyPath: string) {
const data = this.whitelistVerifyRepo.create({
surveyPath,
});
return await this.whitelistVerifyRepo.save(data);
}
// 匹配
async match(surveyPath: string, verifyId: string) {
return await this.whitelistVerifyRepo.findOne({
where: {
surveyPath,
_id: new ObjectId(verifyId),
},
});
}
}

View File

@ -31,6 +31,7 @@ import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
import { WorkspaceGuard } from 'src/guards/workspace.guard';
import { PERMISSION as WORKSPACE_PERMISSION } from 'src/enums/workspace';
import { MemberType, WhitelistType } from 'src/interfaces/survey';
@ApiTags('survey')
@Controller('/api/survey')
@ -214,6 +215,16 @@ export class SurveyController {
surveyMeta.isCollaborated = false;
}
// 白名单相关字段的默认值
const baseConf = surveyConf.code?.baseConf;
if (baseConf) {
baseConf.passwordSwitch = baseConf.passwordSwitch ?? false;
baseConf.password = baseConf.password ?? '';
baseConf.whitelistType = baseConf.whitelistType ?? WhitelistType.ALL;
baseConf.whitelist = baseConf.whitelist ?? [];
baseConf.memberType = baseConf.memberType ?? MemberType.MOBILE;
}
return {
code: 200,
data: {

View File

@ -9,18 +9,14 @@ import { ResponseSchema } from 'src/models/responseSchema.entity';
import { Logger } from 'src/logger';
import { UserService } from 'src/modules/auth/services/user.service';
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
import { WhitelistService } from 'src/modules/auth/services/whitelist.service';
import { AuthService } from 'src/modules/auth/services/auth.service';
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
import { ObjectId } from 'mongodb';
import { WhitelistVerify } from 'src/models/whitelistVerify.entity';
jest.mock('../services/responseScheme.service');
describe('ResponseSchemaController', () => {
let controller: ResponseSchemaController;
let responseSchemaService: ResponseSchemaService;
let whitelistService: WhitelistService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@ -46,12 +42,6 @@ describe('ResponseSchemaController', () => {
findAllByUserId: jest.fn(),
},
},
{
provide: WhitelistService,
useValue: {
create: jest.fn(),
},
},
{
provide: AuthService,
useValue: {
@ -71,7 +61,6 @@ describe('ResponseSchemaController', () => {
responseSchemaService = module.get<ResponseSchemaService>(
ResponseSchemaService,
);
whitelistService = module.get<WhitelistService>(WhitelistService);
});
describe('getSchema', () => {
@ -152,7 +141,7 @@ describe('ResponseSchemaController', () => {
);
});
it('whitelistValidate should return verifyId successfully', async () => {
it('whitelistValidate should be successfully', async () => {
const surveyPath = 'test';
jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
@ -168,16 +157,11 @@ describe('ResponseSchemaController', () => {
},
} as ResponseSchema);
const id = 'c79c6fee22cbed6f0b087a27';
jest.spyOn(whitelistService, 'create').mockResolvedValue({
_id: new ObjectId(id),
surveyPath,
} as WhitelistVerify);
await expect(
controller.whitelistValidate(surveyPath, {
password: '123456',
}),
).resolves.toEqual({ code: 200, data: { verifyId: id } });
).resolves.toEqual({ code: 200, data: null });
});
it('whitelistValidate should throw WHITELIST_ERROR code when mobile or email is incorrect', async () => {
@ -201,7 +185,7 @@ describe('ResponseSchemaController', () => {
await expect(
controller.whitelistValidate(surveyPath, {
password: '123456',
value: '13500000001',
whitelist: '13500000001',
}),
).rejects.toThrow(
new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
@ -228,7 +212,7 @@ describe('ResponseSchemaController', () => {
await expect(
controller.whitelistValidate(surveyPath, {
password: '123456',
value: 'James',
whitelist: 'James',
}),
).rejects.toThrow(
new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
@ -255,17 +239,11 @@ describe('ResponseSchemaController', () => {
},
} as ResponseSchema);
const id = 'c79c6fee22cbed6f0b087a27';
jest.spyOn(whitelistService, 'create').mockResolvedValue({
_id: new ObjectId(id),
surveyPath,
} as WhitelistVerify);
await expect(
controller.whitelistValidate(surveyPath, {
password: '123456',
value: '13500000000',
whitelist: '13500000000',
}),
).resolves.toEqual({ code: 200, data: { verifyId: id } });
).resolves.toEqual({ code: 200, data: null });
});
});

View File

@ -21,10 +21,10 @@ import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugi
import { RECORD_STATUS } from 'src/enums';
import { SurveyResponse } from 'src/models/surveyResponse.entity';
import { Logger } from 'src/logger';
import { WhitelistService } from 'src/modules/auth/services/whitelist.service';
import { MemberType, WhitelistType } from 'src/interfaces/survey';
import { ResponseSchema } from 'src/models/responseSchema.entity';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { UserService } from 'src/modules/auth/services/user.service';
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
const mockDecryptErrorBody = {
surveyPath: 'EBzdmnSp',
@ -80,7 +80,6 @@ describe('SurveyResponseController', () => {
let responseSchemaService: ResponseSchemaService;
let surveyResponseService: SurveyResponseService;
let clientEncryptService: ClientEncryptService;
let whitelistService: WhitelistService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@ -130,9 +129,15 @@ describe('SurveyResponseController', () => {
},
},
{
provide: WhitelistService,
provide: UserService,
useValue: {
match: jest.fn(),
getUserByUsername: jest.fn(),
},
},
{
provide: WorkspaceMemberService,
useValue: {
findAllByUserId: jest.fn(),
},
},
],
@ -147,7 +152,6 @@ describe('SurveyResponseController', () => {
);
clientEncryptService =
module.get<ClientEncryptService>(ClientEncryptService);
whitelistService = module.get<WhitelistService>(WhitelistService);
const pluginManager = module.get<XiaojuSurveyPluginManager>(
XiaojuSurveyPluginManager,
@ -319,36 +323,11 @@ describe('SurveyResponseController', () => {
);
});
it('should throw HttpException if verifyId is empty', async () => {
const reqBody = { ...mockSubmitData };
jest
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValueOnce({
curStatus: {
status: RECORD_STATUS.PUBLISHED,
},
code: {
baseConf: {
passwordSwitch: true,
password: '123456',
whitelistType: WhitelistType.CUSTOM,
memberType: MemberType.MOBILE,
whitelist: ['13500000000'],
},
},
} as ResponseSchema);
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
new HttpException('白名单验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
);
});
it('should throw HttpException if verifyId does not match', async () => {
it('should throw HttpException if password does not match', async () => {
const reqBody = {
...mockSubmitData,
verifyId: 'xxx',
sign: '6e9fda60c7fd9466eda480e3c5a03c2de0e33becc193b82f6aa6d25ff3b69146.1710400229589',
password: '123457',
sign: '4ff02062141d92d80629eae4797ba68056f29a9709cdf59bf206776fc0971c1a.1710400229589',
};
jest
@ -361,15 +340,10 @@ describe('SurveyResponseController', () => {
baseConf: {
passwordSwitch: true,
password: '123456',
whitelistType: WhitelistType.CUSTOM,
memberType: MemberType.MOBILE,
whitelist: ['13500000000'],
},
},
} as ResponseSchema);
jest.spyOn(whitelistService, 'match').mockResolvedValueOnce(null);
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
new HttpException('白名单验证失败', EXCEPTION_CODE.WHITELIST_ERROR),
);

View File

@ -18,7 +18,6 @@ import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException'
import { WhitelistType } from 'src/interfaces/survey';
import { UserService } from 'src/modules/auth/services/user.service';
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
import { WhitelistService } from 'src/modules/auth/services/whitelist.service';
@ApiTags('surveyResponse')
@Controller('/api/responseSchema')
@ -28,7 +27,6 @@ export class ResponseSchemaController {
private readonly logger: Logger,
private readonly userService: UserService,
private readonly workspaceMemberService: WorkspaceMemberService,
private readonly whitelistService: WhitelistService,
) {}
@Get('/getSchema')
@ -73,7 +71,7 @@ export class ResponseSchemaController {
async whitelistValidate(@Param('surveyPath') surveyPath, @Body() body) {
const { value, error } = Joi.object({
password: Joi.string().allow(null, ''),
value: Joi.string().allow(null, ''),
whitelist: Joi.string().allow(null, ''),
}).validate(body, { allowUnknown: true });
if (error) {
@ -88,7 +86,7 @@ export class ResponseSchemaController {
throw new SurveyNotFoundException('该问卷不存在,无法提交');
}
const { password, value: val } = value;
const { password, whitelist: whitelistValue } = value;
const {
passwordSwitch,
password: settingPassword,
@ -105,14 +103,14 @@ export class ResponseSchemaController {
// 名单校验(手机号/邮箱)
if (whitelistType === WhitelistType.CUSTOM) {
if (!whitelist.includes(val)) {
if (!whitelist.includes(whitelistValue)) {
throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR);
}
}
// 团队成员昵称校验
if (whitelistType === WhitelistType.MEMBER) {
const user = await this.userService.getUserByUsername(val);
const user = await this.userService.getUserByUsername(whitelistValue);
if (!user) {
throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR);
}
@ -125,14 +123,9 @@ export class ResponseSchemaController {
}
}
// 返回verifyId
const res = await this.whitelistService.create(surveyPath);
return {
code: 200,
data: {
verifyId: res._id.toString(),
},
data: null,
};
}
}

View File

@ -18,7 +18,8 @@ import * as forge from 'node-forge';
import { ApiTags } from '@nestjs/swagger';
import { Logger } from 'src/logger';
import { WhitelistType } from 'src/interfaces/survey';
import { WhitelistService } from 'src/modules/auth/services/whitelist.service';
import { UserService } from 'src/modules/auth/services/user.service';
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
@ApiTags('surveyResponse')
@Controller('/api/surveyResponse')
@ -30,7 +31,8 @@ export class SurveyResponseController {
private readonly clientEncryptService: ClientEncryptService,
private readonly messagePushingTaskService: MessagePushingTaskService,
private readonly logger: Logger,
private readonly whitelistService: WhitelistService,
private readonly userService: UserService,
private readonly workspaceMemberService: WorkspaceMemberService,
) {}
@Post('/createResponse')
@ -46,7 +48,8 @@ export class SurveyResponseController {
sessionId: Joi.string(),
clientTime: Joi.number().required(),
difTime: Joi.number(),
verifyId: Joi.string().allow(null, ''),
password: Joi.string().allow(null, ''),
whitelist: Joi.string().allow(null, ''),
}).validate(reqBody, { allowUnknown: true });
if (error) {
@ -63,7 +66,8 @@ export class SurveyResponseController {
sessionId,
clientTime,
difTime,
verifyId,
password,
whitelist: whitelistValue,
} = value;
// 查询schema
@ -75,21 +79,41 @@ export class SurveyResponseController {
// 白名单的verifyId校验
const baseConf = responseSchema.code.baseConf;
const shouldValidateVerifyId =
(baseConf?.passwordSwitch && baseConf.password) ||
(baseConf?.whitelistType && baseConf.whitelistType !== WhitelistType.ALL);
if (shouldValidateVerifyId) {
// 无verifyId
if (!verifyId) {
// 密码校验
if (baseConf?.passwordSwitch && baseConf.password) {
if (baseConf.password !== password) {
throw new HttpException(
'白名单验证失败',
EXCEPTION_CODE.WHITELIST_ERROR,
);
}
}
// 名单校验(手机号/邮箱)
if (baseConf?.whitelistType === WhitelistType.CUSTOM) {
if (!baseConf.whitelist.includes(whitelistValue)) {
throw new HttpException(
'白名单验证失败',
EXCEPTION_CODE.WHITELIST_ERROR,
);
}
}
// 团队成员昵称校验
if (baseConf?.whitelistType === WhitelistType.MEMBER) {
const user = await this.userService.getUserByUsername(whitelistValue);
if (!user) {
throw new HttpException(
'白名单验证失败',
EXCEPTION_CODE.WHITELIST_ERROR,
);
}
// 从数据库中查询是否存在对应的verifyId
const record = await this.whitelistService.match(surveyPath, verifyId);
if (!record) {
const workspaceMember = await this.workspaceMemberService.findAllByUserId(
{ userId: user._id.toString() },
);
if (!workspaceMember.length) {
throw new HttpException(
'白名单验证失败',
EXCEPTION_CODE.WHITELIST_ERROR,

View File

@ -31,6 +31,8 @@ import { splitMembers } from '../utils/splitMember';
import { UserService } from 'src/modules/auth/services/user.service';
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
import { Logger } from 'src/logger';
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
import { Workspace } from 'src/models/workspace.entity';
@ApiTags('workspace')
@ApiBearerAuth()
@ -326,4 +328,42 @@ export class WorkspaceController {
code: 200,
};
}
@Get('/member/list')
@HttpCode(200)
async getWorkspaceAndMember(@Request() req) {
const userId = req.user._id.toString();
// 所在所有空间
const workspaceList = await this.workspaceService.findAllByUserId(userId);
if (!workspaceList.length) {
return {
code: 200,
data: [],
};
}
// 所有空间下的所有成员
const workspaceMemberList =
await this.workspaceMemberService.batchSearchByWorkspace(
workspaceList.map((item) => item._id.toString()),
);
const temp: Record<string, WorkspaceMember[]> = {};
const list = workspaceList.map(
(item: Workspace & { members: WorkspaceMember[] }) => {
temp[item._id.toString()] = item.members = [];
return item;
},
);
workspaceMemberList.forEach((member) => {
temp[member.workspaceId.toString()].push(member);
});
return {
code: 200,
data: list,
};
}
}

View File

@ -104,4 +104,27 @@ export class WorkspaceService {
surveyRes,
};
}
// 用户下的所有空间
async findAllByUserId(userId: string) {
return await this.workspaceRepository.find({
where: {
ownerId: userId,
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
},
order: {
_id: -1,
},
select: [
'_id',
'curStatus',
'name',
'description',
'ownerId',
'createDate',
],
});
}
}

View File

@ -140,4 +140,20 @@ export class WorkspaceMemberService {
},
});
}
// 根据空间id批量查询成员
async batchSearchByWorkspace(workspaceList: string[]) {
console.log(workspaceList);
return await this.workspaceMemberRepository.find({
where: {
workspaceId: {
$in: workspaceList,
},
},
order: {
_id: -1,
},
select: ['_id', 'userId', 'username', 'role', 'workspaceId'],
});
}
}