From 98f2fe1701ca9031590a1566428ee4447cfc08f3 Mon Sep 17 00:00:00 2001 From: hdg0116 Date: Thu, 27 Aug 2026 22:22:18 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Feat:=20Auth=20BFF=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=9C=A0=ED=8B=B8=20=EC=B6=94=EA=B0=80=20(#23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/auth/_lib/auth-route.ts | 81 +++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/app/api/auth/_lib/auth-route.ts 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, + } + ); +}; From 2e5f028397f31b5ea9d6f7dc364f6d6ed3350837 Mon Sep 17 00:00:00 2001 From: hdg0116 Date: Thu, 27 Aug 2026 22:22:57 +0900 Subject: [PATCH 2/2] =?UTF-8?q?Feat:=20Auth=20BFF=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=8A=B8=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/auth/access-token/route.ts | 40 ++++++++++++++++++++++++++ src/app/api/auth/login/route.ts | 25 ++++++++++++++++ src/app/api/auth/logout/route.ts | 9 ++++++ 3 files changed, 74 insertions(+) create mode 100644 src/app/api/auth/access-token/route.ts create mode 100644 src/app/api/auth/login/route.ts create mode 100644 src/app/api/auth/logout/route.ts 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}); +}