dh_demo

DreamHanks demo project
git clone git://git.lair.cx/dh_demo
Log | Files | Refs | README

commit 12754f719bb25aa0b1fae3877e44533fe292b8df
parent 2e2e48617364f07ae8d37c3e9d98023d2301b840
Author: Yongbin Kim <iam@yongbin.kim>
Date:   Sun, 29 Jan 2023 14:42:05 +0900

refactor(model): db.query 대신 modelBehaviour를 사용하도록 수정

Signed-off-by: Yongbin Kim <iam@yongbin.kim>

Diffstat:
Mlib/models/login_info.ts | 23++++++++++++++++-------
Mlib/models/signup_request.ts | 43++++++++++++++++++++++---------------------
Mlib/models/user_profile.ts | 29++++++++++++++++-------------
Mlib/models/wiki_info.ts | 6+++---
Mpages/api/users/[id]/index.ts | 2+-
Mpages/api/users/signup/[id].ts | 44++++++++++++++++++++++++++------------------
Mpages/api/users/signup/index.ts | 2+-
7 files changed, 85 insertions(+), 64 deletions(-)

diff --git a/lib/models/login_info.ts b/lib/models/login_info.ts @@ -17,8 +17,14 @@ const SQL_GET_LOGIN_INFO = ` LIMIT 1 ` -export async function getLoginInfo (id: number): Promise<LoginInfo | null> { - const [rows] = await db.query<RowDataPacket[]>(SQL_GET_LOGIN_INFO, [id]) +export const getLoginInfo = modelBehaviour< + [id: number], + LoginInfo | null +>(async (conn, args) => { + const [rows] = await conn.query<RowDataPacket[]>( + SQL_GET_LOGIN_INFO, + args, + ) if (rows.length === 0) { return null } @@ -32,7 +38,7 @@ export async function getLoginInfo (id: number): Promise<LoginInfo | null> { createdAt: row[3], updatedAt: row[4], } -} +}) const SQL_GET_LOGIN_INFO_VIA_EMAIL = ` SELECT id, email, password_hash, created_at, updated_at @@ -68,10 +74,13 @@ const SQL_CREATE_LOGIN_INFO = ` VALUES (?, ?) ` -export async function createLoginInfo (email: string, passwordHash: string): Promise<number> { - const [result] = await db.query<OkPacket>({ +export const createLoginInfo = modelBehaviour< + [email: string, passwordHash: string], + number +>(async (conn, args) => { + const [result] = await conn.query<OkPacket>({ sql: SQL_CREATE_LOGIN_INFO, - }, [email, passwordHash]) + }, args) return result.insertId -} +}) diff --git a/lib/models/signup_request.ts b/lib/models/signup_request.ts @@ -1,4 +1,5 @@ import db from '@/lib/db' +import { modelBehaviour } from '@/lib/model_helpers' import { OkPacket, RowDataPacket } from 'mysql2' export interface SignupRequest { @@ -15,17 +16,16 @@ const SQL_CREATE_SIGNUP_REQUEST = ` VALUES (?, ?, ?) ` -export async function createSignupRequest ( - email: string, - requestedIP: string, - requestedUserAgent: string, -): Promise<number> { - const [result] = await db.query<OkPacket>({ +export const createSignupRequest = modelBehaviour< + [ email: string, requestedIP: string, requestedUserAgent: string ], + number +>(async (conn, args) => { + const [result] = await conn.query<OkPacket>({ sql: SQL_CREATE_SIGNUP_REQUEST, - }, [email, requestedIP, requestedUserAgent]) + }, args) return result.insertId -} +}) const SQL_GET_UNCONFIRMED_SIGNUP_REQUEST = ` SELECT id, email, requested_ip, requested_user_agent, created_at, confirmed_at @@ -36,21 +36,19 @@ const SQL_GET_UNCONFIRMED_SIGNUP_REQUEST = ` LIMIT 1 ` -export async function getUnconfirmedSignupRequest (id: number | string): Promise<SignupRequest | null> { - if (typeof id === 'string') { - id = parseInt(id, 10) - } - - const [rows] = await db.query<RowDataPacket[]>({ +export const getUnconfirmedSignupRequest = modelBehaviour< + [ id: number ], + SignupRequest | null +>(async (conn, args) => { + const [rows] = await conn.query<RowDataPacket[]>({ sql: SQL_GET_UNCONFIRMED_SIGNUP_REQUEST, - }, [id]) + }, args) if (rows.length === 0) { return null } const row = rows[0] - console.log(row) return { id: row[0], email: row[1], @@ -59,7 +57,7 @@ export async function getUnconfirmedSignupRequest (id: number | string): Promise createdAt: row[4], confirmedAt: row[5], } -} +}) const SQL_CONFIRM_SIGNUP_REQUEST = ` UPDATE signup_requests @@ -67,8 +65,11 @@ const SQL_CONFIRM_SIGNUP_REQUEST = ` WHERE id = ? ` -export async function confirmSignupRequest (id: number): Promise<void> { - await db.query<OkPacket>({ +export const confirmSignupRequest = modelBehaviour< + [ id: number ], + void +>(async (conn, args) => { + await conn.query<OkPacket>({ sql: SQL_CONFIRM_SIGNUP_REQUEST, - }, [id]) -} + }, args) +}) diff --git a/lib/models/user_profile.ts b/lib/models/user_profile.ts @@ -1,4 +1,5 @@ import db from '@/lib/db' +import { modelBehaviour } from '@/lib/model_helpers' import { OkPacket, RowDataPacket } from 'mysql2' export interface UserProfile { @@ -13,15 +14,14 @@ const SQL_CREATE_USER_PROFILE = ` VALUES (?, ?, ?) ` -export async function createUserProfile ( - loginId: number, - nickname: string, - bio: string | null, -) { - await db.query<OkPacket>({ +export const createUserProfile = modelBehaviour< + [ loginId: number, nickname: string, bio: string | null ], + void +>(async (conn, args) => { + await conn.query<OkPacket>({ sql: SQL_CREATE_USER_PROFILE, - }, [loginId, nickname, bio]) -} + }, args) +}) const SQL_GET_USER_PROFILE = ` SELECT login_id, nickname, bio, updated_at @@ -30,13 +30,16 @@ const SQL_GET_USER_PROFILE = ` LIMIT 1 ` -export async function getUserProfile (loginId: number): Promise<UserProfile | null> { - const [rows] = await db.query<RowDataPacket[]>({ +export const getUserProfile = modelBehaviour< + [ loginId: number ], + UserProfile | null +>(async (conn, args) => { + const [rows] = await conn.query<RowDataPacket[]>({ sql: SQL_GET_USER_PROFILE, rowsAsArray: true, - }, [loginId]) + }, args) - if (!Array.isArray(rows) || rows.length === 0) { + if (rows.length === 0) { return null } @@ -47,4 +50,4 @@ export async function getUserProfile (loginId: number): Promise<UserProfile | nu bio: row[2], updatedAt: row[3], } -} +}) diff --git a/lib/models/wiki_info.ts b/lib/models/wiki_info.ts @@ -58,7 +58,7 @@ export const listWikiByOwnerId = modelBehaviour< [ ownerId: number ], WikiInfo[] >(async (conn, args) => { - const [rows] = await db.query<RowDataPacket[]>({ + const [rows] = await conn.query<RowDataPacket[]>({ sql: SQL_LIST_WIKI_BY_OWNER_ID, }, [args]) @@ -84,7 +84,7 @@ export const getWiki = modelBehaviour< [ id: number ], WikiInfo | null >(async (conn, args) => { - const [rows] = await db.query<RowDataPacket[]>({ + const [rows] = await conn.query<RowDataPacket[]>({ sql: SQL_GET_WIKI, }, [args]) @@ -116,7 +116,7 @@ export const getWikiViaSlug = modelBehaviour< [slug: string], WikiInfo | null >(async (conn, args) => { - const [rows] = await db.query<RowDataPacket[]>({ + const [rows] = await conn.query<RowDataPacket[]>({ sql: SQL_GET_WIKI_VIA_SLUG, }, args) diff --git a/pages/api/users/[id]/index.ts b/pages/api/users/[id]/index.ts @@ -32,7 +32,7 @@ export default async function handler ( } // 사용자 정보를 받아옴 - const profile = await getUserProfile(id) + const profile = await getUserProfile([id]) if (profile == null) { res.status(404).json(ERR_NOT_FOUND) return diff --git a/pages/api/users/signup/[id].ts b/pages/api/users/signup/[id].ts @@ -1,10 +1,12 @@ -import { ApiError, ERR_METHOD_NOT_ALLOWED } from '@/lib/apierror' +import { ApiError, ERR_INTERNAL, ERR_METHOD_NOT_ALLOWED } from '@/lib/apierror' +import { withConnection } from '@/lib/model_helpers' import { createLoginInfo } from '@/lib/models/login_info' import { confirmSignupRequest, getUnconfirmedSignupRequest, } from '@/lib/models/signup_request' import { createUserProfile } from '@/lib/models/user_profile' +import { parseIntOrDefault } from '@/lib/utils/number' import bcrypt from 'bcrypt' import { NextApiRequest, NextApiResponse } from 'next' @@ -22,8 +24,8 @@ export default async function handler ( return } - const { id } = req.query - if (typeof id !== 'string') { + const id = parseIntOrDefault(req.query.id, -1) + if (id < 0) { res.status(500) .json({ code: 'internal error', message: 'id is null' }) return @@ -45,24 +47,30 @@ export default async function handler ( return } - // 요청 정보가 존재하는지 확인 - const signupRequest = await getUnconfirmedSignupRequest(id) - if (signupRequest == null) { - res.status(404).json({ - code: 'signup_request_not_found', - message: 'signup request was not found or expired', - }) - return - } - // 패스워드 해시 const passwordHash = await bcrypt.hash(password, 10) - // TODO: 트랜잭션으로 묶기 - await confirmSignupRequest(signupRequest.id) + await withConnection(async (conn) => { + try { + // 요청 정보가 존재하는지 확인 + const signupRequest = await getUnconfirmedSignupRequest(conn, [id]) + if (signupRequest == null) { + res.status(404).json({ + code: 'signup_request_not_found', + message: 'signup request was not found or expired', + }) + return + } + + await confirmSignupRequest(conn, [signupRequest.id]) - const userId = await createLoginInfo(signupRequest.email, passwordHash) - await createUserProfile(userId, nickname, null) + const userId = await createLoginInfo(conn, [signupRequest.email, passwordHash]) + await createUserProfile(conn, [userId, nickname, null]) - res.status(201).json({ status: 'ok' }) + res.status(201).json({ status: 'ok' }) + } catch (e) { + console.error('failed to confirm signup request', e) + res.status(500).json(ERR_INTERNAL) + } + }) } diff --git a/pages/api/users/signup/index.ts b/pages/api/users/signup/index.ts @@ -20,7 +20,7 @@ export default async function handler ( const requestedIP = getIPAddress(req) const requestedUserAgent = req.headers['user-agent'] ?? '' - const requestID = await createSignupRequest(email, requestedIP, requestedUserAgent) + const requestID = await createSignupRequest([email, requestedIP, requestedUserAgent]) const requestURL = `${process.env.WIKI_SITE_URL ?? 'http://localhost'}/users/signup/${requestID}` await sendEmail(