Skip to content

Commit 3afbc8e

Browse files
authored
Merge pull request #504 from meowzip/reese
refactor: 인증 API 및 쿠키 처리 로직 개선
2 parents ced54d9 + c8720c9 commit 3afbc8e

File tree

9 files changed

+461
-106
lines changed

9 files changed

+461
-106
lines changed

src/app/api/auth/[...nextauth]/route.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ const handler = NextAuth({
3535
},
3636
callbacks: {
3737
async signIn({ user, account }) {
38-
console.log('🔍 Apple 로그인 응답:', { user, account });
39-
4038
try {
4139
const [signInInfo, signInResult] = await Promise.all([
4240
checkMembershipByEmail(user.email || ''),
@@ -70,12 +68,6 @@ const handler = NextAuth({
7068
const isMember = cookieList.get('Authorization');
7169
return isMember ? '/diary' : '/signin';
7270
}
73-
},
74-
events: {
75-
async signOut() {
76-
cookies().delete('Authorization');
77-
cookies().delete('Authorization-Refresh');
78-
}
7971
}
8072
});
8173

src/app/api/auth/login/route.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { fetchPublicJson } from '@/utils/fetch';
2+
import { NextRequest, NextResponse } from 'next/server';
3+
4+
const extractCookieValue = (
5+
setCookieHeader: string,
6+
cookieName: string
7+
): string | null => {
8+
const cookieMatch = setCookieHeader.match(
9+
new RegExp(`${cookieName}=([^;]+)`)
10+
);
11+
return cookieMatch ? cookieMatch[1] : null;
12+
};
13+
14+
export async function POST(request: NextRequest) {
15+
try {
16+
const body = await request.json();
17+
const { email, password, fcmToken } = body;
18+
19+
const requestOptions = {
20+
method: 'POST',
21+
body: { email, password, fcmToken },
22+
credentials: 'include' as RequestCredentials
23+
};
24+
25+
const response = await fetchPublicJson('/members/login', requestOptions);
26+
27+
if (!response.ok) {
28+
const errorData = (await response.body) as any;
29+
return NextResponse.json(
30+
{ error: errorData.message || '로그인 실패' },
31+
{ status: response.status }
32+
);
33+
}
34+
35+
const accessToken = response.headers.get('Authorization');
36+
37+
const setCookieHeader = response.headers.get('set-cookie');
38+
const refreshToken = setCookieHeader
39+
? extractCookieValue(setCookieHeader, 'Authorization-Refresh')
40+
: null;
41+
42+
if (!accessToken || !refreshToken) {
43+
return NextResponse.json(
44+
{ error: '토큰을 받을 수 없습니다' },
45+
{ status: 500 }
46+
);
47+
}
48+
49+
const successResponse = NextResponse.json(
50+
{
51+
success: true,
52+
message: '로그인 성공',
53+
redirectTo: '/diary'
54+
},
55+
{ status: 200 }
56+
);
57+
58+
const isProduction = process.env.NODE_ENV === 'production';
59+
60+
const baseCookieOptions = {
61+
secure: isProduction,
62+
path: '/',
63+
sameSite: 'lax' as const,
64+
...(isProduction ? {} : { domain: 'localhost' })
65+
};
66+
67+
successResponse.cookies.set('Authorization', accessToken, {
68+
...baseCookieOptions,
69+
maxAge: 60 * 60 * 2,
70+
httpOnly: false
71+
});
72+
73+
successResponse.cookies.set('Authorization-Refresh', refreshToken, {
74+
...baseCookieOptions,
75+
maxAge: 60 * 60 * 24 * 7,
76+
httpOnly: true
77+
});
78+
79+
return successResponse;
80+
} catch (error) {
81+
console.error('Login API Error:', error);
82+
return NextResponse.json(
83+
{ error: '로그인 처리 중 오류 발생' },
84+
{ status: 500 }
85+
);
86+
}
87+
}

src/app/api/auth/logout/route.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { NextResponse } from 'next/server';
2+
3+
export async function POST() {
4+
const response = NextResponse.json({ success: true });
5+
6+
const isProduction = process.env.NODE_ENV === 'production';
7+
const isLocalhost = !isProduction;
8+
9+
const baseConfig = {
10+
secure: isProduction,
11+
path: '/',
12+
...(isLocalhost && { domain: 'localhost' })
13+
};
14+
15+
const cookieConfigs = {
16+
Authorization: {
17+
...baseConfig,
18+
secure: true
19+
},
20+
'Authorization-Refresh': {
21+
...baseConfig,
22+
secure: true
23+
},
24+
25+
'next-auth.session-token': {
26+
...baseConfig,
27+
sameSite: 'lax' as const
28+
},
29+
'next-auth.pkce.code_verifier': {
30+
...baseConfig,
31+
sameSite: 'none' as const,
32+
secure: true // PKCE는 항상 secure=true (NextAuth 내부 설정)
33+
},
34+
'next-auth.state': {
35+
...baseConfig,
36+
sameSite: 'lax' as const
37+
}
38+
};
39+
40+
Object.entries(cookieConfigs).forEach(([name, config]) => {
41+
response.cookies.set(name, '', {
42+
...config,
43+
maxAge: 0,
44+
expires: new Date(0)
45+
});
46+
});
47+
48+
return response;
49+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { fetchPublicJson } from '@/utils/fetch';
2+
import { NextRequest, NextResponse } from 'next/server';
3+
4+
const extractCookieValue = (
5+
setCookieHeader: string,
6+
cookieName: string
7+
): string | null => {
8+
const cookieMatch = setCookieHeader.match(
9+
new RegExp(`${cookieName}=([^;]+)`)
10+
);
11+
return cookieMatch ? cookieMatch[1] : null;
12+
};
13+
14+
const clearPreviousTokens = (response: NextResponse) => {
15+
const isProduction = process.env.NODE_ENV === 'production';
16+
17+
const clearConfig = {
18+
path: '/',
19+
maxAge: 0,
20+
secure: isProduction,
21+
...(isProduction ? {} : { domain: 'localhost' })
22+
};
23+
24+
response.cookies.set('Authorization', '', clearConfig);
25+
response.cookies.set('Authorization-Refresh', '', {
26+
...clearConfig,
27+
httpOnly: true
28+
});
29+
};
30+
31+
export async function POST(request: NextRequest) {
32+
try {
33+
const refreshToken = request.cookies.get('Authorization-Refresh')?.value;
34+
35+
if (!refreshToken) {
36+
return NextResponse.json(
37+
{ error: 'Refresh token이 없습니다' },
38+
{ status: 401 }
39+
);
40+
}
41+
42+
const requestOptions = {
43+
method: 'POST',
44+
headers: {
45+
Cookie: `Authorization-Refresh=${refreshToken}`
46+
}
47+
};
48+
49+
const response = await fetchPublicJson('/tokens/refresh', requestOptions);
50+
51+
if (!response.ok) {
52+
return NextResponse.json(
53+
{ error: '토큰 새로고침 실패' },
54+
{ status: response.status }
55+
);
56+
}
57+
58+
const newAccessToken = response.headers.get('Authorization');
59+
60+
const setCookieHeader = response.headers.get('set-cookie');
61+
const newRefreshToken = setCookieHeader
62+
? extractCookieValue(setCookieHeader, 'Authorization-Refresh')
63+
: null;
64+
65+
if (!newAccessToken) {
66+
return NextResponse.json(
67+
{ error: '새로운 토큰을 받을 수 없습니다' },
68+
{ status: 500 }
69+
);
70+
}
71+
72+
const successResponse = NextResponse.json(
73+
{ success: true, accessToken: newAccessToken },
74+
{ status: 200 }
75+
);
76+
77+
clearPreviousTokens(successResponse);
78+
79+
const isProduction = process.env.NODE_ENV === 'production';
80+
81+
const baseCookieOptions = {
82+
secure: isProduction,
83+
path: '/',
84+
sameSite: 'lax' as const,
85+
...(isProduction ? {} : { domain: 'localhost' })
86+
};
87+
88+
successResponse.cookies.set('Authorization', newAccessToken, {
89+
...baseCookieOptions,
90+
maxAge: 60 * 60 * 2,
91+
httpOnly: false
92+
});
93+
94+
if (newRefreshToken) {
95+
successResponse.cookies.set('Authorization-Refresh', newRefreshToken, {
96+
...baseCookieOptions,
97+
maxAge: 60 * 60 * 24 * 7,
98+
httpOnly: true
99+
});
100+
}
101+
102+
return successResponse;
103+
} catch (error) {
104+
console.error('Refresh Token API Error:', error);
105+
return NextResponse.json(
106+
{ error: '토큰 새로고침 처리 중 오류 발생' },
107+
{ status: 500 }
108+
);
109+
}
110+
}

src/app/profile/setting/page.tsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const SettingPage = () => {
5151
}
5252
}
5353
});
54+
5455
useEffect(() => {
5556
if (
5657
isSuccess &&
@@ -86,17 +87,14 @@ const SettingPage = () => {
8687

8788
const logOut = async () => {
8889
try {
89-
document.cookie =
90-
'Authorization-Refresh=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
91-
92-
setTimeout(() => {
93-
document.cookie =
94-
'Authorization=; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
95-
signOut({ redirect: true });
96-
window.location.href = '/signin';
97-
}, 100);
90+
await fetch('/api/auth/logout', {
91+
method: 'POST',
92+
credentials: 'include'
93+
});
94+
window.location.href = '/signin';
9895
} catch (error) {
99-
console.error('로그아웃 중 오류 발생:', error);
96+
console.error('로그아웃 중 오류:', error);
97+
window.location.href = '/signin';
10098
}
10199
};
102100

src/components/signin/Password.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,11 @@ export default function Password() {
4545
return signInOnServer(reqObj);
4646
},
4747
onSuccess: (response: any) => {
48-
if (response.status === 200) {
49-
router.replace('/diary');
48+
if (response.ok) {
49+
const { body } = response;
50+
const redirectTo = body?.redirectTo || '/diary';
51+
52+
window.location.href = redirectTo;
5053
} else {
5154
router.replace('/signin');
5255
}

0 commit comments

Comments
 (0)