mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 06:55:45 +01:00
added admin check for deletion and updates
This commit is contained in:
@@ -314,6 +314,56 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if user is trying to remove their own admin role
|
||||||
|
const currentUser = req.user;
|
||||||
|
if (currentUser?.userId === id && roles) {
|
||||||
|
const currentUserRoles = await db.all<{ role: string }>(
|
||||||
|
'SELECT role FROM employee_roles WHERE employee_id = ?',
|
||||||
|
[currentUser.userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isCurrentlyAdmin = currentUserRoles.some(role => role.role === 'admin');
|
||||||
|
const willBeAdmin = roles.includes('admin');
|
||||||
|
|
||||||
|
if (isCurrentlyAdmin && !willBeAdmin) {
|
||||||
|
res.status(400).json({ error: 'You cannot remove your own admin role' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check admin count if roles are being updated
|
||||||
|
if (roles) {
|
||||||
|
try {
|
||||||
|
await checkAdminCount(id, roles);
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(400).json({ error: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if trying to deactivate the last admin
|
||||||
|
if (isActive === false) {
|
||||||
|
const isEmployeeAdmin = await db.get<{ count: number }>(
|
||||||
|
`SELECT COUNT(*) as count FROM employee_roles WHERE employee_id = ? AND role = 'admin'`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isEmployeeAdmin && isEmployeeAdmin.count > 0) {
|
||||||
|
const otherAdminCount = await db.get<{ count: number }>(
|
||||||
|
`SELECT COUNT(*) as count
|
||||||
|
FROM employee_roles er
|
||||||
|
JOIN employees e ON er.employee_id = e.id
|
||||||
|
WHERE er.role = 'admin' AND e.is_active = 1 AND er.employee_id != ?`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!otherAdminCount || otherAdminCount.count === 0) {
|
||||||
|
res.status(400).json({ error: 'Cannot deactivate the last admin user' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate employee type if provided
|
// Validate employee type if provided
|
||||||
if (employeeType) {
|
if (employeeType) {
|
||||||
const validEmployeeType = await db.get(
|
const validEmployeeType = await db.get(
|
||||||
@@ -438,7 +488,15 @@ export const deleteEmployee = async (req: AuthRequest, res: Response): Promise<v
|
|||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
console.log('🗑️ Starting deletion process for employee ID:', id);
|
console.log('🗑️ Starting deletion process for employee ID:', id);
|
||||||
|
|
||||||
// UPDATED: Check if employee exists with role from employee_roles
|
const currentUser = req.user;
|
||||||
|
|
||||||
|
// Prevent self-deletion
|
||||||
|
if (currentUser?.userId === id) {
|
||||||
|
res.status(400).json({ error: 'You cannot delete yourself' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if employee exists with role from employee_roles
|
||||||
const existingEmployee = await db.get<any>(`
|
const existingEmployee = await db.get<any>(`
|
||||||
SELECT
|
SELECT
|
||||||
e.id, e.email, e.firstname, e.lastname, e.is_active,
|
e.id, e.email, e.firstname, e.lastname, e.is_active,
|
||||||
@@ -455,6 +513,26 @@ export const deleteEmployee = async (req: AuthRequest, res: Response): Promise<v
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const employeeRoles = await db.all<{ role: string }>(`
|
||||||
|
SELECT role FROM employee_roles WHERE employee_id = ?
|
||||||
|
`, [id]);
|
||||||
|
|
||||||
|
const isEmployeeAdmin = employeeRoles.some(role => role.role === 'admin');
|
||||||
|
|
||||||
|
// Check if this is the last admin
|
||||||
|
if (isEmployeeAdmin) {
|
||||||
|
const adminCount = await db.get<{ count: number }>(
|
||||||
|
`SELECT COUNT(*) as count
|
||||||
|
FROM employee_roles
|
||||||
|
WHERE role = 'admin'`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (adminCount && adminCount.count <= 1) {
|
||||||
|
res.status(400).json({ error: 'Cannot delete the last admin user' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('📝 Found employee to delete:', existingEmployee);
|
console.log('📝 Found employee to delete:', existingEmployee);
|
||||||
|
|
||||||
// Start transaction
|
// Start transaction
|
||||||
@@ -511,7 +589,6 @@ export const deleteEmployee = async (req: AuthRequest, res: Response): Promise<v
|
|||||||
console.error('Error during deletion transaction:', error);
|
console.error('Error during deletion transaction:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting employee:', error);
|
console.error('Error deleting employee:', error);
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
@@ -558,13 +635,49 @@ export const updateAvailabilities = async (req: AuthRequest, res: Response): Pro
|
|||||||
const { employeeId } = req.params;
|
const { employeeId } = req.params;
|
||||||
const { planId, availabilities } = req.body;
|
const { planId, availabilities } = req.body;
|
||||||
|
|
||||||
// Check if employee exists
|
// Check if employee exists and get contract type
|
||||||
const existingEmployee = await db.get('SELECT id FROM employees WHERE id = ?', [employeeId]);
|
const existingEmployee = await db.get<any>(`
|
||||||
|
SELECT e.*, er.role
|
||||||
|
FROM employees e
|
||||||
|
LEFT JOIN employee_roles er ON e.id = er.employee_id
|
||||||
|
WHERE e.id = ?
|
||||||
|
`, [employeeId]);
|
||||||
|
|
||||||
if (!existingEmployee) {
|
if (!existingEmployee) {
|
||||||
res.status(404).json({ error: 'Employee not found' });
|
res.status(404).json({ error: 'Employee not found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if employee is active
|
||||||
|
if (!existingEmployee.is_active) {
|
||||||
|
res.status(400).json({ error: 'Cannot set availability for inactive employee' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate contract type requirements
|
||||||
|
const availableCount = availabilities.filter((avail: any) =>
|
||||||
|
avail.preferenceLevel === 1 || avail.preferenceLevel === 2
|
||||||
|
).length;
|
||||||
|
|
||||||
|
const contractType = existingEmployee.contract_type;
|
||||||
|
|
||||||
|
// Apply contract type minimum requirements
|
||||||
|
if (contractType === 'small' && availableCount < 2) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'Employees with small contract must have at least 2 available shifts'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contractType === 'large' && availableCount < 3) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'Employees with large contract must have at least 3 available shifts'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flexible contract has no minimum requirement
|
||||||
|
|
||||||
await db.run('BEGIN TRANSACTION');
|
await db.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -700,3 +813,35 @@ export const updateLastLogin = async (req: AuthRequest, res: Response): Promise<
|
|||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkAdminCount = async (employeeId: string, newRoles: string[]): Promise<void> => {
|
||||||
|
try {
|
||||||
|
// Count current admins excluding the employee being updated
|
||||||
|
const adminCountResult = await db.get<{ count: number }>(
|
||||||
|
`SELECT COUNT(DISTINCT employee_id) as count
|
||||||
|
FROM employee_roles
|
||||||
|
WHERE role = 'admin' AND employee_id != ?`,
|
||||||
|
[employeeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentAdminCount = adminCountResult?.count || 0;
|
||||||
|
|
||||||
|
// Check ALL current roles for the employee
|
||||||
|
const currentEmployeeRoles = await db.all<{ role: string }>(
|
||||||
|
`SELECT role FROM employee_roles WHERE employee_id = ?`,
|
||||||
|
[employeeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentRoles = currentEmployeeRoles.map(role => role.role);
|
||||||
|
const isCurrentlyAdmin = currentRoles.includes('admin');
|
||||||
|
const willBeAdmin = newRoles.includes('admin');
|
||||||
|
|
||||||
|
// If removing admin role from the last admin, throw error
|
||||||
|
if (isCurrentlyAdmin && !willBeAdmin && currentAdminCount === 0) {
|
||||||
|
throw new Error('Cannot remove admin role from the last admin user');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -18,3 +18,20 @@
|
|||||||
- timeSlotId mapping (handles both naming conventions)
|
- timeSlotId mapping (handles both naming conventions)
|
||||||
- requiredEmployees fallback to 2 if missing
|
- requiredEmployees fallback to 2 if missing
|
||||||
- assignedEmployees fallback to empty array if missing
|
- assignedEmployees fallback to empty array if missing
|
||||||
|
|
||||||
|
## Availability
|
||||||
|
|
||||||
|
### [UPDATE] availability
|
||||||
|
* planId: required valid UUID
|
||||||
|
* availabilities: required array with strict validation:
|
||||||
|
- shiftId: valid UUID
|
||||||
|
- preferenceLevel: 0 (unavailable), 1 (available), or 2 (preferred)
|
||||||
|
- notes: optional, max 500 characters
|
||||||
|
|
||||||
|
## Scheduling
|
||||||
|
|
||||||
|
### [ACTION: generate schedule]
|
||||||
|
* shiftPlan: required object with id (valid UUID)
|
||||||
|
* employees: required array with at least one employee, each with valid UUID
|
||||||
|
* availabilities: required array
|
||||||
|
* constraints: optional array
|
||||||
|
|||||||
@@ -20,12 +20,16 @@ export const validateRegister = [
|
|||||||
body('firstname')
|
body('firstname')
|
||||||
.isLength({ min: 1, max: 100 })
|
.isLength({ min: 1, max: 100 })
|
||||||
.withMessage('First name must be between 1-100 characters')
|
.withMessage('First name must be between 1-100 characters')
|
||||||
|
.notEmpty()
|
||||||
|
.withMessage('First name must not be empty')
|
||||||
.trim()
|
.trim()
|
||||||
.escape(),
|
.escape(),
|
||||||
|
|
||||||
body('lastname')
|
body('lastname')
|
||||||
.isLength({ min: 1, max: 100 })
|
.isLength({ min: 1, max: 100 })
|
||||||
.withMessage('Last name must be between 1-100 characters')
|
.withMessage('Last name must be between 1-100 characters')
|
||||||
|
.notEmpty()
|
||||||
|
.withMessage('Last name must not be empty')
|
||||||
.trim()
|
.trim()
|
||||||
.escape(),
|
.escape(),
|
||||||
|
|
||||||
@@ -42,12 +46,16 @@ export const validateEmployee = [
|
|||||||
body('firstname')
|
body('firstname')
|
||||||
.isLength({ min: 1, max: 100 })
|
.isLength({ min: 1, max: 100 })
|
||||||
.withMessage('First name must be between 1-100 characters')
|
.withMessage('First name must be between 1-100 characters')
|
||||||
|
.notEmpty()
|
||||||
|
.withMessage('First name must not be empty')
|
||||||
.trim()
|
.trim()
|
||||||
.escape(),
|
.escape(),
|
||||||
|
|
||||||
body('lastname')
|
body('lastname')
|
||||||
.isLength({ min: 1, max: 100 })
|
.isLength({ min: 1, max: 100 })
|
||||||
.withMessage('Last name must be between 1-100 characters')
|
.withMessage('Last name must be between 1-100 characters')
|
||||||
|
.notEmpty()
|
||||||
|
.withMessage('Last name must not be empty')
|
||||||
.trim()
|
.trim()
|
||||||
.escape(),
|
.escape(),
|
||||||
|
|
||||||
@@ -62,6 +70,32 @@ export const validateEmployee = [
|
|||||||
.isIn(['manager', 'personell', 'apprentice', 'guest'])
|
.isIn(['manager', 'personell', 'apprentice', 'guest'])
|
||||||
.withMessage('Employee type must be manager, personell, apprentice or guest'),
|
.withMessage('Employee type must be manager, personell, apprentice or guest'),
|
||||||
|
|
||||||
|
body('contractType')
|
||||||
|
.custom((value, { req }) => {
|
||||||
|
const employeeType = req.body.employeeType;
|
||||||
|
|
||||||
|
// Manager, apprentice => contractType must be flexible
|
||||||
|
if (['manager', 'apprentice'].includes(employeeType)) {
|
||||||
|
if (value !== 'flexible') {
|
||||||
|
throw new Error(`contractType must be 'flexible' for employeeType: ${employeeType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Guest => contractType must be undefined/NONE
|
||||||
|
else if (employeeType === 'guest') {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
throw new Error(`contractType is not allowed for employeeType: ${employeeType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Personell => contractType must be small or large
|
||||||
|
else if (employeeType === 'personell') {
|
||||||
|
if (!['small', 'large'].includes(value)) {
|
||||||
|
throw new Error(`contractType must be 'small' or 'large' for employeeType: ${employeeType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
|
||||||
body('contractType')
|
body('contractType')
|
||||||
.optional()
|
.optional()
|
||||||
.isIn(['small', 'large', 'flexible'])
|
.isIn(['small', 'large', 'flexible'])
|
||||||
@@ -98,6 +132,8 @@ export const validateEmployeeUpdate = [
|
|||||||
.optional()
|
.optional()
|
||||||
.isLength({ min: 1, max: 100 })
|
.isLength({ min: 1, max: 100 })
|
||||||
.withMessage('First name must be between 1-100 characters')
|
.withMessage('First name must be between 1-100 characters')
|
||||||
|
.notEmpty()
|
||||||
|
.withMessage('First name must not be empty')
|
||||||
.trim()
|
.trim()
|
||||||
.escape(),
|
.escape(),
|
||||||
|
|
||||||
@@ -105,6 +141,8 @@ export const validateEmployeeUpdate = [
|
|||||||
.optional()
|
.optional()
|
||||||
.isLength({ min: 1, max: 100 })
|
.isLength({ min: 1, max: 100 })
|
||||||
.withMessage('Last name must be between 1-100 characters')
|
.withMessage('Last name must be between 1-100 characters')
|
||||||
|
.notEmpty()
|
||||||
|
.withMessage('Last name must not be empty')
|
||||||
.trim()
|
.trim()
|
||||||
.escape(),
|
.escape(),
|
||||||
|
|
||||||
@@ -115,8 +153,29 @@ export const validateEmployeeUpdate = [
|
|||||||
|
|
||||||
body('contractType')
|
body('contractType')
|
||||||
.optional()
|
.optional()
|
||||||
.isIn(['small', 'large', 'flexible'])
|
.custom((value, { req }) => {
|
||||||
.withMessage('Contract type must be small, large or flexible'),
|
const employeeType = req.body.employeeType;
|
||||||
|
if (!employeeType) return true; // Skip if employeeType not provided
|
||||||
|
|
||||||
|
// Same validation logic as create
|
||||||
|
if (['manager', 'apprentice'].includes(employeeType)) {
|
||||||
|
if (value !== 'flexible') {
|
||||||
|
throw new Error(`contractType must be 'flexible' for employeeType: ${employeeType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (employeeType === 'guest') {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
throw new Error(`contractType is not allowed for employeeType: ${employeeType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (employeeType === 'personell') {
|
||||||
|
if (!['small', 'large'].includes(value)) {
|
||||||
|
throw new Error(`contractType must be 'small' or 'large' for employeeType: ${employeeType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
|
||||||
body('roles')
|
body('roles')
|
||||||
.optional()
|
.optional()
|
||||||
@@ -147,15 +206,22 @@ export const validateEmployeeUpdate = [
|
|||||||
export const validateChangePassword = [
|
export const validateChangePassword = [
|
||||||
body('currentPassword')
|
body('currentPassword')
|
||||||
.optional()
|
.optional()
|
||||||
.isLength({ min: 8 })
|
.isLength({ min: 1 })
|
||||||
.withMessage('Current password must be at least 8 characters'),
|
.withMessage('Current password is required for self-password change'),
|
||||||
|
|
||||||
body('password')
|
body('password')
|
||||||
.optional()
|
|
||||||
.isLength({ min: 8 })
|
.isLength({ min: 8 })
|
||||||
.withMessage('Password must be at least 8 characters')
|
.withMessage('Password must be at least 8 characters')
|
||||||
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?])/)
|
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?])/)
|
||||||
.withMessage('Password must contain uppercase, lowercase, number and special character'),
|
.withMessage('Password must contain uppercase, lowercase, number and special character'),
|
||||||
|
|
||||||
|
body('confirmPassword')
|
||||||
|
.custom((value, { req }) => {
|
||||||
|
if (value !== req.body.password) {
|
||||||
|
throw new Error('Passwords do not match');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
// ===== SHIFT PLAN VALIDATION =====
|
// ===== SHIFT PLAN VALIDATION =====
|
||||||
@@ -297,14 +363,24 @@ export const validateCreateFromPreset = [
|
|||||||
.escape(),
|
.escape(),
|
||||||
|
|
||||||
body('startDate')
|
body('startDate')
|
||||||
.optional()
|
|
||||||
.isISO8601()
|
.isISO8601()
|
||||||
.withMessage('Must be a valid date (ISO format)'),
|
.withMessage('Must be a valid date (ISO format)')
|
||||||
|
.custom((value, { req }) => {
|
||||||
|
if (req.body.endDate && new Date(value) > new Date(req.body.endDate)) {
|
||||||
|
throw new Error('Start date must be before end date');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
|
||||||
body('endDate')
|
body('endDate')
|
||||||
.optional()
|
|
||||||
.isISO8601()
|
.isISO8601()
|
||||||
.withMessage('Must be a valid date (ISO format)'),
|
.withMessage('Must be a valid date (ISO format)')
|
||||||
|
.custom((value, { req }) => {
|
||||||
|
if (req.body.startDate && new Date(value) < new Date(req.body.startDate)) {
|
||||||
|
throw new Error('End date must be after start date');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
|
||||||
body('isTemplate')
|
body('isTemplate')
|
||||||
.optional()
|
.optional()
|
||||||
@@ -386,7 +462,20 @@ export const validateAvailabilities = [
|
|||||||
|
|
||||||
body('availabilities')
|
body('availabilities')
|
||||||
.isArray()
|
.isArray()
|
||||||
.withMessage('Availabilities must be an array'),
|
.withMessage('Availabilities must be an array')
|
||||||
|
.custom((availabilities, { req }) => {
|
||||||
|
// Count available shifts (preference level 1 or 2)
|
||||||
|
const availableCount = availabilities.filter((avail: any) =>
|
||||||
|
avail.preferenceLevel === 1 || avail.preferenceLevel === 2
|
||||||
|
).length;
|
||||||
|
|
||||||
|
// Basic validation - at least one available shift
|
||||||
|
if (availableCount === 0) {
|
||||||
|
throw new Error('At least one available shift is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
|
||||||
body('availabilities.*.shiftId')
|
body('availabilities.*.shiftId')
|
||||||
.isUUID()
|
.isUUID()
|
||||||
|
|||||||
Reference in New Issue
Block a user