Skip to content

Commit 36e8285

Browse files
authored
Merge branch 'develop' into feature-fe-#295
2 parents 113aa69 + 9e4bee2 commit 36e8285

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1754
-1188
lines changed

.gitignore

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
.env*
44
*.local
55
.turbo
6-
.crt
7-
.key
6+
*.crt
7+
*.key
88
!Dockerfile.local
99

1010
# compiled output
@@ -52,3 +52,6 @@ lerna-debug.log*
5252
!.vscode/launch.json
5353
!.vscode/extensions.json
5454
db.sqlite
55+
56+
*.crt
57+
*.key

apps/backend/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"build": "nest build",
1010
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
1111
"start": "nest start",
12-
"dev": "nest start --watch",
12+
"dev": "nest start --tsc --watch --preserveWatchOutput --watchAssets",
1313
"start:debug": "nest start --debug --watch",
1414
"start:prod": "node dist/main",
1515
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
@@ -30,6 +30,7 @@
3030
"@nestjs/platform-express": "^10.0.0",
3131
"@nestjs/platform-socket.io": "^10.4.8",
3232
"@nestjs/platform-ws": "^10.4.7",
33+
"@nestjs/schedule": "^4.1.1",
3334
"@nestjs/serve-static": "^4.0.2",
3435
"@nestjs/swagger": "^8.0.5",
3536
"@nestjs/typeorm": "^10.0.2",
@@ -38,6 +39,8 @@
3839
"@types/multer": "^1.4.12",
3940
"class-transformer": "^0.5.1",
4041
"class-validator": "^0.14.1",
42+
"ioredis": "^5.4.1",
43+
"cookie-parser": "^1.4.7",
4144
"lib0": "^0.2.98",
4245
"passport": "^0.7.0",
4346
"passport-kakao": "^1.0.1",

apps/backend/src/app.module.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,20 @@ import { AuthModule } from './auth/auth.module';
2020
import { UserModule } from './user/user.module';
2121
import { WorkspaceModule } from './workspace/workspace.module';
2222
import { RoleModule } from './role/role.module';
23+
import { RedisService } from './redis/redis.service';
24+
import { RedisModule } from './redis/redis.module';
25+
import { ScheduleModule } from '@nestjs/schedule';
26+
import { TasksService } from './tasks/tasks.service';
2327

2428
@Module({
2529
imports: [
30+
ScheduleModule.forRoot(),
2631
ServeStaticModule.forRoot({
2732
rootPath: path.join(__dirname, '..', '..', 'frontend', 'dist'),
2833
}),
2934
ConfigModule.forRoot({
3035
isGlobal: true,
31-
envFilePath: path.join(__dirname, '..', '..', '..', '.env'), // * nest 디렉터리 기준
36+
envFilePath: '/app/.env', // * docker 내부 디렉터리 기준
3237
}),
3338
TypeOrmModule.forRootAsync({
3439
imports: [ConfigModule],
@@ -54,8 +59,9 @@ import { RoleModule } from './role/role.module';
5459
UserModule,
5560
WorkspaceModule,
5661
RoleModule,
62+
RedisModule,
5763
],
5864
controllers: [AppController],
59-
providers: [AppService],
65+
providers: [AppService, RedisService, TasksService],
6066
})
6167
export class AppModule {}
Lines changed: 22 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
import { Test, TestingModule } from '@nestjs/testing';
22
import { AuthController } from './auth.controller';
33
import { AuthService } from './auth.service';
4-
import { JwtService } from '@nestjs/jwt';
5-
import { InvalidTokenException } from '../exception/invalid.exception';
6-
// import { LoginRequiredException } from '../exception/login.exception';
4+
import { JwtAuthGuard } from './guards/jwt-auth.guard';
5+
import { TokenService } from './token/token.service';
6+
import { LoginRequiredException } from '../exception/login.exception';
77

8-
// TODO: 테스트 코드 개선
98
describe('AuthController', () => {
109
let authController: AuthController;
11-
// let authService: AuthService;
12-
let jwtService: JwtService;
10+
let authService: AuthService;
1311

1412
beforeEach(async () => {
1513
const module: TestingModule = await Test.createTestingModule({
1614
controllers: [AuthController],
1715
providers: [
18-
AuthService,
19-
JwtService,
2016
{
2117
provide: AuthService,
2218
useValue: {
@@ -26,23 +22,23 @@ describe('AuthController', () => {
2622
},
2723
},
2824
{
29-
provide: JwtService,
25+
provide: TokenService,
3026
useValue: {
31-
sign: jest.fn().mockReturnValue('test-token'),
32-
verify: jest.fn((token: string) => {
33-
if (token === 'invalid-token') {
34-
throw new InvalidTokenException();
35-
}
36-
return { sub: 1, provider: 'naver' };
37-
}),
27+
generateAccessToken: jest.fn(() => 'mockedAccessToken'),
28+
generateRefreshToken: jest.fn(() => 'mockedRefreshToken'),
29+
refreshAccessToken: jest.fn(() => 'mockedAccessToken'),
3830
},
3931
},
4032
],
41-
}).compile();
33+
})
34+
.overrideGuard(JwtAuthGuard)
35+
.useValue({
36+
canActivate: jest.fn(() => true),
37+
})
38+
.compile();
4239

4340
authController = module.get<AuthController>(AuthController);
44-
// authService = module.get<AuthService>(AuthService);
45-
jwtService = module.get<JwtService>(JwtService);
41+
authService = module.get<AuthService>(AuthService);
4642
});
4743

4844
it('컨트롤러 클래스가 정상적으로 인스턴스화된다.', () => {
@@ -61,47 +57,13 @@ describe('AuthController', () => {
6157
});
6258
});
6359

64-
// it('JWT 토큰이 유효가지 않은 경우 InvalidTokenException을 throw한다.', async () => {
65-
// const req = {
66-
// headers: { authorization: 'Bearer invalid-token' },
67-
// user: undefined,
68-
// } as any;
69-
// try {
70-
// await authController.getProfile(req);
71-
// } catch (error) {
72-
// expect(error).toBeInstanceOf(InvalidTokenException);
73-
// }
74-
// });
75-
76-
// it('JWT 토큰이 없는 경우 LoginRequiredException을 throw한다.', async () => {
77-
// const req = { headers: {}, user: undefined } as any;
78-
// try {
79-
// await authController.getProfile(req);
80-
// } catch (error) {
81-
// expect(error).toBeInstanceOf(LoginRequiredException);
82-
// }
83-
// });
84-
});
85-
86-
describe('refreshAccessToken', () => {
87-
it('refresh token이 유효한 경우 access token을 성공적으로 발급한다.', async () => {
88-
jest
89-
.spyOn(jwtService, 'verify')
90-
.mockReturnValue({ sub: 1, provider: 'naver' });
91-
const req = { body: { refreshToken: 'valid-refresh-token' } } as any;
92-
const res = {
93-
cookie: jest.fn(),
94-
json: jest.fn(),
95-
} as any;
96-
97-
await authController.refreshAccessToken(req, res);
98-
expect(res.cookie).toHaveBeenCalledWith('accessToken', 'test-token', {
99-
httpOnly: true,
100-
maxAge: 3600000,
101-
});
102-
expect(res.json).toHaveBeenCalledWith({
103-
message: '새로운 Access Token 발급 성공',
104-
});
60+
it('JWT 토큰이 없는 경우 예외를 던진다.', async () => {
61+
const req = {} as any;
62+
try {
63+
authController.getProfile(req);
64+
} catch (error) {
65+
expect(error).toBeInstanceOf(LoginRequiredException);
66+
}
10567
});
10668
});
10769
});

apps/backend/src/auth/auth.controller.ts

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import { Controller, Get, UseGuards, Req, Res, Post } from '@nestjs/common';
22
import { AuthGuard } from '@nestjs/passport';
33
import { AuthService } from './auth.service';
4-
import { JwtService } from '@nestjs/jwt';
54
import { JwtAuthGuard } from './guards/jwt-auth.guard';
65
import { Response } from 'express';
6+
import { MessageResponseDto } from './dtos/messageResponse.dto';
7+
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
8+
import { TokenService } from './token/token.service';
79

8-
const HOUR = 60 * 60 * 1000;
9-
const WEEK = 7 * 24 * 60 * 60 * 1000;
10+
export enum AuthResponseMessage {
11+
AUTH_LOGGED_OUT = '로그아웃하였습니다.',
12+
}
1013

1114
@Controller('auth')
1215
export class AuthController {
1316
constructor(
1417
private readonly authService: AuthService,
15-
private readonly jwtService: JwtService,
18+
private readonly tokenService: TokenService,
1619
) {}
1720

1821
@Get('naver')
@@ -27,17 +30,17 @@ export class AuthController {
2730
async naverCallback(@Req() req, @Res() res: Response) {
2831
// 네이버 인증 후 사용자 정보 반환
2932
const user = req.user;
30-
// TODO: 후에 권한 (workspace 조회, 편집 기능)도 payload에 추가
31-
const payload = { sub: user.id, provider: user.provider };
32-
const accessToken = this.jwtService.sign(payload, { expiresIn: '1h' });
33-
const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' });
33+
34+
// primary Key인 id 포함 payload 생성함
35+
// TODO: 여기서 권한 추가해야함
36+
const payload = { sub: user.id };
37+
const accessToken = this.tokenService.generateAccessToken(payload);
38+
const refreshToken = this.tokenService.generateRefreshToken(payload);
3439

3540
// 토큰을 쿠키에 담아서 메인 페이지로 리디렉션
36-
res.cookie('accessToken', accessToken, { httpOnly: true, maxAge: HOUR });
37-
res.cookie('refreshToken', refreshToken, {
38-
httpOnly: true,
39-
maxAge: WEEK,
40-
});
41+
this.tokenService.setAccessTokenCookie(res, accessToken);
42+
this.tokenService.setRefreshTokenCookie(res, refreshToken);
43+
4144
res.redirect(302, '/');
4245
}
4346

@@ -51,37 +54,31 @@ export class AuthController {
5154
@Get('kakao/callback')
5255
@UseGuards(AuthGuard('kakao'))
5356
async kakaoCallback(@Req() req, @Res() res: Response) {
54-
// 카카오 인증 후 사용자 정보 반환
57+
/// 카카오 인증 후 사용자 정보 반환
5558
const user = req.user;
56-
// TODO: 후에 권한 (workspace 조회, 편집 기능)도 payload에 추가
57-
const payload = { sub: user.id, provider: user.provider };
58-
const accessToken = this.jwtService.sign(payload, { expiresIn: '1h' });
59-
const refreshToken = this.jwtService.sign(payload, { expiresIn: '7d' });
59+
60+
// primary Key인 id 포함 payload 생성함
61+
// TODO: 여기서 권한 추가해야함
62+
const payload = { sub: user.id };
63+
const accessToken = this.tokenService.generateAccessToken(payload);
64+
const refreshToken = this.tokenService.generateRefreshToken(payload);
6065

6166
// 토큰을 쿠키에 담아서 메인 페이지로 리디렉션
62-
res.cookie('accessToken', accessToken, { httpOnly: true, maxAge: HOUR });
63-
res.cookie('refreshToken', refreshToken, {
64-
httpOnly: true,
65-
maxAge: WEEK,
66-
});
67+
this.tokenService.setAccessTokenCookie(res, accessToken);
68+
this.tokenService.setRefreshTokenCookie(res, refreshToken);
69+
6770
res.redirect(302, '/');
6871
}
6972

70-
@Post('refresh')
71-
async refreshAccessToken(@Req() req, @Res() res: Response) {
72-
const { refreshToken } = req.body;
73-
74-
const decoded = this.jwtService.verify(refreshToken, {
75-
secret: process.env.JWT_SECRET,
76-
});
77-
const payload = { sub: decoded.sub, provider: decoded.provider };
78-
const newAccessToken = this.jwtService.sign(payload, { expiresIn: '1h' });
79-
res.cookie('accessToken', newAccessToken, {
80-
httpOnly: true,
81-
maxAge: HOUR,
82-
});
83-
return res.json({
84-
message: '새로운 Access Token 발급 성공',
73+
@ApiResponse({ type: MessageResponseDto })
74+
@ApiOperation({ summary: '사용자가 로그아웃합니다.' })
75+
@Post('logout')
76+
@UseGuards(JwtAuthGuard) // JWT 인증 검사
77+
logout(@Res() res: Response) {
78+
// 쿠키 삭제 (옵션이 일치해야 삭제됨)
79+
this.tokenService.clearCookies(res);
80+
return res.status(200).json({
81+
message: AuthResponseMessage.AUTH_LOGGED_OUT,
8582
});
8683
}
8784

apps/backend/src/auth/auth.module.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,11 @@ import { AuthService } from './auth.service';
55
import { AuthController } from './auth.controller';
66
import { NaverStrategy } from './strategies/naver.strategy';
77
import { KakaoStrategy } from './strategies/kakao.strategy';
8-
import { JwtModule } from '@nestjs/jwt';
98
import { JwtAuthGuard } from './guards/jwt-auth.guard';
10-
import { ConfigModule, ConfigService } from '@nestjs/config';
9+
import { TokenModule } from './token/token.module';
1110

1211
@Module({
13-
imports: [
14-
UserModule,
15-
ConfigModule.forRoot({ isGlobal: true }),
16-
JwtModule.registerAsync({
17-
imports: [ConfigModule],
18-
inject: [ConfigService],
19-
useFactory: async (configService: ConfigService) => ({
20-
secret: configService.get<string>('JWT_SECRET'),
21-
}),
22-
}),
23-
],
12+
imports: [UserModule, TokenModule],
2413
controllers: [AuthController],
2514
providers: [
2615
AuthService,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Test, TestingModule } from '@nestjs/testing';
22
import { AuthService } from './auth.service';
33
import { UserRepository } from '../user/user.repository';
4-
import { SignUpDto } from './dto/signUp.dto';
4+
import { SignUpDto } from './dtos/signUp.dto';
55
import { User } from '../user/user.entity';
66

77
describe('AuthService', () => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Injectable } from '@nestjs/common';
22
import { UserRepository } from '../user/user.repository';
33
import { User } from '../user/user.entity';
4-
import { SignUpDto } from './dto/signUp.dto';
4+
import { SignUpDto } from './dtos/signUp.dto';
55

66
@Injectable()
77
export class AuthService {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsString } from 'class-validator';
3+
4+
export class MessageResponseDto {
5+
@ApiProperty({
6+
example: 'OO 생성에 성공했습니다.',
7+
description: 'api 요청 결과 메시지',
8+
})
9+
@IsString()
10+
message: string;
11+
}

0 commit comments

Comments
 (0)