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
- +
- + - + @@ -517,7 +542,7 @@ const Settings: React.FC = () => { Aktualisieren Sie Ihr Passwort für erhöhte Sicherheit

- +
{/* Current Password Field */} @@ -657,28 +682,28 @@ const Settings: React.FC = () => {
@@ -694,16 +719,16 @@ const Settings: React.FC = () => { Legen Sie Ihre persönliche Verfügbarkeit für Schichtpläne fest

- +
📅

Verfügbarkeit verwalten

Hier können Sie Ihre persönliche Verfügbarkeit für Schichtpläne festlegen. - Legen Sie für jeden Tag und jede Schicht fest, ob Sie bevorzugt, möglicherweise + Legen Sie für jeden Tag und jede Schicht fest, ob Sie bevorzugt, möglicherweise oder nicht verfügbar sind.

- +