Skip to content

Commit

Permalink
Merge pull request #64 from boostcampwm-2024/be/feature/logout
Browse files Browse the repository at this point in the history
[feat] 로그아웃 구현
  • Loading branch information
HBLEEEEE authored Nov 19, 2024
2 parents e13a946 + 0fcee6c commit 628fae2
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 15 deletions.
11 changes: 3 additions & 8 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ name: Deploy CI/CD 파이프라인
on:
push:
branches:
- deploy
- dev

jobs:
build:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -52,11 +53,5 @@ jobs:
echo "Removing existing container..."
docker rm myapp || true
echo "Writing .env file to apps/backend..."
mkdir -p apps/backend
cat <<EOF > apps/backend/.env
${{ secrets.ENV_FILE }}
EOF
echo "Running new container..."
docker run -d --name myapp -p 3000:3000 -p 8080:8080 ${{ secrets.DOCKER_HUB_USERNAME }}/myapp:latest
docker run -d --name myapp -p 3000:3000 -p 8080:8080 --env-file /root/src/config/.env ${{ secrets.DOCKER_HUB_USERNAME }}/myapp:latest
1 change: 1 addition & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@types/node": "^20.3.1",
"@types/passport": "^1.0.17",
"@types/passport-google-oauth20": "^2.0.16",
"@types/passport-kakao": "^1.0.3",
"@types/pg": "^8.11.10",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
Expand Down
13 changes: 13 additions & 0 deletions apps/backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { AuthGuard } from '@nestjs/passport';
import { GoogleLoginDto } from './dto/googleLogin.dto';
import { KakaoLoginDto } from './dto/kakaoLogin.dto';
import { oauthResponseDecorator } from './decorator/oauth.decorator';
import { JwtAuthGuard } from 'src/global/utils/jwtAuthGuard';
import { Request } from 'express';
import { logoutResponseDecorator } from './decorator/logout.decorator';

@Controller('api/auth')
export class AuthController {
Expand All @@ -31,6 +34,16 @@ export class AuthController {
return successhandler(successMessage.LOGIN_SUCCESS, tokens);
}

@Post('logout')
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: '로그아웃 API' })
@logoutResponseDecorator()
async logout(@Req() req: Request) {
const token = req.headers.authorization?.split(' ')[1];
await this.authService.logout(token);
return successhandler(successMessage.LOGOUT_SUCCESS);
}

@Get('google')
@UseGuards(AuthGuard('google'))
@ApiOperation({ summary: '구글 로그인 API' })
Expand Down
20 changes: 18 additions & 2 deletions apps/backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { DatabaseService } from 'src/database/database.service';
import { SignUpDto } from './dto/signUp.dto';
import * as bcrypt from 'bcrypt';
Expand All @@ -7,12 +7,14 @@ import { LoginDto } from './dto/login.dto';
import { JwtService } from '@nestjs/jwt';
import { GoogleLoginDto } from './dto/googleLogin.dto';
import { KakaoLoginDto } from './dto/kakaoLogin.dto';
import { RedisClientType } from 'redis';

@Injectable()
export class AuthService {
constructor(
private readonly databaseService: DatabaseService,
private readonly jwtService: JwtService
private readonly jwtService: JwtService,
@Inject('REDIS_CLIENT') private readonly redisClient: RedisClientType
) {}

async signUp(signUpDto: SignUpDto) {
Expand Down Expand Up @@ -97,4 +99,18 @@ export class AuthService {
const { email, nickname } = kakaoLoginDto;
return this.loginWithSocialMedia(email, nickname);
}

async logout(token: string | undefined) {
if (!token) throw new HttpException('토큰이 필요합니다.', HttpStatus.BAD_REQUEST);

const decodedToken = this.jwtService.decode(token) as { exp: number };
if (!decodedToken || !decodedToken.exp) {
throw new HttpException('유효하지 않은 토큰입니다.', HttpStatus.UNAUTHORIZED);
}

const remainingTime = decodedToken.exp * 1000 - Date.now();
if (remainingTime > 0) {
await this.redisClient.set(`blacklist:${token}`, 'true', { PX: remainingTime });
}
}
}
27 changes: 27 additions & 0 deletions apps/backend/src/auth/decorator/logout.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { applyDecorators } from '@nestjs/common';
import { ApiResponse } from '@nestjs/swagger';
import {
LogoutFailure400ResponseDto,
LogoutFailure401ResponseDto,
LogoutSuccessResponseDto
} from '../dto/logout.dto';

export function logoutResponseDecorator() {
return applyDecorators(
ApiResponse({
status: 200,
description: '로그아웃 성공',
type: LogoutSuccessResponseDto
}),
ApiResponse({
status: 400,
description: '토큰 없음',
type: LogoutFailure400ResponseDto
}),
ApiResponse({
status: 401,
description: '만료된 토큰',
type: LogoutFailure401ResponseDto
})
);
}
43 changes: 43 additions & 0 deletions apps/backend/src/auth/dto/logout.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ApiProperty } from '@nestjs/swagger';

export class LogoutSuccessResponseDto {
@ApiProperty({
description: '응답 코드',
example: 200
})
code: number;

@ApiProperty({
description: '응답 메세지',
example: '로그아웃 되었습니다.'
})
message: string;
}

export class LogoutFailure400ResponseDto {
@ApiProperty({
description: '응답 코드',
example: 400
})
code: number;

@ApiProperty({
description: '응답 메세지',
example: '토큰이 필요합니다.'
})
message: string;
}

export class LogoutFailure401ResponseDto {
@ApiProperty({
description: '응답 코드',
example: 401
})
code: number;

@ApiProperty({
description: '응답 메세지',
example: '유효하지 않은 토큰입니다.'
})
message: string;
}
1 change: 1 addition & 0 deletions apps/backend/src/global/successhandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ interface SuccessMessage {
export const successMessage = {
SIGNUP_SUCCESS: { code: 201, message: '회원 가입되었습니다.' },
LOGIN_SUCCESS: { code: 200, message: '로그인 되었습니다.' },
LOGOUT_SUCCESS: { code: 200, message: '로그아웃 되었습니다.' },
GET_MEMBER_SUCCESS: { code: 201, message: '회원 가입되었습니다.' },
GET_MAIL_SUCCESS: { code: 200, message: '메일 조회를 완료했습니다.' },
DELETE_MAIL_SUCCESS: { code: 200, message: '메일 삭제를 완료했습니다.' },
Expand Down
15 changes: 10 additions & 5 deletions apps/backend/src/global/utils/jwtAuthGuard.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { HttpException, HttpStatus } from '@nestjs/common';
import { RedisClientType } from 'redis';

@Injectable()
export class JwtAuthGuard {
constructor(private jwtService: JwtService) {}
constructor(
private jwtService: JwtService,
@Inject('REDIS_CLIENT') private readonly redisClient: RedisClientType
) {}

async canActivate(context: any) {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization?.split(' ')[1];

if (!token) {
throw new HttpException('허가되지 않은 사용자입니다.', HttpStatus.UNAUTHORIZED);
}
if (!token) throw new HttpException('허가되지 않은 사용자입니다.', HttpStatus.UNAUTHORIZED);

const isBlacklist = await this.redisClient.exists(`blacklist:${token}`);
if (isBlacklist) throw new HttpException('유효하지 않은 토큰입니다.', HttpStatus.UNAUTHORIZED);

try {
const decoded = await this.jwtService.verifyAsync(token);
Expand Down
14 changes: 14 additions & 0 deletions pnpm-lock.yaml

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

0 comments on commit 628fae2

Please sign in to comment.