feat: 新增空间和协作功能 (#252)

This commit is contained in:
luch 2024-05-30 21:32:14 +08:00 committed by GitHub
parent 17b84ef501
commit f9d75962ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
69 changed files with 4217 additions and 439 deletions

View File

@ -13,6 +13,7 @@ import { SurveyResponseModule } from './modules/surveyResponse/surveyResponse.mo
import { AuthModule } from './modules/auth/auth.module';
import { MessageModule } from './modules/message/message.module';
import { FileModule } from './modules/file/file.module';
import { WorkspaceModule } from './modules/workspace/workspace.module';
import { join } from 'path';
@ -31,6 +32,9 @@ import { ClientEncrypt } from './models/clientEncrypt.entity';
import { Word } from './models/word.entity';
import { MessagePushingTask } from './models/messagePushingTask.entity';
import { MessagePushingLog } from './models/messagePushingLog.entity';
import { WorkspaceMember } from './models/workspaceMember.entity';
import { Workspace } from './models/workspace.entity';
import { Collaborator } from './models/collaborator.entity';
import { LoggerProvider } from './logger/logger.provider';
import { PluginManagerProvider } from './securityPlugin/pluginManager.provider';
@ -74,6 +78,9 @@ import { Logger } from './logger';
Word,
MessagePushingTask,
MessagePushingLog,
Workspace,
WorkspaceMember,
Collaborator,
],
};
},
@ -92,6 +99,7 @@ import { Logger } from './logger';
}),
MessageModule,
FileModule,
WorkspaceModule,
],
controllers: [AppController],
providers: [

View File

@ -1,6 +1,8 @@
export enum EXCEPTION_CODE {
AUTHENTICATION_FAILED = 1001, // 没有权限
AUTHENTICATION_FAILED = 1001, // 未授权
PARAMETER_ERROR = 1002, // 参数有误
NO_PERMISSION = 1003, // 没有操作权限
USER_EXISTS = 2001, // 用户已存在
USER_NOT_EXISTS = 2002, // 用户不存在
USER_PASSWORD_WRONG = 2003, // 用户名或密码错误

View File

@ -0,0 +1,20 @@
export enum SURVEY_PERMISSION {
SURVEY_CONF_MANAGE = 'SURVEY_CONF_MANAGE',
SURVEY_RESPONSE_MANAGE = 'SURVEY_RESPONSE_MANAGE',
SURVEY_COOPERATION_MANAGE = 'SURVEY_COOPERATION_MANAGE',
}
export const SURVEY_PERMISSION_DESCRIPTION = {
SURVEY_CONF_MANAGE: {
name: '问卷配置管理',
value: SURVEY_PERMISSION.SURVEY_CONF_MANAGE,
},
surveyResponseManage: {
name: '问卷分析管理',
value: SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
},
surveyCooperatorManage: {
name: '协作者管理',
value: SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
},
};

View File

@ -0,0 +1,41 @@
export enum ROLE {
ADMIN = 'admin',
USER = 'user',
}
export const ROLE_DESCRIPTION = {
ADMIN: {
name: '管理员',
value: ROLE.ADMIN,
},
USER: {
name: '用户',
value: ROLE.USER,
},
};
export enum PERMISSION {
READ_WORKSPACE = 'READ_WORKSPACE',
WRITE_WORKSPACE = 'WRITE_WORKSPACE',
READ_MEMBER = 'READ_MEMBER',
WRITE_MEMBER = 'WRITE_MEMBER',
READ_SURVEY = 'READ_SURVEY',
WRITE_SURVEY = 'WRITE_SURVEY',
}
export const ROLE_PERMISSION: Record<ROLE, PERMISSION[]> = {
[ROLE.ADMIN]: [
PERMISSION.READ_WORKSPACE,
PERMISSION.WRITE_WORKSPACE,
PERMISSION.READ_MEMBER,
PERMISSION.WRITE_MEMBER,
PERMISSION.READ_SURVEY,
PERMISSION.WRITE_SURVEY,
],
[ROLE.USER]: [
PERMISSION.READ_WORKSPACE,
PERMISSION.READ_MEMBER,
PERMISSION.READ_SURVEY,
PERMISSION.WRITE_SURVEY,
],
};

View File

@ -0,0 +1,55 @@
import { Test, TestingModule } from '@nestjs/testing';
import { HttpExceptionsFilter } from '../httpExceptions.filter';
import { ArgumentsHost } from '@nestjs/common';
import { HttpException } from '../httpException';
import { Response } from 'express';
describe('HttpExceptionsFilter', () => {
let filter: HttpExceptionsFilter;
let mockArgumentsHost: ArgumentsHost;
let mockResponse: Partial<Response>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [HttpExceptionsFilter],
}).compile();
filter = module.get<HttpExceptionsFilter>(HttpExceptionsFilter);
mockResponse = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
};
mockArgumentsHost = {
switchToHttp: jest.fn().mockReturnThis(),
getResponse: jest.fn().mockReturnValue(mockResponse),
} as unknown as ArgumentsHost;
});
it('should return 500 status and "Internal Server Error" message for generic errors', () => {
const genericError = new Error('Some error');
filter.catch(genericError, mockArgumentsHost);
expect(mockResponse.status).toHaveBeenCalledWith(500);
expect(mockResponse.json).toHaveBeenCalledWith({
message: 'Internal Server Error',
code: 500,
errmsg: 'Some error',
});
});
it('should return 200 status and specific message for HttpException', () => {
const httpException = new HttpException('Specific error message', 1001);
filter.catch(httpException, mockArgumentsHost);
expect(mockResponse.status).toHaveBeenCalledWith(200);
expect(mockResponse.json).toHaveBeenCalledWith({
message: 'Specific error message',
code: 1001,
errmsg: 'Specific error message',
});
});
});

View File

@ -1,4 +1,3 @@
// all-exceptions.filter.ts
import {
ExceptionFilter,
Catch,

View File

@ -1,8 +1,8 @@
import { HttpException } from './httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
export class NoSurveyPermissionException extends HttpException {
export class NoPermissionException extends HttpException {
constructor(public readonly message: string) {
super(message, EXCEPTION_CODE.NO_SURVEY_PERMISSION);
super(message, EXCEPTION_CODE.NO_PERMISSION);
}
}

View File

@ -1,21 +1,21 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { Authtication } from './authtication';
import { Authentication } from '../authentication.guard';
import { AuthService } from 'src/modules/auth/services/auth.service';
import { AuthenticationException } from 'src/exceptions/authException';
import { User } from 'src/models/user.entity';
jest.mock('jsonwebtoken');
describe('Authtication', () => {
let guard: Authtication;
describe('Authentication', () => {
let guard: Authentication;
let authService: AuthService;
let configService: ConfigService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
Authtication,
Authentication,
{
provide: AuthService,
useValue: {
@ -31,7 +31,7 @@ describe('Authtication', () => {
],
}).compile();
guard = module.get<Authtication>(Authtication);
guard = module.get<Authentication>(Authentication);
authService = module.get<AuthService>(AuthService);
configService = module.get<ConfigService>(ConfigService);
});

View File

@ -0,0 +1,139 @@
import { Reflector } from '@nestjs/core';
import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext } from '@nestjs/common';
import { SurveyGuard } from '../survey.guard';
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
import { CollaboratorService } from 'src/modules/survey/services/collaborator.service';
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
import { NoPermissionException } from 'src/exceptions/noPermissionException';
import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
import { Collaborator } from 'src/models/collaborator.entity';
describe('SurveyGuard', () => {
let guard: SurveyGuard;
let reflector: Reflector;
let collaboratorService: CollaboratorService;
let surveyMetaService: SurveyMetaService;
let workspaceMemberService: WorkspaceMemberService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
SurveyGuard,
{
provide: Reflector,
useValue: {
get: jest.fn(),
},
},
{
provide: CollaboratorService,
useValue: {
getCollaborator: jest.fn(),
},
},
{
provide: SurveyMetaService,
useValue: {
getSurveyById: jest.fn(),
},
},
{
provide: WorkspaceMemberService,
useValue: {
findOne: jest.fn(),
},
},
],
}).compile();
guard = module.get<SurveyGuard>(SurveyGuard);
reflector = module.get<Reflector>(Reflector);
collaboratorService = module.get<CollaboratorService>(CollaboratorService);
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
workspaceMemberService = module.get<WorkspaceMemberService>(
WorkspaceMemberService,
);
});
it('should be defined', () => {
expect(guard).toBeDefined();
});
it('should allow access if no surveyId is present', async () => {
const context = createMockExecutionContext();
jest.spyOn(reflector, 'get').mockReturnValue(null);
const result = await guard.canActivate(context);
expect(result).toBe(true);
});
it('should throw SurveyNotFoundException if survey does not exist', async () => {
const context = createMockExecutionContext();
jest.spyOn(reflector, 'get').mockReturnValue('params.surveyId');
jest.spyOn(surveyMetaService, 'getSurveyById').mockResolvedValue(null);
await expect(guard.canActivate(context)).rejects.toThrow(
SurveyNotFoundException,
);
});
it('should allow access if user is the owner of the survey', async () => {
const context = createMockExecutionContext();
const surveyMeta = { owner: 'testUser', workspaceId: null };
jest.spyOn(reflector, 'get').mockReturnValue('params.surveyId');
jest
.spyOn(surveyMetaService, 'getSurveyById')
.mockResolvedValue(surveyMeta as SurveyMeta);
const result = await guard.canActivate(context);
expect(result).toBe(true);
});
it('should allow access if user is a workspace member', async () => {
const context = createMockExecutionContext();
const surveyMeta = { owner: 'anotherUser', workspaceId: 'workspaceId' };
jest.spyOn(reflector, 'get').mockReturnValue('params.surveyId');
jest
.spyOn(surveyMetaService, 'getSurveyById')
.mockResolvedValue(surveyMeta as SurveyMeta);
jest
.spyOn(workspaceMemberService, 'findOne')
.mockResolvedValue({} as WorkspaceMember);
const result = await guard.canActivate(context);
expect(result).toBe(true);
});
it('should throw NoPermissionException if user has no permissions', async () => {
const context = createMockExecutionContext();
const surveyMeta = { owner: 'anotherUser', workspaceId: null };
jest.spyOn(reflector, 'get').mockReturnValueOnce('params.surveyId');
jest.spyOn(reflector, 'get').mockReturnValueOnce(['requiredPermission']);
jest
.spyOn(surveyMetaService, 'getSurveyById')
.mockResolvedValue(surveyMeta as SurveyMeta);
jest
.spyOn(collaboratorService, 'getCollaborator')
.mockResolvedValue({ permissions: [] } as Collaborator);
await expect(guard.canActivate(context)).rejects.toThrow(
NoPermissionException,
);
});
function createMockExecutionContext(): ExecutionContext {
return {
switchToHttp: jest.fn().mockReturnValue({
getRequest: jest.fn().mockReturnValue({
user: { username: 'testUser', _id: 'testUserId' },
params: { surveyId: 'surveyId' },
}),
}),
getHandler: jest.fn(),
} as unknown as ExecutionContext;
}
});

View File

@ -0,0 +1,137 @@
import { Reflector } from '@nestjs/core';
import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext } from '@nestjs/common';
import { WorkspaceGuard } from '../workspace.guard';
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
import { NoPermissionException } from '../../exceptions/noPermissionException';
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
import { PERMISSION as WORKSPACE_PERMISSION } from 'src/enums/workspace';
describe('WorkspaceGuard', () => {
let guard: WorkspaceGuard;
let reflector: Reflector;
let workspaceMemberService: WorkspaceMemberService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
WorkspaceGuard,
{
provide: Reflector,
useValue: {
get: jest.fn(),
},
},
{
provide: WorkspaceMemberService,
useValue: {
findOne: jest.fn(),
},
},
],
}).compile();
guard = module.get<WorkspaceGuard>(WorkspaceGuard);
reflector = module.get<Reflector>(Reflector);
workspaceMemberService = module.get<WorkspaceMemberService>(
WorkspaceMemberService,
);
});
it('should be defined', () => {
expect(guard).toBeDefined();
});
it('should allow access if no roles are defined', async () => {
const context = createMockExecutionContext();
jest.spyOn(reflector, 'get').mockReturnValue(null);
const result = await guard.canActivate(context);
expect(result).toBe(true);
});
it('should throw NoPermissionException if workspaceId is missing and optional is false', async () => {
const context = createMockExecutionContext();
jest
.spyOn(reflector, 'get')
.mockReturnValueOnce([WORKSPACE_PERMISSION.READ_WORKSPACE]);
jest.spyOn(reflector, 'get').mockReturnValueOnce('params.workspaceId');
await expect(guard.canActivate(context)).rejects.toThrow(
NoPermissionException,
);
});
it('should allow access if workspaceId is missing and optional is true', async () => {
const context = createMockExecutionContext();
jest
.spyOn(reflector, 'get')
.mockReturnValueOnce([WORKSPACE_PERMISSION.WRITE_WORKSPACE]);
jest
.spyOn(reflector, 'get')
.mockReturnValueOnce({ key: 'params.workspaceId', optional: true });
jest
.spyOn(workspaceMemberService, 'findOne')
.mockResolvedValue({ role: 'admin' } as WorkspaceMember);
const result = await guard.canActivate(context);
expect(result).toBe(true);
});
it('should throw NoPermissionException if user is not a member of the workspace', async () => {
const context = createMockExecutionContext();
jest
.spyOn(reflector, 'get')
.mockReturnValueOnce([WORKSPACE_PERMISSION.WRITE_WORKSPACE]);
jest.spyOn(reflector, 'get').mockReturnValueOnce('params.workspaceId');
jest.spyOn(workspaceMemberService, 'findOne').mockResolvedValue(null);
await expect(guard.canActivate(context)).rejects.toThrow(
NoPermissionException,
);
});
it('should throw NoPermissionException if user role is not allowed', async () => {
const context = createMockExecutionContext();
jest
.spyOn(reflector, 'get')
.mockReturnValueOnce([WORKSPACE_PERMISSION.READ_MEMBER]);
jest.spyOn(reflector, 'get').mockReturnValueOnce('params.workspaceId');
jest
.spyOn(workspaceMemberService, 'findOne')
.mockResolvedValue({ role: 'member' } as WorkspaceMember);
await expect(guard.canActivate(context)).rejects.toThrow(
NoPermissionException,
);
});
it('should allow access if user role is allowed', async () => {
const context = createMockExecutionContext();
jest
.spyOn(reflector, 'get')
.mockReturnValueOnce([WORKSPACE_PERMISSION.READ_MEMBER]);
jest.spyOn(reflector, 'get').mockReturnValueOnce('params.workspaceId');
jest
.spyOn(workspaceMemberService, 'findOne')
.mockResolvedValue({ role: 'admin' } as WorkspaceMember);
const result = await guard.canActivate(context);
expect(result).toBe(true);
});
function createMockExecutionContext(): ExecutionContext {
return {
switchToHttp: jest.fn().mockReturnValue({
getRequest: jest.fn().mockReturnValue({
user: { _id: 'testUserId' },
params: { workspaceId: 'workspaceId' },
}),
}),
getHandler: jest.fn(),
} as unknown as ExecutionContext;
}
});

View File

@ -3,7 +3,7 @@ import { AuthenticationException } from '../exceptions/authException';
import { AuthService } from 'src/modules/auth/services/auth.service';
@Injectable()
export class Authtication implements CanActivate {
export class Authentication implements CanActivate {
constructor(private readonly authService: AuthService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {

View File

@ -0,0 +1,88 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { get } from 'lodash';
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
import { CollaboratorService } from 'src/modules/survey/services/collaborator.service';
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
import { NoPermissionException } from 'src/exceptions/noPermissionException';
@Injectable()
export class SurveyGuard implements CanActivate {
constructor(
private reflector: Reflector,
private readonly collaboratorService: CollaboratorService,
private readonly surveyMetaService: SurveyMetaService,
private readonly workspaceMemberService: WorkspaceMemberService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const user = request.user;
const surveyIdKey = this.reflector.get<string>(
'surveyId',
context.getHandler(),
);
const surveyId = get(request, surveyIdKey);
if (!surveyId) {
return true;
}
const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId });
if (!surveyMeta) {
throw new SurveyNotFoundException('问卷不存在');
}
request.surveyMeta = surveyMeta;
// 兼容老的问卷没有ownerId
if (
surveyMeta.ownerId === user._id.toString() ||
surveyMeta.owner === user.username
) {
// 问卷的owner可以访问和操作问卷
return true;
}
if (surveyMeta.workspaceId) {
const memberInfo = await this.workspaceMemberService.findOne({
workspaceId: surveyMeta.workspaceId,
userId: user._id.toString(),
});
if (!memberInfo) {
throw new NoPermissionException('没有权限');
}
return true;
}
const permissions = this.reflector.get<string[]>(
'surveyPermission',
context.getHandler(),
);
if (!Array.isArray(permissions) || permissions.length === 0) {
throw new NoPermissionException('没有权限');
}
const info = await this.collaboratorService.getCollaborator({
surveyId,
userId: user._id.toString(),
});
if (!info) {
throw new NoPermissionException('没有权限');
}
request.collaborator = info;
if (
permissions.some((permission) => info.permissions.includes(permission))
) {
return true;
}
throw new NoPermissionException('没有权限');
}
}

View File

@ -0,0 +1,72 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { get } from 'lodash';
import { NoPermissionException } from '../exceptions/noPermissionException';
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
import { ROLE_PERMISSION as WORKSPACE_ROLE_PERMISSION } from 'src/enums/workspace';
@Injectable()
export class WorkspaceGuard implements CanActivate {
constructor(
private reflector: Reflector,
private readonly workspaceMemberService: WorkspaceMemberService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const allowPermissions = this.reflector.get<string[]>(
'workspacePermissions',
context.getHandler(),
);
if (!allowPermissions) {
return true; // 没有定义权限,可以访问
}
const request = context.switchToHttp().getRequest();
const user = request.user;
const workspaceIdInfo = this.reflector.get(
'workspaceId',
context.getHandler(),
);
let workspaceIdKey, optional;
if (typeof workspaceIdInfo === 'string') {
workspaceIdKey = workspaceIdInfo;
optional = false;
} else {
workspaceIdKey = workspaceIdInfo?.key;
optional = workspaceIdInfo?.optional || false;
}
const workspaceId = get(request, workspaceIdKey);
if (!workspaceId && optional === false) {
throw new NoPermissionException('没有空间权限');
}
if (workspaceId) {
const membersInfo = await this.workspaceMemberService.findOne({
workspaceId,
userId: user._id.toString(),
});
if (!membersInfo) {
throw new NoPermissionException('没有空间权限');
}
const userPermissions = WORKSPACE_ROLE_PERMISSION[membersInfo.role] || [];
if (
allowPermissions.some((permission) =>
userPermissions.includes(permission),
)
) {
return true;
}
throw new NoPermissionException('没有权限');
}
return true;
}
}

View File

@ -34,10 +34,10 @@ export class Logger {
_log(message, options: { dltag?: string; level: string; req?: Request }) {
const datetime = moment().format('YYYY-MM-DD HH:mm:ss.SSS');
const level = options.level;
const dltag = options.dltag ? `${options.dltag}||` : '';
const traceIdStr = options?.req['traceId']
? `traceid=${options?.req['traceId']}||`
const level = options?.level;
const dltag = options?.dltag ? `${options.dltag}||` : '';
const traceIdStr = options?.req?.['traceId']
? `traceid=${options?.req?.['traceId']}||`
: '';
return log4jsLogger[level](
`[${datetime}][${level.toUpperCase()}]${dltag}${traceIdStr}${message}`,

View File

@ -0,0 +1,14 @@
import { Entity, Column } from 'typeorm';
import { BaseEntity } from './base.entity';
@Entity({ name: 'collaborator' })
export class Collaborator extends BaseEntity {
@Column()
surveyId: string;
@Column()
userId: string;
@Column('jsonb')
permissions: Array<string>;
}

View File

@ -21,9 +21,15 @@ export class SurveyMeta extends BaseEntity {
@Column()
owner: string;
@Column()
ownerId: string;
@Column()
createMethod: string;
@Column()
createFrom: string;
@Column()
workspaceId: string;
}

View File

@ -0,0 +1,14 @@
import { Entity, Column } from 'typeorm';
import { BaseEntity } from './base.entity';
@Entity({ name: 'workspace' })
export class Workspace extends BaseEntity {
@Column()
ownerId: string;
@Column()
name: string;
@Column()
description: string;
}

View File

@ -0,0 +1,14 @@
import { Entity, Column } from 'typeorm';
import { BaseEntity } from './base.entity';
@Entity({ name: 'workspaceMember' })
export class WorkspaceMember extends BaseEntity {
@Column()
userId: string;
@Column()
workspaceId: string;
@Column()
role: string;
}

View File

@ -0,0 +1,82 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UserController } from '../controllers/user.controller';
import { UserService } from '../services/user.service';
import { GetUserListDto } from '../dto/getUserList.dto';
import { Authentication } from 'src/guards/authentication.guard';
import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { User } from 'src/models/user.entity';
describe('UserController', () => {
let userController: UserController;
let userService: UserService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UserController],
providers: [
{
provide: UserService,
useValue: {
getUserListByUsername: jest.fn(),
},
},
],
})
.overrideGuard(Authentication)
.useValue({ canActivate: jest.fn(() => true) })
.compile();
userController = module.get<UserController>(UserController);
userService = module.get<UserService>(UserService);
});
describe('getUserList', () => {
it('should return a list of users', async () => {
const mockUserList = [
{ _id: '1', username: 'user1' },
{ _id: '2', username: 'user2' },
];
jest
.spyOn(userService, 'getUserListByUsername')
.mockResolvedValue(mockUserList as unknown as User[]);
const queryInfo: GetUserListDto = {
username: 'testuser',
pageIndex: 1,
pageSize: 10,
};
GetUserListDto.validate = jest
.fn()
.mockReturnValue({ value: queryInfo, error: null });
const result = await userController.getUserList(queryInfo);
expect(result).toEqual({
code: 200,
data: mockUserList.map((item) => ({
userId: item._id,
username: item.username,
})),
});
});
it('should throw an HttpException if validation fails', async () => {
const queryInfo: GetUserListDto = {
username: 'testuser',
pageIndex: 1,
pageSize: 10,
};
const validationError = new Error('Validation failed');
GetUserListDto.validate = jest
.fn()
.mockReturnValue({ value: null, error: validationError });
await expect(userController.getUserList(queryInfo)).rejects.toThrow(
new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR),
);
});
});
});

View File

@ -5,6 +5,7 @@ import { UserService } from '../services/user.service';
import { User } from 'src/models/user.entity';
import { HttpException } from 'src/exceptions/httpException';
import { hash256 } from 'src/utils/hash256';
import { RECORD_STATUS } from 'src/enums';
describe('UserService', () => {
let service: UserService;
@ -135,7 +136,10 @@ describe('UserService', () => {
const user = await service.getUserByUsername(username);
expect(userRepository.findOne).toHaveBeenCalledWith({
where: { username: username },
where: {
'curStatus.status': { $ne: RECORD_STATUS.REMOVED },
username: username,
},
});
expect(user).toEqual(userInfo);
});

View File

@ -4,6 +4,7 @@ import { AuthService } from './services/auth.service';
import { CaptchaService } from './services/captcha.service';
import { AuthController } from './controllers/auth.controller';
import { UserController } from './controllers/user.controller';
import { User } from 'src/models/user.entity';
import { Captcha } from 'src/models/captcha.entity';
@ -13,7 +14,7 @@ import { ConfigModule } from '@nestjs/config';
@Module({
imports: [TypeOrmModule.forFeature([User, Captcha]), ConfigModule],
controllers: [AuthController],
controllers: [AuthController, UserController],
providers: [UserService, AuthService, CaptchaService],
exports: [UserService, AuthService],
})

View File

@ -0,0 +1,46 @@
import { Controller, Get, Query, HttpCode, UseGuards } from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { Authentication } from 'src/guards/authentication.guard';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { HttpException } from 'src/exceptions/httpException';
import { UserService } from '../services/user.service';
import { GetUserListDto } from '../dto/getUserList.dto';
@ApiTags('user')
@ApiBearerAuth()
@Controller('/api/user')
export class UserController {
constructor(private readonly userService: UserService) {}
@UseGuards(Authentication)
@Get('/getUserList')
@HttpCode(200)
async getUserList(
@Query()
queryInfo: GetUserListDto,
) {
const { value, error } = GetUserListDto.validate(queryInfo);
if (error) {
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
}
const userList = await this.userService.getUserListByUsername({
username: value.username,
skip: (value.pageIndex - 1) * value.pageSize,
take: value.pageSize,
});
return {
code: 200,
data: userList.map((item) => {
return {
userId: item._id.toString(),
username: item.username,
};
}),
};
}
}

View File

@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
import Joi from 'joi';
export class GetUserListDto {
@ApiProperty({ description: '用户名', required: true })
username: string;
@ApiProperty({ description: '页码', required: false, default: 1 })
pageIndex?: number;
@ApiProperty({ description: '每页查询数', required: false, default: 10 })
pageSize: number;
static validate(data) {
return Joi.object({
username: Joi.string().required(),
pageIndex: Joi.number().allow(null).default(1),
pageSize: Joi.number().allow(null).default(10),
}).validate(data);
}
}

View File

@ -5,6 +5,8 @@ import { User } from 'src/models/user.entity';
import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { hash256 } from 'src/utils/hash256';
import { RECORD_STATUS } from 'src/enums';
import { ObjectId } from 'mongodb';
@Injectable()
export class UserService {
@ -51,9 +53,55 @@ export class UserService {
const user = await this.userRepository.findOne({
where: {
username: username,
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
},
});
return user;
}
async getUserById(id: string) {
const user = await this.userRepository.findOne({
where: {
_id: new ObjectId(id),
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
},
});
return user;
}
async getUserListByUsername({ username, skip, take }) {
const list = await this.userRepository.find({
where: {
username: new RegExp(username),
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
},
skip,
take,
select: ['_id', 'username', 'createDate'],
});
return list;
}
async getUserListByIds({ idList }) {
const list = await this.userRepository.find({
where: {
_id: {
$in: idList.map((item) => new ObjectId(item)),
},
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
},
select: ['_id', 'username', 'createDate'],
});
return list;
}
}

View File

@ -8,14 +8,16 @@ import {
Body,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ConfigService } from '@nestjs/config';
import { ApiTags } from '@nestjs/swagger';
import { FileService } from '../services/file.service';
import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { AuthService } from 'src/modules/auth/services/auth.service';
import { AuthenticationException } from 'src/exceptions/authException';
import { ConfigService } from '@nestjs/config';
@ApiTags('file')
@Controller('/api/file')
export class FileController {
constructor(

View File

@ -10,7 +10,7 @@ import {
MESSAGE_PUSHING_TYPE,
} from 'src/enums/messagePushing';
import { MessagePushingTask } from 'src/models/messagePushingTask.entity';
import { Authtication } from 'src/guards/authtication';
import { Authentication } from 'src/guards/authentication.guard';
import { UserService } from 'src/modules/auth/services/user.service';
import { UpdateMessagePushingTaskDto } from '../dto/updateMessagePushingTask.dto';
@ -38,7 +38,7 @@ describe('MessagePushingTaskController', () => {
},
},
{
provide: Authtication,
provide: Authentication,
useClass: jest.fn().mockImplementation(() => ({
canActivate: () => true,
})),

View File

@ -25,9 +25,9 @@ import { QueryMessagePushingTaskListDto } from '../dto/queryMessagePushingTaskLi
import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { Authtication } from 'src/guards/authtication';
import { Authentication } from 'src/guards/authentication.guard';
@UseGuards(Authtication)
@UseGuards(Authentication)
@ApiBearerAuth()
@ApiTags('messagePushingTasks')
@Controller('/api/messagePushingTasks')
@ -47,12 +47,10 @@ export class MessagePushingTaskController {
req,
@Body() createMessagePushingTaskDto: CreateMessagePushingTaskDto,
) {
let data;
try {
data = await CreateMessagePushingTaskDto.validate(
const { error, value } = CreateMessagePushingTaskDto.validate(
createMessagePushingTaskDto,
);
} catch (error) {
if (error) {
throw new HttpException(
`参数错误: ${error.message}`,
EXCEPTION_CODE.PARAMETER_ERROR,
@ -61,7 +59,7 @@ export class MessagePushingTaskController {
const userId = req.user._id;
const messagePushingTask = await this.messagePushingTaskService.create({
...data,
...value,
ownerId: userId,
});
return {
@ -83,10 +81,8 @@ export class MessagePushingTaskController {
req,
@Query() query: QueryMessagePushingTaskListDto,
) {
let data;
try {
data = await QueryMessagePushingTaskListDto.validate(query);
} catch (error) {
const { error, value } = QueryMessagePushingTaskListDto.validate(query);
if (error) {
throw new HttpException(
`参数错误: ${error.message}`,
EXCEPTION_CODE.PARAMETER_ERROR,
@ -94,8 +90,8 @@ export class MessagePushingTaskController {
}
const userId = req.user._id;
const list = await this.messagePushingTaskService.findAll({
surveyId: data.surveyId,
hook: data.triggerHook,
surveyId: value.surveyId,
hook: value.triggerHook,
ownerId: userId,
});
return {

View File

@ -29,8 +29,8 @@ export class CreateMessagePushingTaskDto {
})
surveys?: string[];
static async validate(data) {
return await Joi.object({
static validate(data) {
return Joi.object({
name: Joi.string().required(),
type: Joi.string().allow(null).default(MESSAGE_PUSHING_TYPE.HTTP),
pushAddress: Joi.string().required(),
@ -38,6 +38,6 @@ export class CreateMessagePushingTaskDto {
.allow(null)
.default(MESSAGE_PUSHING_HOOK.RESPONSE_INSERTED),
surveys: Joi.array().items(Joi.string()).allow(null).default([]),
}).validateAsync(data);
}).validate(data);
}
}

View File

@ -13,6 +13,6 @@ export class QueryMessagePushingTaskListDto {
return Joi.object({
surveyId: Joi.string().required(),
triggerHook: Joi.string().required(),
}).validateAsync(data);
}).validate(data);
}
}

View File

@ -0,0 +1,220 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CollaboratorController } from '../controllers/collaborator.controller';
import { CollaboratorService } from '../services/collaborator.service';
import { Logger } from 'src/logger';
import { HttpException } from 'src/exceptions/httpException';
import { CreateCollaboratorDto } from '../dto/createCollaborator.dto';
import { Collaborator } from 'src/models/collaborator.entity';
import { GetSurveyCollaboratorListDto } from '../dto/getSurveyCollaboratorList.dto';
import { UserService } from 'src/modules/auth/services/user.service';
import { ObjectId } from 'mongodb';
import { SurveyMetaService } from '../services/surveyMeta.service';
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
jest.mock('src/guards/authentication.guard');
jest.mock('src/guards/survey.guard');
jest.mock('src/guards/workspace.guard');
describe('CollaboratorController', () => {
let controller: CollaboratorController;
let collaboratorService: CollaboratorService;
let logger: Logger;
let userService: UserService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [CollaboratorController],
providers: [
{
provide: CollaboratorService,
useValue: {
create: jest.fn(),
getSurveyCollaboratorList: jest.fn(),
changeUserPermission: jest.fn(),
deleteCollaborator: jest.fn(),
getCollaborator: jest.fn(),
},
},
{
provide: Logger,
useValue: {
error: jest.fn(),
},
},
{
provide: UserService,
useValue: {
getUserById: jest.fn().mockImplementation((id) => {
return Promise.resolve({
_id: new ObjectId(id),
});
}),
getUserListByIds: jest.fn(),
},
},
{
provide: SurveyMetaService,
useValue: {
getSurveyById: jest.fn(),
},
},
{
provide: WorkspaceMemberService,
useValue: {
findOne: jest.fn().mockResolvedValue(null),
},
},
],
}).compile();
controller = module.get<CollaboratorController>(CollaboratorController);
collaboratorService = module.get<CollaboratorService>(CollaboratorService);
logger = module.get<Logger>(Logger);
userService = module.get<UserService>(UserService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('addCollaborator', () => {
it('should add a collaborator successfully', async () => {
const userId = new ObjectId().toString();
const reqBody: CreateCollaboratorDto = {
surveyId: 'surveyId',
userId: new ObjectId().toString(),
permissions: [SURVEY_PERMISSION.SURVEY_CONF_MANAGE],
};
const req = { user: { _id: 'userId' }, surveyMeta: { ownerId: userId } };
const result = { _id: 'collaboratorId' };
jest
.spyOn(collaboratorService, 'create')
.mockResolvedValue(result as unknown as Collaborator);
const response = await controller.addCollaborator(reqBody, req);
expect(response).toEqual({
code: 200,
data: {
collaboratorId: result._id,
},
});
});
it('should throw an exception if validation fails', async () => {
const reqBody: CreateCollaboratorDto = {
surveyId: '',
userId: '',
permissions: [SURVEY_PERMISSION.SURVEY_CONF_MANAGE],
};
const req = { user: { _id: 'userId' } };
await expect(controller.addCollaborator(reqBody, req)).rejects.toThrow(
HttpException,
);
});
});
describe('getSurveyCollaboratorList', () => {
it('should return collaborator list', async () => {
const query = { surveyId: 'surveyId' };
const req = { user: { _id: 'userId' } };
const result = [
{ _id: 'collaboratorId', userId: 'userId', username: '' },
];
jest
.spyOn(collaboratorService, 'getSurveyCollaboratorList')
.mockResolvedValue(result as unknown as Array<Collaborator>);
jest.spyOn(userService, 'getUserListByIds').mockResolvedValueOnce([]);
const response = await controller.getSurveyCollaboratorList(query, req);
expect(response).toEqual({
code: 200,
data: result,
});
});
it('should throw an exception if validation fails', async () => {
const query: GetSurveyCollaboratorListDto = {
surveyId: '',
};
const req = { user: { _id: 'userId' } };
await expect(
controller.getSurveyCollaboratorList(query, req),
).rejects.toThrow(HttpException);
expect(logger.error).toHaveBeenCalledTimes(1);
});
});
describe('changeUserPermission', () => {
it('should change user permission successfully', async () => {
const reqBody = {
surveyId: 'surveyId',
userId: 'userId',
permissions: ['read'],
};
const req = { user: { _id: 'userId' } };
const result = { _id: 'userId', permissions: ['read'] };
jest
.spyOn(collaboratorService, 'changeUserPermission')
.mockResolvedValue(result);
const response = await controller.changeUserPermission(reqBody, req);
expect(response).toEqual({
code: 200,
data: result,
});
});
it('should throw an exception if validation fails', async () => {
const reqBody = {
surveyId: '',
userId: '',
permissions: ['surveyManage'],
};
const req = { user: { _id: 'userId' } };
await expect(
controller.changeUserPermission(reqBody, req),
).rejects.toThrow(HttpException);
expect(logger.error).toHaveBeenCalledTimes(1);
});
});
describe('deleteCollaborator', () => {
it('should delete collaborator successfully', async () => {
const query = { surveyId: 'surveyId', userId: 'userId' };
const req = { user: { _id: 'userId' } };
const result = { acknowledged: true, deletedCount: 1 };
jest
.spyOn(collaboratorService, 'deleteCollaborator')
.mockResolvedValue(result);
const response = await controller.deleteCollaborator(query, req);
expect(response).toEqual({
code: 200,
data: result,
});
});
it('should throw an exception if validation fails', async () => {
const query = { surveyId: '', userId: '' };
const req = { user: { _id: 'userId' } };
await expect(controller.deleteCollaborator(query, req)).rejects.toThrow(
HttpException,
);
expect(logger.error).toHaveBeenCalledTimes(1);
});
});
});

View File

@ -0,0 +1,331 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CollaboratorService } from '../services/collaborator.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Collaborator } from 'src/models/collaborator.entity';
import { MongoRepository } from 'typeorm';
import { Logger } from 'src/logger';
import { InsertManyResult, ObjectId } from 'mongodb';
describe('CollaboratorService', () => {
let service: CollaboratorService;
let repository: MongoRepository<Collaborator>;
let logger: Logger;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CollaboratorService,
{
provide: getRepositoryToken(Collaborator),
useClass: MongoRepository,
},
{
provide: Logger,
useValue: {
info: jest.fn(),
},
},
],
}).compile();
service = module.get<CollaboratorService>(CollaboratorService);
repository = module.get<MongoRepository<Collaborator>>(
getRepositoryToken(Collaborator),
);
logger = module.get<Logger>(Logger);
});
describe('create', () => {
it('should create and save a collaborator', async () => {
const createSpy = jest.spyOn(repository, 'create').mockReturnValue({
surveyId: '1',
userId: '1',
permissions: [],
} as Collaborator);
const collaboratorId = new ObjectId().toString();
const saveSpy = jest.spyOn(repository, 'save').mockResolvedValue({
_id: new ObjectId(collaboratorId),
surveyId: '1',
userId: '1',
permissions: [],
} as Collaborator);
const result = await service.create({
surveyId: '1',
userId: '1',
permissions: [],
});
expect(createSpy).toHaveBeenCalledWith({
surveyId: '1',
userId: '1',
permissions: [],
});
expect(saveSpy).toHaveBeenCalledWith({
surveyId: '1',
userId: '1',
permissions: [],
});
expect(result).toEqual({
_id: new ObjectId(collaboratorId),
surveyId: '1',
userId: '1',
permissions: [],
});
});
});
describe('batchCreate', () => {
it('should batch create collaborators', async () => {
const insertManySpy = jest
.spyOn(repository, 'insertMany')
.mockResolvedValue({
insertedCount: 1,
} as unknown as InsertManyResult<Document>);
const result = await service.batchCreate({
surveyId: '1',
collaboratorList: [{ userId: '1', permissions: [] }],
});
expect(insertManySpy).toHaveBeenCalledWith([
{ surveyId: '1', userId: '1', permissions: [] },
]);
expect(result).toEqual({ insertedCount: 1 });
});
});
describe('getSurveyCollaboratorList', () => {
it('should return a list of collaborators for a survey', async () => {
const collaboratorId = new ObjectId().toString();
const findSpy = jest.spyOn(repository, 'find').mockResolvedValue([
{
_id: new ObjectId(collaboratorId),
surveyId: '1',
userId: '1',
permissions: [],
},
] as Collaborator[]);
const result = await service.getSurveyCollaboratorList({ surveyId: '1' });
expect(findSpy).toHaveBeenCalledWith({ surveyId: '1' });
expect(result).toEqual([
{
_id: new ObjectId(collaboratorId),
surveyId: '1',
userId: '1',
permissions: [],
},
]);
});
});
describe('getCollaboratorListByIds', () => {
it('should return a list of collaborators by ids', async () => {
const collaboratorId = new ObjectId().toString();
const findSpy = jest.spyOn(repository, 'find').mockResolvedValue([
{
_id: new ObjectId(collaboratorId),
surveyId: '1',
userId: '1',
permissions: [],
},
] as Collaborator[]);
const result = await service.getCollaboratorListByIds({
idList: [collaboratorId],
});
expect(findSpy).toHaveBeenCalledWith({
_id: {
$in: [new ObjectId(collaboratorId)],
},
});
expect(result).toEqual([
{
_id: new ObjectId(collaboratorId),
surveyId: '1',
userId: '1',
permissions: [],
},
]);
});
});
describe('getCollaborator', () => {
it('should return a collaborator', async () => {
const collaboratorId = new ObjectId().toString();
const findOneSpy = jest.spyOn(repository, 'findOne').mockResolvedValue({
_id: new ObjectId(collaboratorId),
surveyId: '1',
userId: '1',
permissions: [],
} as Collaborator);
const result = await service.getCollaborator({
userId: '1',
surveyId: '1',
});
expect(findOneSpy).toHaveBeenCalledWith({
where: {
surveyId: '1',
userId: '1',
},
});
expect(result).toEqual({
_id: new ObjectId(collaboratorId),
surveyId: '1',
userId: '1',
permissions: [],
});
});
});
describe('changeUserPermission', () => {
it("should update a user's permissions", async () => {
const updateOneSpy = jest
.spyOn(repository, 'updateOne')
.mockResolvedValue({});
const result = await service.changeUserPermission({
userId: '1',
surveyId: '1',
permission: 'read',
});
expect(updateOneSpy).toHaveBeenCalledWith(
{
surveyId: '1',
userId: '1',
},
{
$set: {
permission: 'read',
},
},
);
expect(result).toEqual({});
});
});
describe('deleteCollaborator', () => {
it('should delete a collaborator', async () => {
const mockResult = { acknowledged: true, deletedCount: 1 };
const deleteOneSpy = jest
.spyOn(repository, 'deleteOne')
.mockResolvedValue(mockResult);
const result = await service.deleteCollaborator({
userId: '1',
surveyId: '1',
});
expect(deleteOneSpy).toHaveBeenCalledWith({
userId: '1',
surveyId: '1',
});
expect(result).toEqual(mockResult);
});
});
describe('batchDelete', () => {
it('should batch delete collaborators', async () => {
const mockResult = { acknowledged: true, deletedCount: 1 };
const deleteManySpy = jest
.spyOn(repository, 'deleteMany')
.mockResolvedValue(mockResult);
const collaboratorId = new ObjectId().toString();
const result = await service.batchDelete({
surveyId: '1',
idList: [collaboratorId],
});
const expectedQuery = {
surveyId: '1',
$or: [
{
_id: {
$in: [new ObjectId(collaboratorId)],
},
},
],
};
expect(logger.info).toHaveBeenCalledWith(JSON.stringify(expectedQuery));
expect(deleteManySpy).toHaveBeenCalledWith(expectedQuery);
expect(result).toEqual(mockResult);
});
});
describe('batchDeleteBySurveyId', () => {
it('should batch delete collaborators by survey id', async () => {
const mockResult = { acknowledged: true, deletedCount: 1 };
const deleteManySpy = jest
.spyOn(repository, 'deleteMany')
.mockResolvedValue(mockResult);
const surveyId = new ObjectId().toString();
const result = await service.batchDeleteBySurveyId(surveyId);
expect(deleteManySpy).toHaveBeenCalledWith({
surveyId,
});
expect(result).toEqual(mockResult);
});
});
describe('updateById', () => {
it('should update collaborator by id', async () => {
const updateOneSpy = jest
.spyOn(repository, 'updateOne')
.mockResolvedValue({});
const collaboratorId = new ObjectId().toString();
const result = await service.updateById({
collaboratorId,
permissions: [],
});
expect(updateOneSpy).toHaveBeenCalledWith(
{
_id: new ObjectId(collaboratorId),
},
{
$set: {
permissions: [],
},
},
);
expect(result).toEqual({});
});
});
describe('getCollaboratorListByUserId', () => {
it('should return a list of collaborators by user id', async () => {
const userId = new ObjectId().toString();
const findSpy = jest.spyOn(repository, 'find').mockResolvedValue([
{
_id: '1',
surveyId: '1',
userId: userId,
permissions: [],
} as unknown as Collaborator,
]);
const result = await service.getCollaboratorListByUserId({ userId });
expect(findSpy).toHaveBeenCalledWith({
where: {
userId,
},
});
expect(result).toEqual([
{ _id: '1', surveyId: '1', userId, permissions: [] },
]);
});
});
});

View File

@ -9,7 +9,8 @@ import { ResponseSchemaService } from '../../surveyResponse/services/responseSch
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
import { Authtication } from 'src/guards/authtication';
import { Logger } from 'src/logger';
import { UserService } from 'src/modules/auth/services/user.service';
import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugin';
import { AuthService } from 'src/modules/auth/services/auth.service';
@ -18,10 +19,13 @@ jest.mock('../services/dataStatistic.service');
jest.mock('../services/surveyMeta.service');
jest.mock('../../surveyResponse/services/responseScheme.service');
jest.mock('src/guards/authentication.guard');
jest.mock('src/guards/survey.guard');
jest.mock('src/guards/workspace.guard');
describe('DataStatisticController', () => {
let controller: DataStatisticController;
let dataStatisticService: DataStatisticService;
let surveyMetaService: SurveyMetaService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@ -32,12 +36,6 @@ describe('DataStatisticController', () => {
ResponseSchemaService,
PluginManagerProvider,
ConfigService,
{
provide: Authtication,
useClass: jest.fn().mockImplementation(() => ({
canActivate: () => true,
})),
},
{
provide: UserService,
useClass: jest.fn().mockImplementation(() => ({
@ -54,13 +52,18 @@ describe('DataStatisticController', () => {
},
})),
},
{
provide: Logger,
useValue: {
error: jest.fn(),
},
},
],
}).compile();
controller = module.get<DataStatisticController>(DataStatisticController);
dataStatisticService =
module.get<DataStatisticService>(DataStatisticService);
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
const pluginManager = module.get<XiaojuSurveyPluginManager>(
XiaojuSurveyPluginManager,
);
@ -101,9 +104,6 @@ describe('DataStatisticController', () => {
],
};
jest
.spyOn(surveyMetaService, 'checkSurveyAccess')
.mockResolvedValueOnce(undefined);
jest
.spyOn(controller['responseSchemaService'], 'getResponseSchemaByPageId')
.mockResolvedValueOnce({} as any);
@ -111,7 +111,7 @@ describe('DataStatisticController', () => {
.spyOn(dataStatisticService, 'getDataTable')
.mockResolvedValueOnce(mockDataTable);
const result = await controller.data(mockRequest.query, mockRequest);
const result = await controller.data(mockRequest.query, {});
expect(result).toEqual({
code: 200,
@ -146,10 +146,6 @@ describe('DataStatisticController', () => {
{ difTime: '0.5', createDate: '2024-02-11', data123: '13800000000' },
],
};
jest
.spyOn(surveyMetaService, 'checkSurveyAccess')
.mockResolvedValueOnce(undefined);
jest
.spyOn(controller['responseSchemaService'], 'getResponseSchemaByPageId')
.mockResolvedValueOnce({} as any);
@ -157,7 +153,7 @@ describe('DataStatisticController', () => {
.spyOn(dataStatisticService, 'getDataTable')
.mockResolvedValueOnce(mockDataTable);
const result = await controller.data(mockRequest.query, mockRequest);
const result = await controller.data(mockRequest.query, {});
expect(result).toEqual({
code: 200,

View File

@ -18,7 +18,9 @@ jest.mock('../../surveyResponse/services/responseScheme.service');
jest.mock('../services/contentSecurity.service');
jest.mock('../services/surveyHistory.service');
jest.mock('src/guards/authtication');
jest.mock('src/guards/authentication.guard');
jest.mock('src/guards/survey.guard');
jest.mock('src/guards/workspace.guard');
describe('SurveyController', () => {
let controller: SurveyController;
@ -98,7 +100,7 @@ describe('SurveyController', () => {
);
const result = await controller.createSurvey(surveyInfo, {
user: { username: 'testUser' },
user: { username: 'testUser', _id: new ObjectId() },
});
expect(result).toEqual({
@ -123,9 +125,6 @@ describe('SurveyController', () => {
createMethod: 'copy',
createFrom: existsSurveyId.toString(),
};
jest
.spyOn(surveyMetaService, 'checkSurveyAccess')
.mockResolvedValue(Promise.resolve(existsSurveyMeta));
jest
.spyOn(surveyMetaService, 'createSurveyMeta')
@ -136,7 +135,10 @@ describe('SurveyController', () => {
return Promise.resolve(result);
});
const request = { user: { username: 'testUser' } }; // 模拟请求对象,根据实际情况进行调整
const request = {
user: { username: 'testUser', _id: new ObjectId() },
surveyMeta: existsSurveyMeta,
}; // 模拟请求对象,根据实际情况进行调整
const result = await controller.createSurvey(params, request);
expect(result?.data?.id).toBeDefined();
});
@ -151,9 +153,6 @@ describe('SurveyController', () => {
owner: 'testUser',
} as SurveyMeta;
jest
.spyOn(surveyMetaService, 'checkSurveyAccess')
.mockResolvedValue(Promise.resolve(surveyMeta));
jest
.spyOn(surveyConfService, 'saveSurveyConf')
.mockResolvedValue(undefined);
@ -183,6 +182,7 @@ describe('SurveyController', () => {
const result = await controller.updateConf(reqBody, {
user: { username: 'testUser', _id: 'testUserId' },
surveyMeta,
});
expect(result).toEqual({
@ -200,9 +200,6 @@ describe('SurveyController', () => {
owner: 'testUser',
} as SurveyMeta;
jest
.spyOn(surveyMetaService, 'checkSurveyAccess')
.mockResolvedValue(Promise.resolve(surveyMeta));
jest
.spyOn(surveyMetaService, 'deleteSurveyMeta')
.mockResolvedValue(undefined);
@ -210,10 +207,10 @@ describe('SurveyController', () => {
.spyOn(responseSchemaService, 'deleteResponseSchema')
.mockResolvedValue(undefined);
const result = await controller.deleteSurvey(
{ surveyId: surveyId.toString() },
{ user: { username: 'testUser' } },
);
const result = await controller.deleteSurvey({
user: { username: 'testUser' },
surveyMeta,
});
expect(result).toEqual({
code: 200,
@ -230,10 +227,6 @@ describe('SurveyController', () => {
owner: 'testUser',
} as SurveyMeta;
jest
.spyOn(surveyMetaService, 'checkSurveyAccess')
.mockResolvedValue(Promise.resolve(surveyMeta));
jest
.spyOn(surveyConfService, 'getSurveyConfBySurveyId')
.mockResolvedValue(
@ -243,7 +236,10 @@ describe('SurveyController', () => {
} as SurveyConf),
);
const request = { user: { username: 'testUser' } };
const request = {
user: { username: 'testUser', _id: new ObjectId() },
surveyMeta,
};
const result = await controller.getSurvey(
{ surveyId: surveyId.toString() },
request,
@ -262,10 +258,6 @@ describe('SurveyController', () => {
owner: 'testUser',
} as SurveyMeta;
jest
.spyOn(surveyMetaService, 'checkSurveyAccess')
.mockResolvedValue(Promise.resolve(surveyMeta));
jest
.spyOn(surveyConfService, 'getSurveyConfBySurveyId')
.mockResolvedValue(
@ -296,7 +288,7 @@ describe('SurveyController', () => {
const result = await controller.publishSurvey(
{ surveyId: surveyId.toString() },
{ user: { username: 'testUser', _id: 'testUserId' } },
{ user: { username: 'testUser', _id: 'testUserId' }, surveyMeta },
);
expect(result).toEqual({
@ -312,10 +304,6 @@ describe('SurveyController', () => {
owner: 'testUser',
} as SurveyMeta;
jest
.spyOn(surveyMetaService, 'checkSurveyAccess')
.mockResolvedValue(Promise.resolve(surveyMeta));
jest
.spyOn(surveyConfService, 'getSurveyConfBySurveyId')
.mockResolvedValue(
@ -338,7 +326,7 @@ describe('SurveyController', () => {
await expect(
controller.publishSurvey(
{ surveyId: surveyId.toString() },
{ user: { username: 'testUser', _id: 'testUserId' } },
{ user: { username: 'testUser', _id: 'testUserId' }, surveyMeta },
),
).rejects.toThrow(
new HttpException(

View File

@ -6,13 +6,16 @@ import { SurveyHistoryService } from '../services/surveyHistory.service';
import { SurveyMetaService } from '../services/surveyMeta.service';
import { UserService } from 'src/modules/auth/services/user.service';
import { Authtication } from 'src/guards/authtication';
import { AuthService } from 'src/modules/auth/services/auth.service';
import { Logger } from 'src/logger';
jest.mock('src/guards/authentication.guard');
jest.mock('src/guards/survey.guard');
jest.mock('src/guards/workspace.guard');
describe('SurveyHistoryController', () => {
let controller: SurveyHistoryController;
let surveyHistoryService: SurveyHistoryService;
let surveyMetaService: SurveyMetaService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@ -25,18 +28,6 @@ describe('SurveyHistoryController', () => {
getHistoryList: jest.fn().mockResolvedValue('mockHistoryList'),
})),
},
{
provide: SurveyMetaService,
useClass: jest.fn().mockImplementation(() => ({
checkSurveyAccess: jest.fn().mockResolvedValue({}),
})),
},
{
provide: Authtication,
useClass: jest.fn().mockImplementation(() => ({
canActivate: () => true,
})),
},
{
provide: UserService,
useClass: jest.fn().mockImplementation(() => ({
@ -53,25 +44,29 @@ describe('SurveyHistoryController', () => {
},
})),
},
{
provide: SurveyMetaService,
useClass: jest.fn().mockImplementation(() => ({})),
},
{
provide: Logger,
useValue: {
info: jest.fn(),
error: jest.fn(),
},
},
],
}).compile();
controller = module.get<SurveyHistoryController>(SurveyHistoryController);
surveyHistoryService =
module.get<SurveyHistoryService>(SurveyHistoryService);
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
});
it('should return history list when query is valid', async () => {
const req = { user: { username: 'testUser' } };
const queryInfo = { surveyId: 'survey123', historyType: 'published' };
await controller.getList(queryInfo, req);
expect(surveyMetaService.checkSurveyAccess).toHaveBeenCalledWith({
surveyId: queryInfo.surveyId,
username: req.user.username,
});
await controller.getList(queryInfo, {});
expect(surveyHistoryService.getHistoryList).toHaveBeenCalledWith({
surveyId: queryInfo.surveyId,
@ -79,6 +74,5 @@ describe('SurveyHistoryController', () => {
});
expect(surveyHistoryService.getHistoryList).toHaveBeenCalledTimes(1);
expect(surveyMetaService.checkSurveyAccess).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,11 +1,15 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SurveyMetaController } from '../controllers/surveyMeta.controller';
import { SurveyMetaService } from '../services/surveyMeta.service';
import { Authtication } from 'src/guards/authtication';
import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { LoggerProvider } from 'src/logger/logger.provider';
import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { CollaboratorService } from '../services/collaborator.service';
import { ObjectId } from 'mongodb';
jest.mock('src/guards/authentication.guard');
jest.mock('src/guards/survey.guard');
jest.mock('src/guards/workspace.guard');
describe('SurveyMetaController', () => {
let controller: SurveyMetaController;
@ -18,7 +22,6 @@ describe('SurveyMetaController', () => {
{
provide: SurveyMetaService,
useValue: {
checkSurveyAccess: jest.fn().mockResolvedValue({}),
editSurveyMeta: jest.fn().mockResolvedValue(undefined),
getSurveyMetaList: jest
.fn()
@ -26,13 +29,14 @@ describe('SurveyMetaController', () => {
},
},
LoggerProvider,
{
provide: CollaboratorService,
useValue: {
getCollaboratorListByUserId: jest.fn().mockResolvedValue([]),
},
},
],
})
.overrideGuard(Authtication)
.useValue({
canActivate: () => true,
})
.compile();
}).compile();
controller = module.get<SurveyMetaController>(SurveyMetaController);
surveyMetaService = module.get<SurveyMetaService>(SurveyMetaService);
@ -44,30 +48,21 @@ describe('SurveyMetaController', () => {
title: 'Test title',
surveyId: 'test-survey-id',
};
const req = {
user: {
username: 'test-user',
},
};
const survey = {
title: '',
remark: '',
};
jest
.spyOn(surveyMetaService, 'checkSurveyAccess')
.mockImplementation(() => {
return Promise.resolve(survey) as Promise<SurveyMeta>;
});
const req = {
user: {
username: 'test-user',
},
surveyMeta: survey,
};
const result = await controller.updateMeta(reqBody, req);
expect(surveyMetaService.checkSurveyAccess).toHaveBeenCalledWith({
surveyId: reqBody.surveyId,
username: req.user.username,
});
expect(surveyMetaService.editSurveyMeta).toHaveBeenCalledWith({
title: reqBody.title,
remark: reqBody.remark,
@ -91,7 +86,6 @@ describe('SurveyMetaController', () => {
expect(error.code).toBe(EXCEPTION_CODE.PARAMETER_ERROR);
}
expect(surveyMetaService.checkSurveyAccess).not.toHaveBeenCalled();
expect(surveyMetaService.editSurveyMeta).not.toHaveBeenCalled();
});
@ -100,13 +94,14 @@ describe('SurveyMetaController', () => {
curPage: 1,
pageSize: 10,
};
const userId = new ObjectId().toString();
const req = {
user: {
username: 'test-user',
_id: new ObjectId(userId),
},
};
try {
jest
.spyOn(surveyMetaService, 'getSurveyMetaList')
.mockImplementation(() => {
@ -115,7 +110,7 @@ describe('SurveyMetaController', () => {
count: 10,
data: [
{
id: '1',
_id: new ObjectId(),
createDate: date,
updateDate: date,
curStatus: {
@ -155,10 +150,10 @@ describe('SurveyMetaController', () => {
username: req.user.username,
filter: {},
order: {},
surveyIdList: [],
userId,
workspaceId: undefined,
});
} catch (error) {
console.log(error);
}
});
it('should get survey meta list with filter and order', async () => {
@ -177,13 +172,14 @@ describe('SurveyMetaController', () => {
]),
order: JSON.stringify([{ field: 'createDate', value: -1 }]),
};
const userId = new ObjectId().toString();
const req = {
user: {
username: 'test-user',
_id: new ObjectId(userId),
},
};
try {
const result = await controller.getList(queryInfo, req);
expect(result.code).toEqual(200);
@ -191,11 +187,11 @@ describe('SurveyMetaController', () => {
pageNum: queryInfo.curPage,
pageSize: queryInfo.pageSize,
username: req.user.username,
surveyIdList: [],
userId,
filter: { surveyType: 'normal', title: { $regex: 'hahah' } },
order: { createDate: -1 },
workspaceId: undefined,
});
} catch (error) {
console.log(error);
}
});
});

View File

@ -2,15 +2,13 @@ import { Test, TestingModule } from '@nestjs/testing';
import { SurveyMetaService } from '../services/surveyMeta.service';
import { MongoRepository } from 'typeorm';
import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { ObjectId } from 'mongodb';
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
import { NoSurveyPermissionException } from 'src/exceptions/noSurveyPermissionException';
import { RECORD_STATUS } from 'src/enums';
import { getRepositoryToken } from '@nestjs/typeorm';
import { HttpException } from 'src/exceptions/httpException';
import { SurveyUtilPlugin } from 'src/securityPlugin/surveyUtilPlugin';
import { ObjectId } from 'mongodb';
describe('SurveyMetaService', () => {
let service: SurveyMetaService;
@ -57,52 +55,6 @@ describe('SurveyMetaService', () => {
});
});
describe('checkSurveyAccess', () => {
it('should return survey when user has access', async () => {
const surveyId = new ObjectId().toHexString();
const username = 'testUser';
const survey = { owner: username } as SurveyMeta;
jest.spyOn(surveyRepository, 'findOne').mockResolvedValue(survey);
const result = await service.checkSurveyAccess({ surveyId, username });
expect(result).toBe(survey);
expect(surveyRepository.findOne).toHaveBeenCalledWith({
where: { _id: new ObjectId(surveyId) },
});
});
it('should throw SurveyNotFoundException when survey not found', async () => {
const surveyId = new ObjectId().toHexString();
const username = 'testUser';
jest.spyOn(surveyRepository, 'findOne').mockResolvedValue(null);
await expect(
service.checkSurveyAccess({ surveyId, username }),
).rejects.toThrow(SurveyNotFoundException);
expect(surveyRepository.findOne).toHaveBeenCalledWith({
where: { _id: new ObjectId(surveyId) },
});
});
it('should throw NoSurveyPermissionException when user has no access', async () => {
const surveyId = new ObjectId().toHexString();
const username = 'testUser';
const surveyOwner = 'otherUser';
const survey = { owner: surveyOwner } as SurveyMeta;
jest.spyOn(surveyRepository, 'findOne').mockResolvedValue(survey);
await expect(
service.checkSurveyAccess({ surveyId, username }),
).rejects.toThrow(NoSurveyPermissionException);
expect(surveyRepository.findOne).toHaveBeenCalledWith({
where: { _id: new ObjectId(surveyId) },
});
});
});
describe('createSurveyMeta', () => {
it('should create a new survey meta and return it', async () => {
const params = {
@ -110,6 +62,7 @@ describe('SurveyMetaService', () => {
remark: 'This is a test survey',
surveyType: 'normal',
username: 'testUser',
userId: new ObjectId().toString(),
createMethod: '',
createFrom: '',
};
@ -133,6 +86,7 @@ describe('SurveyMetaService', () => {
surveyType: params.surveyType,
surveyPath: mockedSurveyPath,
creator: params.username,
ownerId: params.userId,
owner: params.username,
createMethod: params.createMethod,
createFrom: params.createFrom,
@ -213,6 +167,7 @@ describe('SurveyMetaService', () => {
const condition = {
pageNum: 1,
pageSize: 10,
userId: 'testUserId',
username: 'testUser',
filter: {},
order: {},
@ -222,15 +177,7 @@ describe('SurveyMetaService', () => {
// 验证返回值
expect(result).toEqual({ data: mockData, count: mockCount });
// 验证repository方法被正确调用
expect(surveyRepository.findAndCount).toHaveBeenCalledWith({
where: {
owner: 'testUser',
'curStatus.status': { $ne: 'removed' },
},
skip: 0,
take: 10,
order: { createDate: -1 },
});
expect(surveyRepository.findAndCount).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,375 @@
import {
Body,
Controller,
Get,
HttpCode,
Post,
Query,
Request,
SetMetadata,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import * as Joi from 'joi';
import { Authentication } from 'src/guards/authentication.guard';
import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { SurveyGuard } from 'src/guards/survey.guard';
import {
SURVEY_PERMISSION,
SURVEY_PERMISSION_DESCRIPTION,
} from 'src/enums/surveyPermission';
import { Logger } from 'src/logger';
import { WorkspaceMemberService } from 'src/modules/workspace/services/workspaceMember.service';
import { CollaboratorService } from '../services/collaborator.service';
import { UserService } from 'src/modules/auth/services/user.service';
import { CreateCollaboratorDto } from '../dto/createCollaborator.dto';
import { ChangeUserPermissionDto } from '../dto/changeUserPermission.dto';
import { GetSurveyCollaboratorListDto } from '../dto/getSurveyCollaboratorList.dto';
import { BatchSaveCollaboratorDto } from '../dto/batchSaveCollaborator.dto';
import { splitCollaborators } from '../utils/splitCollaborator';
import { SurveyMetaService } from '../services/surveyMeta.service';
@UseGuards(Authentication)
@ApiTags('collaborator')
@ApiBearerAuth()
@Controller('/api/collaborator')
export class CollaboratorController {
constructor(
private readonly collaboratorService: CollaboratorService,
private readonly logger: Logger,
private readonly userService: UserService,
private readonly surveyMetaService: SurveyMetaService,
private readonly workspaceMemberServie: WorkspaceMemberService,
) {}
@Get('getPermissionList')
@HttpCode(200)
async getPermissionList() {
const vals = Object.values(SURVEY_PERMISSION_DESCRIPTION);
return {
code: 200,
data: vals,
};
}
@Post('')
@HttpCode(200)
@UseGuards(SurveyGuard)
@SetMetadata('surveyId', 'body.surveyId')
@SetMetadata('surveyPermission', [
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
])
async addCollaborator(
@Body() reqBody: CreateCollaboratorDto,
@Request() req,
) {
const { error, value } = CreateCollaboratorDto.validate(reqBody);
if (error) {
this.logger.error(error.message, { req });
throw new HttpException(
'系统错误,请联系管理员',
EXCEPTION_CODE.PARAMETER_ERROR,
);
}
// 检查用户是否存在
const user = await this.userService.getUserById(value.userId);
if (!user) {
throw new HttpException('用户不存在', EXCEPTION_CODE.USER_NOT_EXISTS);
}
if (user._id.toString() === req.surveyMeta.ownerId) {
throw new HttpException(
'不能给问卷所有者授权',
EXCEPTION_CODE.PARAMETER_ERROR,
);
}
const collaborator = await this.collaboratorService.getCollaborator({
userId: value.userId,
surveyId: value.surveyId,
});
if (collaborator) {
throw new HttpException(
'用户已经是协作者',
EXCEPTION_CODE.PARAMETER_ERROR,
);
}
const res = await this.collaboratorService.create(value);
return {
code: 200,
data: {
collaboratorId: res._id.toString(),
},
};
}
@Post('batchSave')
@HttpCode(200)
@UseGuards(SurveyGuard)
@SetMetadata('surveyId', 'body.surveyId')
@SetMetadata('surveyPermission', [
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
])
async batchSaveCollaborator(
@Body() reqBody: BatchSaveCollaboratorDto,
@Request() req,
) {
const { error, value } = BatchSaveCollaboratorDto.validate(reqBody);
if (error) {
this.logger.error(error.message, { req });
throw new HttpException(
'系统错误,请联系管理员',
EXCEPTION_CODE.PARAMETER_ERROR,
);
}
if (Array.isArray(value.collaborators) && value.collaborators.length > 0) {
const collaboratorUserIdList = value.collaborators.map(
(item) => item.userId,
);
for (const collaboratorUserId of collaboratorUserIdList) {
if (collaboratorUserId === req.surveyMeta.ownerId) {
throw new HttpException(
'不能给问卷所有者授权',
EXCEPTION_CODE.PARAMETER_ERROR,
);
}
}
// 不能有重复的userId
const userIdSet = new Set(collaboratorUserIdList);
if (collaboratorUserIdList.length !== Array.from(userIdSet).length) {
throw new HttpException(
'不能重复添加用户',
EXCEPTION_CODE.PARAMETER_ERROR,
);
}
const userList = await this.userService.getUserListByIds({
idList: collaboratorUserIdList,
});
const userInfoMap = userList.reduce((pre, cur) => {
const id = cur._id.toString();
pre[id] = cur;
return pre;
}, {});
for (const collaborator of value.collaborators) {
if (!userInfoMap[collaborator.userId]) {
throw new HttpException(
`用户id: {${collaborator.userId}} 不存在`,
EXCEPTION_CODE.PARAMETER_ERROR,
);
}
}
}
if (Array.isArray(value.collaborators) && value.collaborators.length > 0) {
const { newCollaborator, existsCollaborator } = splitCollaborators(
value.collaborators,
);
const collaboratorIdList = existsCollaborator.map((item) => item._id);
const newCollaboratorUserIdList = newCollaborator.map(
(item) => item.userId,
);
const delRes = await this.collaboratorService.batchDelete({
surveyId: value.surveyId,
idList: [],
neIdList: collaboratorIdList,
userIdList: newCollaboratorUserIdList,
});
this.logger.info('batchDelete:' + JSON.stringify(delRes), { req });
if (Array.isArray(newCollaborator) && newCollaborator.length > 0) {
const insertRes = await this.collaboratorService.batchCreate({
surveyId: value.surveyId,
collaboratorList: newCollaborator,
});
this.logger.info(`${JSON.stringify(insertRes)}`);
}
if (Array.isArray(existsCollaborator) && existsCollaborator.length > 0) {
const updateRes = await Promise.all(
existsCollaborator.map((item) =>
this.collaboratorService.updateById({
collaboratorId: item._id,
permissions: item.permissions,
}),
),
);
this.logger.info(`${JSON.stringify(updateRes)}`);
}
} else {
// 删除所有协作者
const delRes = await this.collaboratorService.batchDeleteBySurveyId(
value.surveyId,
);
this.logger.info(JSON.stringify(delRes), { req });
}
return {
code: 200,
};
}
@Get('')
@HttpCode(200)
@UseGuards(SurveyGuard)
@SetMetadata('surveyId', 'query.surveyId')
@SetMetadata('surveyPermission', [
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
])
async getSurveyCollaboratorList(
@Query() query: GetSurveyCollaboratorListDto,
@Request() req,
) {
const { error, value } = GetSurveyCollaboratorListDto.validate(query);
if (error) {
this.logger.error(error.message, { req });
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
}
const res = await this.collaboratorService.getSurveyCollaboratorList(value);
const userIdList = res.map((item) => item.userId);
const userList = await this.userService.getUserListByIds({
idList: userIdList,
});
const userInfoMap = userList.reduce((pre, cur) => {
const id = cur._id.toString();
pre[id] = cur;
return pre;
}, {});
return {
code: 200,
data: res.map((item) => {
return {
...item,
username: userInfoMap[item.userId]?.username || '',
};
}),
};
}
@Post('changeUserPermission')
@HttpCode(200)
@UseGuards(SurveyGuard)
@SetMetadata('surveyId', 'body.surveyId')
@SetMetadata('surveyPermission', [
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
])
async changeUserPermission(
@Body() reqBody: ChangeUserPermissionDto,
@Request() req,
) {
const { error, value } = Joi.object({
surveyId: Joi.string(),
userId: Joi.string(),
permissions: Joi.array().items(Joi.string().required()),
}).validate(reqBody);
if (error) {
this.logger.error(error.message, { req });
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
}
const res = await this.collaboratorService.changeUserPermission(value);
return {
code: 200,
data: res,
};
}
@Post('deleteCollaborator')
@HttpCode(200)
@UseGuards(SurveyGuard)
@SetMetadata('surveyId', 'body.surveyId')
@SetMetadata('surveyPermission', [
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
])
async deleteCollaborator(@Query() query, @Request() req) {
const { error, value } = Joi.object({
surveyId: Joi.string(),
userId: Joi.string(),
}).validate(query);
if (error) {
this.logger.error(error.message, { req });
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
}
const res = await this.collaboratorService.deleteCollaborator(value);
return {
code: 200,
data: res,
};
}
@HttpCode(200)
@Get('permissions')
async getUserSurveyPermissions(@Request() req, @Query() query) {
const user = req.user;
const userId = user._id.toString();
const surveyId = query.surveyId;
const surveyMeta = await this.surveyMetaService.getSurveyById({ surveyId });
if (!surveyMeta) {
throw new HttpException('问卷不存在', EXCEPTION_CODE.SURVEY_NOT_FOUND);
}
// 问卷owner有问卷的权限
if (
surveyMeta?.ownerId === userId ||
surveyMeta?.owner === req.user.username
) {
return {
code: 200,
data: {
isOwner: true,
permissions: [
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
SURVEY_PERMISSION.SURVEY_CONF_MANAGE,
],
},
};
}
// 有空间权限,默认也有所有权限
if (surveyMeta.workspaceId) {
const memberInfo = await this.workspaceMemberServie.findOne({
workspaceId: surveyMeta.workspaceId,
userId,
});
if (memberInfo) {
return {
code: 200,
data: {
isOwner: false,
permissions: [
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
SURVEY_PERMISSION.SURVEY_CONF_MANAGE,
],
},
};
}
}
const colloborator = await this.collaboratorService.getCollaborator({
surveyId,
userId,
});
return {
code: 200,
data: {
isOwner: false,
permissions: colloborator?.permissions || [],
},
};
}
}

View File

@ -4,49 +4,56 @@ import {
Query,
HttpCode,
UseGuards,
SetMetadata,
Request,
} from '@nestjs/common';
import * as Joi from 'joi';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { DataStatisticService } from '../services/dataStatistic.service';
import { SurveyMetaService } from '../services/surveyMeta.service';
import { ResponseSchemaService } from '../../surveyResponse/services/responseScheme.service';
import * as Joi from 'joi';
import { ApiTags } from '@nestjs/swagger';
import { Authtication } from 'src/guards/authtication';
import { Authentication } from 'src/guards/authentication.guard';
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
import { SurveyGuard } from 'src/guards/survey.guard';
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
import { Logger } from 'src/logger';
import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
@ApiTags('survey')
@ApiBearerAuth()
@Controller('/api/survey/dataStatistic')
export class DataStatisticController {
constructor(
private readonly surveyMetaService: SurveyMetaService,
private readonly responseSchemaService: ResponseSchemaService,
private readonly dataStatisticService: DataStatisticService,
private readonly pluginManager: XiaojuSurveyPluginManager,
private readonly logger: Logger,
) {}
@UseGuards(Authtication)
@Get('/dataTable')
@HttpCode(200)
@UseGuards(SurveyGuard)
@SetMetadata('surveyId', 'query.surveyId')
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE])
@UseGuards(Authentication)
async data(
@Query()
queryInfo,
@Request()
req,
@Request() req,
) {
const validationResult = await Joi.object({
const { value, error } = await Joi.object({
surveyId: Joi.string().required(),
isDesensitive: Joi.boolean().default(true), // 默认true就是需要脱敏
page: Joi.number().default(1),
pageSize: Joi.number().default(10),
}).validateAsync(queryInfo);
const { surveyId, isDesensitive, page, pageSize } = validationResult;
const username = req.user.username;
await this.surveyMetaService.checkSurveyAccess({
surveyId,
username,
});
}).validate(queryInfo);
if (error) {
this.logger.error(error.message, { req });
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
}
const { surveyId, isDesensitive, page, pageSize } = value;
const responseSchema =
await this.responseSchemaService.getResponseSchemaByPageId(surveyId);
const { total, listHead, listBody } =

View File

@ -7,7 +7,10 @@ import {
HttpCode,
UseGuards,
Request,
SetMetadata,
} from '@nestjs/common';
import * as Joi from 'joi';
import { ApiTags } from '@nestjs/swagger';
import { SurveyMetaService } from '../services/surveyMeta.service';
import { SurveyConfService } from '../services/surveyConf.service';
@ -16,14 +19,18 @@ import { ContentSecurityService } from '../services/contentSecurity.service';
import { SurveyHistoryService } from '../services/surveyHistory.service';
import BannerData from '../template/banner/index.json';
import { CreateSurveyDto } from '../dto/createSurvey.dto';
import * as Joi from 'joi';
import { ApiTags } from '@nestjs/swagger';
import { Authtication } from 'src/guards/authtication';
import { Authentication } from 'src/guards/authentication.guard';
import { HISTORY_TYPE } from 'src/enums';
import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { Logger } from 'src/logger';
import { SurveyGuard } from 'src/guards/survey.guard';
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
import { WorkspaceGuard } from 'src/guards/workspace.guard';
import { PERMISSION as WORKSPACE_PERMISSION } from 'src/enums/workspace';
@ApiTags('survey')
@Controller('/api/survey')
@ -46,66 +53,57 @@ export class SurveyController {
};
}
@UseGuards(Authtication)
@Post('/createSurvey')
@HttpCode(200)
@UseGuards(SurveyGuard)
@SetMetadata('surveyId', 'body.createFrom')
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
@UseGuards(WorkspaceGuard)
@SetMetadata('workspacePermissions', [WORKSPACE_PERMISSION.READ_SURVEY])
@SetMetadata('workspaceId', { key: 'body.workspaceId', optional: true })
@UseGuards(Authentication)
async createSurvey(
@Body()
reqBody,
reqBody: CreateSurveyDto,
@Request()
req,
) {
let validationResult;
try {
validationResult = await Joi.object({
title: Joi.string().required(),
remark: Joi.string().allow(null, '').default(''),
surveyType: Joi.string().when('createMethod', {
is: 'copy',
then: Joi.allow(null),
otherwise: Joi.required(),
}),
createMethod: Joi.string().allow(null).default('basic'),
createFrom: Joi.string().when('createMethod', {
is: 'copy',
then: Joi.required(),
otherwise: Joi.allow(null),
}),
}).validateAsync(reqBody);
} catch (error) {
const { error, value } = CreateSurveyDto.validate(reqBody);
if (error) {
this.logger.error(`createSurvey_parameter error: ${error.message}`, {
req,
});
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
}
const { title, remark, createMethod, createFrom } = validationResult;
const { title, remark, createMethod, createFrom } = value;
const username = req.user.username;
let surveyType = '';
let surveyType = '',
workspaceId = null;
if (createMethod === 'copy') {
const survey = await this.surveyMetaService.checkSurveyAccess({
surveyId: createFrom,
username,
});
const survey = req.surveyMeta;
surveyType = survey.surveyType;
workspaceId = survey.workspaceId;
} else {
surveyType = validationResult.surveyType;
surveyType = value.surveyType;
workspaceId = value.workspaceId;
}
const surveyMeta = await this.surveyMetaService.createSurveyMeta({
title,
remark,
surveyType,
username,
username: req.user.username,
userId: req.user._id.toString(),
createMethod,
createFrom,
workspaceId,
});
await this.surveyConfService.createSurveyConf({
surveyId: surveyMeta._id.toString(),
surveyType: surveyType,
createMethod: validationResult.createMethod,
createFrom: validationResult.createFrom,
createMethod: value.createMethod,
createFrom: value.createFrom,
});
return {
code: 200,
@ -115,26 +113,30 @@ export class SurveyController {
};
}
@UseGuards(Authtication)
@Post('/updateConf')
@HttpCode(200)
@UseGuards(SurveyGuard)
@SetMetadata('surveyId', 'body.surveyId')
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
@UseGuards(Authentication)
async updateConf(
@Body()
surveyInfo,
@Request()
req,
) {
const validationResult = await Joi.object({
const { value, error } = Joi.object({
surveyId: Joi.string().required(),
configData: Joi.any().required(),
}).validateAsync(surveyInfo);
}).validate(surveyInfo);
if (error) {
this.logger.error(error.message, { req });
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
}
const username = req.user.username;
const surveyId = validationResult.surveyId;
await this.surveyMetaService.checkSurveyAccess({
surveyId,
username,
});
const configData = validationResult.configData;
const surveyId = value.surveyId;
const configData = value.configData;
await this.surveyConfService.saveSurveyConf({
surveyId,
schema: configData,
@ -153,23 +155,18 @@ export class SurveyController {
};
}
@UseGuards(Authtication)
@HttpCode(200)
@Post('/deleteSurvey')
async deleteSurvey(@Body() reqBody, @Request() req) {
const validationResult = await Joi.object({
surveyId: Joi.string().required(),
}).validateAsync(reqBody, { allowUnknown: true });
const username = req.user.username;
const surveyId = validationResult.surveyId;
const survey = await this.surveyMetaService.checkSurveyAccess({
surveyId,
username,
});
@UseGuards(SurveyGuard)
@SetMetadata('surveyId', 'body.surveyId')
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
@UseGuards(Authentication)
async deleteSurvey(@Request() req) {
const surveyMeta = req.surveyMeta;
await this.surveyMetaService.deleteSurveyMeta(survey);
await this.surveyMetaService.deleteSurveyMeta(surveyMeta);
await this.responseSchemaService.deleteResponseSchema({
surveyPath: survey.surveyPath,
surveyPath: surveyMeta.surveyPath,
});
return {
@ -177,9 +174,16 @@ export class SurveyController {
};
}
@UseGuards(Authtication)
@Get('/getSurvey')
@HttpCode(200)
@UseGuards(SurveyGuard)
@SetMetadata('surveyId', 'query.surveyId')
@SetMetadata('surveyPermission', [
SURVEY_PERMISSION.SURVEY_CONF_MANAGE,
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
])
@UseGuards(Authentication)
async getSurvey(
@Query()
queryInfo: {
@ -188,19 +192,28 @@ export class SurveyController {
@Request()
req,
) {
const validationResult = await Joi.object({
const { value, error } = Joi.object({
surveyId: Joi.string().required(),
}).validateAsync(queryInfo);
}).validate(queryInfo);
const username = req.user.username;
const surveyId = validationResult.surveyId;
const surveyMeta = await this.surveyMetaService.checkSurveyAccess({
surveyId,
username,
});
if (error) {
this.logger.error(error.message, { req });
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
}
const surveyId = value.surveyId;
const surveyMeta = req.surveyMeta;
const surveyConf =
await this.surveyConfService.getSurveyConfBySurveyId(surveyId);
surveyMeta.currentUserId = req.user._id.toString();
if (req.collaborator) {
surveyMeta.isCollaborated = true;
surveyMeta.currentPermission = req.collaborator.permissions;
} else {
surveyMeta.isCollaborated = false;
}
return {
code: 200,
data: {
@ -210,24 +223,28 @@ export class SurveyController {
};
}
@UseGuards(Authtication)
@Post('/publishSurvey')
@HttpCode(200)
@UseGuards(SurveyGuard)
@SetMetadata('surveyId', 'body.surveyId')
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
@UseGuards(Authentication)
async publishSurvey(
@Body()
surveyInfo,
@Request()
req,
) {
const validationResult = await Joi.object({
const { value, error } = Joi.object({
surveyId: Joi.string().required(),
}).validateAsync(surveyInfo);
}).validate(surveyInfo);
if (error) {
this.logger.error(error.message, { req });
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
}
const username = req.user.username;
const surveyId = validationResult.surveyId;
const surveyMeta = await this.surveyMetaService.checkSurveyAccess({
surveyId,
username,
});
const surveyId = value.surveyId;
const surveyMeta = req.surveyMeta;
const surveyConf =
await this.surveyConfService.getSurveyConfBySurveyId(surveyId);

View File

@ -4,48 +4,59 @@ import {
Query,
HttpCode,
UseGuards,
SetMetadata,
Request,
} from '@nestjs/common';
import { SurveyHistoryService } from '../services/surveyHistory.service';
import { SurveyMetaService } from '../services/surveyMeta.service';
import * as Joi from 'joi';
import { ApiTags } from '@nestjs/swagger';
import { Authtication } from 'src/guards/authtication';
import { SurveyHistoryService } from '../services/surveyHistory.service';
import { Authentication } from 'src/guards/authentication.guard';
import { SurveyGuard } from 'src/guards/survey.guard';
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
import { Logger } from 'src/logger';
import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
@ApiTags('survey')
@Controller('/api/surveyHisotry')
export class SurveyHistoryController {
constructor(
private readonly surveyHistoryService: SurveyHistoryService,
private readonly surveyMetaService: SurveyMetaService,
private readonly logger: Logger,
) {}
@UseGuards(Authtication)
@Get('/getList')
@HttpCode(200)
@UseGuards(SurveyGuard)
@SetMetadata('surveyId', 'query.surveyId')
@SetMetadata('surveyPermission', [
SURVEY_PERMISSION.SURVEY_CONF_MANAGE,
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
])
@UseGuards(Authentication)
async getList(
@Query()
queryInfo: {
surveyId: string;
historyType: string;
},
@Request()
req,
@Request() req,
) {
const validationResult = await Joi.object({
const { value, error } = Joi.object({
surveyId: Joi.string().required(),
historyType: Joi.string().required(),
}).validateAsync(queryInfo);
}).validate(queryInfo);
const username = req.user.username;
const surveyId = validationResult.surveyId;
const historyType = validationResult.historyType;
await this.surveyMetaService.checkSurveyAccess({
surveyId,
username,
});
if (error) {
this.logger.error(error.message, { req });
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
}
const surveyId = value.surveyId;
const historyType = value.historyType;
const data = await this.surveyHistoryService.getHistoryList({
surveyId,
historyType,

View File

@ -7,18 +7,26 @@ import {
HttpCode,
UseGuards,
Request,
SetMetadata,
} from '@nestjs/common';
import * as Joi from 'joi';
import moment from 'moment';
import { ApiTags } from '@nestjs/swagger';
import { SurveyMetaService } from '../services/surveyMeta.service';
import { getFilter, getOrder } from 'src/utils/surveyUtil';
import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { Authtication } from 'src/guards/authtication';
import { Authentication } from 'src/guards/authentication.guard';
import { Logger } from 'src/logger';
import { SurveyGuard } from 'src/guards/survey.guard';
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
import { WorkspaceGuard } from 'src/guards/workspace.guard';
import { PERMISSION as WORKSPACE_PERMISSION } from 'src/enums/workspace';
import { SurveyMetaService } from '../services/surveyMeta.service';
import { GetSurveyListDto } from '../dto/getSurveyMetaList.dto';
import { CollaboratorService } from '../services/collaborator.service';
@ApiTags('survey')
@Controller('/api/survey')
@ -26,34 +34,31 @@ export class SurveyMetaController {
constructor(
private readonly surveyMetaService: SurveyMetaService,
private readonly logger: Logger,
private readonly collaboratorService: CollaboratorService,
) {}
@UseGuards(Authtication)
@Post('/updateMeta')
@HttpCode(200)
@UseGuards(SurveyGuard)
@SetMetadata('surveyId', 'body.surveyId')
@SetMetadata('surveyPermission', [SURVEY_PERMISSION.SURVEY_CONF_MANAGE])
@UseGuards(Authentication)
async updateMeta(@Body() reqBody, @Request() req) {
let validationResult;
try {
validationResult = await Joi.object({
const { value, error } = Joi.object({
title: Joi.string().required(),
remark: Joi.string().allow(null, '').default(''),
surveyId: Joi.string().required(),
}).validateAsync(reqBody, { allowUnknown: true });
} catch (error) {
}).validate(reqBody, { allowUnknown: true });
if (error) {
this.logger.error(`updateMeta_parameter error: ${error.message}`, {
req,
});
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
}
const username = req.user.username;
const surveyId = validationResult.surveyId;
const survey = await this.surveyMetaService.checkSurveyAccess({
surveyId,
username,
});
survey.title = validationResult.title;
survey.remark = validationResult.remark;
const survey = req.surveyMeta;
survey.title = value.title;
survey.remark = value.remark;
await this.surveyMetaService.editSurveyMeta(survey);
@ -62,52 +67,58 @@ export class SurveyMetaController {
};
}
@UseGuards(Authtication)
@UseGuards(WorkspaceGuard)
@SetMetadata('workspacePermissions', [WORKSPACE_PERMISSION.READ_SURVEY])
@SetMetadata('workspaceId', { optional: true, key: 'query.workspaceId' })
@UseGuards(Authentication)
@Get('/getList')
@HttpCode(200)
async getList(
@Query()
queryInfo: {
curPage: number;
pageSize: number;
},
queryInfo: GetSurveyListDto,
@Request()
req,
) {
const validationResult = await Joi.object({
curPage: Joi.number().required(),
pageSize: Joi.number().allow(null).default(10),
filter: Joi.string().allow(null),
order: Joi.string().allow(null),
}).validateAsync(queryInfo);
const { curPage, pageSize } = validationResult;
const { value, error } = GetSurveyListDto.validate(queryInfo);
if (error) {
this.logger.error(error.message, { req });
throw new HttpException('参数有误', EXCEPTION_CODE.PARAMETER_ERROR);
}
const { curPage, pageSize, workspaceId } = value;
let filter = {},
order = {};
if (validationResult.filter) {
if (value.filter) {
try {
filter = getFilter(
JSON.parse(decodeURIComponent(validationResult.filter)),
);
filter = getFilter(JSON.parse(decodeURIComponent(value.filter)));
} catch (error) {
console.log(error);
this.logger.error(error.message, { req });
}
}
if (validationResult.order) {
if (value.order) {
try {
order = order = getOrder(
JSON.parse(decodeURIComponent(validationResult.order)),
);
order = order = getOrder(JSON.parse(decodeURIComponent(value.order)));
} catch (error) {
console.log(error);
this.logger.error(error.message, { req });
}
}
const userId = req.user._id.toString();
const cooperationList =
await this.collaboratorService.getCollaboratorListByUserId({ userId });
const cooperSurveyIdMap = cooperationList.reduce((pre, cur) => {
pre[cur.surveyId] = cur;
return pre;
}, {});
const surveyIdList = cooperationList.map((item) => item.surveyId);
const username = req.user.username;
const data = await this.surveyMetaService.getSurveyMetaList({
pageNum: curPage,
pageSize: pageSize,
userId,
username,
filter,
order,
workspaceId,
surveyIdList,
});
return {
code: 200,
@ -121,6 +132,15 @@ export class SurveyMetaController {
item.createDate = moment(item.createDate).format(fmt);
item.updateDate = moment(item.updateDate).format(fmt);
item.curStatus.date = moment(item.curStatus.date).format(fmt);
const surveyId = item._id.toString();
if (cooperSurveyIdMap[surveyId]) {
item.isCollaborated = true;
item.currentPermissions = cooperSurveyIdMap[surveyId].permissions;
} else {
item.isCollaborated = false;
item.currentPermissions = [];
}
item.currentUserId = userId;
return item;
}),
},

View File

@ -0,0 +1,51 @@
import { ApiProperty } from '@nestjs/swagger';
import Joi from 'joi';
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
export class CollaboratorDto {
@ApiProperty({ description: '用户id', required: false })
userId: string;
@ApiProperty({
description: '权限',
required: true,
isArray: true,
enum: [
SURVEY_PERMISSION.SURVEY_CONF_MANAGE,
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
],
})
permissions: Array<number>;
}
export class BatchSaveCollaboratorDto {
@ApiProperty({ description: '问卷id', required: true })
surveyId: string;
@ApiProperty({ description: '协作人列表', required: true, isArray: true })
collaborators: Array<CollaboratorDto>;
static validate(data) {
return Joi.object({
surveyId: Joi.string().required(),
collaborators: Joi.array()
.allow(null)
.items(
Joi.object({
_id: Joi.string().allow(null, ''),
userId: Joi.string().required(),
permissions: Joi.array()
.required()
.items(
Joi.string().valid(
SURVEY_PERMISSION.SURVEY_CONF_MANAGE,
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
),
),
}),
),
}).validate(data, { allowUnknown: true });
}
}

View File

@ -0,0 +1,28 @@
import { ApiProperty } from '@nestjs/swagger';
import Joi from 'joi';
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
export class ChangeUserPermissionDto {
@ApiProperty({ description: '问卷id', required: true })
surveyId: string;
@ApiProperty({ description: '用户id', required: false })
userId: string;
@ApiProperty({ description: '权限', required: true })
permissions: Array<string>;
static validate(data) {
return Joi.object({
surveyId: Joi.string(),
userId: Joi.string(),
permissions: Joi.array().items(
Joi.string().valid(
SURVEY_PERMISSION.SURVEY_CONF_MANAGE,
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
),
),
}).validate(data);
}
}

View File

@ -0,0 +1,28 @@
import { ApiProperty } from '@nestjs/swagger';
import Joi from 'joi';
import { SURVEY_PERMISSION } from 'src/enums/surveyPermission';
export class CreateCollaboratorDto {
@ApiProperty({ description: '问卷id', required: true })
surveyId: string;
@ApiProperty({ description: '用户id', required: false })
userId: string;
@ApiProperty({ description: '权限', required: true, enum: SURVEY_PERMISSION })
permissions: Array<string>;
static validate(data) {
return Joi.object({
surveyId: Joi.string(),
userId: Joi.string(),
permissions: Joi.array().items(
Joi.string().valid(
SURVEY_PERMISSION.SURVEY_CONF_MANAGE,
SURVEY_PERMISSION.SURVEY_COOPERATION_MANAGE,
SURVEY_PERMISSION.SURVEY_RESPONSE_MANAGE,
),
),
}).validate(data);
}
}

View File

@ -0,0 +1,41 @@
import { ApiProperty } from '@nestjs/swagger';
import Joi from 'joi';
export class CreateSurveyDto {
@ApiProperty({ description: '问卷标题', required: true })
title: string;
@ApiProperty({ description: '问卷备注', required: false })
remark: string;
@ApiProperty({ description: '问卷类型,复制问卷必传', required: false })
surveyType: string;
@ApiProperty({ description: '创建方法', required: false })
createMethod: string;
@ApiProperty({ description: '创建来源', required: false })
createFrom: string;
@ApiProperty({ description: '问卷创建在哪个空间下', required: false })
workspaceId?: string;
static validate(data) {
return Joi.object({
title: Joi.string().required(),
remark: Joi.string().allow(null, '').default(''),
surveyType: Joi.string().when('createMethod', {
is: 'copy',
then: Joi.allow(null),
otherwise: Joi.required(),
}),
createMethod: Joi.string().allow(null).valid('copy').default('basic'),
createFrom: Joi.string().when('createMethod', {
is: 'copy',
then: Joi.required(),
otherwise: Joi.allow(null),
}),
workspaceId: Joi.string().allow(null, ''),
}).validate(data);
}
}

View File

@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import Joi from 'joi';
export class GetSurveyCollaboratorListDto {
@ApiProperty({ description: '问卷id', required: true })
surveyId: string;
static validate(data) {
return Joi.object({
surveyId: Joi.string().required(),
}).validate(data);
}
}

View File

@ -0,0 +1,29 @@
import { ApiProperty } from '@nestjs/swagger';
import Joi from 'joi';
export class GetSurveyListDto {
@ApiProperty({ description: '当前页码', required: true })
curPage: number;
@ApiProperty({ description: '分页', required: false })
pageSize: number;
@ApiProperty({ description: '过滤调教', required: false })
filter?: string;
@ApiProperty({ description: '排序条件', required: false })
order?: string;
@ApiProperty({ description: '空间id', required: false })
workspaceId?: string;
static validate(data) {
return Joi.object({
curPage: Joi.number().required(),
pageSize: Joi.number().allow(null).default(10),
filter: Joi.string().allow(null),
order: Joi.string().allow(null),
workspaceId: Joi.string().allow(null, ''),
}).validate(data);
}
}

View File

@ -0,0 +1,157 @@
import { Injectable } from '@nestjs/common';
import { Collaborator } from 'src/models/collaborator.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { MongoRepository } from 'typeorm';
import { ObjectId } from 'mongodb';
import { Logger } from 'src/logger';
@Injectable()
export class CollaboratorService {
constructor(
@InjectRepository(Collaborator)
private readonly collaboratorRepository: MongoRepository<Collaborator>,
private readonly logger: Logger,
) {}
async create({ surveyId, userId, permissions }) {
const collaborator = this.collaboratorRepository.create({
surveyId,
userId,
permissions,
});
return this.collaboratorRepository.save(collaborator);
}
async batchCreate({ surveyId, collaboratorList }) {
const res = await this.collaboratorRepository.insertMany(
collaboratorList.map((item) => {
return {
...item,
surveyId,
};
}),
);
return res;
}
async getSurveyCollaboratorList({ surveyId }) {
const list = await this.collaboratorRepository.find({
surveyId,
});
return list;
}
async getCollaboratorListByIds({ idList }) {
const list = await this.collaboratorRepository.find({
_id: {
$in: idList.map((item) => new ObjectId(item)),
},
});
return list;
}
async getCollaborator({ userId, surveyId }) {
const info = await this.collaboratorRepository.findOne({
where: {
surveyId,
userId,
},
});
return info;
}
async changeUserPermission({ userId, surveyId, permission }) {
const updateRes = await this.collaboratorRepository.updateOne(
{
surveyId,
userId,
},
{
$set: {
permission,
},
},
);
return updateRes;
}
async deleteCollaborator({ userId, surveyId }) {
const delRes = await this.collaboratorRepository.deleteOne({
userId,
surveyId,
});
return delRes;
}
async batchDelete({
idList,
neIdList,
userIdList,
surveyId,
}: {
idList?: Array<string>;
neIdList?: Array<string>;
userIdList?: Array<string>;
surveyId: string;
}) {
const query: Record<string, any> = {
surveyId,
$or: [],
};
if (Array.isArray(userIdList) && userIdList.length > 0) {
query.$or.push({
userId: {
$in: userIdList,
},
});
}
if (
(Array.isArray(idList) && idList.length > 0) ||
(Array.isArray(neIdList) && neIdList.length > 0)
) {
const idQuery: Record<string, any> = {
_id: {},
};
if (idList && idList.length > 0) {
idQuery._id.$in = idList.map((item) => new ObjectId(item));
}
if (neIdList && neIdList.length > 0) {
idQuery._id.$nin = neIdList.map((item) => new ObjectId(item));
}
query.$or.push(idQuery);
}
this.logger.info(JSON.stringify(query));
const delRes = await this.collaboratorRepository.deleteMany(query);
return delRes;
}
async batchDeleteBySurveyId(surveyId) {
const delRes = await this.collaboratorRepository.deleteMany({
surveyId,
});
return delRes;
}
updateById({ collaboratorId, permissions }) {
return this.collaboratorRepository.updateOne(
{
_id: new ObjectId(collaboratorId),
},
{
$set: {
permissions,
},
},
);
}
getCollaboratorListByUserId({ userId }) {
return this.collaboratorRepository.find({
where: {
userId,
},
});
}
}

View File

@ -4,9 +4,7 @@ import { MongoRepository, FindOptionsOrder } from 'typeorm';
import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { RECORD_STATUS } from 'src/enums';
import { ObjectId } from 'mongodb';
import { NoSurveyPermissionException } from 'src/exceptions/noSurveyPermissionException';
import { HttpException } from 'src/exceptions/httpException';
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { XiaojuSurveyPluginManager } from 'src/securityPlugin/pluginManager';
@ -34,18 +32,10 @@ export class SurveyMetaService {
return surveyPath;
}
async checkSurveyAccess({ surveyId, username }) {
const survey = await this.surveyRepository.findOne({
async getSurveyById({ surveyId }) {
return this.surveyRepository.findOne({
where: { _id: new ObjectId(surveyId) },
});
if (!survey) {
throw new SurveyNotFoundException('问卷不存在');
}
if (survey.owner !== username) {
throw new NoSurveyPermissionException('没有权限');
}
return survey;
}
async createSurveyMeta(params: {
@ -53,11 +43,21 @@ export class SurveyMetaService {
remark: string;
surveyType: string;
username: string;
userId: string;
createMethod: string;
createFrom: string;
workspaceId?: string;
}) {
const { title, remark, surveyType, username, createMethod, createFrom } =
params;
const {
title,
remark,
surveyType,
username,
createMethod,
createFrom,
userId,
workspaceId,
} = params;
const surveyPath = await this.getNewSurveyPath();
const newSurvey = this.surveyRepository.create({
title,
@ -66,8 +66,10 @@ export class SurveyMetaService {
surveyPath,
creator: username,
owner: username,
ownerId: userId,
createMethod,
createFrom,
workspaceId,
});
return await this.surveyRepository.save(newSurvey);
@ -112,22 +114,48 @@ export class SurveyMetaService {
pageNum: number;
pageSize: number;
username: string;
userId: string;
filter: Record<string, any>;
order: Record<string, any>;
workspaceId?: string;
surveyIdList?: Array<string>;
}): Promise<{ data: any[]; count: number }> {
const { pageNum, pageSize, username } = condition;
const { pageNum, pageSize, userId, username, workspaceId, surveyIdList } =
condition;
const skip = (pageNum - 1) * pageSize;
try {
const query = Object.assign(
const query: Record<string, any> = Object.assign(
{},
{
owner: username,
'curStatus.status': {
$ne: 'removed',
},
},
condition.filter,
);
if (workspaceId) {
query.workspaceId = workspaceId;
} else {
query.workspaceId = {
$exists: false,
};
// 引入空间之前新建的问卷只有owner字段引入空间之后新建的问卷多了ownerId字段使用owenrId字段进行关联更加合理此处做了兼容
query.$or = [
{
owner: username,
},
{
ownerId: userId,
},
];
if (Array.isArray(surveyIdList) && surveyIdList.length > 0) {
query.$or.push({
_id: {
$in: surveyIdList.map((item) => new ObjectId(item)),
},
});
}
}
const order =
condition.order && Object.keys(condition.order).length > 0
? (condition.order as FindOptionsOrder<SurveyMeta>)
@ -160,4 +188,14 @@ export class SurveyMetaService {
}
return this.surveyRepository.save(surveyMeta);
}
async countSurveyMetaByWorkspaceId({ workspaceId }) {
const total = await this.surveyRepository.count({
workspaceId,
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
});
return total;
}
}

View File

@ -6,18 +6,21 @@ import { LoggerProvider } from 'src/logger/logger.provider';
import { SurveyResponseModule } from '../surveyResponse/surveyResponse.module';
import { AuthModule } from '../auth/auth.module';
import { WorkspaceModule } from '../workspace/workspace.module';
import { DataStatisticController } from './controllers/dataStatistic.controller';
import { SurveyController } from './controllers/survey.controller';
import { SurveyHistoryController } from './controllers/surveyHistory.controller';
import { SurveyMetaController } from './controllers/surveyMeta.controller';
import { SurveyUIController } from './controllers/surveyUI.controller';
import { CollaboratorController } from './controllers/collaborator.controller';
import { SurveyConf } from 'src/models/surveyConf.entity';
import { SurveyHistory } from 'src/models/surveyHistory.entity';
import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { SurveyResponse } from 'src/models/surveyResponse.entity';
import { Word } from 'src/models/word.entity';
import { Collaborator } from 'src/models/collaborator.entity';
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
import { DataStatisticService } from './services/dataStatistic.service';
@ -25,6 +28,7 @@ import { SurveyConfService } from './services/surveyConf.service';
import { SurveyHistoryService } from './services/surveyHistory.service';
import { SurveyMetaService } from './services/surveyMeta.service';
import { ContentSecurityService } from './services/contentSecurity.service';
import { CollaboratorService } from './services/collaborator.service';
@Module({
imports: [
@ -34,10 +38,12 @@ import { ContentSecurityService } from './services/contentSecurity.service';
SurveyHistory,
SurveyResponse,
Word,
Collaborator,
]),
ConfigModule,
SurveyResponseModule,
AuthModule,
WorkspaceModule,
],
controllers: [
DataStatisticController,
@ -45,6 +51,7 @@ import { ContentSecurityService } from './services/contentSecurity.service';
SurveyHistoryController,
SurveyMetaController,
SurveyUIController,
CollaboratorController,
],
providers: [
DataStatisticService,
@ -53,6 +60,7 @@ import { ContentSecurityService } from './services/contentSecurity.service';
SurveyMetaService,
PluginManagerProvider,
ContentSecurityService,
CollaboratorService,
LoggerProvider,
],
})

View File

@ -0,0 +1,15 @@
export const splitCollaborators = (collaboratorList) => {
const newCollaborator = [],
existsCollaborator = [];
for (const collaborator of collaboratorList) {
if (collaborator._id) {
existsCollaborator.push(collaborator);
} else {
newCollaborator.push(collaborator);
}
}
return {
newCollaborator,
existsCollaborator,
};
};

View File

@ -20,6 +20,7 @@ import { ResponseSecurityPlugin } from 'src/securityPlugin/responseSecurityPlugi
import { RECORD_STATUS } from 'src/enums';
import { SurveyResponse } from 'src/models/surveyResponse.entity';
import { Logger } from 'src/logger';
const mockDecryptErrorBody = {
surveyPath: 'EBzdmnSp',
@ -116,6 +117,13 @@ describe('SurveyResponseController', () => {
runResponseDataPush: jest.fn(),
},
},
{
provide: Logger,
useValue: {
error: jest.fn(),
info: jest.fn(),
},
},
],
}).compile();
@ -197,7 +205,7 @@ describe('SurveyResponseController', () => {
.spyOn(clientEncryptService, 'deleteEncryptInfo')
.mockResolvedValueOnce(undefined);
const result = await controller.createResponse(reqBody);
const result = await controller.createResponse(reqBody, {});
expect(result).toEqual({ code: 200, msg: '提交成功' });
expect(
@ -244,7 +252,7 @@ describe('SurveyResponseController', () => {
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValueOnce(null);
await expect(controller.createResponse(reqBody)).rejects.toThrow(
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
SurveyNotFoundException,
);
});
@ -253,7 +261,7 @@ describe('SurveyResponseController', () => {
const reqBody = cloneDeep(mockSubmitData);
delete reqBody.sign;
await expect(controller.createResponse(reqBody)).rejects.toThrow(
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
HttpException,
);
@ -266,7 +274,7 @@ describe('SurveyResponseController', () => {
const reqBody = cloneDeep(mockDecryptErrorBody);
reqBody.sign = 'mock sign';
await expect(controller.createResponse(reqBody)).rejects.toThrow(
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
HttpException,
);
@ -282,7 +290,7 @@ describe('SurveyResponseController', () => {
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValueOnce(mockResponseSchema);
await expect(controller.createResponse(reqBody)).rejects.toThrow(
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
HttpException,
);
});
@ -294,7 +302,7 @@ describe('SurveyResponseController', () => {
.spyOn(responseSchemaService, 'getResponseSchemaByPath')
.mockResolvedValueOnce(mockResponseSchema);
await expect(controller.createResponse(reqBody)).rejects.toThrow(
await expect(controller.createResponse(reqBody, {})).rejects.toThrow(
HttpException,
);
});

View File

@ -1,4 +1,4 @@
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
import { Controller, Post, Body, HttpCode, Request } from '@nestjs/common';
import { HttpException } from 'src/exceptions/httpException';
import { SurveyNotFoundException } from 'src/exceptions/surveyNotFoundException';
import { checkSign } from 'src/utils/checkSign';
@ -16,6 +16,7 @@ import moment from 'moment';
import * as Joi from 'joi';
import * as forge from 'node-forge';
import { ApiTags } from '@nestjs/swagger';
import { Logger } from 'src/logger';
@ApiTags('surveyResponse')
@Controller('/api/surveyResponse')
@ -26,25 +27,33 @@ export class SurveyResponseController {
private readonly surveyResponseService: SurveyResponseService,
private readonly clientEncryptService: ClientEncryptService,
private readonly messagePushingTaskService: MessagePushingTaskService,
private readonly logger: Logger,
) {}
@Post('/createResponse')
@HttpCode(200)
async createResponse(@Body() reqBody) {
async createResponse(@Body() reqBody, @Request() req) {
// 检查签名
checkSign(reqBody);
// 校验参数
const validationResult = await Joi.object({
const { value, error } = Joi.object({
surveyPath: Joi.string().required(),
data: Joi.any().required(),
encryptType: Joi.string(),
sessionId: Joi.string(),
clientTime: Joi.number().required(),
difTime: Joi.number(),
}).validateAsync(reqBody, { allowUnknown: true });
}).validate(reqBody, { allowUnknown: true });
if (error) {
this.logger.error(`updateMeta_parameter error: ${error.message}`, {
req,
});
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
}
const { surveyPath, encryptType, data, sessionId, clientTime, difTime } =
validationResult;
value;
// 查询schema
const responseSchema =
@ -153,8 +162,8 @@ export class SurveyResponseController {
// 对用户提交的数据进行遍历处理
for (const field in decryptedData) {
const value = decryptedData[field];
const values = Array.isArray(value) ? value : [value];
const val = decryptedData[field];
const vals = Array.isArray(val) ? val : [val];
if (field in optionTextAndId) {
// 记录选项的提交数量,用于投票题回显、或者拓展上限限制功能
const optionCountData: Record<string, any> =
@ -164,7 +173,7 @@ export class SurveyResponseController {
type: 'option',
})) || { total: 0 };
optionCountData.total++;
for (const val of values) {
for (const val of vals) {
if (!optionCountData[val]) {
optionCountData[val] = 1;
} else {
@ -183,7 +192,7 @@ export class SurveyResponseController {
// 入库
const surveyResponse =
await this.surveyResponseService.createSurveyResponse({
surveyPath: validationResult.surveyPath,
surveyPath: value.surveyPath,
data: decryptedData,
clientTime,
difTime,

View File

@ -1,4 +1,6 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { MessageModule } from '../message/message.module';
@ -11,6 +13,7 @@ import { ResponseSchema } from 'src/models/responseSchema.entity';
import { Counter } from 'src/models/counter.entity';
import { SurveyResponse } from 'src/models/surveyResponse.entity';
import { ClientEncrypt } from 'src/models/clientEncrypt.entity';
import { Logger } from 'src/logger';
import { ClientEncryptController } from './controllers/clientEncrpt.controller';
import { CounterController } from './controllers/counter.controller';
@ -18,9 +21,6 @@ import { ResponseSchemaController } from './controllers/responseSchema.controlle
import { SurveyResponseController } from './controllers/surveyResponse.controller';
import { SurveyResponseUIController } from './controllers/surveyResponseUI.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
TypeOrmModule.forFeature([
@ -44,6 +44,7 @@ import { ConfigModule } from '@nestjs/config';
SurveyResponseService,
CounterService,
ClientEncryptService,
Logger,
],
exports: [
ResponseSchemaService,

View File

@ -0,0 +1,50 @@
import { splitMembers, Member } from '../utils/splitMember';
import { ROLE as WORKSPACE_ROLE } from 'src/enums/workspace';
describe('splitMembers', () => {
it('should split members into newMembers, adminMembers, and userMembers', () => {
const members = [
{ userId: 'user1', role: WORKSPACE_ROLE.ADMIN, _id: '1' },
{ userId: 'user2', role: WORKSPACE_ROLE.USER, _id: '2' },
{ userId: 'user3', role: WORKSPACE_ROLE.ADMIN, _id: '3' },
{ userId: 'user4', role: WORKSPACE_ROLE.USER, _id: '4' },
{ userId: 'user5', role: WORKSPACE_ROLE.USER },
];
const result = splitMembers(members);
expect(result).toEqual({
newMembers: [{ userId: 'user5', role: WORKSPACE_ROLE.USER }],
adminMembers: ['1', '3'],
userMembers: ['2', '4'],
});
});
it('should handle an empty members array', () => {
const members: Array<Member> = [];
const result = splitMembers(members);
expect(result).toEqual({
newMembers: [],
adminMembers: [],
userMembers: [],
});
});
it('should handle members with no role', () => {
const members = [
{ userId: 'user1', role: WORKSPACE_ROLE.ADMIN, _id: '1' },
{ userId: 'user2', role: '', _id: '2' },
{ userId: 'user3', role: '', _id: '3' },
];
const result = splitMembers(members);
expect(result).toEqual({
newMembers: [],
adminMembers: ['1'],
userMembers: ['2', '3'],
});
});
});

View File

@ -0,0 +1,229 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ObjectId } from 'mongodb';
import { WorkspaceController } from '../controllers/workspace.controller';
import { WorkspaceService } from '../services/workspace.service';
import { WorkspaceMemberService } from '../services/workspaceMember.service';
import { CreateWorkspaceDto } from '../dto/createWorkspace.dto';
import { HttpException } from 'src/exceptions/httpException';
import { ROLE as WORKSPACE_ROLE } from 'src/enums/workspace';
import { Workspace } from 'src/models/workspace.entity';
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
import { UserService } from 'src/modules/auth/services/user.service';
import { SurveyMetaService } from 'src/modules/survey/services/surveyMeta.service';
import { Logger } from 'src/logger';
import { User } from 'src/models/user.entity';
jest.mock('src/guards/authentication.guard');
jest.mock('src/guards/survey.guard');
jest.mock('src/guards/workspace.guard');
describe('WorkspaceController', () => {
let controller: WorkspaceController;
let workspaceService: WorkspaceService;
let workspaceMemberService: WorkspaceMemberService;
let userService: UserService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [WorkspaceController],
providers: [
{
provide: WorkspaceService,
useValue: {
create: jest.fn(),
findAllById: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
},
{
provide: WorkspaceMemberService,
useValue: {
create: jest.fn(),
batchCreate: jest.fn(),
findAllByUserId: jest.fn(),
batchUpdate: jest.fn(),
batchDelete: jest.fn(),
countByWorkspaceId: jest.fn(),
},
},
{
provide: UserService,
useValue: {
getUserListByIds: jest.fn(),
},
},
{
provide: SurveyMetaService,
useValue: {
countSurveyMetaByWorkspaceId: jest.fn().mockImplementation(() => {
return Math.floor(Math.random() * 10);
}),
},
},
{
provide: Logger,
useValue: {
info: jest.fn(),
error: jest.fn(),
},
},
],
}).compile();
controller = module.get<WorkspaceController>(WorkspaceController);
workspaceService = module.get<WorkspaceService>(WorkspaceService);
workspaceMemberService = module.get<WorkspaceMemberService>(
WorkspaceMemberService,
);
userService = module.get<UserService>(UserService);
});
describe('create', () => {
it('should create a workspace and return workspaceId', async () => {
const createWorkspaceDto: CreateWorkspaceDto = {
name: 'Test Workspace',
description: 'Test Description',
members: [{ userId: 'userId1', role: WORKSPACE_ROLE.USER }],
};
const req = { user: { _id: new ObjectId() } };
const createdWorkspace = { _id: new ObjectId() };
jest.spyOn(userService, 'getUserListByIds').mockResolvedValue([
{
_id: 'userId1',
},
] as unknown as Array<User>);
jest
.spyOn(workspaceService, 'create')
.mockResolvedValue(createdWorkspace as Workspace);
jest.spyOn(workspaceMemberService, 'create').mockResolvedValue(null);
jest.spyOn(workspaceMemberService, 'batchCreate').mockResolvedValue(null);
const result = await controller.create(createWorkspaceDto, req);
expect(result).toEqual({
code: 200,
data: { workspaceId: createdWorkspace._id.toString() },
});
expect(workspaceService.create).toHaveBeenCalledWith({
name: createWorkspaceDto.name,
description: createWorkspaceDto.description,
ownerId: req.user._id.toString(),
});
expect(workspaceMemberService.create).toHaveBeenCalledWith({
userId: req.user._id.toString(),
workspaceId: createdWorkspace._id.toString(),
role: WORKSPACE_ROLE.ADMIN,
});
expect(workspaceMemberService.batchCreate).toHaveBeenCalledWith({
workspaceId: createdWorkspace._id.toString(),
members: createWorkspaceDto.members,
});
});
it('should throw an exception if validation fails', async () => {
const createWorkspaceDto: CreateWorkspaceDto = {
name: '',
members: [],
};
const req = { user: { _id: new ObjectId() } };
await expect(controller.create(createWorkspaceDto, req)).rejects.toThrow(
HttpException,
);
});
});
describe('findAll', () => {
it('should return a list of workspaces for the user', async () => {
const req = { user: { _id: new ObjectId() } };
const memberList = [{ workspaceId: new ObjectId().toString() }];
const workspaces = [{ _id: new ObjectId(), name: 'Test Workspace' }];
jest
.spyOn(workspaceMemberService, 'findAllByUserId')
.mockResolvedValue(memberList as unknown as Array<WorkspaceMember>);
jest
.spyOn(workspaceService, 'findAllById')
.mockResolvedValue(workspaces as Array<Workspace>);
jest.spyOn(userService, 'getUserListByIds').mockResolvedValue([]);
const result = await controller.findAll(req);
expect(result.code).toEqual(200);
expect(workspaceMemberService.findAllByUserId).toHaveBeenCalledWith({
userId: req.user._id.toString(),
});
expect(workspaceService.findAllById).toHaveBeenCalledWith({
workspaceIdList: memberList.map((item) => item.workspaceId),
});
});
});
describe('update', () => {
it('should update a workspace and its members', async () => {
const id = new ObjectId().toString();
const userId = new ObjectId();
const members = {
newMembers: [{ userId: userId.toString(), role: WORKSPACE_ROLE.ADMIN }],
adminMembers: [],
userMembers: [],
};
jest.spyOn(userService, 'getUserListByIds').mockResolvedValue([
{
_id: userId,
},
] as Array<User>);
const updateDto = {
name: 'Updated Workspace',
members: [
...members.newMembers,
...members.adminMembers,
...members.userMembers,
],
};
const updateResult = { affected: 1, raw: '', generatedMaps: [] };
jest.spyOn(workspaceService, 'update').mockResolvedValue(updateResult);
jest.spyOn(workspaceMemberService, 'batchCreate').mockResolvedValue(null);
jest.spyOn(workspaceMemberService, 'batchUpdate').mockResolvedValue(null);
const result = await controller.update(id, updateDto);
expect(result).toEqual({
code: 200,
});
expect(workspaceService.update).toHaveBeenCalledWith(id, {
name: updateDto.name,
});
expect(workspaceMemberService.batchCreate).toHaveBeenCalledWith({
workspaceId: id,
members: members.newMembers,
});
expect(workspaceMemberService.batchUpdate).toHaveBeenCalledWith({
idList: members.adminMembers,
role: WORKSPACE_ROLE.ADMIN,
});
expect(workspaceMemberService.batchUpdate).toHaveBeenCalledWith({
idList: members.userMembers,
role: WORKSPACE_ROLE.USER,
});
});
});
describe('delete', () => {
it('should delete a workspace', async () => {
const id = 'workspaceId';
jest.spyOn(workspaceService, 'delete').mockResolvedValue(null);
const result = await controller.delete(id);
expect(result).toEqual({ code: 200 });
expect(workspaceService.delete).toHaveBeenCalledWith(id);
});
});
});

View File

@ -0,0 +1,126 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { MongoRepository } from 'typeorm';
import { ObjectId } from 'mongodb';
import { WorkspaceService } from '../services/workspace.service';
import { Workspace } from 'src/models/workspace.entity';
import { SurveyMeta } from 'src/models/surveyMeta.entity';
jest.mock('src/guards/authentication.guard');
jest.mock('src/guards/survey.guard');
jest.mock('src/guards/workspace.guard');
describe('WorkspaceService', () => {
let service: WorkspaceService;
let workspaceRepository: MongoRepository<Workspace>;
let surveyMetaRepository: MongoRepository<SurveyMeta>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
WorkspaceService,
{
provide: getRepositoryToken(Workspace),
useClass: MongoRepository,
},
{
provide: getRepositoryToken(SurveyMeta),
useClass: MongoRepository,
},
],
}).compile();
service = module.get<WorkspaceService>(WorkspaceService);
workspaceRepository = module.get<MongoRepository<Workspace>>(
getRepositoryToken(Workspace),
);
surveyMetaRepository = module.get<MongoRepository<SurveyMeta>>(
getRepositoryToken(SurveyMeta),
);
});
describe('create', () => {
it('should create a new workspace', async () => {
const workspace = {
name: 'Test Workspace',
description: 'Description',
ownerId: 'ownerId',
};
const createdWorkspace = { ...workspace, _id: new ObjectId() };
jest
.spyOn(workspaceRepository, 'create')
.mockReturnValue(createdWorkspace as any);
jest
.spyOn(workspaceRepository, 'save')
.mockResolvedValue(createdWorkspace as any);
const result = await service.create(workspace);
expect(result).toEqual(createdWorkspace);
expect(workspaceRepository.create).toHaveBeenCalledWith(workspace);
expect(workspaceRepository.save).toHaveBeenCalledWith(createdWorkspace);
});
});
describe('findAllById', () => {
it('should return a list of workspaces', 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.findAllById({ workspaceIdList });
expect(result).toEqual(workspaces);
expect(workspaceRepository.find).toHaveBeenCalledTimes(1);
});
});
describe('update', () => {
it('should update a workspace', async () => {
const workspaceId = 'workspaceId';
const updateData = { name: 'Updated Workspace' };
jest
.spyOn(workspaceRepository, 'update')
.mockResolvedValue({ affected: 1 } as any);
const result = await service.update(workspaceId, updateData);
expect(result).toEqual({ affected: 1 });
expect(workspaceRepository.update).toHaveBeenCalledWith(
workspaceId,
updateData,
);
});
});
describe('delete', () => {
it('should delete a workspace and update related surveyMeta', async () => {
const workspaceId = new ObjectId().toString();
jest
.spyOn(workspaceRepository, 'updateOne')
.mockResolvedValue({ modifiedCount: 1 } as any);
jest
.spyOn(surveyMetaRepository, 'updateMany')
.mockResolvedValue({ modifiedCount: 1 } as any);
await service.delete(workspaceId);
expect(workspaceRepository.updateOne).toHaveBeenCalledTimes(1);
expect(surveyMetaRepository.updateMany).toHaveBeenCalledTimes(1);
});
});
});

View File

@ -0,0 +1,183 @@
import { Test, TestingModule } from '@nestjs/testing';
import { WorkspaceMemberController } from '../controllers/workspaceMember.controller';
import { WorkspaceMemberService } from '../services/workspaceMember.service';
import { CreateWorkspaceMemberDto } from '../dto/createWorkspaceMember.dto';
import { UpdateWorkspaceMemberDto } from '../dto/updateWorkspaceMember.dto';
import { DeleteWorkspaceMemberDto } from '../dto/deleteWorkspaceMember.dto';
import { HttpException } from 'src/exceptions/httpException';
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
import { ROLE as WORKSPACE_ROLE } from 'src/enums/workspace';
jest.mock('src/guards/authentication.guard');
jest.mock('src/guards/survey.guard');
jest.mock('src/guards/workspace.guard');
describe('WorkspaceMemberController', () => {
let controller: WorkspaceMemberController;
let workspaceMemberService: WorkspaceMemberService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [WorkspaceMemberController],
providers: [
{
provide: WorkspaceMemberService,
useValue: {
create: jest.fn(),
findAllByWorkspaceId: jest.fn(),
updateRole: jest.fn(),
deleteMember: jest.fn(),
},
},
],
}).compile();
controller = module.get<WorkspaceMemberController>(
WorkspaceMemberController,
);
workspaceMemberService = module.get<WorkspaceMemberService>(
WorkspaceMemberService,
);
});
describe('create', () => {
it('should create a workspace member and return memberId', async () => {
const createDto: CreateWorkspaceMemberDto = {
workspaceId: 'workspaceId',
userId: 'userId',
role: WORKSPACE_ROLE.ADMIN,
};
const createdMember = { _id: 'memberId' };
jest
.spyOn(workspaceMemberService, 'create')
.mockResolvedValue(createdMember as unknown as WorkspaceMember);
const result = await controller.create(createDto);
expect(result).toEqual({
code: 200,
data: {
memberId: createdMember._id,
},
});
expect(workspaceMemberService.create).toHaveBeenCalledWith({
userId: createDto.userId,
workspaceId: createDto.workspaceId,
role: createDto.role,
});
});
it('should throw an exception if validation fails', async () => {
const createDto: CreateWorkspaceMemberDto = {
workspaceId: '',
userId: '',
role: '',
};
await expect(controller.create(createDto)).rejects.toThrow(HttpException);
});
});
describe('findAll', () => {
it('should return a list of workspace members', async () => {
const req = { query: { workspaceId: 'workspaceId' } };
const members = [{ userId: 'userId1', role: 'USER' }];
jest
.spyOn(workspaceMemberService, 'findAllByWorkspaceId')
.mockResolvedValue(members as unknown as Array<WorkspaceMember>);
const result = await controller.findAll(req);
expect(result).toEqual({
code: 200,
data: members,
});
expect(workspaceMemberService.findAllByWorkspaceId).toHaveBeenCalledWith({
workspaceId: 'workspaceId',
});
});
it('should throw an exception if workspaceId is not provided', async () => {
const req = { query: {} };
await expect(controller.findAll(req)).rejects.toThrow(HttpException);
});
});
describe('updateRole', () => {
it('should update the role of a workspace member and return modifiedCount', async () => {
const updateDto: UpdateWorkspaceMemberDto = {
workspaceId: 'workspaceId',
userId: 'userId',
role: WORKSPACE_ROLE.ADMIN,
};
const updateResult = { modifiedCount: 1 };
jest
.spyOn(workspaceMemberService, 'updateRole')
.mockResolvedValue(updateResult);
const result = await controller.updateRole(updateDto);
expect(result).toEqual({
code: 200,
data: {
modifiedCount: updateResult.modifiedCount,
},
});
expect(workspaceMemberService.updateRole).toHaveBeenCalledWith({
workspaceId: updateDto.workspaceId,
userId: updateDto.userId,
role: updateDto.role,
});
});
it('should throw an exception if validation fails', async () => {
const updateDto: UpdateWorkspaceMemberDto = {
workspaceId: '',
userId: '',
role: '',
};
await expect(controller.updateRole(updateDto)).rejects.toThrow(
HttpException,
);
});
});
describe('delete', () => {
it('should delete a workspace member and return deletedCount', async () => {
const deleteDto: DeleteWorkspaceMemberDto = {
workspaceId: 'workspaceId',
userId: 'userId',
};
const deleteResult = { acknowledged: true, deletedCount: 1 };
jest
.spyOn(workspaceMemberService, 'deleteMember')
.mockResolvedValue(deleteResult);
const result = await controller.delete(deleteDto);
expect(result).toEqual({
code: 200,
data: {
deletedCount: deleteResult.deletedCount,
},
});
expect(workspaceMemberService.deleteMember).toHaveBeenCalledWith(
deleteDto,
);
});
it('should throw an exception if validation fails', async () => {
const deleteDto: DeleteWorkspaceMemberDto = {
userId: '',
workspaceId: '',
};
await expect(controller.delete(deleteDto)).rejects.toThrow(HttpException);
});
});
});

View File

@ -0,0 +1,196 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { MongoRepository } from 'typeorm';
import { ObjectId } from 'mongodb';
import { WorkspaceMemberService } from '../services/workspaceMember.service';
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
jest.mock('src/guards/authentication.guard');
jest.mock('src/guards/survey.guard');
jest.mock('src/guards/workspace.guard');
describe('WorkspaceMemberService', () => {
let service: WorkspaceMemberService;
let repository: MongoRepository<WorkspaceMember>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
WorkspaceMemberService,
{
provide: getRepositoryToken(WorkspaceMember),
useClass: MongoRepository,
},
],
}).compile();
service = module.get<WorkspaceMemberService>(WorkspaceMemberService);
repository = module.get<MongoRepository<WorkspaceMember>>(
getRepositoryToken(WorkspaceMember),
);
});
describe('create', () => {
it('should create a new workspace member', async () => {
const member = {
role: 'admin',
userId: 'userId',
workspaceId: 'workspaceId',
};
const createdMember = { ...member, _id: new ObjectId() };
jest.spyOn(repository, 'create').mockReturnValue(createdMember as any);
jest.spyOn(repository, 'save').mockResolvedValue(createdMember as any);
const result = await service.create(member);
expect(result).toEqual(createdMember);
expect(repository.create).toHaveBeenCalledWith(member);
expect(repository.save).toHaveBeenCalledWith(createdMember);
});
});
describe('batchCreate', () => {
it('should batch create workspace members', async () => {
const workspaceId = 'workspaceId';
const members = [
{ userId: 'userId1', role: 'admin' },
{ userId: 'userId2', role: 'user' },
];
const dataToInsert = members.map((item) => ({ ...item, workspaceId }));
jest
.spyOn(repository, 'insertMany')
.mockResolvedValueOnce({ insertedCount: members.length } as any);
const result = await service.batchCreate({ workspaceId, members });
expect(result).toEqual({ insertedCount: members.length });
expect(repository.insertMany).toHaveBeenCalledWith(dataToInsert);
});
it('should return insertedCount 0 if no members to insert', async () => {
const workspaceId = new ObjectId().toString();
const members = [];
const result = await service.batchCreate({ workspaceId, members });
expect(result).toEqual({ insertedCount: 0 });
});
});
describe('batchUpdate', () => {
it('should batch update workspace members roles', async () => {
const idList = [new ObjectId().toString(), new ObjectId().toString()];
const role = 'user';
jest
.spyOn(repository, 'updateMany')
.mockResolvedValue({ modifiedCount: idList.length } as any);
const result = await service.batchUpdate({ idList, role });
expect(result).toEqual({ modifiedCount: idList.length });
});
it('should return modifiedCount 0 if no ids to update', async () => {
const idList = [];
const role = 'user';
const result = await service.batchUpdate({ idList, role });
expect(result).toEqual({ modifiedCount: 0 });
});
});
describe('findAllByUserId', () => {
it('should return all workspace members by userId', async () => {
const userId = 'userId';
const members = [
{ userId, workspaceId: 'workspaceId1' },
{ userId, workspaceId: 'workspaceId2' },
];
jest.spyOn(repository, 'find').mockResolvedValue(members as any);
const result = await service.findAllByUserId({ userId });
expect(result).toEqual(members);
expect(repository.find).toHaveBeenCalledWith({ where: { userId } });
});
});
describe('findAllByWorkspaceId', () => {
it('should return all workspace members by workspaceId', async () => {
const workspaceId = 'workspaceId';
const members = [
{ userId: 'userId1', workspaceId },
{ userId: 'userId2', workspaceId },
];
jest.spyOn(repository, 'find').mockResolvedValue(members as any);
const result = await service.findAllByWorkspaceId({ workspaceId });
expect(result).toEqual(members);
expect(repository.find).toHaveBeenCalledTimes(1);
});
});
describe('findOne', () => {
it('should return a single workspace member', async () => {
const workspaceId = 'workspaceId';
const userId = 'userId';
const member = { userId, workspaceId };
jest.spyOn(repository, 'findOne').mockResolvedValue(member as any);
const result = await service.findOne({ workspaceId, userId });
expect(result).toEqual(member);
expect(repository.findOne).toHaveBeenCalledWith({
where: { workspaceId, userId },
});
});
});
describe('updateRole', () => {
it('should update the role of a workspace member', async () => {
const workspaceId = 'workspaceId';
const userId = 'userId';
const role = 'admin';
jest
.spyOn(repository, 'updateOne')
.mockResolvedValue({ modifiedCount: 1 } as any);
const result = await service.updateRole({ workspaceId, userId, role });
expect(result).toEqual({ modifiedCount: 1 });
expect(repository.updateOne).toHaveBeenCalledWith(
{ workspaceId, userId },
{ $set: { role } },
);
});
});
describe('deleteMember', () => {
it('should delete a workspace member', async () => {
const workspaceId = 'workspaceId';
const userId = 'userId';
jest
.spyOn(repository, 'deleteOne')
.mockResolvedValue({ deletedCount: 1 } as any);
const result = await service.deleteMember({ workspaceId, userId });
expect(result).toEqual({ deletedCount: 1 });
expect(repository.deleteOne).toHaveBeenCalledWith({
workspaceId,
userId,
});
});
});
});

View File

@ -0,0 +1,329 @@
import {
Controller,
Get,
Post,
Delete,
Body,
Param,
UseGuards,
Request,
SetMetadata,
HttpCode,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import moment from 'moment';
import { Authentication } from 'src/guards/authentication.guard';
import { WorkspaceGuard } from 'src/guards/workspace.guard';
import { WorkspaceService } from '../services/workspace.service';
import { WorkspaceMemberService } from '../services/workspaceMember.service';
import { CreateWorkspaceDto } from '../dto/createWorkspace.dto';
import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import {
ROLE as WORKSPACE_ROLE,
PERMISSION as WORKSPACE_PERMISSION,
ROLE_PERMISSION as WORKSPACE_ROLE_PERMISSION,
} from 'src/enums/workspace';
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';
@ApiTags('workspace')
@ApiBearerAuth()
@UseGuards(Authentication)
@Controller('/api/workspace')
export class WorkspaceController {
constructor(
private readonly workspaceService: WorkspaceService,
private readonly workspaceMemberService: WorkspaceMemberService,
private readonly userService: UserService,
private readonly surveyMetaService: SurveyMetaService,
private readonly logger: Logger,
) {}
@Get('getRoleList')
@HttpCode(200)
async getRoleList() {
const rolePermissions = Object.values(WORKSPACE_ROLE_PERMISSION);
return {
code: 200,
data: rolePermissions,
};
}
@Post()
@HttpCode(200)
async create(@Body() workspace: CreateWorkspaceDto, @Request() req) {
const { value, error } = CreateWorkspaceDto.validate(workspace);
if (error) {
this.logger.error(
`CreateWorkspaceDto validate failed: ${error.message}`,
{ req },
);
throw new HttpException(
`参数错误: 请联系管理员`,
EXCEPTION_CODE.PARAMETER_ERROR,
);
}
if (Array.isArray(value.members) && value.members.length > 0) {
// 校验用户是否真实存在
const userIdList = value.members.map((item) => item.userId);
// 不能有重复的userId
const userIdSet = new Set(userIdList);
if (userIdList.length !== Array.from(userIdSet).length) {
throw new HttpException(
'不能重复添加用户',
EXCEPTION_CODE.PARAMETER_ERROR,
);
}
const userList = await this.userService.getUserListByIds({
idList: userIdList,
});
const userInfoMap = userList.reduce((pre, cur) => {
const id = cur._id.toString();
pre[id] = cur;
return pre;
}, {});
for (const member of value.members) {
if (!userInfoMap[member.userId]) {
throw new HttpException(
`用户id: {${member.userId}} 不存在`,
EXCEPTION_CODE.PARAMETER_ERROR,
);
}
}
}
const userId = req.user._id.toString();
// 插入空间表
const retWorkspace = await this.workspaceService.create({
name: value.name,
description: value.description,
ownerId: userId,
});
const workspaceId = retWorkspace._id.toString();
// 空间的成员表要新增一条管理员数据
await this.workspaceMemberService.create({
userId,
workspaceId,
role: WORKSPACE_ROLE.ADMIN,
});
if (Array.isArray(value.members) && value.members.length > 0) {
await this.workspaceMemberService.batchCreate({
workspaceId,
members: value.members,
});
}
return {
code: 200,
data: {
workspaceId,
},
};
}
@Get()
@HttpCode(200)
async findAll(@Request() req) {
const userId = req.user._id.toString();
// 查询当前用户参与的空间
const workspaceInfoList = await this.workspaceMemberService.findAllByUserId(
{ userId },
);
const workspaceIdList = workspaceInfoList.map((item) => item.workspaceId);
const workspaceInfoMap = workspaceInfoList.reduce((pre, cur) => {
pre[cur.workspaceId] = cur;
return pre;
}, {});
// 查询当前用户的空间列表
const list = await this.workspaceService.findAllById({ workspaceIdList });
const ownerIdList = list.map((item) => item.ownerId);
const userList = await this.userService.getUserListByIds({
idList: ownerIdList,
});
const userInfoMap = userList.reduce((pre, cur) => {
const id = cur._id.toString();
pre[id] = cur;
return pre;
}, {});
const surveyTotalList = await Promise.all(
workspaceIdList.map((item) => {
return this.surveyMetaService.countSurveyMetaByWorkspaceId({
workspaceId: item,
});
}),
);
const surveyTotalMap = workspaceIdList.reduce((pre, cur, index) => {
const total = surveyTotalList[index];
pre[cur] = total;
return pre;
}, {});
const memberTotalList = await Promise.all(
workspaceIdList.map((item) => {
return this.workspaceMemberService.countByWorkspaceId({
workspaceId: item,
});
}),
);
const memberTotalMap = workspaceIdList.reduce((pre, cur, index) => {
const total = memberTotalList[index];
pre[cur] = total;
return pre;
}, {});
return {
code: 200,
data: {
list: list.map((item) => {
const workspaceId = item._id.toString();
const curWorkspaceInfo = workspaceInfoMap?.[workspaceId] || {};
const ownerInfo = userInfoMap?.[item.ownerId] || {};
return {
...item,
createDate: moment(item.createDate).format('YYYY-MM-DD HH:mm:ss'),
owner: ownerInfo.username,
currentUserId: curWorkspaceInfo.userId,
currentUserRole: curWorkspaceInfo.role,
surveyTotal: surveyTotalMap[workspaceId] || 0,
memberTotal: memberTotalMap[workspaceId] || 0,
};
}),
},
};
}
@Get(':id')
@HttpCode(200)
@UseGuards(WorkspaceGuard)
@SetMetadata('workspacePermissions', [WORKSPACE_PERMISSION.READ_WORKSPACE])
@SetMetadata('workspaceId', 'params.id')
async getWorkspaceInfo(@Param('id') workspaceId: string, @Request() req) {
const workspaceInfo = await this.workspaceService.findOneById(workspaceId);
const members = await this.workspaceMemberService.findAllByWorkspaceId({
workspaceId,
});
const memberInfoMap = members.reduce((pre, cur) => {
cur[cur.userId] = cur;
return pre;
}, {});
const userIdList = members.map((item) => item.userId);
const userList = await this.userService.getUserListByIds({
idList: userIdList,
});
const userInfoMap = userList.reduce((pre, cur) => {
const id = cur._id.toString();
pre[id] = cur;
return pre;
}, {});
const currentUserId = req.user._id.toString();
return {
code: 200,
data: {
_id: workspaceInfo._id,
name: workspaceInfo.name,
description: workspaceInfo.description,
currentUserId,
currentUserRole: memberInfoMap?.[currentUserId]?.role,
members: members.map((item) => {
return {
_id: item._id,
userId: item.userId,
role: item.role,
username: userInfoMap[item.userId].username,
};
}),
},
};
}
@Post(':id')
@HttpCode(200)
@UseGuards(WorkspaceGuard)
@SetMetadata('workspacePermissions', [WORKSPACE_PERMISSION.WRITE_WORKSPACE])
@SetMetadata('workspaceId', 'params.id')
async update(@Param('id') id: string, @Body() workspace: CreateWorkspaceDto) {
const members = workspace.members;
if (!Array.isArray(members) || members.length === 0) {
throw new HttpException('成员不能为空', EXCEPTION_CODE.PARAMETER_ERROR);
}
delete workspace.members;
const updateRes = await this.workspaceService.update(id, workspace);
this.logger.info(`updateRes: ${JSON.stringify(updateRes)}`);
const { newMembers, adminMembers, userMembers } = splitMembers(members);
if (
adminMembers.length === 0 &&
!newMembers.some((item) => item.role === WORKSPACE_ROLE.ADMIN)
) {
throw new HttpException(
'空间不能没有管理员',
EXCEPTION_CODE.PARAMETER_ERROR,
);
}
const allUserIdList = members.map((item) => item.userId);
// 不能有重复的userId
const allUserIdSet = new Set(allUserIdList);
if (allUserIdList.length !== Array.from(allUserIdSet).length) {
throw new HttpException(
'不能重复添加用户',
EXCEPTION_CODE.PARAMETER_ERROR,
);
}
// 检查所有成员是否真实存在
const allUserList = await this.userService.getUserListByIds({
idList: allUserIdList,
});
const allUserInfoMap = allUserList.reduce((pre, cur) => {
const id = cur._id.toString();
pre[id] = cur;
return pre;
}, {});
for (const member of members) {
if (!allUserInfoMap[member.userId]) {
throw new HttpException(
`用户id: {${member.userId}} 不存在`,
EXCEPTION_CODE.PARAMETER_ERROR,
);
}
}
const allIds = [...adminMembers, ...userMembers];
// 新增和更新成员,把数据库里已删除的成员删掉
const res = await Promise.all([
this.workspaceMemberService.batchDelete({ idList: [], neIdList: allIds }),
this.workspaceMemberService.batchCreate({
workspaceId: id,
members: newMembers,
}),
this.workspaceMemberService.batchUpdate({
idList: adminMembers,
role: WORKSPACE_ROLE.ADMIN,
}),
this.workspaceMemberService.batchUpdate({
idList: userMembers,
role: WORKSPACE_ROLE.USER,
}),
]);
this.logger.info(`updateRes: ${JSON.stringify(res)}`);
return {
code: 200,
};
}
@Delete(':id')
@HttpCode(200)
@UseGuards(WorkspaceGuard)
@SetMetadata('workspacePermissions', [WORKSPACE_PERMISSION.WRITE_WORKSPACE])
@SetMetadata('workspaceId', 'params.id')
async delete(@Param('id') id: string) {
const res = await this.workspaceService.delete(id);
this.logger.info(`res: ${JSON.stringify(res)}`);
return {
code: 200,
};
}
}

View File

@ -0,0 +1,120 @@
import {
Controller,
Get,
Post,
Body,
UseGuards,
Request,
SetMetadata,
} from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { Authentication } from 'src/guards/authentication.guard';
import { WorkspaceGuard } from 'src/guards/workspace.guard';
import { WorkspaceMemberService } from '../services/workspaceMember.service';
import { CreateWorkspaceMemberDto } from '../dto/createWorkspaceMember.dto';
import { UpdateWorkspaceMemberDto } from '../dto/updateWorkspaceMember.dto';
import { DeleteWorkspaceMemberDto } from '../dto/deleteWorkspaceMember.dto';
import { HttpException } from 'src/exceptions/httpException';
import { EXCEPTION_CODE } from 'src/enums/exceptionCode';
import { PERMISSION as WORKSPACE_PERMISSION } from 'src/enums/workspace';
@ApiTags('workspaceMember')
@ApiBearerAuth()
@UseGuards(WorkspaceGuard)
@UseGuards(Authentication)
@Controller('/api/workspaceMember')
export class WorkspaceMemberController {
constructor(
private readonly workspaceMemberService: WorkspaceMemberService,
) {}
@Post()
@SetMetadata('workspacePermissions', [WORKSPACE_PERMISSION.WRITE_MEMBER])
@SetMetadata('workspaceId', 'body.workspaceId')
async create(@Body() member: CreateWorkspaceMemberDto) {
const { error, value } = CreateWorkspaceMemberDto.validate(member);
if (error) {
throw new HttpException(
`参数错误: ${error.message}`,
EXCEPTION_CODE.PARAMETER_ERROR,
);
}
const { workspaceId, role, userId } = value;
const res = await this.workspaceMemberService.create({
userId,
workspaceId,
role,
});
return {
code: 200,
data: {
memberId: res._id.toString(),
},
};
}
@Get()
@SetMetadata('workspacePermissions', [WORKSPACE_PERMISSION.READ_MEMBER])
@SetMetadata('workspaceId', 'query.workspaceId')
async findAll(@Request() req) {
const workspaceId = req.query.workspaceId;
if (!workspaceId) {
throw new HttpException('参数错误', EXCEPTION_CODE.PARAMETER_ERROR);
}
const list = await this.workspaceMemberService.findAllByWorkspaceId({
workspaceId,
});
return {
code: 200,
data: list,
};
}
@Post('updateRole')
@SetMetadata('workspacePermissions', [WORKSPACE_PERMISSION.WRITE_MEMBER])
@SetMetadata('workspaceId', 'body.workspaceId')
async updateRole(@Body() updateDto: UpdateWorkspaceMemberDto) {
const { error, value } = UpdateWorkspaceMemberDto.validate(updateDto);
if (error) {
throw new HttpException(
`参数错误: ${error.message}`,
EXCEPTION_CODE.PARAMETER_ERROR,
);
}
const updateRes = await this.workspaceMemberService.updateRole({
role: value.role,
workspaceId: value.workspaceId,
userId: value.userId,
});
return {
code: 200,
data: {
modifiedCount: updateRes.modifiedCount,
},
};
}
@Post('deleteMember')
@SetMetadata('workspacePermissions', [WORKSPACE_PERMISSION.WRITE_MEMBER])
@SetMetadata('workspaceId', 'body.id')
async delete(@Body() deleteDto: DeleteWorkspaceMemberDto) {
const { value, error } = DeleteWorkspaceMemberDto.validate(deleteDto);
if (error) {
throw new HttpException(
`参数错误: ${error.message}`,
EXCEPTION_CODE.PARAMETER_ERROR,
);
}
const res = await this.workspaceMemberService.deleteMember({ ...value });
return {
code: 200,
data: {
deletedCount: res.deletedCount,
},
};
}
}

View File

@ -0,0 +1,29 @@
import { ApiProperty } from '@nestjs/swagger';
import Joi from 'joi';
import { ROLE as WORKSPACE_ROLE } from 'src/enums/workspace';
export class CreateWorkspaceDto {
@ApiProperty({ description: '空间名称', required: true })
name: string;
@ApiProperty({ description: '空间描述', required: false })
description?: string;
@ApiProperty({ description: '空间成员', required: true })
members: Array<{ userId: string; role: WORKSPACE_ROLE; _id?: string }>;
static validate(data) {
return Joi.object({
name: Joi.string().required(),
description: Joi.string().allow(null, ''),
members: Joi.array()
.allow(null)
.items(
Joi.object({
userId: Joi.string().required(),
role: Joi.string().valid(WORKSPACE_ROLE.ADMIN, WORKSPACE_ROLE.USER),
}),
),
}).validate(data, { allowUnknown: true });
}
}

View File

@ -0,0 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';
import Joi from 'joi';
import { ROLE as WORKSPACE_ROLE } from 'src/enums/workspace';
export class CreateWorkspaceMemberDto {
@ApiProperty({ description: '空间角色', required: true })
role: string;
@ApiProperty({ description: '空间id', required: false })
workspaceId: string;
@ApiProperty({ description: '用户id', required: true })
userId: string;
static validate(data) {
return Joi.object({
role: Joi.string()
.valid(WORKSPACE_ROLE.ADMIN, WORKSPACE_ROLE.USER)
.required(),
workspaceId: Joi.string().required(),
userId: Joi.string().required(),
}).validate(data);
}
}

View File

@ -0,0 +1,17 @@
import { ApiProperty } from '@nestjs/swagger';
import Joi from 'joi';
export class DeleteWorkspaceMemberDto {
@ApiProperty({ description: '空间id', required: false })
workspaceId: string;
@ApiProperty({ description: '用户id', required: false })
userId: string;
static validate(data) {
return Joi.object({
workspaceId: Joi.string().required(),
userId: Joi.string().required(),
}).validate(data);
}
}

View File

@ -0,0 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';
import Joi from 'joi';
import { ROLE as WORKSPACE_ROLE } from 'src/enums/workspace';
export class UpdateWorkspaceMemberDto {
@ApiProperty({ description: '空间角色', required: true })
role: string;
@ApiProperty({ description: '空间id', required: false })
workspaceId: string;
@ApiProperty({ description: '用户id', required: false })
userId: string;
static validate(data) {
return Joi.object({
role: Joi.string()
.valid(WORKSPACE_ROLE.ADMIN, WORKSPACE_ROLE.USER)
.required(),
workspaceId: Joi.string().required(),
userId: Joi.string().required(),
}).validate(data);
}
}

View File

@ -0,0 +1,107 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { MongoRepository } from 'typeorm';
import { Workspace } from 'src/models/workspace.entity';
import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { ObjectId } from 'mongodb';
import { RECORD_STATUS } from 'src/enums';
@Injectable()
export class WorkspaceService {
constructor(
@InjectRepository(Workspace)
private workspaceRepository: MongoRepository<Workspace>,
@InjectRepository(SurveyMeta)
private surveyMetaRepository: MongoRepository<SurveyMeta>,
) {}
async create(workspace: {
name: string;
description: string;
ownerId: string;
}): Promise<Workspace> {
const newWorkspace = this.workspaceRepository.create({
...workspace,
});
return this.workspaceRepository.save(newWorkspace);
}
async findOneById(id) {
return this.workspaceRepository.findOne({
where: {
_id: new ObjectId(id),
},
});
}
async findAllById({
workspaceIdList,
}: {
workspaceIdList: string[];
}): Promise<Workspace[]> {
return this.workspaceRepository.find({
where: {
_id: {
$in: workspaceIdList.map((item) => new ObjectId(item)),
},
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
},
order: {
_id: -1,
},
select: [
'_id',
'curStatus',
'name',
'description',
'ownerId',
'createDate',
],
});
}
update(id: string, workspace: Partial<Workspace>) {
return this.workspaceRepository.update(id, workspace);
}
async delete(id: string) {
const newStatus = {
status: RECORD_STATUS.REMOVED,
date: Date.now(),
};
const workspaceRes = await this.workspaceRepository.updateOne(
{
_id: new ObjectId(id),
},
{
$set: {
curStatus: newStatus,
},
$push: {
statusList: newStatus as never,
},
},
);
const surveyRes = await this.surveyMetaRepository.updateMany(
{
workspaceId: id,
},
{
$set: {
curStatus: newStatus,
},
$push: {
statusList: newStatus as never,
},
},
);
return {
workspaceRes,
surveyRes,
};
}
}

View File

@ -0,0 +1,143 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { MongoRepository } from 'typeorm';
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
import { ObjectId } from 'mongodb';
import { RECORD_STATUS } from 'src/enums';
@Injectable()
export class WorkspaceMemberService {
constructor(
@InjectRepository(WorkspaceMember)
private workspaceMemberRepository: MongoRepository<WorkspaceMember>,
) {}
async create(member: {
role: string;
userId: string;
workspaceId: string;
}): Promise<WorkspaceMember> {
const newMember = this.workspaceMemberRepository.create(member);
return this.workspaceMemberRepository.save(newMember);
}
async batchCreate({
workspaceId,
members,
}: {
workspaceId: string;
members: Array<{ userId: string; role: string }>;
}) {
if (members.length === 0) {
return {
insertedCount: 0,
};
}
const dataToInsert = members.map((item) => {
return {
...item,
workspaceId,
};
});
return this.workspaceMemberRepository.insertMany(dataToInsert);
}
async batchUpdate({ idList, role }: { idList: Array<string>; role: string }) {
if (idList.length === 0) {
return {
modifiedCount: 0,
};
}
return this.workspaceMemberRepository.updateMany(
{
_id: {
$in: idList.map((item) => new ObjectId(item)),
},
},
{
$set: {
role,
},
},
);
}
async batchDelete({
idList,
neIdList,
}: {
idList: Array<string>;
neIdList: Array<string>;
}) {
if (idList.length === 0 || neIdList.length === 0) {
return {
modifiedCount: 0,
};
}
return this.workspaceMemberRepository.deleteMany({
_id: {
$in: idList.map((item) => new ObjectId(item)),
$nin: neIdList.map((item) => new ObjectId(item)),
},
});
}
async findAllByUserId({ userId }): Promise<WorkspaceMember[]> {
return this.workspaceMemberRepository.find({
where: {
userId,
},
});
}
async findAllByWorkspaceId({ workspaceId }): Promise<WorkspaceMember[]> {
return this.workspaceMemberRepository.find({
where: {
workspaceId,
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
},
select: ['_id', 'createDate', 'curStatus', 'role', 'userId'],
});
}
async findOne({ workspaceId, userId }): Promise<WorkspaceMember> {
return this.workspaceMemberRepository.findOne({
where: {
workspaceId,
userId,
},
});
}
async updateRole({ workspaceId, userId, role }) {
return this.workspaceMemberRepository.updateOne(
{
workspaceId,
userId,
},
{
$set: {
role,
},
},
);
}
async deleteMember({ workspaceId, userId }) {
return this.workspaceMemberRepository.deleteOne({
workspaceId,
userId,
});
}
async countByWorkspaceId({ workspaceId }) {
return this.workspaceMemberRepository.count({
workspaceId,
'curStatus.status': {
$ne: RECORD_STATUS.REMOVED,
},
});
}
}

View File

@ -0,0 +1,26 @@
import { ROLE as WORKSPACE_ROLE } from 'src/enums/workspace';
export type Member = {
userId: string;
role: string;
_id?: string;
};
export const splitMembers = (members: Array<Member>) => {
const newMembers = [],
adminMembers = [],
userMembers = [];
for (const member of members) {
if (!member._id) {
newMembers.push(member);
} else if (member.role === WORKSPACE_ROLE.ADMIN) {
adminMembers.push(member._id);
} else {
userMembers.push(member._id);
}
}
return {
newMembers,
adminMembers,
userMembers,
};
};

View File

@ -0,0 +1,38 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { WorkspaceService } from './services/workspace.service';
import { WorkspaceMemberService } from './services/workspaceMember.service';
import { SurveyMetaService } from '../survey/services/surveyMeta.service';
import { WorkspaceController } from './controllers/workspace.controller';
import { Workspace } from 'src/models/workspace.entity';
import { WorkspaceMember } from 'src/models/workspaceMember.entity';
import { SurveyMeta } from 'src/models/surveyMeta.entity';
import { AuthModule } from '../auth/auth.module';
import { LoggerProvider } from 'src/logger/logger.provider';
import { WorkspaceGuard } from 'src/guards/workspace.guard';
import { PluginManagerProvider } from 'src/securityPlugin/pluginManager.provider';
@Module({
imports: [
TypeOrmModule.forFeature([Workspace, WorkspaceMember, SurveyMeta]),
ConfigModule,
AuthModule,
],
controllers: [WorkspaceController],
providers: [
WorkspaceService,
WorkspaceMemberService,
LoggerProvider,
WorkspaceGuard,
SurveyMetaService,
PluginManagerProvider,
],
exports: [WorkspaceMemberService],
})
export class WorkspaceModule {}