/**
* Authentication controller
*
* Implements authentication and authorization endpoints
*/
import jwt from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid';
import { config } from '../config/index.js';
import { authService } from '../services/auth.service.js';
import { APIError } from '../middleware/errorHandler.js';
import { logger } from '../utils/logger.js';
export const authController = {
/**
* Login with username and password
*/
login: async (req, res, next) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return next(new APIError('Missing username or password', 400, 'MISSING_CREDENTIALS'));
}
const user = await authService.authenticateUser(username, password);
if (!user) {
return next(new APIError('Invalid username or password', 401, 'INVALID_CREDENTIALS'));
}
// Generate JWT token
const token = jwt.sign(
{
sub: user.id,
username: user.username,
roles: user.roles
},
config.JWT.SECRET,
{ expiresIn: config.JWT.EXPIRES_IN }
);
// Generate refresh token
const refreshToken = await authService.createRefreshToken(user.id);
res.json({
token,
refreshToken,
user: {
id: user.id,
username: user.username,
name: user.name,
email: user.email,
roles: user.roles
}
});
} catch (error) {
next(new APIError(`Authentication failed: ${error.message}`, 500));
}
},
/**
* Logout (revoke refresh token)
*/
logout: async (req, res, next) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return next(new APIError('Missing refresh token', 400, 'MISSING_REFRESH_TOKEN'));
}
await authService.revokeRefreshToken(refreshToken);
res.status(204).end();
} catch (error) {
next(new APIError(`Logout failed: ${error.message}`, 500));
}
},
/**
* Refresh access token using refresh token
*/
refresh: async (req, res, next) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return next(new APIError('Missing refresh token', 400, 'MISSING_REFRESH_TOKEN'));
}
const tokenData = await authService.validateRefreshToken(refreshToken);
if (!tokenData) {
return next(new APIError('Invalid or expired refresh token', 401, 'INVALID_REFRESH_TOKEN'));
}
const user = await authService.getUserById(tokenData.userId);
if (!user) {
return next(new APIError('User not found', 404, 'USER_NOT_FOUND'));
}
// Generate new JWT token
const token = jwt.sign(
{
sub: user.id,
username: user.username,
roles: user.roles
},
config.JWT.SECRET,
{ expiresIn: config.JWT.EXPIRES_IN }
);
// Generate new refresh token
const newRefreshToken = await authService.rotateRefreshToken(refreshToken);
res.json({
token,
refreshToken: newRefreshToken
});
} catch (error) {
next(new APIError(`Token refresh failed: ${error.message}`, 500));
}
},
/**
* Create API key
*/
createApiKey: async (req, res, next) => {
try {
const { name, expiresIn, permissions } = req.body;
const userId = req.user.id;
if (!name) {
return next(new APIError('Missing API key name', 400, 'MISSING_KEY_NAME'));
}
const apiKey = await authService.createApiKey(userId, name, expiresIn, permissions);
res.status(201).json({
id: apiKey.id,
key: apiKey.key, // Only time the full key is returned
name: apiKey.name,
createdAt: apiKey.createdAt,
expiresAt: apiKey.expiresAt,
permissions: apiKey.permissions
});
} catch (error) {
next(new APIError(`Failed to create API key: ${error.message}`, 500));
}
},
/**
* List API keys for current user
*/
listApiKeys: async (req, res, next) => {
try {
const userId = req.user.id;
const apiKeys = await authService.listApiKeys(userId);
// Never return the actual key in the list
const safeApiKeys = apiKeys.map(key => ({
id: key.id,
name: key.name,
createdAt: key.createdAt,
expiresAt: key.expiresAt,
lastUsed: key.lastUsed,
permissions: key.permissions
}));
res.json(safeApiKeys);
} catch (error) {
next(new APIError(`Failed to list API keys: ${error.message}`, 500));
}
},
/**
* Revoke API key
*/
revokeApiKey: async (req, res, next) => {
try {
const { keyId } = req.params;
const userId = req.user.id;
const success = await authService.revokeApiKey(userId, keyId);
if (!success) {
return next(new APIError(`API key not found: ${keyId}`, 404, 'API_KEY_NOT_FOUND'));
}
res.status(204).end();
} catch (error) {
next(new APIError(`Failed to revoke API key: ${error.message}`, 500));
}
},
/**
* Get current user information
*/
getCurrentUser: async (req, res, next) => {
try {
const userId = req.user.id;
const user = await authService.getUserById(userId);
if (!user) {
return next(new APIError('User not found', 404, 'USER_NOT_FOUND'));
}
// Never return sensitive information
res.json({
id: user.id,
username: user.username,
name: user.name,
email: user.email,
roles: user.roles,
createdAt: user.createdAt,
lastLogin: user.lastLogin
});
} catch (error) {
next(new APIError(`Failed to get user information: ${error.message}`, 500));
}
},
/**
* Update current user information
*/
updateCurrentUser: async (req, res, next) => {
try {
const userId = req.user.id;
const { name, email } = req.body;
const user = await authService.updateUser(userId, { name, email });
if (!user) {
return next(new APIError('User not found', 404, 'USER_NOT_FOUND'));
}
// Never return sensitive information
res.json({
id: user.id,
username: user.username,
name: user.name,
email: user.email,
roles: user.roles,
updatedAt: user.updatedAt
});
} catch (error) {
next(new APIError(`Failed to update user: ${error.message}`, 500));
}
},
/**
* Change password
*/
changePassword: async (req, res, next) => {
try {
const userId = req.user.id;
const { currentPassword, newPassword } = req.body;
if (!currentPassword || !newPassword) {
return next(new APIError('Missing current or new password', 400, 'MISSING_PASSWORD'));
}
const success = await authService.changePassword(userId, currentPassword, newPassword);
if (!success) {
return next(new APIError('Invalid current password', 401, 'INVALID_CURRENT_PASSWORD'));
}
res.status(204).end();
} catch (error) {
next(new APIError(`Failed to change password: ${error.message}`, 500));
}
}
};
export default authController;