From ee9a1ea9c7c3227c91c71fb6d4a4d5f6ae0c2fdd Mon Sep 17 00:00:00 2001 From: Stahsf <30379566+50431040@users.noreply.github.com> Date: Sat, 20 Jul 2024 14:11:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=99=BD=E5=90=8D=E5=8D=95=E5=8A=9F?= =?UTF-8?q?=E8=83=BD-=E6=9C=8D=E5=8A=A1=E7=AB=AF=20(#357)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/enums/exceptionCode.ts | 1 + server/src/interfaces/survey.ts | 29 +++ .../survey/controllers/survey.controller.ts | 11 ++ .../__test/responseSchema.controller.spec.ts | 181 +++++++++++++++++- .../__test/surveyResponse.controller.spec.ts | 42 ++++ .../controllers/responseSchema.controller.ts | 93 ++++++++- .../controllers/surveyResponse.controller.ts | 63 +++++- .../surveyResponse/surveyResponse.module.ts | 4 + .../_test/workspace.controller.spec.ts | 29 +++ .../workspace/_test/workspace.service.spec.ts | 21 ++ .../_test/workspaceMember.service.spec.ts | 17 ++ .../controllers/workspace.controller.ts | 40 ++++ .../workspace/services/workspace.service.ts | 23 +++ .../services/workspaceMember.service.ts | 15 ++ 14 files changed, 564 insertions(+), 5 deletions(-) diff --git a/server/src/enums/exceptionCode.ts b/server/src/enums/exceptionCode.ts index 30d943ba..85cffaa7 100644 --- a/server/src/enums/exceptionCode.ts +++ b/server/src/enums/exceptionCode.ts @@ -12,6 +12,7 @@ export enum EXCEPTION_CODE { SURVEY_NOT_FOUND = 3004, // 问卷不存在 SURVEY_CONTENT_NOT_ALLOW = 3005, // 存在禁用内容 CAPTCHA_INCORRECT = 4001, // 验证码不正确 + WHITELIST_ERROR = 4002, // 白名单校验错误 RESPONSE_SIGN_ERROR = 9001, // 签名不正确 RESPONSE_CURRENT_TIME_NOT_ALLOW = 9002, // 当前时间不允许提交 diff --git a/server/src/interfaces/survey.ts b/server/src/interfaces/survey.ts index 30403597..1e68afcc 100644 --- a/server/src/interfaces/survey.ts +++ b/server/src/interfaces/survey.ts @@ -94,6 +94,23 @@ export interface SubmitConf { msgContent: MsgContent; } +// 白名单类型 +export enum WhitelistType { + ALL = 'ALL', + // 空间成员 + MEMBER = 'MEMBER', + // 自定义 + CUSTOM = 'CUSTOM', +} + +// 白名单用户类型 +export enum MemberType { + // 手机号 + MOBILE = 'MOBILE', + // 邮箱 + EMAIL = 'EMAIL', +} + export interface BaseConf { begTime: string; endTime: string; @@ -101,6 +118,18 @@ export interface BaseConf { answerEndTime: string; tLimit: number; language: string; + // 访问密码开关 + passwordSwitch?: boolean; + // 密码 + password?: string | null; + // 白名单类型 + whitelistType?: WhitelistType; + // 白名单用户类型 + memberType?: MemberType; + // 白名单列表 + whitelist?: string[]; + // 提示语 + whitelistTip?: string; } export interface SkinConf { diff --git a/server/src/modules/survey/controllers/survey.controller.ts b/server/src/modules/survey/controllers/survey.controller.ts index df1a3d62..6976b5c6 100644 --- a/server/src/modules/survey/controllers/survey.controller.ts +++ b/server/src/modules/survey/controllers/survey.controller.ts @@ -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: { diff --git a/server/src/modules/surveyResponse/__test/responseSchema.controller.spec.ts b/server/src/modules/surveyResponse/__test/responseSchema.controller.spec.ts index bb1395e9..497580df 100644 --- a/server/src/modules/surveyResponse/__test/responseSchema.controller.spec.ts +++ b/server/src/modules/surveyResponse/__test/responseSchema.controller.spec.ts @@ -6,6 +6,11 @@ import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { RECORD_STATUS } from 'src/enums'; 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 { AuthService } from 'src/modules/auth/services/auth.service'; +import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException'; jest.mock('../services/responseScheme.service'); @@ -16,7 +21,40 @@ describe('ResponseSchemaController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [ResponseSchemaController], - providers: [ResponseSchemaService], + providers: [ + ResponseSchemaService, + AuthService, + { + provide: Logger, + useValue: { + info: jest.fn(), + }, + }, + { + provide: UserService, + useValue: { + getUserByUsername: jest.fn(), + }, + }, + { + provide: WorkspaceMemberService, + useValue: { + findAllByUserId: jest.fn(), + }, + }, + { + provide: AuthService, + useValue: { + create: jest.fn(), + }, + }, + { + provide: Logger, + useValue: { + error: jest.fn(), + }, + }, + ], }).compile(); controller = module.get(ResponseSchemaController); @@ -66,5 +104,146 @@ describe('ResponseSchemaController', () => { new HttpException('问卷已删除', EXCEPTION_CODE.RESPONSE_SCHEMA_REMOVED), ); }); + + it('whitelistValidate should throw SurveyNotFoundException when survey is removed', async () => { + const surveyPath = ''; + jest + .spyOn(responseSchemaService, 'getResponseSchemaByPath') + .mockResolvedValue(null); + await expect( + controller.whitelistValidate(surveyPath, { + password: '123456', + }), + ).rejects.toThrow(new SurveyNotFoundException('该问卷不存在,无法提交')); + }); + + it('whitelistValidate should throw WHITELIST_ERROR code when password is incorrect', async () => { + const surveyPath = ''; + jest + .spyOn(responseSchemaService, 'getResponseSchemaByPath') + .mockResolvedValue({ + curStatus: { + status: 'published', + }, + code: { + baseConf: { + passwordSwitch: true, + password: '123456', + }, + }, + } as ResponseSchema); + await expect( + controller.whitelistValidate(surveyPath, { + password: '123457', + }), + ).rejects.toThrow( + new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR), + ); + }); + + it('whitelistValidate should be successfully', async () => { + const surveyPath = 'test'; + jest + .spyOn(responseSchemaService, 'getResponseSchemaByPath') + .mockResolvedValue({ + curStatus: { + status: 'published', + }, + code: { + baseConf: { + passwordSwitch: true, + password: '123456', + }, + }, + } as ResponseSchema); + + await expect( + controller.whitelistValidate(surveyPath, { + password: '123456', + }), + ).resolves.toEqual({ code: 200, data: null }); + }); + + it('whitelistValidate should throw WHITELIST_ERROR code when mobile or email is incorrect', async () => { + const surveyPath = ''; + jest + .spyOn(responseSchemaService, 'getResponseSchemaByPath') + .mockResolvedValue({ + curStatus: { + status: 'published', + }, + code: { + baseConf: { + passwordSwitch: true, + password: '123456', + whitelistType: 'CUSTOM', + memberType: 'MOBILE', + whitelist: ['13500000000'], + }, + }, + } as ResponseSchema); + await expect( + controller.whitelistValidate(surveyPath, { + password: '123456', + whitelist: '13500000001', + }), + ).rejects.toThrow( + new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR), + ); + }); + + it('whitelistValidate should throw WHITELIST_ERROR code when member is incorrect', async () => { + const surveyPath = ''; + jest + .spyOn(responseSchemaService, 'getResponseSchemaByPath') + .mockResolvedValue({ + curStatus: { + status: 'published', + }, + code: { + baseConf: { + passwordSwitch: true, + password: '123456', + whitelistType: 'MEMBER', + whitelist: ['Jack'], + }, + }, + } as ResponseSchema); + await expect( + controller.whitelistValidate(surveyPath, { + password: '123456', + whitelist: 'James', + }), + ).rejects.toThrow( + new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR), + ); + }); + }); + + it('whitelistValidate should return verifyId successfully', async () => { + const surveyPath = ''; + jest + .spyOn(responseSchemaService, 'getResponseSchemaByPath') + .mockResolvedValue({ + curStatus: { + status: 'published', + }, + code: { + baseConf: { + passwordSwitch: true, + password: '123456', + whitelistType: 'CUSTOM', + memberType: 'MOBILE', + whitelist: ['13500000000'], + }, + }, + } as ResponseSchema); + + await expect( + controller.whitelistValidate(surveyPath, { + password: '123456', + whitelist: '13500000000', + }), + ).resolves.toEqual({ code: 200, data: null }); }); }); diff --git a/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts b/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts index 5cb5a950..7ba95267 100644 --- a/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts +++ b/server/src/modules/surveyResponse/__test/surveyResponse.controller.spec.ts @@ -21,6 +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 { 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', @@ -124,6 +128,18 @@ describe('SurveyResponseController', () => { info: jest.fn(), }, }, + { + provide: UserService, + useValue: { + getUserByUsername: jest.fn(), + }, + }, + { + provide: WorkspaceMemberService, + useValue: { + findAllByUserId: jest.fn(), + }, + }, ], }).compile(); @@ -306,5 +322,31 @@ describe('SurveyResponseController', () => { HttpException, ); }); + + it('should throw HttpException if password does not match', async () => { + const reqBody = { + ...mockSubmitData, + password: '123457', + sign: '4ff02062141d92d80629eae4797ba68056f29a9709cdf59bf206776fc0971c1a.1710400229589', + }; + + jest + .spyOn(responseSchemaService, 'getResponseSchemaByPath') + .mockResolvedValueOnce({ + curStatus: { + status: RECORD_STATUS.PUBLISHED, + }, + code: { + baseConf: { + passwordSwitch: true, + password: '123456', + }, + }, + } as ResponseSchema); + + await expect(controller.createResponse(reqBody, {})).rejects.toThrow( + new HttpException('白名单验证失败', EXCEPTION_CODE.WHITELIST_ERROR), + ); + }); }); }); diff --git a/server/src/modules/surveyResponse/controllers/responseSchema.controller.ts b/server/src/modules/surveyResponse/controllers/responseSchema.controller.ts index 30fd2055..8df092bf 100644 --- a/server/src/modules/surveyResponse/controllers/responseSchema.controller.ts +++ b/server/src/modules/surveyResponse/controllers/responseSchema.controller.ts @@ -1,14 +1,33 @@ -import { Controller, Get, HttpCode, Query } from '@nestjs/common'; +import { + Body, + Controller, + Get, + HttpCode, + Param, + Post, + Query, +} from '@nestjs/common'; import { ResponseSchemaService } from '../services/responseScheme.service'; import { HttpException } from 'src/exceptions/httpException'; import { EXCEPTION_CODE } from 'src/enums/exceptionCode'; import { RECORD_STATUS } from 'src/enums'; import { ApiTags } from '@nestjs/swagger'; +import Joi from 'joi'; +import { Logger } from 'src/logger'; +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'; @ApiTags('surveyResponse') @Controller('/api/responseSchema') export class ResponseSchemaController { - constructor(private readonly responseSchemaService: ResponseSchemaService) {} + constructor( + private readonly responseSchemaService: ResponseSchemaService, + private readonly logger: Logger, + private readonly userService: UserService, + private readonly workspaceMemberService: WorkspaceMemberService, + ) {} @Get('/getSchema') @HttpCode(200) @@ -34,9 +53,79 @@ export class ResponseSchemaController { EXCEPTION_CODE.RESPONSE_SCHEMA_REMOVED, ); } + + // 去掉C端的敏感字段 + if (responseSchema.code?.baseConf) { + responseSchema.code.baseConf.password = null; + responseSchema.code.baseConf.whitelist = []; + } return { code: 200, data: responseSchema, }; } + + // 白名单验证 + @Post('/:surveyPath/validate') + @HttpCode(200) + async whitelistValidate(@Param('surveyPath') surveyPath, @Body() body) { + const { value, error } = Joi.object({ + password: Joi.string().allow(null, ''), + whitelist: Joi.string().allow(null, ''), + }).validate(body, { allowUnknown: true }); + + if (error) { + this.logger.error(`whitelistValidate error: ${error.message}`, {}); + throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR); + } + + // 问卷信息 + const schema = + await this.responseSchemaService.getResponseSchemaByPath(surveyPath); + if (!schema || schema.curStatus.status === 'removed') { + throw new SurveyNotFoundException('该问卷不存在,无法提交'); + } + + const { password, whitelist: whitelistValue } = value; + const { + passwordSwitch, + password: settingPassword, + whitelistType, + whitelist, + } = schema.code.baseConf; + + // 密码校验 + if (passwordSwitch) { + if (settingPassword !== password) { + throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR); + } + } + + // 名单校验(手机号/邮箱) + if (whitelistType === WhitelistType.CUSTOM) { + if (!whitelist.includes(whitelistValue)) { + throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR); + } + } + + // 团队成员昵称校验 + if (whitelistType === WhitelistType.MEMBER) { + const user = await this.userService.getUserByUsername(whitelistValue); + if (!user) { + throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR); + } + + const workspaceMember = await this.workspaceMemberService.findAllByUserId( + { userId: user._id.toString() }, + ); + if (!workspaceMember.length) { + throw new HttpException('验证失败', EXCEPTION_CODE.WHITELIST_ERROR); + } + } + + return { + code: 200, + data: null, + }; + } } diff --git a/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts b/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts index 9f3bbb61..bdf493f3 100644 --- a/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts +++ b/server/src/modules/surveyResponse/controllers/surveyResponse.controller.ts @@ -17,6 +17,9 @@ import * as Joi from 'joi'; import * as forge from 'node-forge'; import { ApiTags } from '@nestjs/swagger'; import { Logger } from 'src/logger'; +import { WhitelistType } from 'src/interfaces/survey'; +import { UserService } from 'src/modules/auth/services/user.service'; +import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service'; @ApiTags('surveyResponse') @Controller('/api/surveyResponse') @@ -28,6 +31,8 @@ export class SurveyResponseController { private readonly clientEncryptService: ClientEncryptService, private readonly messagePushingTaskService: MessagePushingTaskService, private readonly logger: Logger, + private readonly userService: UserService, + private readonly workspaceMemberService: WorkspaceMemberService, ) {} @Post('/createResponse') @@ -43,6 +48,8 @@ export class SurveyResponseController { sessionId: Joi.string(), clientTime: Joi.number().required(), difTime: Joi.number(), + password: Joi.string().allow(null, ''), + whitelist: Joi.string().allow(null, ''), }).validate(reqBody, { allowUnknown: true }); if (error) { @@ -52,8 +59,16 @@ export class SurveyResponseController { throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR); } - const { surveyPath, encryptType, data, sessionId, clientTime, difTime } = - value; + const { + surveyPath, + encryptType, + data, + sessionId, + clientTime, + difTime, + password, + whitelist: whitelistValue, + } = value; // 查询schema const responseSchema = @@ -62,6 +77,50 @@ export class SurveyResponseController { throw new SurveyNotFoundException('该问卷不存在,无法提交'); } + // 白名单的verifyId校验 + const baseConf = responseSchema.code.baseConf; + + // 密码校验 + 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, + ); + } + + const workspaceMember = await this.workspaceMemberService.findAllByUserId( + { userId: user._id.toString() }, + ); + if (!workspaceMember.length) { + throw new HttpException( + '白名单验证失败', + EXCEPTION_CODE.WHITELIST_ERROR, + ); + } + } + const now = Date.now(); // 提交时间限制 const begTime = responseSchema.code?.baseConf?.begTime || 0; diff --git a/server/src/modules/surveyResponse/surveyResponse.module.ts b/server/src/modules/surveyResponse/surveyResponse.module.ts index 6f9b6fc9..faf1a86d 100644 --- a/server/src/modules/surveyResponse/surveyResponse.module.ts +++ b/server/src/modules/surveyResponse/surveyResponse.module.ts @@ -20,6 +20,8 @@ import { CounterController } from './controllers/counter.controller'; import { ResponseSchemaController } from './controllers/responseSchema.controller'; import { SurveyResponseController } from './controllers/surveyResponse.controller'; import { SurveyResponseUIController } from './controllers/surveyResponseUI.controller'; +import { AuthModule } from '../auth/auth.module'; +import { WorkspaceModule } from '../workspace/workspace.module'; @Module({ imports: [ @@ -31,6 +33,8 @@ import { SurveyResponseUIController } from './controllers/surveyResponseUI.contr ]), ConfigModule, MessageModule, + AuthModule, + WorkspaceModule, ], controllers: [ ClientEncryptController, diff --git a/server/src/modules/workspace/_test/workspace.controller.spec.ts b/server/src/modules/workspace/_test/workspace.controller.spec.ts index e34409fb..73d84ab3 100644 --- a/server/src/modules/workspace/_test/workspace.controller.spec.ts +++ b/server/src/modules/workspace/_test/workspace.controller.spec.ts @@ -35,6 +35,7 @@ describe('WorkspaceController', () => { findAllByIdWithPagination: jest.fn(), update: jest.fn(), delete: jest.fn(), + findAllByUserId: jest.fn(), }, }, { @@ -46,6 +47,7 @@ describe('WorkspaceController', () => { batchUpdate: jest.fn(), batchDelete: jest.fn(), countByWorkspaceId: jest.fn(), + batchSearchByWorkspace: jest.fn(), }, }, { @@ -237,4 +239,31 @@ describe('WorkspaceController', () => { expect(workspaceService.delete).toHaveBeenCalledWith(id); }); }); + + describe('getWorkspaceAndMember', () => { + it('should return a list of workspaces and members for the user', async () => { + const req = { user: { _id: new ObjectId() } }; + + const workspaceId = new ObjectId(); + const memberList = [{ workspaceId, userId: new ObjectId() }]; + const workspaces = [{ _id: workspaceId, name: 'Test Workspace' }]; + + jest + .spyOn(workspaceService, 'findAllByUserId') + .mockResolvedValue(workspaces as Array); + jest + .spyOn(workspaceMemberService, 'batchSearchByWorkspace') + .mockResolvedValue(memberList as unknown as Array); + + const result = await controller.getWorkspaceAndMember(req); + + expect(result.code).toEqual(200); + expect(workspaceService.findAllByUserId).toHaveBeenCalledWith( + req.user._id.toString(), + ); + expect( + workspaceMemberService.batchSearchByWorkspace, + ).toHaveBeenCalledWith(workspaces.map((item) => item._id.toString())); + }); + }); }); diff --git a/server/src/modules/workspace/_test/workspace.service.spec.ts b/server/src/modules/workspace/_test/workspace.service.spec.ts index 1eb12233..a243dec6 100644 --- a/server/src/modules/workspace/_test/workspace.service.spec.ts +++ b/server/src/modules/workspace/_test/workspace.service.spec.ts @@ -123,4 +123,25 @@ describe('WorkspaceService', () => { expect(surveyMetaRepository.updateMany).toHaveBeenCalledTimes(1); }); }); + + describe('findAllByUserId', () => { + it('should return all workspaces under a user', async () => { + const workspaceIdList = [ + new ObjectId().toString(), + new ObjectId().toString(), + ]; + const workspaces = [ + { _id: workspaceIdList[0], name: 'Workspace 1' }, + { _id: workspaceIdList[1], name: 'Workspace 2' }, + ]; + + jest + .spyOn(workspaceRepository, 'find') + .mockResolvedValue(workspaces as any); + + const result = await service.findAllByUserId(''); + expect(result).toEqual(workspaces); + expect(workspaceRepository.find).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/server/src/modules/workspace/_test/workspaceMember.service.spec.ts b/server/src/modules/workspace/_test/workspaceMember.service.spec.ts index d115c3fe..8770bf20 100644 --- a/server/src/modules/workspace/_test/workspaceMember.service.spec.ts +++ b/server/src/modules/workspace/_test/workspaceMember.service.spec.ts @@ -193,4 +193,21 @@ describe('WorkspaceMemberService', () => { }); }); }); + + describe('batchSearchByWorkspace', () => { + it('should return all workspace members by workspace id list', async () => { + const workspaceList = ['workspaceId1', 'workspaceId2']; + const members = [ + { userId: 'userId1', workspaceId: workspaceList[0] }, + { userId: 'userId2', workspaceId: workspaceList[1] }, + ]; + + jest.spyOn(repository, 'find').mockResolvedValue(members as any); + + const result = await service.batchSearchByWorkspace(workspaceList); + + expect(result).toEqual(members); + expect(repository.find).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/server/src/modules/workspace/controllers/workspace.controller.ts b/server/src/modules/workspace/controllers/workspace.controller.ts index fad75ca9..93928cd8 100644 --- a/server/src/modules/workspace/controllers/workspace.controller.ts +++ b/server/src/modules/workspace/controllers/workspace.controller.ts @@ -33,6 +33,8 @@ import { UserService } from 'src/modules/auth/services/user.service'; import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service'; import { Logger } from 'src/logger'; import { GetWorkspaceListDto } from '../dto/getWorkspaceList.dto'; +import { WorkspaceMember } from 'src/models/workspaceMember.entity'; +import { Workspace } from 'src/models/workspace.entity'; @ApiTags('workspace') @ApiBearerAuth() @@ -349,4 +351,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 = {}; + 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, + }; + } } diff --git a/server/src/modules/workspace/services/workspace.service.ts b/server/src/modules/workspace/services/workspace.service.ts index 7ceeec6d..fb097a7a 100644 --- a/server/src/modules/workspace/services/workspace.service.ts +++ b/server/src/modules/workspace/services/workspace.service.ts @@ -149,4 +149,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', + ], + }); + } } diff --git a/server/src/modules/workspace/services/workspaceMember.service.ts b/server/src/modules/workspace/services/workspaceMember.service.ts index f4535f75..baf9f8b2 100644 --- a/server/src/modules/workspace/services/workspaceMember.service.ts +++ b/server/src/modules/workspace/services/workspaceMember.service.ts @@ -140,4 +140,19 @@ export class WorkspaceMemberService { }, }); } + + // 根据空间id批量查询成员 + async batchSearchByWorkspace(workspaceList: string[]) { + return await this.workspaceMemberRepository.find({ + where: { + workspaceId: { + $in: workspaceList, + }, + }, + order: { + _id: -1, + }, + select: ['_id', 'userId', 'username', 'role', 'workspaceId'], + }); + } }