diff --git a/database.js b/database.js index ad8de1b..623e61e 100644 --- a/database.js +++ b/database.js @@ -148,6 +148,14 @@ function initDatabase() { } }); + // Migration: Krank-Status hinzufügen + db.run(`ALTER TABLE timesheet_entries ADD COLUMN sick_status INTEGER DEFAULT 0`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + if (err && !err.message.includes('duplicate column')) { + console.warn('Warnung beim Hinzufügen der Spalte sick_status:', err.message); + } + }); + // Migration: Pausen-Zeiten für API-Zeiterfassung hinzufügen db.run(`ALTER TABLE timesheet_entries ADD COLUMN pause_start_time TEXT`, (err) => { // Fehler ignorieren wenn Spalte bereits existiert diff --git a/ldap-service.js b/ldap-service.js index 2a7ba06..5826fec 100644 --- a/ldap-service.js +++ b/ldap-service.js @@ -113,6 +113,19 @@ class LDAPService { return Array.isArray(attr.values) ? attr.values[0] : attr.values; } + /** + * Escaped einen Wert für LDAP-Filter (verhindert LDAP-Injection) + */ + static escapeLDAPFilter(value) { + if (!value) return ''; + return value + .replace(/\\/g, '\\5c') + .replace(/\*/g, '\\2a') + .replace(/\(/g, '\\28') + .replace(/\)/g, '\\29') + .replace(/\0/g, '\\00'); + } + /** * Benutzer in SQLite synchronisieren */ @@ -217,6 +230,83 @@ class LDAPService { ); } + /** + * Benutzer gegen LDAP authentifizieren + */ + static authenticate(username, password, callback) { + // Konfiguration abrufen + this.getConfig((err, config) => { + if (err || !config || !config.enabled) { + return callback(new Error('LDAP ist nicht aktiviert'), false); + } + + // LDAP-Verbindung herstellen (mit Service-Account) + this.connect(config, (err, client) => { + if (err) { + return callback(err, false); + } + + // Suche nach dem Benutzer in LDAP + const baseDN = config.base_dn || ''; + const usernameAttr = config.username_attribute || 'cn'; + const escapedUsername = this.escapeLDAPFilter(username); + const searchFilter = `(${usernameAttr}=${escapedUsername})`; + const searchOptions = { + filter: searchFilter, + scope: 'sub', + attributes: ['dn', usernameAttr] + }; + + let userDN = null; + + client.search(baseDN, searchOptions, (err, res) => { + if (err) { + client.unbind(); + return callback(err, false); + } + + res.on('searchEntry', (entry) => { + userDN = entry.dn.toString(); + }); + + res.on('error', (err) => { + client.unbind(); + callback(err, false); + }); + + res.on('end', (result) => { + // Service-Account-Verbindung schließen + client.unbind(); + + if (!userDN) { + return callback(new Error('Benutzer nicht gefunden'), false); + } + + // Versuche, sich mit den Benutzer-Credentials zu binden + const authClient = ldap.createClient({ + url: config.url, + timeout: 10000, + connectTimeout: 10000 + }); + + authClient.on('error', (err) => { + authClient.unbind(); + callback(err, false); + }); + + authClient.bind(userDN, password, (err) => { + authClient.unbind(); + if (err) { + return callback(new Error('Ungültiges Passwort'), false); + } + callback(null, true); + }); + }); + }); + }); + }); + } + /** * Vollständige Synchronisation durchführen */ diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 020328d..f4a5ccf 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -261,6 +261,7 @@ function renderWeek() { const hours = entry.total_hours || 0; const overtimeTaken = entry.overtime_taken_hours || ''; const vacationType = entry.vacation_type || ''; + const sickStatus = entry.sick_status || false; // Tätigkeiten laden const activities = [ @@ -272,26 +273,27 @@ function renderWeek() { ]; // Prüfen ob Werktag (Montag-Freitag, i < 5) ausgefüllt ist - // Bei ganztägigem Urlaub gilt der Tag als ausgefüllt - if (i < 5 && vacationType !== 'full' && (!startTime || !endTime || startTime.trim() === '' || endTime.trim() === '')) { + // Bei ganztägigem Urlaub oder Krank gilt der Tag als ausgefüllt + if (i < 5 && vacationType !== 'full' && !sickStatus && (!startTime || !endTime || startTime.trim() === '' || endTime.trim() === '')) { allWeekdaysFilled = false; } // Stunden zur Summe hinzufügen - // Bei ganztägigem Urlaub sollten es bereits 8 Stunden sein (vom Backend gesetzt) + // Bei ganztägigem Urlaub oder Krank sollten es bereits 8 Stunden sein (vom Backend gesetzt) // Bei halbem Tag Urlaub werden die Urlaubsstunden später in der Überstunden-Berechnung hinzugezählt totalHours += hours; // Bearbeitung ist immer möglich, auch nach Abschicken - // Bei ganztägigem Urlaub werden Zeitfelder deaktiviert + // Bei ganztägigem Urlaub oder Krank werden Zeitfelder deaktiviert const isFullDayVacation = vacationType === 'full'; - const timeFieldsDisabled = isFullDayVacation ? 'disabled' : ''; + const isSick = sickStatus === true || sickStatus === 1; + const timeFieldsDisabled = (isFullDayVacation || isSick) ? 'disabled' : ''; const disabled = ''; html += ` ${getWeekday(dateStr)} - ${formatDateDE(dateStr)}${isFullDayVacation ? ' (Urlaub - ganzer Tag)' : ''} + ${formatDateDE(dateStr)}${isFullDayVacation ? ' (Urlaub - ganzer Tag)' : ''}${isSick ? ' (Krank)' : ''} - ${isFullDayVacation ? '8.00 h (Urlaub)' : hours.toFixed(2) + ' h'} + ${isFullDayVacation ? '8.00 h (Urlaub)' : isSick ? '8.00 h (Krank)' : hours.toFixed(2) + ' h'} @@ -395,6 +397,21 @@ function renderWeek() { +
+ +
+ +
+
@@ -486,12 +503,16 @@ function updateOvertimeDisplay() { date.setDate(date.getDate() + i); const dateStr = formatDate(date); - // Prüfe Urlaub-Status + // Prüfe Urlaub-Status und Krank-Status const vacationSelect = document.querySelector(`select[data-date="${dateStr}"][data-field="vacation_type"]`); const vacationType = vacationSelect ? vacationSelect.value : (currentEntries[dateStr]?.vacation_type || ''); + const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`); + const sickStatus = sickCheckbox ? sickCheckbox.checked : (currentEntries[dateStr]?.sick_status || false); if (vacationType === 'full') { totalHours += 8; // Ganzer Tag Urlaub = 8 Stunden + } else if (sickStatus) { + totalHours += 8; // Krank = 8 Stunden } else { // Berechne Stunden direkt aus Start-/Endzeit und Pause const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`); @@ -594,6 +615,7 @@ async function saveEntry(input) { const notesInput = document.querySelector(`textarea[data-date="${date}"][data-field="notes"]`); const vacationSelect = document.querySelector(`select[data-date="${date}"][data-field="vacation_type"]`); const overtimeInput = document.querySelector(`input[data-date="${date}"][data-field="overtime_taken_hours"]`); + const sickCheckbox = document.querySelector(`input[data-date="${date}"][data-field="sick_status"]`); // Wenn das aktuelle Input-Element das gesuchte Feld ist, verwende dessen Wert direkt // Das stellt sicher, dass der Wert auch bei oninput/onchange sofort verfügbar ist @@ -612,6 +634,7 @@ async function saveEntry(input) { const notes = notesInput ? (notesInput.value || '') : (currentEntries[date].notes || ''); const vacation_type = vacationSelect && vacationSelect.value ? vacationSelect.value : (currentEntries[date].vacation_type || null); const overtime_taken_hours = overtimeInput && overtimeInput.value ? overtimeInput.value : (currentEntries[date].overtime_taken_hours || null); + const sick_status = sickCheckbox ? (sickCheckbox.checked ? true : false) : (currentEntries[date].sick_status || false); // Activity-Felder aus DOM lesen const activities = []; @@ -634,6 +657,7 @@ async function saveEntry(input) { currentEntries[date].notes = notes; currentEntries[date].vacation_type = vacation_type; currentEntries[date].overtime_taken_hours = overtime_taken_hours; + currentEntries[date].sick_status = sick_status; for (let i = 1; i <= 5; i++) { currentEntries[date][`activity${i}_desc`] = activities[i-1].desc; currentEntries[date][`activity${i}_hours`] = activities[i-1].hours; @@ -671,7 +695,8 @@ async function saveEntry(input) { activity5_hours: activities[4].hours, activity5_project_number: activities[4].projectNumber, overtime_taken_hours: overtime_taken_hours, - vacation_type: vacation_type + vacation_type: vacation_type, + sick_status: sick_status }) }); @@ -727,15 +752,17 @@ function checkWeekComplete() { date.setDate(date.getDate() + i); const dateStr = formatDate(date); - // Prüfe Urlaub-Status + // Prüfe Urlaub-Status und Krank-Status const entry = currentEntries[dateStr] || {}; const vacationType = entry.vacation_type; const vacationSelect = document.querySelector(`select[data-date="${dateStr}"][data-field="vacation_type"]`); const vacationValue = vacationSelect ? vacationSelect.value : (vacationType || ''); + const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`); + const sickStatus = sickCheckbox ? sickCheckbox.checked : (entry.sick_status || false); - // Wenn ganzer Tag Urlaub, dann ist der Tag als ausgefüllt zu betrachten - if (vacationValue === 'full') { - continue; // Tag ist ausgefüllt (ganzer Tag Urlaub) + // Wenn ganzer Tag Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten + if (vacationValue === 'full' || sickStatus) { + continue; // Tag ist ausgefüllt (ganzer Tag Urlaub oder Krank) } // Prüfe IMMER direkt die Input-Felder im DOM (das ist die zuverlässigste Quelle) @@ -832,14 +859,16 @@ async function submitWeek() { const weekday = getWeekday(dateStr); const dateDisplay = formatDateDE(dateStr); - // Prüfe Urlaub-Status + // Prüfe Urlaub-Status und Krank-Status const entry = currentEntries[dateStr] || {}; const vacationSelect = document.querySelector(`select[data-date="${dateStr}"][data-field="vacation_type"]`); const vacationValue = vacationSelect ? vacationSelect.value : (entry.vacation_type || ''); + const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`); + const sickStatus = sickCheckbox ? sickCheckbox.checked : (entry.sick_status || false); - // Wenn ganzer Tag Urlaub, dann ist der Tag als ausgefüllt zu betrachten - if (vacationValue === 'full') { - continue; // Tag ist ausgefüllt (ganzer Tag Urlaub) + // Wenn ganzer Tag Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten + if (vacationValue === 'full' || sickStatus) { + continue; // Tag ist ausgefüllt (ganzer Tag Urlaub oder Krank) } // Prüfe IMMER direkt die Input-Felder im DOM - auch bei manueller Eingabe @@ -1105,3 +1134,36 @@ function toggleVacationSelect(dateStr) { } } } + +// Krank-Status ein-/ausblenden +function toggleSickStatus(dateStr) { + const checkboxDiv = document.getElementById(`sick-checkbox-${dateStr}`); + if (checkboxDiv) { + if (checkboxDiv.style.display === 'none' || !checkboxDiv.style.display) { + checkboxDiv.style.display = 'inline-block'; + const checkbox = checkboxDiv.querySelector('input[type="checkbox"]'); + if (checkbox) { + // Prüfe aktuellen Status aus currentEntries + const currentSickStatus = currentEntries[dateStr]?.sick_status || false; + checkbox.checked = currentSickStatus || true; // Wenn nicht gesetzt, auf true setzen + checkbox.focus(); + // Sofort speichern wenn aktiviert + if (!currentSickStatus) { + saveEntry(checkbox); + } + } + } else { + // Wert löschen wenn ausgeblendet + const checkbox = checkboxDiv.querySelector('input[type="checkbox"]'); + if (checkbox) { + checkbox.checked = false; + // Speichern + if (currentEntries[dateStr]) { + currentEntries[dateStr].sick_status = false; + saveEntry(checkbox); + } + } + checkboxDiv.style.display = 'none'; + } + } +} diff --git a/server.js b/server.js index 806af8e..b09ee4d 100644 --- a/server.js +++ b/server.js @@ -139,56 +139,101 @@ app.get('/login', (req, res) => { app.post('/login', (req, res) => { const { username, password } = req.body; - db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => { - if (err || !user) { - return res.render('login', { error: 'Ungültiger Benutzername oder Passwort' }); + // Prüfe ob LDAP aktiviert ist + LDAPService.getConfig((err, ldapConfig) => { + if (err) { + console.error('Fehler beim Abrufen der LDAP-Konfiguration:', err); } - if (bcrypt.compareSync(password, user.password)) { - // Rollen als JSON-Array parsen - let roles = []; - try { - roles = JSON.parse(user.role); - if (!Array.isArray(roles)) { - // Fallback: Falls kein Array, erstelle Array mit vorhandener Rolle - roles = [user.role]; + const isLDAPEnabled = ldapConfig && ldapConfig.enabled === 1; + + // Wenn LDAP aktiviert ist, authentifiziere gegen LDAP + if (isLDAPEnabled) { + LDAPService.authenticate(username, password, (authErr, authSuccess) => { + if (authErr || !authSuccess) { + // LDAP-Authentifizierung fehlgeschlagen - prüfe lokale Datenbank als Fallback + db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => { + if (err || !user) { + return res.render('login', { error: 'Ungültiger Benutzername oder Passwort' }); + } + + // Versuche lokale Authentifizierung + if (bcrypt.compareSync(password, user.password)) { + handleSuccessfulLogin(req, res, user); + } else { + res.render('login', { error: 'Ungültiger Benutzername oder Passwort' }); + } + }); + } else { + // LDAP-Authentifizierung erfolgreich - hole Benutzer aus Datenbank + db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => { + if (err || !user) { + return res.render('login', { error: 'Benutzer nicht in der Datenbank gefunden. Bitte führen Sie eine LDAP-Synchronisation durch.' }); + } + + handleSuccessfulLogin(req, res, user); + }); } - } catch (e) { - // Fallback: Falls kein JSON, erstelle Array mit vorhandener Rolle - roles = [user.role || 'mitarbeiter']; - } - - // Standard-Rolle bestimmen: Immer "mitarbeiter" wenn vorhanden, sonst höchste Priorität - let defaultRole; - if (roles.includes('mitarbeiter')) { - defaultRole = 'mitarbeiter'; - } else { - defaultRole = getDefaultRole(roles); - } - - req.session.userId = user.id; - req.session.username = user.username; - req.session.roles = roles; - req.session.currentRole = defaultRole; - req.session.firstname = user.firstname; - req.session.lastname = user.lastname; - - // Redirect: Immer zu Dashboard wenn Mitarbeiter-Rolle vorhanden, sonst basierend auf Standard-Rolle - if (roles.includes('mitarbeiter')) { - res.redirect('/dashboard'); - } else if (defaultRole === 'admin') { - res.redirect('/admin'); - } else if (defaultRole === 'verwaltung') { - res.redirect('/verwaltung'); - } else { - res.redirect('/dashboard'); - } + }); } else { - res.render('login', { error: 'Ungültiger Benutzername oder Passwort' }); + // LDAP nicht aktiviert - verwende lokale Authentifizierung + db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => { + if (err || !user) { + return res.render('login', { error: 'Ungültiger Benutzername oder Passwort' }); + } + + if (bcrypt.compareSync(password, user.password)) { + handleSuccessfulLogin(req, res, user); + } else { + res.render('login', { error: 'Ungültiger Benutzername oder Passwort' }); + } + }); } }); }); +// Helper-Funktion für erfolgreiche Anmeldung +function handleSuccessfulLogin(req, res, user) { + // Rollen als JSON-Array parsen + let roles = []; + try { + roles = JSON.parse(user.role); + if (!Array.isArray(roles)) { + // Fallback: Falls kein Array, erstelle Array mit vorhandener Rolle + roles = [user.role]; + } + } catch (e) { + // Fallback: Falls kein JSON, erstelle Array mit vorhandener Rolle + roles = [user.role || 'mitarbeiter']; + } + + // Standard-Rolle bestimmen: Immer "mitarbeiter" wenn vorhanden, sonst höchste Priorität + let defaultRole; + if (roles.includes('mitarbeiter')) { + defaultRole = 'mitarbeiter'; + } else { + defaultRole = getDefaultRole(roles); + } + + req.session.userId = user.id; + req.session.username = user.username; + req.session.roles = roles; + req.session.currentRole = defaultRole; + req.session.firstname = user.firstname; + req.session.lastname = user.lastname; + + // Redirect: Immer zu Dashboard wenn Mitarbeiter-Rolle vorhanden, sonst basierend auf Standard-Rolle + if (roles.includes('mitarbeiter')) { + res.redirect('/dashboard'); + } else if (defaultRole === 'admin') { + res.redirect('/admin'); + } else if (defaultRole === 'verwaltung') { + res.redirect('/verwaltung'); + } else { + res.redirect('/dashboard'); + } +} + // Logout app.get('/logout', (req, res) => { req.session.destroy(); @@ -810,7 +855,7 @@ app.post('/api/timesheet/save', requireAuth, (req, res) => { activity3_desc, activity3_hours, activity3_project_number, activity4_desc, activity4_hours, activity4_project_number, activity5_desc, activity5_hours, activity5_project_number, - overtime_taken_hours, vacation_type + overtime_taken_hours, vacation_type, sick_status } = req.body; const userId = req.session.userId; @@ -818,11 +863,21 @@ app.post('/api/timesheet/save', requireAuth, (req, res) => { const normalizedEndTime = (end_time && typeof end_time === 'string' && end_time.trim() !== '') ? end_time.trim() : (end_time || null); const normalizedStartTime = (start_time && typeof start_time === 'string' && start_time.trim() !== '') ? start_time.trim() : (start_time || null); + // Normalisiere sick_status: Boolean oder 1/0 zu Boolean + const isSick = sick_status === true || sick_status === 1 || sick_status === 'true' || sick_status === '1'; + // Gesamtstunden berechnen (aus Start- und Endzeit, nicht aus Tätigkeiten) - // Wenn ganzer Tag Urlaub, dann zählt dieser als 8 Stunden normale Arbeitszeit + // Wenn ganzer Tag Urlaub oder Krank, dann zählt dieser als 8 Stunden normale Arbeitszeit let total_hours = 0; + let finalActivity1Desc = activity1_desc; + let finalActivity1Hours = parseFloat(activity1_hours) || 0; + if (vacation_type === 'full') { total_hours = 8; // Ganzer Tag Urlaub = 8 Stunden normale Arbeitszeit + } else if (isSick) { + total_hours = 8; // Krank = 8 Stunden normale Arbeitszeit + finalActivity1Desc = 'Krank'; + finalActivity1Hours = 8; } else if (normalizedStartTime && normalizedEndTime) { const start = new Date(`2000-01-01T${normalizedStartTime}`); const end = new Date(`2000-01-01T${normalizedEndTime}`); @@ -844,18 +899,19 @@ app.post('/api/timesheet/save', requireAuth, (req, res) => { activity3_desc = ?, activity3_hours = ?, activity3_project_number = ?, activity4_desc = ?, activity4_hours = ?, activity4_project_number = ?, activity5_desc = ?, activity5_hours = ?, activity5_project_number = ?, - overtime_taken_hours = ?, vacation_type = ?, + overtime_taken_hours = ?, vacation_type = ?, sick_status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [ normalizedStartTime, normalizedEndTime, break_minutes, total_hours, notes, - activity1_desc || null, parseFloat(activity1_hours) || 0, activity1_project_number || null, + finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null, activity2_desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null, activity3_desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null, activity4_desc || null, parseFloat(activity4_hours) || 0, activity4_project_number || null, activity5_desc || null, parseFloat(activity5_hours) || 0, activity5_project_number || null, overtime_taken_hours ? parseFloat(overtime_taken_hours) : null, vacation_type || null, + isSick ? 1 : 0, row.id ], (err) => { @@ -874,17 +930,18 @@ app.post('/api/timesheet/save', requireAuth, (req, res) => { activity3_desc, activity3_hours, activity3_project_number, activity4_desc, activity4_hours, activity4_project_number, activity5_desc, activity5_hours, activity5_project_number, - overtime_taken_hours, vacation_type) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + overtime_taken_hours, vacation_type, sick_status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ userId, date, normalizedStartTime, normalizedEndTime, break_minutes, total_hours, notes, - activity1_desc || null, parseFloat(activity1_hours) || 0, activity1_project_number || null, + finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null, activity2_desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null, activity3_desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null, activity4_desc || null, parseFloat(activity4_hours) || 0, activity4_project_number || null, activity5_desc || null, parseFloat(activity5_hours) || 0, activity5_project_number || null, overtime_taken_hours ? parseFloat(overtime_taken_hours) : null, - vacation_type || null + vacation_type || null, + isSick ? 1 : 0 ], (err) => { if (err) { @@ -1049,10 +1106,10 @@ app.post('/api/timesheet/submit', requireAuth, (req, res) => { const { week_start, week_end, version_reason } = req.body; const userId = req.session.userId; - // Validierung: Prüfen ob alle 7 Tage der Woche ausgefüllt sind - db.all(`SELECT id, date, start_time, end_time, vacation_type, updated_at FROM timesheet_entries - WHERE user_id = ? AND date >= ? AND date <= ? - ORDER BY date, updated_at DESC, id DESC`, + // Validierung: Prüfen ob alle 7 Tage der Woche ausgefüllt sind + db.all(`SELECT id, date, start_time, end_time, vacation_type, sick_status, updated_at FROM timesheet_entries + WHERE user_id = ? AND date >= ? AND date <= ? + ORDER BY date, updated_at DESC, id DESC`, [userId, week_start, week_end], (err, entries) => { if (err) { @@ -1097,8 +1154,9 @@ app.post('/api/timesheet/submit', requireAuth, (req, res) => { const dateStr = `${year}-${month}-${day}`; const entry = entriesByDate[dateStr]; - // Wenn ganztägiger Urlaub, dann ist der Tag als ausgefüllt zu betrachten - if (entry && entry.vacation_type === 'full') { + // Wenn ganztägiger Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten + const isSick = entry && (entry.sick_status === 1 || entry.sick_status === true); + if (entry && (entry.vacation_type === 'full' || isSick)) { continue; // Tag ist ausgefüllt }