From f056f8194f4835061a1916f4c107981acad769d4 Mon Sep 17 00:00:00 2001 From: devleejb Date: Mon, 22 Jan 2024 14:19:02 +0900 Subject: [PATCH] Change generate invitation and sharing token --- backend/package-lock.json | 9 ++++ backend/package.json | 1 + backend/prisma/schema.prisma | 5 ++- backend/src/documents/documents.module.ts | 13 +----- backend/src/documents/documents.service.ts | 18 ++++---- backend/src/utils/functions/random-string.ts | 3 ++ backend/src/utils/types/sharing.type.ts | 6 --- ...eate-workspace-document-share-token.dto.ts | 2 +- .../workspace-documents.controller.ts | 2 +- .../workspace-documents.module.ts | 13 +----- .../workspace-documents.service.ts | 22 +++++----- .../dto/create-invitation-token.dto.ts | 8 ++++ .../types/inviation-token-payload.type.ts | 4 -- .../src/workspaces/workspaces.controller.ts | 10 ++++- backend/src/workspaces/workspaces.module.ts | 14 +------ backend/src/workspaces/workspaces.service.ts | 41 +++++++++++++------ 16 files changed, 84 insertions(+), 87 deletions(-) create mode 100644 backend/src/utils/functions/random-string.ts delete mode 100644 backend/src/utils/types/sharing.type.ts create mode 100644 backend/src/workspaces/dto/create-invitation-token.dto.ts delete mode 100644 backend/src/workspaces/types/inviation-token-payload.type.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index 9f1e0fbc..79ca26e2 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -19,6 +19,7 @@ "@prisma/client": "^5.8.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "moment": "^2.30.1", "passport-github": "^1.1.0", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.1.13", @@ -6678,6 +6679,14 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/backend/package.json b/backend/package.json index 11756834..956b58e8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -31,6 +31,7 @@ "@prisma/client": "^5.8.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "moment": "^2.30.1", "passport-github": "^1.1.0", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.1.13", diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 2d596c65..97bcda47 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -65,7 +65,7 @@ model Document { model WorkspaceInvitationToken { id String @id @default(auto()) @map("_id") @db.ObjectId - token String + token String @unique workspace Workspace @relation(fields: [workspaceId], references: [id]) workspaceId String @map("workspace_id") @db.ObjectId expiredAt DateTime? @map("expired_at") @@ -77,7 +77,8 @@ model WorkspaceInvitationToken { model DocumentSharingToken { id String @id @default(auto()) @map("_id") @db.ObjectId - token String + token String @unique + role String document Document @relation(fields: [documentId], references: [id]) documentId String @map("document_id") @db.ObjectId expiredAt DateTime? @map("expired_at") diff --git a/backend/src/documents/documents.module.ts b/backend/src/documents/documents.module.ts index eaf85655..345ae0c8 100644 --- a/backend/src/documents/documents.module.ts +++ b/backend/src/documents/documents.module.ts @@ -1,21 +1,10 @@ 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("JWT_SHARING_SECRET"), - }; - }, - inject: [ConfigService], - }), - ], + imports: [], controllers: [DocumentsController], providers: [DocumentsService, PrismaService], }) diff --git a/backend/src/documents/documents.service.ts b/backend/src/documents/documents.service.ts index 7cb65679..4d4fee61 100644 --- a/backend/src/documents/documents.service.ts +++ b/backend/src/documents/documents.service.ts @@ -1,17 +1,12 @@ 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 - ) {} + constructor(private prismaService: PrismaService) {} async findDocumentFromSharingToken( sharingToken: string @@ -19,9 +14,14 @@ export class DocumentsService { let documentId: string, role: ShareRoleEnum; try { - const payload = this.jwtService.verify(sharingToken); - documentId = payload.documentId; - role = payload.role; + const documentSharingToken = + await this.prismaService.documentSharingToken.findFirstOrThrow({ + where: { + token: sharingToken, + }, + }); + documentId = documentSharingToken.documentId; + role = documentSharingToken.role as ShareRoleEnum; } catch (e) { throw new UnauthorizedException("Invalid sharing token"); } diff --git a/backend/src/utils/functions/random-string.ts b/backend/src/utils/functions/random-string.ts new file mode 100644 index 00000000..ab00ca77 --- /dev/null +++ b/backend/src/utils/functions/random-string.ts @@ -0,0 +1,3 @@ +export const generateRandomKey = () => { + return Math.random().toString(36).substring(7); +}; diff --git a/backend/src/utils/types/sharing.type.ts b/backend/src/utils/types/sharing.type.ts deleted file mode 100644 index 3d824c7c..00000000 --- a/backend/src/utils/types/sharing.type.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ShareRoleEnum } from "../constants/share-role"; - -export class SharingPayload { - documentId: string; - role: ShareRoleEnum; -} diff --git a/backend/src/workspace-documents/dto/create-workspace-document-share-token.dto.ts b/backend/src/workspace-documents/dto/create-workspace-document-share-token.dto.ts index c91eadc8..dcc2a9a6 100644 --- a/backend/src/workspace-documents/dto/create-workspace-document-share-token.dto.ts +++ b/backend/src/workspace-documents/dto/create-workspace-document-share-token.dto.ts @@ -10,5 +10,5 @@ export class CreateWorkspaceDocumentShareTokenDto { @ApiProperty({ type: Date, description: "Share link expiration date" }) @Type(() => Date) @IsDate() - expirationDate: Date; + expiredAt: Date; } diff --git a/backend/src/workspace-documents/workspace-documents.controller.ts b/backend/src/workspace-documents/workspace-documents.controller.ts index afcd8fea..453e0274 100644 --- a/backend/src/workspace-documents/workspace-documents.controller.ts +++ b/backend/src/workspace-documents/workspace-documents.controller.ts @@ -133,7 +133,7 @@ export class WorkspaceDocumentsController { workspaceId, documentId, createWorkspaceDocumentShareTokenDto.role, - createWorkspaceDocumentShareTokenDto.expirationDate + createWorkspaceDocumentShareTokenDto.expiredAt ); } } diff --git a/backend/src/workspace-documents/workspace-documents.module.ts b/backend/src/workspace-documents/workspace-documents.module.ts index 23f52353..486cfec6 100644 --- a/backend/src/workspace-documents/workspace-documents.module.ts +++ b/backend/src/workspace-documents/workspace-documents.module.ts @@ -2,20 +2,9 @@ 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("JWT_SHARING_SECRET"), - }; - }, - inject: [ConfigService], - }), - ], + imports: [], providers: [WorkspaceDocumentsService, PrismaService], controllers: [WorkspaceDocumentsController], }) diff --git a/backend/src/workspace-documents/workspace-documents.service.ts b/backend/src/workspace-documents/workspace-documents.service.ts index ca813074..27dca6a0 100644 --- a/backend/src/workspace-documents/workspace-documents.service.ts +++ b/backend/src/workspace-documents/workspace-documents.service.ts @@ -2,17 +2,14 @@ import { Injectable, NotFoundException } from "@nestjs/common"; import { Document, Prisma } from "@prisma/client"; import { PrismaService } from "src/db/prisma.service"; import { FindWorkspaceDocumentsResponse } from "./types/find-workspace-documents-response.type"; -import { JwtService } from "@nestjs/jwt"; import { CreateWorkspaceDocumentShareTokenResponse } from "./types/create-workspace-document-share-token-response.type"; import { ShareRole } from "src/utils/types/share-role.type"; import slugify from "slugify"; +import { generateRandomKey } from "src/utils/functions/random-string"; @Injectable() export class WorkspaceDocumentsService { - constructor( - private prismaService: PrismaService, - private jwtService: JwtService - ) {} + constructor(private prismaService: PrismaService) {} async create(userId: string, workspaceId: string, title: string) { try { @@ -134,18 +131,19 @@ export class WorkspaceDocumentsService { throw new NotFoundException(); } - const sharingToken = this.jwtService.sign( - { + const token = generateRandomKey(); + + await this.prismaService.documentSharingToken.create({ + data: { documentId: document.id, + token, + expiredAt: expirationDate, role, }, - { - expiresIn: expirationDate.getTime() - Date.now(), - } - ); + }); return { - sharingToken, + sharingToken: token, }; } } diff --git a/backend/src/workspaces/dto/create-invitation-token.dto.ts b/backend/src/workspaces/dto/create-invitation-token.dto.ts new file mode 100644 index 00000000..d8eeebad --- /dev/null +++ b/backend/src/workspaces/dto/create-invitation-token.dto.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; + +export class CreateInvitationTokenDto { + @ApiProperty({ description: "Expiration date of invitation token", type: Date }) + @Type(() => Date) + expiredAt: Date; +} diff --git a/backend/src/workspaces/types/inviation-token-payload.type.ts b/backend/src/workspaces/types/inviation-token-payload.type.ts deleted file mode 100644 index 90f44d0f..00000000 --- a/backend/src/workspaces/types/inviation-token-payload.type.ts +++ /dev/null @@ -1,4 +0,0 @@ -export class InvitationTokenPayload { - sub: string; - workspaceId: string; -} diff --git a/backend/src/workspaces/workspaces.controller.ts b/backend/src/workspaces/workspaces.controller.ts index 2899efdc..bce4f6b4 100644 --- a/backend/src/workspaces/workspaces.controller.ts +++ b/backend/src/workspaces/workspaces.controller.ts @@ -32,6 +32,7 @@ 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"; +import { CreateInvitationTokenDto } from "./dto/create-invitation-token.dto"; @ApiTags("Workspaces") @ApiBearerAuth() @@ -115,9 +116,14 @@ export class WorkspacesController { }) async createInvitationToken( @Req() req: AuthroizedRequest, - @Param("workspace_id") workspaceId: string + @Param("workspace_id") workspaceId: string, + @Body() createInvitationTokenDto: CreateInvitationTokenDto ): Promise { - return this.workspacesService.createInvitationToken(req.user.id, workspaceId); + return this.workspacesService.createInvitationToken( + req.user.id, + workspaceId, + createInvitationTokenDto.expiredAt + ); } @Post("join") diff --git a/backend/src/workspaces/workspaces.module.ts b/backend/src/workspaces/workspaces.module.ts index 28dded52..99ef2608 100644 --- a/backend/src/workspaces/workspaces.module.ts +++ b/backend/src/workspaces/workspaces.module.ts @@ -2,21 +2,9 @@ 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("JWT_INVITATION_SECRET"), - }; - }, - inject: [ConfigService], - }), - ], + imports: [], controllers: [WorkspacesController], providers: [WorkspacesService, PrismaService], }) diff --git a/backend/src/workspaces/workspaces.service.ts b/backend/src/workspaces/workspaces.service.ts index 0a8b957a..fb1f8a94 100644 --- a/backend/src/workspaces/workspaces.service.ts +++ b/backend/src/workspaces/workspaces.service.ts @@ -2,18 +2,15 @@ import { Injectable, NotFoundException, UnauthorizedException } from "@nestjs/co 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"; import slugify from "slugify"; +import { generateRandomKey } from "src/utils/functions/random-string"; +import moment from "moment"; @Injectable() export class WorkspacesService { - constructor( - private prismaService: PrismaService, - private jwtService: JwtService - ) {} + constructor(private prismaService: PrismaService) {} async create(userId: string, title: string): Promise { let slug = slugify(title); @@ -103,7 +100,8 @@ export class WorkspacesService { async createInvitationToken( userId: string, - workspaceId: string + workspaceId: string, + expiredAt: Date ): Promise { try { await this.prismaService.userWorkspace.findFirstOrThrow({ @@ -116,13 +114,18 @@ export class WorkspacesService { throw new NotFoundException(); } - const invitationToken = this.jwtService.sign({ - sub: userId, - workspaceId, + const token = generateRandomKey(); + + await this.prismaService.workspaceInvitationToken.create({ + data: { + workspaceId, + token, + expiredAt, + }, }); return { - invitationToken, + invitationToken: token, }; } @@ -130,9 +133,21 @@ export class WorkspacesService { let workspaceId: string; try { - const payload = this.jwtService.verify(invitationToken); + const workspaceInvitationToken = + await this.prismaService.workspaceInvitationToken.findFirst({ + where: { + token: invitationToken, + }, + }); + + workspaceId = workspaceInvitationToken.workspaceId; - workspaceId = payload.workspaceId; + if ( + workspaceInvitationToken.expiredAt && + moment().isAfter(workspaceInvitationToken.expiredAt) + ) { + throw new Error(); + } } catch (err) { throw new UnauthorizedException("Invitation token is invalid or expired."); }