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