From 6d3b0ec877c4b41459c027135096aebb7d717c0e Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Mon, 21 Apr 2025 08:40:55 +0900 Subject: [PATCH 01/32] =?UTF-8?q?feat:=20qrcode=20app=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC,=20=EC=84=9C=EB=B9=84=EC=8A=A4,=20=EB=A0=88?= =?UTF-8?q?=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC,=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80,=20=EA=B4=80=EB=A0=A8=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 9 +++ src/controllers/qr.controller.ts | 54 ++++++++++++++ src/repositories/__test__/qr.repo.test.ts | 56 +++++++++++++++ src/repositories/qr.repository.ts | 36 ++++++++++ src/routes/index.ts | 3 + src/routes/qr.router.ts | 50 +++++++++++++ src/services/__test__/qr.service.test.ts | 83 ++++++++++++++++++++++ src/services/qr.service.ts | 17 +++++ src/types/dto/responses/qrResponse.type.ts | 7 ++ src/types/models/QRLoginToken.type.ts | 9 +++ 11 files changed, 325 insertions(+) create mode 100644 src/controllers/qr.controller.ts create mode 100644 src/repositories/__test__/qr.repo.test.ts create mode 100644 src/repositories/qr.repository.ts create mode 100644 src/routes/qr.router.ts create mode 100644 src/services/__test__/qr.service.test.ts create mode 100644 src/services/qr.service.ts create mode 100644 src/types/dto/responses/qrResponse.type.ts create mode 100644 src/types/models/QRLoginToken.type.ts diff --git a/package.json b/package.json index b18877e..335b829 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "reflect-metadata": "^0.2.2", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", + "uuid": "^11.1.0", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c181ba..9a5e55d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: swagger-ui-express: specifier: ^5.0.1 version: 5.0.1(express@4.21.1) + uuid: + specifier: ^11.1.0 + version: 11.1.0 winston: specifier: ^3.17.0 version: 3.17.0 @@ -2628,6 +2631,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -5509,6 +5516,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@11.1.0: {} + v8-compile-cache-lib@3.0.1: optional: true diff --git a/src/controllers/qr.controller.ts b/src/controllers/qr.controller.ts new file mode 100644 index 0000000..4a09e71 --- /dev/null +++ b/src/controllers/qr.controller.ts @@ -0,0 +1,54 @@ +import { NextFunction, Request, RequestHandler, Response } from 'express'; +import logger from '@/configs/logger.config'; +import { QRLoginTokenService } from "@/services/qr.service"; +import { QRLoginTokenResponseDto } from "@/types/dto/responses/qrResponse.type"; + +export class QRLoginController { + constructor(private qrService: QRLoginTokenService) {} + + createToken: RequestHandler = async ( + req: Request, + res: Response, + next: NextFunction, + ) => { + try { + const user = req.user; + const ip = req.ip ?? ''; + const userAgent = req.headers['user-agent'] || ''; + + const token = await this.qrService.create(user.id, ip, userAgent); + + const response = new QRLoginTokenResponseDto( + true, + 'QR 토큰 생성 완료', + { token: token }, + null + ); + res.status(200).json(response); + } catch (error) { + logger.error('생성 실패:', error); + next(error); + } + }; + + getToken: RequestHandler = async (req, res, next) => { + try { + const token = req.query.token as string; + + if (!token) { + res.status(400).json({ success: false, message: '토큰이 필요합니다.' }); + } + + const found = await this.qrService.getByToken(token); + + if (!found) { + res.status(404).json({ success: false, message: '유효하지 않거나 만료된 토큰입니다.' }); + } + + res.status(200).json({ success: true, message: '유효한 QR 토큰입니다.', token: found }); + } catch (error) { + logger.error('QR 토큰 조회 실패', error); + next(error); + } + }; +} \ No newline at end of file diff --git a/src/repositories/__test__/qr.repo.test.ts b/src/repositories/__test__/qr.repo.test.ts new file mode 100644 index 0000000..75cfc59 --- /dev/null +++ b/src/repositories/__test__/qr.repo.test.ts @@ -0,0 +1,56 @@ +import { QRLoginTokenRepository } from '@/repositories/qr.repository'; +import { DBError } from '@/exception'; +import { Pool } from 'pg'; + +const mockPool: Partial = { + query: jest.fn() +}; + +describe('QRLoginTokenRepository', () => { + let repo: QRLoginTokenRepository; + + beforeEach(() => { + repo = new QRLoginTokenRepository(mockPool as Pool); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should insert QR login token', async () => { + (mockPool.query as jest.Mock).mockResolvedValueOnce(undefined); + + await expect( + repo.createQRLoginToken('token', 1, 'ip', 'agent') + ).resolves.not.toThrow(); + + expect(mockPool.query).toHaveBeenCalled(); + }); + + it('should throw DBError on insert failure', async () => { + (mockPool.query as jest.Mock).mockRejectedValueOnce(new Error('fail')); + + await expect(repo.createQRLoginToken('token', 1, 'ip', 'agent')) + .rejects.toThrow(DBError); + }); + + it('should return token if found', async () => { + (mockPool.query as jest.Mock).mockResolvedValueOnce({ rows: [{ token: 'token' }] }); + + const result = await repo.findQRLoginToken('token'); + expect(result).toEqual({ token: 'token' }); + }); + + it('should return null if token not found', async () => { + (mockPool.query as jest.Mock).mockResolvedValueOnce({ rows: [] }); + + const result = await repo.findQRLoginToken('token'); + expect(result).toBeNull(); + }); + + it('should throw DBError on select failure', async () => { + (mockPool.query as jest.Mock).mockRejectedValueOnce(new Error('fail')); + + await expect(repo.findQRLoginToken('token')).rejects.toThrow(DBError); + }); +}); \ No newline at end of file diff --git a/src/repositories/qr.repository.ts b/src/repositories/qr.repository.ts new file mode 100644 index 0000000..e7a9db7 --- /dev/null +++ b/src/repositories/qr.repository.ts @@ -0,0 +1,36 @@ +import { Pool } from 'pg'; +import logger from '@/configs/logger.config'; +import { DBError } from '@/exception'; +import { QRLoginToken } from '@/types/models/QRLoginToken.type'; + +export class QRLoginTokenRepository { + constructor(private pool: Pool) { } + + async createQRLoginToken(token: String, userId: number, ip: string, userAgent: string): Promise { + try { + const query = ` + INSERT INTO qr_login_tokens (token, user_id, created_at, expires_at, is_used, ip_address, user_agent) + VALUES ($1, $2, NOW(), NOW() + INTERVAL '5 minutes', false, $3, $4); + `; + await this.pool.query(query, [token, userId, ip, userAgent]); + } catch (error) { + logger.error('QRLoginToken Repo Create Error : ', error); + throw new DBError('QR 코드 토큰 생성 중 문제가 발생했습니다.'); + } + } + + async findQRLoginToken(token: string): Promise { + try { + const query = ` + SELECT * + FROM qr_login_tokens + WHERE token = $1 AND is_used = false AND expires_at > NOW(); + `; + const result = await this.pool.query(query, [token]); + return result.rows[0] ?? null; + } catch (error) { + logger.error('QRLoginToken Repo find QR Code Error : ', error); + throw new DBError('QR 코드 토큰 조회 중 문제가 발생했습니다.'); + } + } +} \ No newline at end of file diff --git a/src/routes/index.ts b/src/routes/index.ts index 351fecc..30251d2 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -2,6 +2,7 @@ import express, { Router } from 'express'; import UserRouter from './user.router'; import PostRouter from './post.router'; import NotiRouter from './noti.router'; +import QrRouter from './qr.router'; const router: Router = express.Router(); @@ -12,4 +13,6 @@ router.use('/ping', (req, res) => { router.use('/', UserRouter); router.use('/', PostRouter); router.use('/', NotiRouter); +router.use('/', QrRouter); + export default router; diff --git a/src/routes/qr.router.ts b/src/routes/qr.router.ts new file mode 100644 index 0000000..56cc91e --- /dev/null +++ b/src/routes/qr.router.ts @@ -0,0 +1,50 @@ +import express, { Router } from 'express'; +import pool from '@/configs/db.config'; + +import { authMiddleware } from '@/middlewares/auth.middleware'; +import { QRLoginTokenRepository } from '@/repositories/qr.repository'; +import { QRLoginTokenService } from '@/services/qr.service'; +import { QRLoginController } from '@/controllers/qr.controller'; + +const router: Router = express.Router(); + +const qrRepository = new QRLoginTokenRepository(pool); +const qrService = new QRLoginTokenService(qrRepository); +const qrController = new QRLoginController(qrService); + +/** + * @swagger + * /api/qr-login: + * post: + * summary: QR 로그인 토큰 생성 + * tags: [QRLogin] + * security: + * - bearerAuth: [] + * responses: + * 201: + * description: QR 로그인 토큰 생성 성공 + */ +router.post('/qr-login', authMiddleware.login, qrController.createToken); + +/** + * @swagger + * /api/qr-login: + * get: + * summary: QR 로그인 토큰 조회 + * tags: [QRLogin] + * parameters: + * - in: query + * name: token + * required: true + * schema: + * type: string + * description: 조회할 QR 토큰 + * responses: + * 200: + * description: 유효한 토큰 + * 404: + * description: 토큰 없음 or 만료 + */ +router.get('/qr-login', qrController.getToken); + +export default router; diff --git a/src/services/__test__/qr.service.test.ts b/src/services/__test__/qr.service.test.ts new file mode 100644 index 0000000..bea062b --- /dev/null +++ b/src/services/__test__/qr.service.test.ts @@ -0,0 +1,83 @@ +import { QRLoginTokenService } from '@/services/qr.service'; +import { QRLoginTokenRepository } from '@/repositories/qr.repository'; +import { DBError } from '@/exception'; +import { QRLoginToken } from '@/types/models/QRLoginToken.type'; + +jest.mock('@/repositories/qr.repository'); + +describe('QRLoginTokenService', () => { + let service: QRLoginTokenService; + let repo: jest.Mocked; + + beforeEach(() => { + const repoInstance = new QRLoginTokenRepository({} as any) + repo = repoInstance as jest.Mocked; + service = new QRLoginTokenService(repo); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('create', () => { + it('QR 토큰을 생성하고 반환해야 한다', async () => { + const userId = 1; + const ip = '127.0.0.1'; + const userAgent = 'Chrome'; + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + + const token = await service.create(userId, ip, userAgent); + + expect(token).toMatch(uuidRegex); + expect(repo.createQRLoginToken).toHaveBeenCalledWith(token, userId, ip, userAgent); + }); + + it('QR 토큰 생성 중 오류 발생 시 예외 발생', async () => { + const userId = 1; + const ip = '127.0.0.1'; + const userAgent = 'Mozilla'; + repo.createQRLoginToken.mockRejectedValueOnce(new DBError('생성 실패')); + + await expect(service.create(userId, ip, userAgent)).rejects.toThrow('생성 실패'); + expect(repo.createQRLoginToken).toHaveBeenCalled(); + }); + }); + + describe('getByToken', () => { + const mockToken = 'sample-token'; + const mockQRToken: QRLoginToken = { + token: mockToken, + user: 1, + created_at: new Date(), + expires_at: new Date(Date.now() + 1000 * 60 * 5), + is_used: false, + ip_address: '127.0.0.1', + user_agent: 'Chrome', + }; + + it('유효한 토큰 조회 시 QRLoginToken 반환', async () => { + repo.findQRLoginToken.mockResolvedValue(mockQRToken); + + const result = await service.getByToken(mockToken); + + expect(result).toEqual(mockQRToken); + expect(repo.findQRLoginToken).toHaveBeenCalledWith(mockToken); + }); + + it('토큰이 없을 경우 null 반환', async () => { + repo.findQRLoginToken.mockResolvedValue(null); + + const result = await service.getByToken(mockToken); + + expect(result).toBeNull(); + expect(repo.findQRLoginToken).toHaveBeenCalledWith(mockToken); + }); + + it('토큰 조회 중 오류 발생 시 예외 발생', async () => { + repo.findQRLoginToken.mockRejectedValueOnce(new DBError('조회 실패')); + + await expect(service.getByToken(mockToken)).rejects.toThrow('조회 실패'); + expect(repo.findQRLoginToken).toHaveBeenCalledWith(mockToken); + }); + }); +}); diff --git a/src/services/qr.service.ts b/src/services/qr.service.ts new file mode 100644 index 0000000..e59ddf1 --- /dev/null +++ b/src/services/qr.service.ts @@ -0,0 +1,17 @@ +import { QRLoginTokenRepository } from "@/repositories/qr.repository"; +import { QRLoginToken } from "@/types/models/QRLoginToken.type"; +import { randomUUID } from "crypto"; + +export class QRLoginTokenService { + constructor(private qrRepo: QRLoginTokenRepository) {} + + async create(userId: number, ip: string, userAgent: string): Promise { + const token = randomUUID(); + await this.qrRepo.createQRLoginToken(token, userId, ip, userAgent); + return token; + } + + async getByToken(token: string): Promise { + return await this.qrRepo.findQRLoginToken(token); + } +} \ No newline at end of file diff --git a/src/types/dto/responses/qrResponse.type.ts b/src/types/dto/responses/qrResponse.type.ts new file mode 100644 index 0000000..17aa632 --- /dev/null +++ b/src/types/dto/responses/qrResponse.type.ts @@ -0,0 +1,7 @@ +import { BaseResponseDto } from '@/types/dto/responses/baseResponse.type'; + +interface QRLoginTokenResponseData { + token: string; + } + +export class QRLoginTokenResponseDto extends BaseResponseDto { } diff --git a/src/types/models/QRLoginToken.type.ts b/src/types/models/QRLoginToken.type.ts new file mode 100644 index 0000000..7b9eb2b --- /dev/null +++ b/src/types/models/QRLoginToken.type.ts @@ -0,0 +1,9 @@ +export interface QRLoginToken { + token: string; + user: number; + created_at: Date; + expires_at: Date; + is_used: boolean; + ip_address: string; + user_agent: string; +} \ No newline at end of file From 3b6adaab1263d7278a40941a048336fc4f5e634d Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Mon, 21 Apr 2025 08:56:07 +0900 Subject: [PATCH 02/32] =?UTF-8?q?hotfix:=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__test__/qr.repo.integration.test.ts | 77 +++++++++++++++++++ src/repositories/qr.repository.ts | 2 +- src/routes/qr.router.ts | 2 +- .../dto/requests/createQrRequest.type.ts | 20 +++++ 4 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 src/repositories/__test__/qr.repo.integration.test.ts create mode 100644 src/types/dto/requests/createQrRequest.type.ts diff --git a/src/repositories/__test__/qr.repo.integration.test.ts b/src/repositories/__test__/qr.repo.integration.test.ts new file mode 100644 index 0000000..238aa2b --- /dev/null +++ b/src/repositories/__test__/qr.repo.integration.test.ts @@ -0,0 +1,77 @@ +import dotenv from 'dotenv'; +import { Pool } from 'pg'; +import pg from 'pg'; +import { QRLoginTokenRepository } from '@/repositories/qr.repository'; +import { v4 as uuidv4 } from 'uuid'; + +dotenv.config(); +jest.setTimeout(30000); + +describe('QRLoginTokenRepository Integration Test', () => { + let pool: Pool; + let repo: QRLoginTokenRepository; + + beforeAll(async () => { + try { + const testPoolConfig: pg.PoolConfig = { + user: process.env.POSTGRES_USER, + host: process.env.POSTGRES_HOST, + database: process.env.DATABASE_NAME, + password: process.env.POSTGRES_PASSWORD, + port: Number(process.env.POSTGRES_PORT), + max: 1, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 5000, + statement_timeout: 30000, + }; + + if (process.env.POSTGRES_HOST !== 'localhost') { + testPoolConfig.ssl = { + rejectUnauthorized: false, + }; + } + + pool = new Pool(testPoolConfig); + await pool.query('SELECT 1'); // 연결 확인 + console.info('QR 테스트 DB 연결 성공'); + + // 필요한 테이블 및 유저 존재 확인 (선택 사항) + const tableCheck = await pool.query(`SELECT to_regclass('qr_login_tokens')`); + if (!tableCheck.rows[0].to_regclass) { + throw new Error('qr_login_tokens 테이블이 존재하지 않습니다.'); + } + + const userCheck = await pool.query(`SELECT COUNT(*) FROM users WHERE id = $1`, [1]); + if (parseInt(userCheck.rows[0].count) === 0) { + throw new Error('user_id = 1 유저가 존재하지 않습니다.'); + } + + repo = new QRLoginTokenRepository(pool); + } catch (error) { + console.error('QR 테스트 설정 중 오류:', error); + throw error; + } + }); + + afterAll(async () => { + await pool.end(); + }); + + it('should insert and retrieve QR token', async () => { + const token = uuidv4(); + const userId = 1; + const ip = '127.0.0.1'; + const userAgent = 'test-agent'; + + await repo.createQRLoginToken(token, userId, ip, userAgent); + const result = await repo.findQRLoginToken(token); + + expect(result).not.toBeNull(); + expect(result?.token).toBe(token); + }); + + it('should return null for expired or used token', async () => { + const result = await repo.findQRLoginToken('invalid-token'); + expect(result).toBeNull(); + }); +}); \ No newline at end of file diff --git a/src/repositories/qr.repository.ts b/src/repositories/qr.repository.ts index e7a9db7..16500a6 100644 --- a/src/repositories/qr.repository.ts +++ b/src/repositories/qr.repository.ts @@ -6,7 +6,7 @@ import { QRLoginToken } from '@/types/models/QRLoginToken.type'; export class QRLoginTokenRepository { constructor(private pool: Pool) { } - async createQRLoginToken(token: String, userId: number, ip: string, userAgent: string): Promise { + async createQRLoginToken(token: string, userId: number, ip: string, userAgent: string): Promise { try { const query = ` INSERT INTO qr_login_tokens (token, user_id, created_at, expires_at, is_used, ip_address, user_agent) diff --git a/src/routes/qr.router.ts b/src/routes/qr.router.ts index 56cc91e..7f3a57f 100644 --- a/src/routes/qr.router.ts +++ b/src/routes/qr.router.ts @@ -21,7 +21,7 @@ const qrController = new QRLoginController(qrService); * security: * - bearerAuth: [] * responses: - * 201: + * 200: * description: QR 로그인 토큰 생성 성공 */ router.post('/qr-login', authMiddleware.login, qrController.createToken); diff --git a/src/types/dto/requests/createQrRequest.type.ts b/src/types/dto/requests/createQrRequest.type.ts new file mode 100644 index 0000000..3819fd6 --- /dev/null +++ b/src/types/dto/requests/createQrRequest.type.ts @@ -0,0 +1,20 @@ +// // src/types/dto/requests/qrLoginRequest.dto.ts +// import { IsString, IsNotEmpty } from 'class-validator'; + +// /** +// * @swagger +// * components: +// * schemas: +// * QRLoginRequestDto: +// * type: object +// * required: +// * - userId +// * properties: +// * userId: +// * type: integer +// * description: 로그인 요청한 사용자 ID +// */ +// export class QRLoginRequestDto { +// @IsNotEmpty() +// userId: number; +// } From b70a582ddea9b1d435615e3c88d268fbd868d1b2 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Mon, 21 Apr 2025 09:00:14 +0900 Subject: [PATCH 03/32] =?UTF-8?q?modify:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__test__/qr.repo.integration.test.ts | 77 ------------------- .../dto/requests/createQrRequest.type.ts | 20 ----- 2 files changed, 97 deletions(-) delete mode 100644 src/repositories/__test__/qr.repo.integration.test.ts delete mode 100644 src/types/dto/requests/createQrRequest.type.ts diff --git a/src/repositories/__test__/qr.repo.integration.test.ts b/src/repositories/__test__/qr.repo.integration.test.ts deleted file mode 100644 index 238aa2b..0000000 --- a/src/repositories/__test__/qr.repo.integration.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import dotenv from 'dotenv'; -import { Pool } from 'pg'; -import pg from 'pg'; -import { QRLoginTokenRepository } from '@/repositories/qr.repository'; -import { v4 as uuidv4 } from 'uuid'; - -dotenv.config(); -jest.setTimeout(30000); - -describe('QRLoginTokenRepository Integration Test', () => { - let pool: Pool; - let repo: QRLoginTokenRepository; - - beforeAll(async () => { - try { - const testPoolConfig: pg.PoolConfig = { - user: process.env.POSTGRES_USER, - host: process.env.POSTGRES_HOST, - database: process.env.DATABASE_NAME, - password: process.env.POSTGRES_PASSWORD, - port: Number(process.env.POSTGRES_PORT), - max: 1, - idleTimeoutMillis: 30000, - connectionTimeoutMillis: 5000, - statement_timeout: 30000, - }; - - if (process.env.POSTGRES_HOST !== 'localhost') { - testPoolConfig.ssl = { - rejectUnauthorized: false, - }; - } - - pool = new Pool(testPoolConfig); - await pool.query('SELECT 1'); // 연결 확인 - console.info('QR 테스트 DB 연결 성공'); - - // 필요한 테이블 및 유저 존재 확인 (선택 사항) - const tableCheck = await pool.query(`SELECT to_regclass('qr_login_tokens')`); - if (!tableCheck.rows[0].to_regclass) { - throw new Error('qr_login_tokens 테이블이 존재하지 않습니다.'); - } - - const userCheck = await pool.query(`SELECT COUNT(*) FROM users WHERE id = $1`, [1]); - if (parseInt(userCheck.rows[0].count) === 0) { - throw new Error('user_id = 1 유저가 존재하지 않습니다.'); - } - - repo = new QRLoginTokenRepository(pool); - } catch (error) { - console.error('QR 테스트 설정 중 오류:', error); - throw error; - } - }); - - afterAll(async () => { - await pool.end(); - }); - - it('should insert and retrieve QR token', async () => { - const token = uuidv4(); - const userId = 1; - const ip = '127.0.0.1'; - const userAgent = 'test-agent'; - - await repo.createQRLoginToken(token, userId, ip, userAgent); - const result = await repo.findQRLoginToken(token); - - expect(result).not.toBeNull(); - expect(result?.token).toBe(token); - }); - - it('should return null for expired or used token', async () => { - const result = await repo.findQRLoginToken('invalid-token'); - expect(result).toBeNull(); - }); -}); \ No newline at end of file diff --git a/src/types/dto/requests/createQrRequest.type.ts b/src/types/dto/requests/createQrRequest.type.ts deleted file mode 100644 index 3819fd6..0000000 --- a/src/types/dto/requests/createQrRequest.type.ts +++ /dev/null @@ -1,20 +0,0 @@ -// // src/types/dto/requests/qrLoginRequest.dto.ts -// import { IsString, IsNotEmpty } from 'class-validator'; - -// /** -// * @swagger -// * components: -// * schemas: -// * QRLoginRequestDto: -// * type: object -// * required: -// * - userId -// * properties: -// * userId: -// * type: integer -// * description: 로그인 요청한 사용자 ID -// */ -// export class QRLoginRequestDto { -// @IsNotEmpty() -// userId: number; -// } From 56c2283e2890c38d0d85a671cf8ebc473b0fc07b Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Fri, 25 Apr 2025 19:32:19 +0900 Subject: [PATCH 04/32] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 335b829..b18877e 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "reflect-metadata": "^0.2.2", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", - "uuid": "^11.1.0", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" }, From ac396198b96928d9879749e809ca9c5acca1d2af Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Fri, 25 Apr 2025 19:32:32 +0900 Subject: [PATCH 05/32] =?UTF-8?q?refactor:=20=EA=B3=B5=EB=B0=B1=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/qr.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/qr.service.ts b/src/services/qr.service.ts index e59ddf1..3fdbce5 100644 --- a/src/services/qr.service.ts +++ b/src/services/qr.service.ts @@ -6,7 +6,7 @@ export class QRLoginTokenService { constructor(private qrRepo: QRLoginTokenRepository) {} async create(userId: number, ip: string, userAgent: string): Promise { - const token = randomUUID(); + const token = randomUUID(); await this.qrRepo.createQRLoginToken(token, userId, ip, userAgent); return token; } @@ -14,4 +14,4 @@ export class QRLoginTokenService { async getByToken(token: string): Promise { return await this.qrRepo.findQRLoginToken(token); } -} \ No newline at end of file +} From d118b1610518300a6663542fe4cff25fa3fadfd3 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Fri, 25 Apr 2025 19:33:28 +0900 Subject: [PATCH 06/32] =?UTF-8?q?refactor:=20=ED=99=9C=EC=9A=A9=EB=8F=84?= =?UTF-8?q?=20=EB=82=AE=EC=9D=80=20=ED=95=84=EB=93=9C=20=EC=88=9C=EC=84=9C?= =?UTF-8?q?=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/types/models/QRLoginToken.type.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/types/models/QRLoginToken.type.ts b/src/types/models/QRLoginToken.type.ts index 7b9eb2b..1e3930b 100644 --- a/src/types/models/QRLoginToken.type.ts +++ b/src/types/models/QRLoginToken.type.ts @@ -1,9 +1,9 @@ export interface QRLoginToken { token: string; user: number; - created_at: Date; - expires_at: Date; is_used: boolean; ip_address: string; user_agent: string; -} \ No newline at end of file + created_at: Date; + expires_at: Date; +} From f6f39360705c5fa37cf54b78e35bf1dbfdc19dda Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Fri, 25 Apr 2025 19:34:39 +0900 Subject: [PATCH 07/32] =?UTF-8?q?modify:=20token=20=EA=B3=A0=EC=A0=95=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=B4=20=EA=B0=92=20=EB=AA=85=EC=8B=9C=20&=20excep?= =?UTF-8?q?tion=20=EC=9E=AC=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/qr.controller.ts | 14 ++++++++++---- src/types/dto/responses/qrResponse.type.ts | 12 +++++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/controllers/qr.controller.ts b/src/controllers/qr.controller.ts index 4a09e71..84c0112 100644 --- a/src/controllers/qr.controller.ts +++ b/src/controllers/qr.controller.ts @@ -2,6 +2,10 @@ import { NextFunction, Request, RequestHandler, Response } from 'express'; import logger from '@/configs/logger.config'; import { QRLoginTokenService } from "@/services/qr.service"; import { QRLoginTokenResponseDto } from "@/types/dto/responses/qrResponse.type"; +import { InvalidTokenError, TokenExpiredError } from '@/exception/token.exception'; + +// TODO: randomUUID() 기반으로 길이 36 +type Token32 = string & { __lengthBrand: 32 }; export class QRLoginController { constructor(private qrService: QRLoginTokenService) {} @@ -18,10 +22,12 @@ export class QRLoginController { const token = await this.qrService.create(user.id, ip, userAgent); + const typedToken = token as Token32; + const response = new QRLoginTokenResponseDto( true, 'QR 토큰 생성 완료', - { token: token }, + { token: typedToken }, null ); res.status(200).json(response); @@ -36,13 +42,13 @@ export class QRLoginController { const token = req.query.token as string; if (!token) { - res.status(400).json({ success: false, message: '토큰이 필요합니다.' }); + throw new InvalidTokenError('토큰이 필요합니다.'); } const found = await this.qrService.getByToken(token); if (!found) { - res.status(404).json({ success: false, message: '유효하지 않거나 만료된 토큰입니다.' }); + throw new TokenExpiredError(); } res.status(200).json({ success: true, message: '유효한 QR 토큰입니다.', token: found }); @@ -51,4 +57,4 @@ export class QRLoginController { next(error); } }; -} \ No newline at end of file +} diff --git a/src/types/dto/responses/qrResponse.type.ts b/src/types/dto/responses/qrResponse.type.ts index 17aa632..3c3fe26 100644 --- a/src/types/dto/responses/qrResponse.type.ts +++ b/src/types/dto/responses/qrResponse.type.ts @@ -1,7 +1,9 @@ import { BaseResponseDto } from '@/types/dto/responses/baseResponse.type'; -interface QRLoginTokenResponseData { - token: string; - } - -export class QRLoginTokenResponseDto extends BaseResponseDto { } +type Token32 = string & { __lengthBrand: 32 }; + +export interface QRLoginTokenResponseData { + token: Token32; +} + +export class QRLoginTokenResponseDto extends BaseResponseDto {} From 065879cc657cd46a79016740da25fa746ed6deb2 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Fri, 25 Apr 2025 19:35:09 +0900 Subject: [PATCH 08/32] =?UTF-8?q?refactor:=20repository=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/repositories/__test__/qr.repo.test.ts | 58 +++++++++++++---------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/src/repositories/__test__/qr.repo.test.ts b/src/repositories/__test__/qr.repo.test.ts index 75cfc59..6b945a1 100644 --- a/src/repositories/__test__/qr.repo.test.ts +++ b/src/repositories/__test__/qr.repo.test.ts @@ -3,7 +3,7 @@ import { DBError } from '@/exception'; import { Pool } from 'pg'; const mockPool: Partial = { - query: jest.fn() + query: jest.fn(), }; describe('QRLoginTokenRepository', () => { @@ -17,40 +17,46 @@ describe('QRLoginTokenRepository', () => { jest.clearAllMocks(); }); - it('should insert QR login token', async () => { - (mockPool.query as jest.Mock).mockResolvedValueOnce(undefined); + describe('createQRLoginToken', () => { + it('QR 토큰을 성공적으로 삽입해야 한다', async () => { + (mockPool.query as jest.Mock).mockResolvedValueOnce(undefined); - await expect( - repo.createQRLoginToken('token', 1, 'ip', 'agent') - ).resolves.not.toThrow(); + await expect( + repo.createQRLoginToken('token', 1, 'ip', 'agent') + ).resolves.not.toThrow(); - expect(mockPool.query).toHaveBeenCalled(); - }); + expect(mockPool.query).toHaveBeenCalled(); + }); - it('should throw DBError on insert failure', async () => { - (mockPool.query as jest.Mock).mockRejectedValueOnce(new Error('fail')); + it('삽입 중 오류 발생 시 DBError를 던져야 한다', async () => { + (mockPool.query as jest.Mock).mockRejectedValueOnce(new Error('fail')); - await expect(repo.createQRLoginToken('token', 1, 'ip', 'agent')) - .rejects.toThrow(DBError); + await expect( + repo.createQRLoginToken('token', 1, 'ip', 'agent') + ).rejects.toThrow(DBError); + }); }); - it('should return token if found', async () => { - (mockPool.query as jest.Mock).mockResolvedValueOnce({ rows: [{ token: 'token' }] }); + describe('findQRLoginToken', () => { + it('토큰이 존재할 경우 반환해야 한다', async () => { + const mockTokenData = { token: 'token', user: 1 }; + (mockPool.query as jest.Mock).mockResolvedValueOnce({ rows: [mockTokenData] }); - const result = await repo.findQRLoginToken('token'); - expect(result).toEqual({ token: 'token' }); - }); + const result = await repo.findQRLoginToken('token'); + expect(result).toEqual(mockTokenData); + }); - it('should return null if token not found', async () => { - (mockPool.query as jest.Mock).mockResolvedValueOnce({ rows: [] }); + it('토큰이 존재하지 않으면 null을 반환해야 한다', async () => { + (mockPool.query as jest.Mock).mockResolvedValueOnce({ rows: [] }); - const result = await repo.findQRLoginToken('token'); - expect(result).toBeNull(); - }); + const result = await repo.findQRLoginToken('token'); + expect(result).toBeNull(); + }); - it('should throw DBError on select failure', async () => { - (mockPool.query as jest.Mock).mockRejectedValueOnce(new Error('fail')); + it('조회 중 오류 발생 시 DBError를 던져야 한다', async () => { + (mockPool.query as jest.Mock).mockRejectedValueOnce(new Error('fail')); - await expect(repo.findQRLoginToken('token')).rejects.toThrow(DBError); + await expect(repo.findQRLoginToken('token')).rejects.toThrow(DBError); + }); }); -}); \ No newline at end of file +}); From 3faf17344f096dd4e7ae4939374c7d93ca5c5f52 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Fri, 25 Apr 2025 19:42:14 +0900 Subject: [PATCH 09/32] =?UTF-8?q?hotfix:=20uuid=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pnpm-lock.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a5e55d..9c181ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,9 +41,6 @@ importers: swagger-ui-express: specifier: ^5.0.1 version: 5.0.1(express@4.21.1) - uuid: - specifier: ^11.1.0 - version: 11.1.0 winston: specifier: ^3.17.0 version: 3.17.0 @@ -2631,10 +2628,6 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} - uuid@11.1.0: - resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} - hasBin: true - v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -5516,8 +5509,6 @@ snapshots: utils-merge@1.0.1: {} - uuid@11.1.0: {} - v8-compile-cache-lib@3.0.1: optional: true From a0078704ecd2d1538fb4580b9983fe6aea9d8636 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Fri, 25 Apr 2025 20:13:46 +0900 Subject: [PATCH 10/32] =?UTF-8?q?hotfix:=20=EB=B9=84=EC=A6=88=EB=8B=88?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=ED=9D=90=EB=A6=84=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 비즈니스 로직 흐름에 맞게 수정 - 쿠키 값 확인 및 유효성 체크 - 쿠키 설정 및 자동 로그인 - 메인 페이지 리다이렉션 --- src/controllers/qr.controller.ts | 54 ++++++++++--- src/repositories/__test__/qr.repo.test.ts | 18 +++++ src/repositories/qr.repository.ts | 12 +++ src/routes/qr.router.ts | 16 ++-- src/services/__test__/qr.service.test.ts | 93 +++++++++++++++-------- src/services/qr.service.ts | 11 +++ 6 files changed, 158 insertions(+), 46 deletions(-) diff --git a/src/controllers/qr.controller.ts b/src/controllers/qr.controller.ts index 84c0112..26836c7 100644 --- a/src/controllers/qr.controller.ts +++ b/src/controllers/qr.controller.ts @@ -3,12 +3,36 @@ import logger from '@/configs/logger.config'; import { QRLoginTokenService } from "@/services/qr.service"; import { QRLoginTokenResponseDto } from "@/types/dto/responses/qrResponse.type"; import { InvalidTokenError, TokenExpiredError } from '@/exception/token.exception'; +import { UserService } from '@/services/user.service'; +import { NotFoundError } from '@/exception'; +import { CookieOptions } from 'express'; // TODO: randomUUID() 기반으로 길이 36 type Token32 = string & { __lengthBrand: 32 }; export class QRLoginController { - constructor(private qrService: QRLoginTokenService) {} + constructor( + private qrService: QRLoginTokenService, + private userService: UserService + ) {} + + private cookieOption(): CookieOptions { + const isProd = process.env.NODE_ENV === 'production'; + + const baseOptions: CookieOptions = { + httpOnly: isProd, + secure: isProd, + }; + + if (isProd) { + baseOptions.sameSite = 'lax'; + baseOptions.domain = "velog-dashboard.kro.kr"; + } else { + baseOptions.domain = 'localhost'; + } + + return baseOptions; + } createToken: RequestHandler = async ( req: Request, @@ -21,7 +45,6 @@ export class QRLoginController { const userAgent = req.headers['user-agent'] || ''; const token = await this.qrService.create(user.id, ip, userAgent); - const typedToken = token as Token32; const response = new QRLoginTokenResponseDto( @@ -32,28 +55,41 @@ export class QRLoginController { ); res.status(200).json(response); } catch (error) { - logger.error('생성 실패:', error); + logger.error('QR 토큰 생성 실패:', error); next(error); } }; - getToken: RequestHandler = async (req, res, next) => { + getToken: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { try { const token = req.query.token as string; - if (!token) { throw new InvalidTokenError('토큰이 필요합니다.'); } - const found = await this.qrService.getByToken(token); - + const found = await this.qrService.useToken(token); if (!found) { throw new TokenExpiredError(); } - res.status(200).json({ success: true, message: '유효한 QR 토큰입니다.', token: found }); + const user = await this.userService.findByVelogUUID(found.user.toString()); + if (!user) throw new NotFoundError('유저를 찾을 수 없습니다.'); + + const { decryptedAccessToken, decryptedRefreshToken } = this.userService['decryptTokens']( + user.group_id, + user.access_token, + user.refresh_token + ); + + res.clearCookie('access_token', this.cookieOption()); + res.clearCookie('refresh_token', this.cookieOption()); + + res.cookie('access_token', decryptedAccessToken, this.cookieOption()); + res.cookie('refresh_token', decryptedRefreshToken, this.cookieOption()); + + res.redirect('/main'); } catch (error) { - logger.error('QR 토큰 조회 실패', error); + logger.error('QR 토큰 로그인 처리 실패', error); next(error); } }; diff --git a/src/repositories/__test__/qr.repo.test.ts b/src/repositories/__test__/qr.repo.test.ts index 6b945a1..22ae9bb 100644 --- a/src/repositories/__test__/qr.repo.test.ts +++ b/src/repositories/__test__/qr.repo.test.ts @@ -59,4 +59,22 @@ describe('QRLoginTokenRepository', () => { await expect(repo.findQRLoginToken('token')).rejects.toThrow(DBError); }); }); + + describe('markTokenUsed', () => { + it('토큰을 사용 처리해야 한다', async () => { + (mockPool.query as jest.Mock).mockResolvedValueOnce(undefined); + + await expect(repo.markTokenUsed('token')).resolves.not.toThrow(); + expect(mockPool.query).toHaveBeenCalledWith( + expect.stringContaining('UPDATE qr_login_tokens SET is_used = true'), + ['token'] + ); + }); + + it('토큰 사용 처리 중 오류 발생 시 DBError를 던져야 한다', async () => { + (mockPool.query as jest.Mock).mockRejectedValueOnce(new Error('fail')); + + await expect(repo.markTokenUsed('token')).rejects.toThrow(DBError); + }); + }); }); diff --git a/src/repositories/qr.repository.ts b/src/repositories/qr.repository.ts index 16500a6..05aa38f 100644 --- a/src/repositories/qr.repository.ts +++ b/src/repositories/qr.repository.ts @@ -33,4 +33,16 @@ export class QRLoginTokenRepository { throw new DBError('QR 코드 토큰 조회 중 문제가 발생했습니다.'); } } + + async markTokenUsed(token: string): Promise { + try { + const query = ` + UPDATE qr_login_tokens SET is_used = true WHERE token = $1; + `; + await this.pool.query(query, [token]); + } catch (error) { + logger.error('QRLoginToken Repo mark as used Error : ', error); + throw new DBError('QR 코드 사용 처리 중 문제가 발생했습니다.'); + } + } } \ No newline at end of file diff --git a/src/routes/qr.router.ts b/src/routes/qr.router.ts index 7f3a57f..91e9b79 100644 --- a/src/routes/qr.router.ts +++ b/src/routes/qr.router.ts @@ -5,12 +5,16 @@ import { authMiddleware } from '@/middlewares/auth.middleware'; import { QRLoginTokenRepository } from '@/repositories/qr.repository'; import { QRLoginTokenService } from '@/services/qr.service'; import { QRLoginController } from '@/controllers/qr.controller'; +import { UserRepository } from '@/repositories/user.repository'; +import { UserService } from '@/services/user.service'; const router: Router = express.Router(); const qrRepository = new QRLoginTokenRepository(pool); +const userRepository = new UserRepository(pool); +const userService = new UserService(userRepository); const qrService = new QRLoginTokenService(qrRepository); -const qrController = new QRLoginController(qrService); +const qrController = new QRLoginController(qrService, userService); /** * @swagger @@ -30,7 +34,7 @@ router.post('/qr-login', authMiddleware.login, qrController.createToken); * @swagger * /api/qr-login: * get: - * summary: QR 로그인 토큰 조회 + * summary: QR 로그인 토큰 조회 및 자동 로그인 처리 * tags: [QRLogin] * parameters: * - in: query @@ -40,10 +44,12 @@ router.post('/qr-login', authMiddleware.login, qrController.createToken); * type: string * description: 조회할 QR 토큰 * responses: - * 200: - * description: 유효한 토큰 + * 302: + * description: 자동 로그인 완료 후 메인 페이지로 리디렉션 + * 400: + * description: 잘못된 토큰 * 404: - * description: 토큰 없음 or 만료 + * description: 만료 또는 존재하지 않는 토큰 */ router.get('/qr-login', qrController.getToken); diff --git a/src/services/__test__/qr.service.test.ts b/src/services/__test__/qr.service.test.ts index bea062b..9e649dd 100644 --- a/src/services/__test__/qr.service.test.ts +++ b/src/services/__test__/qr.service.test.ts @@ -10,7 +10,7 @@ describe('QRLoginTokenService', () => { let repo: jest.Mocked; beforeEach(() => { - const repoInstance = new QRLoginTokenRepository({} as any) + const repoInstance = new QRLoginTokenRepository({} as any); repo = repoInstance as jest.Mocked; service = new QRLoginTokenService(repo); }); @@ -33,51 +33,80 @@ describe('QRLoginTokenService', () => { }); it('QR 토큰 생성 중 오류 발생 시 예외 발생', async () => { - const userId = 1; - const ip = '127.0.0.1'; - const userAgent = 'Mozilla'; repo.createQRLoginToken.mockRejectedValueOnce(new DBError('생성 실패')); - await expect(service.create(userId, ip, userAgent)).rejects.toThrow('생성 실패'); - expect(repo.createQRLoginToken).toHaveBeenCalled(); + await expect(service.create(1, 'ip', 'agent')).rejects.toThrow('생성 실패'); }); }); describe('getByToken', () => { - const mockToken = 'sample-token'; - const mockQRToken: QRLoginToken = { - token: mockToken, - user: 1, - created_at: new Date(), - expires_at: new Date(Date.now() + 1000 * 60 * 5), - is_used: false, - ip_address: '127.0.0.1', - user_agent: 'Chrome', - }; - - it('유효한 토큰 조회 시 QRLoginToken 반환', async () => { - repo.findQRLoginToken.mockResolvedValue(mockQRToken); - - const result = await service.getByToken(mockToken); - - expect(result).toEqual(mockQRToken); - expect(repo.findQRLoginToken).toHaveBeenCalledWith(mockToken); + it('유효한 토큰 조회 시 반환해야 한다', async () => { + const mockToken: QRLoginToken = { + token: 'token', + user: 1, + created_at: new Date(), + expires_at: new Date(), + is_used: false, + ip_address: '127.0.0.1', + user_agent: 'Chrome', + }; + repo.findQRLoginToken.mockResolvedValue(mockToken); + + const result = await service.getByToken('token'); + expect(result).toEqual(mockToken); }); - it('토큰이 없을 경우 null 반환', async () => { + it('토큰이 없으면 null 반환', async () => { repo.findQRLoginToken.mockResolvedValue(null); - - const result = await service.getByToken(mockToken); - + const result = await service.getByToken('token'); expect(result).toBeNull(); - expect(repo.findQRLoginToken).toHaveBeenCalledWith(mockToken); }); - it('토큰 조회 중 오류 발생 시 예외 발생', async () => { + it('조회 중 오류 발생 시 예외 발생', async () => { repo.findQRLoginToken.mockRejectedValueOnce(new DBError('조회 실패')); + await expect(service.getByToken('token')).rejects.toThrow('조회 실패'); + }); + }); + + describe('useToken', () => { + it('유효한 토큰 사용 처리 후 반환', async () => { + const mockToken: QRLoginToken = { + token: 'token', + user: 1, + created_at: new Date(), + expires_at: new Date(), + is_used: false, + ip_address: '127.0.0.1', + user_agent: 'Chrome', + }; + repo.findQRLoginToken.mockResolvedValue(mockToken); + + const result = await service.useToken('token'); + + expect(result).toEqual(mockToken); + expect(repo.markTokenUsed).toHaveBeenCalledWith('token'); + }); + + it('토큰이 존재하지 않으면 null 반환', async () => { + repo.findQRLoginToken.mockResolvedValue(null); + const result = await service.useToken('token'); + expect(result).toBeNull(); + }); - await expect(service.getByToken(mockToken)).rejects.toThrow('조회 실패'); - expect(repo.findQRLoginToken).toHaveBeenCalledWith(mockToken); + it('markTokenUsed 호출 시 예외 발생하면 전파', async () => { + const mockToken: QRLoginToken = { + token: 'token', + user: 1, + created_at: new Date(), + expires_at: new Date(), + is_used: false, + ip_address: '127.0.0.1', + user_agent: 'Chrome', + }; + repo.findQRLoginToken.mockResolvedValue(mockToken); + repo.markTokenUsed.mockRejectedValueOnce(new DBError('사용 처리 실패')); + + await expect(service.useToken('token')).rejects.toThrow('사용 처리 실패'); }); }); }); diff --git a/src/services/qr.service.ts b/src/services/qr.service.ts index 3fdbce5..980cadd 100644 --- a/src/services/qr.service.ts +++ b/src/services/qr.service.ts @@ -14,4 +14,15 @@ export class QRLoginTokenService { async getByToken(token: string): Promise { return await this.qrRepo.findQRLoginToken(token); } + + async useToken(token: string): Promise { + const qrToken = await this.qrRepo.findQRLoginToken(token); + + if (!qrToken) { + return null; + } + + await this.qrRepo.markTokenUsed(token); + return qrToken; + } } From a95635e05263ba13f325658d197e854e48766b87 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Fri, 25 Apr 2025 20:32:59 +0900 Subject: [PATCH 11/32] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/qr.controller.ts | 9 ++++----- src/services/user.service.ts | 4 ++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/controllers/qr.controller.ts b/src/controllers/qr.controller.ts index 26836c7..2a1a9bf 100644 --- a/src/controllers/qr.controller.ts +++ b/src/controllers/qr.controller.ts @@ -7,7 +7,6 @@ import { UserService } from '@/services/user.service'; import { NotFoundError } from '@/exception'; import { CookieOptions } from 'express'; -// TODO: randomUUID() 기반으로 길이 36 type Token32 = string & { __lengthBrand: 32 }; export class QRLoginController { @@ -75,10 +74,10 @@ export class QRLoginController { const user = await this.userService.findByVelogUUID(found.user.toString()); if (!user) throw new NotFoundError('유저를 찾을 수 없습니다.'); - const { decryptedAccessToken, decryptedRefreshToken } = this.userService['decryptTokens']( - user.group_id, - user.access_token, - user.refresh_token + const { decryptedAccessToken, decryptedRefreshToken } = this.userService.getDecryptedTokens( + user.group_id, + user.access_token, + user.refresh_token ); res.clearCookie('access_token', this.cookieOption()); diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 6e0ff1d..5cf8576 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -122,4 +122,8 @@ export class UserService { async updateUserTokens(userData: UserWithTokenDto) { return await this.userRepo.updateTokens(userData.id, userData.accessToken, userData.refreshToken); } + + public getDecryptedTokens(userId: number, accessToken: string, refreshToken: string) { + return this.decryptTokens(userId, accessToken, refreshToken); + } } From 245a721f9391d3e06bf6e4c9b4f8647317fdbcd4 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Tue, 29 Apr 2025 21:34:26 +0900 Subject: [PATCH 12/32] =?UTF-8?q?hotfix:=20token=2036=EC=9E=90=EA=B0=80=20?= =?UTF-8?q?=EC=95=84=EB=8B=8C=2010=EC=9E=90=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/qr.controller.ts | 2 +- src/services/__test__/qr.service.test.ts | 9 +++++---- src/services/qr.service.ts | 4 ++-- src/types/dto/responses/qrResponse.type.ts | 4 ++-- src/utils/generateRandomToken.util.ts | 8 ++++++++ 5 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 src/utils/generateRandomToken.util.ts diff --git a/src/controllers/qr.controller.ts b/src/controllers/qr.controller.ts index 2a1a9bf..ed3e016 100644 --- a/src/controllers/qr.controller.ts +++ b/src/controllers/qr.controller.ts @@ -7,7 +7,7 @@ import { UserService } from '@/services/user.service'; import { NotFoundError } from '@/exception'; import { CookieOptions } from 'express'; -type Token32 = string & { __lengthBrand: 32 }; +type Token32 = string & { __lengthBrand: 10 }; export class QRLoginController { constructor( diff --git a/src/services/__test__/qr.service.test.ts b/src/services/__test__/qr.service.test.ts index 9e649dd..080ca71 100644 --- a/src/services/__test__/qr.service.test.ts +++ b/src/services/__test__/qr.service.test.ts @@ -24,11 +24,12 @@ describe('QRLoginTokenService', () => { const userId = 1; const ip = '127.0.0.1'; const userAgent = 'Chrome'; - const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - + const token = await service.create(userId, ip, userAgent); - - expect(token).toMatch(uuidRegex); + + expect(typeof token).toBe('string'); + expect(token.length).toBe(10); + expect(/^[A-Za-z0-9]{10}$/.test(token)).toBe(true); expect(repo.createQRLoginToken).toHaveBeenCalledWith(token, userId, ip, userAgent); }); diff --git a/src/services/qr.service.ts b/src/services/qr.service.ts index 980cadd..43488a3 100644 --- a/src/services/qr.service.ts +++ b/src/services/qr.service.ts @@ -1,12 +1,12 @@ import { QRLoginTokenRepository } from "@/repositories/qr.repository"; import { QRLoginToken } from "@/types/models/QRLoginToken.type"; -import { randomUUID } from "crypto"; +import { generateRandomToken } from '@/utils/generateRandomToken.util'; export class QRLoginTokenService { constructor(private qrRepo: QRLoginTokenRepository) {} async create(userId: number, ip: string, userAgent: string): Promise { - const token = randomUUID(); + const token = generateRandomToken(10); await this.qrRepo.createQRLoginToken(token, userId, ip, userAgent); return token; } diff --git a/src/types/dto/responses/qrResponse.type.ts b/src/types/dto/responses/qrResponse.type.ts index 3c3fe26..ff88846 100644 --- a/src/types/dto/responses/qrResponse.type.ts +++ b/src/types/dto/responses/qrResponse.type.ts @@ -1,9 +1,9 @@ import { BaseResponseDto } from '@/types/dto/responses/baseResponse.type'; -type Token32 = string & { __lengthBrand: 32 }; +type Token10 = string & { __lengthBrand: 10 }; export interface QRLoginTokenResponseData { - token: Token32; + token: Token10; } export class QRLoginTokenResponseDto extends BaseResponseDto {} diff --git a/src/utils/generateRandomToken.util.ts b/src/utils/generateRandomToken.util.ts new file mode 100644 index 0000000..d87232e --- /dev/null +++ b/src/utils/generateRandomToken.util.ts @@ -0,0 +1,8 @@ +export function generateRandomToken(length: number = 10): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; + } \ No newline at end of file From 4fd26ac6976ff1002ca6f251e2564ae2c1eb3c02 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Tue, 29 Apr 2025 21:35:07 +0900 Subject: [PATCH 13/32] =?UTF-8?q?hotfix:=20=EC=BF=BC=EB=A6=AC=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EC=8B=9C=20qr=5Flogin=5Ftokens=EA=B0=80=20?= =?UTF-8?q?=EC=95=84=EB=8B=8C=20users=5Fqrlogintoken=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/repositories/__test__/qr.repo.test.ts | 2 +- src/repositories/qr.repository.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/repositories/__test__/qr.repo.test.ts b/src/repositories/__test__/qr.repo.test.ts index 22ae9bb..7171529 100644 --- a/src/repositories/__test__/qr.repo.test.ts +++ b/src/repositories/__test__/qr.repo.test.ts @@ -66,7 +66,7 @@ describe('QRLoginTokenRepository', () => { await expect(repo.markTokenUsed('token')).resolves.not.toThrow(); expect(mockPool.query).toHaveBeenCalledWith( - expect.stringContaining('UPDATE qr_login_tokens SET is_used = true'), + expect.stringContaining('UPDATE users_qrlogintoken SET is_used = true'), ['token'] ); }); diff --git a/src/repositories/qr.repository.ts b/src/repositories/qr.repository.ts index 05aa38f..6f5231c 100644 --- a/src/repositories/qr.repository.ts +++ b/src/repositories/qr.repository.ts @@ -9,7 +9,7 @@ export class QRLoginTokenRepository { async createQRLoginToken(token: string, userId: number, ip: string, userAgent: string): Promise { try { const query = ` - INSERT INTO qr_login_tokens (token, user_id, created_at, expires_at, is_used, ip_address, user_agent) + INSERT INTO users_qrlogintoken (token, user_id, created_at, expires_at, is_used, ip_address, user_agent) VALUES ($1, $2, NOW(), NOW() + INTERVAL '5 minutes', false, $3, $4); `; await this.pool.query(query, [token, userId, ip, userAgent]); @@ -23,7 +23,7 @@ export class QRLoginTokenRepository { try { const query = ` SELECT * - FROM qr_login_tokens + FROM users_qrlogintoken WHERE token = $1 AND is_used = false AND expires_at > NOW(); `; const result = await this.pool.query(query, [token]); @@ -37,7 +37,7 @@ export class QRLoginTokenRepository { async markTokenUsed(token: string): Promise { try { const query = ` - UPDATE qr_login_tokens SET is_used = true WHERE token = $1; + UPDATE users_qrlogintoken SET is_used = true WHERE token = $1; `; await this.pool.query(query, [token]); } catch (error) { From c7e9019d047f69b85c42c8d0f78a04d824f59378 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Tue, 29 Apr 2025 21:35:27 +0900 Subject: [PATCH 14/32] =?UTF-8?q?test:=20=EB=A0=88=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__test__/qr.repo.integration.test.ts | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 src/repositories/__test__/qr.repo.integration.test.ts diff --git a/src/repositories/__test__/qr.repo.integration.test.ts b/src/repositories/__test__/qr.repo.integration.test.ts new file mode 100644 index 0000000..39a7186 --- /dev/null +++ b/src/repositories/__test__/qr.repo.integration.test.ts @@ -0,0 +1,148 @@ +import dotenv from 'dotenv'; +import { Pool } from 'pg'; +import pg from 'pg'; +import { DBError } from '@/exception'; +import { QRLoginTokenRepository } from '@/repositories/qr.repository'; +import { generateRandomToken } from '@/utils/generateRandomToken.util'; +import logger from '@/configs/logger.config'; + +dotenv.config(); +jest.setTimeout(30000); + +describe('QRLoginTokenRepository 통합 테스트', () => { + let testPool: Pool; + let repo: QRLoginTokenRepository; + + const TEST_DATA = { + USER_ID: 1, + }; + + beforeAll(async () => { + const testPoolConfig: pg.PoolConfig = { + database: process.env.DATABASE_NAME, + user: process.env.POSTGRES_USER, + host: process.env.POSTGRES_HOST, + password: process.env.POSTGRES_PASSWORD, + port: Number(process.env.POSTGRES_PORT), + max: 1, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 5000, + allowExitOnIdle: false, + statement_timeout: 30000, + }; + + if (process.env.POSTGRES_HOST !== 'localhost') { + testPoolConfig.ssl = { rejectUnauthorized: false }; + } + + testPool = new Pool(testPoolConfig); + + await testPool.query('SELECT 1'); + logger.info('테스트 DB 연결 성공'); + + repo = new QRLoginTokenRepository(testPool); + }); + + afterAll(async () => { + try { + await new Promise(resolve => setTimeout(resolve, 1000)); + if (testPool) { + await testPool.end(); + } + await new Promise(resolve => setTimeout(resolve, 1000)); + logger.info('테스트 DB 연결 종료'); + } catch (error) { + logger.error('테스트 종료 중 오류:', error); + } + }); + + describe('QR 토큰 생성 및 조회', () => { + it('QR 토큰을 생성하고 정상 조회할 수 있어야 한다', async () => { + const token = generateRandomToken(); + const ip = '127.0.0.1'; + const userAgent = 'test-agent'; + + await repo.createQRLoginToken(token, TEST_DATA.USER_ID, ip, userAgent); + const foundToken = await repo.findQRLoginToken(token); + + expect(foundToken).not.toBeNull(); + expect(foundToken?.token).toBe(token); + expect(foundToken?.is_used).toBe(false); + expect(new Date(foundToken!.expires_at).getTime()).toBeGreaterThan(new Date(foundToken!.created_at).getTime()); + }); + + it('존재하지 않는 토큰 조회 시 null을 반환해야 한다', async () => { + const invalidToken = generateRandomToken(); + const result = await repo.findQRLoginToken(invalidToken); + + expect(result).toBeNull(); + }); + }); + + describe('QR 토큰 사용 처리', () => { + it('QR 토큰을 사용 처리한 후 조회되지 않아야 한다', async () => { + const token = generateRandomToken(); + const ip = '127.0.0.1'; + const userAgent = 'test-agent'; + + await repo.createQRLoginToken(token, TEST_DATA.USER_ID, ip, userAgent); + await repo.markTokenUsed(token); + + const found = await repo.findQRLoginToken(token); + + expect(found).toBeNull(); + }); + }); + + describe('QR 토큰 만료 처리', () => { + it('만료된 토큰은 조회되지 않아야 한다', async () => { + const token = generateRandomToken(); + const ip = '127.0.0.1'; + const userAgent = 'test-agent'; + + await testPool.query( + ` + INSERT INTO users_qrlogintoken (token, user_id, created_at, expires_at, is_used, ip_address, user_agent) + VALUES ($1, $2, NOW() - INTERVAL '10 minutes', NOW() - INTERVAL '5 minutes', false, $3, $4) + `, + [token, TEST_DATA.USER_ID, ip, userAgent] + ); + + const found = await repo.findQRLoginToken(token); + + expect(found).toBeNull(); + }); + + it('만료되고 사용된 토큰도 조회되지 않아야 한다', async () => { + const token = generateRandomToken(); + const ip = '127.0.0.1'; + const userAgent = 'test-agent'; + + await testPool.query( + ` + INSERT INTO users_qrlogintoken (token, user_id, created_at, expires_at, is_used, ip_address, user_agent) + VALUES ($1, $2, NOW() - INTERVAL '10 minutes', NOW() - INTERVAL '5 minutes', true, $3, $4) + `, + [token, TEST_DATA.USER_ID, ip, userAgent] + ); + + const found = await repo.findQRLoginToken(token); + + expect(found).toBeNull(); + }); + }); + + describe('예외 상황', () => { + it('중복 토큰 삽입 시 DBError를 발생시켜야 한다', async () => { + const token = generateRandomToken(); + const ip = '127.0.0.1'; + const userAgent = 'test-agent'; + + await repo.createQRLoginToken(token, TEST_DATA.USER_ID, ip, userAgent); + + await expect( + repo.createQRLoginToken(token, TEST_DATA.USER_ID, ip, userAgent) + ).rejects.toThrow(DBError); + }); + }); +}); From 5932dac170b9e5ca9c9c43cdfe6ae7ef50472873 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Tue, 29 Apr 2025 21:45:49 +0900 Subject: [PATCH 15/32] =?UTF-8?q?hotfix:=20=EC=A4=91=EB=B3=B5=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=9B=84=20DB=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20=EC=A2=85=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/repositories/__test__/qr.repo.integration.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/repositories/__test__/qr.repo.integration.test.ts b/src/repositories/__test__/qr.repo.integration.test.ts index 39a7186..662ece5 100644 --- a/src/repositories/__test__/qr.repo.integration.test.ts +++ b/src/repositories/__test__/qr.repo.integration.test.ts @@ -137,12 +137,15 @@ describe('QRLoginTokenRepository 통합 테스트', () => { const token = generateRandomToken(); const ip = '127.0.0.1'; const userAgent = 'test-agent'; - + await repo.createQRLoginToken(token, TEST_DATA.USER_ID, ip, userAgent); - + await expect( repo.createQRLoginToken(token, TEST_DATA.USER_ID, ip, userAgent) ).rejects.toThrow(DBError); + + await testPool.end(); + logger.info('중복 토큰 테스트 후 DB 연결 종료'); }); }); }); From 85b502cdbe57ee953b20505b209f154e43fe7e35 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Tue, 29 Apr 2025 21:49:49 +0900 Subject: [PATCH 16/32] =?UTF-8?q?hotfix:=20=EC=A4=91=EB=B3=B5=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=82=BD=EC=9E=85=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__test__/qr.repo.integration.test.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/repositories/__test__/qr.repo.integration.test.ts b/src/repositories/__test__/qr.repo.integration.test.ts index 662ece5..3e06bca 100644 --- a/src/repositories/__test__/qr.repo.integration.test.ts +++ b/src/repositories/__test__/qr.repo.integration.test.ts @@ -1,7 +1,6 @@ import dotenv from 'dotenv'; import { Pool } from 'pg'; import pg from 'pg'; -import { DBError } from '@/exception'; import { QRLoginTokenRepository } from '@/repositories/qr.repository'; import { generateRandomToken } from '@/utils/generateRandomToken.util'; import logger from '@/configs/logger.config'; @@ -131,21 +130,4 @@ describe('QRLoginTokenRepository 통합 테스트', () => { expect(found).toBeNull(); }); }); - - describe('예외 상황', () => { - it('중복 토큰 삽입 시 DBError를 발생시켜야 한다', async () => { - const token = generateRandomToken(); - const ip = '127.0.0.1'; - const userAgent = 'test-agent'; - - await repo.createQRLoginToken(token, TEST_DATA.USER_ID, ip, userAgent); - - await expect( - repo.createQRLoginToken(token, TEST_DATA.USER_ID, ip, userAgent) - ).rejects.toThrow(DBError); - - await testPool.end(); - logger.info('중복 토큰 테스트 후 DB 연결 종료'); - }); - }); }); From 37b9633bbb839aef63483e28a79223351d277cc3 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Tue, 29 Apr 2025 22:17:22 +0900 Subject: [PATCH 17/32] =?UTF-8?q?modify:=20QRLoginToken=20=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=ED=84=B0,=20=EC=84=9C=EB=B9=84=EC=8A=A4,=20=EB=A0=88?= =?UTF-8?q?=ED=8F=AC=20User=EC=AA=BD=EC=9C=BC=EB=A1=9C=20=ED=95=A9?= =?UTF-8?q?=EC=B9=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/qr.controller.ts | 95 ------------------- src/controllers/user.controller.ts | 66 +++++++++++++ .../__test__/qr.repo.integration.test.ts | 6 +- src/repositories/__test__/qr.repo.test.ts | 6 +- src/repositories/qr.repository.ts | 48 ---------- src/repositories/user.repository.ts | 43 ++++++++- src/routes/index.ts | 2 - src/routes/qr.router.ts | 56 ----------- src/routes/user.router.ts | 37 ++++++++ src/services/__test__/qr.service.test.ts | 18 ++-- src/services/qr.service.ts | 28 ------ src/services/user.service.ts | 23 +++++ 12 files changed, 183 insertions(+), 245 deletions(-) delete mode 100644 src/controllers/qr.controller.ts delete mode 100644 src/repositories/qr.repository.ts delete mode 100644 src/routes/qr.router.ts delete mode 100644 src/services/qr.service.ts diff --git a/src/controllers/qr.controller.ts b/src/controllers/qr.controller.ts deleted file mode 100644 index ed3e016..0000000 --- a/src/controllers/qr.controller.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { NextFunction, Request, RequestHandler, Response } from 'express'; -import logger from '@/configs/logger.config'; -import { QRLoginTokenService } from "@/services/qr.service"; -import { QRLoginTokenResponseDto } from "@/types/dto/responses/qrResponse.type"; -import { InvalidTokenError, TokenExpiredError } from '@/exception/token.exception'; -import { UserService } from '@/services/user.service'; -import { NotFoundError } from '@/exception'; -import { CookieOptions } from 'express'; - -type Token32 = string & { __lengthBrand: 10 }; - -export class QRLoginController { - constructor( - private qrService: QRLoginTokenService, - private userService: UserService - ) {} - - private cookieOption(): CookieOptions { - const isProd = process.env.NODE_ENV === 'production'; - - const baseOptions: CookieOptions = { - httpOnly: isProd, - secure: isProd, - }; - - if (isProd) { - baseOptions.sameSite = 'lax'; - baseOptions.domain = "velog-dashboard.kro.kr"; - } else { - baseOptions.domain = 'localhost'; - } - - return baseOptions; - } - - createToken: RequestHandler = async ( - req: Request, - res: Response, - next: NextFunction, - ) => { - try { - const user = req.user; - const ip = req.ip ?? ''; - const userAgent = req.headers['user-agent'] || ''; - - const token = await this.qrService.create(user.id, ip, userAgent); - const typedToken = token as Token32; - - const response = new QRLoginTokenResponseDto( - true, - 'QR 토큰 생성 완료', - { token: typedToken }, - null - ); - res.status(200).json(response); - } catch (error) { - logger.error('QR 토큰 생성 실패:', error); - next(error); - } - }; - - getToken: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { - try { - const token = req.query.token as string; - if (!token) { - throw new InvalidTokenError('토큰이 필요합니다.'); - } - - const found = await this.qrService.useToken(token); - if (!found) { - throw new TokenExpiredError(); - } - - const user = await this.userService.findByVelogUUID(found.user.toString()); - if (!user) throw new NotFoundError('유저를 찾을 수 없습니다.'); - - const { decryptedAccessToken, decryptedRefreshToken } = this.userService.getDecryptedTokens( - user.group_id, - user.access_token, - user.refresh_token - ); - - res.clearCookie('access_token', this.cookieOption()); - res.clearCookie('refresh_token', this.cookieOption()); - - res.cookie('access_token', decryptedAccessToken, this.cookieOption()); - res.cookie('refresh_token', decryptedRefreshToken, this.cookieOption()); - - res.redirect('/main'); - } catch (error) { - logger.error('QR 토큰 로그인 처리 실패', error); - next(error); - } - }; -} diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 37d9f90..8407e9f 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -1,7 +1,13 @@ import { NextFunction, Request, Response, RequestHandler, CookieOptions } from 'express'; import logger from '@/configs/logger.config'; import { EmptyResponseDto, LoginResponseDto, UserWithTokenDto } from '@/types'; +import { QRLoginTokenResponseDto } from '@/types/dto/responses/qrResponse.type'; import { UserService } from '@/services/user.service'; +import { InvalidTokenError, TokenExpiredError } from '@/exception/token.exception'; +import { NotFoundError } from '@/exception'; + +type Token32 = string & { __lengthBrand: 10 }; + export class UserController { constructor(private userService: UserService) { } @@ -102,4 +108,64 @@ export class UserController { res.status(200).json(response); }; + + createToken: RequestHandler = async ( + req: Request, + res: Response, + next: NextFunction, + ) => { + try { + const user = req.user; + const ip = req.ip ?? ''; + const userAgent = req.headers['user-agent'] || ''; + + const token = await this.userService.create(user.id, ip, userAgent); + const typedToken = token as Token32; + + const response = new QRLoginTokenResponseDto( + true, + 'QR 토큰 생성 완료', + { token: typedToken }, + null + ); + res.status(200).json(response); + } catch (error) { + logger.error('QR 토큰 생성 실패:', error); + next(error); + } + }; + + getToken: RequestHandler = async (req: Request, res: Response, next: NextFunction) => { + try { + const token = req.query.token as string; + if (!token) { + throw new InvalidTokenError('토큰이 필요합니다.'); + } + + const found = await this.userService.useToken(token); + if (!found) { + throw new TokenExpiredError(); + } + + const user = await this.userService.findByVelogUUID(found.user.toString()); + if (!user) throw new NotFoundError('유저를 찾을 수 없습니다.'); + + const { decryptedAccessToken, decryptedRefreshToken } = this.userService.getDecryptedTokens( + user.group_id, + user.access_token, + user.refresh_token + ); + + res.clearCookie('access_token', this.cookieOption()); + res.clearCookie('refresh_token', this.cookieOption()); + + res.cookie('access_token', decryptedAccessToken, this.cookieOption()); + res.cookie('refresh_token', decryptedRefreshToken, this.cookieOption()); + + res.redirect('/main'); + } catch (error) { + logger.error('QR 토큰 로그인 처리 실패', error); + next(error); + } + }; } diff --git a/src/repositories/__test__/qr.repo.integration.test.ts b/src/repositories/__test__/qr.repo.integration.test.ts index 3e06bca..844fdd5 100644 --- a/src/repositories/__test__/qr.repo.integration.test.ts +++ b/src/repositories/__test__/qr.repo.integration.test.ts @@ -1,7 +1,7 @@ import dotenv from 'dotenv'; import { Pool } from 'pg'; import pg from 'pg'; -import { QRLoginTokenRepository } from '@/repositories/qr.repository'; +import { UserRepository } from '@/repositories/user.repository'; import { generateRandomToken } from '@/utils/generateRandomToken.util'; import logger from '@/configs/logger.config'; @@ -10,7 +10,7 @@ jest.setTimeout(30000); describe('QRLoginTokenRepository 통합 테스트', () => { let testPool: Pool; - let repo: QRLoginTokenRepository; + let repo: UserRepository; const TEST_DATA = { USER_ID: 1, @@ -39,7 +39,7 @@ describe('QRLoginTokenRepository 통합 테스트', () => { await testPool.query('SELECT 1'); logger.info('테스트 DB 연결 성공'); - repo = new QRLoginTokenRepository(testPool); + repo = new UserRepository(testPool); }); afterAll(async () => { diff --git a/src/repositories/__test__/qr.repo.test.ts b/src/repositories/__test__/qr.repo.test.ts index 7171529..184fb85 100644 --- a/src/repositories/__test__/qr.repo.test.ts +++ b/src/repositories/__test__/qr.repo.test.ts @@ -1,4 +1,4 @@ -import { QRLoginTokenRepository } from '@/repositories/qr.repository'; +import { UserRepository } from '@/repositories/user.repository'; import { DBError } from '@/exception'; import { Pool } from 'pg'; @@ -7,10 +7,10 @@ const mockPool: Partial = { }; describe('QRLoginTokenRepository', () => { - let repo: QRLoginTokenRepository; + let repo: UserRepository; beforeEach(() => { - repo = new QRLoginTokenRepository(mockPool as Pool); + repo = new UserRepository(mockPool as Pool); }); afterEach(() => { diff --git a/src/repositories/qr.repository.ts b/src/repositories/qr.repository.ts deleted file mode 100644 index 6f5231c..0000000 --- a/src/repositories/qr.repository.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Pool } from 'pg'; -import logger from '@/configs/logger.config'; -import { DBError } from '@/exception'; -import { QRLoginToken } from '@/types/models/QRLoginToken.type'; - -export class QRLoginTokenRepository { - constructor(private pool: Pool) { } - - async createQRLoginToken(token: string, userId: number, ip: string, userAgent: string): Promise { - try { - const query = ` - INSERT INTO users_qrlogintoken (token, user_id, created_at, expires_at, is_used, ip_address, user_agent) - VALUES ($1, $2, NOW(), NOW() + INTERVAL '5 minutes', false, $3, $4); - `; - await this.pool.query(query, [token, userId, ip, userAgent]); - } catch (error) { - logger.error('QRLoginToken Repo Create Error : ', error); - throw new DBError('QR 코드 토큰 생성 중 문제가 발생했습니다.'); - } - } - - async findQRLoginToken(token: string): Promise { - try { - const query = ` - SELECT * - FROM users_qrlogintoken - WHERE token = $1 AND is_used = false AND expires_at > NOW(); - `; - const result = await this.pool.query(query, [token]); - return result.rows[0] ?? null; - } catch (error) { - logger.error('QRLoginToken Repo find QR Code Error : ', error); - throw new DBError('QR 코드 토큰 조회 중 문제가 발생했습니다.'); - } - } - - async markTokenUsed(token: string): Promise { - try { - const query = ` - UPDATE users_qrlogintoken SET is_used = true WHERE token = $1; - `; - await this.pool.query(query, [token]); - } catch (error) { - logger.error('QRLoginToken Repo mark as used Error : ', error); - throw new DBError('QR 코드 사용 처리 중 문제가 발생했습니다.'); - } - } -} \ No newline at end of file diff --git a/src/repositories/user.repository.ts b/src/repositories/user.repository.ts index a862aff..76c4655 100644 --- a/src/repositories/user.repository.ts +++ b/src/repositories/user.repository.ts @@ -1,10 +1,11 @@ import { Pool } from 'pg'; import logger from '@/configs/logger.config'; import { User } from '@/types'; +import { QRLoginToken } from "@/types/models/QRLoginToken.type"; import { DBError } from '@/exception'; export class UserRepository { - constructor(private readonly pool: Pool) { } + constructor(private readonly pool: Pool) {} async findByUserVelogUUID(uuid: string): Promise { try { @@ -91,4 +92,44 @@ export class UserRepository { throw new DBError('유저 생성 중 문제가 발생했습니다.'); } } + + async createQRLoginToken(token: string, userId: number, ip: string, userAgent: string): Promise { + try { + const query = ` + INSERT INTO users_qrlogintoken (token, user_id, created_at, expires_at, is_used, ip_address, user_agent) + VALUES ($1, $2, NOW(), NOW() + INTERVAL '5 minutes', false, $3, $4); + `; + await this.pool.query(query, [token, userId, ip, userAgent]); + } catch (error) { + logger.error('QRLoginToken Repo Create Error : ', error); + throw new DBError('QR 코드 토큰 생성 중 문제가 발생했습니다.'); + } + } + + async findQRLoginToken(token: string): Promise { + try { + const query = ` + SELECT * + FROM users_qrlogintoken + WHERE token = $1 AND is_used = false AND expires_at > NOW(); + `; + const result = await this.pool.query(query, [token]); + return result.rows[0] ?? null; + } catch (error) { + logger.error('QRLoginToken Repo find QR Code Error : ', error); + throw new DBError('QR 코드 토큰 조회 중 문제가 발생했습니다.'); + } + } + + async markTokenUsed(token: string): Promise { + try { + const query = ` + UPDATE users_qrlogintoken SET is_used = true WHERE token = $1; + `; + await this.pool.query(query, [token]); + } catch (error) { + logger.error('QRLoginToken Repo mark as used Error : ', error); + throw new DBError('QR 코드 사용 처리 중 문제가 발생했습니다.'); + } + } } diff --git a/src/routes/index.ts b/src/routes/index.ts index 66144ac..bcd21bc 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -2,7 +2,6 @@ import express, { Router } from 'express'; import UserRouter from './user.router'; import PostRouter from './post.router'; import NotiRouter from './noti.router'; -import QrRouter from './qr.router'; import LeaderboardRouter from './leaderboard.router'; const router: Router = express.Router(); @@ -14,7 +13,6 @@ router.use('/ping', (req, res) => { router.use('/', UserRouter); router.use('/', PostRouter); router.use('/', NotiRouter); -router.use('/', QrRouter); router.use('/', LeaderboardRouter); export default router; diff --git a/src/routes/qr.router.ts b/src/routes/qr.router.ts deleted file mode 100644 index 91e9b79..0000000 --- a/src/routes/qr.router.ts +++ /dev/null @@ -1,56 +0,0 @@ -import express, { Router } from 'express'; -import pool from '@/configs/db.config'; - -import { authMiddleware } from '@/middlewares/auth.middleware'; -import { QRLoginTokenRepository } from '@/repositories/qr.repository'; -import { QRLoginTokenService } from '@/services/qr.service'; -import { QRLoginController } from '@/controllers/qr.controller'; -import { UserRepository } from '@/repositories/user.repository'; -import { UserService } from '@/services/user.service'; - -const router: Router = express.Router(); - -const qrRepository = new QRLoginTokenRepository(pool); -const userRepository = new UserRepository(pool); -const userService = new UserService(userRepository); -const qrService = new QRLoginTokenService(qrRepository); -const qrController = new QRLoginController(qrService, userService); - -/** - * @swagger - * /api/qr-login: - * post: - * summary: QR 로그인 토큰 생성 - * tags: [QRLogin] - * security: - * - bearerAuth: [] - * responses: - * 200: - * description: QR 로그인 토큰 생성 성공 - */ -router.post('/qr-login', authMiddleware.login, qrController.createToken); - -/** - * @swagger - * /api/qr-login: - * get: - * summary: QR 로그인 토큰 조회 및 자동 로그인 처리 - * tags: [QRLogin] - * parameters: - * - in: query - * name: token - * required: true - * schema: - * type: string - * description: 조회할 QR 토큰 - * responses: - * 302: - * description: 자동 로그인 완료 후 메인 페이지로 리디렉션 - * 400: - * description: 잘못된 토큰 - * 404: - * description: 만료 또는 존재하지 않는 토큰 - */ -router.get('/qr-login', qrController.getToken); - -export default router; diff --git a/src/routes/user.router.ts b/src/routes/user.router.ts index 7f30846..4cfe1ef 100644 --- a/src/routes/user.router.ts +++ b/src/routes/user.router.ts @@ -115,4 +115,41 @@ router.post('/logout', authMiddleware.verify, userController.logout); */ router.get('/me', authMiddleware.login, userController.fetchCurrentUser); +/** + * @swagger + * /api/qr-login: + * post: + * summary: QR 로그인 토큰 생성 + * tags: [QRLogin] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: QR 로그인 토큰 생성 성공 + */ +router.post('/qr-login', authMiddleware.login, userController.createToken); + +/** + * @swagger + * /api/qr-login: + * get: + * summary: QR 로그인 토큰 조회 및 자동 로그인 처리 + * tags: [QRLogin] + * parameters: + * - in: query + * name: token + * required: true + * schema: + * type: string + * description: 조회할 QR 토큰 + * responses: + * 302: + * description: 자동 로그인 완료 후 메인 페이지로 리디렉션 + * 400: + * description: 잘못된 토큰 + * 404: + * description: 만료 또는 존재하지 않는 토큰 + */ +router.get('/qr-login', userController.getToken); + export default router; diff --git a/src/services/__test__/qr.service.test.ts b/src/services/__test__/qr.service.test.ts index 080ca71..5dd7484 100644 --- a/src/services/__test__/qr.service.test.ts +++ b/src/services/__test__/qr.service.test.ts @@ -1,18 +1,18 @@ -import { QRLoginTokenService } from '@/services/qr.service'; -import { QRLoginTokenRepository } from '@/repositories/qr.repository'; +import { UserService } from '@/services/user.service'; +import { UserRepository } from '@/repositories/user.repository'; import { DBError } from '@/exception'; import { QRLoginToken } from '@/types/models/QRLoginToken.type'; -jest.mock('@/repositories/qr.repository'); +jest.mock('@/repositories/user.repository'); -describe('QRLoginTokenService', () => { - let service: QRLoginTokenService; - let repo: jest.Mocked; +describe('UserService', () => { + let service: UserService; + let repo: jest.Mocked; beforeEach(() => { - const repoInstance = new QRLoginTokenRepository({} as any); - repo = repoInstance as jest.Mocked; - service = new QRLoginTokenService(repo); + const repoInstance = new UserRepository({} as any); + repo = repoInstance as jest.Mocked; + service = new UserService(repo); }); afterEach(() => { diff --git a/src/services/qr.service.ts b/src/services/qr.service.ts deleted file mode 100644 index 43488a3..0000000 --- a/src/services/qr.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { QRLoginTokenRepository } from "@/repositories/qr.repository"; -import { QRLoginToken } from "@/types/models/QRLoginToken.type"; -import { generateRandomToken } from '@/utils/generateRandomToken.util'; - -export class QRLoginTokenService { - constructor(private qrRepo: QRLoginTokenRepository) {} - - async create(userId: number, ip: string, userAgent: string): Promise { - const token = generateRandomToken(10); - await this.qrRepo.createQRLoginToken(token, userId, ip, userAgent); - return token; - } - - async getByToken(token: string): Promise { - return await this.qrRepo.findQRLoginToken(token); - } - - async useToken(token: string): Promise { - const qrToken = await this.qrRepo.findQRLoginToken(token); - - if (!qrToken) { - return null; - } - - await this.qrRepo.markTokenUsed(token); - return qrToken; - } -} diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 5cf8576..ed626e8 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -6,6 +6,8 @@ import { sendSlackMessage } from '@/modules/slack/slack.notifier'; import { UserRepository } from '@/repositories/user.repository'; import { UserWithTokenDto, User, SampleUser } from '@/types'; import { generateRandomGroupId } from '@/utils/generateGroupId.util'; +import { QRLoginToken } from "@/types/models/QRLoginToken.type"; +import { generateRandomToken } from '@/utils/generateRandomToken.util'; export class UserService { constructor(private userRepo: UserRepository) { } @@ -126,4 +128,25 @@ export class UserService { public getDecryptedTokens(userId: number, accessToken: string, refreshToken: string) { return this.decryptTokens(userId, accessToken, refreshToken); } + + async create(userId: number, ip: string, userAgent: string): Promise { + const token = generateRandomToken(10); + await this.userRepo.createQRLoginToken(token, userId, ip, userAgent); + return token; +} + +async getByToken(token: string): Promise { + return await this.userRepo.findQRLoginToken(token); +} + +async useToken(token: string): Promise { + const qrToken = await this.userRepo.findQRLoginToken(token); + + if (!qrToken) { + return null; + } + + await this.userRepo.markTokenUsed(token); + return qrToken; + } } From 3440fa27e57b3382463148b520b7559b6acf4e41 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Tue, 29 Apr 2025 22:30:11 +0900 Subject: [PATCH 18/32] =?UTF-8?q?modify:=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EB=A6=AC=EB=B7=B0=201=EC=B0=A8=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/__test__/qr.service.test.ts | 4 +++- src/services/user.service.ts | 4 ++-- src/utils/generateRandomToken.util.ts | 5 ++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/services/__test__/qr.service.test.ts b/src/services/__test__/qr.service.test.ts index 5dd7484..75d92b7 100644 --- a/src/services/__test__/qr.service.test.ts +++ b/src/services/__test__/qr.service.test.ts @@ -2,6 +2,7 @@ import { UserService } from '@/services/user.service'; import { UserRepository } from '@/repositories/user.repository'; import { DBError } from '@/exception'; import { QRLoginToken } from '@/types/models/QRLoginToken.type'; +import { Pool } from 'pg'; jest.mock('@/repositories/user.repository'); @@ -10,7 +11,8 @@ describe('UserService', () => { let repo: jest.Mocked; beforeEach(() => { - const repoInstance = new UserRepository({} as any); + const mockPool = {} as jest.Mocked; + const repoInstance = new UserRepository(mockPool); repo = repoInstance as jest.Mocked; service = new UserService(repo); }); diff --git a/src/services/user.service.ts b/src/services/user.service.ts index ed626e8..7cdab43 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -125,8 +125,8 @@ export class UserService { return await this.userRepo.updateTokens(userData.id, userData.accessToken, userData.refreshToken); } - public getDecryptedTokens(userId: number, accessToken: string, refreshToken: string) { - return this.decryptTokens(userId, accessToken, refreshToken); + public getDecryptedTokens(groupId: number, accessToken: string, refreshToken: string) { + return this.decryptTokens(groupId, accessToken, refreshToken); } async create(userId: number, ip: string, userAgent: string): Promise { diff --git a/src/utils/generateRandomToken.util.ts b/src/utils/generateRandomToken.util.ts index d87232e..b04a3ef 100644 --- a/src/utils/generateRandomToken.util.ts +++ b/src/utils/generateRandomToken.util.ts @@ -1,8 +1,11 @@ +import crypto from 'crypto'; + export function generateRandomToken(length: number = 10): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; + const randomBytes = crypto.randomBytes(length); for (let i = 0; i < length; i++) { - result += chars.charAt(Math.floor(Math.random() * chars.length)); + result += chars.charAt(randomBytes[i] % chars.length); } return result; } \ No newline at end of file From d2d127af2f2037b90e8c50596c9163d10f630f80 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Wed, 30 Apr 2025 12:49:11 +0900 Subject: [PATCH 19/32] =?UTF-8?q?hotfix:=20process.env=20=EB=8C=80?= =?UTF-8?q?=EC=8B=A0=20=EC=9E=84=EC=9D=98=EC=9D=98=20=EB=82=9C=EC=88=98?= =?UTF-8?q?=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/__test__/qr.service.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/services/__test__/qr.service.test.ts b/src/services/__test__/qr.service.test.ts index 75d92b7..441dea7 100644 --- a/src/services/__test__/qr.service.test.ts +++ b/src/services/__test__/qr.service.test.ts @@ -3,6 +3,13 @@ import { UserRepository } from '@/repositories/user.repository'; import { DBError } from '@/exception'; import { QRLoginToken } from '@/types/models/QRLoginToken.type'; import { Pool } from 'pg'; +import crypto from 'crypto'; + +const validKey = crypto.randomBytes(32).toString('hex').slice(0, 32); + +jest.mock('@/utils/key.util', () => ({ + getKeyByGroup: () => validKey, +})); jest.mock('@/repositories/user.repository'); From fe68599649df95cea5356d1b70e127c835cb64ba Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Wed, 30 Apr 2025 13:03:58 +0900 Subject: [PATCH 20/32] =?UTF-8?q?refactor:=20lint=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/__test__/qr.service.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/__test__/qr.service.test.ts b/src/services/__test__/qr.service.test.ts index 441dea7..3999154 100644 --- a/src/services/__test__/qr.service.test.ts +++ b/src/services/__test__/qr.service.test.ts @@ -5,7 +5,7 @@ import { QRLoginToken } from '@/types/models/QRLoginToken.type'; import { Pool } from 'pg'; import crypto from 'crypto'; -const validKey = crypto.randomBytes(32).toString('hex').slice(0, 32); +const validKey = crypto.randomBytes(32).toString('utf8'); jest.mock('@/utils/key.util', () => ({ getKeyByGroup: () => validKey, From f8801e02170b58570efb1b81b3a0c27f76feda4e Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Wed, 30 Apr 2025 13:12:03 +0900 Subject: [PATCH 21/32] =?UTF-8?q?refactor:=20=EB=93=A4=EC=97=AC=EC=93=B0?= =?UTF-8?q?=EA=B8=B0=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/user.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 7cdab43..43d65ab 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -133,13 +133,13 @@ export class UserService { const token = generateRandomToken(10); await this.userRepo.createQRLoginToken(token, userId, ip, userAgent); return token; -} + } -async getByToken(token: string): Promise { + async getByToken(token: string): Promise { return await this.userRepo.findQRLoginToken(token); -} + } -async useToken(token: string): Promise { + async useToken(token: string): Promise { const qrToken = await this.userRepo.findQRLoginToken(token); if (!qrToken) { From 5613dbbc082b0abd200acb8e7936df51122cb18d Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Wed, 30 Apr 2025 15:34:02 +0900 Subject: [PATCH 22/32] =?UTF-8?q?hotfix:=20=EC=8A=AC=EB=9E=99=EB=8F=84=20m?= =?UTF-8?q?ocking=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/__test__/qr.service.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/__test__/qr.service.test.ts b/src/services/__test__/qr.service.test.ts index 3999154..09df9dd 100644 --- a/src/services/__test__/qr.service.test.ts +++ b/src/services/__test__/qr.service.test.ts @@ -11,6 +11,10 @@ jest.mock('@/utils/key.util', () => ({ getKeyByGroup: () => validKey, })); +jest.mock('@/modules/slack/slack.notifier', () => ({ + sendSlackMessage: jest.fn(), +})); + jest.mock('@/repositories/user.repository'); describe('UserService', () => { From 31b1a43f7bbee35d69b1c5ec482bd48c921ef7dc Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Fri, 2 May 2025 01:14:20 +0900 Subject: [PATCH 23/32] =?UTF-8?q?hotfix:=20=ED=8A=B9=EC=A0=95=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EA=B0=80=20=EC=99=84=EB=A3=8C=EB=90=9C=20?= =?UTF-8?q?=ED=9B=84=20=EC=A7=80=EC=9A=B0=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__test__/qr.repo.integration.test.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/repositories/__test__/qr.repo.integration.test.ts b/src/repositories/__test__/qr.repo.integration.test.ts index 844fdd5..e04e93e 100644 --- a/src/repositories/__test__/qr.repo.integration.test.ts +++ b/src/repositories/__test__/qr.repo.integration.test.ts @@ -6,7 +6,7 @@ import { generateRandomToken } from '@/utils/generateRandomToken.util'; import logger from '@/configs/logger.config'; dotenv.config(); -jest.setTimeout(30000); +jest.setTimeout(5000); describe('QRLoginTokenRepository 통합 테스트', () => { let testPool: Pool; @@ -44,12 +44,24 @@ describe('QRLoginTokenRepository 통합 테스트', () => { afterAll(async () => { try { + await testPool.query( + ` + DELETE FROM users_qrlogintoken + WHERE ip_address = '127.0.0.1' + AND user_agent = 'test-agent' + AND user_id = $1 + `, + [TEST_DATA.USER_ID] + ); + await new Promise(resolve => setTimeout(resolve, 1000)); + if (testPool) { await testPool.end(); } + await new Promise(resolve => setTimeout(resolve, 1000)); - logger.info('테스트 DB 연결 종료'); + logger.info('테스트 DB 연결 종료 및 테스트 데이터 정리 완료'); } catch (error) { logger.error('테스트 종료 중 오류:', error); } @@ -67,7 +79,9 @@ describe('QRLoginTokenRepository 통합 테스트', () => { expect(foundToken).not.toBeNull(); expect(foundToken?.token).toBe(token); expect(foundToken?.is_used).toBe(false); - expect(new Date(foundToken!.expires_at).getTime()).toBeGreaterThan(new Date(foundToken!.created_at).getTime()); + if (foundToken) { + expect(new Date(foundToken.expires_at).getTime()).toBeGreaterThan(new Date(foundToken.created_at).getTime()); + } }); it('존재하지 않는 토큰 조회 시 null을 반환해야 한다', async () => { From 6d6f918d4e397defe6007eb31c5481eef6310c97 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Fri, 2 May 2025 01:15:38 +0900 Subject: [PATCH 24/32] =?UTF-8?q?docs:=20Swagger=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/user.router.ts | 2 +- src/types/dto/responses/qrResponse.type.ts | 26 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/routes/user.router.ts b/src/routes/user.router.ts index 4cfe1ef..65a6427 100644 --- a/src/routes/user.router.ts +++ b/src/routes/user.router.ts @@ -131,7 +131,7 @@ router.post('/qr-login', authMiddleware.login, userController.createToken); /** * @swagger - * /api/qr-login: + * /qr-login: * get: * summary: QR 로그인 토큰 조회 및 자동 로그인 처리 * tags: [QRLogin] diff --git a/src/types/dto/responses/qrResponse.type.ts b/src/types/dto/responses/qrResponse.type.ts index ff88846..1a8a652 100644 --- a/src/types/dto/responses/qrResponse.type.ts +++ b/src/types/dto/responses/qrResponse.type.ts @@ -1,9 +1,35 @@ import { BaseResponseDto } from '@/types/dto/responses/baseResponse.type'; +/** + * @swagger + * components: + * schemas: + * QRLoginTokenResponseData: + * type: object + * properties: + * token: + * type: string + * minLength: 10 + * maxLength: 10 + * example: ABC123EFGH + * description: QR 로그인용 10자리 토큰 + */ type Token10 = string & { __lengthBrand: 10 }; export interface QRLoginTokenResponseData { token: Token10; } +/** + * @swagger + * components: + * schemas: + * QRLoginTokenResponseDto: + * allOf: + * - $ref: '#/components/schemas/BaseResponseDto' + * - type: object + * properties: + * data: + * $ref: '#/components/schemas/QRLoginTokenResponseData' + */ export class QRLoginTokenResponseDto extends BaseResponseDto {} From 3ce8994921c217815f7db1dcc615463856841873 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Fri, 2 May 2025 01:16:24 +0900 Subject: [PATCH 25/32] =?UTF-8?q?hotfix:=20=ED=86=A0=ED=81=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/__test__/qr.service.test.ts | 4 ++-- src/utils/generateRandomToken.util.ts | 20 ++++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/services/__test__/qr.service.test.ts b/src/services/__test__/qr.service.test.ts index 09df9dd..08c527c 100644 --- a/src/services/__test__/qr.service.test.ts +++ b/src/services/__test__/qr.service.test.ts @@ -17,7 +17,7 @@ jest.mock('@/modules/slack/slack.notifier', () => ({ jest.mock('@/repositories/user.repository'); -describe('UserService', () => { +describe('UserService 의 QRService', () => { let service: UserService; let repo: jest.Mocked; @@ -42,7 +42,7 @@ describe('UserService', () => { expect(typeof token).toBe('string'); expect(token.length).toBe(10); - expect(/^[A-Za-z0-9]{10}$/.test(token)).toBe(true); + expect(/^[A-Za-z0-9\-_.~!]{10}$/.test(token)).toBe(true); expect(repo.createQRLoginToken).toHaveBeenCalledWith(token, userId, ip, userAgent); }); diff --git a/src/utils/generateRandomToken.util.ts b/src/utils/generateRandomToken.util.ts index b04a3ef..67bb3b1 100644 --- a/src/utils/generateRandomToken.util.ts +++ b/src/utils/generateRandomToken.util.ts @@ -1,11 +1,15 @@ import crypto from 'crypto'; +const CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!'; +const CHARSET_LENGTH = CHARSET.length; + export function generateRandomToken(length: number = 10): string { - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - let result = ''; - const randomBytes = crypto.randomBytes(length); - for (let i = 0; i < length; i++) { - result += chars.charAt(randomBytes[i] % chars.length); - } - return result; - } \ No newline at end of file + const randomBytes = crypto.randomBytes(length); + const result = new Array(length); + + for (let i = 0; i < length; i++) { + result[i] = CHARSET[randomBytes[i] % CHARSET_LENGTH]; + } + + return result.join(''); +} \ No newline at end of file From 5c8f0a9f3b5f9571335752c00faa8adc88f3f679 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Fri, 2 May 2025 01:17:23 +0900 Subject: [PATCH 26/32] =?UTF-8?q?hotfix:=20=EC=8B=A4=EC=A0=9C=20=ED=81=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=EC=9D=98=20IP=EC=97=90=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20&=20logger=20=EA=B5=AC=EC=B2=B4=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.ts | 2 ++ src/controllers/user.controller.ts | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/app.ts b/src/app.ts index 327750b..8174eb1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -13,6 +13,8 @@ import { NotFoundError } from './exception'; dotenv.config(); const app: Application = express(); +// 실제 클라이언트 IP를 알기 위한 trust proxy 설정 +app.set('trust proxy', true); const swaggerSpec = swaggerJSDoc(options); app.use(cookieParser()); diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 8407e9f..56e0314 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -6,7 +6,7 @@ import { UserService } from '@/services/user.service'; import { InvalidTokenError, TokenExpiredError } from '@/exception/token.exception'; import { NotFoundError } from '@/exception'; -type Token32 = string & { __lengthBrand: 10 }; +type Token10 = string & { __lengthBrand: 10 }; export class UserController { constructor(private userService: UserService) { } @@ -116,11 +116,11 @@ export class UserController { ) => { try { const user = req.user; - const ip = req.ip ?? ''; + const ip = typeof req.headers['x-forwarded-for'] === 'string' ? req.headers['x-forwarded-for'].split(',')[0].trim() : req.ip ?? ''; const userAgent = req.headers['user-agent'] || ''; const token = await this.userService.create(user.id, ip, userAgent); - const typedToken = token as Token32; + const typedToken = token as Token10; const response = new QRLoginTokenResponseDto( true, @@ -130,7 +130,7 @@ export class UserController { ); res.status(200).json(response); } catch (error) { - logger.error('QR 토큰 생성 실패:', error); + logger.error(`QR 토큰 생성 실패: [userId: ${req.user?.id || 'anonymous'}]`, error); next(error); } }; @@ -164,7 +164,7 @@ export class UserController { res.redirect('/main'); } catch (error) { - logger.error('QR 토큰 로그인 처리 실패', error); + logger.error(`QR 토큰 로그인 처리 실패: [userId: ${req.user?.id || 'anonymous'}]`, error); next(error); } }; From 076949752ab47b36ce2de23f7ac0826b867f3459 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Fri, 2 May 2025 01:26:37 +0900 Subject: [PATCH 27/32] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/repositories/__test__/qr.repo.integration.test.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/repositories/__test__/qr.repo.integration.test.ts b/src/repositories/__test__/qr.repo.integration.test.ts index e04e93e..d18a048 100644 --- a/src/repositories/__test__/qr.repo.integration.test.ts +++ b/src/repositories/__test__/qr.repo.integration.test.ts @@ -76,12 +76,13 @@ describe('QRLoginTokenRepository 통합 테스트', () => { await repo.createQRLoginToken(token, TEST_DATA.USER_ID, ip, userAgent); const foundToken = await repo.findQRLoginToken(token); + // 토큰이 존재함을 확인하고 타입 단언 expect(foundToken).not.toBeNull(); - expect(foundToken?.token).toBe(token); - expect(foundToken?.is_used).toBe(false); - if (foundToken) { - expect(new Date(foundToken.expires_at).getTime()).toBeGreaterThan(new Date(foundToken.created_at).getTime()); - } + const nonNullToken = foundToken as NonNullable; + + expect(nonNullToken.token).toBe(token); + expect(nonNullToken.is_used).toBe(false); + expect(new Date(nonNullToken.expires_at).getTime()).toBeGreaterThan(new Date(nonNullToken.created_at).getTime()); }); it('존재하지 않는 토큰 조회 시 null을 반환해야 한다', async () => { From 624be8909cf0656ee26d85d20b3cf3845311e4d0 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Fri, 2 May 2025 17:41:14 +0900 Subject: [PATCH 28/32] =?UTF-8?q?docs:=20swagger=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/user.router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/user.router.ts b/src/routes/user.router.ts index 65a6427..a841540 100644 --- a/src/routes/user.router.ts +++ b/src/routes/user.router.ts @@ -117,7 +117,7 @@ router.get('/me', authMiddleware.login, userController.fetchCurrentUser); /** * @swagger - * /api/qr-login: + * /qr-login: * post: * summary: QR 로그인 토큰 생성 * tags: [QRLogin] From c20e48e7c981ca8af73e9e90b5095132ec8d9562 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Fri, 2 May 2025 17:41:53 +0900 Subject: [PATCH 29/32] =?UTF-8?q?modify:=20service=EC=9D=98=20getByToken?= =?UTF-8?q?=EC=9D=B4=20=EC=95=84=EB=8B=8C=20repo=EC=9D=98=20findQRLoginTok?= =?UTF-8?q?en=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/__test__/qr.service.test.ts | 6 +++--- src/services/user.service.ts | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/services/__test__/qr.service.test.ts b/src/services/__test__/qr.service.test.ts index 08c527c..2376407 100644 --- a/src/services/__test__/qr.service.test.ts +++ b/src/services/__test__/qr.service.test.ts @@ -66,19 +66,19 @@ describe('UserService 의 QRService', () => { }; repo.findQRLoginToken.mockResolvedValue(mockToken); - const result = await service.getByToken('token'); + const result = await repo.findQRLoginToken('token'); expect(result).toEqual(mockToken); }); it('토큰이 없으면 null 반환', async () => { repo.findQRLoginToken.mockResolvedValue(null); - const result = await service.getByToken('token'); + const result = await repo.findQRLoginToken('token'); expect(result).toBeNull(); }); it('조회 중 오류 발생 시 예외 발생', async () => { repo.findQRLoginToken.mockRejectedValueOnce(new DBError('조회 실패')); - await expect(service.getByToken('token')).rejects.toThrow('조회 실패'); + await expect(repo.findQRLoginToken('token')).rejects.toThrow('조회 실패'); }); }); diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 43d65ab..d212728 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -135,10 +135,6 @@ export class UserService { return token; } - async getByToken(token: string): Promise { - return await this.userRepo.findQRLoginToken(token); - } - async useToken(token: string): Promise { const qrToken = await this.userRepo.findQRLoginToken(token); From b08d834f0ae303f632c1fdfb7056cf5244a11c60 Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Sat, 3 May 2025 11:05:53 +0900 Subject: [PATCH 30/32] =?UTF-8?q?refactor:=20findByVelogUUID=20&=20getDecr?= =?UTF-8?q?yptedTokens=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=B3=91=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/user.controller.ts | 12 +++--------- src/services/user.service.ts | 21 ++++++++++++++------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 56e0314..3953ba3 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -146,15 +146,9 @@ export class UserController { if (!found) { throw new TokenExpiredError(); } - - const user = await this.userService.findByVelogUUID(found.user.toString()); - if (!user) throw new NotFoundError('유저를 찾을 수 없습니다.'); - - const { decryptedAccessToken, decryptedRefreshToken } = this.userService.getDecryptedTokens( - user.group_id, - user.access_token, - user.refresh_token - ); + + const { user, decryptedAccessToken, decryptedRefreshToken } = + await this.userService.findUserAndTokensByVelogUUID(found.user.toString()); res.clearCookie('access_token', this.cookieOption()); res.clearCookie('refresh_token', this.cookieOption()); diff --git a/src/services/user.service.ts b/src/services/user.service.ts index d212728..13e4073 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -53,7 +53,7 @@ export class UserService { async handleUserTokensByVelogUUID(userData: UserWithTokenDto) { const { id, email, accessToken, refreshToken } = userData; try { - let user = await this.findByVelogUUID(id); + let user = await this.userRepo.findByUserVelogUUID(id); if (!user) { user = await this.createUser({ @@ -82,10 +82,6 @@ export class UserService { } } - async findByVelogUUID(uuid: string): Promise { - return await this.userRepo.findByUserVelogUUID(uuid); - } - async findSampleUser(): Promise { const user = await this.userRepo.findSampleUser(); if (!user) { @@ -125,8 +121,19 @@ export class UserService { return await this.userRepo.updateTokens(userData.id, userData.accessToken, userData.refreshToken); } - public getDecryptedTokens(groupId: number, accessToken: string, refreshToken: string) { - return this.decryptTokens(groupId, accessToken, refreshToken); + async findUserAndTokensByVelogUUID(uuid: string): Promise<{ user: User; decryptedAccessToken: string; decryptedRefreshToken: string }> { + const user = await this.userRepo.findByUserVelogUUID(uuid); + if (!user) { + throw new NotFoundError('유저를 찾을 수 없습니다.'); + } + + const { decryptedAccessToken, decryptedRefreshToken } = this.decryptTokens( + user.group_id, + user.access_token, + user.refresh_token, + ); + + return { user, decryptedAccessToken, decryptedRefreshToken }; } async create(userId: number, ip: string, userAgent: string): Promise { From 21764eeec53b5bea7c9a494340533f5e2a44ac2b Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Sat, 3 May 2025 11:06:10 +0900 Subject: [PATCH 31/32] =?UTF-8?q?refactor:=20=EB=A0=88=ED=8F=AC=20?= =?UTF-8?q?=EA=B3=84=EC=B8=B5=EA=B3=BC=EC=9D=98=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/__test__/qr.service.test.ts | 29 ------------------------ 1 file changed, 29 deletions(-) diff --git a/src/services/__test__/qr.service.test.ts b/src/services/__test__/qr.service.test.ts index 2376407..9315fac 100644 --- a/src/services/__test__/qr.service.test.ts +++ b/src/services/__test__/qr.service.test.ts @@ -53,35 +53,6 @@ describe('UserService 의 QRService', () => { }); }); - describe('getByToken', () => { - it('유효한 토큰 조회 시 반환해야 한다', async () => { - const mockToken: QRLoginToken = { - token: 'token', - user: 1, - created_at: new Date(), - expires_at: new Date(), - is_used: false, - ip_address: '127.0.0.1', - user_agent: 'Chrome', - }; - repo.findQRLoginToken.mockResolvedValue(mockToken); - - const result = await repo.findQRLoginToken('token'); - expect(result).toEqual(mockToken); - }); - - it('토큰이 없으면 null 반환', async () => { - repo.findQRLoginToken.mockResolvedValue(null); - const result = await repo.findQRLoginToken('token'); - expect(result).toBeNull(); - }); - - it('조회 중 오류 발생 시 예외 발생', async () => { - repo.findQRLoginToken.mockRejectedValueOnce(new DBError('조회 실패')); - await expect(repo.findQRLoginToken('token')).rejects.toThrow('조회 실패'); - }); - }); - describe('useToken', () => { it('유효한 토큰 사용 처리 후 반환', async () => { const mockToken: QRLoginToken = { From c2dd34a5635cb69d37ac6cabe0ba1cc25ad5c02f Mon Sep 17 00:00:00 2001 From: Jihyun3478 Date: Sat, 3 May 2025 11:10:31 +0900 Subject: [PATCH 32/32] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20import=20&=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/user.controller.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 3953ba3..dab0d9b 100644 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -4,7 +4,6 @@ import { EmptyResponseDto, LoginResponseDto, UserWithTokenDto } from '@/types'; import { QRLoginTokenResponseDto } from '@/types/dto/responses/qrResponse.type'; import { UserService } from '@/services/user.service'; import { InvalidTokenError, TokenExpiredError } from '@/exception/token.exception'; -import { NotFoundError } from '@/exception'; type Token10 = string & { __lengthBrand: 10 }; @@ -147,7 +146,7 @@ export class UserController { throw new TokenExpiredError(); } - const { user, decryptedAccessToken, decryptedRefreshToken } = + const { decryptedAccessToken, decryptedRefreshToken } = await this.userService.findUserAndTokensByVelogUUID(found.user.toString()); res.clearCookie('access_token', this.cookieOption());