deckhearth/api/auth/login.ts

150 lines
4.2 KiB
TypeScript
Raw Normal View History

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 {
const body: LoginRequest = await req.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();
}
}