Files
sam-react-prod/src/app/api/auth/login/route.ts

185 lines
5.5 KiB
TypeScript
Raw Normal View History

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
/**
* 🔵 Next.js API - (PHP )
*
* :
* - 보안: HttpOnly (JavaScript )
* - 패턴: PHP API
* - 보호: 토큰을 JavaScript에
*
* 🔄 :
* 1. Next.js /api/auth/login (user_id, user_pwd)
* 2. Next.js PHP /api/v1/login ( )
* 3. PHP Next.js (access_token, refresh_token, )
* 4. Next.js: 토큰을 HttpOnly
* 5. Next.js ( )
*
* 🔐 :
* -
* - HttpOnly: XSS
* - Secure: HTTPS만
* - SameSite=Strict: CSRF
*
* :
* - API는 PHP /api/v1/login의
* - PHP
*/
/**
* API
*/
interface BackendLoginResponse {
message: string;
access_token: string;
refresh_token: string;
token_type: string;
expires_in: number;
expires_at: string;
user: {
id: number;
user_id: string;
name: string;
email: string;
phone: string;
};
tenant: {
id: number;
company_name: string;
business_num: string;
tenant_st_code: string;
other_tenants: unknown[];
};
menus: Array<{
id: number;
parent_id: number | null;
name: string;
url: string;
icon: string;
sort_order: number;
is_external: number;
external_url: string | null;
}>;
roles: Array<{
id: number;
name: string;
description: string;
}>;
}
/**
* ( )
*/
interface FrontendLoginResponse {
message: string;
user: BackendLoginResponse['user'];
tenant: BackendLoginResponse['tenant'];
menus: BackendLoginResponse['menus'];
roles: BackendLoginResponse['roles'];
token_type: string;
expires_in: number;
expires_at: string;
}
/**
* Login Proxy Route Handler
*
* Purpose:
* - Proxy login requests to PHP backend
* - Store token in HttpOnly cookie (XSS protection)
* - Never expose token to client JavaScript
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { user_id, user_pwd } = body;
// Validate input
if (!user_id || !user_pwd) {
return NextResponse.json(
{ error: 'User ID and password are required' },
{ status: 400 }
);
}
// Call PHP backend API
const backendResponse = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
},
body: JSON.stringify({ user_id, user_pwd }),
});
if (!backendResponse.ok) {
// Don't expose detailed backend error messages to client
// Use generic error messages based on status code
let errorMessage = 'Authentication failed';
if (backendResponse.status === 422) {
errorMessage = 'Invalid credentials provided';
} else if (backendResponse.status === 429) {
errorMessage = 'Too many login attempts. Please try again later';
} else if (backendResponse.status >= 500) {
errorMessage = 'Service temporarily unavailable';
}
return NextResponse.json(
{ error: errorMessage },
{ status: backendResponse.status === 422 ? 401 : backendResponse.status }
);
}
const data: BackendLoginResponse = await backendResponse.json();
// Prepare response with user data (no token exposed)
const responseData: FrontendLoginResponse = {
message: data.message,
user: data.user,
tenant: data.tenant,
menus: data.menus,
roles: data.roles, // ✅ roles 데이터 추가
token_type: data.token_type,
expires_in: data.expires_in,
expires_at: data.expires_at,
};
// Set HttpOnly cookies for both access_token and refresh_token
const accessTokenCookie = [
`access_token=${data.access_token}`,
'HttpOnly', // ✅ JavaScript cannot access
'Secure', // ✅ HTTPS only (production)
'SameSite=Strict', // ✅ CSRF protection
'Path=/',
`Max-Age=${data.expires_in || 7200}`, // Use backend expiry (default 2 hours)
].join('; ');
const refreshTokenCookie = [
`refresh_token=${data.refresh_token}`,
'HttpOnly', // ✅ JavaScript cannot access
'Secure', // ✅ HTTPS only (production)
'SameSite=Strict', // ✅ CSRF protection
'Path=/',
'Max-Age=604800', // 7 days (longer for refresh token)
].join('; ');
console.log('✅ Login successful - Access & Refresh tokens stored in HttpOnly cookies');
const response = NextResponse.json(responseData, { status: 200 });
response.headers.append('Set-Cookie', accessTokenCookie);
response.headers.append('Set-Cookie', refreshTokenCookie);
return response;
} catch (error) {
console.error('Login proxy error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}