V1.0
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
---
|
||||
name: MySQL-Integration mit Admin-Konfiguration
|
||||
overview: Erweitere das System um MySQL-Unterstützung mit einer Admin-Oberfläche zur Konfiguration. Die Datenbank wird in einer Konfigurationsdatei gespeichert und kann im Admin-Bereich geändert werden (erfordert Server-Neustart).
|
||||
todos: []
|
||||
---
|
||||
|
||||
# MySQL/MariaDB-Integration mit Admin-Konfiguration
|
||||
|
||||
## Übersicht
|
||||
|
||||
Erweitere das System um MySQL/MariaDB-Unterstützung neben SQLite3. Die Datenbank-Konfiguration wird in einer Konfigurationsdatei gespeichert und kann im Admin-Bereich verwaltet werden. Ein Wechsel erfordert einen Server-Neustart.
|
||||
|
||||
**Hinweis:** MariaDB ist vollständig MySQL-kompatibel und funktioniert mit derselben Schnittstelle (`mysql2` Package).
|
||||
|
||||
## Architektur
|
||||
|
||||
### Datenbank-Abstraktionsschicht
|
||||
|
||||
Erstelle eine Abstraktionsschicht, die sowohl SQLite als auch MySQL unterstützt:
|
||||
|
||||
1. **Neue Datei:** `database/db-adapter.js`
|
||||
|
||||
- Abstrahiert `db.get()`, `db.run()`, `db.all()` für beide Datenbanken
|
||||
- Promise-basierte API (für bessere Kompatibilität)
|
||||
- Automatische Fehlerbehandlung
|
||||
- SQL-Dialekt-Anpassung (z.B. `AUTOINCREMENT` vs `AUTO_INCREMENT`)
|
||||
|
||||
2. **Neue Datei:** `database/sqlite-adapter.js`
|
||||
|
||||
- SQLite-spezifische Implementierung
|
||||
- Wrapper um sqlite3
|
||||
|
||||
3. **Neue Datei:** `database/mysql-adapter.js`
|
||||
|
||||
- MySQL/MariaDB-spezifische Implementierung
|
||||
- Verwendet `mysql2` Package (funktioniert mit MySQL und MariaDB)
|
||||
|
||||
4. **Anpassung:** `database.js`
|
||||
|
||||
- Lädt Konfiguration aus `config/database.json`
|
||||
- Initialisiert entsprechenden Adapter
|
||||
- Behält `initDatabase()` Funktion bei
|
||||
|
||||
### Konfigurationsdatei
|
||||
|
||||
**Neue Datei:** `config/database.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "sqlite",
|
||||
"sqlite": {
|
||||
"path": "./stundenerfassung.db"
|
||||
},
|
||||
"mysql": {
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"user": "root",
|
||||
"password": "",
|
||||
"database": "stundenerfassung",
|
||||
"charset": "utf8mb4"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Admin-Interface
|
||||
|
||||
**Anpassung:** `routes/admin.js`
|
||||
|
||||
- Neue Route `/admin/database/config` (GET/PUT)
|
||||
- Zeigt aktuelle Datenbank-Konfiguration
|
||||
- Formular zum Ändern der Datenbank-Einstellungen
|
||||
- Warnung bei Wechsel (Server-Neustart erforderlich)
|
||||
|
||||
**Anpassung:** `views/admin.ejs`
|
||||
|
||||
- Neuer Abschnitt "Datenbank-Konfiguration"
|
||||
- Formular für SQLite/MySQL/MariaDB-Auswahl
|
||||
- Eingabefelder für MySQL/MariaDB-Verbindungsdaten
|
||||
- Test-Verbindung Button
|
||||
- Warnung bei Änderungen
|
||||
- Hinweis: MariaDB funktioniert mit MySQL-Konfiguration
|
||||
|
||||
### SQL-Anpassungen
|
||||
|
||||
**Unterschiede zwischen SQLite und MySQL:**
|
||||
|
||||
- `INTEGER PRIMARY KEY AUTOINCREMENT` → `INT AUTO_INCREMENT PRIMARY KEY`
|
||||
- `TEXT` → `VARCHAR(255)` oder `TEXT`
|
||||
- `REAL` → `DOUBLE` oder `DECIMAL`
|
||||
- `TEXT` (unbegrenzt) → `TEXT` oder `LONGTEXT`
|
||||
- `DATETIME DEFAULT CURRENT_TIMESTAMP` → `DATETIME DEFAULT CURRENT_TIMESTAMP` (gleich)
|
||||
- `INSERT OR IGNORE` → `INSERT IGNORE`
|
||||
- `COLLATE NOCASE` → `COLLATE utf8mb4_general_ci` (für case-insensitive)
|
||||
- Foreign Keys müssen in MySQL explizit aktiviert werden
|
||||
|
||||
**Anpassung:** `database.js` - `initDatabase()`
|
||||
|
||||
- SQL-Generierung basierend auf Datenbanktyp
|
||||
- Separate CREATE TABLE Statements für SQLite und MySQL
|
||||
- Migrationen anpassen (ALTER TABLE Syntax)
|
||||
|
||||
### Package-Abhängigkeiten
|
||||
|
||||
**Anpassung:** `package.json`
|
||||
|
||||
- `mysql2` hinzufügen (bessere Promise-Unterstützung als `mysql`, funktioniert mit MySQL und MariaDB)
|
||||
|
||||
### Services und Routes
|
||||
|
||||
Alle Dateien, die `db` verwenden, müssen angepasst werden:
|
||||
|
||||
- `routes/*.js` (7 Dateien)
|
||||
- `services/*.js` (5 Dateien)
|
||||
- `checkin-server.js`
|
||||
|
||||
**Änderungen:**
|
||||
|
||||
- Promise-basierte Queries verwenden (statt Callbacks)
|
||||
- Oder: Callback-Wrapper in Adapter
|
||||
|
||||
### Initialisierung
|
||||
|
||||
**Anpassung:** `server.js`
|
||||
|
||||
- Prüft ob `config/database.json` existiert
|
||||
- Erstellt Standard-Konfiguration falls nicht vorhanden
|
||||
- Initialisiert Datenbank entsprechend Konfiguration
|
||||
|
||||
## Implementierungsschritte
|
||||
|
||||
1. **Datenbank-Abstraktionsschicht erstellen**
|
||||
|
||||
- `database/db-adapter.js` - Interface
|
||||
- `database/sqlite-adapter.js` - SQLite-Implementierung
|
||||
- `database/mysql-adapter.js` - MySQL-Implementierung
|
||||
|
||||
2. **Konfigurationssystem**
|
||||
|
||||
- `config/database.json` Template
|
||||
- Konfigurations-Loader in `database.js`
|
||||
|
||||
3. **SQL-Anpassungen**
|
||||
|
||||
- `initDatabase()` für beide Datenbanken anpassen
|
||||
- SQL-Dialekt-Unterschiede behandeln
|
||||
|
||||
4. **Admin-Interface**
|
||||
|
||||
- Route für Datenbank-Konfiguration
|
||||
- View mit Konfigurationsformular
|
||||
- Test-Verbindung Funktion
|
||||
|
||||
5. **Alle Queries anpassen**
|
||||
|
||||
- Callbacks zu Promises oder Callback-Wrapper
|
||||
- SQLite-spezifische Syntax entfernen (z.B. `COLLATE NOCASE`)
|
||||
|
||||
6. **Dokumentation**
|
||||
|
||||
- README.md aktualisieren
|
||||
- Konfigurationsanleitung
|
||||
|
||||
## Technische Details
|
||||
|
||||
- **MySQL/MariaDB-Package:** `mysql2` (bessere Promise-Unterstützung, funktioniert mit beiden)
|
||||
- **Kompatibilität:** MariaDB ist vollständig MySQL-kompatibel, verwendet dieselbe Konfiguration
|
||||
- **Fallback:** Bei Fehler in MySQL/MariaDB-Verbindung auf SQLite zurückfallen
|
||||
- **Validierung:** Test-Verbindung vor Speichern der Konfiguration
|
||||
- **Sicherheit:** MySQL/MariaDB-Passwörter verschlüsselt in Config speichern (optional)
|
||||
|
||||
## Dateien die angepasst werden müssen
|
||||
|
||||
- `database.js` → `database/` Ordner mit mehreren Dateien
|
||||
- `routes/admin.js` - Datenbank-Konfiguration
|
||||
- `views/admin.ejs` - Konfigurations-UI
|
||||
- `package.json` - mysql2 Dependency
|
||||
- `server.js` - Konfigurationspr
|
||||
@@ -18,3 +18,4 @@ Thumbs.db
|
||||
logs
|
||||
*.log
|
||||
dev
|
||||
backup/
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,6 @@
|
||||
#Backups
|
||||
backup/
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
|
||||
153
README.md
153
README.md
@@ -1,26 +1,47 @@
|
||||
# Stundenerfassungs-System
|
||||
|
||||
Eine webbasierte Anwendung zur Erfassung von Arbeitszeiten mit Admin-Bereich und PDF-Export.
|
||||
Eine webbasierte Anwendung zur Erfassung von Arbeitszeiten mit Admin-Bereich, PDF-Export, Überstunden-Verwaltung und LDAP-Integration.
|
||||
|
||||
## Features
|
||||
|
||||
### Für Mitarbeiter
|
||||
- ✅ Login mit Benutzername und Passwort
|
||||
- ✅ Login mit Benutzername und Passwort oder LDAP
|
||||
- ✅ Wöchentliche Stundenerfassung (Montag - Sonntag)
|
||||
- ✅ Automatisches Speichern der Einträge
|
||||
- ✅ Eingabe von Start-/Endzeit, Pausen und Notizen
|
||||
- ✅ Mehrere Tätigkeiten pro Tag mit Projektnummern (bis zu 5)
|
||||
- ✅ Automatische Berechnung der Gesamtstunden
|
||||
- ✅ Überstunden-Verwaltung (Anzeige und Verbrauch)
|
||||
- ✅ Urlaubsverwaltung (ganzer/halber Tag)
|
||||
- ✅ Krankheitstage erfassen
|
||||
- ✅ Feiertage werden automatisch erkannt
|
||||
- ✅ Wochenend-Prozentsätze (konfigurierbar)
|
||||
- ✅ Wöchentliches Abschicken des Stundenzettels
|
||||
- ✅ Überstunden-Auswertung pro Woche mit detaillierter Aufschlüsselung
|
||||
- ✅ Anzeige aktueller Überstunden und verbleibender Urlaubstage
|
||||
- ✅ Automatische Zeiterfassung via IP-Ping (optional)
|
||||
- ✅ QR-Code für Check-in/Check-out
|
||||
|
||||
### Für Administratoren
|
||||
- ✅ Benutzerverwaltung (Anlegen, Löschen)
|
||||
- ✅ Benutzerverwaltung (Anlegen, Löschen, Bearbeiten)
|
||||
- ✅ Rollenvergabe (Mitarbeiter, Verwaltung, Admin)
|
||||
- ✅ Übersicht aller Benutzer
|
||||
- ✅ Mehrere Rollen pro Benutzer möglich
|
||||
- ✅ Übersicht aller Benutzer mit Statistiken
|
||||
- ✅ Personalnummern verwalten
|
||||
- ✅ Wochenstunden und Urlaubstage pro Mitarbeiter konfigurieren
|
||||
- ✅ LDAP-Integration für Benutzer-Synchronisation
|
||||
- ✅ LDAP-Konfiguration und manuelle/automatische Synchronisation
|
||||
- ✅ System-Optionen (Wochenend-Prozentsätze)
|
||||
|
||||
### Für Verwaltung
|
||||
- ✅ Postfach mit eingereichten Stundenzetteln
|
||||
- ✅ PDF-Generierung und Download
|
||||
- ✅ Übersichtliche Darstellung aller Einreichungen
|
||||
- ✅ Wochenansicht mit Statistiken pro Mitarbeiter
|
||||
- ✅ Überstunden-Offset (manuelle Korrekturen)
|
||||
- ✅ Admin-Kommentare zu Stundenzetteln
|
||||
- ✅ Versionsverwaltung bei Änderungen
|
||||
- ✅ Übersicht über Überstunden und Urlaubstage aller Mitarbeiter
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -47,6 +68,14 @@ Für Entwicklung mit automatischem Neustart:
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Docker (optional)
|
||||
|
||||
Das System kann auch mit Docker betrieben werden:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Standard-Zugangsdaten
|
||||
|
||||
Nach der Installation sind folgende Benutzer verfügbar:
|
||||
@@ -54,12 +83,12 @@ Nach der Installation sind folgende Benutzer verfügbar:
|
||||
### Administrator
|
||||
- **Benutzername:** admin
|
||||
- **Passwort:** admin123
|
||||
- **Funktion:** Kann Benutzer anlegen und verwalten
|
||||
- **Funktion:** Kann Benutzer anlegen und verwalten, LDAP konfigurieren
|
||||
|
||||
### Verwaltung
|
||||
- **Benutzername:** verwaltung
|
||||
- **Passwort:** verwaltung123
|
||||
- **Funktion:** Kann eingereichte Stundenzettel einsehen und PDFs erstellen
|
||||
- **Funktion:** Kann eingereichte Stundenzettel einsehen, PDFs erstellen und Überstunden korrigieren
|
||||
|
||||
**WICHTIG:** Bitte ändern Sie diese Passwörter nach der ersten Anmeldung!
|
||||
|
||||
@@ -73,10 +102,15 @@ Nach der Installation sind folgende Benutzer verfügbar:
|
||||
- **Start:** Arbeitsbeginn
|
||||
- **Ende:** Arbeitsende
|
||||
- **Pause:** Pausenzeit in Minuten
|
||||
- **Tätigkeiten:** Bis zu 5 Tätigkeiten mit Beschreibung, Stunden und Projektnummer
|
||||
- **Notizen:** Optional, z.B. Projekt oder Tätigkeit
|
||||
- **Überstunden:** Optional, wenn Überstunden verbraucht werden sollen
|
||||
- **Urlaub:** Ganzer oder halber Tag Urlaub
|
||||
- **Krank:** Krankheitstag markieren
|
||||
4. Die Einträge werden automatisch gespeichert
|
||||
5. Am Ende der Woche: Klicken Sie auf **"Woche abschicken"**
|
||||
6. Nach dem Abschicken können keine Änderungen mehr vorgenommen werden
|
||||
7. **Überstunden-Auswertung:** Klicken Sie auf "Details anzeigen" im Bereich "Aktuelle Überstunden" für eine wöchentliche Aufschlüsselung
|
||||
|
||||
### Für Administratoren
|
||||
|
||||
@@ -84,11 +118,16 @@ Nach der Installation sind folgende Benutzer verfügbar:
|
||||
2. Sie gelangen automatisch zur Benutzerverwaltung
|
||||
3. **Neuen Benutzer anlegen:**
|
||||
- Füllen Sie das Formular aus
|
||||
- Wählen Sie die passende Rolle
|
||||
- Wählen Sie die passende Rolle (mehrere möglich)
|
||||
- Personalnummer, Wochenstunden und Urlaubstage konfigurieren
|
||||
- Klicken Sie auf "Benutzer anlegen"
|
||||
4. **Benutzer löschen:**
|
||||
- Klicken Sie auf "Löschen" neben dem gewünschten Benutzer
|
||||
- System-Benutzer (Admin, Verwaltung) können nicht gelöscht werden
|
||||
5. **LDAP konfigurieren:**
|
||||
- Navigieren Sie zum LDAP-Bereich
|
||||
- Konfigurieren Sie LDAP-Verbindung und Synchronisation
|
||||
- Führen Sie manuelle Synchronisationen durch oder aktivieren Sie automatische Syncs
|
||||
|
||||
### Für Verwaltung
|
||||
|
||||
@@ -97,10 +136,16 @@ Nach der Installation sind folgende Benutzer verfügbar:
|
||||
3. **PDF erstellen:**
|
||||
- Klicken Sie auf "PDF herunterladen" neben dem gewünschten Stundenzettel
|
||||
- Die PDF wird automatisch generiert und heruntergeladen
|
||||
4. Die PDF enthält:
|
||||
- Mitarbeitername
|
||||
4. **Überstunden korrigieren:**
|
||||
- In der Wochenansicht können Sie manuelle Korrekturen (Offset) für jeden Mitarbeiter vornehmen
|
||||
5. **Kommentare hinzufügen:**
|
||||
- Fügen Sie Admin-Kommentare zu Stundenzetteln hinzu
|
||||
6. Die PDF enthält:
|
||||
- Mitarbeitername und Personalnummer
|
||||
- Zeitraum
|
||||
- Alle Tageseinträge mit Start, Ende, Pause, Stunden und Notizen
|
||||
- Alle Tageseinträge mit Start, Ende, Pause, Stunden, Tätigkeiten und Notizen
|
||||
- Überstunden-Verbrauch
|
||||
- Urlaubstage
|
||||
- Gesamtstundensumme
|
||||
|
||||
## Technologie-Stack
|
||||
@@ -110,20 +155,101 @@ Nach der Installation sind folgende Benutzer verfügbar:
|
||||
- **Template Engine:** EJS
|
||||
- **PDF-Generierung:** PDFKit
|
||||
- **Authentifizierung:** bcryptjs + express-session
|
||||
- **LDAP:** ldapjs
|
||||
- **QR-Codes:** qrcode
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```
|
||||
stundenerfassung/
|
||||
├── routes/ # API-Routen
|
||||
│ ├── admin.js # Admin-Funktionen
|
||||
│ ├── admin-ldap.js # LDAP-Verwaltung
|
||||
│ ├── auth.js # Authentifizierung
|
||||
│ ├── dashboard.js # Dashboard-Routen
|
||||
│ ├── timesheet.js # Stundenerfassung
|
||||
│ ├── user.js # Benutzer-APIs
|
||||
│ └── verwaltung.js # Verwaltungs-Funktionen
|
||||
├── services/ # Services
|
||||
│ ├── feiertage-service.js # Feiertags-API
|
||||
│ ├── ldap-service.js # LDAP-Service
|
||||
│ ├── ldap-scheduler.js # LDAP-Synchronisation
|
||||
│ ├── pdf-service.js # PDF-Generierung
|
||||
│ └── ping-service.js # IP-basierte Zeiterfassung
|
||||
├── views/ # EJS-Templates
|
||||
├── public/ # Statische Dateien
|
||||
│ ├── css/
|
||||
│ ├── js/
|
||||
│ └── images/
|
||||
└── helpers/ # Helper-Funktionen
|
||||
```
|
||||
|
||||
## Datenbankstruktur
|
||||
|
||||
### Tabelle: users
|
||||
- Speichert Benutzerinformationen und Zugangsdaten
|
||||
- Passwörter werden verschlüsselt gespeichert
|
||||
- Unterstützt mehrere Rollen pro Benutzer
|
||||
- Personalnummer, Wochenstunden, Urlaubstage, Überstunden-Offset
|
||||
|
||||
### Tabelle: timesheet_entries
|
||||
- Speichert einzelne Tageseinträge
|
||||
- Automatische Berechnung der Gesamtstunden
|
||||
- Unterstützt mehrere Tätigkeiten pro Tag
|
||||
- Überstunden-Verbrauch, Urlaub, Krankheit
|
||||
|
||||
### Tabelle: weekly_timesheets
|
||||
- Speichert eingereichte Wochenstundenzettel
|
||||
- Verknüpfung mit Benutzer und Status
|
||||
- Versionsverwaltung und Admin-Kommentare
|
||||
|
||||
### Tabelle: system_options
|
||||
- Systemweite Einstellungen
|
||||
- Wochenend-Prozentsätze (Samstag/Sonntag)
|
||||
|
||||
### Tabelle: public_holidays
|
||||
- Öffentliche Feiertage (Baden-Württemberg)
|
||||
- Automatische Aktualisierung
|
||||
|
||||
### Tabelle: ldap_config
|
||||
- LDAP-Konfiguration
|
||||
- Synchronisationseinstellungen
|
||||
|
||||
### Tabelle: ping_status
|
||||
- IP-basierte Zeiterfassung
|
||||
- Ping-Status pro Benutzer und Tag
|
||||
|
||||
## Features im Detail
|
||||
|
||||
### Überstunden-Verwaltung
|
||||
- Automatische Berechnung basierend auf Sollstunden
|
||||
- Berücksichtigung von Wochenend-Prozentsätzen
|
||||
- Feiertagsstunden werden automatisch hinzugefügt
|
||||
- Urlaubsstunden werden in die Berechnung einbezogen
|
||||
- Manuelle Korrekturen durch Verwaltung möglich
|
||||
- Detaillierte wöchentliche Auswertung für Mitarbeiter
|
||||
|
||||
### Urlaubsverwaltung
|
||||
- Ganzer oder halber Tag Urlaub
|
||||
- Verbleibende Urlaubstage werden angezeigt
|
||||
- Verplante Urlaubstage werden erfasst
|
||||
- Urlaubstage werden in Überstunden-Berechnung berücksichtigt
|
||||
|
||||
### Wochenend-Prozentsätze
|
||||
- Konfigurierbare Prozentsätze für Samstag und Sonntag
|
||||
- Werden automatisch auf gearbeitete Stunden angewendet
|
||||
- Standard: 100% (keine Zuschläge)
|
||||
|
||||
### Feiertage
|
||||
- Automatische Erkennung öffentlicher Feiertage (Baden-Württemberg)
|
||||
- Feiertagsstunden werden automatisch berechnet
|
||||
- Arbeit an Feiertagen zählt als Überstunden
|
||||
|
||||
### LDAP-Integration
|
||||
- Automatische Benutzer-Synchronisation
|
||||
- Konfigurierbare Synchronisationsintervalle
|
||||
- Manuelle Synchronisation möglich
|
||||
- LDAP-Authentifizierung als Alternative zu lokalen Passwörtern
|
||||
|
||||
## Sicherheit
|
||||
|
||||
@@ -131,6 +257,8 @@ Nach der Installation sind folgende Benutzer verfügbar:
|
||||
- ✅ Session-basierte Authentifizierung
|
||||
- ✅ Rollenbasierte Zugriffskontrolle
|
||||
- ✅ CSRF-Schutz durch Sessions
|
||||
- ✅ LDAP-Integration für zentrale Authentifizierung
|
||||
- ✅ SQL-Injection-Schutz durch parametrisierte Queries
|
||||
|
||||
## Anpassungen
|
||||
|
||||
@@ -142,6 +270,11 @@ const PORT = 3333; // Ändern Sie hier den Port
|
||||
|
||||
### Datenbank-Speicherort
|
||||
Die Datenbank wird standardmäßig als `stundenerfassung.db` im Projektverzeichnis gespeichert.
|
||||
Über die Umgebungsvariable `DB_PATH` kann ein anderer Pfad angegeben werden.
|
||||
|
||||
### Wochenend-Prozentsätze
|
||||
Im Admin-Bereich können die Prozentsätze für Samstag und Sonntag konfiguriert werden.
|
||||
Standard: 100% (keine Zuschläge).
|
||||
|
||||
## Lizenz
|
||||
|
||||
|
||||
10
database.js
10
database.js
@@ -223,7 +223,7 @@ function initDatabase() {
|
||||
bind_password TEXT,
|
||||
base_dn TEXT,
|
||||
user_search_filter TEXT,
|
||||
username_attribute TEXT DEFAULT 'cn',
|
||||
username_attribute TEXT DEFAULT 'sAMAccountName',
|
||||
firstname_attribute TEXT DEFAULT 'givenName',
|
||||
lastname_attribute TEXT DEFAULT 'sn',
|
||||
sync_interval INTEGER DEFAULT 0,
|
||||
@@ -265,6 +265,14 @@ function initDatabase() {
|
||||
}
|
||||
});
|
||||
|
||||
// Migration: checkin_root_url Spalte hinzufügen
|
||||
db.run(`ALTER TABLE system_options ADD COLUMN checkin_root_url TEXT`, (err) => {
|
||||
// Fehler ignorieren wenn Spalte bereits existiert
|
||||
if (err && !err.message.includes('duplicate column')) {
|
||||
console.warn('Warnung beim Hinzufügen der Spalte checkin_root_url:', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Migration: Bestehende Rollen zu JSON-Arrays konvertieren
|
||||
// Prüfe ob Rollen noch als einfache Strings gespeichert sind (nicht als JSON-Array)
|
||||
db.all('SELECT id, role FROM users', (err, users) => {
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
version: "3"
|
||||
|
||||
volumes:
|
||||
lldap_data:
|
||||
driver: local
|
||||
|
||||
services:
|
||||
lldap:
|
||||
image: lldap/lldap:stable
|
||||
ports:
|
||||
# For LDAP, not recommended to expose, see Usage section.
|
||||
- "3890:3890"
|
||||
# For LDAPS (LDAP Over SSL), enable port if LLDAP_LDAPS_OPTIONS__ENABLED set true, look env below
|
||||
#- "6360:6360"
|
||||
# For the web front-end
|
||||
- "17170:17170"
|
||||
volumes:
|
||||
- "lldap_data:/data"
|
||||
# Alternatively, you can mount a local folder
|
||||
# - "./lldap_data:/data"
|
||||
environment:
|
||||
- UID=1000
|
||||
- GID=1000
|
||||
- TZ=Europe/Berlin
|
||||
- LLDAP_JWT_SECRET=1omV4UDprT1PAJFYXGisVlei/V5b5Uaiqssl9qburwV+T1S0ox8iurI6FJkWPnX5xRUMbHswJwZLG7QzUnzdZw==
|
||||
- LLDAP_KEY_SEED=ffcomviFeT8RByJf7jT3AuzDcFbrgWb+oSuMDp72pql96J4Rdq5gno2Dk1xfWrYPLH5OoS/bpDuOp/oE9T5+sA==
|
||||
- LLDAP_LDAP_BASE_DN=dc=gmbh,dc=de
|
||||
- LLDAP_LDAP_USER_PASS=Delfine1!!! # If the password contains '$', escape it (e.g. Pas$$word sets Pas$word)
|
||||
# If using LDAPS, set enabled true and configure cert and key path
|
||||
# - LLDAP_LDAPS_OPTIONS__ENABLED=true
|
||||
# - LLDAP_LDAPS_OPTIONS__CERT_FILE=/path/to/certfile.crt
|
||||
# - LLDAP_LDAPS_OPTIONS__KEY_FILE=/path/to/keyfile.key
|
||||
# You can also set a different database:
|
||||
# - LLDAP_DATABASE_URL=mysql://mysql-user:password@mysql-server/my-database
|
||||
# - LLDAP_DATABASE_URL=postgres://postgres-user:password@postgres-server/my-database
|
||||
# If using SMTP, set the following variables
|
||||
# - LLDAP_SMTP_OPTIONS__ENABLE_PASSWORD_RESET=true
|
||||
# - LLDAP_SMTP_OPTIONS__SERVER=smtp.example.com
|
||||
# - LLDAP_SMTP_OPTIONS__PORT=465 # Check your smtp provider's documentation for this setting
|
||||
# - LLDAP_SMTP_OPTIONS__SMTP_ENCRYPTION=TLS # How the connection is encrypted, either "NONE" (no encryption, port 25), "TLS" (sometimes called SSL, port 465) or "STARTTLS" (sometimes called TLS, port 587).
|
||||
# - LLDAP_SMTP_OPTIONS__USER=no-reply@example.com # The SMTP user, usually your email address
|
||||
# - LLDAP_SMTP_OPTIONS__PASSWORD=PasswordGoesHere # The SMTP password
|
||||
# - LLDAP_SMTP_OPTIONS__FROM=no-reply <no-reply@example.com> # The header field, optional: how the sender appears in the email. The first is a free-form name, followed by an email between <>.
|
||||
# - LLDAP_SMTP_OPTIONS__TO=admin <admin@example.com> # Same for reply-to, optional.
|
||||
@@ -65,7 +65,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
const formData = {
|
||||
saturday_percentage: document.getElementById('saturdayPercentage').value,
|
||||
sunday_percentage: document.getElementById('sundayPercentage').value
|
||||
sunday_percentage: document.getElementById('sundayPercentage').value,
|
||||
checkin_root_url: document.getElementById('checkinRootUrl') ? document.getElementById('checkinRootUrl').value : null
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -213,6 +214,9 @@ async function loadOptions() {
|
||||
if (document.getElementById('sundayPercentage')) {
|
||||
document.getElementById('sundayPercentage').value = config.sunday_percentage || 0;
|
||||
}
|
||||
if (document.getElementById('checkinRootUrl')) {
|
||||
document.getElementById('checkinRootUrl').value = config.checkin_root_url || '';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Optionen:', error);
|
||||
@@ -244,7 +248,7 @@ async function loadLDAPConfig() {
|
||||
document.getElementById('ldapSearchFilter').value = config.user_search_filter || '(objectClass=person)';
|
||||
}
|
||||
if (document.getElementById('ldapUsernameAttr')) {
|
||||
document.getElementById('ldapUsernameAttr').value = config.username_attribute || 'cn';
|
||||
document.getElementById('ldapUsernameAttr').value = config.username_attribute || 'sAMAccountName';
|
||||
}
|
||||
if (document.getElementById('ldapFirstnameAttr')) {
|
||||
document.getElementById('ldapFirstnameAttr').value = config.firstname_attribute || 'givenName';
|
||||
|
||||
@@ -307,7 +307,7 @@ resetDatabase().catch((error) => {
|
||||
bind_password TEXT,
|
||||
base_dn TEXT,
|
||||
user_search_filter TEXT,
|
||||
username_attribute TEXT DEFAULT 'cn',
|
||||
username_attribute TEXT DEFAULT 'sAMAccountName',
|
||||
firstname_attribute TEXT DEFAULT 'givenName',
|
||||
lastname_attribute TEXT DEFAULT 'sn',
|
||||
sync_interval INTEGER DEFAULT 0,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// LDAP Admin Routes
|
||||
|
||||
const { db } = require('../database');
|
||||
const LDAPService = require('../ldap-service');
|
||||
const LDAPService = require('../services/ldap-service');
|
||||
const { requireAdmin } = require('../middleware/auth');
|
||||
|
||||
// Routes registrieren
|
||||
@@ -55,7 +55,7 @@ function registerAdminLDAPRoutes(app) {
|
||||
bind_password: bind_password ? bind_password.trim() : null,
|
||||
base_dn: base_dn.trim(),
|
||||
user_search_filter: user_search_filter ? user_search_filter.trim() : '(objectClass=person)',
|
||||
username_attribute: username_attribute ? username_attribute.trim() : 'cn',
|
||||
username_attribute: username_attribute ? username_attribute.trim() : 'sAMAccountName',
|
||||
firstname_attribute: firstname_attribute ? firstname_attribute.trim() : 'givenName',
|
||||
lastname_attribute: lastname_attribute ? lastname_attribute.trim() : 'sn',
|
||||
sync_interval: parseInt(sync_interval) || 0,
|
||||
|
||||
@@ -164,7 +164,8 @@ function registerAdminRoutes(app) {
|
||||
return res.json({
|
||||
config: {
|
||||
saturday_percentage: 100,
|
||||
sunday_percentage: 100
|
||||
sunday_percentage: 100,
|
||||
checkin_root_url: null
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -174,7 +175,7 @@ function registerAdminRoutes(app) {
|
||||
|
||||
// Optionen speichern
|
||||
app.post('/admin/options', requireAdmin, (req, res) => {
|
||||
const { saturday_percentage, sunday_percentage } = req.body;
|
||||
const { saturday_percentage, sunday_percentage, checkin_root_url } = req.body;
|
||||
|
||||
// Validierung
|
||||
const satPercent = parseFloat(saturday_percentage);
|
||||
@@ -188,6 +189,12 @@ function registerAdminRoutes(app) {
|
||||
return res.status(400).json({ error: 'Prozentsätze müssen zwischen 100 und 200 liegen' });
|
||||
}
|
||||
|
||||
// Validierung der Root URL (optional, kann leer sein)
|
||||
let rootUrl = checkin_root_url ? checkin_root_url.trim() : null;
|
||||
if (rootUrl === '') {
|
||||
rootUrl = null;
|
||||
}
|
||||
|
||||
// Prüfe ob Eintrag existiert
|
||||
db.get('SELECT id FROM system_options WHERE id = 1', (err, existing) => {
|
||||
if (err) {
|
||||
@@ -196,8 +203,8 @@ function registerAdminRoutes(app) {
|
||||
|
||||
if (existing) {
|
||||
// Update
|
||||
db.run('UPDATE system_options SET saturday_percentage = ?, sunday_percentage = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1',
|
||||
[satPercent, sunPercent],
|
||||
db.run('UPDATE system_options SET saturday_percentage = ?, sunday_percentage = ?, checkin_root_url = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1',
|
||||
[satPercent, sunPercent, rootUrl],
|
||||
(err) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Fehler beim Speichern der Optionen' });
|
||||
@@ -206,8 +213,8 @@ function registerAdminRoutes(app) {
|
||||
});
|
||||
} else {
|
||||
// Insert
|
||||
db.run('INSERT INTO system_options (id, saturday_percentage, sunday_percentage) VALUES (1, ?, ?)',
|
||||
[satPercent, sunPercent],
|
||||
db.run('INSERT INTO system_options (id, saturday_percentage, sunday_percentage, checkin_root_url) VALUES (1, ?, ?, ?)',
|
||||
[satPercent, sunPercent, rootUrl],
|
||||
(err) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Fehler beim Speichern der Optionen' });
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { db } = require('../database');
|
||||
const LDAPService = require('../ldap-service');
|
||||
const LDAPService = require('../services/ldap-service');
|
||||
const { getDefaultRole } = require('../helpers/utils');
|
||||
|
||||
// Helper-Funktion für erfolgreiche Anmeldung
|
||||
@@ -76,16 +76,14 @@ function registerAuthRoutes(app) {
|
||||
|
||||
// Wenn LDAP aktiviert ist, authentifiziere gegen LDAP
|
||||
if (isLDAPEnabled) {
|
||||
LDAPService.authenticate(username, password, (authErr, authSuccess) => {
|
||||
LDAPService.authenticate(username, password, (authErr, authSuccess, ldapUserInfo) => {
|
||||
if (authErr || !authSuccess) {
|
||||
// LDAP-Authentifizierung fehlgeschlagen - prüfe lokale Datenbank als Fallback
|
||||
// Case-insensitive Suche: COLLATE NOCASE macht den Vergleich case-insensitive
|
||||
db.get('SELECT * FROM users WHERE username = ? COLLATE NOCASE', [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, rememberMe);
|
||||
} else {
|
||||
@@ -93,9 +91,10 @@ function registerAuthRoutes(app) {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// LDAP-Authentifizierung erfolgreich - hole Benutzer aus Datenbank
|
||||
// Case-insensitive Suche: COLLATE NOCASE macht den Vergleich case-insensitive
|
||||
db.get('SELECT * FROM users WHERE username = ? COLLATE NOCASE', [username], (err, user) => {
|
||||
// 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;
|
||||
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.' });
|
||||
}
|
||||
|
||||
@@ -3,15 +3,36 @@
|
||||
const { hasRole } = require('../helpers/utils');
|
||||
const { requireAuth } = require('../middleware/auth');
|
||||
const { generateCheckinCheckoutQRPDF } = require('../services/pdf-service');
|
||||
const { db } = require('../database');
|
||||
|
||||
// Routes registrieren
|
||||
function registerDashboardRoutes(app) {
|
||||
// Check-in Root URL abrufen (öffentlich zugänglich für Konfiguration)
|
||||
app.get('/api/checkin-root-url', (req, res) => {
|
||||
db.get('SELECT checkin_root_url FROM system_options WHERE id = 1', (err, options) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Fehler beim Laden der Root URL' });
|
||||
}
|
||||
res.json({
|
||||
root_url: options && options.checkin_root_url ? options.checkin_root_url : null
|
||||
});
|
||||
});
|
||||
});
|
||||
// QR-Code-PDF (Check-in/Check-out) – nur für eingeloggte Nutzer mit Mitarbeiter-Rolle
|
||||
app.get('/api/dashboard/qr-pdf', requireAuth, (req, res) => {
|
||||
// Interne URLs
|
||||
app.get('/api/dashboard/qr-pdf/internal', requireAuth, (req, res) => {
|
||||
if (!hasRole(req, 'mitarbeiter')) {
|
||||
return res.status(403).send('Zugriff verweigert');
|
||||
}
|
||||
generateCheckinCheckoutQRPDF(req, res);
|
||||
generateCheckinCheckoutQRPDF(req, res, 'internal');
|
||||
});
|
||||
|
||||
// Externe URLs
|
||||
app.get('/api/dashboard/qr-pdf/external', requireAuth, (req, res) => {
|
||||
if (!hasRole(req, 'mitarbeiter')) {
|
||||
return res.status(403).send('Zugriff verweigert');
|
||||
}
|
||||
generateCheckinCheckoutQRPDF(req, res, 'external');
|
||||
});
|
||||
|
||||
// Dashboard für Mitarbeiter
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// LDAP-Scheduler Service
|
||||
|
||||
const { db } = require('../database');
|
||||
const LDAPService = require('../ldap-service');
|
||||
const LDAPService = require('./ldap-service');
|
||||
|
||||
// Automatische LDAP-Synchronisation einrichten
|
||||
function setupLDAPScheduler() {
|
||||
|
||||
@@ -1,402 +1,417 @@
|
||||
const ldap = require('ldapjs');
|
||||
const { db } = require('./database');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
/**
|
||||
* LDAP-Service für Benutzer-Synchronisation
|
||||
*/
|
||||
class LDAPService {
|
||||
/**
|
||||
* LDAP-Konfiguration aus der Datenbank abrufen
|
||||
*/
|
||||
static getConfig(callback) {
|
||||
db.get('SELECT * FROM ldap_config WHERE id = 1', (err, config) => {
|
||||
if (err) {
|
||||
return callback(err, null);
|
||||
}
|
||||
callback(null, config);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* LDAP-Verbindung herstellen
|
||||
*/
|
||||
static connect(config, callback) {
|
||||
if (!config || !config.enabled || !config.url) {
|
||||
return callback(new Error('LDAP ist nicht konfiguriert oder deaktiviert'));
|
||||
}
|
||||
|
||||
const client = ldap.createClient({
|
||||
url: config.url,
|
||||
timeout: 10000,
|
||||
connectTimeout: 10000
|
||||
});
|
||||
|
||||
// Fehlerbehandlung
|
||||
client.on('error', (err) => {
|
||||
callback(err, null);
|
||||
});
|
||||
|
||||
// Bind mit Credentials
|
||||
const bindDN = config.bind_dn || '';
|
||||
const bindPassword = config.bind_password || '';
|
||||
|
||||
// Hinweis: Passwort wird im Klartext gespeichert
|
||||
// In einer produktiven Umgebung sollte man eine Verschlüsselung mit einem Master-Key verwenden
|
||||
|
||||
client.bind(bindDN, bindPassword, (err) => {
|
||||
if (err) {
|
||||
client.unbind();
|
||||
return callback(err, null);
|
||||
}
|
||||
callback(null, client);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Benutzer aus LDAP abrufen
|
||||
*/
|
||||
static searchUsers(client, config, callback) {
|
||||
const baseDN = config.base_dn || '';
|
||||
const searchFilter = config.user_search_filter || '(objectClass=person)';
|
||||
const searchOptions = {
|
||||
filter: searchFilter,
|
||||
scope: 'sub',
|
||||
attributes: [
|
||||
config.username_attribute || 'cn',
|
||||
config.firstname_attribute || 'givenName',
|
||||
config.lastname_attribute || 'sn'
|
||||
]
|
||||
};
|
||||
|
||||
const users = [];
|
||||
|
||||
client.search(baseDN, searchOptions, (err, res) => {
|
||||
if (err) {
|
||||
return callback(err, null);
|
||||
}
|
||||
|
||||
res.on('searchEntry', (entry) => {
|
||||
const user = {
|
||||
username: this.getAttributeValue(entry, config.username_attribute || 'cn'),
|
||||
firstname: this.getAttributeValue(entry, config.firstname_attribute || 'givenName'),
|
||||
lastname: this.getAttributeValue(entry, config.lastname_attribute || 'sn')
|
||||
};
|
||||
|
||||
// Nur Benutzer mit allen erforderlichen Feldern hinzufügen
|
||||
if (user.username && user.firstname && user.lastname) {
|
||||
users.push(user);
|
||||
}
|
||||
});
|
||||
|
||||
res.on('error', (err) => {
|
||||
callback(err, null);
|
||||
});
|
||||
|
||||
res.on('end', (result) => {
|
||||
if (result && result.status !== 0) {
|
||||
return callback(new Error(`LDAP-Suche fehlgeschlagen: ${result.status}`), null);
|
||||
}
|
||||
callback(null, users);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wert eines LDAP-Attributs extrahieren
|
||||
*
|
||||
* Die ldapjs-Bibliothek behandelt UTF-8-Zeichen automatisch korrekt.
|
||||
* Diese Funktion stellt sicher, dass UTF-8-Zeichen wie ß, ä, ö, ü korrekt zurückgegeben werden.
|
||||
*/
|
||||
static getAttributeValue(entry, attributeName) {
|
||||
const attr = entry.attributes.find(a => a.type === attributeName);
|
||||
if (!attr) {
|
||||
return null;
|
||||
}
|
||||
const value = Array.isArray(attr.values) ? attr.values[0] : attr.values;
|
||||
// Stelle sicher, dass der Wert als String zurückgegeben wird (UTF-8 wird automatisch korrekt behandelt)
|
||||
return value != null ? String(value) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escaped einen Wert für LDAP-Filter (verhindert LDAP-Injection)
|
||||
*
|
||||
* WICHTIG: UTF-8-Zeichen wie ß, ä, ö, ü müssen NICHT escaped werden.
|
||||
* LDAP-Filter unterstützen UTF-8 direkt nach RFC 4515.
|
||||
* Nur die speziellen LDAP-Filter-Zeichen werden escaped.
|
||||
*/
|
||||
static escapeLDAPFilter(value) {
|
||||
if (!value) return '';
|
||||
|
||||
// Stelle sicher, dass der Wert als String behandelt wird
|
||||
const str = String(value);
|
||||
|
||||
// Escape nur die speziellen LDAP-Filter-Zeichen
|
||||
// UTF-8-Zeichen wie ß, ä, ö, ü werden direkt verwendet
|
||||
return 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Benutzer in SQLite synchronisieren
|
||||
*/
|
||||
static syncUsers(ldapUsers, callback) {
|
||||
let syncedCount = 0;
|
||||
let errorCount = 0;
|
||||
const errors = [];
|
||||
|
||||
if (!ldapUsers || ldapUsers.length === 0) {
|
||||
return callback(null, { synced: 0, errors: [] });
|
||||
}
|
||||
|
||||
// Verarbeite jeden Benutzer
|
||||
const processUser = (index) => {
|
||||
if (index >= ldapUsers.length) {
|
||||
return callback(null, { synced: syncedCount, errors: errors });
|
||||
}
|
||||
|
||||
const ldapUser = ldapUsers[index];
|
||||
// .trim() behält UTF-8-Zeichen wie ß, ä, ö, ü korrekt bei
|
||||
// Stelle sicher, dass Werte als String behandelt werden
|
||||
const username = String(ldapUser.username || '').trim();
|
||||
const firstname = String(ldapUser.firstname || '').trim();
|
||||
const lastname = String(ldapUser.lastname || '').trim();
|
||||
|
||||
// Prüfe ob Benutzer bereits existiert (case-insensitive)
|
||||
db.get('SELECT id, role FROM users WHERE username = ? COLLATE NOCASE', [username], (err, existingUser) => {
|
||||
if (err) {
|
||||
errors.push(`Fehler beim Prüfen von ${username}: ${err.message}`);
|
||||
errorCount++;
|
||||
return processUser(index + 1);
|
||||
}
|
||||
|
||||
if (existingUser) {
|
||||
// Benutzer existiert - aktualisiere nur Name, behalte Rolle (case-insensitive)
|
||||
db.run(
|
||||
'UPDATE users SET firstname = ?, lastname = ? WHERE username = ? COLLATE NOCASE',
|
||||
[firstname, lastname, username],
|
||||
(err) => {
|
||||
if (err) {
|
||||
errors.push(`Fehler beim Aktualisieren von ${username}: ${err.message}`);
|
||||
errorCount++;
|
||||
} else {
|
||||
syncedCount++;
|
||||
}
|
||||
processUser(index + 1);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Neuer Benutzer - erstelle mit Standard-Rolle
|
||||
// Generiere ein zufälliges Passwort (Benutzer muss es beim ersten Login ändern)
|
||||
const defaultPassword = bcrypt.hashSync('changeme123', 10);
|
||||
|
||||
db.run(
|
||||
'INSERT INTO users (username, password, firstname, lastname, role) VALUES (?, ?, ?, ?, ?)',
|
||||
[username, defaultPassword, firstname, lastname, 'mitarbeiter'],
|
||||
(err) => {
|
||||
if (err) {
|
||||
errors.push(`Fehler beim Erstellen von ${username}: ${err.message}`);
|
||||
errorCount++;
|
||||
} else {
|
||||
syncedCount++;
|
||||
}
|
||||
processUser(index + 1);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
processUser(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync-Log-Eintrag erstellen
|
||||
*/
|
||||
static createSyncLog(syncType, status, usersSynced, errorMessage, callback) {
|
||||
const startedAt = new Date().toISOString();
|
||||
const completedAt = new Date().toISOString();
|
||||
|
||||
db.run(
|
||||
`INSERT INTO ldap_sync_log (sync_type, status, users_synced, error_message, sync_started_at, sync_completed_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[syncType, status, usersSynced, errorMessage || null, startedAt, completedAt],
|
||||
(err) => {
|
||||
if (callback) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Letzte Synchronisation aktualisieren
|
||||
*/
|
||||
static updateLastSync(callback) {
|
||||
db.run(
|
||||
'UPDATE ldap_config SET last_sync = CURRENT_TIMESTAMP WHERE id = 1',
|
||||
(err) => {
|
||||
if (callback) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Benutzer gegen LDAP authentifizieren
|
||||
*
|
||||
* Unterstützt UTF-8-Zeichen wie ß, ä, ö, ü in Usernamen.
|
||||
* Die ldapjs-Bibliothek behandelt UTF-8 automatisch korrekt.
|
||||
*/
|
||||
static authenticate(username, password, callback) {
|
||||
// Stelle sicher, dass Username als String behandelt wird (UTF-8 wird korrekt unterstützt)
|
||||
const usernameStr = String(username || '').trim();
|
||||
|
||||
if (!usernameStr) {
|
||||
return callback(new Error('Benutzername darf nicht leer sein'), false);
|
||||
}
|
||||
|
||||
// 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';
|
||||
// escapeLDAPFilter behandelt UTF-8-Zeichen korrekt (escaped sie nicht)
|
||||
const escapedUsername = this.escapeLDAPFilter(usernameStr);
|
||||
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();
|
||||
// Verbesserte Fehlermeldung für mögliche Encoding-Probleme
|
||||
const errorMsg = err.message || String(err);
|
||||
return callback(new Error(`LDAP-Suche fehlgeschlagen: ${errorMsg}. Hinweis: Prüfen Sie, ob der Benutzername UTF-8-Zeichen (wie ß, ä, ö, ü) korrekt enthält.`), false);
|
||||
}
|
||||
|
||||
res.on('searchEntry', (entry) => {
|
||||
userDN = entry.dn.toString();
|
||||
});
|
||||
|
||||
res.on('error', (err) => {
|
||||
client.unbind();
|
||||
const errorMsg = err.message || String(err);
|
||||
callback(new Error(`LDAP-Suchfehler: ${errorMsg}`), false);
|
||||
});
|
||||
|
||||
res.on('end', (result) => {
|
||||
// Service-Account-Verbindung schließen
|
||||
client.unbind();
|
||||
|
||||
if (!userDN) {
|
||||
// Verbesserte Fehlermeldung: Hinweis auf mögliche Encoding-Probleme
|
||||
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);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const errorMsg = err.message || String(err);
|
||||
return callback(new Error(`Ungültiges Passwort oder Authentifizierungsfehler: ${errorMsg}`), false);
|
||||
}
|
||||
callback(null, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollständige Synchronisation durchführen
|
||||
*/
|
||||
static performSync(syncType, callback) {
|
||||
const startedAt = new Date();
|
||||
|
||||
// Konfiguration abrufen
|
||||
this.getConfig((err, config) => {
|
||||
if (err) {
|
||||
this.createSyncLog(syncType, 'error', 0, `Fehler beim Abrufen der Konfiguration: ${err.message}`, () => {});
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!config || !config.enabled) {
|
||||
const errorMsg = 'LDAP-Synchronisation ist nicht aktiviert';
|
||||
this.createSyncLog(syncType, 'error', 0, errorMsg, () => {});
|
||||
return callback(new Error(errorMsg));
|
||||
}
|
||||
|
||||
// LDAP-Verbindung herstellen
|
||||
this.connect(config, (err, client) => {
|
||||
if (err) {
|
||||
this.createSyncLog(syncType, 'error', 0, `LDAP-Verbindungsfehler: ${err.message}`, () => {});
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Benutzer aus LDAP abrufen
|
||||
this.searchUsers(client, config, (err, ldapUsers) => {
|
||||
// Verbindung schließen
|
||||
client.unbind();
|
||||
|
||||
if (err) {
|
||||
this.createSyncLog(syncType, 'error', 0, `LDAP-Suchfehler: ${err.message}`, () => {});
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Benutzer synchronisieren
|
||||
this.syncUsers(ldapUsers, (err, result) => {
|
||||
if (err) {
|
||||
this.createSyncLog(syncType, 'error', result.synced, `Sync-Fehler: ${err.message}`, () => {});
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Letzte Synchronisation aktualisieren
|
||||
this.updateLastSync(() => {
|
||||
const status = result.errors.length > 0 ? 'error' : 'success';
|
||||
const errorMsg = result.errors.length > 0 ? result.errors.join('; ') : null;
|
||||
|
||||
this.createSyncLog(syncType, status, result.synced, errorMsg, () => {
|
||||
callback(null, result);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LDAPService;
|
||||
const ldap = require('ldapjs');
|
||||
const { db } = require('../database');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
/**
|
||||
* LDAP-Service für Benutzer-Synchronisation
|
||||
*/
|
||||
class LDAPService {
|
||||
/**
|
||||
* LDAP-Konfiguration aus der Datenbank abrufen
|
||||
*/
|
||||
static getConfig(callback) {
|
||||
db.get('SELECT * FROM ldap_config WHERE id = 1', (err, config) => {
|
||||
if (err) {
|
||||
return callback(err, null);
|
||||
}
|
||||
callback(null, config);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* LDAP-Verbindung herstellen
|
||||
*/
|
||||
static connect(config, callback) {
|
||||
if (!config || !config.enabled || !config.url) {
|
||||
return callback(new Error('LDAP ist nicht konfiguriert oder deaktiviert'));
|
||||
}
|
||||
|
||||
const client = ldap.createClient({
|
||||
url: config.url,
|
||||
timeout: 10000,
|
||||
connectTimeout: 10000
|
||||
});
|
||||
|
||||
// Fehlerbehandlung
|
||||
client.on('error', (err) => {
|
||||
callback(err, null);
|
||||
});
|
||||
|
||||
// Bind mit Credentials
|
||||
const bindDN = config.bind_dn || '';
|
||||
const bindPassword = config.bind_password || '';
|
||||
|
||||
// Hinweis: Passwort wird im Klartext gespeichert
|
||||
// In einer produktiven Umgebung sollte man eine Verschlüsselung mit einem Master-Key verwenden
|
||||
|
||||
client.bind(bindDN, bindPassword, (err) => {
|
||||
if (err) {
|
||||
client.unbind();
|
||||
return callback(err, null);
|
||||
}
|
||||
callback(null, client);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Benutzer aus LDAP abrufen
|
||||
*/
|
||||
static searchUsers(client, config, callback) {
|
||||
const baseDN = config.base_dn || '';
|
||||
const searchFilter = config.user_search_filter || '(objectClass=person)';
|
||||
const searchOptions = {
|
||||
filter: searchFilter,
|
||||
scope: 'sub',
|
||||
attributes: [
|
||||
config.username_attribute || 'sAMAccountName',
|
||||
config.firstname_attribute || 'givenName',
|
||||
config.lastname_attribute || 'sn'
|
||||
]
|
||||
};
|
||||
|
||||
const users = [];
|
||||
|
||||
client.search(baseDN, searchOptions, (err, res) => {
|
||||
if (err) {
|
||||
return callback(err, null);
|
||||
}
|
||||
|
||||
res.on('searchEntry', (entry) => {
|
||||
const user = {
|
||||
username: this.getAttributeValue(entry, config.username_attribute || 'sAMAccountName'),
|
||||
firstname: this.getAttributeValue(entry, config.firstname_attribute || 'givenName'),
|
||||
lastname: this.getAttributeValue(entry, config.lastname_attribute || 'sn')
|
||||
};
|
||||
|
||||
// Nur Benutzer mit allen erforderlichen Feldern hinzufügen
|
||||
if (user.username && user.firstname && user.lastname) {
|
||||
users.push(user);
|
||||
}
|
||||
});
|
||||
|
||||
res.on('error', (err) => {
|
||||
callback(err, null);
|
||||
});
|
||||
|
||||
res.on('end', (result) => {
|
||||
if (result && result.status !== 0) {
|
||||
return callback(new Error(`LDAP-Suche fehlgeschlagen: ${result.status}`), null);
|
||||
}
|
||||
callback(null, users);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wert eines LDAP-Attributs extrahieren
|
||||
*
|
||||
* Die ldapjs-Bibliothek behandelt UTF-8-Zeichen automatisch korrekt.
|
||||
* Diese Funktion stellt sicher, dass UTF-8-Zeichen wie ß, ä, ö, ü korrekt zurückgegeben werden.
|
||||
*/
|
||||
static getAttributeValue(entry, attributeName) {
|
||||
const attr = entry.attributes.find(a => a.type === attributeName);
|
||||
if (!attr) {
|
||||
return null;
|
||||
}
|
||||
const value = Array.isArray(attr.values) ? attr.values[0] : attr.values;
|
||||
// Stelle sicher, dass der Wert als String zurückgegeben wird (UTF-8 wird automatisch korrekt behandelt)
|
||||
return value != null ? String(value) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escaped einen Wert für LDAP-Filter (verhindert LDAP-Injection)
|
||||
*
|
||||
* WICHTIG: UTF-8-Zeichen wie ß, ä, ö, ü müssen NICHT escaped werden.
|
||||
* LDAP-Filter unterstützen UTF-8 direkt nach RFC 4515.
|
||||
* Nur die speziellen LDAP-Filter-Zeichen werden escaped.
|
||||
*/
|
||||
static escapeLDAPFilter(value) {
|
||||
if (!value) return '';
|
||||
|
||||
// Stelle sicher, dass der Wert als String behandelt wird
|
||||
const str = String(value);
|
||||
|
||||
// Escape nur die speziellen LDAP-Filter-Zeichen
|
||||
// UTF-8-Zeichen wie ß, ä, ö, ü werden direkt verwendet
|
||||
return 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Benutzer in SQLite synchronisieren
|
||||
*/
|
||||
static syncUsers(ldapUsers, callback) {
|
||||
let syncedCount = 0;
|
||||
let errorCount = 0;
|
||||
const errors = [];
|
||||
|
||||
if (!ldapUsers || ldapUsers.length === 0) {
|
||||
return callback(null, { synced: 0, errors: [] });
|
||||
}
|
||||
|
||||
// Verarbeite jeden Benutzer
|
||||
const processUser = (index) => {
|
||||
if (index >= ldapUsers.length) {
|
||||
return callback(null, { synced: syncedCount, errors: errors });
|
||||
}
|
||||
|
||||
const ldapUser = ldapUsers[index];
|
||||
// .trim() behält UTF-8-Zeichen wie ß, ä, ö, ü korrekt bei
|
||||
// Stelle sicher, dass Werte als String behandelt werden
|
||||
const username = String(ldapUser.username || '').trim();
|
||||
const firstname = String(ldapUser.firstname || '').trim();
|
||||
const lastname = String(ldapUser.lastname || '').trim();
|
||||
|
||||
// Prüfe ob Benutzer bereits existiert (case-insensitive)
|
||||
db.get('SELECT id, role FROM users WHERE username = ? COLLATE NOCASE', [username], (err, existingUser) => {
|
||||
if (err) {
|
||||
errors.push(`Fehler beim Prüfen von ${username}: ${err.message}`);
|
||||
errorCount++;
|
||||
return processUser(index + 1);
|
||||
}
|
||||
|
||||
if (existingUser) {
|
||||
// Benutzer existiert - aktualisiere nur Name, behalte Rolle (case-insensitive)
|
||||
db.run(
|
||||
'UPDATE users SET firstname = ?, lastname = ? WHERE username = ? COLLATE NOCASE',
|
||||
[firstname, lastname, username],
|
||||
(err) => {
|
||||
if (err) {
|
||||
errors.push(`Fehler beim Aktualisieren von ${username}: ${err.message}`);
|
||||
errorCount++;
|
||||
} else {
|
||||
syncedCount++;
|
||||
}
|
||||
processUser(index + 1);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Neuer Benutzer - erstelle mit Standard-Rolle
|
||||
// Generiere ein zufälliges Passwort (Benutzer muss es beim ersten Login ändern)
|
||||
const defaultPassword = bcrypt.hashSync('changeme123', 10);
|
||||
|
||||
db.run(
|
||||
'INSERT INTO users (username, password, firstname, lastname, role) VALUES (?, ?, ?, ?, ?)',
|
||||
[username, defaultPassword, firstname, lastname, 'mitarbeiter'],
|
||||
(err) => {
|
||||
if (err) {
|
||||
errors.push(`Fehler beim Erstellen von ${username}: ${err.message}`);
|
||||
errorCount++;
|
||||
} else {
|
||||
syncedCount++;
|
||||
}
|
||||
processUser(index + 1);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
processUser(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync-Log-Eintrag erstellen
|
||||
*/
|
||||
static createSyncLog(syncType, status, usersSynced, errorMessage, callback) {
|
||||
const startedAt = new Date().toISOString();
|
||||
const completedAt = new Date().toISOString();
|
||||
|
||||
db.run(
|
||||
`INSERT INTO ldap_sync_log (sync_type, status, users_synced, error_message, sync_started_at, sync_completed_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[syncType, status, usersSynced, errorMessage || null, startedAt, completedAt],
|
||||
(err) => {
|
||||
if (callback) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Letzte Synchronisation aktualisieren
|
||||
*/
|
||||
static updateLastSync(callback) {
|
||||
db.run(
|
||||
'UPDATE ldap_config SET last_sync = CURRENT_TIMESTAMP WHERE id = 1',
|
||||
(err) => {
|
||||
if (callback) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Suchvarianten für Benutzername (case-insensitive, ß/ss) für LDAP-Filter.
|
||||
* So findet der Login auch "GeißlerJ", wenn LDAP "geißlerj" oder "GeisslerJ" speichert.
|
||||
*/
|
||||
static getUsernameSearchVariants(usernameStr) {
|
||||
const variants = new Set();
|
||||
variants.add(usernameStr);
|
||||
variants.add(usernameStr.toLowerCase());
|
||||
variants.add(usernameStr.replace(/\u00df/g, 'ss')); // ß -> ss
|
||||
variants.add(usernameStr.replace(/ss/g, '\u00df')); // ss -> ß (z. B. Geissler -> Geißler)
|
||||
return [...variants];
|
||||
}
|
||||
|
||||
/**
|
||||
* Benutzer gegen LDAP authentifizieren
|
||||
*
|
||||
* Unterstützt UTF-8-Zeichen wie ß, ä, ö, ü in Usernamen.
|
||||
* Sucht mit mehreren Varianten (Groß-/Kleinschreibung, ß/ss), damit z. B. "GeißlerJ"
|
||||
* auch gefunden wird, wenn LDAP "geißlerj" oder "GeisslerJ" speichert.
|
||||
* Gibt bei Erfolg den kanonischen LDAP-Benutzernamen zurück (für DB-Lookup).
|
||||
*/
|
||||
static authenticate(username, password, callback) {
|
||||
// Stelle sicher, dass Username als String behandelt wird (UTF-8 wird korrekt unterstützt)
|
||||
const usernameStr = String(username || '').trim();
|
||||
|
||||
if (!usernameStr) {
|
||||
return callback(new Error('Benutzername darf nicht leer sein'), false);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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,
|
||||
scope: 'sub',
|
||||
attributes: ['dn', usernameAttr]
|
||||
};
|
||||
|
||||
let userDN = null;
|
||||
let canonicalUsername = null;
|
||||
|
||||
client.search(baseDN, searchOptions, (err, res) => {
|
||||
if (err) {
|
||||
client.unbind();
|
||||
const errorMsg = err.message || String(err);
|
||||
return callback(new Error(`LDAP-Suche fehlgeschlagen: ${errorMsg}. Hinweis: Prüfen Sie, ob der Benutzername UTF-8-Zeichen (wie ß, ä, ö, ü) korrekt enthält.`), false);
|
||||
}
|
||||
|
||||
res.on('searchEntry', (entry) => {
|
||||
userDN = entry.dn.toString();
|
||||
// Kanonischen Benutzernamen aus LDAP verwenden (für DB-Lookup nach Sync)
|
||||
canonicalUsername = this.getAttributeValue(entry, usernameAttr) || usernameStr;
|
||||
});
|
||||
|
||||
res.on('error', (err) => {
|
||||
client.unbind();
|
||||
const errorMsg = err.message || String(err);
|
||||
callback(new Error(`LDAP-Suchfehler: ${errorMsg}`), false);
|
||||
});
|
||||
|
||||
res.on('end', (result) => {
|
||||
client.unbind();
|
||||
|
||||
if (!userDN) {
|
||||
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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollständige Synchronisation durchführen
|
||||
*/
|
||||
static performSync(syncType, callback) {
|
||||
const startedAt = new Date();
|
||||
|
||||
// Konfiguration abrufen
|
||||
this.getConfig((err, config) => {
|
||||
if (err) {
|
||||
this.createSyncLog(syncType, 'error', 0, `Fehler beim Abrufen der Konfiguration: ${err.message}`, () => {});
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!config || !config.enabled) {
|
||||
const errorMsg = 'LDAP-Synchronisation ist nicht aktiviert';
|
||||
this.createSyncLog(syncType, 'error', 0, errorMsg, () => {});
|
||||
return callback(new Error(errorMsg));
|
||||
}
|
||||
|
||||
// LDAP-Verbindung herstellen
|
||||
this.connect(config, (err, client) => {
|
||||
if (err) {
|
||||
this.createSyncLog(syncType, 'error', 0, `LDAP-Verbindungsfehler: ${err.message}`, () => {});
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Benutzer aus LDAP abrufen
|
||||
this.searchUsers(client, config, (err, ldapUsers) => {
|
||||
// Verbindung schließen
|
||||
client.unbind();
|
||||
|
||||
if (err) {
|
||||
this.createSyncLog(syncType, 'error', 0, `LDAP-Suchfehler: ${err.message}`, () => {});
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Benutzer synchronisieren
|
||||
this.syncUsers(ldapUsers, (err, result) => {
|
||||
if (err) {
|
||||
this.createSyncLog(syncType, 'error', result.synced, `Sync-Fehler: ${err.message}`, () => {});
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Letzte Synchronisation aktualisieren
|
||||
this.updateLastSync(() => {
|
||||
const status = result.errors.length > 0 ? 'error' : 'success';
|
||||
const errorMsg = result.errors.length > 0 ? result.errors.join('; ') : null;
|
||||
|
||||
this.createSyncLog(syncType, status, result.synced, errorMsg, () => {
|
||||
callback(null, result);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LDAPService;
|
||||
@@ -530,18 +530,56 @@ function generatePDFToBuffer(timesheetId, req) {
|
||||
}
|
||||
|
||||
// Check-in/Check-out URL-Basis (wie im Dashboard-Frontend)
|
||||
function getCheckinBaseUrl(req) {
|
||||
const baseUrl = `${req.protocol}://${req.get('host')}`;
|
||||
return baseUrl.replace(/:\d+$/, ':3334');
|
||||
function getCheckinBaseUrl(req, callback) {
|
||||
// Versuche Root URL aus Datenbank zu laden
|
||||
db.get('SELECT checkin_root_url FROM system_options WHERE id = 1', (err, options) => {
|
||||
if (err) {
|
||||
console.warn('Fehler beim Laden der Root URL, verwende Fallback:', err);
|
||||
}
|
||||
|
||||
let checkinBaseUrl = null;
|
||||
if (options && options.checkin_root_url && options.checkin_root_url.trim() !== '') {
|
||||
checkinBaseUrl = options.checkin_root_url.trim();
|
||||
// Stelle sicher, dass kein trailing slash vorhanden ist
|
||||
checkinBaseUrl = checkinBaseUrl.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
// Fallback: Konstruiere URL aus Request (Port 3334 für Check-in)
|
||||
if (!checkinBaseUrl) {
|
||||
const baseUrl = `${req.protocol}://${req.get('host')}`;
|
||||
checkinBaseUrl = baseUrl.replace(/:\d+$/, ':3334');
|
||||
}
|
||||
|
||||
callback(checkinBaseUrl);
|
||||
});
|
||||
}
|
||||
|
||||
// PDF mit Check-in- und Check-out-QR-Codes (A4)
|
||||
async function generateCheckinCheckoutQRPDF(req, res) {
|
||||
// urlType: 'internal' oder 'external'
|
||||
async function generateCheckinCheckoutQRPDF(req, res, urlType = 'internal') {
|
||||
const userId = req.session.userId;
|
||||
if (!userId) {
|
||||
return res.status(401).send('Nicht angemeldet');
|
||||
}
|
||||
const checkinBaseUrl = getCheckinBaseUrl(req);
|
||||
|
||||
let checkinBaseUrl;
|
||||
|
||||
if (urlType === 'external') {
|
||||
// Externe URL: Lade aus Datenbank
|
||||
checkinBaseUrl = await new Promise((resolve) => {
|
||||
getCheckinBaseUrl(req, resolve);
|
||||
});
|
||||
} else {
|
||||
// Interne URL: Konstruiere aus Request (Port 3334)
|
||||
const baseUrl = `${req.protocol}://${req.get('host')}`;
|
||||
if (baseUrl.match(/:\d+$/)) {
|
||||
checkinBaseUrl = baseUrl.replace(/:\d+$/, ':3334');
|
||||
} else {
|
||||
const url = new URL(baseUrl);
|
||||
checkinBaseUrl = `${url.protocol}//${url.hostname}:3334`;
|
||||
}
|
||||
}
|
||||
|
||||
const checkinUrl = `${checkinBaseUrl}/api/checkin/${userId}`;
|
||||
const checkoutUrl = `${checkinBaseUrl}/api/checkout/${userId}`;
|
||||
|
||||
@@ -554,11 +592,12 @@ async function generateCheckinCheckoutQRPDF(req, res) {
|
||||
const firstname = (req.session.firstname || '').replace(/\s+/g, '');
|
||||
const lastname = (req.session.lastname || '').replace(/\s+/g, '');
|
||||
const namePart = [firstname, lastname].filter(Boolean).join('_') || 'User';
|
||||
const urlTypeLabel = urlType === 'external' ? 'Extern' : 'Intern';
|
||||
const today = new Date();
|
||||
const dateStr = today.getFullYear() + '-' +
|
||||
String(today.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(today.getDate()).padStart(2, '0');
|
||||
const filename = `Check-in_Check-out_QR_${namePart}_${dateStr}.pdf`;
|
||||
const filename = `Check-in_Check-out_QR_${urlTypeLabel}_${namePart}_${dateStr}.pdf`;
|
||||
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
@@ -573,7 +612,10 @@ async function generateCheckinCheckoutQRPDF(req, res) {
|
||||
const left1 = 50 + (pageWidth / 2 - qrSize - gap / 2);
|
||||
const left2 = 50 + (pageWidth / 2 + gap / 2);
|
||||
|
||||
doc.fontSize(18).text('Check-in / Check-out – Zeiterfassung', { align: 'center' });
|
||||
const title = urlType === 'external'
|
||||
? 'Check-in / Check-out – Zeiterfassung (Externe URLs)'
|
||||
: 'Check-in / Check-out – Zeiterfassung (Interne URLs)';
|
||||
doc.fontSize(18).text(title, { align: 'center' });
|
||||
doc.moveDown(1.5);
|
||||
|
||||
const topY = doc.y;
|
||||
|
||||
@@ -234,6 +234,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top: 20px;">
|
||||
<h3 style="margin-bottom: 10px;">Check-in URL-Konfiguration</h3>
|
||||
<p style="margin-bottom: 15px; color: #666;">Definieren Sie die Basis-URL für alle Check-in und Check-out URLs. Diese URL wird vor <code>/api</code> verwendet. Beispiel: https://example.com:3334</p>
|
||||
<label for="checkinRootUrl">Check-in Root URL</label>
|
||||
<input type="text" id="checkinRootUrl" name="checkin_root_url" class="form-control" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" placeholder="https://example.com:3334" value="<%= (typeof options !== 'undefined' && options && options.checkin_root_url) ? options.checkin_root_url : '' %>">
|
||||
<small style="color: #666; display: block; margin-top: 5px;">Lassen Sie dieses Feld leer, um die URL automatisch aus der aktuellen Seite zu generieren.</small>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Optionen speichern</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -289,7 +297,7 @@
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="ldapUsernameAttr">Username-Attribut</label>
|
||||
<input type="text" id="ldapUsernameAttr" name="username_attribute" placeholder="cn" value="cn">
|
||||
<input type="text" id="ldapUsernameAttr" name="username_attribute" placeholder="sAMAccountName" value="sAMAccountName">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
||||
@@ -99,28 +99,59 @@
|
||||
Automatische Zeiterfassung
|
||||
</h3>
|
||||
<div id="timeCaptureContent" style="display: none; margin-top: 15px;">
|
||||
<!-- URL-Erfassung -->
|
||||
<!-- Interne URLs -->
|
||||
<div style="margin-bottom: 20px;">
|
||||
<h4 style="font-size: 13px; margin-bottom: 10px; color: #555; display: flex; align-items: center; gap: 5px;">
|
||||
Zeiterfassung per URL
|
||||
<span class="help-icon" onclick="showHelpModal('url-help')" style="cursor: pointer; color: #3498db; font-size: 14px; font-weight: bold; width: 18px; height: 18px; border-radius: 50%; background: #e8f4f8; display: inline-flex; align-items: center; justify-content: center; line-height: 1;">?</span>
|
||||
<h4 style="font-size: 13px; margin-bottom: 10px; color: #555; cursor: pointer; user-select: none; display: flex; align-items: center; gap: 8px;" onclick="toggleInternalUrls()">
|
||||
<span class="toggle-icon-internal-urls" style="display: inline-block; transition: transform 0.3s; font-size: 10px;">▶</span>
|
||||
Interne URLs
|
||||
<span class="help-icon" onclick="event.stopPropagation(); showHelpModal('url-help')" style="cursor: pointer; color: #3498db; font-size: 14px; font-weight: bold; width: 18px; height: 18px; border-radius: 50%; background: #e8f4f8; display: inline-flex; align-items: center; justify-content: center; line-height: 1; margin-left: auto;">?</span>
|
||||
</h4>
|
||||
<div class="form-group" style="margin-bottom: 15px;">
|
||||
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-in URL</label>
|
||||
<div style="display: flex; gap: 5px;">
|
||||
<input type="text" id="checkinUrl" readonly style="flex: 1; padding: 8px; font-size: 11px; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa;">
|
||||
<button onclick="copyToClipboard('checkinUrl')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
|
||||
<div id="internalUrlsContent" style="display: none; margin-top: 10px;">
|
||||
<div class="form-group" style="margin-bottom: 15px;">
|
||||
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-in URL</label>
|
||||
<div style="display: flex; gap: 5px;">
|
||||
<input type="text" id="checkinUrlInternal" readonly style="flex: 1; padding: 8px; font-size: 11px; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa;">
|
||||
<button onclick="copyToClipboard('checkinUrlInternal')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 15px;">
|
||||
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-out URL</label>
|
||||
<div style="display: flex; gap: 5px;">
|
||||
<input type="text" id="checkoutUrlInternal" readonly style="flex: 1; padding: 8px; font-size: 11px; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa;">
|
||||
<button onclick="copyToClipboard('checkoutUrlInternal')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 12px;">
|
||||
<a href="/api/dashboard/qr-pdf/internal" class="btn btn-sm btn-secondary" style="padding: 8px 12px; text-decoration: none; display: inline-block;" download>QR-Code-PDF herunterladen</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 15px;">
|
||||
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-out URL</label>
|
||||
<div style="display: flex; gap: 5px;">
|
||||
<input type="text" id="checkoutUrl" readonly style="flex: 1; padding: 8px; font-size: 11px; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa;">
|
||||
<button onclick="copyToClipboard('checkoutUrl')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
|
||||
</div>
|
||||
|
||||
<!-- Externe URLs -->
|
||||
<div id="externalUrlsSection" style="margin-bottom: 20px; display: none;">
|
||||
<h4 style="font-size: 13px; margin-bottom: 10px; color: #555; cursor: pointer; user-select: none; display: flex; align-items: center; gap: 8px;" onclick="toggleExternalUrls()">
|
||||
<span class="toggle-icon-external-urls" style="display: inline-block; transition: transform 0.3s; font-size: 10px;">▶</span>
|
||||
Externe URLs
|
||||
<span class="help-icon" onclick="event.stopPropagation(); showHelpModal('url-help')" style="cursor: pointer; color: #3498db; font-size: 14px; font-weight: bold; width: 18px; height: 18px; border-radius: 50%; background: #e8f4f8; display: inline-flex; align-items: center; justify-content: center; line-height: 1; margin-left: auto;">?</span>
|
||||
</h4>
|
||||
<div id="externalUrlsContent" style="display: none; margin-top: 10px;">
|
||||
<div class="form-group" style="margin-bottom: 15px;">
|
||||
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-in URL</label>
|
||||
<div style="display: flex; gap: 5px;">
|
||||
<input type="text" id="checkinUrlExternal" readonly style="flex: 1; padding: 8px; font-size: 11px; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa;">
|
||||
<button onclick="copyToClipboard('checkinUrlExternal')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 15px;">
|
||||
<label style="font-size: 12px; color: #666; margin-bottom: 5px;">Check-out URL</label>
|
||||
<div style="display: flex; gap: 5px;">
|
||||
<input type="text" id="checkoutUrlExternal" readonly style="flex: 1; padding: 8px; font-size: 11px; border: 1px solid #ddd; border-radius: 4px; background: #f8f9fa;">
|
||||
<button onclick="copyToClipboard('checkoutUrlExternal')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 12px;">
|
||||
<a href="/api/dashboard/qr-pdf/external" class="btn btn-sm btn-secondary" style="padding: 8px 12px; text-decoration: none; display: inline-block;" download>QR-Code-PDF herunterladen</a>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 12px;">
|
||||
<a href="/api/dashboard/qr-pdf" class="btn btn-sm btn-secondary" style="padding: 8px 12px; text-decoration: none; display: inline-block;" download>QR-Code-PDF herunterladen</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -180,6 +211,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Interne URLs ein-/ausklappen
|
||||
function toggleInternalUrls() {
|
||||
const content = document.getElementById('internalUrlsContent');
|
||||
const icon = document.querySelector('.toggle-icon-internal-urls');
|
||||
|
||||
if (content && icon) {
|
||||
if (content.style.display === 'none') {
|
||||
content.style.display = 'block';
|
||||
icon.style.transform = 'rotate(90deg)';
|
||||
} else {
|
||||
content.style.display = 'none';
|
||||
icon.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Externe URLs ein-/ausklappen
|
||||
function toggleExternalUrls() {
|
||||
const content = document.getElementById('externalUrlsContent');
|
||||
const icon = document.querySelector('.toggle-icon-external-urls');
|
||||
|
||||
if (content && icon) {
|
||||
if (content.style.display === 'none') {
|
||||
content.style.display = 'block';
|
||||
icon.style.transform = 'rotate(90deg)';
|
||||
} else {
|
||||
content.style.display = 'none';
|
||||
icon.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// URL-Kopier-Funktion
|
||||
function copyToClipboard(inputId) {
|
||||
const input = document.getElementById(inputId);
|
||||
@@ -201,24 +264,56 @@
|
||||
}
|
||||
|
||||
// URLs mit aktueller Domain aktualisieren (Port 3334 für Check-in)
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
const userId = '<%= user.id %>';
|
||||
|
||||
// Interne URLs: Konstruiere aus window.location.origin (Port 3334 für Check-in)
|
||||
const baseUrl = window.location.origin;
|
||||
// Check-in URLs verwenden Port 3334
|
||||
// Ersetze Port in URL oder füge Port hinzu falls nicht vorhanden
|
||||
let checkinBaseUrl;
|
||||
let internalBaseUrl;
|
||||
if (baseUrl.match(/:\d+$/)) {
|
||||
// Port vorhanden - ersetze ihn
|
||||
checkinBaseUrl = baseUrl.replace(/:\d+$/, ':3334');
|
||||
internalBaseUrl = baseUrl.replace(/:\d+$/, ':3334');
|
||||
} else {
|
||||
// Kein Port - füge Port hinzu
|
||||
const url = new URL(baseUrl);
|
||||
checkinBaseUrl = `${url.protocol}//${url.hostname}:3334`;
|
||||
internalBaseUrl = `${url.protocol}//${url.hostname}:3334`;
|
||||
}
|
||||
|
||||
// Setze interne URLs
|
||||
const checkinInputInternal = document.getElementById('checkinUrlInternal');
|
||||
const checkoutInputInternal = document.getElementById('checkoutUrlInternal');
|
||||
if (checkinInputInternal) checkinInputInternal.value = `${internalBaseUrl}/api/checkin/${userId}`;
|
||||
if (checkoutInputInternal) checkoutInputInternal.value = `${internalBaseUrl}/api/checkout/${userId}`;
|
||||
|
||||
// Externe URLs: Versuche Root URL von API zu laden
|
||||
let externalBaseUrl = null;
|
||||
try {
|
||||
const response = await fetch('/api/checkin-root-url');
|
||||
const result = await response.json();
|
||||
if (result.root_url && result.root_url.trim() !== '') {
|
||||
externalBaseUrl = result.root_url.trim();
|
||||
// Stelle sicher, dass kein trailing slash vorhanden ist
|
||||
externalBaseUrl = externalBaseUrl.replace(/\/+$/, '');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Fehler beim Laden der externen Root URL:', error);
|
||||
}
|
||||
|
||||
// Zeige externe Sektion nur an, wenn Root URL konfiguriert ist
|
||||
const externalSection = document.getElementById('externalUrlsSection');
|
||||
if (externalBaseUrl) {
|
||||
// Setze externe URLs
|
||||
const checkinInputExternal = document.getElementById('checkinUrlExternal');
|
||||
const checkoutInputExternal = document.getElementById('checkoutUrlExternal');
|
||||
if (checkinInputExternal) checkinInputExternal.value = `${externalBaseUrl}/api/checkin/${userId}`;
|
||||
if (checkoutInputExternal) checkoutInputExternal.value = `${externalBaseUrl}/api/checkout/${userId}`;
|
||||
|
||||
// Zeige externe Sektion an
|
||||
if (externalSection) externalSection.style.display = 'block';
|
||||
} else {
|
||||
// Verstecke externe Sektion
|
||||
if (externalSection) externalSection.style.display = 'none';
|
||||
}
|
||||
const checkinInput = document.getElementById('checkinUrl');
|
||||
const checkoutInput = document.getElementById('checkoutUrl');
|
||||
if (checkinInput) checkinInput.value = `${checkinBaseUrl}/api/checkin/${userId}`;
|
||||
if (checkoutInput) checkoutInput.value = `${checkinBaseUrl}/api/checkout/${userId}`;
|
||||
|
||||
// Rollenwechsel-Handler
|
||||
const roleSwitcher = document.getElementById('roleSwitcher');
|
||||
|
||||
Reference in New Issue
Block a user