Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 초대링크 변경 API 구현 #336

Merged
merged 1 commit into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IsNotEmpty, Matches } from 'class-validator';

export class InviteLinkUpdateRequestDto {
@Matches(/^update$/)
action: string;

@IsNotEmpty()
content: Record<string, any>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class inviteLinkDto {
inviteLinkId: string;

static of(inviteLinkId: string): inviteLinkDto {
const dto = new inviteLinkDto();
dto.inviteLinkId = inviteLinkId;
return dto;
}
}

export class InviteLinkUpdateResponseDto {
domain: string;
action: string;
content: inviteLinkDto;

static of(inviteLinkId: string): InviteLinkUpdateResponseDto {
const dto = new InviteLinkUpdateResponseDto();
dto.domain = 'inviteLink';
dto.action = 'update';
dto.content = inviteLinkDto.of(inviteLinkId);
return dto;
}
}
2 changes: 2 additions & 0 deletions backend/src/project/project.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { WsProjectStoryController } from './ws-controller/ws-project-story.contr
import { Task } from './entity/task.entity';
import { WsProjectTaskController } from './ws-controller/ws-project-task.controller';
import { WsProjectInfoController } from './ws-controller/ws-project-info.controller';
import { WsProjectInviteLinkController } from './ws-controller/ws-project-invite-link.controller';

@Module({
imports: [
Expand Down Expand Up @@ -53,6 +54,7 @@ import { WsProjectInfoController } from './ws-controller/ws-project-info.control
WsProjectStoryController,
WsProjectTaskController,
WsProjectInfoController,
WsProjectInviteLinkController,
],
})
export class ProjectModule {}
11 changes: 11 additions & 0 deletions backend/src/project/project.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ export class ProjectRepository {
});
}

async updateInviteLink(
projectId: number,
newInviteLinkId: string,
): Promise<boolean> {
const result = await this.projectRepository.update(
{ id: projectId },
{ inviteLinkId: newInviteLinkId },
);
return !!result.affected;
}

getProject(projectId: number): Promise<Project | null> {
return this.projectRepository.findOne({ where: { id: projectId } });
}
Expand Down
14 changes: 14 additions & 0 deletions backend/src/project/service/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Story, StoryStatus } from '../entity/story.entity';
import { Task, TaskStatus } from '../entity/task.entity';
import { LexoRank } from 'lexorank';
import { MemberRole } from '../enum/MemberRole.enum';
import { v4 as uuidv4 } from 'uuid';

@Injectable()
export class ProjectService {
Expand Down Expand Up @@ -51,6 +52,19 @@ export class ProjectService {
return project;
}

async updateInviteLink(projectId: number, member: Member): Promise<string> {
if (!(await this.isProjectLeader(projectId, member))) {
throw new Error('Member is not the project leader');
}
const newInviteLinkId = await uuidv4();
const isUpdated = await this.projectRepository.updateInviteLink(
projectId,
newInviteLinkId,
);
if (!isUpdated) throw new Error('invite link not updated');
return newInviteLinkId;
}

async getProject(projectId: number, member: Member): Promise<Project | null> {
if (!(await this.isExistProject(projectId)))
throw new Error('Project not found');
Expand Down
12 changes: 12 additions & 0 deletions backend/src/project/websocket.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { WsProjectEpicController } from './ws-controller/ws-project-epic.control
import { WsProjectStoryController } from './ws-controller/ws-project-story.controller';
import { WsProjectTaskController } from './ws-controller/ws-project-task.controller';
import { WsProjectInfoController } from './ws-controller/ws-project-info.controller';
import { WsProjectInviteLinkController } from './ws-controller/ws-project-invite-link.controller';

@WebSocketGateway({
namespace: /project-\d+/,
Expand All @@ -42,6 +43,7 @@ export class ProjectWebsocketGateway
private readonly wsProjectStoryController: WsProjectStoryController,
private readonly wsProjectTaskController: WsProjectTaskController,
private readonly wsProjectInfoController: WsProjectInfoController,
private readonly wsProjectInviteLinkController: WsProjectInviteLinkController,
) {
this.namespaceMap = new Map();
}
Expand Down Expand Up @@ -132,6 +134,16 @@ export class ProjectWebsocketGateway
}
}

@SubscribeMessage('inviteLink')
async handleInviteLinkEvent(
@ConnectedSocket() client: ClientSocket,
@MessageBody() data: any,
) {
if (data.action === 'update') {
this.wsProjectInviteLinkController.updateInviteLink(client, data);
}
}

@SubscribeMessage('joinBacklog')
async handleJoinBacklogEvent(@ConnectedSocket() client: ClientSocket) {
this.wsProjectController.joinBacklogPage(client);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Injectable } from '@nestjs/common';
import { ProjectService } from '../service/project.service';
import { ClientSocket } from '../type/ClientSocket.type';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
import { getRecursiveErrorMsgList } from '../util/validation.util';
import { InviteLinkUpdateRequestDto } from '../dto/invite-link/InviteLinkUpdateRequest.dto';
import { InviteLinkUpdateResponseDto } from '../dto/invite-link/InviteLinkUpdateResponse.dto';

@Injectable()
export class WsProjectInviteLinkController {
constructor(private readonly projectService: ProjectService) {}
async updateInviteLink(client: ClientSocket, data: any) {
const errors = await validate(
plainToClass(InviteLinkUpdateRequestDto, data),
);
if (errors.length > 0) {
const errorList = getRecursiveErrorMsgList(errors);
client.emit('error', { errorList });
return;
}
try {
const newInviteLinkId = await this.projectService.updateInviteLink(
client.project.id,
client.member,
);
client.emit('landing', InviteLinkUpdateResponseDto.of(newInviteLinkId));
} catch (e) {
if (e.message === 'Member is not the project leader') {
client.disconnect(true);
} else throw e;
}
}
}
93 changes: 93 additions & 0 deletions backend/test/project/ws-landing-page/ws-invite-link.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {
app,
appInit,
connectServer,
createMember,
createProject,
getProjectLinkId,
joinProject,
listenAppAndSetPortEnv,
memberFixture,
memberFixture2,
projectPayload,
} from 'test/setup';
import {
emitJoinLanding,
handleConnectErrorWithReject,
handleErrorWithReject,
initLanding,
} from '../ws-common';

describe('WS invite link', () => {
beforeEach(async () => {
await app.close();
await appInit();
await listenAppAndSetPortEnv(app);
});
describe('update invite link', () => {
it('should return updated invite link data when project leader request', async () => {
let socket;
return new Promise<void>(async (resolve, reject) => {
const accessToken = (await createMember(memberFixture, app))
.accessToken;
const project = await createProject(accessToken, projectPayload, app);
socket = connectServer(project.id, accessToken);
handleConnectErrorWithReject(socket, reject);
handleErrorWithReject(socket, reject);
await emitJoinLanding(socket);
await initLanding(socket);
const data = {
action: 'update',
content: {},
};
socket.emit('inviteLink', data);
await expectUpdateInviteLink(socket);
resolve();
}).finally(() => {
socket.close();
});
});
const expectUpdateInviteLink = async (socket) => {
return await new Promise<void>((res) => {
socket.once('landing', async (data) => {
const { content, action, domain } = data;
expect(domain).toBe('inviteLink');
expect(action).toBe('update');
expect(content.inviteLinkId).toBeDefined();
expect(typeof content.inviteLinkId).toBe('string');
res();
});
});
};

it('should disconnect if the requester is not the project leader', async () => {
let socket;
return new Promise<void>(async (resolve, reject) => {
const accessToken = (await createMember(memberFixture, app))
.accessToken;
const project = await createProject(accessToken, projectPayload, app);
const projectLinkId = await getProjectLinkId(accessToken, project.id);

const accessToken2 = (await createMember(memberFixture2, app))
.accessToken;
await joinProject(accessToken2, projectLinkId);

socket = connectServer(project.id, accessToken2);
handleConnectErrorWithReject(socket, reject);
handleErrorWithReject(socket, reject);
await emitJoinLanding(socket);
await initLanding(socket);
const data = {
action: 'update',
content: {},
};
socket.emit('inviteLink', data);
socket.on('disconnect', () => {
resolve();
});
}).finally(() => {
socket.close();
});
});
});
});
Loading