Skip to content

Commit daa134b

Browse files
committed
refactor: HttpOnly 쿠키 기반 인증으로 전환
Access Token을 쿠키로 관리하도록 변경하고 클라이언트 측 토큰 저장/헤더 설정 로직을 제거한다. jwt-decode 패키지 삭제.
1 parent acd059c commit daa134b

8 files changed

Lines changed: 91 additions & 237 deletions

File tree

package-lock.json

Lines changed: 0 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
"clsx": "^2.1.1",
2424
"cmdk": "^1.1.1",
2525
"framer-motion": "^12.26.2",
26-
"jwt-decode": "^4.0.0",
2726
"lucide-react": "^0.562.0",
2827
"next": "16.1.3",
2928
"next-themes": "^0.4.6",

src/app/auth/callback/page.tsx

Lines changed: 23 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
"use client"
22

3-
import { useEffect, useState, Suspense } from "react"
4-
import { useRouter, useSearchParams } from "next/navigation"
3+
import { useEffect, useRef, useState, Suspense } from "react"
4+
import { useRouter } from "next/navigation"
55
import { useAuthStore } from "@/features/auth/store/auth-store"
66
import { apiClient, getErrorMessage } from "@/shared/lib/api-client"
7-
import { getUser } from "@/features/user/api/user-service"
8-
import { jwtDecode } from "jwt-decode"
7+
import { RegisterUserResponse } from "@/shared/types/api"
98
import { toast } from "sonner"
109
import { Card } from "@/shared/components/card"
1110
import { Button } from "@/shared/components/button"
@@ -14,13 +13,6 @@ import { Loader2, AlertCircle, RefreshCcw } from "lucide-react"
1413
import { motion, AnimatePresence } from "framer-motion"
1514
import Link from "next/link"
1615

17-
interface JwtPayload {
18-
sub: string;
19-
role: string;
20-
iat: number;
21-
exp: number;
22-
}
23-
2416
const LOADING_STEPS = [
2517
"GitHub 계정을 확인하고 있습니다...",
2618
"공개 레포지토리 데이터를 수집 중입니다...",
@@ -31,11 +23,11 @@ const LOADING_STEPS = [
3123

3224
function RedirectHandler() {
3325
const router = useRouter()
34-
const searchParams = useSearchParams()
3526
const { login } = useAuthStore()
3627

3728
const [currentStep, setCurrentStep] = useState(0)
3829
const [error, setError] = useState<string | null>(null)
30+
const hasCalledRef = useRef(false)
3931

4032
// Fake Progress Logic
4133
useEffect(() => {
@@ -48,39 +40,26 @@ function RedirectHandler() {
4840
}, [error]);
4941

5042
useEffect(() => {
51-
const accessToken = searchParams.get("accessToken")
52-
53-
if (accessToken) {
54-
apiClient.defaults.headers.common["Authorization"] = `Bearer ${accessToken}`
55-
56-
try {
57-
const decoded = jwtDecode<JwtPayload>(accessToken)
58-
const username = decoded.sub
59-
60-
getUser(username).then((user) => {
61-
login(user, accessToken)
62-
toast.success(`환영합니다, ${user.username}님!`)
63-
router.replace(`/users/${user.username}`)
64-
}).catch((err) => {
65-
if (process.env.NODE_ENV === "development") {
66-
console.error("Failed to fetch user info", err)
67-
}
68-
const errorMessage = getErrorMessage(err, "사용자 정보를 불러오는데 실패했습니다.")
69-
setError(errorMessage)
70-
toast.error(errorMessage)
71-
})
72-
73-
} catch (e) {
43+
if (hasCalledRef.current) return
44+
hasCalledRef.current = true
45+
46+
// 쿠키는 이미 Set-Cookie로 설정되어 있으므로, /users/me로 사용자 정보 조회
47+
apiClient.get<void, RegisterUserResponse>('/users/me')
48+
.then((user) => {
49+
login(user)
50+
toast.success(`환영합니다, ${user.username}님!`)
51+
router.replace(`/users/${user.username}`)
52+
})
53+
.catch((err) => {
7454
if (process.env.NODE_ENV === "development") {
75-
console.error("Invalid Token", e)
55+
console.error("Failed to fetch user info", err)
7656
}
77-
setError("로그인 토큰이 올바르지 않습니다.")
78-
toast.error("로그인 토큰이 올바르지 않습니다.")
79-
}
80-
} else {
81-
router.replace("/login")
82-
}
83-
}, [searchParams, router, login])
57+
const errorMessage = getErrorMessage(err, "사용자 정보를 불러오는데 실패했습니다.")
58+
setError(errorMessage)
59+
toast.error(errorMessage)
60+
})
61+
// eslint-disable-next-line react-hooks/exhaustive-deps
62+
}, [])
8463

8564
// Error State UI
8665
if (error) {
@@ -194,4 +173,4 @@ export default function AuthCallbackPage() {
194173
</Suspense>
195174
</div>
196175
)
197-
}
176+
}

src/app/oauth2/redirect/page.tsx

Lines changed: 23 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
"use client"
22

3-
import { useEffect, useState, Suspense } from "react"
4-
import { useRouter, useSearchParams } from "next/navigation"
3+
import { useEffect, useRef, useState, Suspense } from "react"
4+
import { useRouter } from "next/navigation"
55
import { useAuthStore } from "@/features/auth/store/auth-store"
66
import { apiClient, getErrorMessage } from "@/shared/lib/api-client"
7-
import { getUser } from "@/features/user/api/user-service"
8-
import { jwtDecode } from "jwt-decode"
7+
import { RegisterUserResponse } from "@/shared/types/api"
98
import { toast } from "sonner"
109
import { Card } from "@/shared/components/card"
1110
import { Button } from "@/shared/components/button"
@@ -14,13 +13,6 @@ import { Loader2, AlertCircle, RefreshCcw } from "lucide-react"
1413
import { motion, AnimatePresence } from "framer-motion"
1514
import Link from "next/link"
1615

17-
interface JwtPayload {
18-
sub: string;
19-
role: string;
20-
iat: number;
21-
exp: number;
22-
}
23-
2416
const LOADING_STEPS = [
2517
"GitHub 계정을 확인하고 있습니다...",
2618
"공개 레포지토리 데이터를 수집 중입니다...",
@@ -31,11 +23,11 @@ const LOADING_STEPS = [
3123

3224
function RedirectHandler() {
3325
const router = useRouter()
34-
const searchParams = useSearchParams()
3526
const { login } = useAuthStore()
3627

3728
const [currentStep, setCurrentStep] = useState(0)
3829
const [error, setError] = useState<string | null>(null)
30+
const hasCalledRef = useRef(false)
3931

4032
// Fake Progress Logic
4133
useEffect(() => {
@@ -48,39 +40,26 @@ function RedirectHandler() {
4840
}, [error]);
4941

5042
useEffect(() => {
51-
const accessToken = searchParams.get("accessToken")
52-
53-
if (accessToken) {
54-
apiClient.defaults.headers.common["Authorization"] = `Bearer ${accessToken}`
55-
56-
try {
57-
const decoded = jwtDecode<JwtPayload>(accessToken)
58-
const username = decoded.sub
59-
60-
getUser(username).then((user) => {
61-
login(user, accessToken)
62-
toast.success(`환영합니다, ${user.username}님!`)
63-
router.replace(`/users/${user.username}`)
64-
}).catch((err) => {
65-
if (process.env.NODE_ENV === "development") {
66-
console.error("Failed to fetch user info", err)
67-
}
68-
const errorMessage = getErrorMessage(err, "사용자 정보를 불러오는데 실패했습니다.")
69-
setError(errorMessage)
70-
toast.error(errorMessage)
71-
})
72-
73-
} catch (e) {
43+
if (hasCalledRef.current) return
44+
hasCalledRef.current = true
45+
46+
// 쿠키는 이미 Set-Cookie로 설정되어 있으므로, /users/me로 사용자 정보 조회
47+
apiClient.get<void, RegisterUserResponse>('/users/me')
48+
.then((user) => {
49+
login(user)
50+
toast.success(`환영합니다, ${user.username}님!`)
51+
router.replace(`/users/${user.username}`)
52+
})
53+
.catch((err) => {
7454
if (process.env.NODE_ENV === "development") {
75-
console.error("Invalid Token", e)
55+
console.error("Failed to fetch user info", err)
7656
}
77-
setError("로그인 토큰이 올바르지 않습니다.")
78-
toast.error("로그인 토큰이 올바르지 않습니다.")
79-
}
80-
} else {
81-
router.replace("/login")
82-
}
83-
}, [searchParams, router, login])
57+
const errorMessage = getErrorMessage(err, "사용자 정보를 불러오는데 실패했습니다.")
58+
setError(errorMessage)
59+
toast.error(errorMessage)
60+
})
61+
// eslint-disable-next-line react-hooks/exhaustive-deps
62+
}, [])
8463

8564
// Error State UI
8665
if (error) {
@@ -194,4 +173,4 @@ export default function OAuth2RedirectPage() {
194173
</Suspense>
195174
</div>
196175
)
197-
}
176+
}
Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,24 @@
11
import { create } from 'zustand';
22
import { persist } from 'zustand/middleware';
33
import { User } from '@/shared/types/api';
4-
import { apiClient } from '@/shared/lib/api-client';
54

65
interface AuthState {
76
user: User | null;
87
isAuthenticated: boolean;
9-
accessToken: string | null;
10-
login: (user: User, accessToken?: string) => void;
8+
login: (user: User) => void;
119
logout: () => void;
12-
setAccessToken: (token: string) => void;
1310
}
1411

1512
export const useAuthStore = create<AuthState>()(
1613
persist(
1714
(set) => ({
1815
user: null,
1916
isAuthenticated: false,
20-
accessToken: null,
21-
login: (user, accessToken) => set((state) => ({
22-
user,
23-
isAuthenticated: true,
24-
accessToken: accessToken || state.accessToken
25-
})),
26-
logout: () => {
27-
set({ user: null, isAuthenticated: false, accessToken: null });
28-
delete apiClient.defaults.headers.common["Authorization"];
29-
},
30-
setAccessToken: (token) => {
31-
set({ accessToken: token });
32-
apiClient.defaults.headers.common["Authorization"] = `Bearer ${token}`;
33-
}
17+
login: (user) => set({ user, isAuthenticated: true }),
18+
logout: () => set({ user: null, isAuthenticated: false }),
3419
}),
3520
{
3621
name: 'auth-storage',
37-
onRehydrateStorage: () => (state) => {
38-
if (state?.accessToken) {
39-
apiClient.defaults.headers.common["Authorization"] = `Bearer ${state.accessToken}`;
40-
}
41-
}
4222
}
4323
)
44-
);
24+
);

0 commit comments

Comments
 (0)