From b302c447f89884783a0dc32b877b1fbb39a4f7b4 Mon Sep 17 00:00:00 2001
From: donpat1to
Date: Fri, 31 Oct 2025 12:30:54 +0100
Subject: [PATCH] admin has to confirm current password as well on self
password change
---
backend/src/controllers/employeeController.ts | 132 ++---
backend/src/middleware/validation.ts | 16 +-
backend/src/routes/employees.ts | 20 +-
frontend/src/hooks/useBackendValidation.ts | 38 +-
frontend/src/pages/Settings/Settings.tsx | 191 +++---
.../src/pages/Settings/type/SettingsType.tsx | 548 +++++++++---------
frontend/src/services/authService.ts | 35 +-
frontend/src/services/employeeService.ts | 60 +-
8 files changed, 522 insertions(+), 518 deletions(-)
diff --git a/backend/src/controllers/employeeController.ts b/backend/src/controllers/employeeController.ts
index bfb6f2d..4c88e3e 100644
--- a/backend/src/controllers/employeeController.ts
+++ b/backend/src/controllers/employeeController.ts
@@ -18,17 +18,17 @@ function generateEmail(firstname: string, lastname: string): string {
const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, '');
const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, '');
-
+
return `${cleanFirstname}.${cleanLastname}@sp.de`;
}
export const getEmployees = async (req: AuthRequest, res: Response): Promise => {
try {
console.log('🔍 Fetching employees - User:', req.user);
-
+
const { includeInactive } = req.query;
const includeInactiveFlag = includeInactive === 'true';
-
+
let query = `
SELECT
e.id, e.email, e.firstname, e.lastname,
@@ -43,13 +43,13 @@ export const getEmployees = async (req: AuthRequest, res: Response): Promise(query);
// Format employees with proper field names and roles array
@@ -132,12 +132,12 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise(
+ const employeeTypeInfo = await db.get<{ type: string, category: string, has_contract_type: number }>(
'SELECT type, category, has_contract_type FROM employee_types WHERE type = ?',
[employeeType]
);
if (!employeeTypeInfo) {
- res.status(400).json({
- error: `Ungültiger employeeType: ${employeeType}. Gültige Typen: manager, personell, apprentice, guest`
+ res.status(400).json({
+ error: `Ungültiger employeeType: ${employeeType}. Gültige Typen: manager, personell, apprentice, guest`
});
return;
}
@@ -169,22 +169,22 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise('SELECT id FROM employees WHERE email = ? AND is_active = 1', [email]);
-
+
if (existingUser) {
console.log('❌ Generated email already exists:', email);
- res.status(409).json({
- error: `Employee with email ${email} already exists. Please use different firstname/lastname.`
+ res.status(409).json({
+ error: `Employee with email ${email} already exists. Please use different firstname/lastname.`
});
return;
}
@@ -228,12 +228,12 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise role.role === 'admin');
const willBeAdmin = roles.includes('admin');
-
+
if (isCurrentlyAdmin && !willBeAdmin) {
res.status(400).json({ error: 'You cannot remove your own admin role' });
return;
@@ -372,8 +372,8 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise(
- 'SELECT id FROM employees WHERE email = ? AND id != ? AND is_active = 1',
+ 'SELECT id FROM employees WHERE email = ? AND id != ? AND is_active = 1',
[email, id]
);
-
+
if (emailExists) {
- res.status(409).json({
- error: `Cannot update name - email ${email} already exists for another employee`
+ res.status(409).json({
+ error: `Cannot update name - email ${email} already exists for another employee`
});
return;
}
@@ -423,7 +423,7 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise(
- 'SELECT id, assigned_employees FROM scheduled_shifts WHERE json_extract(assigned_employees, "$") LIKE ?',
+ 'SELECT id, assigned_employees FROM scheduled_shifts WHERE json_extract(assigned_employees, "$") LIKE ?',
[`%${id}%`]
);
-
+
for (const shift of assignedShifts) {
try {
const employeesArray: string[] = JSON.parse(shift.assigned_employees || '[]');
@@ -581,7 +581,7 @@ export const deleteEmployee = async (req: AuthRequest, res: Response): Promise
+ 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'
+ 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'
+ res.status(400).json({
+ error: 'Employees with large contract must have at least 3 available shifts'
});
return;
}
@@ -742,12 +742,12 @@ export const changePassword = async (req: AuthRequest, res: Response): Promise('SELECT password FROM employees WHERE id = ?', [id]);
@@ -756,8 +756,8 @@ export const changePassword = async (req: AuthRequest, res: Response): Promise(
`SELECT role FROM employee_roles WHERE employee_id = ?`,
diff --git a/backend/src/middleware/validation.ts b/backend/src/middleware/validation.ts
index a5482f0..c7e4722 100644
--- a/backend/src/middleware/validation.ts
+++ b/backend/src/middleware/validation.ts
@@ -73,7 +73,7 @@ export const validateEmployee = [
body('contractType')
.custom((value, { req }) => {
const employeeType = req.body.employeeType;
-
+
// Manager, apprentice => contractType must be flexible
if (['manager', 'apprentice'].includes(employeeType)) {
if (value !== 'flexible') {
@@ -92,7 +92,7 @@ export const validateEmployee = [
throw new Error(`contractType must be 'small' or 'large' for employeeType: ${employeeType}`);
}
}
-
+
return true;
}),
@@ -156,7 +156,7 @@ export const validateEmployeeUpdate = [
.custom((value, { req }) => {
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') {
@@ -173,7 +173,7 @@ export const validateEmployeeUpdate = [
throw new Error(`contractType must be 'small' or 'large' for employeeType: ${employeeType}`);
}
}
-
+
return true;
}),
@@ -209,7 +209,7 @@ export const validateChangePassword = [
.isLength({ min: 1 })
.withMessage('Current password is required for self-password change'),
- body('password')
+ body('newPassword')
.isLength({ min: 8 })
.withMessage('Password must be at least 8 characters')
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?])/)
@@ -217,7 +217,7 @@ export const validateChangePassword = [
body('confirmPassword')
.custom((value, { req }) => {
- if (value !== req.body.password) {
+ if (value !== req.body.newPassword) {
throw new Error('Passwords do not match');
}
return true;
@@ -465,7 +465,7 @@ export const validateAvailabilities = [
.withMessage('Availabilities must be an array')
.custom((availabilities, { req }) => {
// Count available shifts (preference level 1 or 2)
- const availableCount = availabilities.filter((avail: any) =>
+ const availableCount = availabilities.filter((avail: any) =>
avail.preferenceLevel === 1 || avail.preferenceLevel === 2
).length;
@@ -473,7 +473,7 @@ export const validateAvailabilities = [
if (availableCount === 0) {
throw new Error('At least one available shift is required');
}
-
+
return true;
}),
diff --git a/backend/src/routes/employees.ts b/backend/src/routes/employees.ts
index 7c65a54..58a5949 100644
--- a/backend/src/routes/employees.ts
+++ b/backend/src/routes/employees.ts
@@ -11,15 +11,15 @@ import {
changePassword,
updateLastLogin
} from '../controllers/employeeController.js';
-import {
- handleValidationErrors,
- validateEmployee,
- validateEmployeeUpdate,
+import {
+ handleValidationErrors,
+ validateEmployee,
+ validateEmployeeUpdate,
validateChangePassword,
validateId,
validateEmployeeId,
validateAvailabilities,
- validatePagination
+ validatePagination
} from '../middleware/validation.js';
const router = express.Router();
@@ -28,18 +28,18 @@ const router = express.Router();
router.use(authMiddleware);
// Employee CRUD Routes
-router.get('/', validatePagination, handleValidationErrors, getEmployees);
+router.get('/', validatePagination, handleValidationErrors, authMiddleware, getEmployees);
router.get('/:id', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), getEmployee);
router.post('/', validateEmployee, handleValidationErrors, requireRole(['admin']), createEmployee);
router.put('/:id', validateId, validateEmployeeUpdate, handleValidationErrors, requireRole(['admin', 'maintenance']), updateEmployee);
router.delete('/:id', validateId, handleValidationErrors, requireRole(['admin']), deleteEmployee);
// Password & Login Routes
-router.put('/:id/password', validateId, validateChangePassword, handleValidationErrors, changePassword);
-router.put('/:id/last-login', validateId, handleValidationErrors, updateLastLogin);
+router.put('/:id/password', validateId, validateChangePassword, handleValidationErrors, authMiddleware, changePassword);
+router.put('/:id/last-login', validateId, handleValidationErrors, authMiddleware, updateLastLogin);
// Availability Routes
-router.get('/:employeeId/availabilities', validateEmployeeId, handleValidationErrors, getAvailabilities);
-router.put('/:employeeId/availabilities', validateEmployeeId, validateAvailabilities, handleValidationErrors, updateAvailabilities);
+router.get('/:employeeId/availabilities', validateEmployeeId, handleValidationErrors, authMiddleware, getAvailabilities);
+router.put('/:employeeId/availabilities', validateEmployeeId, validateAvailabilities, handleValidationErrors, authMiddleware, updateAvailabilities);
export default router;
\ No newline at end of file
diff --git a/frontend/src/hooks/useBackendValidation.ts b/frontend/src/hooks/useBackendValidation.ts
index eaa3c83..b2e13b9 100644
--- a/frontend/src/hooks/useBackendValidation.ts
+++ b/frontend/src/hooks/useBackendValidation.ts
@@ -33,35 +33,19 @@ export const useBackendValidation = () => {
const result = await apiCall();
return result;
} catch (error: any) {
- if (error.validationErrors) {
+ if (error.validationErrors && Array.isArray(error.validationErrors)) {
setValidationErrors(error.validationErrors);
-
- // Show specific validation error messages
- if (error.validationErrors.length > 0) {
- // Show the first validation error as the main notification
- const firstError = error.validationErrors[0];
- showNotification({
- type: 'error',
- title: 'Validierungsfehler',
- message: firstError.message
- });
- // If there are multiple errors, show additional notifications for each
- if (error.validationErrors.length > 1) {
- // Wait a bit before showing additional notifications to avoid overlap
- setTimeout(() => {
- error.validationErrors.slice(1).forEach((validationError: ValidationError, index: number) => {
- setTimeout(() => {
- showNotification({
- type: 'error',
- title: 'Weiterer Fehler',
- message: validationError.message
- });
- }, index * 300); // Stagger the notifications
- });
- }, 500);
- }
- }
+ // Show specific validation error messages from backend
+ error.validationErrors.forEach((validationError: ValidationError, index: number) => {
+ setTimeout(() => {
+ showNotification({
+ type: 'error',
+ title: 'Validierungsfehler',
+ message: `${validationError.field ? `${validationError.field}: ` : ''}${validationError.message}`
+ });
+ }, index * 500); // Stagger the notifications
+ });
} else {
// Show notification for other errors
showNotification({
diff --git a/frontend/src/pages/Settings/Settings.tsx b/frontend/src/pages/Settings/Settings.tsx
index f35bf60..e85d7fd 100644
--- a/frontend/src/pages/Settings/Settings.tsx
+++ b/frontend/src/pages/Settings/Settings.tsx
@@ -1,8 +1,9 @@
-// frontend/src/pages/Settings/Settings.tsx - UPDATED WITH NEW STYLES
+// frontend/src/pages/Settings/Settings.tsx - UPDATED WITH VALIDATION STRATEGY
import React, { useState, useEffect, useRef } from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { employeeService } from '../../services/employeeService';
import { useNotification } from '../../contexts/NotificationContext';
+import { useBackendValidation } from '../../hooks/useBackendValidation';
import AvailabilityManager from '../Employees/components/AvailabilityManager';
import { Employee } from '../../models/Employee';
import { styles } from './type/SettingsType';
@@ -10,11 +11,12 @@ import { styles } from './type/SettingsType';
const Settings: React.FC = () => {
const { user: currentUser, updateUser } = useAuth();
const { showNotification } = useNotification();
+ const { executeWithValidation, clearErrors, isSubmitting } = useBackendValidation();
+
const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'availability'>('profile');
- const [loading, setLoading] = useState(false);
const [showAvailabilityManager, setShowAvailabilityManager] = useState(false);
-
- // Profile form state - updated for firstname/lastname
+
+ // Profile form state
const [profileForm, setProfileForm] = useState({
firstname: currentUser?.firstname || '',
lastname: currentUser?.lastname || ''
@@ -73,7 +75,7 @@ const Settings: React.FC = () => {
}));
};
- // Password visibility handlers for current password
+ // Password visibility handlers
const handleCurrentPasswordMouseDown = () => {
currentPasswordTimeoutRef.current = setTimeout(() => {
setShowCurrentPassword(true);
@@ -88,7 +90,6 @@ const Settings: React.FC = () => {
setShowCurrentPassword(false);
};
- // Password visibility handlers for new password
const handleNewPasswordMouseDown = () => {
newPasswordTimeoutRef.current = setTimeout(() => {
setShowNewPassword(true);
@@ -103,7 +104,6 @@ const Settings: React.FC = () => {
setShowNewPassword(false);
};
- // Password visibility handlers for confirm password
const handleConfirmPasswordMouseDown = () => {
confirmPasswordTimeoutRef.current = setTimeout(() => {
setShowConfirmPassword(true);
@@ -129,7 +129,6 @@ const Settings: React.FC = () => {
cleanup();
};
- // Prevent context menu
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault();
};
@@ -138,40 +137,46 @@ const Settings: React.FC = () => {
e.preventDefault();
if (!currentUser) return;
- // Validation
- if (!profileForm.firstname.trim() || !profileForm.lastname.trim()) {
+ // BASIC FRONTEND VALIDATION: Only check required fields
+ if (!profileForm.firstname.trim()) {
showNotification({
type: 'error',
title: 'Fehler',
- message: 'Vorname und Nachname sind erforderlich'
+ message: 'Vorname ist erforderlich'
+ });
+ return;
+ }
+
+ if (!profileForm.lastname.trim()) {
+ showNotification({
+ type: 'error',
+ title: 'Fehler',
+ message: 'Nachname ist erforderlich'
});
return;
}
try {
- setLoading(true);
- await employeeService.updateEmployee(currentUser.id, {
- firstname: profileForm.firstname.trim(),
- lastname: profileForm.lastname.trim()
- });
+ // Use executeWithValidation to handle backend validation
+ await executeWithValidation(async () => {
+ const updatedEmployee = await employeeService.updateEmployee(currentUser.id, {
+ firstname: profileForm.firstname.trim(),
+ lastname: profileForm.lastname.trim()
+ });
- // Update the auth context with new user data
- const updatedUser = await employeeService.getEmployee(currentUser.id);
- updateUser(updatedUser);
+ // Update the auth context with new user data
+ updateUser(updatedEmployee);
- showNotification({
- type: 'success',
- title: 'Erfolg',
- message: 'Profil erfolgreich aktualisiert'
+ showNotification({
+ type: 'success',
+ title: 'Erfolg',
+ message: 'Profil erfolgreich aktualisiert'
+ });
});
- } catch (error: any) {
- showNotification({
- type: 'error',
- title: 'Fehler',
- message: error.message || 'Profil konnte nicht aktualisiert werden'
- });
- } finally {
- setLoading(false);
+ } catch (error) {
+ // Backend validation errors are already handled by executeWithValidation
+ // We only need to handle unexpected errors here
+ console.error('Unexpected error:', error);
}
};
@@ -179,7 +184,25 @@ const Settings: React.FC = () => {
e.preventDefault();
if (!currentUser) return;
- // Validation
+ // BASIC FRONTEND VALIDATION: Only check minimum requirements
+ if (!passwordForm.currentPassword) {
+ showNotification({
+ type: 'error',
+ title: 'Fehler',
+ message: 'Aktuelles Passwort ist erforderlich'
+ });
+ return;
+ }
+
+ if (!passwordForm.newPassword) {
+ showNotification({
+ type: 'error',
+ title: 'Fehler',
+ message: 'Neues Passwort ist erforderlich'
+ });
+ return;
+ }
+
if (passwordForm.newPassword.length < 6) {
showNotification({
type: 'error',
@@ -199,34 +222,30 @@ const Settings: React.FC = () => {
}
try {
- setLoading(true);
-
- // Use the actual password change endpoint
- await employeeService.changePassword(currentUser.id, {
- currentPassword: passwordForm.currentPassword,
- newPassword: passwordForm.newPassword
- });
+ // Use executeWithValidation to handle backend validation
+ await executeWithValidation(async () => {
+ await employeeService.changePassword(currentUser.id, {
+ currentPassword: passwordForm.currentPassword,
+ newPassword: passwordForm.newPassword,
+ confirmPassword: passwordForm.confirmPassword
+ });
- showNotification({
- type: 'success',
- title: 'Erfolg',
- message: 'Passwort erfolgreich geändert'
- });
+ showNotification({
+ type: 'success',
+ title: 'Erfolg',
+ message: 'Passwort erfolgreich geändert'
+ });
- // Clear password form
- setPasswordForm({
- currentPassword: '',
- newPassword: '',
- confirmPassword: ''
+ // Clear password form
+ setPasswordForm({
+ currentPassword: '',
+ newPassword: '',
+ confirmPassword: ''
+ });
});
- } catch (error: any) {
- showNotification({
- type: 'error',
- title: 'Fehler',
- message: error.message || 'Passwort konnte nicht geändert werden'
- });
- } finally {
- setLoading(false);
+ } catch (error) {
+ // Backend validation errors are already handled by executeWithValidation
+ console.error('Unexpected error:', error);
}
};
@@ -243,12 +262,18 @@ const Settings: React.FC = () => {
setShowAvailabilityManager(false);
};
+ // Clear validation errors when switching tabs
+ const handleTabChange = (tab: 'profile' | 'password' | 'availability') => {
+ clearErrors();
+ setActiveTab(tab);
+ };
+
if (!currentUser) {
- return Nicht eingeloggt
;
}
@@ -270,10 +295,10 @@ const Settings: React.FC = () => {
Einstellungen
Verwalten Sie Ihre Kontoeinstellungen und Präferenzen
-
+
-
+
-
+
-
+