Skip to content

Commit c9529cf

Browse files
authored
Merge pull request #138 from GDGoCINHA/develop
Feat/ API 라우터 수정 및 미들웨어 일시적 추가
2 parents 90eb395 + f4117ce commit c9529cf

File tree

12 files changed

+385
-113
lines changed

12 files changed

+385
-113
lines changed

.github/workflows/deploy.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
name: Build Docker and Upload to S3
2+
permissions:
3+
contents: read
24

35
on:
46
push:

.github/workflows/note.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/app/api/auth/refresh/route.js

Lines changed: 0 additions & 51 deletions
This file was deleted.

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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import axios, { AxiosError, AxiosResponse } from 'axios';
3+
4+
const API_BASE_URL = process.env.NEXT_PUBLIC_BASE_API_URL;
5+
const TIMEOUT = 5000;
6+
7+
interface RefreshResponse {
8+
accessToken?: string;
9+
refreshToken?: string;
10+
message?: string;
11+
}
12+
13+
interface ErrorResponse {
14+
error: string;
15+
}
16+
17+
export async function POST(req: NextRequest): Promise<NextResponse<RefreshResponse | ErrorResponse>> {
18+
const refreshUrl = `${API_BASE_URL}/auth/refresh`;
19+
const isProd = process.env.NODE_ENV === 'production';
20+
21+
try {
22+
const cookies = req.headers.get('cookie') || '';
23+
const body = await req.json();
24+
25+
const response: AxiosResponse<RefreshResponse> = await axios.post(
26+
refreshUrl,
27+
body,
28+
{
29+
headers: {
30+
'Content-Type': 'application/json',
31+
Cookie: cookies,
32+
},
33+
withCredentials: true,
34+
timeout: TIMEOUT,
35+
}
36+
);
37+
38+
const nextResponse = NextResponse.json(response.data, {
39+
status: response.status,
40+
});
41+
42+
const setCookies = response.headers['set-cookie'];
43+
if (setCookies) {
44+
setCookies.forEach((cookieStr: string) => {
45+
const [nameValue] = cookieStr.split(';');
46+
const [name, value] = nameValue.split('=');
47+
nextResponse.cookies.set(name, value, {
48+
path: '/',
49+
httpOnly: true,
50+
secure: isProd,
51+
sameSite: isProd ? 'none' : 'lax',
52+
domain: isProd ? '.gdgocinha.com' : undefined,
53+
});
54+
});
55+
}
56+
57+
return nextResponse;
58+
} catch (error) {
59+
const axiosError = error as AxiosError<ErrorResponse>;
60+
const status = axiosError?.response?.status || 500;
61+
let errorMessage = 'Authentication failed';
62+
63+
if (axiosError.code === 'ECONNABORTED') {
64+
errorMessage = 'Request timeout';
65+
} else if (axiosError.response?.data?.error) {
66+
errorMessage = axiosError.response.data.error;
67+
}
68+
69+
console.error('[AUTH PROXY ERROR] /refresh', axiosError.message);
70+
return NextResponse.json({ error: errorMessage }, { status });
71+
}
72+
}

src/app/api/auth/signin/route.js

Lines changed: 0 additions & 60 deletions
This file was deleted.

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

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { NextResponse } from 'next/server';
2+
import axios from 'axios';
3+
4+
import { rateLimit } from '@/lib/rate-limit';
5+
6+
const ORIGINAL_AUTH_URL = process.env.NEXT_PUBLIC_BASE_API_URL;
7+
8+
interface LoginRequest {
9+
email: string;
10+
password: string;
11+
}
12+
13+
interface LoginResponse {
14+
error?: string;
15+
[key: string]: any;
16+
}
17+
18+
export async function POST(request: Request): Promise<NextResponse> {
19+
try {
20+
// Rate limiting 적용
21+
const limiter = rateLimit({
22+
interval: 60 * 1000, // 1분
23+
uniqueTokenPerInterval: 500,
24+
});
25+
26+
try {
27+
await limiter.check(5, 'LOGIN_ATTEMPT'); // 1분당 5회 시도 제한
28+
} catch {
29+
return NextResponse.json(
30+
{ error: '너무 많은 로그인 시도가 있었습니다. 잠시 후 다시 시도해주세요.' },
31+
{ status: 429 }
32+
);
33+
}
34+
35+
// 클라이언트로부터 받은 요청 데이터 추출
36+
const { email, password }: LoginRequest = await request.json();
37+
38+
// 입력값 검증
39+
if (!email || !password) {
40+
return NextResponse.json(
41+
{ error: '이메일과 비밀번호를 모두 입력해주세요.' },
42+
{ status: 400 }
43+
);
44+
}
45+
46+
if (!email.includes('@') || !email.includes('.')) {
47+
return NextResponse.json(
48+
{ error: '유효한 이메일 주소를 입력해주세요.' },
49+
{ status: 400 }
50+
);
51+
}
52+
53+
if (password.length < 8) {
54+
return NextResponse.json(
55+
{ error: '비밀번호는 8자 이상이어야 합니다.' },
56+
{ status: 400 }
57+
);
58+
}
59+
60+
const isProd = process.env.NODE_ENV === 'production';
61+
62+
// 기존 refresh_token 쿠키 삭제
63+
const response = NextResponse.json({});
64+
response.cookies.set('refresh_token', '', {
65+
path: '/',
66+
httpOnly: true,
67+
secure: isProd,
68+
sameSite: isProd ? 'none' : 'lax',
69+
domain: isProd ? '.gdgocinha.com' : undefined,
70+
expires: new Date(0),
71+
});
72+
73+
const authResponse = await axios.post(
74+
`${ORIGINAL_AUTH_URL}/auth/login`,
75+
{ email, password },
76+
{
77+
headers: { 'Content-Type': 'application/json' },
78+
withCredentials: true,
79+
}
80+
);
81+
82+
const data = authResponse.data;
83+
84+
const nextResponse = NextResponse.json(data, {
85+
status: authResponse.status,
86+
statusText: authResponse.statusText,
87+
});
88+
89+
// 원본 응답의 쿠키가 있으면 추출하여 현재 도메인에 설정
90+
const cookies = authResponse.headers['set-cookie'];
91+
if (cookies) {
92+
cookies.forEach((cookie: string) => {
93+
const cookieParts = cookie.split(';')[0].split('=');
94+
const cookieName = cookieParts[0];
95+
const cookieValue = cookieParts.slice(1).join('=');
96+
97+
nextResponse.cookies.set(cookieName, cookieValue, {
98+
path: '/',
99+
httpOnly: true,
100+
secure: isProd,
101+
sameSite: isProd ? 'none' : 'lax',
102+
domain: isProd ? '.gdgocinha.com' : undefined,
103+
});
104+
});
105+
}
106+
107+
return nextResponse;
108+
} catch (error: any) {
109+
console.error('로그인 프록시 오류:', error);
110+
111+
// 구체적인 에러 메시지 처리
112+
if (error.response) {
113+
switch (error.response.status) {
114+
case 401:
115+
return NextResponse.json(
116+
{ error: '이메일 또는 비밀번호가 올바르지 않습니다.' },
117+
{ status: 401 }
118+
);
119+
case 403:
120+
return NextResponse.json(
121+
{ error: '접근이 거부되었습니다.' },
122+
{ status: 403 }
123+
);
124+
case 404:
125+
return NextResponse.json(
126+
{ error: '서비스를 찾을 수 없습니다.' },
127+
{ status: 404 }
128+
);
129+
default:
130+
return NextResponse.json(
131+
{ error: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' },
132+
{ status: error.response.status }
133+
);
134+
}
135+
}
136+
137+
return NextResponse.json(
138+
{ error: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' },
139+
{ status: 500 }
140+
);
141+
}
142+
}

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { NextResponse } from 'next/server';
2+
import axios from 'axios';
3+
4+
const ORIGINAL_AUTH_URL = process.env.NEXT_PUBLIC_BASE_API_URL;
5+
6+
export async function POST(request: Request): Promise<NextResponse> {
7+
try {
8+
const response = await axios.post(
9+
`${ORIGINAL_AUTH_URL}/auth/logout`,
10+
{},
11+
{
12+
headers: { 'Content-Type': 'application/json' },
13+
withCredentials: true,
14+
}
15+
);
16+
17+
// 응답 생성
18+
const nextResponse = NextResponse.json(
19+
{ message: '로그아웃이 완료되었습니다.' },
20+
{
21+
status: response.status,
22+
statusText: response.statusText,
23+
}
24+
);
25+
26+
// 쿠키 삭제
27+
const cookies = response.headers['set-cookie'];
28+
if (cookies) {
29+
cookies.forEach((cookie: string) => {
30+
const cookieParts = cookie.split(';')[0].split('=');
31+
const cookieName = cookieParts[0];
32+
33+
// 쿠키 삭제
34+
nextResponse.cookies.delete(cookieName);
35+
});
36+
}
37+
38+
nextResponse.cookies.delete('refresh_token');
39+
40+
return nextResponse;
41+
} catch (error: any) {
42+
console.error('로그아웃 프록시 오류:', error);
43+
44+
// 에러 응답 생성
45+
const errorResponse = NextResponse.json(
46+
{ error: '로그아웃 처리 중 오류가 발생했습니다.' },
47+
{ status: error.response?.status || 500 }
48+
);
49+
50+
// 에러가 발생하더라도 클라이언트 측 쿠키는 삭제
51+
errorResponse.cookies.delete('refresh_token');
52+
53+
return errorResponse;
54+
}
55+
}

0 commit comments

Comments
 (0)