🐛 Bug Fix: - Replace req.json() with req.text() + JSON.parse() for Vercel compatibility - Add proper error handling for malformed JSON requests - Fix authentication endpoints (login/register) body parsing - Fix admin users endpoint body parsing 🔧 Technical Details: - Vercel functions don't support req.json() method directly - Use req.text() to get raw body content then parse manually - Add try/catch blocks for JSON parsing errors - Maintain same API interface and error responses ✅ Endpoints Fixed: - /api/auth/register - User registration - /api/auth/login - User authentication - /api/admin/users - Admin user management This resolves the 'req.json is not a function' error in production.
163 lines
No EOL
4.5 KiB
TypeScript
163 lines
No EOL
4.5 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { Pool } from 'pg';
|
|
import bcrypt from 'bcryptjs';
|
|
import jwt from 'jsonwebtoken';
|
|
|
|
const pool = new Pool({
|
|
connectionString: process.env.DATABASE_URL,
|
|
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
|
|
});
|
|
|
|
interface LoginRequest {
|
|
username: string;
|
|
password: string;
|
|
}
|
|
|
|
export default async function handler(req: NextRequest) {
|
|
if (req.method !== 'POST') {
|
|
return new NextResponse(JSON.stringify({ error: 'Method not allowed' }), {
|
|
status: 405,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
const client = await pool.connect();
|
|
|
|
try {
|
|
// Parse request body
|
|
let body: LoginRequest;
|
|
try {
|
|
const bodyText = await req.text();
|
|
body = JSON.parse(bodyText);
|
|
} catch (parseError) {
|
|
return new NextResponse(JSON.stringify({
|
|
error: 'Invalid JSON in request body'
|
|
}), {
|
|
status: 400,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
const { username, password } = body;
|
|
|
|
// Validate input
|
|
if (!username || !password) {
|
|
return new NextResponse(JSON.stringify({
|
|
error: 'Username and password are required'
|
|
}), {
|
|
status: 400,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
// Get user by username or email
|
|
const userResult = await client.query(`
|
|
SELECT id, username, email, password_hash, first_name, last_name, is_active, last_login
|
|
FROM users
|
|
WHERE (username = $1 OR email = $1) AND is_active = true
|
|
`, [username]);
|
|
|
|
if (userResult.rows.length === 0) {
|
|
return new NextResponse(JSON.stringify({
|
|
error: 'Invalid credentials'
|
|
}), {
|
|
status: 401,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
const user = userResult.rows[0];
|
|
|
|
// Verify password
|
|
const isValidPassword = await bcrypt.compare(password, user.password_hash);
|
|
if (!isValidPassword) {
|
|
return new NextResponse(JSON.stringify({
|
|
error: 'Invalid credentials'
|
|
}), {
|
|
status: 401,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
// Get user roles and permissions
|
|
const userRoles = await client.query(`
|
|
SELECT r.name, r.description,
|
|
array_agg(p.name) as permissions
|
|
FROM roles r
|
|
JOIN user_roles ur ON r.id = ur.role_id
|
|
LEFT JOIN role_permissions rp ON r.id = rp.role_id
|
|
LEFT JOIN permissions p ON rp.permission_id = p.id
|
|
WHERE ur.user_id = $1
|
|
GROUP BY r.id, r.name, r.description
|
|
`, [user.id]);
|
|
|
|
const roles = userRoles.rows.map(r => r.name);
|
|
const permissions = [...new Set(userRoles.rows.flatMap(r => r.permissions || []))];
|
|
|
|
// Generate JWT token
|
|
const jwtSecret = process.env.JWT_SECRET || 'fallback-secret-change-in-production';
|
|
const token = jwt.sign(
|
|
{
|
|
userId: user.id,
|
|
username: user.username,
|
|
email: user.email,
|
|
roles,
|
|
permissions
|
|
},
|
|
jwtSecret,
|
|
{ expiresIn: '7d' }
|
|
);
|
|
|
|
// Store session
|
|
const tokenHash = await bcrypt.hash(token, 10);
|
|
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
|
|
|
|
await client.query(`
|
|
INSERT INTO user_sessions (user_id, token_hash, expires_at, user_agent, ip_address)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
`, [
|
|
user.id,
|
|
tokenHash,
|
|
expiresAt,
|
|
req.headers.get('user-agent') || null,
|
|
req.headers.get('x-forwarded-for') || req.headers.get('x-real-ip') || null
|
|
]);
|
|
|
|
// Update last login
|
|
await client.query(
|
|
'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = $1',
|
|
[user.id]
|
|
);
|
|
|
|
return new NextResponse(JSON.stringify({
|
|
success: true,
|
|
message: 'Login successful',
|
|
user: {
|
|
id: user.id,
|
|
username: user.username,
|
|
email: user.email,
|
|
firstName: user.first_name,
|
|
lastName: user.last_name,
|
|
roles,
|
|
permissions,
|
|
lastLogin: user.last_login
|
|
},
|
|
token
|
|
}), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Login error:', error);
|
|
return new NextResponse(JSON.stringify({
|
|
error: 'Login failed',
|
|
details: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined
|
|
}), {
|
|
status: 500,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|