Skip to content

Commit b5a6ada

Browse files
feat: 초대링크 생성 구현
1 parent bd2ee08 commit b5a6ada

File tree

8 files changed

+166
-146
lines changed

8 files changed

+166
-146
lines changed

apps/backend/src/auth/token/token.service.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { JwtService } from '@nestjs/jwt';
33
import { Response } from 'express';
44

55
const HOUR = 60 * 60;
6+
const DAY = 24 * 60 * 60;
67
const FIVE_MONTHS = 5 * 30 * 24 * 60 * 60;
78
const MS_HALF_YEAR = 6 * 30 * 24 * 60 * 60 * 1000;
89

@@ -22,6 +23,15 @@ export class TokenService {
2223
});
2324
}
2425

26+
generateInviteToken(workspaceId: number, role: string): string {
27+
// 초대용 JWT 토큰 생성
28+
const payload = { workspaceId, role };
29+
return this.jwtService.sign(payload, {
30+
expiresIn: DAY, // 초대 유효 기간: 1일
31+
secret: process.env.JWT_SECRET,
32+
});
33+
}
34+
2535
// 후에 DB 로직 (지금은 refreshToken이 DB로 관리 X)
2636
// 추가될 때를 위해 일단 비동기 선언
2737
async refreshAccessToken(refreshToken: string): Promise<string> {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsString } from 'class-validator';
3+
4+
export class CreateWorkspaceInviteUrlDto {
5+
@ApiProperty({
6+
example: 'OO 생성에 성공했습니다.',
7+
description: 'api 요청 결과 메시지',
8+
})
9+
@IsString()
10+
message: string;
11+
12+
@ApiProperty({
13+
example: 'https://octodocs.local/api/workspace/join?token=12345',
14+
description: '워크스페이스 초대용 링크입니다.',
15+
})
16+
@IsString()
17+
inviteUrl: string;
18+
}

apps/backend/src/workspace/workspace.controller.spec.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CreateWorkspaceDto } from './dtos/createWorkspace.dto';
66
import { WorkspaceResponseMessage } from './workspace.controller';
77
import { NotWorkspaceOwnerException } from '../exception/workspace-auth.exception';
88
import { UserWorkspaceDto } from './dtos/userWorkspace.dto';
9+
import { TokenService } from '../auth/token/token.service';
910

1011
describe('WorkspaceController', () => {
1112
let controller: WorkspaceController;
@@ -23,6 +24,10 @@ describe('WorkspaceController', () => {
2324
getUserWorkspaces: jest.fn(),
2425
},
2526
},
27+
{
28+
provide: TokenService,
29+
useValue: {},
30+
},
2631
],
2732
})
2833
.overrideGuard(JwtAuthGuard)
@@ -116,14 +121,19 @@ describe('WorkspaceController', () => {
116121
},
117122
] as UserWorkspaceDto[];
118123

124+
const expectedResult = {
125+
message: WorkspaceResponseMessage.WORKSPACES_RETURNED,
126+
workspaces: mockWorkspaces,
127+
};
128+
119129
jest
120130
.spyOn(service, 'getUserWorkspaces')
121131
.mockResolvedValue(mockWorkspaces);
122132

123133
const result = await controller.getUserWorkspaces(req);
124134

125135
expect(service.getUserWorkspaces).toHaveBeenCalledWith(req.user.sub);
126-
expect(result).toEqual(mockWorkspaces);
136+
expect(result).toEqual(expectedResult);
127137
});
128138
});
129139
});

apps/backend/src/workspace/workspace.controller.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import { WorkspaceService } from './workspace.service';
1515
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
1616
import { MessageResponseDto } from './dtos/messageResponse.dto';
1717
import { CreateWorkspaceDto } from './dtos/createWorkspace.dto';
18-
import { UserWorkspaceDto } from './dtos/userWorkspace.dto';
1918
import { CreateWorkspaceResponseDto } from './dtos/createWorkspaceResponse.dto';
2019
import { GetUserWorkspacesResponseDto } from './dtos/getUserWorkspacesResponse.dto';
20+
import { CreateWorkspaceInviteUrlDto } from './dtos/createWorkspaceInviteUrl.dto';
2121

2222
export enum WorkspaceResponseMessage {
2323
WORKSPACE_CREATED = '워크스페이스를 생성했습니다.',
@@ -84,26 +84,26 @@ export class WorkspaceController {
8484
@HttpCode(HttpStatus.OK)
8585
async getUserWorkspaces(@Request() req) {
8686
const userId = req.user.sub; // 인증된 사용자의 ID
87-
const workspaces = this.workspaceService.getUserWorkspaces(userId);
87+
const workspaces = await this.workspaceService.getUserWorkspaces(userId);
8888
return {
8989
message: WorkspaceResponseMessage.WORKSPACES_RETURNED,
9090
workspaces,
9191
};
9292
}
9393

94-
// TODO: 후에 역할 나눠서 초대링크 만들 수 있게 확장
94+
@ApiResponse({
95+
type: CreateWorkspaceInviteUrlDto,
96+
})
97+
@ApiOperation({
98+
summary: '사용자가 워크스페이스의 초대 링크를 생성합니다.',
99+
})
95100
@Post('/:id/invite')
96101
@UseGuards(JwtAuthGuard) // 로그인 인증
97102
@HttpCode(HttpStatus.CREATED)
98-
async generateInviteLink(
99-
@Request() req,
100-
@Param('workspaceId') workspaceId: string,
101-
) {
103+
async generateInviteLink(@Request() req, @Param('id') id: string) {
104+
// TODO: 나중에 guest말도 다른 역할 초대 링크로도 확장
102105
const userId = req.user.sub; // 인증된 사용자 ID
103-
const inviteUrl = await this.workspaceService.generateInviteToken(
104-
userId,
105-
id,
106-
);
106+
const inviteUrl = await this.workspaceService.generateInviteUrl(userId, id);
107107

108108
return {
109109
message: '초대 URL이 생성되었습니다.',

apps/backend/src/workspace/workspace.module.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,16 @@ import { WorkspaceRepository } from './workspace.repository';
77
import { UserModule } from '../user/user.module';
88
import { RoleModule } from '../role/role.module';
99
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
10-
import { JwtModule } from '@nestjs/jwt';
11-
import { ConfigModule, ConfigService } from '@nestjs/config';
10+
import { TokenModule } from '../auth/token/token.module';
1211
import { TokenService } from '../auth/token/token.service';
12+
import { JwtService } from '@nestjs/jwt';
1313

1414
@Module({
1515
imports: [
1616
TypeOrmModule.forFeature([Workspace]),
1717
UserModule,
1818
RoleModule,
19-
JwtModule.registerAsync({
20-
imports: [ConfigModule],
21-
inject: [ConfigService],
22-
useFactory: async (configService: ConfigService) => ({
23-
secret: configService.get<string>('JWT_SECRET'),
24-
}),
25-
}),
19+
TokenModule,
2620
],
2721
controllers: [WorkspaceController],
2822
providers: [

apps/backend/src/workspace/workspace.service.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { CreateWorkspaceDto } from './dtos/createWorkspace.dto';
1010
import { Workspace } from './workspace.entity';
1111
import { Role } from '../role/role.entity';
1212
import { User } from '../user/user.entity';
13+
import { TokenModule } from '../auth/token/token.module';
1314

1415
describe('WorkspaceService', () => {
1516
let service: WorkspaceService;
@@ -19,6 +20,7 @@ describe('WorkspaceService', () => {
1920

2021
beforeEach(async () => {
2122
const module: TestingModule = await Test.createTestingModule({
23+
imports: [TokenModule],
2224
providers: [
2325
WorkspaceService,
2426
{

apps/backend/src/workspace/workspace.service.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import { UserNotFoundException } from '../exception/user.exception';
88
import { Workspace } from './workspace.entity';
99
import { WorkspaceNotFoundException } from '../exception/workspace.exception';
1010
import { NotWorkspaceOwnerException } from '../exception/workspace-auth.exception';
11+
import { TokenService } from '../auth/token/token.service';
1112

1213
@Injectable()
1314
export class WorkspaceService {
1415
constructor(
1516
private readonly workspaceRepository: WorkspaceRepository,
1617
private readonly userRepository: UserRepository,
1718
private readonly roleRepository: RoleRepository,
19+
private readonly tokenService: TokenService,
1820
) {}
1921

2022
async createWorkspace(
@@ -57,7 +59,6 @@ export class WorkspaceService {
5759
throw new WorkspaceNotFoundException();
5860
}
5961

60-
// Role Repository에서 해당 workspace의 owner이 userId인지 확인
6162
// Role Repository에서 해당 workspace의 owner인지 확인
6263
const role = await this.roleRepository.findOneBy({
6364
workspaceId: workspace.id,
@@ -89,4 +90,35 @@ export class WorkspaceService {
8990
role: role.role as 'owner' | 'guest',
9091
}));
9192
}
93+
94+
async generateInviteUrl(
95+
userId: number,
96+
workspaceId: string,
97+
): Promise<string> {
98+
// 워크스페이스가 존재하는지 확인
99+
const workspace = await this.workspaceRepository.findOneBy({
100+
snowflakeId: workspaceId,
101+
});
102+
103+
if (!workspace) {
104+
throw new WorkspaceNotFoundException();
105+
}
106+
107+
// Role Repository에서 해당 사용자가 소유자인지 확인
108+
const role = await this.roleRepository.findOneBy({
109+
userId,
110+
workspaceId: workspace.id,
111+
role: 'owner',
112+
});
113+
114+
if (!role) {
115+
throw new NotWorkspaceOwnerException();
116+
}
117+
118+
// 게스트용 초대용 토큰 생성
119+
const token = this.tokenService.generateInviteToken(workspace.id, 'guest');
120+
121+
// TODO: 하드코딩 -> 바꿔야할듯?
122+
return `https://octodocs.local/api/workspace/join?token=${token}`;
123+
}
92124
}

0 commit comments

Comments
 (0)