Skip to content

Commit a007870

Browse files
committed
hotfix: 비즈니스 로직 흐름에 맞게 수정
비즈니스 로직 흐름에 맞게 수정 - 쿠키 값 확인 및 유효성 체크 - 쿠키 설정 및 자동 로그인 - 메인 페이지 리다이렉션
1 parent 3faf173 commit a007870

File tree

6 files changed

+158
-46
lines changed

6 files changed

+158
-46
lines changed

src/controllers/qr.controller.ts

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,36 @@ import logger from '@/configs/logger.config';
33
import { QRLoginTokenService } from "@/services/qr.service";
44
import { QRLoginTokenResponseDto } from "@/types/dto/responses/qrResponse.type";
55
import { InvalidTokenError, TokenExpiredError } from '@/exception/token.exception';
6+
import { UserService } from '@/services/user.service';
7+
import { NotFoundError } from '@/exception';
8+
import { CookieOptions } from 'express';
69

710
// TODO: randomUUID() 기반으로 길이 36
811
type Token32 = string & { __lengthBrand: 32 };
912

1013
export class QRLoginController {
11-
constructor(private qrService: QRLoginTokenService) {}
14+
constructor(
15+
private qrService: QRLoginTokenService,
16+
private userService: UserService
17+
) {}
18+
19+
private cookieOption(): CookieOptions {
20+
const isProd = process.env.NODE_ENV === 'production';
21+
22+
const baseOptions: CookieOptions = {
23+
httpOnly: isProd,
24+
secure: isProd,
25+
};
26+
27+
if (isProd) {
28+
baseOptions.sameSite = 'lax';
29+
baseOptions.domain = "velog-dashboard.kro.kr";
30+
} else {
31+
baseOptions.domain = 'localhost';
32+
}
33+
34+
return baseOptions;
35+
}
1236

1337
createToken: RequestHandler = async (
1438
req: Request,
@@ -21,7 +45,6 @@ export class QRLoginController {
2145
const userAgent = req.headers['user-agent'] || '';
2246

2347
const token = await this.qrService.create(user.id, ip, userAgent);
24-
2548
const typedToken = token as Token32;
2649

2750
const response = new QRLoginTokenResponseDto(
@@ -32,28 +55,41 @@ export class QRLoginController {
3255
);
3356
res.status(200).json(response);
3457
} catch (error) {
35-
logger.error('생성 실패:', error);
58+
logger.error('QR 토큰 생성 실패:', error);
3659
next(error);
3760
}
3861
};
3962

40-
getToken: RequestHandler = async (req, res, next) => {
63+
getToken: RequestHandler = async (req: Request, res: Response, next: NextFunction) => {
4164
try {
4265
const token = req.query.token as string;
43-
4466
if (!token) {
4567
throw new InvalidTokenError('토큰이 필요합니다.');
4668
}
4769

48-
const found = await this.qrService.getByToken(token);
49-
70+
const found = await this.qrService.useToken(token);
5071
if (!found) {
5172
throw new TokenExpiredError();
5273
}
5374

54-
res.status(200).json({ success: true, message: '유효한 QR 토큰입니다.', token: found });
75+
const user = await this.userService.findByVelogUUID(found.user.toString());
76+
if (!user) throw new NotFoundError('유저를 찾을 수 없습니다.');
77+
78+
const { decryptedAccessToken, decryptedRefreshToken } = this.userService['decryptTokens'](
79+
user.group_id,
80+
user.access_token,
81+
user.refresh_token
82+
);
83+
84+
res.clearCookie('access_token', this.cookieOption());
85+
res.clearCookie('refresh_token', this.cookieOption());
86+
87+
res.cookie('access_token', decryptedAccessToken, this.cookieOption());
88+
res.cookie('refresh_token', decryptedRefreshToken, this.cookieOption());
89+
90+
res.redirect('/main');
5591
} catch (error) {
56-
logger.error('QR 토큰 조회 실패', error);
92+
logger.error('QR 토큰 로그인 처리 실패', error);
5793
next(error);
5894
}
5995
};

src/repositories/__test__/qr.repo.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,22 @@ describe('QRLoginTokenRepository', () => {
5959
await expect(repo.findQRLoginToken('token')).rejects.toThrow(DBError);
6060
});
6161
});
62+
63+
describe('markTokenUsed', () => {
64+
it('토큰을 사용 처리해야 한다', async () => {
65+
(mockPool.query as jest.Mock).mockResolvedValueOnce(undefined);
66+
67+
await expect(repo.markTokenUsed('token')).resolves.not.toThrow();
68+
expect(mockPool.query).toHaveBeenCalledWith(
69+
expect.stringContaining('UPDATE qr_login_tokens SET is_used = true'),
70+
['token']
71+
);
72+
});
73+
74+
it('토큰 사용 처리 중 오류 발생 시 DBError를 던져야 한다', async () => {
75+
(mockPool.query as jest.Mock).mockRejectedValueOnce(new Error('fail'));
76+
77+
await expect(repo.markTokenUsed('token')).rejects.toThrow(DBError);
78+
});
79+
});
6280
});

src/repositories/qr.repository.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,16 @@ export class QRLoginTokenRepository {
3333
throw new DBError('QR 코드 토큰 조회 중 문제가 발생했습니다.');
3434
}
3535
}
36+
37+
async markTokenUsed(token: string): Promise<void> {
38+
try {
39+
const query = `
40+
UPDATE qr_login_tokens SET is_used = true WHERE token = $1;
41+
`;
42+
await this.pool.query(query, [token]);
43+
} catch (error) {
44+
logger.error('QRLoginToken Repo mark as used Error : ', error);
45+
throw new DBError('QR 코드 사용 처리 중 문제가 발생했습니다.');
46+
}
47+
}
3648
}

src/routes/qr.router.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ import { authMiddleware } from '@/middlewares/auth.middleware';
55
import { QRLoginTokenRepository } from '@/repositories/qr.repository';
66
import { QRLoginTokenService } from '@/services/qr.service';
77
import { QRLoginController } from '@/controllers/qr.controller';
8+
import { UserRepository } from '@/repositories/user.repository';
9+
import { UserService } from '@/services/user.service';
810

911
const router: Router = express.Router();
1012

1113
const qrRepository = new QRLoginTokenRepository(pool);
14+
const userRepository = new UserRepository(pool);
15+
const userService = new UserService(userRepository);
1216
const qrService = new QRLoginTokenService(qrRepository);
13-
const qrController = new QRLoginController(qrService);
17+
const qrController = new QRLoginController(qrService, userService);
1418

1519
/**
1620
* @swagger
@@ -30,7 +34,7 @@ router.post('/qr-login', authMiddleware.login, qrController.createToken);
3034
* @swagger
3135
* /api/qr-login:
3236
* get:
33-
* summary: QR 로그인 토큰 조회
37+
* summary: QR 로그인 토큰 조회 및 자동 로그인 처리
3438
* tags: [QRLogin]
3539
* parameters:
3640
* - in: query
@@ -40,10 +44,12 @@ router.post('/qr-login', authMiddleware.login, qrController.createToken);
4044
* type: string
4145
* description: 조회할 QR 토큰
4246
* responses:
43-
* 200:
44-
* description: 유효한 토큰
47+
* 302:
48+
* description: 자동 로그인 완료 후 메인 페이지로 리디렉션
49+
* 400:
50+
* description: 잘못된 토큰
4551
* 404:
46-
* description: 토큰 없음 or 만료
52+
* description: 만료 또는 존재하지 않는 토큰
4753
*/
4854
router.get('/qr-login', qrController.getToken);
4955

src/services/__test__/qr.service.test.ts

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe('QRLoginTokenService', () => {
1010
let repo: jest.Mocked<QRLoginTokenRepository>;
1111

1212
beforeEach(() => {
13-
const repoInstance = new QRLoginTokenRepository({} as any)
13+
const repoInstance = new QRLoginTokenRepository({} as any);
1414
repo = repoInstance as jest.Mocked<QRLoginTokenRepository>;
1515
service = new QRLoginTokenService(repo);
1616
});
@@ -33,51 +33,80 @@ describe('QRLoginTokenService', () => {
3333
});
3434

3535
it('QR 토큰 생성 중 오류 발생 시 예외 발생', async () => {
36-
const userId = 1;
37-
const ip = '127.0.0.1';
38-
const userAgent = 'Mozilla';
3936
repo.createQRLoginToken.mockRejectedValueOnce(new DBError('생성 실패'));
4037

41-
await expect(service.create(userId, ip, userAgent)).rejects.toThrow('생성 실패');
42-
expect(repo.createQRLoginToken).toHaveBeenCalled();
38+
await expect(service.create(1, 'ip', 'agent')).rejects.toThrow('생성 실패');
4339
});
4440
});
4541

4642
describe('getByToken', () => {
47-
const mockToken = 'sample-token';
48-
const mockQRToken: QRLoginToken = {
49-
token: mockToken,
50-
user: 1,
51-
created_at: new Date(),
52-
expires_at: new Date(Date.now() + 1000 * 60 * 5),
53-
is_used: false,
54-
ip_address: '127.0.0.1',
55-
user_agent: 'Chrome',
56-
};
57-
58-
it('유효한 토큰 조회 시 QRLoginToken 반환', async () => {
59-
repo.findQRLoginToken.mockResolvedValue(mockQRToken);
60-
61-
const result = await service.getByToken(mockToken);
62-
63-
expect(result).toEqual(mockQRToken);
64-
expect(repo.findQRLoginToken).toHaveBeenCalledWith(mockToken);
43+
it('유효한 토큰 조회 시 반환해야 한다', async () => {
44+
const mockToken: QRLoginToken = {
45+
token: 'token',
46+
user: 1,
47+
created_at: new Date(),
48+
expires_at: new Date(),
49+
is_used: false,
50+
ip_address: '127.0.0.1',
51+
user_agent: 'Chrome',
52+
};
53+
repo.findQRLoginToken.mockResolvedValue(mockToken);
54+
55+
const result = await service.getByToken('token');
56+
expect(result).toEqual(mockToken);
6557
});
6658

67-
it('토큰이 없을 경우 null 반환', async () => {
59+
it('토큰이 없으면 null 반환', async () => {
6860
repo.findQRLoginToken.mockResolvedValue(null);
69-
70-
const result = await service.getByToken(mockToken);
71-
61+
const result = await service.getByToken('token');
7262
expect(result).toBeNull();
73-
expect(repo.findQRLoginToken).toHaveBeenCalledWith(mockToken);
7463
});
7564

76-
it('토큰 조회 중 오류 발생 시 예외 발생', async () => {
65+
it('조회 중 오류 발생 시 예외 발생', async () => {
7766
repo.findQRLoginToken.mockRejectedValueOnce(new DBError('조회 실패'));
67+
await expect(service.getByToken('token')).rejects.toThrow('조회 실패');
68+
});
69+
});
70+
71+
describe('useToken', () => {
72+
it('유효한 토큰 사용 처리 후 반환', async () => {
73+
const mockToken: QRLoginToken = {
74+
token: 'token',
75+
user: 1,
76+
created_at: new Date(),
77+
expires_at: new Date(),
78+
is_used: false,
79+
ip_address: '127.0.0.1',
80+
user_agent: 'Chrome',
81+
};
82+
repo.findQRLoginToken.mockResolvedValue(mockToken);
83+
84+
const result = await service.useToken('token');
85+
86+
expect(result).toEqual(mockToken);
87+
expect(repo.markTokenUsed).toHaveBeenCalledWith('token');
88+
});
89+
90+
it('토큰이 존재하지 않으면 null 반환', async () => {
91+
repo.findQRLoginToken.mockResolvedValue(null);
92+
const result = await service.useToken('token');
93+
expect(result).toBeNull();
94+
});
7895

79-
await expect(service.getByToken(mockToken)).rejects.toThrow('조회 실패');
80-
expect(repo.findQRLoginToken).toHaveBeenCalledWith(mockToken);
96+
it('markTokenUsed 호출 시 예외 발생하면 전파', async () => {
97+
const mockToken: QRLoginToken = {
98+
token: 'token',
99+
user: 1,
100+
created_at: new Date(),
101+
expires_at: new Date(),
102+
is_used: false,
103+
ip_address: '127.0.0.1',
104+
user_agent: 'Chrome',
105+
};
106+
repo.findQRLoginToken.mockResolvedValue(mockToken);
107+
repo.markTokenUsed.mockRejectedValueOnce(new DBError('사용 처리 실패'));
108+
109+
await expect(service.useToken('token')).rejects.toThrow('사용 처리 실패');
81110
});
82111
});
83112
});

src/services/qr.service.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,15 @@ export class QRLoginTokenService {
1414
async getByToken(token: string): Promise<QRLoginToken | null> {
1515
return await this.qrRepo.findQRLoginToken(token);
1616
}
17+
18+
async useToken(token: string): Promise<QRLoginToken | null> {
19+
const qrToken = await this.qrRepo.findQRLoginToken(token);
20+
21+
if (!qrToken) {
22+
return null;
23+
}
24+
25+
await this.qrRepo.markTokenUsed(token);
26+
return qrToken;
27+
}
1728
}

0 commit comments

Comments
 (0)