- Notifications
You must be signed in to change notification settings - Fork 0
[25.04.21 / TASK-148] Feature - qrcode app & QRCode API #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
6d3b0ec 3b6adaa b70a582 56c2283 ac39619 d118b16 f6f3936 065879c 3faf173 a007870 a95635e 245a721 4fd26ac c7e9019 51a0162 5932dac 85b502c 37b9633 3440fa2 d2d127a fe68599 f8801e0 5613dbb 31b1a43 6d6f918 3ce8994 5c8f0a9 0769497 624be89 c20e48e b08d834 21764ee c2dd34a File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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<QRLoginTokenResponseDto>, | ||||||||||||||||||||||
| 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: 'ํ ํฐ์ด ํ์ํฉ๋๋ค.' }); | ||||||||||||||||||||||
Jihyun3478 marked this conversation as resolved. Outdated Show resolved Hide resolved | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
Nuung marked this conversation as resolved. Outdated Show resolved Hide resolved | ||||||||||||||||||||||
| | ||||||||||||||||||||||
| const found = await this.qrService.getByToken(token); | ||||||||||||||||||||||
| | ||||||||||||||||||||||
| if (!found) { | ||||||||||||||||||||||
| res.status(404).json({ success: false, message: '์ ํจํ์ง ์๊ฑฐ๋ ๋ง๋ฃ๋ ํ ํฐ์ ๋๋ค.' }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
Nuung marked this conversation as resolved. Outdated Show resolved Hide resolved Jihyun3478 marked this conversation as resolved. Outdated Show resolved Hide resolved | ||||||||||||||||||||||
| | ||||||||||||||||||||||
| res.status(200).json({ success: true, message: '์ ํจํ QR ํ ํฐ์ ๋๋ค.', token: found }); | ||||||||||||||||||||||
| ||||||||||||||||||||||
| res.status(200).json({ success: true, message: '์ ํจํ QR ํ ํฐ์ ๋๋ค.', token: found }); | |
| res.status(200).json({ | |
| success: true, | |
| message: '์ ํจํ QR ํ ํฐ์ ๋๋ค.', | |
| data: { | |
| token: found.token, | |
| user: found.user, | |
| expires_at: found.expires_at | |
| } | |
| }); |
Jihyun3478 marked this conversation as resolved. Show resolved Hide resolved |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { QRLoginTokenRepository } from '@/repositories/qr.repository'; | ||
| import { DBError } from '@/exception'; | ||
| import { Pool } from 'pg'; | ||
| | ||
| const mockPool: Partial<Pool> = { | ||
| 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); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<void> { | ||
| Check failure on line 9 in src/repositories/qr.repository.ts | ||
coderabbitai[bot] marked this conversation as resolved. Outdated Show resolved Hide resolved | ||
| 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<QRLoginToken | null> { | ||
| 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 ์ฝ๋ ํ ํฐ ์กฐํ ์ค ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.'); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
coderabbitai[bot] marked this conversation as resolved. Outdated Show resolved Hide resolved | ||
| | ||
| /** | ||
| * @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; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<QRLoginTokenRepository>; | ||
| | ||
| beforeEach(() => { | ||
| const repoInstance = new QRLoginTokenRepository({} as any) | ||
| repo = repoInstance as jest.Mocked<QRLoginTokenRepository>; | ||
| 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); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string> { | ||
| const token = randomUUID(); | ||
Jihyun3478 marked this conversation as resolved. Outdated Show resolved Hide resolved | ||
| await this.qrRepo.createQRLoginToken(token, userId, ip, userAgent); | ||
| return token; | ||
| } | ||
| | ||
| async getByToken(token: string): Promise<QRLoginToken | null> { | ||
| return await this.qrRepo.findQRLoginToken(token); | ||
| } | ||
| } | ||
Jihyun3478 marked this conversation as resolved. Show resolved Hide resolved |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import { BaseResponseDto } from '@/types/dto/responses/baseResponse.type'; | ||
| | ||
| interface QRLoginTokenResponseData { | ||
| token: string; | ||
| } | ||
| | ||
| export class QRLoginTokenResponseDto extends BaseResponseDto<QRLoginTokenResponseData> { } | ||
Jihyun3478 marked this conversation as resolved. Outdated Show resolved Hide resolved | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| export interface QRLoginToken { | ||
| token: string; | ||
| user: number; | ||
| created_at: Date; | ||
| expires_at: Date; | ||
Jihyun3478 marked this conversation as resolved. Outdated Show resolved Hide resolved | ||
| is_used: boolean; | ||
| ip_address: string; | ||
| user_agent: string; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.