Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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