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
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(); |