Re_Backend/src/controllers/admin.controller.ts

865 lines
24 KiB
TypeScript

import { Request, Response } from 'express';
import { Holiday, HolidayType } from '@models/Holiday';
import { holidayService } from '@services/holiday.service';
import { sequelize } from '@config/database';
import { QueryTypes, Op } from 'sequelize';
import logger from '@utils/logger';
import { initializeHolidaysCache, clearWorkingHoursCache } from '@utils/tatTimeUtils';
import { clearConfigCache } from '@services/configReader.service';
import { User, UserRole } from '@models/User';
/**
* Get all holidays (with optional year filter)
*/
export const getAllHolidays = async (req: Request, res: Response): Promise<void> => {
try {
const { year } = req.query;
const yearNum = year ? parseInt(year as string) : undefined;
const holidays = await holidayService.getAllActiveHolidays(yearNum);
res.json({
success: true,
data: holidays,
count: holidays.length
});
} catch (error) {
logger.error('[Admin] Error fetching holidays:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch holidays'
});
}
};
/**
* Get holiday calendar for a specific year
*/
export const getHolidayCalendar = async (req: Request, res: Response): Promise<void> => {
try {
const { year } = req.params;
const yearNum = parseInt(year);
if (isNaN(yearNum) || yearNum < 2000 || yearNum > 2100) {
res.status(400).json({
success: false,
error: 'Invalid year'
});
return;
}
const calendar = await holidayService.getHolidayCalendar(yearNum);
res.json({
success: true,
year: yearNum,
holidays: calendar,
count: calendar.length
});
} catch (error) {
logger.error('[Admin] Error fetching holiday calendar:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch holiday calendar'
});
}
};
/**
* Create a new holiday
*/
export const createHoliday = async (req: Request, res: Response): Promise<void> => {
try {
const userId = req.user?.userId;
if (!userId) {
res.status(401).json({
success: false,
error: 'User not authenticated'
});
return;
}
const {
holidayDate,
holidayName,
description,
holidayType,
isRecurring,
recurrenceRule,
appliesToDepartments,
appliesToLocations
} = req.body;
// Validate required fields
if (!holidayDate || !holidayName) {
res.status(400).json({
success: false,
error: 'Holiday date and name are required'
});
return;
}
const holiday = await holidayService.createHoliday({
holidayDate,
holidayName,
description,
holidayType: holidayType || HolidayType.ORGANIZATIONAL,
isRecurring: isRecurring || false,
recurrenceRule,
appliesToDepartments,
appliesToLocations,
createdBy: userId
});
// Reload holidays cache
await initializeHolidaysCache();
res.status(201).json({
success: true,
message: 'Holiday created successfully',
data: holiday
});
} catch (error: any) {
logger.error('[Admin] Error creating holiday:', error);
res.status(500).json({
success: false,
error: error.message || 'Failed to create holiday'
});
}
};
/**
* Update a holiday
*/
export const updateHoliday = async (req: Request, res: Response): Promise<void> => {
try {
const userId = req.user?.userId;
if (!userId) {
res.status(401).json({
success: false,
error: 'User not authenticated'
});
return;
}
const { holidayId } = req.params;
const updates = req.body;
const holiday = await holidayService.updateHoliday(holidayId, updates, userId);
if (!holiday) {
res.status(404).json({
success: false,
error: 'Holiday not found'
});
return;
}
// Reload holidays cache
await initializeHolidaysCache();
res.json({
success: true,
message: 'Holiday updated successfully',
data: holiday
});
} catch (error: any) {
logger.error('[Admin] Error updating holiday:', error);
res.status(500).json({
success: false,
error: error.message || 'Failed to update holiday'
});
}
};
/**
* Delete (deactivate) a holiday
*/
export const deleteHoliday = async (req: Request, res: Response): Promise<void> => {
try {
const { holidayId } = req.params;
await holidayService.deleteHoliday(holidayId);
// Reload holidays cache
await initializeHolidaysCache();
res.json({
success: true,
message: 'Holiday deleted successfully'
});
} catch (error: any) {
logger.error('[Admin] Error deleting holiday:', error);
res.status(500).json({
success: false,
error: error.message || 'Failed to delete holiday'
});
}
};
/**
* Bulk import holidays from CSV/JSON
*/
export const bulkImportHolidays = async (req: Request, res: Response): Promise<void> => {
try {
const userId = req.user?.userId;
if (!userId) {
res.status(401).json({
success: false,
error: 'User not authenticated'
});
return;
}
const { holidays } = req.body;
if (!Array.isArray(holidays) || holidays.length === 0) {
res.status(400).json({
success: false,
error: 'Holidays array is required'
});
return;
}
const result = await holidayService.bulkImportHolidays(holidays, userId);
// Reload holidays cache
await initializeHolidaysCache();
res.json({
success: true,
message: `Imported ${result.success} holidays, ${result.failed} failed`,
data: result
});
} catch (error: any) {
logger.error('[Admin] Error bulk importing holidays:', error);
res.status(500).json({
success: false,
error: error.message || 'Failed to import holidays'
});
}
};
/**
* Get public configurations (read-only, non-sensitive)
* Accessible to all authenticated users
*/
export const getPublicConfigurations = async (req: Request, res: Response): Promise<void> => {
try {
const { category } = req.query;
// Only allow certain categories for public access
const allowedCategories = ['DOCUMENT_POLICY', 'TAT_SETTINGS', 'WORKFLOW_SHARING'];
if (category && !allowedCategories.includes(category as string)) {
res.status(403).json({
success: false,
error: 'Access denied to this configuration category'
});
return;
}
let whereClause = '';
if (category) {
whereClause = `WHERE config_category = '${category}' AND is_sensitive = false`;
} else {
whereClause = `WHERE config_category IN ('DOCUMENT_POLICY', 'TAT_SETTINGS', 'WORKFLOW_SHARING') AND is_sensitive = false`;
}
const rawConfigurations = await sequelize.query(`
SELECT
config_key,
config_category,
config_value,
value_type,
display_name,
description
FROM admin_configurations
${whereClause}
ORDER BY config_category, sort_order
`, { type: QueryTypes.SELECT });
// Map snake_case to camelCase for frontend
const configurations = (rawConfigurations as any[]).map((config: any) => ({
configKey: config.config_key,
configCategory: config.config_category,
configValue: config.config_value,
valueType: config.value_type,
displayName: config.display_name,
description: config.description
}));
res.json({
success: true,
data: configurations,
count: configurations.length
});
} catch (error) {
logger.error('[Admin] Error fetching public configurations:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch configurations'
});
}
};
/**
* Get all admin configurations
*/
export const getAllConfigurations = async (req: Request, res: Response): Promise<void> => {
try {
const { category } = req.query;
let whereClause = '';
if (category) {
whereClause = `WHERE config_category = '${category}'`;
}
const rawConfigurations = await sequelize.query(`
SELECT
config_id,
config_key,
config_category,
config_value,
value_type,
display_name,
description,
default_value,
is_editable,
is_sensitive,
validation_rules,
ui_component,
options,
sort_order,
requires_restart,
last_modified_at,
last_modified_by
FROM admin_configurations
${whereClause}
ORDER BY config_category, sort_order
`, { type: QueryTypes.SELECT });
// Map snake_case to camelCase for frontend
const configurations = (rawConfigurations as any[]).map((config: any) => ({
configId: config.config_id,
configKey: config.config_key,
configCategory: config.config_category,
configValue: config.config_value,
valueType: config.value_type,
displayName: config.display_name,
description: config.description,
defaultValue: config.default_value,
isEditable: config.is_editable,
isSensitive: config.is_sensitive || false,
validationRules: config.validation_rules,
uiComponent: config.ui_component,
options: config.options,
sortOrder: config.sort_order,
requiresRestart: config.requires_restart || false,
lastModifiedAt: config.last_modified_at,
lastModifiedBy: config.last_modified_by
}));
res.json({
success: true,
data: configurations,
count: configurations.length
});
} catch (error) {
logger.error('[Admin] Error fetching configurations:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch configurations'
});
}
};
/**
* Update a configuration
*/
export const updateConfiguration = async (req: Request, res: Response): Promise<void> => {
try {
const userId = req.user?.userId;
if (!userId) {
res.status(401).json({
success: false,
error: 'User not authenticated'
});
return;
}
const { configKey } = req.params;
const { configValue } = req.body;
if (configValue === undefined) {
res.status(400).json({
success: false,
error: 'Config value is required'
});
return;
}
// Update configuration
const result = await sequelize.query(`
UPDATE admin_configurations
SET
config_value = :configValue,
last_modified_by = :userId,
last_modified_at = NOW(),
updated_at = NOW()
WHERE config_key = :configKey
AND is_editable = true
RETURNING *
`, {
replacements: { configValue, userId, configKey },
type: QueryTypes.UPDATE
});
if (!result || (result[1] as any) === 0) {
res.status(404).json({
success: false,
error: 'Configuration not found or not editable'
});
return;
}
// Clear config cache so new values are used immediately
clearConfigCache();
// If working hours config was updated, also clear working hours cache
const workingHoursKeys = ['WORK_START_HOUR', 'WORK_END_HOUR', 'WORK_START_DAY', 'WORK_END_DAY'];
if (workingHoursKeys.includes(configKey)) {
await clearWorkingHoursCache();
logger.info(`[Admin] Working hours configuration '${configKey}' updated - cache cleared and reloaded`);
}
// If AI config was updated, reinitialize AI service
const aiConfigKeys = ['AI_ENABLED'];
if (aiConfigKeys.includes(configKey)) {
try {
const { aiService } = require('../services/ai.service');
await aiService.reinitialize();
logger.info(`[Admin] AI configuration '${configKey}' updated - AI service reinitialized with ${aiService.getProviderName()}`);
} catch (error) {
logger.error(`[Admin] Failed to reinitialize AI service:`, error);
}
} else {
logger.info(`[Admin] Configuration '${configKey}' updated and cache cleared`);
}
res.json({
success: true,
message: 'Configuration updated successfully'
});
} catch (error: any) {
logger.error('[Admin] Error updating configuration:', error);
res.status(500).json({
success: false,
error: error.message || 'Failed to update configuration'
});
}
};
/**
* Reset configuration to default value
*/
export const resetConfiguration = async (req: Request, res: Response): Promise<void> => {
try {
const { configKey } = req.params;
await sequelize.query(`
UPDATE admin_configurations
SET config_value = default_value,
updated_at = NOW()
WHERE config_key = :configKey
`, {
replacements: { configKey },
type: QueryTypes.UPDATE
});
// Clear config cache so reset values are used immediately
clearConfigCache();
// If working hours config was reset, also clear working hours cache
const workingHoursKeys = ['WORK_START_HOUR', 'WORK_END_HOUR', 'WORK_START_DAY', 'WORK_END_DAY'];
if (workingHoursKeys.includes(configKey)) {
await clearWorkingHoursCache();
logger.info(`[Admin] Working hours configuration '${configKey}' reset to default - cache cleared and reloaded`);
} else {
logger.info(`[Admin] Configuration '${configKey}' reset to default and cache cleared`);
}
res.json({
success: true,
message: 'Configuration reset to default'
});
} catch (error: any) {
logger.error('[Admin] Error resetting configuration:', error);
res.status(500).json({
success: false,
error: error.message || 'Failed to reset configuration'
});
}
};
/**
* ============================================
* USER ROLE MANAGEMENT (RBAC)
* ============================================
*/
/**
* Update User Role
*
* Purpose: Change user's role (USER, MANAGEMENT, ADMIN)
*
* Access: ADMIN only
*
* Body: { role: 'USER' | 'MANAGEMENT' | 'ADMIN' }
*/
export const updateUserRole = async (req: Request, res: Response): Promise<void> => {
try {
const { userId } = req.params;
const { role } = req.body;
// Validate role
const validRoles: UserRole[] = ['USER', 'MANAGEMENT', 'ADMIN'];
if (!role || !validRoles.includes(role)) {
res.status(400).json({
success: false,
error: 'Invalid role. Must be USER, MANAGEMENT, or ADMIN'
});
return;
}
// Find user
const user = await User.findByPk(userId);
if (!user) {
res.status(404).json({
success: false,
error: 'User not found'
});
return;
}
// Store old role for logging
const oldRole = user.role;
// Prevent self-demotion from ADMIN (safety check)
const adminUser = req.user;
if (adminUser?.userId === userId && role !== 'ADMIN') {
res.status(400).json({
success: false,
error: 'Cannot remove your own admin privileges. Ask another admin to change your role.'
});
return;
}
// Update role
user.role = role;
await user.save();
logger.info(`✅ User role updated by ${adminUser?.email}: ${user.email} - ${oldRole}${role}`);
res.json({
success: true,
message: `User role updated from ${oldRole} to ${role}`,
data: {
userId: user.userId,
email: user.email,
displayName: user.displayName,
role: user.role,
previousRole: oldRole,
updatedAt: user.updatedAt
}
});
} catch (error) {
logger.error('[Admin] Error updating user role:', error);
res.status(500).json({
success: false,
error: 'Failed to update user role'
});
}
};
/**
* Get All Users by Role (with pagination and filtering)
*
* Purpose: List all users with optional role filtering and pagination
*
* Access: ADMIN only
*
* Query:
* - ?role=ADMIN | MANAGEMENT | USER | ALL | ELEVATED (default: ELEVATED for ADMIN+MANAGEMENT only)
* - ?page=1 (default)
* - ?limit=10 (default)
*/
export const getUsersByRole = async (req: Request, res: Response): Promise<void> => {
try {
const { role, page = '1', limit = '10' } = req.query;
const pageNum = parseInt(page as string) || 1;
const limitNum = Math.min(parseInt(limit as string) || 10, 100); // Max 100 per page
const offset = (pageNum - 1) * limitNum;
const whereClause: any = { isActive: true };
// Handle role filtering
if (role && role !== 'ALL' && role !== 'ELEVATED') {
const validRoles: UserRole[] = ['USER', 'MANAGEMENT', 'ADMIN'];
if (!validRoles.includes(role as UserRole)) {
res.status(400).json({
success: false,
error: 'Invalid role. Must be USER, MANAGEMENT, ADMIN, ALL, or ELEVATED'
});
return;
}
whereClause.role = role;
} else if (role === 'ELEVATED' || !role) {
// Default: Show only ADMIN and MANAGEMENT (elevated users)
whereClause.role = { [Op.in]: ['ADMIN', 'MANAGEMENT'] };
}
// If role === 'ALL', don't filter by role (show all users)
// Get total count for pagination
const totalUsers = await User.count({ where: whereClause });
const totalPages = Math.ceil(totalUsers / limitNum);
// Get paginated users
const users = await User.findAll({
where: whereClause,
attributes: [
'userId',
'email',
'displayName',
'firstName',
'lastName',
'department',
'designation',
'role',
'manager',
'postalAddress',
'lastLogin',
'createdAt'
],
order: [
['role', 'ASC'], // ADMIN first, then MANAGEMENT, then USER
['displayName', 'ASC']
],
limit: limitNum,
offset: offset
});
// Get role summary (across all users, not just current page)
const roleStats = await sequelize.query(`
SELECT
role,
COUNT(*) as count
FROM users
WHERE is_active = true
GROUP BY role
ORDER BY
CASE role
WHEN 'ADMIN' THEN 1
WHEN 'MANAGEMENT' THEN 2
WHEN 'USER' THEN 3
END
`, {
type: QueryTypes.SELECT
});
const summary = {
ADMIN: parseInt((roleStats.find((s: any) => s.role === 'ADMIN') as any)?.count || '0'),
MANAGEMENT: parseInt((roleStats.find((s: any) => s.role === 'MANAGEMENT') as any)?.count || '0'),
USER: parseInt((roleStats.find((s: any) => s.role === 'USER') as any)?.count || '0')
};
res.json({
success: true,
data: {
users: users,
pagination: {
currentPage: pageNum,
totalPages: totalPages,
totalUsers: totalUsers,
limit: limitNum,
hasNextPage: pageNum < totalPages,
hasPrevPage: pageNum > 1
},
summary,
filter: role || 'ELEVATED'
}
});
} catch (error) {
logger.error('[Admin] Error fetching users by role:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch users'
});
}
};
/**
* Get Role Statistics
*
* Purpose: Get count of users in each role
*
* Access: ADMIN only
*/
export const getRoleStatistics = async (req: Request, res: Response): Promise<void> => {
try {
const stats = await sequelize.query(`
SELECT
role,
COUNT(*) as count,
COUNT(CASE WHEN is_active = true THEN 1 END) as active_count,
COUNT(CASE WHEN is_active = false THEN 1 END) as inactive_count
FROM users
GROUP BY role
ORDER BY
CASE role
WHEN 'ADMIN' THEN 1
WHEN 'MANAGEMENT' THEN 2
WHEN 'USER' THEN 3
END
`, {
type: QueryTypes.SELECT
});
res.json({
success: true,
data: {
statistics: stats,
total: stats.reduce((sum: number, stat: any) => sum + parseInt(stat.count), 0)
}
});
} catch (error) {
logger.error('[Admin] Error fetching role statistics:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch role statistics'
});
}
};
/**
* Assign role to user by email
*
* Purpose: Search user in Okta, create if doesn't exist, then assign role
*
* Access: ADMIN only
*
* Body: { email: string, role: 'USER' | 'MANAGEMENT' | 'ADMIN' }
*/
export const assignRoleByEmail = async (req: Request, res: Response): Promise<void> => {
try {
const { email, role } = req.body;
const currentUserId = req.user?.userId;
// Validate inputs
if (!email || !role) {
res.status(400).json({
success: false,
error: 'Email and role are required'
});
return;
}
// Validate role
if (!['USER', 'MANAGEMENT', 'ADMIN'].includes(role)) {
res.status(400).json({
success: false,
error: 'Invalid role. Must be USER, MANAGEMENT, or ADMIN'
});
return;
}
logger.info(`[Admin] Assigning role ${role} to ${email} by user ${currentUserId}`);
// First, check if user already exists in our database
let user = await User.findOne({ where: { email } });
if (!user) {
// User doesn't exist, need to fetch from Okta and create
logger.info(`[Admin] User ${email} not found in database, fetching from Okta...`);
// Import UserService to search Okta
const { UserService } = await import('@services/user.service');
const userService = new UserService();
try {
// Search Okta for this user
const oktaUsers = await userService.searchUsers(email, 1);
if (!oktaUsers || oktaUsers.length === 0) {
res.status(404).json({
success: false,
error: 'User not found in Okta. Please ensure the email is correct.'
});
return;
}
const oktaUser = oktaUsers[0];
// Create user in our database
user = await User.create({
email: oktaUser.email,
oktaSub: (oktaUser as any).userId || (oktaUser as any).oktaSub, // Okta user ID as oktaSub
employeeId: (oktaUser as any).employeeNumber || (oktaUser as any).employeeId || null,
firstName: oktaUser.firstName || null,
lastName: oktaUser.lastName || null,
displayName: oktaUser.displayName || `${oktaUser.firstName || ''} ${oktaUser.lastName || ''}`.trim() || oktaUser.email,
department: oktaUser.department || null,
designation: (oktaUser as any).designation || (oktaUser as any).title || null,
phone: (oktaUser as any).phone || (oktaUser as any).mobilePhone || null,
isActive: true,
role: role, // Assign the requested role
lastLogin: undefined // Not logged in yet
});
logger.info(`[Admin] Created new user ${email} with role ${role}`);
} catch (oktaError: any) {
logger.error('[Admin] Error fetching from Okta:', oktaError);
res.status(500).json({
success: false,
error: 'Failed to fetch user from Okta: ' + (oktaError.message || 'Unknown error')
});
return;
}
} else {
// User exists, update their role
const previousRole = user.role;
// Prevent self-demotion
if (user.userId === currentUserId && role !== 'ADMIN') {
res.status(403).json({
success: false,
error: 'You cannot demote yourself from ADMIN role'
});
return;
}
await user.update({ role });
logger.info(`[Admin] Updated user ${email} role from ${previousRole} to ${role}`);
}
res.json({
success: true,
message: `Successfully assigned ${role} role to ${user.displayName || email}`,
data: {
userId: user.userId,
email: user.email,
displayName: user.displayName,
role: user.role
}
});
} catch (error) {
logger.error('[Admin] Error assigning role by email:', error);
res.status(500).json({
success: false,
error: 'Failed to assign role'
});
}
};