Skip to content

Commit

Permalink
(BE) Add API for Workspace Invitation (#64)
Browse files Browse the repository at this point in the history
* Add service for creating invitation token

* Add swagger decoration for creating invitation token API

* Add service for creating invitation token

* Add controller for joining to workspace using invitation code
  • Loading branch information
devleejb authored Jan 18, 2024
1 parent 57f4acc commit 4aaf6d9
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 6 deletions.
2 changes: 1 addition & 1 deletion backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { JwtStrategy } from "./jwt.strategy";
useFactory: async (configService: ConfigService) => {
return {
signOptions: { expiresIn: "24h" },
secret: configService.get<string>("JWT_SECRET"),
secret: configService.get<string>("JWT_AUTH_SECRET"),
};
},
inject: [ConfigService],
Expand Down
2 changes: 1 addition & 1 deletion backend/src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class JwtStrategy extends PassportStrategy(PassportJwtStrategy) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>("JWT_SECRET"),
secretOrKey: configService.get<string>("JWT_AUTH_SECRET"),
});
}

Expand Down
4 changes: 4 additions & 0 deletions backend/src/utils/constants/auth-role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const WorkspaceRoleConstants = {
OWNER: "OWNER",
MEMBER: "MEMBER",
};
6 changes: 6 additions & 0 deletions backend/src/workspaces/dto/join-workspace.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ApiProperty } from "@nestjs/swagger";

export class JoinWorkspaceDto {
@ApiProperty({ description: "Invitation token of workspace to join", type: String })
invitationToken: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ApiProperty } from "@nestjs/swagger";

export class CreateInvitationTokenResponse {
@ApiProperty({ type: String, description: "Token for invitation" })
invitationToken: string;
}
4 changes: 4 additions & 0 deletions backend/src/workspaces/types/inviation-token-payload.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export class InvitationTokenPayload {
sub: string;
workspaceId: string;
}
3 changes: 3 additions & 0 deletions backend/src/workspaces/types/join-workspace-response.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { WorkspaceDomain } from "./workspace-domain.type";

export class JoinWorkspaceResponse extends WorkspaceDomain {}
54 changes: 53 additions & 1 deletion backend/src/workspaces/workspaces.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,28 @@ import {
Req,
} from "@nestjs/common";
import { WorkspacesService } from "./workspaces.service";
import { CreateWorkspaceDto } from "./dto/CreateWorkspace.dto";
import { CreateWorkspaceDto } from "./dto/create-workspace.dto";
import {
ApiBearerAuth,
ApiBody,
ApiCreatedResponse,
ApiFoundResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiParam,
ApiQuery,
ApiTags,
ApiUnauthorizedResponse,
} from "@nestjs/swagger";
import { AuthroizedRequest } from "src/utils/types/req.type";
import { CreateWorkspaceResponse } from "./types/create-workspace-response.type";
import { FindWorkspaceResponse } from "./types/find-workspace-response.type";
import { HttpExceptionResponse } from "src/utils/types/http-exception-response.type";
import { FindWorkspacesResponse } from "./types/find-workspaces-response.type";
import { CreateInvitationTokenResponse } from "./types/create-inviation-token-response.type";
import { JoinWorkspaceDto } from "./dto/join-workspace.dto";
import { JoinWorkspaceResponse } from "./types/join-workspace-response.type";

@ApiTags("Workspaces")
@ApiBearerAuth()
Expand Down Expand Up @@ -90,4 +96,50 @@ export class WorkspacesController {
): Promise<FindWorkspacesResponse> {
return this.workspacesService.findMany(req.user.id, pageSize, cursor);
}

@Post(":workspace_id/invite-token")
@ApiOperation({
summary: "Create a Invitation Token",
description: "Create a inviation token using JWT.",
})
@ApiParam({
name: "workspace_id",
description: "ID of workspace to create invitation token",
})
@ApiOkResponse({
type: CreateInvitationTokenResponse,
})
@ApiNotFoundResponse({
type: HttpExceptionResponse,
description: "The workspace does not exist, or the user lacks the appropriate permissions.",
})
async createInvitationToken(
@Req() req: AuthroizedRequest,
@Param("workspace_id") workspaceId: string
): Promise<CreateInvitationTokenResponse> {
return this.workspacesService.createInvitationToken(req.user.id, workspaceId);
}

@Post("join")
@ApiOperation({
summary: "Join to the Workspace",
description: "Join to the workspace using JWT invitation token.",
})
@ApiOkResponse({
type: JoinWorkspaceResponse,
})
@ApiUnauthorizedResponse({
type: HttpExceptionResponse,
description: "Invitation token is invalid or expired.",
})
@ApiNotFoundResponse({
description: "The workspace does not exist.",
type: HttpExceptionResponse,
})
async join(
@Req() req: AuthroizedRequest,
@Body() joinWorkspaceDto: JoinWorkspaceDto
): Promise<JoinWorkspaceResponse> {
return this.workspacesService.join(req.user.id, joinWorkspaceDto.invitationToken);
}
}
13 changes: 13 additions & 0 deletions backend/src/workspaces/workspaces.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,21 @@ import { Module } from "@nestjs/common";
import { WorkspacesController } from "./workspaces.controller";
import { WorkspacesService } from "./workspaces.service";
import { PrismaService } from "src/db/prisma.service";
import { JwtModule } from "@nestjs/jwt";
import { ConfigService } from "@nestjs/config";

@Module({
imports: [
JwtModule.registerAsync({
useFactory: async (configService: ConfigService) => {
return {
signOptions: { expiresIn: "12h" },
secret: configService.get<string>("JWT_INVITATION_SECRET"),
};
},
inject: [ConfigService],
}),
],
controllers: [WorkspacesController],
providers: [WorkspacesService, PrismaService],
})
Expand Down
87 changes: 84 additions & 3 deletions backend/src/workspaces/workspaces.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { Injectable, NotFoundException, UnauthorizedException } from "@nestjs/common";
import { Prisma, Workspace } from "@prisma/client";
import { PrismaService } from "src/db/prisma.service";
import { FindWorkspacesResponse } from "./types/find-workspaces-response.type";
import { JwtService } from "@nestjs/jwt";
import { CreateInvitationTokenResponse } from "./types/create-inviation-token-response.type";
import { InvitationTokenPayload } from "./types/inviation-token-payload.type";
import { WorkspaceRoleConstants } from "src/utils/constants/auth-role";

@Injectable()
export class WorkspacesService {
constructor(private prismaService: PrismaService) {}
constructor(
private prismaService: PrismaService,
private jwtService: JwtService
) {}

async create(userId: string, title: string): Promise<Workspace> {
const workspace = await this.prismaService.workspace.create({
Expand All @@ -18,7 +25,7 @@ export class WorkspacesService {
data: {
workspaceId: workspace.id,
userId,
role: "OWNER",
role: WorkspaceRoleConstants.OWNER,
},
});

Expand Down Expand Up @@ -77,4 +84,78 @@ export class WorkspacesService {
cursor: workspaceList.length > pageSize ? workspaceList[pageSize].id : null,
};
}

async createInvitationToken(
userId: string,
workspaceId: string
): Promise<CreateInvitationTokenResponse> {
try {
await this.prismaService.userWorkspace.findFirstOrThrow({
where: {
userId,
workspaceId,
},
});
} catch (e) {
throw new NotFoundException();
}

const invitationToken = this.jwtService.sign({
sub: userId,
workspaceId,
});

return {
invitationToken,
};
}

async join(userId: string, invitationToken: string) {
let workspaceId: string;

try {
const payload = this.jwtService.verify<InvitationTokenPayload>(invitationToken);

workspaceId = payload.workspaceId;
} catch (err) {
throw new UnauthorizedException("Invitation token is invalid or expired.");
}

try {
await this.prismaService.workspace.findUniqueOrThrow({
where: {
id: workspaceId,
},
});
} catch (e) {
throw new NotFoundException("The workspace is deleted.");
}

const userWorkspace = await this.prismaService.userWorkspace.findFirst({
where: {
userId,
workspaceId,
},
include: {
workspace: true,
},
});

if (!userWorkspace) {
return userWorkspace.workspace;
}

const newUserWorkspace = await this.prismaService.userWorkspace.create({
data: {
userId,
workspaceId,
role: WorkspaceRoleConstants.MEMBER,
},
include: {
workspace: true,
},
});

return newUserWorkspace.workspace;
}
}

0 comments on commit 4aaf6d9

Please sign in to comment.