Skip to content

Commit 11785cd

Browse files
[24.12.04 / TASK-54] Refactor: Token 검증 및 Velog API 호출 (#6)
* feature: database connection 추가 * refactor: db.comfig.ts -> db.config.ts 오타 수정 * feature: pre-commit 기능추가, 변경된 파일만 해당 * feature: add cors * modify: Type folder changed from camelCase to snake_case * refactor: changed TypeScript runtime executor from ts-node to tsx * feature: 간단한 토큰 검증 후 Velog API 호출 * modify: 오타 및 상대 경로 -> 절대 경로 Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * modify: user.controller.ts 절대 경로 -> 상대 경로 Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * refactor: Token 추출 로직 개선 Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * refactor: cors 설정 및 개발 환경변수 추가 * refactor: cors 설정 오탈자 수정 * refactor: src/app.ts 미들웨어 순서 조정 * featrue: 유저 저장 및 업데이트 * modify: .env.sample 수정 * modify: 에러 핸들링 next제거 express.d.ts 수정 * modify src/app.ts 에러핸들링 위치 변경 Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * modify: 에러 핸들링 미들웨어 * modify: 에러 핸들링 미들웨어 * feature: login 요쳥 시 token 인증 및 사용자 저장, 업데이트 * modify: query문 수정 Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * refactor: user controller 응답값 통일화 및 에러 미들웨어 수정 * modify: key.util 문서 오타 수정 * refactor: User Repo 에러 핸들링 * modify: repositories 오타 수정 * refactor: user service 에러 헨들링 * modify: 전체 파일 절대경로 -> 상대경로 * refactor: user service 리펙토링 * refactor: dto 검증 미들웨어 try문으로 변경 및 수정 * modify: 로그인 반환값 수정 * refactor: custom error * modify: db error status code Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * modify: import 순서 수정 * feature: log저장 * refactor: User Service encryptTokens Error * refactor: import model path * refactor: error handling * fix: 오탈자 수정 및 경로 수정 --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 9c5ffed commit 11785cd

28 files changed

+731
-143
lines changed

.env.sample

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ AES_KEY_6="81f92ab73c4e59d8a67f83b21d6e4c53"
1414
AES_KEY_7="d87b61e9f34a2c85f19a7e53c6d8f21a"
1515
AES_KEY_8="7c58f92ae1d3b67a4c29f8b36e17d4f9"
1616
AES_KEY_9="a93b4f7e2c6d81a7f5c3b2e89d47f612"
17-
ALLOWED_ORIGINS="http://localhost:8080,https://myapp.com"
17+
ALLOWED_ORIGINS="http://localhost:3000,https://myapp.com"
1818
NODE_ENV="development"

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,16 @@
2525
"license": "ISC",
2626
"dependencies": {
2727
"axios": "^1.7.8",
28+
"class-transformer": "^0.5.1",
29+
"class-validator": "^0.14.1",
2830
"cookie-parser": "^1.4.7",
2931
"cors": "^2.8.5",
3032
"dotenv": "^16.4.5",
3133
"express": "^4.21.1",
32-
"pg": "^8.13.1"
34+
"pg": "^8.13.1",
35+
"reflect-metadata": "^0.2.2",
36+
"winston": "^3.17.0",
37+
"winston-daily-rotate-file": "^5.0.0"
3338
},
3439
"devDependencies": {
3540
"@eslint/eslintrc": "^3.2.0",

pnpm-lock.yaml

Lines changed: 260 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import 'reflect-metadata';
12
import express, { Application } from 'express';
23
import dotenv from 'dotenv';
34
import cors from 'cors';
4-
import router from './routes/user.router';
55
import cookieParser from 'cookie-parser';
6+
import router from './routes/user.router';
7+
import { errorHandlingMiddleware } from './middlewares/error-handling.middleware';
8+
69
dotenv.config();
710

811
const app: Application = express();
@@ -12,15 +15,16 @@ app.use(express.json());
1215
app.use(express.urlencoded({ extended: true }));
1316
app.use(
1417
cors({
15-
origin: process.env.NODE_ENV === 'production' ? process.env.ALLOWED_ORIGINS?.split(',') : 'http://localhost:8080',
16-
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
18+
origin: process.env.NODE_ENV === 'production' ? process.env.ALLOWED_ORIGINS?.split(',') : 'http://localhost:3000',
19+
methods: ['GET', 'POST'],
1720
allowedHeaders: ['Content-Type', 'Authorization', 'Cookie'],
1821
credentials: true,
1922
}),
2023
);
21-
2224
app.use('/', router);
2325
app.get('/', (req, res) => {
2426
res.send('Hello, V.D.!');
2527
});
28+
app.use(errorHandlingMiddleware);
29+
2630
export default app;

src/configs/logger.config.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import fs from 'fs';
2+
import winston from 'winston';
3+
import winstonDaily from 'winston-daily-rotate-file';
4+
5+
const logDir = `${process.cwd()}/logs`;
6+
const errorLogDir = `${logDir}/error`;
7+
8+
if (!fs.existsSync(logDir)) {
9+
fs.mkdirSync(logDir);
10+
}
11+
if (!fs.existsSync(errorLogDir)) {
12+
fs.mkdirSync(errorLogDir);
13+
}
14+
15+
const logger = winston.createLogger({
16+
format: winston.format.combine(
17+
winston.format.timestamp({
18+
format: 'YYYY-MM-DD HH:mm:ss',
19+
}),
20+
winston.format.printf((info) => {
21+
return `${info.timestamp} ${info.level}: ${info.message}`;
22+
}),
23+
),
24+
transports: [
25+
new winston.transports.Console({
26+
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
27+
}),
28+
29+
new winstonDaily({
30+
level: 'debug',
31+
datePattern: 'YYYY-MM-DD',
32+
zippedArchive: true,
33+
filename: `%DATE%.log`,
34+
dirname: logDir,
35+
maxFiles: '7d',
36+
}),
37+
new winstonDaily({
38+
level: 'error',
39+
datePattern: 'YYYY-MM-DD',
40+
zippedArchive: true,
41+
filename: `%DATE%error.log`,
42+
dirname: errorLogDir,
43+
maxFiles: '7d',
44+
}),
45+
],
46+
});
47+
export default logger;

src/controllers/user.controller.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
1-
import { CustomRequest } from 'src/middlewares/auth.middleware';
1+
import { NextFunction, Request, Response, RequestHandler } from 'express';
2+
import logger from '../configs/logger.config';
3+
import { UserWithTokenDto } from '../types/dto/user-with-token.dto';
24
import { UserService } from '../services/user.service';
3-
import { Response } from 'express';
45

56
export class UserController {
67
constructor(private userService: UserService) {}
78

8-
velogApi = async (req: CustomRequest, res: Response): Promise<void> => {
9-
console.log(req.user);
10-
console.log(req.body);
11-
res.status(200).json({ data: req.user });
12-
};
9+
login = (async (req: Request, res: Response, next: NextFunction) => {
10+
try {
11+
const { id, email, profile } = req.user;
12+
const { accessToken, refreshToken } = req.tokens;
13+
14+
const userWithToken: UserWithTokenDto = { id, email, accessToken, refreshToken };
15+
const isExistUser = await this.userService.handleUserTokensByVelogUUID(userWithToken);
16+
return res.status(200).json({
17+
success: true,
18+
message: '로그인에 성공하였습니다.',
19+
data: { id: isExistUser.id, email: isExistUser.email, profile },
20+
});
21+
} catch (error) {
22+
logger.error('로그인 실패', error);
23+
next(error);
24+
}
25+
}) as RequestHandler;
1326
}

src/exception/custom.exception.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export class CustomError extends Error {
2+
code: string;
3+
statusCode?: number;
4+
constructor(message: string, code: string, statusCode?: number) {
5+
super(message);
6+
this.name = this.constructor.name;
7+
this.code = code;
8+
this.statusCode = statusCode;
9+
}
10+
}

src/exception/db.exception.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { CustomError } from './custom.exception';
2+
3+
export class DBError extends CustomError {
4+
constructor(message: string) {
5+
super(message, 'DB_ERROR', 500);
6+
}
7+
}

src/exception/token.exception.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { CustomError } from './custom.exception';
2+
export class TokenError extends CustomError {
3+
constructor(message: string) {
4+
super(message, 'TOKEN_ERROR', 401);
5+
}
6+
}

src/middlewares/auth.middleware.ts

Lines changed: 62 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,47 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
12
import { NextFunction, Request, Response } from 'express';
23
import axios from 'axios';
4+
import logger from '../configs/logger.config';
5+
import { TokenError } from '../exception/token.exception';
36

4-
interface VelogUser {
5-
id: string;
6-
username: string;
7-
email: string;
8-
profile: {
9-
id: string;
10-
thumbnail: string;
11-
display_name: string;
12-
short_bio: string;
13-
profile_links: Record<string, string>;
14-
};
15-
user_meta: {
16-
id: string;
17-
email_notification: boolean;
18-
email_promotion: boolean;
19-
};
20-
}
21-
22-
// Request에 user 프로퍼티를 추가하기 위한 타입 확장
23-
export interface CustomRequest extends Request {
24-
user?: VelogUser;
25-
}
26-
27-
/**
28-
* authmiddleware의 역할
29-
* 1. Velog API 호출 전 간단한 Bearer 검증
30-
* 2. Velog API 호출
31-
* 3. 무사히 응답이 되었다면 next로 엔드포인트 진입
32-
*/
7+
const VELOG_API_URL = 'https://v3.velog.io/graphql';
8+
const QUERIES = {
9+
LOGIN: `query currentUser {
10+
currentUser {
11+
id
12+
username
13+
email
14+
profile {
15+
thumbnail
16+
}
17+
}
18+
}`,
19+
};
3320

3421
/**
3522
* 요청에서 토큰을 추출하는 함수
36-
* @param {CustomRequest} req - req에 user 프로퍼티를 추가한 Express 객체
37-
* @returns {string | null, string | null} accessToken 및 refreshToken 객체 반환
38-
* @description 다음과 같은 순서로 토큰을 확인합니다
39-
* 1. 요청 본문 - 신규 유저인 경우
40-
* 2. 요청 헤더 - 기존 유저인 경우
41-
* 3. 요청 쿠키 - 기존 유저인 경우
23+
* @param req - Express Request 객체
24+
* @returns 추출된 토큰 객체
25+
* @description 다음 순서로 토큰을 확인합니다:
26+
* 1. 요청 본문 (req.body) - 신규 로그인
27+
* 2. 요청 헤더 - API 호출
28+
* 3. 쿠키 - 웹 클라이언트
4229
*/
43-
const extractTokens = (req: CustomRequest): { accessToken: string | undefined; refreshToken: string | undefined } => {
30+
const extractTokens = (req: Request): { accessToken: string; refreshToken: string } => {
4431
const accessToken = req.body.accessToken || req.headers['access_token'] || req.cookies['access_token'];
4532
const refreshToken = req.body.refreshToken || req.headers['refresh_token'] || req.cookies['refresh_token'];
4633

4734
return { accessToken, refreshToken };
4835
};
4936

5037
/**
51-
* Velog GraphQL API를 호출하여 액세스 토큰을 검증하는 함수
52-
* @param {string} accessToken - Velog Api 검증에 사용할 Access Token
53-
* @returns {Promise<VelogUser | null>} 검증 성공 시 사용자 데이터, 실패 시 null 반환
38+
* Velog API를 통해 사용자 정보를 조회합니다.
39+
* @param query - GraphQL 쿼리 문자열
40+
* @param accessToken - Velog access token
41+
* @throws {Error} API 호출 실패 시
42+
* @returns Promise<VelogUserLoginResponse | null>
5443
*/
55-
const verifyVelogToken = async (accessToken: string): Promise<VelogUser | null> => {
56-
// eslint-disable-next-line @typescript-eslint/naming-convention
57-
const VELOG_API_URL = 'https://v3.velog.io/graphql';
58-
const query = `
59-
query currentUser {
60-
currentUser {
61-
id
62-
username
63-
email
64-
profile {
65-
id
66-
thumbnail
67-
display_name
68-
short_bio
69-
profile_links
70-
}
71-
user_meta {
72-
id
73-
email_notification
74-
email_promotion
75-
}
76-
}
77-
}
78-
`;
79-
44+
const fetchVelogApi = async (query: string, accessToken: string) => {
8045
try {
8146
const response = await axios.post(
8247
VELOG_API_URL,
@@ -93,54 +58,50 @@ const verifyVelogToken = async (accessToken: string): Promise<VelogUser | null>
9358
const result = response.data;
9459

9560
if (result.errors) {
96-
console.error('GraphQL Errors:', result.errors);
61+
logger.error('GraphQL Errors:', result.errors);
9762
return null;
9863
}
9964

100-
return result.data?.currentUser || null;
65+
return result.data.currentUser || null;
10166
} catch (error) {
102-
console.error('Velog API 호출 중 오류:', error);
67+
logger.error('Velog API 호출 중 오류:', error);
10368
return null;
10469
}
10570
};
10671

10772
/**
108-
* Bearer 토큰을 검증하고 Velog 사용자를 인증하는 미들웨어
109-
* @param {CustomRequest} req - req에 user 프로퍼티를 추가한 Express 객체
110-
* @param {Response} res - 응답 객체
111-
* @param {NextFunction} next - 다음 미들 웨어 화출 함수
112-
* @returns {Promise<void>}
113-
* @description
114-
* 인증 처리 과정:
115-
* 1. 요청에서 액세스 토큰과 리프레시 토큰 추출
116-
* 2. Velog API를 통해 토큰 유효성 검증
117-
* 3. 검증 성공 시 요청 객체에 사용자 정보 첨부
118-
* 4. 토큰이 유효하지 않거나 누락된 경우 에러 응답 반환
119-
*
120-
* @throws {Error} 토큰 검증 실패 시 에러 발생
73+
* Bearer 토큰을 검증하고 Velog 사용자를 인증하는 함수
74+
* @param query - 사용자 정보를 조회할 GraphQL 쿼리
75+
* @returns
12176
*/
122-
export const verifyBearerTokens = async (req: CustomRequest, res: Response, next: NextFunction): Promise<void> => {
123-
try {
124-
// 1. 토큰 추출
125-
const { accessToken, refreshToken } = extractTokens(req);
126-
if (!accessToken || !refreshToken) {
127-
res.status(401).json({ message: 'access_token과 refresh_token은 필수값 입니다.' });
128-
return;
129-
}
77+
export const verifyBearerTokens = (query: string) => {
78+
return async (req: Request, res: Response, next: NextFunction) => {
79+
try {
80+
const { accessToken, refreshToken } = extractTokens(req);
13081

131-
// 2. Velog API를 통한 토큰 검증
132-
const velogUser = await verifyVelogToken(accessToken);
133-
if (!velogUser) {
134-
res.status(401).json({ message: '유효하지 않은 토큰입니다.' });
135-
return;
136-
}
82+
if (!accessToken || !refreshToken) {
83+
throw new TokenError('accessToken과 refreshToken의 입력이 올바르지 않습니다');
84+
}
13785

138-
// 3. 사용자 정보를 객체에 추가
139-
req.user = velogUser;
86+
const velogUser = await fetchVelogApi(query, accessToken);
14087

141-
next();
142-
} catch (error) {
143-
console.error('토큰 검증 중 오류 발생:', error);
144-
res.status(500).json({ message: '서버 오류로 인해 토큰 검증에 실패했습니다.' });
145-
}
88+
if (!velogUser) {
89+
throw new TokenError('유효하지 않은 토큰입니다.');
90+
}
91+
92+
req.user = velogUser;
93+
req.tokens = { accessToken, refreshToken };
94+
next();
95+
} catch (error) {
96+
logger.error('인증 처리중 오류가 발생하였습니다.', error);
97+
next(error);
98+
}
99+
};
100+
};
101+
/**
102+
* Velog 사용자 인증을 위한 미들웨어 모음
103+
* @property {Function} login - 사용자의 전체 정보를 조회하는 인증 미들웨어
104+
*/
105+
export const authMiddleware = {
106+
login: verifyBearerTokens(QUERIES.LOGIN),
146107
};

0 commit comments

Comments
 (0)