added hashed passwords

This commit is contained in:
2025-10-08 18:11:04 +02:00
parent a6ec865571
commit 96a36d68a9
7 changed files with 469 additions and 158 deletions

View File

@@ -9,7 +9,7 @@ export interface Employee {
phone?: string; phone?: string;
department?: string; department?: string;
createdAt: string; createdAt: string;
lastLogin?: string; lastLogin?: string | null;
} }
export interface Availability { export interface Availability {

View File

@@ -1,31 +1,49 @@
// backend/src/server.ts - Erweitert // backend/src/server.ts - Login für alle Benutzer
import express from 'express'; const express = require('express');
import cors from 'cors'; const cors = require('cors');
import { db } from './services/databaseService.js'; const { v4: uuidv4 } = require('uuid');
import { seedData } from './scripts/seedData.js'; const bcrypt = require('bcryptjs');
import authRoutes from './routes/auth.js';
import shiftTemplateRoutes from './routes/shiftTemplates.js';
import shiftPlanRoutes from './routes/shiftPlans.js';
import employeeRoutes from './routes/employees.js'; // NEU HINZUGEFÜGT
const app = express(); const app = express();
const PORT = 3002; const PORT = 3002;
// Middleware // IN-MEMORY STORE für Mitarbeiter (mit gehashten Passwörtern)
let employees = [
{
id: '1',
email: 'admin@schichtplan.de',
password: '$2a$10$8K1p/a0dRTlB0ZQ1.5Q.2e5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q', // admin123
name: 'Admin User',
role: 'admin',
isActive: true,
phone: '+49 123 456789',
department: 'IT',
createdAt: new Date().toISOString(),
lastLogin: new Date().toISOString()
},
{
id: '2',
email: 'instandhalter@schichtplan.de',
password: '$2a$10$8K1p/a0dRTlB0ZQ1.5Q.2e5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q', // instandhalter123
name: 'Max Instandhalter',
role: 'instandhalter',
isActive: true,
phone: '+49 123 456790',
department: 'Produktion',
createdAt: new Date().toISOString(),
lastLogin: new Date().toISOString()
}
];
// CORS und Middleware
app.use(cors({ app.use(cors({
origin: 'http://localhost:3000', origin: 'http://localhost:3000',
credentials: true credentials: true
})); }));
app.use(express.json()); app.use(express.json());
// Routes // Health route
app.use('/api/auth', authRoutes); app.get('/api/health', (req: any, res: any) => {
app.use('/api/shift-templates', shiftTemplateRoutes);
app.use('/api/shift-plans', shiftPlanRoutes);
app.use('/api/employees', employeeRoutes); // NEU HINZUGEFÜGT
// Health check
app.get('/api/health', (req, res) => {
res.json({ res.json({
status: 'OK', status: 'OK',
message: 'Backend läuft!', message: 'Backend läuft!',
@@ -33,12 +51,271 @@ app.get('/api/health', (req, res) => {
}); });
}); });
// Login Route für ALLE Benutzer
app.post('/api/auth/login', async (req: any, res: any) => {
try {
const { email, password } = req.body;
console.log('🔐 Login attempt for:', email);
if (!email || !password) {
return res.status(400).json({ error: 'Email and password are required' });
}
// Benutzer in der employees Liste suchen
const user = employees.find(emp => emp.email === email && emp.isActive);
if (!user) {
console.log('❌ User not found or inactive:', email);
return res.status(401).json({ error: 'Invalid credentials' });
}
// Passwort vergleichen
// Für Test: Wenn Passwort nicht gehasht ist (neue Benutzer), direkt vergleichen
let isPasswordValid = false;
if (user.password.startsWith('$2a$')) {
// Gehashtes Passwort (bcrypt)
isPasswordValid = await bcrypt.compare(password, user.password);
} else {
// Klartext-Passwort (für Test)
isPasswordValid = password === user.password;
}
if (!isPasswordValid) {
console.log('❌ Invalid password for:', email);
return res.status(401).json({ error: 'Invalid credentials' });
}
// Last Login aktualisieren
user.lastLogin = new Date().toISOString();
console.log('✅ Login successful for:', email);
// User ohne Passwort zurückgeben
const { password: _, ...userWithoutPassword } = user;
res.json({
user: userWithoutPassword,
token: 'jwt-token-' + Date.now() + '-' + user.id,
expiresIn: '7d'
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// EMPLOYEE ROUTES mit In-Memory Store
app.get('/api/employees', async (req: any, res: any) => {
try {
console.log('📋 Fetching employees - Total:', employees.length);
// Passwörter ausblenden
const employeesWithoutPasswords = employees.map(emp => {
const { password, ...empWithoutPassword } = emp;
return empWithoutPassword;
});
res.json(employeesWithoutPasswords);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
app.post('/api/employees', async (req: any, res: any) => {
try {
const { email, password, name, role, phone, department } = req.body;
console.log('👤 Creating employee:', { email, name, role });
// Validierung
if (!email || !password || !name || !role) {
return res.status(400).json({ error: 'Email, password, name and role are required' });
}
if (password.length < 6) {
return res.status(400).json({ error: 'Password must be at least 6 characters long' });
}
// Check if email already exists
if (employees.find(emp => emp.email === email)) {
return res.status(409).json({ error: 'Email already exists' });
}
// Passwort hashen für neue Benutzer
const hashedPassword = await bcrypt.hash(password, 10);
// Neuen Mitarbeiter erstellen
const newEmployee = {
id: uuidv4(),
email,
password: hashedPassword, // Gehashtes Passwort speichern
name,
role,
isActive: true,
phone: phone || '',
department: department || '',
createdAt: new Date().toISOString(),
lastLogin: ''
};
// Zum Store hinzufügen
employees.push(newEmployee);
console.log('✅ Employee created. Total employees:', employees.length);
// Response ohne Passwort
const { password: _, ...employeeWithoutPassword } = newEmployee;
res.status(201).json(employeeWithoutPassword);
} catch (error) {
console.error('Error creating employee:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
app.put('/api/employees/:id', async (req: any, res: any) => {
try {
const { id } = req.params;
const { name, role, isActive, phone, department } = req.body;
console.log('✏️ Updating employee:', id);
// Mitarbeiter finden
const employeeIndex = employees.findIndex(emp => emp.id === id);
if (employeeIndex === -1) {
return res.status(404).json({ error: 'Employee not found' });
}
// Mitarbeiter aktualisieren (Passwort bleibt unverändert)
employees[employeeIndex] = {
...employees[employeeIndex],
name: name || employees[employeeIndex].name,
role: role || employees[employeeIndex].role,
isActive: isActive !== undefined ? isActive : employees[employeeIndex].isActive,
phone: phone || employees[employeeIndex].phone,
department: department || employees[employeeIndex].department
};
console.log('✅ Employee updated:', employees[employeeIndex].name);
// Response ohne Passwort
const { password, ...employeeWithoutPassword } = employees[employeeIndex];
res.json(employeeWithoutPassword);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
app.delete('/api/employees/:id', async (req: any, res: any) => {
try {
const { id } = req.params;
console.log('🗑️ Deleting employee:', id);
// Mitarbeiter finden
const employeeIndex = employees.findIndex(emp => emp.id === id);
if (employeeIndex === -1) {
return res.status(404).json({ error: 'Employee not found' });
}
// Soft delete - set isActive to false
employees[employeeIndex].isActive = false;
console.log('✅ Employee deactivated:', employees[employeeIndex].name);
res.status(204).send();
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
// Get current user profile
app.get('/api/auth/me', async (req: any, res: any) => {
try {
// Einfache Mock-Implementation
// In einer echten App würde man den Token verifizieren
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
// Einfache Token-"Validierung" für Demo
const tokenParts = token.split('-');
const userId = tokenParts[tokenParts.length - 1];
const user = employees.find(emp => emp.id === userId && emp.isActive);
if (!user) {
return res.status(401).json({ error: 'Invalid token' });
}
// User ohne Passwort zurückgeben
const { password, ...userWithoutPassword } = user;
res.json(userWithoutPassword);
} catch (error) {
console.error('Error getting user profile:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Availability Routes (Mock)
app.get('/api/employees/:employeeId/availabilities', async (req: any, res: any) => {
try {
const { employeeId } = req.params;
console.log('📅 Fetching availabilities for:', employeeId);
// Mock Verfügbarkeiten
const daysOfWeek = [0, 1, 2, 3, 4, 5, 6];
const timeSlots = [
{ name: 'Vormittag', start: '08:00', end: '12:00' },
{ name: 'Nachmittag', start: '12:00', end: '16:00' },
{ name: 'Abend', start: '16:00', end: '20:00' }
];
const mockAvailabilities = daysOfWeek.flatMap(day =>
timeSlots.map((slot, index) => ({
id: `avail-${employeeId}-${day}-${index}`,
employeeId,
dayOfWeek: day,
startTime: slot.start,
endTime: slot.end,
isAvailable: day >= 1 && day <= 5 // Nur Mo-Fr verfügbar
}))
);
res.json(mockAvailabilities);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
app.put('/api/employees/:employeeId/availabilities', async (req: any, res: any) => {
try {
const { employeeId } = req.params;
const availabilities = req.body;
console.log('💾 Saving availabilities for:', employeeId);
console.log('Data:', availabilities);
// Mock erfolgreiches Speichern
res.json(availabilities);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
// Start server // Start server
app.listen(PORT, async () => { app.listen(PORT, () => {
console.log('🎉 BACKEND STARTED SUCCESSFULLY!'); console.log('🎉 BACKEND STARTED SUCCESSFULLY!');
console.log(`📍 Port: ${PORT}`); console.log(`📍 Port: ${PORT}`);
console.log(`📍 Health: http://localhost:${PORT}/api/health`); console.log(`📍 Health: http://localhost:${PORT}/api/health`);
console.log('📊 Employee management ready!'); console.log('🔐 Login system READY for ALL users!');
console.log('👥 Employee management READY with proper authentication!');
await seedData();
}); });

View File

@@ -1,11 +1,10 @@
// frontend/src/contexts/AuthContext.tsx // frontend/src/contexts/AuthContext.tsx
import React, { createContext, useContext, useState, useEffect } from 'react'; import React, { createContext, useContext, useState, useEffect } from 'react';
import { authService, User, LoginRequest, RegisterRequest } from '../services/authService'; import { authService, User, LoginRequest } from '../services/authService';
interface AuthContextType { interface AuthContextType {
user: User | null; user: User | null;
login: (credentials: LoginRequest) => Promise<void>; login: (credentials: LoginRequest) => Promise<void>;
register: (userData: RegisterRequest) => Promise<void>;
logout: () => void; logout: () => void;
hasRole: (roles: string[]) => boolean; hasRole: (roles: string[]) => boolean;
loading: boolean; loading: boolean;
@@ -19,43 +18,26 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
useEffect(() => { useEffect(() => {
// User aus localStorage laden beim Start // User aus localStorage laden beim Start
const initAuth = async () => {
const savedUser = authService.getCurrentUser(); const savedUser = authService.getCurrentUser();
if (savedUser) { if (savedUser) {
setUser(savedUser); setUser(savedUser);
} }
setLoading(false); setLoading(false);
};
initAuth();
}, []); }, []);
const login = async (credentials: LoginRequest) => { const login = async (credentials: LoginRequest) => {
try { try {
const response = await authService.login(credentials); const response = await authService.login(credentials);
setUser(response.user); // ← WICHTIG: User State updaten! setUser(response.user);
console.log('AuthContext: User nach Login gesetzt', response.user);
} catch (error) { } catch (error) {
console.error('AuthContext: Login fehlgeschlagen', error); console.error('AuthContext: Login fehlgeschlagen', error);
throw error; throw error;
} }
}; };
const register = async (userData: RegisterRequest) => {
try {
const response = await authService.register(userData);
setUser(response.user);
console.log('AuthContext: User nach Registrierung gesetzt', response.user);
} catch (error) {
console.error('AuthContext: Registrierung fehlgeschlagen', error);
throw error;
}
};
const logout = () => { const logout = () => {
authService.logout(); authService.logout();
setUser(null); setUser(null);
console.log('AuthContext: User nach Logout entfernt');
}; };
const hasRole = (roles: string[]) => { const hasRole = (roles: string[]) => {
@@ -65,7 +47,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const value = { const value = {
user, user,
login, login,
register,
logout, logout,
hasRole, hasRole,
loading loading

View File

@@ -24,9 +24,13 @@ const EmployeeManagement: React.FC = () => {
const loadEmployees = async () => { const loadEmployees = async () => {
try { try {
setLoading(true); setLoading(true);
setError('');
console.log('🔄 Loading employees...');
const data = await employeeService.getEmployees(); const data = await employeeService.getEmployees();
console.log('✅ Employees loaded:', data);
setEmployees(data); setEmployees(data);
} catch (err: any) { } catch (err: any) {
console.error('❌ Error loading employees:', err);
setError(err.message || 'Fehler beim Laden der Mitarbeiter'); setError(err.message || 'Fehler beim Laden der Mitarbeiter');
} finally { } finally {
setLoading(false); setLoading(false);
@@ -51,15 +55,19 @@ const EmployeeManagement: React.FC = () => {
const handleBackToList = () => { const handleBackToList = () => {
setViewMode('list'); setViewMode('list');
setSelectedEmployee(null); setSelectedEmployee(null);
loadEmployees(); // Daten aktualisieren
}; };
// KORRIGIERT: Explizit Daten neu laden nach Create/Update
const handleEmployeeCreated = () => { const handleEmployeeCreated = () => {
handleBackToList(); console.log('🔄 Reloading employees after creation...');
loadEmployees(); // Daten neu laden
setViewMode('list'); // Zurück zur Liste
}; };
const handleEmployeeUpdated = () => { const handleEmployeeUpdated = () => {
handleBackToList(); console.log('🔄 Reloading employees after update...');
loadEmployees(); // Daten neu laden
setViewMode('list'); // Zurück zur Liste
}; };
const handleDeleteEmployee = async (employeeId: string) => { const handleDeleteEmployee = async (employeeId: string) => {
@@ -75,6 +83,13 @@ const EmployeeManagement: React.FC = () => {
} }
}; };
// Debug: Zeige aktuellen State
console.log('📊 Current state:', {
viewMode,
employeesCount: employees.length,
selectedEmployee: selectedEmployee?.name
});
if (loading && viewMode === 'list') { if (loading && viewMode === 'list') {
return ( return (
<div style={{ textAlign: 'center', padding: '40px' }}> <div style={{ textAlign: 'center', padding: '40px' }}>
@@ -97,7 +112,7 @@ const EmployeeManagement: React.FC = () => {
<div> <div>
<h1 style={{ margin: 0, color: '#2c3e50' }}>👥 Mitarbeiter Verwaltung</h1> <h1 style={{ margin: 0, color: '#2c3e50' }}>👥 Mitarbeiter Verwaltung</h1>
<p style={{ margin: '5px 0 0 0', color: '#7f8c8d' }}> <p style={{ margin: '5px 0 0 0', color: '#7f8c8d' }}>
Verwalten Sie Mitarbeiterkonten und Verfügbarkeiten {employees.length} Mitarbeiter gefunden
</p> </p>
</div> </div>
@@ -197,7 +212,7 @@ const EmployeeManagement: React.FC = () => {
{viewMode === 'availability' && selectedEmployee && ( {viewMode === 'availability' && selectedEmployee && (
<AvailabilityManager <AvailabilityManager
employee={selectedEmployee} employee={selectedEmployee}
onSave={handleBackToList} onSave={handleEmployeeUpdated}
onCancel={handleBackToList} onCancel={handleBackToList}
/> />
)} )}

View File

@@ -152,13 +152,13 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
overflow: 'hidden', overflow: 'hidden',
marginBottom: '30px' marginBottom: '30px'
}}> }}>
{daysOfWeek.map(day => { {daysOfWeek.map((day, dayIndex) => {
const dayAvailabilities = getAvailabilitiesForDay(day.id); const dayAvailabilities = getAvailabilitiesForDay(day.id);
const isLastDay = dayIndex === daysOfWeek.length - 1;
return ( return (
<div key={day.id} style={{ <div key={day.id} style={{
borderBottom: '1px solid #f0f0f0', borderBottom: isLastDay ? 'none' : '1px solid #f0f0f0'
':last-child': { borderBottom: 'none' }
}}> }}>
{/* Tag Header */} {/* Tag Header */}
<div style={{ <div style={{
@@ -173,7 +173,10 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
{/* Zeit-Slots */} {/* Zeit-Slots */}
<div style={{ padding: '15px 20px' }}> <div style={{ padding: '15px 20px' }}>
{dayAvailabilities.map(availability => ( {dayAvailabilities.map((availability, availabilityIndex) => {
const isLastAvailability = availabilityIndex === dayAvailabilities.length - 1;
return (
<div <div
key={availability.id} key={availability.id}
style={{ style={{
@@ -182,8 +185,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
gap: '15px', gap: '15px',
alignItems: 'center', alignItems: 'center',
padding: '10px 0', padding: '10px 0',
borderBottom: '1px solid #f8f9fa', borderBottom: isLastAvailability ? 'none' : '1px solid #f8f9fa'
':last-child': { borderBottom: 'none' }
}} }}
> >
{/* Verfügbarkeit Toggle */} {/* Verfügbarkeit Toggle */}
@@ -241,7 +243,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
border: `1px solid ${availability.isAvailable ? '#ddd' : '#f0f0f0'}`, border: `1px solid ${availability.isAvailable ? '#ddd' : '#f0f0f0'}`,
borderRadius: '4px', borderRadius: '4px',
backgroundColor: availability.isAvailable ? 'white' : '#f8f9fa', backgroundColor: availability.isAvailable ? 'white' : '#f8f9fa',
color: availability.isavailable ? '#333' : '#999' color: availability.isAvailable ? '#333' : '#999'
}} }}
/> />
</div> </div>
@@ -262,7 +264,8 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
</span> </span>
</div> </div>
</div> </div>
))} );
})}
</div> </div>
</div> </div>
); );

View File

@@ -11,6 +11,8 @@ export interface RegisterRequest {
password: string; password: string;
name: string; name: string;
role?: string; role?: string;
phone?: string;
department?: string;
} }
export interface AuthResponse { export interface AuthResponse {
@@ -25,6 +27,10 @@ export interface User {
name: string; name: string;
role: 'admin' | 'instandhalter' | 'user'; role: 'admin' | 'instandhalter' | 'user';
createdAt: string; createdAt: string;
lastLogin?: string;
phone?: string;
department?: string;
isActive?: boolean;
} }
class AuthService { class AuthService {
@@ -38,7 +44,8 @@ class AuthService {
}); });
if (!response.ok) { if (!response.ok) {
throw new Error('Login fehlgeschlagen'); const errorData = await response.json();
throw new Error(errorData.error || 'Login fehlgeschlagen');
} }
const data: AuthResponse = await response.json(); const data: AuthResponse = await response.json();
@@ -49,23 +56,56 @@ class AuthService {
return data; return data;
} }
// Register Methode hinzufügen
async register(userData: RegisterRequest): Promise<AuthResponse> { async register(userData: RegisterRequest): Promise<AuthResponse> {
const response = await fetch(`${API_BASE}/auth/register`, { const response = await fetch(`${API_BASE}/employees`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData) body: JSON.stringify(userData)
}); });
if (!response.ok) { if (!response.ok) {
throw new Error('Registrierung fehlgeschlagen'); const errorData = await response.json();
throw new Error(errorData.error || 'Registrierung fehlgeschlagen');
} }
const data: AuthResponse = await response.json(); // Nach der Erstellung automatisch einloggen
this.token = data.token; return this.login({
localStorage.setItem('token', data.token); email: userData.email,
localStorage.setItem('user', JSON.stringify(data.user)); password: userData.password
});
}
return data; // getCurrentUser als SYNCHRON machen
getCurrentUser(): User | null {
const userStr = localStorage.getItem('user');
return userStr ? JSON.parse(userStr) : null;
}
// Asynchrone Methode für Server-Abfrage
async fetchCurrentUser(): Promise<User | null> {
const token = this.getToken();
if (!token) {
return null;
}
try {
const response = await fetch(`${API_BASE}/auth/me`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const user = await response.json();
localStorage.setItem('user', JSON.stringify(user));
return user;
}
} catch (error) {
console.error('Error fetching current user:', error);
}
return null;
} }
logout(): void { logout(): void {
@@ -81,11 +121,6 @@ class AuthService {
return this.token; return this.token;
} }
getCurrentUser(): User | null {
const userStr = localStorage.getItem('user');
return userStr ? JSON.parse(userStr) : null;
}
isAuthenticated(): boolean { isAuthenticated(): boolean {
return this.getToken() !== null; return this.getToken() !== null;
} }

View File

@@ -6,7 +6,7 @@ export interface Employee {
role: 'admin' | 'instandhalter' | 'user'; role: 'admin' | 'instandhalter' | 'user';
isActive: boolean; isActive: boolean;
createdAt: string; createdAt: string;
lastLogin?: string; lastLogin?: string | null;
phone?: string; phone?: string;
department?: string; department?: string;
} }