From d6e985998a57ac74dedd160fdcaa14bf0370bd53 Mon Sep 17 00:00:00 2001 From: Carsten Graf Date: Tue, 3 Feb 2026 23:25:37 +0100 Subject: [PATCH] Diverse anpassungen --- Stunderfassung todo.txt | 18 ++-- public/js/dashboard.js | 71 ++++--------- routes/auth-routes.js | 53 ++++++++++ server.js | 14 ++- services/ldap-service.js | 216 +++++++++++++++++++++++++++++++++------ views/admin.ejs | 2 +- views/login.ejs | 52 +++++++++- 7 files changed, 332 insertions(+), 94 deletions(-) diff --git a/Stunderfassung todo.txt b/Stunderfassung todo.txt index 0d76971..eb39ffd 100644 --- a/Stunderfassung todo.txt +++ b/Stunderfassung todo.txt @@ -1,11 +1,11 @@ -- Mitarbeiter Name in den QR code Sheets -> DONE -- Pause vorbelegen (einstellbar in der Admin) -> DONE Wird anhand der Gesetztlichen vorgaben berechnet -- Offset für die Verwaltung für Urlaubstage -> DONE -- Stunden pro Tag und wie viele Tage arbeit -> DONE -- Reisen für Wochenende -> DONE -- LDAP Prüfung -- DSGVO Sicherheit -- Feiertage müssen als ausgefüllt zählen -> DONE -- Mitarbeiter sollen PDF ansehen können. -> DONE +- Mitarbeiter Name in den QR code Sheets -> DONE +- Pause vorbelegen (einstellbar in der Admin) -> DONE Wird anhand der Gesetztlichen vorgaben berechnet +- Offset für die Verwaltung für Urlaubstage -> DONE +- Stunden pro Tag und wie viele Tage arbeit -> DONE +- Reisen für Wochenende -> DONE +- LDAP Prüfung -> DONE TESTEn mit Jessi und Jörg +- DSGVO Sicherheit -> DONE +- Feiertage müssen als ausgefüllt zählen -> DONE +- Mitarbeiter sollen PDF ansehen können. -> DONE - Wenn bereits heruntergeladen wurde und neue version da ist Meldung an Verwaltung. -> DONE Muss getestet werden - \ No newline at end of file diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 4d95f6a..56d1010 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -914,26 +914,25 @@ function handleOvertimeChange(dateStr, overtimeHours) { const fullDayHours = getFullDayHours(); const overtimeValue = parseFloat(overtimeHours) || 0; + // Entferne "Überstunden" aus Activity-Feldern falls vorhanden + // (Überstunden werden nur im PDF angezeigt, nicht als Tätigkeit) + for (let i = 1; i <= 5; i++) { + const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_desc"]`); + const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours"]`); + + if (descInput && descInput.value && descInput.value.trim().toLowerCase() === 'überstunden') { + descInput.value = ''; + saveEntry(descInput); + if (hoursInput) { + hoursInput.value = ''; + saveEntry(hoursInput); + } + } + } + // Prüfe ob ganzer Tag Überstunden if (overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01) { - // Ganzer Tag Überstunden - // Setze Activity1 auf "Überstunden" mit 0 Stunden - const activity1DescInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity1_desc"]`); - const activity1HoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity1_hours"]`); - - if (activity1DescInput) { - activity1DescInput.value = 'Überstunden'; - // Trigger saveEntry für dieses Feld - saveEntry(activity1DescInput); - } - - if (activity1HoursInput) { - activity1HoursInput.value = '0'; - // Trigger saveEntry für dieses Feld - saveEntry(activity1HoursInput); - } - - // Leere Start- und End-Zeit + // Ganzer Tag Überstunden - leere Start- und End-Zeit const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`); const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`); @@ -946,41 +945,9 @@ function handleOvertimeChange(dateStr, overtimeHours) { endInput.value = ''; saveEntry(endInput); } - - } else if (overtimeValue > 0 && overtimeValue < fullDayHours) { - // Weniger als ganzer Tag - füge "Überstunden" als Tätigkeit hinzu - // Finde erste freie Activity-Spalte oder prüfe ob bereits vorhanden - let foundOvertime = false; - let firstEmptySlot = null; - - for (let i = 1; i <= 5; i++) { - const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_desc"]`); - const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours"]`); - - if (descInput && descInput.value && descInput.value.trim().toLowerCase() === 'überstunden') { - foundOvertime = true; - break; // Bereits vorhanden - } - - if (!firstEmptySlot && descInput && (!descInput.value || descInput.value.trim() === '')) { - firstEmptySlot = i; - } - } - - // Wenn nicht gefunden und freier Slot vorhanden, füge hinzu - if (!foundOvertime && firstEmptySlot) { - const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${firstEmptySlot}_desc"]`); - const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${firstEmptySlot}_hours"]`); - - if (descInput) { - descInput.value = 'Überstunden'; - saveEntry(descInput); - } - - // Stunden bleiben unverändert (werden vom User eingegeben oder bleiben leer) - // total_hours bleibt auch unverändert - } } + // Bei weniger als ganzer Tag oder keine Überstunden: keine weiteren Aktionen + // Überstunden werden nur im PDF als Information angezeigt } // Berechnet die gesetzlich erforderliche Mindestpause basierend auf der Arbeitszeit diff --git a/routes/auth-routes.js b/routes/auth-routes.js index 5a13bd9..9af9e39 100644 --- a/routes/auth-routes.js +++ b/routes/auth-routes.js @@ -5,6 +5,40 @@ const { db } = require('../database'); const LDAPService = require('../services/ldap-service'); const { getDefaultRole } = require('../helpers/utils'); +// Helper-Funktion für UTF-8 Debug-Logging +function logUsernameEncoding(label, username) { + if (!username) { + console.log(`[${label}] Username is null or undefined`); + return; + } + + console.group(`🔍 ${label} - Server Console`); + console.log('Original String:', username); + console.log('String Length:', username.length); + console.log('Type:', typeof username); + + // UTF-8 Byte-Repräsentation + const utf8Bytes = Buffer.from(username, 'utf8'); + console.log('UTF-8 Bytes:', Array.from(utf8Bytes)); + console.log('UTF-8 Bytes (Hex):', Array.from(utf8Bytes).map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ')); + + // Einzelne Zeichen analysieren + console.log('=== Character Analysis ==='); + for (let i = 0; i < username.length; i++) { + const char = username[i]; + const codePoint = char.codePointAt(0); + const utf8BytesForChar = Buffer.from(char, 'utf8'); + console.log(`Position ${i}: "${char}" | CodePoint: U+${codePoint.toString(16).toUpperCase().padStart(4, '0')} (${codePoint}) | UTF-8 Bytes: [${Array.from(utf8BytesForChar).join(', ')}]`); + } + + // URL-Encoding Test + console.log('=== URL Encoding Test ==='); + console.log('encodeURIComponent(username):', encodeURIComponent(username)); + console.log('encodeURI(username):', encodeURI(username)); + + console.groupEnd(); +} + // Helper-Funktion für erfolgreiche Anmeldung function handleSuccessfulLogin(req, res, user, rememberMe = false) { // Rollen als JSON-Array parsen @@ -66,6 +100,12 @@ function registerAuthRoutes(app) { const { username, password, remember_me } = req.body; const rememberMe = remember_me === 'on' || remember_me === true; + // Debug-Logging: Empfangener Username vom Client + console.log('\n========== LOGIN REQUEST RECEIVED =========='); + logUsernameEncoding('Username received from client', username); + console.log('Request headers content-type:', req.headers['content-type']); + console.log('Request body keys:', Object.keys(req.body)); + // Prüfe ob LDAP aktiviert ist LDAPService.getConfig((err, ldapConfig) => { if (err) { @@ -73,11 +113,20 @@ function registerAuthRoutes(app) { } const isLDAPEnabled = ldapConfig && ldapConfig.enabled === 1; + console.log('LDAP enabled:', isLDAPEnabled); // Wenn LDAP aktiviert ist, authentifiziere gegen LDAP if (isLDAPEnabled) { + console.log('Starting LDAP authentication...'); LDAPService.authenticate(username, password, (authErr, authSuccess, ldapUserInfo) => { + console.log('\n========== LDAP AUTHENTICATION RESULT =========='); + console.log('authErr:', authErr ? authErr.message : null); + console.log('authSuccess:', authSuccess); + console.log('ldapUserInfo:', ldapUserInfo); + if (authErr || !authSuccess) { + console.log('LDAP authentication failed, trying local database fallback...'); + logUsernameEncoding('Username for DB fallback lookup', username); // LDAP-Authentifizierung fehlgeschlagen - prüfe lokale Datenbank als Fallback db.get('SELECT * FROM users WHERE username = ? COLLATE NOCASE', [username], (err, user) => { if (err || !user) { @@ -94,6 +143,10 @@ function registerAuthRoutes(app) { // LDAP-Authentifizierung erfolgreich - Benutzer anhand des kanonischen LDAP-Benutzernamens aus der DB holen // (Sync speichert den exakten LDAP-Wert, z. B. "geißlerj" oder "GeisslerJ") const dbLookupUsername = (ldapUserInfo && ldapUserInfo.username) ? ldapUserInfo.username : username; + console.log('LDAP authentication successful!'); + console.log('Original username:', username); + console.log('Canonical username from LDAP:', dbLookupUsername); + logUsernameEncoding('Canonical username for DB lookup', dbLookupUsername); db.get('SELECT * FROM users WHERE username = ? COLLATE NOCASE', [dbLookupUsername], (err, user) => { if (err || !user) { return res.render('login', { error: 'Benutzer nicht in der Datenbank gefunden. Bitte führen Sie eine LDAP-Synchronisation durch.' }); diff --git a/server.js b/server.js index 9f69dc0..fa44852 100644 --- a/server.js +++ b/server.js @@ -9,8 +9,18 @@ const app = express(); const PORT = 3333; // Middleware -app.use(bodyParser.urlencoded({ extended: true })); -app.use(bodyParser.json()); +// UTF-8 explizit für URL-encoded Daten setzen (wichtig für Sonderzeichen wie ß, ü, ö, ä) +app.use(bodyParser.urlencoded({ + extended: true, + type: 'application/x-www-form-urlencoded', + parameterLimit: 1000, + limit: '10mb' +})); +// Explizit UTF-8 für JSON +app.use(bodyParser.json({ + type: 'application/json', + limit: '10mb' +})); // Trust proxy für korrekte Client-IP-Erkennung (wichtig bei Proxies/Reverse Proxies) app.set('trust proxy', true); app.use(express.static('public')); diff --git a/services/ldap-service.js b/services/ldap-service.js index 243efb5..3dc1144 100644 --- a/services/ldap-service.js +++ b/services/ldap-service.js @@ -125,20 +125,35 @@ class LDAPService { * LDAP-Filter unterstützen UTF-8 direkt nach RFC 4515. * Nur die speziellen LDAP-Filter-Zeichen werden escaped. */ - static escapeLDAPFilter(value) { + static escapeLDAPFilter(value, debugLabel = '') { if (!value) return ''; // Stelle sicher, dass der Wert als String behandelt wird const str = String(value); + // Debug-Logging + if (debugLabel) { + console.log(`[escapeLDAPFilter ${debugLabel}] Input:`, str); + const utf8Bytes = Buffer.from(str, 'utf8'); + console.log(`[escapeLDAPFilter ${debugLabel}] UTF-8 Bytes:`, Array.from(utf8Bytes)); + } + // Escape nur die speziellen LDAP-Filter-Zeichen // UTF-8-Zeichen wie ß, ä, ö, ü werden direkt verwendet - return str + const escaped = str .replace(/\\/g, '\\5c') // Backslash .replace(/\*/g, '\\2a') // Stern .replace(/\(/g, '\\28') // Öffnende Klammer .replace(/\)/g, '\\29') // Schließende Klammer .replace(/\0/g, '\\00'); // Null-Byte + + if (debugLabel) { + console.log(`[escapeLDAPFilter ${debugLabel}] Output:`, escaped); + const escapedBytes = Buffer.from(escaped, 'utf8'); + console.log(`[escapeLDAPFilter ${debugLabel}] Escaped UTF-8 Bytes:`, Array.from(escapedBytes)); + } + + return escaped; } /** @@ -272,6 +287,22 @@ class LDAPService { // Stelle sicher, dass Username als String behandelt wird (UTF-8 wird korrekt unterstützt) const usernameStr = String(username || '').trim(); + console.log('\n========== LDAP AUTHENTICATE - START =========='); + console.log('[LDAP] Username input:', usernameStr); + const utf8Bytes = Buffer.from(usernameStr, 'utf8'); + console.log('[LDAP] Username UTF-8 Bytes:', Array.from(utf8Bytes)); + console.log('[LDAP] Username UTF-8 Bytes (Hex):', Array.from(utf8Bytes).map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ')); + console.log('[LDAP] Username length:', usernameStr.length); + + // Character-by-character analysis + console.log('[LDAP] Character analysis:'); + for (let i = 0; i < usernameStr.length; i++) { + const char = usernameStr[i]; + const codePoint = char.codePointAt(0); + const charBytes = Buffer.from(char, 'utf8'); + console.log(` [${i}] "${char}" | U+${codePoint.toString(16).toUpperCase().padStart(4, '0')} (${codePoint}) | Bytes: [${Array.from(charBytes).join(', ')}]`); + } + if (!usernameStr) { return callback(new Error('Benutzername darf nicht leer sein'), false); } @@ -282,24 +313,121 @@ class LDAPService { return callback(new Error('LDAP ist nicht aktiviert'), false); } + console.log('[LDAP] Config loaded - baseDN:', config.base_dn); + console.log('[LDAP] Config - usernameAttr:', config.username_attribute || 'sAMAccountName'); + // LDAP-Verbindung herstellen (mit Service-Account) this.connect(config, (err, client) => { if (err) { + console.error('[LDAP] Connection error:', err); return callback(err, false); } + console.log('[LDAP] Connected successfully'); + const baseDN = config.base_dn || ''; const usernameAttr = config.username_attribute || 'sAMAccountName'; - // OR-Filter mit mehreren Varianten (exakt, lowercase, ß/ss), damit Login trotz unterschiedlicher Schreibweise funktioniert - const variants = this.getUsernameSearchVariants(usernameStr); - const filterParts = variants.map(v => `(${usernameAttr}=${this.escapeLDAPFilter(v)})`); - const searchFilter = filterParts.length === 1 ? filterParts[0] : `(|${filterParts.join('')})`; - const searchOptions = { - filter: searchFilter, + + // Primär: Exakte Übereinstimmung mit sAMAccountName (wie in AD gespeichert) + // Varianten werden als Fallback verwendet, falls exakte Suche fehlschlägt + console.log('[LDAP] Using exact sAMAccountName match (as stored in AD)'); + const exactEscaped = this.escapeLDAPFilter(usernameStr, 'exact'); + const exactFilter = `(${usernameAttr}=${exactEscaped})`; + + console.log('[LDAP] Exact LDAP Search Filter:', exactFilter); + console.log('[LDAP] Exact Filter UTF-8 Bytes:', Array.from(Buffer.from(exactFilter, 'utf8'))); + + const exactSearchOptions = { + filter: exactFilter, scope: 'sub', attributes: ['dn', usernameAttr] }; + console.log('[LDAP] Starting LDAP search with exact match...'); + console.log('[LDAP] Search options:', JSON.stringify(exactSearchOptions, null, 2)); + + let userDN = null; + let canonicalUsername = null; + let searchAttempted = false; + + // Zuerst exakte Suche versuchen + client.search(baseDN, exactSearchOptions, (err, res) => { + if (err) { + console.error('[LDAP] Exact search error:', err); + // Bei Fehler: Varianten als Fallback versuchen + return this.tryVariantSearch(client, baseDN, usernameAttr, usernameStr, password, config, callback); + } + + searchAttempted = true; + + res.on('searchEntry', (entry) => { + userDN = entry.dn.toString(); + canonicalUsername = this.getAttributeValue(entry, usernameAttr) || usernameStr; + + console.log('[LDAP] Exact search entry found!'); + console.log('[LDAP] userDN:', userDN); + console.log('[LDAP] canonicalUsername:', canonicalUsername); + const canonicalBytes = Buffer.from(canonicalUsername, 'utf8'); + console.log('[LDAP] canonicalUsername UTF-8 Bytes:', Array.from(canonicalBytes)); + console.log('[LDAP] canonicalUsername UTF-8 Bytes (Hex):', Array.from(canonicalBytes).map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ')); + }); + + res.on('error', (err) => { + const errorMsg = err.message || String(err); + console.error('[LDAP] Exact search error:', errorMsg); + console.error('[LDAP] Error details:', err); + // Bei Fehler: Varianten als Fallback versuchen + if (!userDN) { + return this.tryVariantSearch(client, baseDN, usernameAttr, usernameStr, password, config, callback); + } + client.unbind(); + callback(new Error(`LDAP-Suchfehler: ${errorMsg}`), false); + }); + + res.on('end', (result) => { + console.log('[LDAP] Exact search ended. Status:', result ? result.status : 'unknown'); + console.log('[LDAP] userDN found:', userDN ? 'YES' : 'NO'); + + if (!userDN) { + console.log('[LDAP] Exact match not found, trying variants as fallback...'); + // Exakte Suche fehlgeschlagen, Varianten als Fallback versuchen + return this.tryVariantSearch(client, baseDN, usernameAttr, usernameStr, password, config, callback); + } + + // Exakte Suche erfolgreich - mit Bind fortfahren + this.performBind(config, userDN, password, canonicalUsername, callback); + }); + }); + }); + }); + } + + /** + * Varianten-Suche als Fallback (wenn exakte Suche fehlschlägt) + */ + static tryVariantSearch(client, baseDN, usernameAttr, usernameStr, password, config, callback) { + console.log('[LDAP] Starting variant search as fallback...'); + + // OR-Filter mit mehreren Varianten (lowercase, ß/ss), damit Login trotz unterschiedlicher Schreibweise funktioniert + const variants = this.getUsernameSearchVariants(usernameStr); + console.log('[LDAP] Username variants generated:', variants); + console.log('[LDAP] Number of variants:', variants.length); + + const filterParts = variants.map((v, idx) => { + const escaped = this.escapeLDAPFilter(v, `variant-${idx}`); + return `(${usernameAttr}=${escaped})`; + }); + const searchFilter = filterParts.length === 1 ? filterParts[0] : `(|${filterParts.join('')})`; + + console.log('[LDAP] Variant LDAP Search Filter:', searchFilter); + console.log('[LDAP] Variant Filter UTF-8 Bytes:', Array.from(Buffer.from(searchFilter, 'utf8'))); + + const searchOptions = { + filter: searchFilter, + scope: 'sub', + attributes: ['dn', usernameAttr] + }; + let userDN = null; let canonicalUsername = null; @@ -314,44 +442,74 @@ class LDAPService { userDN = entry.dn.toString(); // Kanonischen Benutzernamen aus LDAP verwenden (für DB-Lookup nach Sync) canonicalUsername = this.getAttributeValue(entry, usernameAttr) || usernameStr; + + console.log('[LDAP] Search entry found!'); + console.log('[LDAP] userDN:', userDN); + console.log('[LDAP] canonicalUsername:', canonicalUsername); + const canonicalBytes = Buffer.from(canonicalUsername, 'utf8'); + console.log('[LDAP] canonicalUsername UTF-8 Bytes:', Array.from(canonicalBytes)); + console.log('[LDAP] canonicalUsername UTF-8 Bytes (Hex):', Array.from(canonicalBytes).map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ')); }); res.on('error', (err) => { client.unbind(); const errorMsg = err.message || String(err); + console.error('[LDAP] Search error:', errorMsg); + console.error('[LDAP] Error details:', err); callback(new Error(`LDAP-Suchfehler: ${errorMsg}`), false); }); res.on('end', (result) => { client.unbind(); + console.log('[LDAP] Search ended. Status:', result ? result.status : 'unknown'); + console.log('[LDAP] userDN found:', userDN ? 'YES' : 'NO'); + console.log('[LDAP] canonicalUsername:', canonicalUsername); + if (!userDN) { + console.error('[LDAP] User not found in LDAP (even with variants)!'); + console.error('[LDAP] Searched for:', usernameStr); + console.error('[LDAP] Used filter:', searchFilter); return callback(new Error(`Benutzer "${usernameStr}" nicht gefunden. Hinweis: Prüfen Sie, ob der Benutzername korrekt ist und UTF-8-Zeichen (wie ß, ä, ö, ü) korrekt geschrieben sind.`), false); } - 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) { - const errorMsg = err.message || String(err); - return callback(new Error(`Ungültiges Passwort oder Authentifizierungsfehler: ${errorMsg}`), false); - } - // Erfolg: kanonischen Benutzernamen mitgeben, damit die DB-Lookup mit dem Sync-Benutzernamen funktioniert - callback(null, true, { username: canonicalUsername }); - }); + // Varianten-Suche erfolgreich - mit Bind fortfahren + this.performBind(config, userDN, password, canonicalUsername, callback); }); }); - }); + } + + /** + * LDAP Bind durchführen (Passwort-Authentifizierung) + */ + static performBind(config, userDN, password, canonicalUsername, callback) { + console.log('[LDAP] Attempting bind with userDN:', userDN); + + const authClient = ldap.createClient({ + url: config.url, + timeout: 10000, + connectTimeout: 10000 + }); + + authClient.on('error', (err) => { + authClient.unbind(); + console.error('[LDAP] Bind client error:', err); + callback(err, false); + }); + + authClient.bind(userDN, password, (err) => { + authClient.unbind(); + if (err) { + const errorMsg = err.message || String(err); + console.error('[LDAP] Bind failed:', errorMsg); + console.error('[LDAP] Bind error details:', err); + return callback(new Error(`Ungültiges Passwort oder Authentifizierungsfehler: ${errorMsg}`), false); + } + // Erfolg: kanonischen Benutzernamen mitgeben, damit die DB-Lookup mit dem Sync-Benutzernamen funktioniert + console.log('[LDAP] Bind successful!'); + console.log('[LDAP] Returning canonicalUsername:', canonicalUsername); + console.log('========== LDAP AUTHENTICATE - SUCCESS ==========\n'); + callback(null, true, { username: canonicalUsername }); }); } diff --git a/views/admin.ejs b/views/admin.ejs index fa00a63..b621f47 100644 --- a/views/admin.ejs +++ b/views/admin.ejs @@ -133,7 +133,7 @@ Rolle Personalnummer Wochenstunden - Arbeitstage + Arbeitstage pro Woche Urlaubstage Erstellt am Aktionen diff --git a/views/login.ejs b/views/login.ejs index 2621dc9..faf155f 100644 --- a/views/login.ejs +++ b/views/login.ejs @@ -17,7 +17,7 @@
<%= error %>
<% } %> -
+
@@ -40,6 +40,56 @@ + +