You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

232 lines
6.4 KiB
JavaScript

const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const pool = require('../config/database');
const logger = require('../config/logger');
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
const JWT_EXPIRES_IN = '24h';
const REFRESH_TOKEN_EXPIRES_DAYS = 7;
class AuthController {
// Login
async login(req, res) {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Username and password required' });
}
// Find user
const userResult = await pool.query(
'SELECT * FROM users WHERE username = $1 AND is_active = true',
[username]
);
if (userResult.rows.length === 0) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const user = userResult.rows[0];
// Verify password
const validPassword = await bcrypt.compare(password, user.password_hash);
if (!validPassword) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate tokens
const accessToken = jwt.sign(
{ id: user.id, username: user.username, role: user.role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
const refreshToken = jwt.sign(
{ id: user.id, type: 'refresh' },
JWT_SECRET,
{ expiresIn: `${REFRESH_TOKEN_EXPIRES_DAYS}d` }
);
// Save refresh token
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + REFRESH_TOKEN_EXPIRES_DAYS);
await pool.query(
`INSERT INTO user_sessions (user_id, refresh_token, expires_at, ip_address, user_agent)
VALUES ($1, $2, $3, $4, $5)`,
[user.id, refreshToken, expiresAt, req.ip, req.get('user-agent')]
);
// Update last login
await pool.query(
'UPDATE users SET last_login = NOW() WHERE id = $1',
[user.id]
);
// Remove password hash from response
delete user.password_hash;
logger.info(`User logged in: ${username}`);
res.json({
user,
accessToken,
refreshToken
});
} catch (error) {
logger.error('Login error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
// Refresh token
async refresh(req, res) {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({ error: 'Refresh token required' });
}
// Verify refresh token
let decoded;
try {
decoded = jwt.verify(refreshToken, JWT_SECRET);
} catch (error) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
// Check if session exists and not expired
const sessionResult = await pool.query(
`SELECT s.*, u.username, u.role, u.is_active
FROM user_sessions s
JOIN users u ON s.user_id = u.id
WHERE s.refresh_token = $1 AND s.expires_at > NOW()`,
[refreshToken]
);
if (sessionResult.rows.length === 0) {
return res.status(401).json({ error: 'Session expired or invalid' });
}
const session = sessionResult.rows[0];
if (!session.is_active) {
return res.status(401).json({ error: 'User is inactive' });
}
// Generate new access token
const accessToken = jwt.sign(
{ id: session.user_id, username: session.username, role: session.role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
res.json({ accessToken });
} catch (error) {
logger.error('Token refresh error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
// Logout
async logout(req, res) {
try {
const { refreshToken } = req.body;
if (refreshToken) {
await pool.query(
'DELETE FROM user_sessions WHERE refresh_token = $1',
[refreshToken]
);
}
res.json({ message: 'Logged out successfully' });
} catch (error) {
logger.error('Logout error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
// Get current user
async me(req, res) {
try {
const userResult = await pool.query(
'SELECT id, username, full_name, email, role, avatar_url, last_login, created_at FROM users WHERE id = $1',
[req.user.id]
);
if (userResult.rows.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
res.json(userResult.rows[0]);
} catch (error) {
logger.error('Get user error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
// Change password
async changePassword(req, res) {
try {
const { currentPassword, newPassword } = req.body;
if (!currentPassword || !newPassword) {
return res.status(400).json({ error: 'Current and new password required' });
}
if (newPassword.length < 6) {
return res.status(400).json({ error: 'Password must be at least 6 characters' });
}
// Get user
const userResult = await pool.query(
'SELECT password_hash FROM users WHERE id = $1',
[req.user.id]
);
if (userResult.rows.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
// Verify current password
const validPassword = await bcrypt.compare(
currentPassword,
userResult.rows[0].password_hash
);
if (!validPassword) {
return res.status(401).json({ error: 'Current password is incorrect' });
}
// Hash new password
const newPasswordHash = await bcrypt.hash(newPassword, 10);
// Update password
await pool.query(
'UPDATE users SET password_hash = $1 WHERE id = $2',
[newPasswordHash, req.user.id]
);
// Invalidate all sessions except current
const { refreshToken } = req.body;
if (refreshToken) {
await pool.query(
'DELETE FROM user_sessions WHERE user_id = $1 AND refresh_token != $2',
[req.user.id, refreshToken]
);
}
logger.info(`Password changed for user: ${req.user.username}`);
res.json({ message: 'Password changed successfully' });
} catch (error) {
logger.error('Change password error:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
}
module.exports = new AuthController();