Skip to content

Commit 7f14143

Browse files
committed
feat(proxy) : 프록시 파일 구현 및 레이아웃 수정
1 parent 7bb6b47 commit 7f14143

3 files changed

Lines changed: 163 additions & 16 deletions

File tree

src/app/(auth)/layout.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
1-
import { getAuthStatus } from "@/lib/auth/auth.server";
2-
import { redirect } from "next/navigation";
1+
import React from "react";
32

43
export default async function Layout({ children }: { children: React.ReactNode }) {
5-
const isLoggedIn = await getAuthStatus();
6-
7-
if (isLoggedIn) {
8-
redirect("/home");
9-
}
10-
114
return (
125
<div className="grid min-h-screen grid-cols-2">
136
{/* TODO: 왼쪽 이미지 */}

src/app/(protect)/layout.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
1-
import { getAuthStatus } from "@/lib/auth/auth.server";
2-
import { redirect } from "next/navigation";
1+
import React from "react";
32

43
export default async function Layout({ children }: { children: React.ReactNode }) {
5-
const isLoggedIn = await getAuthStatus();
6-
7-
if (!isLoggedIn) {
8-
redirect("/sign-in");
9-
}
10-
114
return <>{children}</>;
125
}

src/proxy.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import type { NextRequest } from "next/server";
2+
import { NextResponse } from "next/server";
3+
import jwt from "jsonwebtoken";
4+
5+
const API_URL = process.env.NEXT_PUBLIC_API_BASE_URL;
6+
7+
const AUTH_REQUIRED_PATHS = ["/my-page", "/planner"];
8+
const GUEST_ONLY_PATHS = ["/sign-in", "/sign-up"];
9+
10+
export async function proxy(request: NextRequest) {
11+
const pathname = request.nextUrl.pathname;
12+
13+
const accessToken = getCookie(request, "ACCESS_TOKEN");
14+
const refreshToken = getCookie(request, "REFRESH_TOKEN");
15+
16+
// 0) 게스트 전용 경로 처리 (/sign-in, /sign-up)
17+
if (isGuestOnlyPath(pathname)) {
18+
const res = await handleGuestOnlyRoute(request, accessToken, refreshToken);
19+
if (res) return res;
20+
return NextResponse.next();
21+
}
22+
23+
// 1) 인증 필요 없는 경로
24+
if (!isAuthRequiredPath(pathname)) return NextResponse.next();
25+
26+
// 2) 인증 필요 경로: access 없으면 refresh 시도
27+
if (!accessToken) {
28+
if (!refreshToken) return redirectToLogin(request);
29+
return refreshAccessAndContinueOrLogout(request, refreshToken);
30+
}
31+
32+
// 3) access 검증
33+
const accessState = verifyAccessToken(accessToken);
34+
35+
if (accessState === "valid") return NextResponse.next();
36+
37+
if (accessState === "expired") {
38+
if (!refreshToken) return redirectToLogin(request);
39+
return refreshAccessAndContinueOrLogout(request, refreshToken);
40+
}
41+
42+
return redirectToLogin(request);
43+
}
44+
45+
/* -------------------------------------------------------------------------- */
46+
/* Guest-only */
47+
/* -------------------------------------------------------------------------- */
48+
49+
async function handleGuestOnlyRoute(
50+
request: NextRequest,
51+
accessToken: string | null,
52+
refreshToken: string | null
53+
): Promise<NextResponse | null> {
54+
// access가 유효하면 이미 로그인 상태 → 대시보드로 이동
55+
if (accessToken && verifyAccessToken(accessToken) === "valid") {
56+
return NextResponse.redirect(new URL("/home", request.url));
57+
}
58+
59+
// access가 없거나 만료/invalid인데 refresh가 있으면,
60+
// refresh가 "살아있으면" 새 access 받아서 로그인 상태로 보고 대시보드로 이동,
61+
// refresh가 "죽었으면" 쿠키 정리하고 /login 접근 허용
62+
if (refreshToken) {
63+
const newAccess = await requestNewAccessToken(refreshToken);
64+
65+
if (newAccess) {
66+
const res = NextResponse.redirect(new URL("/home", request.url));
67+
setAccessCookie(res, newAccess);
68+
return res;
69+
}
70+
71+
// refresh도 만료/invalid → 쿠키 삭제하고 /login 그대로 보여주기
72+
const res = NextResponse.next();
73+
clearAuthCookies(res);
74+
return res;
75+
}
76+
77+
// 토큰 자체가 없으면 그냥 /login 접근 허용
78+
return null;
79+
}
80+
81+
/* -------------------------------------------------------------------------- */
82+
/* Helpers */
83+
/* -------------------------------------------------------------------------- */
84+
85+
function isAuthRequiredPath(pathname: string) {
86+
return AUTH_REQUIRED_PATHS.some((p) => pathname.startsWith(p));
87+
}
88+
89+
function isGuestOnlyPath(pathname: string) {
90+
return GUEST_ONLY_PATHS.some((p) => pathname.startsWith(p));
91+
}
92+
93+
function getCookie(request: NextRequest, name: string) {
94+
return request.cookies.get(name)?.value ?? null;
95+
}
96+
97+
function redirectToLogin(request: NextRequest) {
98+
const res = NextResponse.redirect(new URL("/sign-in", request.url));
99+
clearAuthCookies(res);
100+
return res;
101+
}
102+
103+
function clearAuthCookies(res: NextResponse) {
104+
res.cookies.set("access_token", "", { maxAge: 0, path: "/" });
105+
res.cookies.set("refresh_token", "", { maxAge: 0, path: "/" });
106+
}
107+
108+
function setAccessCookie(res: NextResponse, token: string) {
109+
res.cookies.set("access_token", token, {
110+
httpOnly: true,
111+
sameSite: "lax", // 동일한 도메인에만 쿠키를 저장할 것인지를 정하는 것
112+
path: "/",
113+
secure: true, // HTTPS면 켜기
114+
});
115+
}
116+
117+
function verifyAccessToken(token: string): "valid" | "expired" | "invalid" {
118+
try {
119+
const t = token.trim().replace(/^"|"$/g, "");
120+
121+
const secret = Buffer.from(process.env.SECRET_KEY!, "base64url");
122+
123+
jwt.verify(t, secret, { algorithms: ["HS512"] });
124+
return "valid";
125+
} catch (err) {
126+
if (err instanceof Error && err?.name === "TokenExpiredError") return "expired";
127+
return "invalid";
128+
}
129+
}
130+
131+
async function refreshAccessAndContinueOrLogout(request: NextRequest, refreshToken: string) {
132+
const newAccessToken = await requestNewAccessToken(refreshToken);
133+
134+
if (!newAccessToken) return redirectToLogin(request);
135+
136+
const res = NextResponse.next();
137+
setAccessCookie(res, newAccessToken);
138+
return res;
139+
}
140+
141+
async function requestNewAccessToken(refreshToken: string): Promise<string | null> {
142+
if (!API_URL) return null;
143+
144+
try {
145+
const res = await fetch(`${API_URL}/api/v1/auth/refresh`, {
146+
method: "POST",
147+
headers: { "Content-Type": "application/json" },
148+
body: JSON.stringify({ refreshToken }),
149+
});
150+
151+
if (!res.ok) return null;
152+
153+
const data = (await res.json()) as {
154+
accessToken?: string;
155+
access_token?: string;
156+
};
157+
return data.accessToken ?? data.access_token ?? null;
158+
} catch {
159+
return null;
160+
}
161+
}

0 commit comments

Comments
 (0)