Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
name: Build Docker and Upload to S3
permissions:
contents: read

on:
push:
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/note.md

This file was deleted.

51 changes: 0 additions & 51 deletions src/app/api/auth/refresh/route.js

This file was deleted.

72 changes: 72 additions & 0 deletions src/app/api/auth/refresh/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { NextRequest, NextResponse } from 'next/server';
import axios, { AxiosError, AxiosResponse } from 'axios';

const API_BASE_URL = process.env.NEXT_PUBLIC_BASE_API_URL;
const TIMEOUT = 5000;

interface RefreshResponse {
accessToken?: string;
refreshToken?: string;
message?: string;
}

interface ErrorResponse {
error: string;
}

export async function POST(req: NextRequest): Promise<NextResponse<RefreshResponse | ErrorResponse>> {
const refreshUrl = `${API_BASE_URL}/auth/refresh`;
const isProd = process.env.NODE_ENV === 'production';

try {
const cookies = req.headers.get('cookie') || '';
const body = await req.json();

const response: AxiosResponse<RefreshResponse> = await axios.post(
refreshUrl,
body,
{
headers: {
'Content-Type': 'application/json',
Cookie: cookies,
},
withCredentials: true,
timeout: TIMEOUT,
}
);

const nextResponse = NextResponse.json(response.data, {
status: response.status,
});

const setCookies = response.headers['set-cookie'];
if (setCookies) {
setCookies.forEach((cookieStr: string) => {
const [nameValue] = cookieStr.split(';');
const [name, value] = nameValue.split('=');
nextResponse.cookies.set(name, value, {
path: '/',
httpOnly: true,
secure: isProd,
sameSite: isProd ? 'none' : 'lax',
domain: isProd ? '.gdgocinha.com' : undefined,
});
});
}

return nextResponse;
} catch (error) {
const axiosError = error as AxiosError<ErrorResponse>;
const status = axiosError?.response?.status || 500;
let errorMessage = 'Authentication failed';

if (axiosError.code === 'ECONNABORTED') {
errorMessage = 'Request timeout';
} else if (axiosError.response?.data?.error) {
errorMessage = axiosError.response.data.error;
}

console.error('[AUTH PROXY ERROR] /refresh', axiosError.message);
return NextResponse.json({ error: errorMessage }, { status });
}
}
60 changes: 0 additions & 60 deletions src/app/api/auth/signin/route.js

This file was deleted.

142 changes: 142 additions & 0 deletions src/app/api/auth/signin/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { NextResponse } from 'next/server';
import axios from 'axios';

import { rateLimit } from '@/lib/rate-limit';

const ORIGINAL_AUTH_URL = process.env.NEXT_PUBLIC_BASE_API_URL;

interface LoginRequest {
email: string;
password: string;
}

interface LoginResponse {
error?: string;
[key: string]: any;
}

export async function POST(request: Request): Promise<NextResponse> {
try {
// Rate limiting 적용
const limiter = rateLimit({
interval: 60 * 1000, // 1분
uniqueTokenPerInterval: 500,
});

try {
await limiter.check(5, 'LOGIN_ATTEMPT'); // 1분당 5회 시도 제한
} catch {
return NextResponse.json(
{ error: '너무 많은 로그인 시도가 있었습니다. 잠시 후 다시 시도해주세요.' },
{ status: 429 }
);
}

// 클라이언트로부터 받은 요청 데이터 추출
const { email, password }: LoginRequest = await request.json();

// 입력값 검증
if (!email || !password) {
return NextResponse.json(
{ error: '이메일과 비밀번호를 모두 입력해주세요.' },
{ status: 400 }
);
}

if (!email.includes('@') || !email.includes('.')) {
return NextResponse.json(
{ error: '유효한 이메일 주소를 입력해주세요.' },
{ status: 400 }
);
}

if (password.length < 8) {
return NextResponse.json(
{ error: '비밀번호는 8자 이상이어야 합니다.' },
{ status: 400 }
);
}

const isProd = process.env.NODE_ENV === 'production';

// 기존 refresh_token 쿠키 삭제
const response = NextResponse.json({});
response.cookies.set('refresh_token', '', {
path: '/',
httpOnly: true,
secure: isProd,
sameSite: isProd ? 'none' : 'lax',
domain: isProd ? '.gdgocinha.com' : undefined,
expires: new Date(0),
});

const authResponse = await axios.post(
`${ORIGINAL_AUTH_URL}/auth/login`,
{ email, password },
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
}
);

const data = authResponse.data;

const nextResponse = NextResponse.json(data, {
status: authResponse.status,
statusText: authResponse.statusText,
});

// 원본 응답의 쿠키가 있으면 추출하여 현재 도메인에 설정
const cookies = authResponse.headers['set-cookie'];
if (cookies) {
cookies.forEach((cookie: string) => {
const cookieParts = cookie.split(';')[0].split('=');
const cookieName = cookieParts[0];
const cookieValue = cookieParts.slice(1).join('=');

nextResponse.cookies.set(cookieName, cookieValue, {
path: '/',
httpOnly: true,
secure: isProd,
sameSite: isProd ? 'none' : 'lax',
domain: isProd ? '.gdgocinha.com' : undefined,
});
});
}

return nextResponse;
} catch (error: any) {
console.error('로그인 프록시 오류:', error);

// 구체적인 에러 메시지 처리
if (error.response) {
switch (error.response.status) {
case 401:
return NextResponse.json(
{ error: '이메일 또는 비밀번호가 올바르지 않습니다.' },
{ status: 401 }
);
case 403:
return NextResponse.json(
{ error: '접근이 거부되었습니다.' },
{ status: 403 }
);
case 404:
return NextResponse.json(
{ error: '서비스를 찾을 수 없습니다.' },
{ status: 404 }
);
default:
return NextResponse.json(
{ error: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' },
{ status: error.response.status }
);
}
}

return NextResponse.json(
{ error: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' },
{ status: 500 }
);
}
}
55 changes: 55 additions & 0 deletions src/app/api/auth/signout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { NextResponse } from 'next/server';
import axios from 'axios';

const ORIGINAL_AUTH_URL = process.env.NEXT_PUBLIC_BASE_API_URL;

export async function POST(request: Request): Promise<NextResponse> {
try {
const response = await axios.post(
`${ORIGINAL_AUTH_URL}/auth/logout`,
{},
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true,
}
);

// 응답 생성
const nextResponse = NextResponse.json(
{ message: '로그아웃이 완료되었습니다.' },
{
status: response.status,
statusText: response.statusText,
}
);

// 쿠키 삭제
const cookies = response.headers['set-cookie'];
if (cookies) {
cookies.forEach((cookie: string) => {
const cookieParts = cookie.split(';')[0].split('=');
const cookieName = cookieParts[0];

// 쿠키 삭제
nextResponse.cookies.delete(cookieName);
});
}

nextResponse.cookies.delete('refresh_token');

return nextResponse;
} catch (error: any) {
console.error('로그아웃 프록시 오류:', error);

// 에러 응답 생성
const errorResponse = NextResponse.json(
{ error: '로그아웃 처리 중 오류가 발생했습니다.' },
{ status: error.response?.status || 500 }
);

// 에러가 발생하더라도 클라이언트 측 쿠키는 삭제
errorResponse.cookies.delete('refresh_token');

return errorResponse;
}
}
Loading