Skip to content

Commit

Permalink
(BE) Add API for Document Sharing (#65)
Browse files Browse the repository at this point in the history
* Add JwtModule to workspace-documents module for sharing

* Add service for sharing token

* Add controller for creating sharing token

* Add documents module base code

* Fix formatting

* Add JwtModule to document module

* Add service for document sharing

* Add controller for document sharing
  • Loading branch information
devleejb authored Jan 18, 2024
1 parent 4aaf6d9 commit fde4941
Show file tree
Hide file tree
Showing 19 changed files with 313 additions and 15 deletions.
17 changes: 9 additions & 8 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { JwtAuthGuard } from "./auth/jwt.guard";
import { WorkspacesModule } from "./workspaces/workspaces.module";
import { WorkspaceUsersModule } from "./workspace-users/workspace-users.module";
import { WorkspaceDocumentsModule } from "./workspace-documents/workspace-documents.module";
import { DocumentsModule } from "./documents/documents.module";

@Module({
imports: [
Expand All @@ -17,6 +18,7 @@ import { WorkspaceDocumentsModule } from "./workspace-documents/workspace-docume
WorkspacesModule,
WorkspaceUsersModule,
WorkspaceDocumentsModule,
DocumentsModule,
],
controllers: [],
providers: [
Expand Down
18 changes: 18 additions & 0 deletions backend/src/documents/documents.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from "@nestjs/testing";
import { DocumentsController } from "./documents.controller";

describe("DocumentsController", () => {
let controller: DocumentsController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [DocumentsController],
}).compile();

controller = module.get<DocumentsController>(DocumentsController);
});

it("should be defined", () => {
expect(controller).toBeDefined();
});
});
41 changes: 41 additions & 0 deletions backend/src/documents/documents.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Controller, Get, Query } from "@nestjs/common";
import { DocumentsService } from "./documents.service";
import { Public } from "src/utils/decorators/auth.decorator";
import {
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiQuery,
ApiTags,
ApiUnauthorizedResponse,
} from "@nestjs/swagger";
import { FindDocumentFromSharingTokenResponse } from "./types/find-document-from-sharing-token-response.type";
import { HttpExceptionResponse } from "src/utils/types/http-exception-response.type";

@ApiTags("Documents")
@Controller("documents")
export class DocumentsController {
constructor(private documentsService: DocumentsService) {}

@Public()
@Get("share")
@ApiOperation({
summary: "Retrieve a Shared Document using Sharing Token",
description: "If the user has the access permissions, return a shared document.",
})
@ApiQuery({ type: String, name: "token", description: "Sharing Token" })
@ApiOkResponse({ type: FindDocumentFromSharingTokenResponse })
@ApiNotFoundResponse({
type: HttpExceptionResponse,
description: "The document does not exist.",
})
@ApiUnauthorizedResponse({
type: HttpExceptionResponse,
description: "The sharing token is expired or invalid.",
})
async findDocumentFromSharingToken(
@Query("token") token: string
): Promise<FindDocumentFromSharingTokenResponse> {
return this.documentsService.findDocumentFromSharingToken(token);
}
}
22 changes: 22 additions & 0 deletions backend/src/documents/documents.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Module } from "@nestjs/common";
import { DocumentsController } from "./documents.controller";
import { DocumentsService } from "./documents.service";
import { JwtModule } from "@nestjs/jwt";
import { ConfigService } from "@nestjs/config";
import { PrismaService } from "src/db/prisma.service";

@Module({
imports: [
JwtModule.registerAsync({
useFactory: async (configService: ConfigService) => {
return {
secret: configService.get<string>("JWT_SHARING_SECRET"),
};
},
inject: [ConfigService],
}),
],
controllers: [DocumentsController],
providers: [DocumentsService, PrismaService],
})
export class DocumentsModule {}
18 changes: 18 additions & 0 deletions backend/src/documents/documents.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from "@nestjs/testing";
import { DocumentsService } from "./documents.service";

describe("DocumentsService", () => {
let service: DocumentsService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DocumentsService],
}).compile();

service = module.get<DocumentsService>(DocumentsService);
});

it("should be defined", () => {
expect(service).toBeDefined();
});
});
46 changes: 46 additions & 0 deletions backend/src/documents/documents.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Injectable, NotFoundException, UnauthorizedException } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { Document } from "@prisma/client";
import { PrismaService } from "src/db/prisma.service";
import { SharingPayload } from "src/utils/types/sharing.type";
import { FindDocumentFromSharingTokenResponse } from "./types/find-document-from-sharing-token-response.type";
import { ShareRoleEnum } from "src/utils/constants/share-role";

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

async findDocumentFromSharingToken(
sharingToken: string
): Promise<FindDocumentFromSharingTokenResponse> {
let documentId: string, role: ShareRoleEnum;

try {
const payload = this.jwtService.verify<SharingPayload>(sharingToken);
documentId = payload.documentId;
role = payload.role;
} catch (e) {
throw new UnauthorizedException("Invalid sharing token");
}

let document: Document;

try {
document = await this.prismaService.document.findUniqueOrThrow({
where: {
id: documentId,
},
});
} catch (e) {
throw new NotFoundException();
}

return {
...document,
role,
};
}
}
18 changes: 18 additions & 0 deletions backend/src/documents/types/document-domain.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ApiProperty } from "@nestjs/swagger";

export class DocumentDomain {
@ApiProperty({ type: String, description: "ID of the document" })
id: string;
@ApiProperty({ type: String, description: "Yorkie Document ID of the document" })
yorkieDocumentId: string;
@ApiProperty({ type: String, description: "Title of the document" })
title: string;
@ApiProperty({ type: String, description: "Content of the document", required: false })
content?: string;
@ApiProperty({ type: Date, description: "Created date of the document" })
createdAt: Date;
@ApiProperty({ type: Date, description: "Updated date of the document" })
updatedAt: Date;
@ApiProperty({ type: String, description: "ID of the workspace that includes the document" })
workspaceId: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ShareRoleEnum } from "src/utils/constants/share-role";
import { DocumentDomain } from "./document-domain.type";
import { ApiProperty } from "@nestjs/swagger";

export class FindDocumentFromSharingTokenResponse extends DocumentDomain {
@ApiProperty({ enum: ShareRoleEnum, description: "Role of share token" })
role: ShareRoleEnum;
}
2 changes: 1 addition & 1 deletion backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ async function bootstrap() {
SwaggerModule.setup("api", app, document);

// Auto Validation
app.useGlobalPipes(new ValidationPipe());
app.useGlobalPipes(new ValidationPipe({ transform: true }));

await app.listen(3000);
}
Expand Down
8 changes: 4 additions & 4 deletions backend/src/utils/constants/auth-role.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const WorkspaceRoleConstants = {
OWNER: "OWNER",
MEMBER: "MEMBER",
};
export enum WorkspaceRoleConstants {
OWNER = "OWNER",
MEMBER = "MEMBER",
}
4 changes: 4 additions & 0 deletions backend/src/utils/constants/share-role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum ShareRoleEnum {
READ = "READ",
EDIT = "EDIT",
}
1 change: 1 addition & 0 deletions backend/src/utils/types/share-role.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type ShareRole = "READ" | "EDIT";
6 changes: 6 additions & 0 deletions backend/src/utils/types/sharing.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ShareRoleEnum } from "../constants/share-role";

export class SharingPayload {
documentId: string;
role: ShareRoleEnum;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsDate } from "class-validator";
import { ShareRoleEnum } from "src/utils/constants/share-role";

export class CreateWorkspaceDocumentShareTokenDto {
@ApiProperty({ enum: ShareRoleEnum, description: "Role to share" })
role: ShareRoleEnum;

@ApiProperty({ type: Date, description: "Share link expiration date" })
@Type(() => Date)
@IsDate()
expirationDate: Date;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ApiProperty } from "@nestjs/swagger";

export class CreateWorkspaceDocumentShareTokenResponse {
@ApiProperty({
type: String,
description: "Sharing token",
})
sharingToken: string;
}
30 changes: 30 additions & 0 deletions backend/src/workspace-documents/workspace-documents.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ApiCreatedResponse,
ApiFoundResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiQuery,
ApiTags,
Expand All @@ -26,6 +27,8 @@ import { CreateWorkspaceDocumentResponse } from "./types/create-workspace-docume
import { HttpExceptionResponse } from "src/utils/types/http-exception-response.type";
import { FindWorkspaceDocumentResponse } from "./types/find-workspace-document-response.type";
import { FindWorkspaceDocumentsResponse } from "./types/find-workspace-documents-response.type";
import { CreateWorkspaceDocumentShareTokenResponse } from "./types/create-workspace-document-share-token-response.type";
import { CreateWorkspaceDocumentShareTokenDto } from "./dto/create-workspace-document-share-token.dto";

@ApiTags("Workspace.Documents")
@ApiBearerAuth()
Expand Down Expand Up @@ -106,4 +109,31 @@ export class WorkspaceDocumentsController {
createWorkspaceDocumentDto.title
);
}

@Post(":document_id/share-token")
@ApiOperation({
summary: "Retrieve a Share Token for the Document",
description: "If the user has the access permissions, return a share token.",
})
@ApiBody({ type: CreateWorkspaceDocumentShareTokenDto })
@ApiOkResponse({ type: CreateWorkspaceDocumentShareTokenResponse })
@ApiNotFoundResponse({
type: HttpExceptionResponse,
description:
"The workspace or document does not exist, or the user lacks the appropriate permissions.",
})
async createShareToken(
@Req() req: AuthroizedRequest,
@Param("workspace_id") workspaceId: string,
@Param("document_id") documentId: string,
@Body() createWorkspaceDocumentShareTokenDto: CreateWorkspaceDocumentShareTokenDto
): Promise<CreateWorkspaceDocumentShareTokenResponse> {
return this.workspaceDocumentsService.createSharingToken(
req.user.id,
workspaceId,
documentId,
createWorkspaceDocumentShareTokenDto.role,
createWorkspaceDocumentShareTokenDto.expirationDate
);
}
}
12 changes: 12 additions & 0 deletions backend/src/workspace-documents/workspace-documents.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,20 @@ import { Module } from "@nestjs/common";
import { WorkspaceDocumentsService } from "./workspace-documents.service";
import { WorkspaceDocumentsController } from "./workspace-documents.controller";
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 {
secret: configService.get<string>("JWT_SHARING_SECRET"),
};
},
inject: [ConfigService],
}),
],
providers: [WorkspaceDocumentsService, PrismaService],
controllers: [WorkspaceDocumentsController],
})
Expand Down
Loading

0 comments on commit fde4941

Please sign in to comment.