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

103 lines
3.2 KiB
TypeScript
Raw Normal View History

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
/**
* 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 = await backendResponse.json();
// Prepare response with user data (no token exposed)
const responseData = {
message: data.message,
user: data.user,
tenant: data.tenant,
menus: data.menus,
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 }
);
}
}