Skip to content
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
20 changes: 20 additions & 0 deletions backend/src/project/dto/ProjectInvitePreviewResponse.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
20 changes: 20 additions & 0 deletions backend/src/project/dto/service/ProjectBriefInfo.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
29 changes: 29 additions & 0 deletions backend/src/project/project.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Controller,
Get,
NotFoundException,
Param,
Post,
Req,
Res,
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
),
);
}
}
11 changes: 11 additions & 0 deletions backend/src/project/project.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ export class ProjectRepository {
);
}

async getProjectWithMemberListByLinkId(
inviteLinkId: string,
): Promise<Project> {
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<Project | null> {
return this.projectRepository.findOne({
where: { inviteLinkId: projectLinkId },
Expand Down
22 changes: 22 additions & 0 deletions backend/src/project/service/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -124,6 +125,27 @@ export class ProjectService {
return projectToMember?.role === MemberRole.LEADER;
}

async getProjectBriefInfoByInviteLinkId(
inviteLinkId: string,
): Promise<ProjectBriefInfoDto> {
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<Project | null> {
return this.projectRepository.getProjectByLinkId(projectLinkId);
}
Expand Down
91 changes: 91 additions & 0 deletions backend/test/project/get-project-preview.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
Loading