dh_demo

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

token.ts (2428B)


      1 import { ApiError } from '@/lib/apierror'
      2 import { getAccessTokenCookieName, getRefreshTokenCookieName } from '@/lib/env'
      3 import { withConnection } from '@/lib/model_helpers'
      4 import { getLoginInfoViaEmail } from '@/lib/models/login_info'
      5 import { getUserACLGroups } from '@/lib/models/user_acl_groups'
      6 import { comparePasswordHash } from '@/lib/security/password'
      7 import { putSession } from '@/lib/security/session'
      8 import { REFRESH_TOKEN_EXPIRES_IN, signToken, TOKEN_EXPIRES_IN } from '@/lib/security/token'
      9 import { nanoid } from 'nanoid'
     10 import type { NextApiRequest, NextApiResponse } from 'next'
     11 
     12 export interface TokenRequest {
     13   email: string
     14   password: string
     15 }
     16 
     17 export interface TokenResponse {
     18   expires: number
     19 }
     20 
     21 const ERR_INVALID_CREDENTIALS: ApiError = {
     22   code: 'invalid_credentials',
     23   message: 'Email or password is incorrect',
     24 }
     25 
     26 export default async function handler (
     27   req: NextApiRequest,
     28   res: NextApiResponse<TokenResponse | ApiError>,
     29 ) {
     30   if (req.method !== 'POST') {
     31     res.status(405).json({
     32       code: 'method_not_allowed',
     33       message: 'Method Not Allowed',
     34     })
     35     return
     36   }
     37 
     38   // 사용자 정보를 가져옴
     39   const { email, password } = req.body as TokenRequest
     40 
     41   await withConnection(async (conn) => {
     42     const loginInfo = await getLoginInfoViaEmail(conn, [email])
     43     if (loginInfo == null) {
     44       res.status(401).json(ERR_INVALID_CREDENTIALS)
     45       return
     46     }
     47 
     48     if (!await comparePasswordHash(password, loginInfo.passwordHash)) {
     49       res.status(401).json(ERR_INVALID_CREDENTIALS)
     50       return
     51     }
     52 
     53     const tid = nanoid()
     54     const aclGroups = await getUserACLGroups(conn, [loginInfo.id])
     55 
     56     await putSession({
     57       id: tid,
     58       uid: loginInfo.id,
     59       aclGroups: aclGroups,
     60     })
     61 
     62     // JWT 토큰 발급
     63     await signAndSendToken(res, tid, loginInfo.id)
     64   })
     65 }
     66 
     67 /**
     68  * 토큰에 서명하고 클라이언트에 보냅니다.
     69  */
     70 export async function signAndSendToken (
     71   res: NextApiResponse<TokenResponse>,
     72   tid: string,
     73   uid: number,
     74 ) {
     75   const [accessToken, refreshToken] = signToken(tid, uid)
     76 
     77   res.setHeader('Set-Cookie', [
     78     `${getAccessTokenCookieName()}=${accessToken}; Path=/; SameSite=Strict; Max-Age=${TOKEN_EXPIRES_IN}`,
     79     `${getRefreshTokenCookieName()}=${refreshToken}; HttpOnly; Path=/; SameSite=Strict; Max-Age=${REFRESH_TOKEN_EXPIRES_IN}`,
     80   ])
     81 
     82   res.status(200).json({
     83     expires: Date.now() + TOKEN_EXPIRES_IN * 1000,
     84   })
     85 }