-
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 2 commits
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
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.
Show resolved
Hide resolved
|
||||||||||||||||||||||
} | ||||||||||||||||||||||
Nuung marked this conversation as resolved.
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.
Show resolved
Hide resolved
Jihyun3478 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
|
||||||||||||||||||||||
res.status(200).json({ success: true, message: '์ ํจํ QR ํ ํฐ์ ๋๋ค.', token: found }); | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐ ๏ธ Refactor suggestion ๋ณด์์ ์ํด ํ ํฐ ์๋ต ๋ฐ์ดํฐ๋ฅผ ์ ํํ์ธ์. ํ์ฌ ์๋ต์๋ ์ ์ฒด ํ ํฐ ๊ฐ์ฒด๊ฐ ํฌํจ๋์ด ์์ด ๋ฏผ๊ฐํ ์ ๋ณด๊ฐ ๋ ธ์ถ๋ ์ ์์ต๋๋ค. ํ์ํ ์ ๋ณด๋ง ๋ฐํํ๋๋ก ์์ ํ๋ ๊ฒ์ด ์ข์ต๋๋ค. ๋ค์๊ณผ ๊ฐ์ด ์๋ต ๋ฐ์ดํฐ๋ฅผ ์ ํํ์ธ์: - 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
+ }
+ }); ๐ Committable suggestion
Suggested change
|
||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||
logger.error('QR ํ ํฐ ์กฐํ ์คํจ', error); | ||||||||||||||||||||||
next(error); | ||||||||||||||||||||||
} | ||||||||||||||||||||||
}; | ||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
Jihyun3478 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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, | ||
}; | ||
} | ||
Jihyun3478 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 ์ ์ ๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค.'); | ||
} | ||
Jihyun3478 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
repo = new QRLoginTokenRepository(pool); | ||
} catch (error) { | ||
console.error('QR ํ ์คํธ ์ค์ ์ค ์ค๋ฅ:', error); | ||
throw error; | ||
} | ||
Jihyun3478 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
|
||
afterAll(async () => { | ||
await pool.end(); | ||
}); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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); | ||
}); | ||
Jihyun3478 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
it('should return null for expired or used token', async () => { | ||
const result = await repo.findQRLoginToken('invalid-token'); | ||
expect(result).toBeNull(); | ||
}); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); |
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> { | ||
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: | ||
* 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: | ||
* 200: | ||
* description: ์ ํจํ ํ ํฐ | ||
* 404: | ||
* description: ํ ํฐ ์์ or ๋ง๋ฃ | ||
*/ | ||
router.get('/qr-login', qrController.getToken); | ||
|
||
export default router; |
Uh oh!
There was an error while loading. Please reload this page.