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 }