Source: services/auth.service.js

/**
 * Authentication service
 * 
 * Handles user authentication, API keys, and authorization
 */

import bcrypt from 'bcryptjs';
import { v4 as uuidv4 } from 'uuid';
import { config } from '../config/index.js';
import { logger } from '../utils/logger.js';

// In-memory database for development; in production, use a real database
const users = [
  {
    id: '001',
    username: 'admin',
    password: '$2a$10$zGqGqP2hs5DxsJ0/vTJx2eGlmAsC1C7iqmgT5uOFgUW.y1J3S1n.e', // "admin123"
    name: 'Admin User',
    email: 'admin@example.com',
    roles: ['admin', 'user'],
    createdAt: new Date('2025-01-01').toISOString(),
    updatedAt: new Date('2025-01-01').toISOString(),
    lastLogin: null
  },
  {
    id: '002',
    username: 'user',
    password: '$2a$10$XSF1xV0g0GXK4pGvJuROEunjMU8sMBE2xrSTgPmDJU.ULmMB.7AqW', // "user123"
    name: 'Regular User',
    email: 'user@example.com',
    roles: ['user'],
    createdAt: new Date('2025-01-02').toISOString(),
    updatedAt: new Date('2025-01-02').toISOString(),
    lastLogin: null
  }
];

const refreshTokens = [];
const apiKeys = [];

export const authService = {
  /**
   * Authenticate a user with username and password
   */
  authenticateUser: async (username, password) => {
    try {
      // Find user by username
      const user = users.find(u => u.username === username);
      
      if (!user) {
        return null;
      }
      
      // Compare passwords
      const isPasswordValid = await bcrypt.compare(password, user.password);
      
      if (!isPasswordValid) {
        return null;
      }
      
      // Update last login time
      user.lastLogin = new Date().toISOString();
      
      // Return user without password
      const { password: _, ...userWithoutPassword } = user;
      return userWithoutPassword;
    } catch (error) {
      logger.error('Authentication error:', error);
      throw new Error(`Authentication error: ${error.message}`);
    }
  },
  
  /**
   * Get user by ID
   */
  getUserById: async (userId) => {
    try {
      const user = users.find(u => u.id === userId);
      
      if (!user) {
        return null;
      }
      
      // Return user without password
      const { password: _, ...userWithoutPassword } = user;
      return userWithoutPassword;
    } catch (error) {
      logger.error(`Error getting user by ID ${userId}:`, error);
      throw new Error(`Error getting user: ${error.message}`);
    }
  },
  
  /**
   * Update user information
   */
  updateUser: async (userId, updates) => {
    try {
      const userIndex = users.findIndex(u => u.id === userId);
      
      if (userIndex === -1) {
        return null;
      }
      
      // Update user
      users[userIndex] = {
        ...users[userIndex],
        ...updates,
        updatedAt: new Date().toISOString()
      };
      
      // Return user without password
      const { password: _, ...userWithoutPassword } = users[userIndex];
      return userWithoutPassword;
    } catch (error) {
      logger.error(`Error updating user ${userId}:`, error);
      throw new Error(`Error updating user: ${error.message}`);
    }
  },
  
  /**
   * Change user password
   */
  changePassword: async (userId, currentPassword, newPassword) => {
    try {
      const userIndex = users.findIndex(u => u.id === userId);
      
      if (userIndex === -1) {
        return false;
      }
      
      // Verify current password
      const isPasswordValid = await bcrypt.compare(currentPassword, users[userIndex].password);
      
      if (!isPasswordValid) {
        return false;
      }
      
      // Hash new password
      const hashedPassword = await bcrypt.hash(newPassword, 10);
      
      // Update user password
      users[userIndex].password = hashedPassword;
      users[userIndex].updatedAt = new Date().toISOString();
      
      return true;
    } catch (error) {
      logger.error(`Error changing password for user ${userId}:`, error);
      throw new Error(`Error changing password: ${error.message}`);
    }
  },
  
  /**
   * Create a refresh token for a user
   */
  createRefreshToken: async (userId) => {
    try {
      const tokenId = uuidv4();
      const expiresAt = new Date();
      expiresAt.setDate(expiresAt.getDate() + 30); // 30 days expiration
      
      const refreshToken = {
        id: tokenId,
        userId,
        token: uuidv4(),
        createdAt: new Date().toISOString(),
        expiresAt: expiresAt.toISOString(),
        revokedAt: null
      };
      
      refreshTokens.push(refreshToken);
      
      return refreshToken.token;
    } catch (error) {
      logger.error(`Error creating refresh token for user ${userId}:`, error);
      throw new Error(`Error creating refresh token: ${error.message}`);
    }
  },
  
  /**
   * Validate a refresh token
   */
  validateRefreshToken: async (token) => {
    try {
      const refreshToken = refreshTokens.find(t => t.token === token && !t.revokedAt);
      
      if (!refreshToken) {
        return null;
      }
      
      // Check if token is expired
      if (new Date(refreshToken.expiresAt) < new Date()) {
        return null;
      }
      
      return {
        id: refreshToken.id,
        userId: refreshToken.userId
      };
    } catch (error) {
      logger.error(`Error validating refresh token:`, error);
      throw new Error(`Error validating refresh token: ${error.message}`);
    }
  },
  
  /**
   * Revoke a refresh token
   */
  revokeRefreshToken: async (token) => {
    try {
      const tokenIndex = refreshTokens.findIndex(t => t.token === token);
      
      if (tokenIndex !== -1) {
        refreshTokens[tokenIndex].revokedAt = new Date().toISOString();
      }
      
      return true;
    } catch (error) {
      logger.error(`Error revoking refresh token:`, error);
      throw new Error(`Error revoking refresh token: ${error.message}`);
    }
  },
  
  /**
   * Rotate refresh token (revoke old one and create new one)
   */
  rotateRefreshToken: async (oldToken) => {
    try {
      const tokenData = await authService.validateRefreshToken(oldToken);
      
      if (!tokenData) {
        throw new Error('Invalid refresh token');
      }
      
      // Revoke old token
      await authService.revokeRefreshToken(oldToken);
      
      // Create new token
      return await authService.createRefreshToken(tokenData.userId);
    } catch (error) {
      logger.error(`Error rotating refresh token:`, error);
      throw new Error(`Error rotating refresh token: ${error.message}`);
    }
  },
  
  /**
   * Create an API key for a user
   */
  createApiKey: async (userId, name, expiresIn = '365d', permissions = []) => {
    try {
      const keyId = uuidv4();
      const key = `bf_${uuidv4().replace(/-/g, '')}`;
      
      // Calculate expiration date
      const expiresAt = new Date();
      const match = expiresIn.match(/^(\d+)([dmy])$/);
      
      if (match) {
        const [_, value, unit] = match;
        switch (unit) {
          case 'd':
            expiresAt.setDate(expiresAt.getDate() + parseInt(value));
            break;
          case 'm':
            expiresAt.setMonth(expiresAt.getMonth() + parseInt(value));
            break;
          case 'y':
            expiresAt.setFullYear(expiresAt.getFullYear() + parseInt(value));
            break;
        }
      } else {
        // Default to 1 year if format is invalid
        expiresAt.setFullYear(expiresAt.getFullYear() + 1);
      }
      
      const apiKey = {
        id: keyId,
        userId,
        key,
        name,
        createdAt: new Date().toISOString(),
        expiresAt: expiresAt.toISOString(),
        lastUsed: null,
        revokedAt: null,
        permissions: permissions.length > 0 ? permissions : ['read']
      };
      
      apiKeys.push(apiKey);
      
      return apiKey;
    } catch (error) {
      logger.error(`Error creating API key for user ${userId}:`, error);
      throw new Error(`Error creating API key: ${error.message}`);
    }
  },
  
  /**
   * List API keys for a user
   */
  listApiKeys: async (userId) => {
    try {
      return apiKeys.filter(k => k.userId === userId && !k.revokedAt);
    } catch (error) {
      logger.error(`Error listing API keys for user ${userId}:`, error);
      throw new Error(`Error listing API keys: ${error.message}`);
    }
  },
  
  /**
   * Revoke an API key
   */
  revokeApiKey: async (userId, keyId) => {
    try {
      const keyIndex = apiKeys.findIndex(k => k.id === keyId && k.userId === userId);
      
      if (keyIndex === -1) {
        return false;
      }
      
      apiKeys[keyIndex].revokedAt = new Date().toISOString();
      
      return true;
    } catch (error) {
      logger.error(`Error revoking API key ${keyId} for user ${userId}:`, error);
      throw new Error(`Error revoking API key: ${error.message}`);
    }
  },
  
  /**
   * Validate an API key
   */
  validateApiKey: async (apiKey) => {
    try {
      const key = apiKeys.find(k => k.key === apiKey && !k.revokedAt);
      
      if (!key) {
        return null;
      }
      
      // Check if key is expired
      if (new Date(key.expiresAt) < new Date()) {
        return null;
      }
      
      // Update last used time
      key.lastUsed = new Date().toISOString();
      
      return {
        userId: key.userId,
        permissions: key.permissions
      };
    } catch (error) {
      logger.error(`Error validating API key:`, error);
      throw new Error(`Error validating API key: ${error.message}`);
    }
  }
};

export default authService;