Source: controllers/auth.controller.js

/**
 * 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;