diff --git a/src/app/api/auth/_lib/auth-route.ts b/src/app/api/auth/_lib/auth-route.ts new file mode 100644 index 0000000..180c4f8 --- /dev/null +++ b/src/app/api/auth/_lib/auth-route.ts @@ -0,0 +1,81 @@ +import {cookies} from 'next/headers'; +import {NextResponse} from 'next/server'; + +import {ApiError, normalizeApiError} from '@/src/shared/api/api-error'; +import {AUTH_COOKIE_NAMES, AUTH_COOKIE_PATHS} from '@/src/shared/auth'; + +interface AuthTokenPair { + accessToken?: string; + refreshToken?: string; +} + +const AUTH_COOKIE_OPTIONS = { + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', +} as const; + +const getCookieStore = () => { + return cookies(); +}; + +const createMissingTokenError = () => { + return new ApiError({ + message: '인증 토큰 응답이 올바르지 않습니다.', + status: 502, + type: 'server', + }); +}; + +export const setAuthCookies = async ({ + accessToken, + refreshToken, +}: AuthTokenPair) => { + if (!accessToken || !refreshToken) { + throw createMissingTokenError(); + } + + const cookieStore = await getCookieStore(); + + cookieStore.set(AUTH_COOKIE_NAMES.accessToken, accessToken, { + ...AUTH_COOKIE_OPTIONS, + path: AUTH_COOKIE_PATHS.accessToken, + }); + cookieStore.set(AUTH_COOKIE_NAMES.refreshToken, refreshToken, { + ...AUTH_COOKIE_OPTIONS, + path: AUTH_COOKIE_PATHS.refreshToken, + }); +}; + +export const clearAuthCookies = async () => { + const cookieStore = await getCookieStore(); + + cookieStore.set(AUTH_COOKIE_NAMES.accessToken, '', { + ...AUTH_COOKIE_OPTIONS, + maxAge: 0, + path: AUTH_COOKIE_PATHS.accessToken, + }); + cookieStore.set(AUTH_COOKIE_NAMES.refreshToken, '', { + ...AUTH_COOKIE_OPTIONS, + maxAge: 0, + path: AUTH_COOKIE_PATHS.refreshToken, + }); +}; + +export const getRefreshToken = async () => { + return (await getCookieStore()).get(AUTH_COOKIE_NAMES.refreshToken)?.value; +}; + +export const createApiErrorResponse = (error: unknown) => { + const apiError = normalizeApiError(error); + + return NextResponse.json( + { + code: apiError.code, + message: apiError.message, + }, + { + status: apiError.status ?? 500, + } + ); +}; diff --git a/src/app/api/auth/access-token/route.ts b/src/app/api/auth/access-token/route.ts new file mode 100644 index 0000000..0809f02 --- /dev/null +++ b/src/app/api/auth/access-token/route.ts @@ -0,0 +1,40 @@ +import {NextResponse} from 'next/server'; + +import {API_ENDPOINTS, type AdminApiTypes} from '@/src/shared/api'; +import {ApiError} from '@/src/shared/api/api-error'; +import {createServerApi} from '@/src/shared/api/server-client'; + +import { + createApiErrorResponse, + getRefreshToken, + setAuthCookies, +} from '../_lib/auth-route'; + +const createMissingRefreshTokenError = () => { + return new ApiError({ + message: '리프레시 토큰이 없습니다.', + status: 401, + type: 'auth', + }); +}; + +export async function POST() { + try { + const refreshToken = await getRefreshToken(); + + if (!refreshToken) { + throw createMissingRefreshTokenError(); + } + + const serverApi = await createServerApi({accessToken: refreshToken}); + const {data} = await serverApi.get( + API_ENDPOINTS.auth.accessToken + ); + + await setAuthCookies(data); + + return new NextResponse(null, {status: 204}); + } catch (error) { + return createApiErrorResponse(error); + } +} diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..af24c88 --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,25 @@ +import {NextResponse} from 'next/server'; + +import {API_ENDPOINTS, type AdminApiTypes} from '@/src/shared/api'; +import {createServerApi} from '@/src/shared/api/server-client'; + +import {createApiErrorResponse, setAuthCookies} from '../_lib/auth-route'; + +export async function POST(request: Request) { + try { + const body = (await request.json()) as AdminApiTypes.PostLoginRequest; + const serverApi = await createServerApi({includeAccessToken: false}); + const {data} = await serverApi.post( + API_ENDPOINTS.auth.login, + body + ); + + await setAuthCookies(data); + + return NextResponse.json({ + adminId: data.adminId, + }); + } catch (error) { + return createApiErrorResponse(error); + } +} diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..ab6d218 --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,9 @@ +import {NextResponse} from 'next/server'; + +import {clearAuthCookies} from '../_lib/auth-route'; + +export async function POST() { + await clearAuthCookies(); + + return new NextResponse(null, {status: 204}); +}