diff --git a/backend/src/project/dto/ProjectInvitePreviewResponse.dto.ts b/backend/src/project/dto/ProjectInvitePreviewResponse.dto.ts new file mode 100644 index 00000000..3eaca4a1 --- /dev/null +++ b/backend/src/project/dto/ProjectInvitePreviewResponse.dto.ts @@ -0,0 +1,20 @@ +export class ProjectInvitePreviewResponseDto { + id: number; + title: string; + subject: string; + leaderUsername: string; + + static of( + id: number, + title: string, + subject: string, + leaderUsername: string, + ) { + const dto = new ProjectInvitePreviewResponseDto(); + dto.id = id; + dto.title = title; + dto.subject = subject; + dto.leaderUsername = leaderUsername; + return dto; + } +} diff --git a/backend/src/project/dto/service/ProjectBriefInfo.dto.ts b/backend/src/project/dto/service/ProjectBriefInfo.dto.ts new file mode 100644 index 00000000..4cc8aa2d --- /dev/null +++ b/backend/src/project/dto/service/ProjectBriefInfo.dto.ts @@ -0,0 +1,20 @@ +export class ProjectBriefInfoDto { + id: number; + title: string; + subject: string; + leaderUsername: string; + + static of( + id: number, + title: string, + subject: string, + leaderUsername: string, + ) { + const dto = new ProjectBriefInfoDto(); + dto.id = id; + dto.title = title; + dto.subject = subject; + dto.leaderUsername = leaderUsername; + return dto; + } +} diff --git a/backend/src/project/project.controller.ts b/backend/src/project/project.controller.ts index 534ba83b..ac4f84e9 100644 --- a/backend/src/project/project.controller.ts +++ b/backend/src/project/project.controller.ts @@ -4,6 +4,7 @@ import { Controller, Get, NotFoundException, + Param, Post, Req, Res, @@ -14,6 +15,7 @@ import { MemberRequest } from 'src/common/guard/authentication.guard'; import { JoinProjectRequestDto } from './dto/JoinProjectRequest.dto'; import { Response } from 'express'; import { ProjectWebsocketGateway } from './websocket.gateway'; +import { ProjectInvitePreviewResponseDto } from './dto/ProjectInvitePreviewResponse.dto'; @Controller('project') export class ProjectController { @@ -82,4 +84,31 @@ export class ProjectController { ); return response.status(201).send(); } + + @Get('/invite-preview/:inviteLinkId') + async getProjectInvitePreview( + @Param('inviteLinkId') inviteLinkId: string, + @Res() response: Response, + ) { + let projectPublicInfo; + try { + projectPublicInfo = + await this.projectService.getProjectBriefInfoByInviteLinkId( + inviteLinkId, + ); + } catch (err) { + if (err.message === 'Project Not Found') throw new NotFoundException(); + throw err; + } + return response + .status(200) + .send( + ProjectInvitePreviewResponseDto.of( + projectPublicInfo.id, + projectPublicInfo.title, + projectPublicInfo.subject, + projectPublicInfo.leaderUsername, + ), + ); + } } diff --git a/backend/src/project/project.repository.ts b/backend/src/project/project.repository.ts index e7f8b8af..e1401253 100644 --- a/backend/src/project/project.repository.ts +++ b/backend/src/project/project.repository.ts @@ -75,6 +75,17 @@ export class ProjectRepository { ); } + async getProjectWithMemberListByLinkId( + inviteLinkId: string, + ): Promise { + const projectWithMemberList = await this.projectRepository.findOne({ + where: { inviteLinkId }, + relations: { projectToMember: { member: true } }, + }); + if (!projectWithMemberList) throw new Error('Project Not Found'); + return projectWithMemberList; + } + getProjectByLinkId(projectLinkId: string): Promise { return this.projectRepository.findOne({ where: { inviteLinkId: projectLinkId }, diff --git a/backend/src/project/service/project.service.ts b/backend/src/project/service/project.service.ts index 85739aaf..a2c778f3 100644 --- a/backend/src/project/service/project.service.ts +++ b/backend/src/project/service/project.service.ts @@ -11,6 +11,7 @@ import { Task, TaskStatus } from '../entity/task.entity'; import { LexoRank } from 'lexorank'; import { MemberRole } from '../enum/MemberRole.enum'; import { v4 as uuidv4 } from 'uuid'; +import { ProjectBriefInfoDto } from '../dto/service/ProjectBriefInfo.dto'; @Injectable() export class ProjectService { @@ -124,6 +125,27 @@ export class ProjectService { return projectToMember?.role === MemberRole.LEADER; } + async getProjectBriefInfoByInviteLinkId( + inviteLinkId: string, + ): Promise { + const project = + await this.projectRepository.getProjectWithMemberListByLinkId( + inviteLinkId, + ); + const leader = project.projectToMember.find( + (member) => member.role === MemberRole.LEADER, + ); + if (!leader) { + throw new Error('Project does not have a leader'); + } + return ProjectBriefInfoDto.of( + project.id, + project.title, + project.subject, + leader.member.username, + ); + } + getProjectByLinkId(projectLinkId: string): Promise { return this.projectRepository.getProjectByLinkId(projectLinkId); } diff --git a/backend/test/project/get-project-preview.e2e-spec.ts b/backend/test/project/get-project-preview.e2e-spec.ts new file mode 100644 index 00000000..58dc1735 --- /dev/null +++ b/backend/test/project/get-project-preview.e2e-spec.ts @@ -0,0 +1,91 @@ +import * as request from 'supertest'; +import { + app, + appInit, + createMember, + createProject, + getProjectLinkId, + listenAppAndSetPortEnv, + memberFixture, + memberFixture2, + projectPayload, +} from 'test/setup'; + +describe('GET /project/invite-preview', () => { + beforeEach(async () => { + await app.close(); + await appInit(); + await listenAppAndSetPortEnv(app); + }); + + it('should return 200 when given valid invite link id', async () => { + const { accessToken } = await createMember(memberFixture, app); + const { id: projectId } = await createProject( + accessToken, + projectPayload, + app, + ); + const inviteLinkId = await getProjectLinkId(accessToken, projectId); + const { accessToken: newAccessToken } = await createMember( + memberFixture2, + app, + ); + + const response = await request(app.getHttpServer()) + .get(`/api/project/invite-preview/${inviteLinkId}`) + .set('Authorization', `Bearer ${newAccessToken}`); + + expect(response.status).toBe(200); + expect(typeof response.body.id).toBe('number'); + expect(response.body.title).toBe(projectPayload.title); + expect(response.body.subject).toBe(projectPayload.subject); + expect(response.body.leaderUsername).toBe(memberFixture.username); + }); + + it('should return 404 when project link ID is not found', async () => { + const invalidUUID = 'c93a87e8-a0a4-4b55-bdf2-59bf691f5c37'; + const { accessToken: newAccessToken } = await createMember( + memberFixture2, + app, + ); + + const response = await request(app.getHttpServer()) + .get(`/api/project/invite-preview/${invalidUUID}`) + .set('Authorization', `Bearer ${newAccessToken}`); + + expect(response.status).toBe(404); + }); + + it('should return 401 (Bearer Token is missing)', async () => { + const { accessToken } = await createMember(memberFixture, app); + const { id: projectId } = await createProject( + accessToken, + projectPayload, + app, + ); + const projectLinkId = await getProjectLinkId(accessToken, projectId); + + const response = await request(app.getHttpServer()).get( + `/api/project/invite-preview/${projectLinkId}`, + ); + + expect(response.status).toBe(401); + }); + + it('should return 401 (Expired:accessToken) when given invalid access token', async () => { + const { accessToken } = await createMember(memberFixture, app); + const { id: projectId } = await createProject( + accessToken, + projectPayload, + app, + ); + const projectLinkId = await getProjectLinkId(accessToken, projectId); + + const response = await request(app.getHttpServer()) + .get(`/api/project/invite-preview/${projectLinkId}`) + .set('Authorization', `Bearer invalidToken`); + + expect(response.status).toBe(401); + expect(response.body.message).toBe('Expired:accessToken'); + }); +});