From a1ddaf5a3522317a39c682ec07c4dfa36c12e510 Mon Sep 17 00:00:00 2001 From: Carsten Graf Date: Fri, 30 Jan 2026 10:44:07 +0100 Subject: [PATCH] Init --- .dockerignore | 20 + .gitignore | 27 + Dockerfile | 22 + README.md | 152 + SCHNELLSTART.md | 110 + checkin-server.js | 127 + database.js | 294 + dev/ldapserver/docker-compose.yml | 44 + docker-compose.yml | 15 + email-mitarbeiter-stundenerfassung.txt | 18 + helpers/utils.js | 131 + ldap-service.js | 402 ++ middleware/auth.js | 48 + package-lock.json | 7431 ++++++++++++++++++++++++ package.json | 27 + public/css/style.css | 1054 ++++ public/images/favicon.png | Bin 0 -> 46071 bytes public/images/header.png | Bin 0 -> 41724 bytes public/js/admin.js | 342 ++ public/js/dashboard.js | 1539 +++++ reset-db.js | 445 ++ routes/admin-ldap.js | 167 + routes/admin.js | 154 + routes/auth.js | 132 + routes/dashboard.js | 35 + routes/timesheet.js | 379 ++ routes/user.js | 468 ++ routes/verwaltung.js | 391 ++ server.js | 92 + services/feiertage-service.js | 109 + services/ldap-scheduler.js | 34 + services/pdf-service.js | 531 ++ services/ping-service.js | 182 + views/admin.ejs | 419 ++ views/dashboard.ejs | 406 ++ views/login.ejs | 46 + views/verwaltung.ejs | 780 +++ 37 files changed, 16573 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 SCHNELLSTART.md create mode 100644 checkin-server.js create mode 100644 database.js create mode 100644 dev/ldapserver/docker-compose.yml create mode 100644 docker-compose.yml create mode 100644 email-mitarbeiter-stundenerfassung.txt create mode 100644 helpers/utils.js create mode 100644 ldap-service.js create mode 100644 middleware/auth.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/css/style.css create mode 100644 public/images/favicon.png create mode 100644 public/images/header.png create mode 100644 public/js/admin.js create mode 100644 public/js/dashboard.js create mode 100644 reset-db.js create mode 100644 routes/admin-ldap.js create mode 100644 routes/admin.js create mode 100644 routes/auth.js create mode 100644 routes/dashboard.js create mode 100644 routes/timesheet.js create mode 100644 routes/user.js create mode 100644 routes/verwaltung.js create mode 100644 server.js create mode 100644 services/feiertage-service.js create mode 100644 services/ldap-scheduler.js create mode 100644 services/pdf-service.js create mode 100644 services/ping-service.js create mode 100644 views/admin.ejs create mode 100644 views/dashboard.ejs create mode 100644 views/login.ejs create mode 100644 views/verwaltung.ejs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..effa617 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +node_modules +npm-debug.log +.git +.gitignore +README.md +SCHNELLSTART.md +.env +.env.local +*.db +*.sqlite +*.sqlite3 +.vscode +.idea +*.swp +*.swo +.DS_Store +Thumbs.db +logs +*.log +dev diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b58196 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Dependencies +node_modules/ +npm-debug.log* + +# Database +*.db +*.sqlite +*.sqlite3 +data/ + +# Environment variables +.env +.env.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +logs/ +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..86f8e76 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM node:18-alpine + +WORKDIR /app + +# Zeitzone einrichten +RUN apk add --no-cache tzdata +ENV TZ=Europe/Berlin + +# Package-Dateien kopieren +COPY package*.json ./ + +# Dependencies installieren +RUN npm ci --only=production + +# Anwendungsdateien kopieren +COPY . . + +# Ports freigeben +EXPOSE 3333 3334 + +# Anwendung starten +CMD ["node", "server.js"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..a3a1869 --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +# Stundenerfassungs-System + +Eine webbasierte Anwendung zur Erfassung von Arbeitszeiten mit Admin-Bereich und PDF-Export. + +## Features + +### Für Mitarbeiter +- ✅ Login mit Benutzername und Passwort +- ✅ Wöchentliche Stundenerfassung (Montag - Sonntag) +- ✅ Automatisches Speichern der Einträge +- ✅ Eingabe von Start-/Endzeit, Pausen und Notizen +- ✅ Automatische Berechnung der Gesamtstunden +- ✅ Wöchentliches Abschicken des Stundenzettels + +### Für Administratoren +- ✅ Benutzerverwaltung (Anlegen, Löschen) +- ✅ Rollenvergabe (Mitarbeiter, Verwaltung, Admin) +- ✅ Übersicht aller Benutzer + +### Für Verwaltung +- ✅ Postfach mit eingereichten Stundenzetteln +- ✅ PDF-Generierung und Download +- ✅ Übersichtliche Darstellung aller Einreichungen + +## Installation + +### Voraussetzungen +- Node.js (Version 14 oder höher) +- npm (wird mit Node.js installiert) + +### Schritt 1: Dependencies installieren + +```bash +npm install +``` + +### Schritt 2: Server starten + +```bash +npm start +``` + +Der Server läuft nun auf `http://localhost:3333` + +Für Entwicklung mit automatischem Neustart: +```bash +npm run dev +``` + +## Standard-Zugangsdaten + +Nach der Installation sind folgende Benutzer verfügbar: + +### Administrator +- **Benutzername:** admin +- **Passwort:** admin123 +- **Funktion:** Kann Benutzer anlegen und verwalten + +### Verwaltung +- **Benutzername:** verwaltung +- **Passwort:** verwaltung123 +- **Funktion:** Kann eingereichte Stundenzettel einsehen und PDFs erstellen + +**WICHTIG:** Bitte ändern Sie diese Passwörter nach der ersten Anmeldung! + +## Verwendung + +### Für Mitarbeiter + +1. Melden Sie sich mit Ihren Zugangsdaten an +2. Wählen Sie die gewünschte Woche aus (Pfeiltasten) +3. Tragen Sie Ihre Arbeitszeiten ein: + - **Start:** Arbeitsbeginn + - **Ende:** Arbeitsende + - **Pause:** Pausenzeit in Minuten + - **Notizen:** Optional, z.B. Projekt oder Tätigkeit +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 + +### Für Administratoren + +1. Melden Sie sich als Admin an +2. Sie gelangen automatisch zur Benutzerverwaltung +3. **Neuen Benutzer anlegen:** + - Füllen Sie das Formular aus + - Wählen Sie die passende Rolle + - 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 + +### Für Verwaltung + +1. Melden Sie sich als Verwaltungs-Benutzer an +2. Sie sehen alle eingereichten Stundenzettel im Postfach +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 + - Zeitraum + - Alle Tageseinträge mit Start, Ende, Pause, Stunden und Notizen + - Gesamtstundensumme + +## Technologie-Stack + +- **Backend:** Node.js + Express +- **Datenbank:** SQLite3 +- **Template Engine:** EJS +- **PDF-Generierung:** PDFKit +- **Authentifizierung:** bcryptjs + express-session + +## Datenbankstruktur + +### Tabelle: users +- Speichert Benutzerinformationen und Zugangsdaten +- Passwörter werden verschlüsselt gespeichert + +### Tabelle: timesheet_entries +- Speichert einzelne Tageseinträge +- Automatische Berechnung der Gesamtstunden + +### Tabelle: weekly_timesheets +- Speichert eingereichte Wochenstundenzettel +- Verknüpfung mit Benutzer und Status + +## Sicherheit + +- ✅ Passwörter werden mit bcrypt verschlüsselt +- ✅ Session-basierte Authentifizierung +- ✅ Rollenbasierte Zugriffskontrolle +- ✅ CSRF-Schutz durch Sessions + +## Anpassungen + +### Port ändern +Bearbeiten Sie in `server.js` die Zeile: +```javascript +const PORT = 3333; // Ändern Sie hier den Port +``` + +### Datenbank-Speicherort +Die Datenbank wird standardmäßig als `stundenerfassung.db` im Projektverzeichnis gespeichert. + +## Lizenz + +Proprietär - Für interne Firmennutzung + +## Support + +Bei Fragen oder Problemen wenden Sie sich bitte an Ihre IT-Abteilung. diff --git a/SCHNELLSTART.md b/SCHNELLSTART.md new file mode 100644 index 0000000..7683813 --- /dev/null +++ b/SCHNELLSTART.md @@ -0,0 +1,110 @@ +# Schnellstart-Anleitung + +## Installation in 3 Schritten + +### 1. Projekt entpacken +Entpacken Sie das Projekt-Archiv in einen beliebigen Ordner auf Ihrem Server. + +### 2. Dependencies installieren +Öffnen Sie ein Terminal/Kommandozeile im Projekt-Ordner und führen Sie aus: + +```bash +npm install +``` + +Dies installiert alle benötigten Pakete: +- express (Webserver) +- sqlite3 (Datenbank) +- bcryptjs (Passwort-Verschlüsselung) +- express-session (Session-Verwaltung) +- ejs (Template Engine) +- pdfkit (PDF-Generierung) +- body-parser (Request-Verarbeitung) + +### 3. Server starten +```bash +npm start +``` + +Die Anwendung ist nun unter `http://localhost:3000` erreichbar. + +## Erster Login + +### Als Administrator +- URL: `http://localhost:3000` +- Benutzername: `admin` +- Passwort: `admin123` + +Nach dem Login können Sie: +- Neue Mitarbeiter anlegen +- Rollen vergeben (Mitarbeiter, Verwaltung, Admin) +- Benutzer verwalten + +### Als Verwaltung +- URL: `http://localhost:3000` +- Benutzername: `verwaltung` +- Passwort: `verwaltung123` + +Nach dem Login können Sie: +- Eingereichte Stundenzettel einsehen +- PDFs erstellen und herunterladen + +## Wichtige Hinweise + +⚠️ **Passwörter ändern!** +Bitte ändern Sie die Standard-Passwörter nach der ersten Anmeldung! + +⚠️ **Firewall-Einstellungen** +Stellen Sie sicher, dass Port 3000 in Ihrer Firewall geöffnet ist, falls Sie von anderen Computern darauf zugreifen möchten. + +⚠️ **Produktiv-Einsatz** +Für den Produktiv-Einsatz empfehlen wir: +- HTTPS verwenden (z.B. mit nginx als Reverse Proxy) +- Starke Passwörter verwenden +- Regelmäßige Backups der Datenbank erstellen + +## Port ändern + +Falls Port 3000 bereits belegt ist, können Sie den Port ändern: + +1. Öffnen Sie `server.js` +2. Ändern Sie die Zeile `const PORT = 3000;` auf den gewünschten Port +3. Speichern und Server neu starten + +## Datenbank-Speicherort + +Die SQLite-Datenbank wird automatisch als `stundenerfassung.db` im Projekt-Verzeichnis erstellt. + +**Backup erstellen:** +Kopieren Sie einfach die Datei `stundenerfassung.db` an einen sicheren Ort. + +## Problemlösung + +### Server startet nicht +- Prüfen Sie, ob Port 3000 bereits belegt ist +- Prüfen Sie, ob Node.js installiert ist: `node --version` +- Prüfen Sie, ob alle Dependencies installiert sind: `npm install` + +### Login funktioniert nicht +- Löschen Sie die Datei `stundenerfassung.db` und starten Sie den Server neu +- Die Datenbank wird dann mit den Standard-Benutzern neu erstellt + +### PDF-Download funktioniert nicht +- Prüfen Sie die Browser-Konsole auf Fehler +- Stellen Sie sicher, dass Popups für die Seite erlaubt sind + +## Workflow + +1. **Admin** legt neue Mitarbeiter an +2. **Mitarbeiter** melden sich an und erfassen ihre Stunden +3. Mitarbeiter sehen ihre Woche (Montag-Sonntag) +4. Einträge werden automatisch beim Ausfüllen gespeichert +5. Am Ende der Woche: "Woche abschicken" klicken +6. **Verwaltung** sieht eingereichte Stundenzettel im Postfach +7. Verwaltung kann PDFs erstellen und herunterladen + +## Support + +Bei Fragen oder Problemen: +- Prüfen Sie die ausführliche README.md +- Kontaktieren Sie Ihre IT-Abteilung diff --git a/checkin-server.js b/checkin-server.js new file mode 100644 index 0000000..ef3a1f2 --- /dev/null +++ b/checkin-server.js @@ -0,0 +1,127 @@ +// Check-in Server (separater Express-App auf Port 3334) + +const express = require('express'); +const { db } = require('./database'); +const { getCurrentDate, getCurrentTime, updateTotalHours } = require('./helpers/utils'); + +const checkinApp = express(); +const CHECKIN_PORT = 3336; + +// Middleware für Check-in-Server +checkinApp.use(express.json()); + +// API: Check-in (Kommen) +checkinApp.get('/api/checkin/:userId', (req, res) => { + const userId = parseInt(req.params.userId); + const currentDate = getCurrentDate(); + const currentTime = getCurrentTime(); + + // Prüfe ob User existiert + db.get('SELECT id FROM users WHERE id = ?', [userId], (err, user) => { + if (err || !user) { + return res.status(404).json({ success: false, error: 'Benutzer nicht gefunden' }); + } + + // Prüfe ob bereits ein Eintrag für heute existiert + db.get('SELECT * FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1', + [userId, currentDate], (err, entry) => { + if (err) { + return res.status(500).json({ success: false, error: 'Fehler beim Abrufen des Eintrags' }); + } + + if (!entry) { + // Kein Eintrag existiert → Erstelle neuen mit start_time + db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)`, + [userId, currentDate, currentTime], (err) => { + if (err) { + return res.status(500).json({ success: false, error: 'Fehler beim Erstellen des Eintrags' }); + } + res.json({ + success: true, + message: `Start-Zeit erfasst: ${currentTime}`, + start_time: currentTime, + date: currentDate + }); + }); + } else if (!entry.start_time) { + // Eintrag existiert, aber keine Start-Zeit → Setze start_time + db.run('UPDATE timesheet_entries SET start_time = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', + [currentTime, entry.id], (err) => { + if (err) { + return res.status(500).json({ success: false, error: 'Fehler beim Aktualisieren' }); + } + res.json({ + success: true, + message: `Start-Zeit erfasst: ${currentTime}`, + start_time: currentTime, + date: currentDate + }); + }); + } else { + // Start-Zeit bereits vorhanden → Ignoriere weiteren Check-in + res.json({ + success: true, + message: `Bereits eingecheckt um ${entry.start_time}. Check-in ignoriert.`, + start_time: entry.start_time, + date: currentDate + }); + } + }); + }); +}); + +// API: Check-out (Gehen) +checkinApp.get('/api/checkout/:userId', (req, res) => { + const userId = parseInt(req.params.userId); + const currentDate = getCurrentDate(); + const currentTime = getCurrentTime(); + + // Prüfe ob User existiert + db.get('SELECT id FROM users WHERE id = ?', [userId], (err, user) => { + if (err || !user) { + return res.status(404).json({ success: false, error: 'Benutzer nicht gefunden' }); + } + + // Prüfe ob bereits ein Eintrag für heute existiert + db.get('SELECT * FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1', + [userId, currentDate], (err, entry) => { + if (err) { + return res.status(500).json({ success: false, error: 'Fehler beim Abrufen des Eintrags' }); + } + + if (!entry || !entry.start_time) { + // Kein Eintrag oder keine Start-Zeit → Fehler + return res.status(400).json({ + success: false, + error: 'Bitte zuerst einchecken (Kommen).' + }); + } + + // Berechne total_hours basierend auf start_time, end_time und break_minutes + const breakMinutes = entry.break_minutes || 0; + const totalHours = updateTotalHours(entry.start_time, currentTime, breakMinutes); + + // Setze end_time (überschreibt vorherige End-Zeit falls vorhanden) + db.run('UPDATE timesheet_entries SET end_time = ?, total_hours = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', + [currentTime, totalHours, entry.id], (err) => { + if (err) { + return res.status(500).json({ success: false, error: 'Fehler beim Aktualisieren' }); + } + res.json({ + success: true, + message: `End-Zeit erfasst: ${currentTime}. Gesamtstunden: ${totalHours.toFixed(2)} h`, + end_time: currentTime, + total_hours: totalHours, + date: currentDate + }); + }); + }); + }); +}); + +// Check-in-Server starten (auf Port 3334) +checkinApp.listen(CHECKIN_PORT, () => { + console.log(`Check-in Server läuft auf http://localhost:${CHECKIN_PORT}`); +}); + +module.exports = checkinApp; diff --git a/database.js b/database.js new file mode 100644 index 0000000..9bcb239 --- /dev/null +++ b/database.js @@ -0,0 +1,294 @@ +const sqlite3 = require('sqlite3').verbose(); +const bcrypt = require('bcryptjs'); +const path = require('path'); + +// Datenbank-Pfad: Umgebungsvariable oder Standard-Pfad +const dbPath = process.env.DB_PATH || path.join(__dirname, 'stundenerfassung.db'); +const db = new sqlite3.Database(dbPath); + +// Datenbank initialisieren +function initDatabase() { + db.serialize(() => { + // Benutzer-Tabelle + db.run(`CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + firstname TEXT NOT NULL, + lastname TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'mitarbeiter', + last_week_start TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`); + + // Migration: last_week_start Spalte hinzufügen falls sie nicht existiert + db.run(`ALTER TABLE users ADD COLUMN last_week_start TEXT`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + }); + + // Stundenerfassung-Tabelle + db.run(`CREATE TABLE IF NOT EXISTS timesheet_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + date TEXT NOT NULL, + start_time TEXT, + end_time TEXT, + break_minutes INTEGER DEFAULT 0, + total_hours REAL, + notes TEXT, + activity1_desc TEXT, + activity1_hours REAL, + activity2_desc TEXT, + activity2_hours REAL, + activity3_desc TEXT, + activity3_hours REAL, + activity4_desc TEXT, + activity4_hours REAL, + activity5_desc TEXT, + activity5_hours REAL, + status TEXT DEFAULT 'offen', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + )`); + + // Migration: Tätigkeitsfelder hinzufügen falls sie nicht existieren + const activityColumns = [ + 'activity1_desc', 'activity1_hours', + 'activity2_desc', 'activity2_hours', + 'activity3_desc', 'activity3_hours', + 'activity4_desc', 'activity4_hours', + 'activity5_desc', 'activity5_hours' + ]; + + activityColumns.forEach(col => { + const colType = col.includes('_hours') ? 'REAL' : 'TEXT'; + db.run(`ALTER TABLE timesheet_entries ADD COLUMN ${col} ${colType}`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + }); + }); + + // Wöchentliche Stundenzettel-Tabelle + db.run(`CREATE TABLE IF NOT EXISTS weekly_timesheets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + week_start TEXT NOT NULL, + week_end TEXT NOT NULL, + version INTEGER DEFAULT 1, + status TEXT DEFAULT 'eingereicht', + submitted_at DATETIME DEFAULT CURRENT_TIMESTAMP, + reviewed_by INTEGER, + reviewed_at DATETIME, + pdf_downloaded_at DATETIME, + pdf_downloaded_by INTEGER, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (reviewed_by) REFERENCES users(id), + FOREIGN KEY (pdf_downloaded_by) REFERENCES users(id) + )`); + + // Migration: version Spalte hinzufügen falls sie nicht existiert + db.run(`ALTER TABLE weekly_timesheets ADD COLUMN version INTEGER DEFAULT 1`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + // Wenn Spalte neu erstellt wurde, bestehende Einträge haben automatisch version = 1 + }); + + // Migration: pdf_downloaded_at Spalte hinzufügen falls sie nicht existiert + db.run(`ALTER TABLE weekly_timesheets ADD COLUMN pdf_downloaded_at DATETIME`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + }); + + // Migration: pdf_downloaded_by Spalte hinzufügen falls sie nicht existiert + db.run(`ALTER TABLE weekly_timesheets ADD COLUMN pdf_downloaded_by INTEGER`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + }); + + // Migration: version_reason Spalte hinzufügen + db.run(`ALTER TABLE weekly_timesheets ADD COLUMN version_reason TEXT`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + if (err && !err.message.includes('duplicate column')) { + console.warn('Warnung beim Hinzufügen der Spalte version_reason:', err.message); + } + }); + + // Migration: admin_comment Spalte hinzufügen + db.run(`ALTER TABLE weekly_timesheets ADD COLUMN admin_comment TEXT`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + if (err && !err.message.includes('duplicate column')) { + console.warn('Warnung beim Hinzufügen der Spalte admin_comment:', err.message); + } + }); + + // Migration: Projektnummern für Tätigkeiten hinzufügen + const projectNumberColumns = [ + 'activity1_project_number', 'activity2_project_number', + 'activity3_project_number', 'activity4_project_number', + 'activity5_project_number' + ]; + + projectNumberColumns.forEach(col => { + db.run(`ALTER TABLE timesheet_entries ADD COLUMN ${col} TEXT`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + if (err && !err.message.includes('duplicate column')) { + console.warn(`Warnung beim Hinzufügen der Spalte ${col}:`, err.message); + } + }); + }); + + // Migration: Überstunden und Urlaub hinzufügen + db.run(`ALTER TABLE timesheet_entries ADD COLUMN overtime_taken_hours REAL`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + if (err && !err.message.includes('duplicate column')) { + console.warn('Warnung beim Hinzufügen der Spalte overtime_taken_hours:', err.message); + } + }); + + db.run(`ALTER TABLE timesheet_entries ADD COLUMN vacation_type TEXT`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + if (err && !err.message.includes('duplicate column')) { + console.warn('Warnung beim Hinzufügen der Spalte vacation_type:', err.message); + } + }); + + // Migration: Krank-Status hinzufügen + db.run(`ALTER TABLE timesheet_entries ADD COLUMN sick_status INTEGER DEFAULT 0`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + if (err && !err.message.includes('duplicate column')) { + console.warn('Warnung beim Hinzufügen der Spalte sick_status:', err.message); + } + }); + + // Migration: Pausen-Zeiten für API-Zeiterfassung hinzufügen + db.run(`ALTER TABLE timesheet_entries ADD COLUMN pause_start_time TEXT`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + if (err && !err.message.includes('duplicate column')) { + console.warn('Warnung beim Hinzufügen der Spalte pause_start_time:', err.message); + } + }); + + db.run(`ALTER TABLE timesheet_entries ADD COLUMN pause_end_time TEXT`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + if (err && !err.message.includes('duplicate column')) { + console.warn('Warnung beim Hinzufügen der Spalte pause_end_time:', err.message); + } + }); + + // Migration: User-Felder hinzufügen (Personalnummer, Wochenstunden, Urlaubstage) + db.run(`ALTER TABLE users ADD COLUMN personalnummer TEXT`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + }); + + db.run(`ALTER TABLE users ADD COLUMN wochenstunden REAL`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + }); + + db.run(`ALTER TABLE users ADD COLUMN urlaubstage REAL`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + }); + + // Migration: Überstunden-Offset (manuelle Korrektur durch Verwaltung) + db.run(`ALTER TABLE users ADD COLUMN overtime_offset_hours REAL DEFAULT 0`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + if (err && !err.message.includes('duplicate column')) { + console.warn('Warnung beim Hinzufügen der Spalte overtime_offset_hours:', err.message); + } + }); + + // Migration: ping_ip Spalte hinzufügen + db.run(`ALTER TABLE users ADD COLUMN ping_ip TEXT`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + }); + + // Ping-Status-Tabelle für IP-basierte Zeiterfassung + db.run(`CREATE TABLE IF NOT EXISTS ping_status ( + user_id INTEGER NOT NULL, + date TEXT NOT NULL, + last_successful_ping DATETIME, + failed_ping_count INTEGER DEFAULT 0, + start_time_set INTEGER DEFAULT 0, + first_failed_ping_time DATETIME, + PRIMARY KEY (user_id, date), + FOREIGN KEY (user_id) REFERENCES users(id) + )`, (err) => { + if (err && !err.message.includes('duplicate column')) { + console.warn('Warnung beim Erstellen der ping_status Tabelle:', err.message); + } + }); + + // LDAP-Konfiguration-Tabelle + db.run(`CREATE TABLE IF NOT EXISTS ldap_config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + enabled INTEGER DEFAULT 0, + url TEXT, + bind_dn TEXT, + bind_password TEXT, + base_dn TEXT, + user_search_filter TEXT, + username_attribute TEXT DEFAULT 'cn', + firstname_attribute TEXT DEFAULT 'givenName', + lastname_attribute TEXT DEFAULT 'sn', + sync_interval INTEGER DEFAULT 0, + last_sync DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`); + + // LDAP-Sync-Log-Tabelle + db.run(`CREATE TABLE IF NOT EXISTS ldap_sync_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sync_type TEXT NOT NULL, + status TEXT NOT NULL, + users_synced INTEGER DEFAULT 0, + error_message TEXT, + sync_started_at DATETIME DEFAULT CURRENT_TIMESTAMP, + sync_completed_at DATETIME + )`); + + // Feiertage (öffentliche Feiertage BW) – API wird nur 1x pro Jahr aufgerufen + db.run(`CREATE TABLE IF NOT EXISTS public_holidays ( + date TEXT PRIMARY KEY, + name TEXT + )`); + + // 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) => { + if (!err && users) { + users.forEach(user => { + let roleValue = user.role; + // Prüfe ob es bereits ein JSON-Array ist + try { + const parsed = JSON.parse(roleValue); + // Wenn erfolgreich geparst und es ist ein Array, nichts tun + if (Array.isArray(parsed)) { + return; // Bereits JSON-Array + } + } catch (e) { + // Nicht JSON, konvertiere zu JSON-Array + } + + // Konvertiere zu JSON-Array + const roleArray = JSON.stringify([roleValue]); + db.run('UPDATE users SET role = ? WHERE id = ?', [roleArray, user.id], (err) => { + if (err) { + console.warn(`Warnung beim Konvertieren der Rolle für User ${user.id}:`, err.message); + } + }); + }); + } + }); + + // Standard Admin-Benutzer erstellen + const adminPassword = bcrypt.hashSync('admin123', 10); + db.run(`INSERT OR IGNORE INTO users (id, username, password, firstname, lastname, role) + VALUES (1, 'admin', ?, 'System', 'Administrator', ?)`, + [adminPassword, JSON.stringify(['admin'])]); + + // Standard Verwaltungs-Benutzer erstellen + const verwaltungPassword = bcrypt.hashSync('verwaltung123', 10); + db.run(`INSERT OR IGNORE INTO users (id, username, password, firstname, lastname, role) + VALUES (2, 'verwaltung', ?, 'Verwaltung', 'User', ?)`, + [verwaltungPassword, JSON.stringify(['verwaltung'])]); + }); +} + +module.exports = { db, initDatabase }; diff --git a/dev/ldapserver/docker-compose.yml b/dev/ldapserver/docker-compose.yml new file mode 100644 index 0000000..e773fa1 --- /dev/null +++ b/dev/ldapserver/docker-compose.yml @@ -0,0 +1,44 @@ +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 # 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 # Same for reply-to, optional. \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..792f05a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + stundenerfassung: + build: . + container_name: stundenerfassung + ports: + - "3333:3333" # Hauptserver + - "3334:3334" # Check-in Server + volumes: + # Datenbank: Verzeichnis mounten (nicht einzelne Datei – sonst erzeugt Docker ein Verzeichnis) + - ./data:/app/data + environment: + - NODE_ENV=production + - DB_PATH=/app/data/stundenerfassung.db + - TZ=Europe/Berlin + restart: unless-stopped diff --git a/email-mitarbeiter-stundenerfassung.txt b/email-mitarbeiter-stundenerfassung.txt new file mode 100644 index 0000000..1377e05 --- /dev/null +++ b/email-mitarbeiter-stundenerfassung.txt @@ -0,0 +1,18 @@ +Test - Stundenerfassung + +Hallo zusammen, + +Mara ist auf mich mit einer Bitte herangetreten, ob ich die Stundenerfassung digitalisieren kann. +Das habe ich die letzten 2 Wochen am abend und am WE gemacht. + +Ich gleube, dass das System jetzt fest fertig ist und ihr es testen könnt +Der test soll kleinere Fehler finden und mir noch die möglichkeit geben diese dann zu beheben. + +Am Montag würde ich gerne eine kurze Einführung für die Leute im Büro geben. +Um ca. 11:00 Uhr für so 10-15 Minuten. + +Achtet bitte auf die Überstundenerechnung, da könnte noch der ein oder andere Fehler drin sein. + +Viele Grüße +Carsten Graf + diff --git a/helpers/utils.js b/helpers/utils.js new file mode 100644 index 0000000..f3bfb61 --- /dev/null +++ b/helpers/utils.js @@ -0,0 +1,131 @@ +// Helper-Funktionen für das Stundenerfassungs-System + +// Helper: Prüft ob User eine bestimmte Rolle hat +function hasRole(req, role) { + if (!req.session.roles || !Array.isArray(req.session.roles)) { + return false; + } + return req.session.roles.includes(role); +} + +// Helper: Bestimmt die Standard-Rolle (höchste Priorität: admin > verwaltung > mitarbeiter) +function getDefaultRole(roles) { + if (!Array.isArray(roles) || roles.length === 0) { + return 'mitarbeiter'; + } + if (roles.includes('admin')) return 'admin'; + if (roles.includes('verwaltung')) return 'verwaltung'; + return roles[0]; // Fallback auf erste Rolle +} + +// Helper: Gibt aktuelles Datum als YYYY-MM-DD zurück +function getCurrentDate() { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +// Helper: Gibt aktuelle Zeit als HH:MM zurück +function getCurrentTime() { + const now = new Date(); + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + return `${hours}:${minutes}`; +} + +// Helper: Berechnet Pausenzeit in Minuten zwischen zwei Zeiten +function calculateBreakMinutes(pauseStart, pauseEnd) { + if (!pauseStart || !pauseEnd) return 0; + + const [startHours, startMinutes] = pauseStart.split(':').map(Number); + const [endHours, endMinutes] = pauseEnd.split(':').map(Number); + + const startTotalMinutes = startHours * 60 + startMinutes; + const endTotalMinutes = endHours * 60 + endMinutes; + + return endTotalMinutes - startTotalMinutes; +} + +// Helper: Berechnet total_hours basierend auf start_time, end_time und break_minutes +function updateTotalHours(startTime, endTime, breakMinutes) { + if (!startTime || !endTime) return 0; + + const [startHours, startMinutes] = startTime.split(':').map(Number); + const [endHours, endMinutes] = endTime.split(':').map(Number); + + const startTotalMinutes = startHours * 60 + startMinutes; + const endTotalMinutes = endHours * 60 + endMinutes; + + const totalMinutes = endTotalMinutes - startTotalMinutes - (breakMinutes || 0); + return totalMinutes / 60; // Konvertiere zu Stunden +} + +// Helper: Formatiert Datum für Anzeige (DD.MM.YYYY) +function formatDate(dateStr) { + const date = new Date(dateStr); + return date.toLocaleDateString('de-DE'); +} + +// Helper: Formatiert Datum und Zeit für Anzeige +function formatDateTime(dateStr) { + const date = new Date(dateStr); + return date.toLocaleString('de-DE'); +} + +// Helper: Berechnet Kalenderwoche aus einem Datum (ISO 8601) +function getCalendarWeek(dateStr) { + const date = new Date(dateStr); + const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); + const dayNum = d.getUTCDay() || 7; + d.setUTCDate(d.getUTCDate() + 4 - dayNum); + const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7); + return weekNo; +} + +// Helper: Berechnet week_start (Montag) und week_end (Sonntag) aus Jahr und Kalenderwoche (ISO 8601) +function getWeekDatesFromCalendarWeek(year, weekNumber) { + // ISO 8601: Woche beginnt am Montag, erste Woche enthält den 4. Januar + const jan4 = new Date(Date.UTC(year, 0, 4)); + const jan4Day = jan4.getUTCDay() || 7; // 1 = Montag, 7 = Sonntag + const daysToMonday = jan4Day === 1 ? 0 : 1 - jan4Day; + + // Montag der ersten Woche + const firstMonday = new Date(Date.UTC(year, 0, 4 + daysToMonday)); + + // Montag der gewünschten Woche (Woche 1 = erste Woche) + const weekMonday = new Date(firstMonday); + weekMonday.setUTCDate(firstMonday.getUTCDate() + (weekNumber - 1) * 7); + + // Sonntag der Woche (6 Tage nach Montag) + const weekSunday = new Date(weekMonday); + weekSunday.setUTCDate(weekMonday.getUTCDate() + 6); + + // Format: YYYY-MM-DD + const formatDate = (date) => { + const y = date.getUTCFullYear(); + const m = String(date.getUTCMonth() + 1).padStart(2, '0'); + const d = String(date.getUTCDate()).padStart(2, '0'); + return `${y}-${m}-${d}`; + }; + + return { + week_start: formatDate(weekMonday), + week_end: formatDate(weekSunday) + }; +} + +module.exports = { + hasRole, + getDefaultRole, + getCurrentDate, + getCurrentTime, + calculateBreakMinutes, + updateTotalHours, + formatDate, + formatDateTime, + getCalendarWeek, + getWeekDatesFromCalendarWeek +}; diff --git a/ldap-service.js b/ldap-service.js new file mode 100644 index 0000000..0993fd8 --- /dev/null +++ b/ldap-service.js @@ -0,0 +1,402 @@ +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; diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..cbda134 --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,48 @@ +// Authentifizierungs-Middleware + +const { hasRole } = require('../helpers/utils'); + +// Middleware: Authentifizierung prüfen +function requireAuth(req, res, next) { + if (req.session.userId) { + next(); + } else { + res.redirect('/login'); + } +} + +// Middleware: Prüft ob User eine bestimmte Rolle hat +function requireRole(role) { + return (req, res, next) => { + if (req.session.userId && hasRole(req, role)) { + next(); + } else { + res.status(403).send('Zugriff verweigert'); + } + }; +} + +// Middleware: Admin-Rolle prüfen +function requireAdmin(req, res, next) { + if (req.session.userId && hasRole(req, 'admin')) { + next(); + } else { + res.status(403).send('Zugriff verweigert'); + } +} + +// Middleware: Verwaltung-Rolle prüfen (Verwaltung oder Admin) +function requireVerwaltung(req, res, next) { + if (req.session.userId && (hasRole(req, 'verwaltung') || hasRole(req, 'admin'))) { + next(); + } else { + res.status(403).send('Zugriff verweigert'); + } +} + +module.exports = { + requireAuth, + requireRole, + requireAdmin, + requireVerwaltung +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..25996ce --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7431 @@ +{ + "name": "stundenerfassung", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "stundenerfassung", + "version": "1.0.0", + "dependencies": { + "archiver": "^7.0.1", + "bcryptjs": "^2.4.3", + "body-parser": "^1.20.2", + "ejs": "^3.1.9", + "express": "^4.18.2", + "express-session": "^1.17.3", + "ldapjs": "^3.0.7", + "node-cron": "^3.0.3", + "pdfkit": "^0.13.0", + "ping": "^0.4.4", + "sqlite3": "^5.1.6" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@ldapjs/asn1": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-2.0.0.tgz", + "integrity": "sha512-G9+DkEOirNgdPmD0I8nu57ygQJKOOgFEMKknEuQvIHbGLwP3ny1mY+OTUYLCbCaGJP4sox5eYgBJRuSUpnAddA==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md" + }, + "node_modules/@ldapjs/attribute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@ldapjs/attribute/-/attribute-1.0.0.tgz", + "integrity": "sha512-ptMl2d/5xJ0q+RgmnqOi3Zgwk/TMJYG7dYMC0Keko+yZU6n+oFM59MjQOUht5pxJeS4FWrImhu/LebX24vJNRQ==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", + "dependencies": { + "@ldapjs/asn1": "2.0.0", + "@ldapjs/protocol": "^1.2.1", + "process-warning": "^2.1.0" + } + }, + "node_modules/@ldapjs/change": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@ldapjs/change/-/change-1.0.0.tgz", + "integrity": "sha512-EOQNFH1RIku3M1s0OAJOzGfAohuFYXFY4s73wOhRm4KFGhmQQ7MChOh2YtYu9Kwgvuq1B0xKciXVzHCGkB5V+Q==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", + "dependencies": { + "@ldapjs/asn1": "2.0.0", + "@ldapjs/attribute": "1.0.0" + } + }, + "node_modules/@ldapjs/controls": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@ldapjs/controls/-/controls-2.1.0.tgz", + "integrity": "sha512-2pFdD1yRC9V9hXfAWvCCO2RRWK9OdIEcJIos/9cCVP9O4k72BY1bLDQQ4KpUoJnl4y/JoD4iFgM+YWT3IfITWw==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", + "dependencies": { + "@ldapjs/asn1": "^1.2.0", + "@ldapjs/protocol": "^1.2.1" + } + }, + "node_modules/@ldapjs/controls/node_modules/@ldapjs/asn1": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-1.2.0.tgz", + "integrity": "sha512-KX/qQJ2xxzvO2/WOvr1UdQ+8P5dVvuOLk/C9b1bIkXxZss8BaR28njXdPgFCpj5aHaf1t8PmuVnea+N9YG9YMw==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md" + }, + "node_modules/@ldapjs/dn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ldapjs/dn/-/dn-1.1.0.tgz", + "integrity": "sha512-R72zH5ZeBj/Fujf/yBu78YzpJjJXG46YHFo5E4W1EqfNpo1UsVPqdLrRMXeKIsJT3x9dJVIfR6OpzgINlKpi0A==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", + "dependencies": { + "@ldapjs/asn1": "2.0.0", + "process-warning": "^2.1.0" + } + }, + "node_modules/@ldapjs/filter": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@ldapjs/filter/-/filter-2.1.1.tgz", + "integrity": "sha512-TwPK5eEgNdUO1ABPBUQabcZ+h9heDORE4V9WNZqCtYLKc06+6+UAJ3IAbr0L0bYTnkkWC/JEQD2F+zAFsuikNw==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", + "dependencies": { + "@ldapjs/asn1": "2.0.0", + "@ldapjs/protocol": "^1.2.1", + "process-warning": "^2.1.0" + } + }, + "node_modules/@ldapjs/messages": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ldapjs/messages/-/messages-1.3.0.tgz", + "integrity": "sha512-K7xZpXJ21bj92jS35wtRbdcNrwmxAtPwy4myeh9duy/eR3xQKvikVycbdWVzkYEAVE5Ce520VXNOwCHjomjCZw==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", + "dependencies": { + "@ldapjs/asn1": "^2.0.0", + "@ldapjs/attribute": "^1.0.0", + "@ldapjs/change": "^1.0.0", + "@ldapjs/controls": "^2.1.0", + "@ldapjs/dn": "^1.1.0", + "@ldapjs/filter": "^2.1.1", + "@ldapjs/protocol": "^1.2.1", + "process-warning": "^2.2.0" + } + }, + "node_modules/@ldapjs/protocol": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@ldapjs/protocol/-/protocol-1.2.1.tgz", + "integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md" + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@swc/helpers": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.3.17.tgz", + "integrity": "sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "optional": true + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "optional": true + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/backoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", + "integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==", + "dependencies": { + "precond": "0.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "devOptional": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fontkit": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.9.0.tgz", + "integrity": "sha512-HkW/8Lrk8jl18kzQHvAw9aTHe1cqsyx5sDnxncx652+CIfhawokEPkeM3BoIC+z/Xv7a0yMr0f3pRRwhGH455g==", + "dependencies": { + "@swc/helpers": "^0.3.13", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "deep-equal": "^2.0.5", + "dfa": "^1.2.0", + "restructure": "^2.0.1", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.3.1", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "optional": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "optional": true + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "optional": true + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "optional": true + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/ldapjs": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-3.0.7.tgz", + "integrity": "sha512-1ky+WrN+4CFMuoekUOv7Y1037XWdjKpu0xAPwSP+9KdvmV9PG+qOKlssDV6a+U32apwxdD3is/BZcWOYzN30cg==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", + "dependencies": { + "@ldapjs/asn1": "^2.0.0", + "@ldapjs/attribute": "^1.0.0", + "@ldapjs/change": "^1.0.0", + "@ldapjs/controls": "^2.1.0", + "@ldapjs/dn": "^1.1.0", + "@ldapjs/filter": "^2.1.1", + "@ldapjs/messages": "^1.3.0", + "@ldapjs/protocol": "^1.2.1", + "abstract-logging": "^2.0.1", + "assert-plus": "^1.0.0", + "backoff": "^2.5.0", + "once": "^1.4.0", + "vasync": "^2.2.1", + "verror": "^1.10.1" + } + }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" + }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/pdfkit": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.13.0.tgz", + "integrity": "sha512-AW79eHU5eLd2vgRDS9z3bSoi0FA+gYm+100LLosrQQMLUzOBGVOhG7ABcMFpJu7Bpg+MT74XYHi4k9EuU/9EZw==", + "dependencies": { + "crypto-js": "^4.0.0", + "fontkit": "^1.8.1", + "linebreak": "^1.0.2", + "png-js": "^1.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/ping": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/ping/-/ping-0.4.4.tgz", + "integrity": "sha512-56ZMC0j7SCsMMLdOoUg12VZCfj/+ZO+yfOSjaNCRrmZZr6GLbN2X/Ui56T15dI8NhiHckaw5X2pvyfAomanwqQ==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/precond": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", + "integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/process-warning": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz", + "integrity": "sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==" + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/restructure": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-2.0.1.tgz", + "integrity": "sha512-e0dOpjm5DseomnXx2M5lpdZ5zoHqF1+bqdMJUohoYVVQa7cBdnk7fdmeI6byNWP/kiME72EeTiSypTCVnpLiDg==" + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "optional": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "optional": true, + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "optional": true + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vasync": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz", + "integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "verror": "1.10.0" + } + }, + "node_modules/vasync/node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + } + }, + "dependencies": { + "@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==" + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "requires": { + "ansi-regex": "^6.0.1" + } + } + } + }, + "@ldapjs/asn1": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-2.0.0.tgz", + "integrity": "sha512-G9+DkEOirNgdPmD0I8nu57ygQJKOOgFEMKknEuQvIHbGLwP3ny1mY+OTUYLCbCaGJP4sox5eYgBJRuSUpnAddA==" + }, + "@ldapjs/attribute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@ldapjs/attribute/-/attribute-1.0.0.tgz", + "integrity": "sha512-ptMl2d/5xJ0q+RgmnqOi3Zgwk/TMJYG7dYMC0Keko+yZU6n+oFM59MjQOUht5pxJeS4FWrImhu/LebX24vJNRQ==", + "requires": { + "@ldapjs/asn1": "2.0.0", + "@ldapjs/protocol": "^1.2.1", + "process-warning": "^2.1.0" + } + }, + "@ldapjs/change": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@ldapjs/change/-/change-1.0.0.tgz", + "integrity": "sha512-EOQNFH1RIku3M1s0OAJOzGfAohuFYXFY4s73wOhRm4KFGhmQQ7MChOh2YtYu9Kwgvuq1B0xKciXVzHCGkB5V+Q==", + "requires": { + "@ldapjs/asn1": "2.0.0", + "@ldapjs/attribute": "1.0.0" + } + }, + "@ldapjs/controls": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@ldapjs/controls/-/controls-2.1.0.tgz", + "integrity": "sha512-2pFdD1yRC9V9hXfAWvCCO2RRWK9OdIEcJIos/9cCVP9O4k72BY1bLDQQ4KpUoJnl4y/JoD4iFgM+YWT3IfITWw==", + "requires": { + "@ldapjs/asn1": "^1.2.0", + "@ldapjs/protocol": "^1.2.1" + }, + "dependencies": { + "@ldapjs/asn1": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-1.2.0.tgz", + "integrity": "sha512-KX/qQJ2xxzvO2/WOvr1UdQ+8P5dVvuOLk/C9b1bIkXxZss8BaR28njXdPgFCpj5aHaf1t8PmuVnea+N9YG9YMw==" + } + } + }, + "@ldapjs/dn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ldapjs/dn/-/dn-1.1.0.tgz", + "integrity": "sha512-R72zH5ZeBj/Fujf/yBu78YzpJjJXG46YHFo5E4W1EqfNpo1UsVPqdLrRMXeKIsJT3x9dJVIfR6OpzgINlKpi0A==", + "requires": { + "@ldapjs/asn1": "2.0.0", + "process-warning": "^2.1.0" + } + }, + "@ldapjs/filter": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@ldapjs/filter/-/filter-2.1.1.tgz", + "integrity": "sha512-TwPK5eEgNdUO1ABPBUQabcZ+h9heDORE4V9WNZqCtYLKc06+6+UAJ3IAbr0L0bYTnkkWC/JEQD2F+zAFsuikNw==", + "requires": { + "@ldapjs/asn1": "2.0.0", + "@ldapjs/protocol": "^1.2.1", + "process-warning": "^2.1.0" + } + }, + "@ldapjs/messages": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ldapjs/messages/-/messages-1.3.0.tgz", + "integrity": "sha512-K7xZpXJ21bj92jS35wtRbdcNrwmxAtPwy4myeh9duy/eR3xQKvikVycbdWVzkYEAVE5Ce520VXNOwCHjomjCZw==", + "requires": { + "@ldapjs/asn1": "^2.0.0", + "@ldapjs/attribute": "^1.0.0", + "@ldapjs/change": "^1.0.0", + "@ldapjs/controls": "^2.1.0", + "@ldapjs/dn": "^1.1.0", + "@ldapjs/filter": "^2.1.1", + "@ldapjs/protocol": "^1.2.1", + "process-warning": "^2.2.0" + } + }, + "@ldapjs/protocol": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@ldapjs/protocol/-/protocol-1.2.1.tgz", + "integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ==" + }, + "@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "optional": true, + "requires": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "optional": true, + "requires": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true + }, + "@swc/helpers": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.3.17.tgz", + "integrity": "sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==", + "requires": { + "tslib": "^2.4.0" + } + }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "optional": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "optional": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "optional": true + } + } + }, + "agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "optional": true, + "requires": { + "humanize-ms": "^1.2.1" + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "optional": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==" + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "optional": true + }, + "archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "requires": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "requires": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + } + } + }, + "archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "requires": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" + }, + "readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + } + } + }, + "are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "requires": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" + }, + "async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, + "available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "requires": { + "possible-typed-array-names": "^1.0.0" + } + }, + "b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "requires": {} + }, + "backoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", + "integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==", + "requires": { + "precond": "0.2" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "requires": {} + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, + "binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "requires": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + } + }, + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "requires": { + "base64-js": "^1.1.2" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==" + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "optional": true, + "requires": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + } + }, + "call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "requires": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + } + }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + } + }, + "chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "optional": true + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==" + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true + }, + "compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "requires": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "devOptional": true + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + }, + "cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, + "crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==" + }, + "crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "requires": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + } + } + }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, + "deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "requires": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==" + }, + "dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==" + }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "requires": { + "jake": "^10.8.5" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "requires": { + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "requires": { + "once": "^1.4.0" + } + }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true + }, + "err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true + }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + } + }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "requires": { + "bare-events": "^2.7.0" + } + }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + }, + "express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "requires": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + } + }, + "extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==" + }, + "fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "requires": { + "minimatch": "^5.0.1" + } + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + } + }, + "fontkit": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.9.0.tgz", + "integrity": "sha512-HkW/8Lrk8jl18kzQHvAw9aTHe1cqsyx5sDnxncx652+CIfhawokEPkeM3BoIC+z/Xv7a0yMr0f3pRRwhGH455g==", + "requires": { + "@swc/helpers": "^0.3.13", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "deep-equal": "^2.0.5", + "dfa": "^1.2.0", + "restructure": "^2.0.1", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.3.1", + "unicode-trie": "^2.0.0" + } + }, + "for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "requires": { + "is-callable": "^1.2.7" + } + }, + "foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "requires": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + } + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "optional": true + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" + }, + "gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "optional": true, + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + } + }, + "get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "requires": { + "has-symbols": "^1.0.3" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, + "http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "optional": true + }, + "http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "requires": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + } + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "optional": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "optional": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "optional": true + } + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "optional": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "optional": true + } + } + }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "optional": true, + "requires": { + "ms": "^2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "optional": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "optional": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "optional": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "requires": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + } + }, + "ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "optional": true + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "requires": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + } + }, + "is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + } + }, + "is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "requires": { + "has-bigints": "^1.0.2" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "requires": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" + }, + "is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "requires": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "optional": true + }, + "is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "requires": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + } + }, + "is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "requires": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, + "is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==" + }, + "is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "requires": { + "call-bound": "^1.0.3" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "requires": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + } + }, + "is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "requires": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + } + }, + "is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==" + }, + "is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "requires": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "requires": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + } + }, + "lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "requires": { + "readable-stream": "^2.0.5" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "ldapjs": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-3.0.7.tgz", + "integrity": "sha512-1ky+WrN+4CFMuoekUOv7Y1037XWdjKpu0xAPwSP+9KdvmV9PG+qOKlssDV6a+U32apwxdD3is/BZcWOYzN30cg==", + "requires": { + "@ldapjs/asn1": "^2.0.0", + "@ldapjs/attribute": "^1.0.0", + "@ldapjs/change": "^1.0.0", + "@ldapjs/controls": "^2.1.0", + "@ldapjs/dn": "^1.1.0", + "@ldapjs/filter": "^2.1.1", + "@ldapjs/messages": "^1.3.0", + "@ldapjs/protocol": "^1.2.1", + "abstract-logging": "^2.0.1", + "assert-plus": "^1.0.0", + "backoff": "^2.5.0", + "once": "^1.4.0", + "vasync": "^2.2.1", + "verror": "^1.10.1" + } + }, + "linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "requires": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + }, + "dependencies": { + "base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==" + } + } + }, + "lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "optional": true, + "requires": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + } + }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "optional": true, + "requires": { + "encoding": "^0.1.12", + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "requires": { + "semver": "^7.3.5" + } + }, + "node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" + }, + "node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "requires": { + "uuid": "8.3.2" + } + }, + "node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "optional": true, + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + } + }, + "nodemon": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "dev": true, + "requires": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "optional": true, + "requires": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + } + }, + "object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" + }, + "object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + } + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "optional": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "optional": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" + } + } + }, + "path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "pdfkit": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.13.0.tgz", + "integrity": "sha512-AW79eHU5eLd2vgRDS9z3bSoi0FA+gYm+100LLosrQQMLUzOBGVOhG7ABcMFpJu7Bpg+MT74XYHi4k9EuU/9EZw==", + "requires": { + "crypto-js": "^4.0.0", + "fontkit": "^1.8.1", + "linebreak": "^1.0.2", + "png-js": "^1.0.0" + } + }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "ping": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/ping/-/ping-0.4.4.tgz", + "integrity": "sha512-56ZMC0j7SCsMMLdOoUg12VZCfj/+ZO+yfOSjaNCRrmZZr6GLbN2X/Ui56T15dI8NhiHckaw5X2pvyfAomanwqQ==" + }, + "png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, + "possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==" + }, + "prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "requires": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + } + }, + "precond": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", + "integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==" + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "process-warning": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz", + "integrity": "sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==" + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "optional": true + }, + "promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "requires": { + "side-channel": "^1.1.0" + } + }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "requires": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "requires": { + "minimatch": "^5.1.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "requires": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + } + }, + "restructure": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-2.0.1.tgz", + "integrity": "sha512-e0dOpjm5DseomnXx2M5lpdZ5zoHqF1+bqdMJUohoYVVQa7cBdnk7fdmeI6byNWP/kiME72EeTiSypTCVnpLiDg==" + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "optional": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==" + }, + "send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "requires": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "optional": true + }, + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + } + }, + "set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "optional": true + }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "optional": true + }, + "socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "optional": true, + "requires": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "optional": true, + "requires": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "dependencies": { + "debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "optional": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "optional": true + } + } + }, + "sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "requires": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "node-gyp": "8.x", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + } + }, + "ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "optional": true, + "requires": { + "minipass": "^3.1.1" + } + }, + "statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" + }, + "stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "requires": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + } + }, + "streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "requires": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" + } + } + }, + "tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + }, + "dependencies": { + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + } + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "requires": { + "b4a": "^1.6.4" + } + }, + "tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true + }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, + "undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "requires": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "requires": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "optional": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "optional": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "vasync": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz", + "integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==", + "requires": { + "verror": "1.10.0" + }, + "dependencies": { + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + } + } + }, + "verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "requires": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + } + }, + "which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "requires": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + } + }, + "which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + } + }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==" + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "requires": { + "ansi-regex": "^6.0.1" + } + } + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "requires": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..91e23b8 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "stundenerfassung", + "version": "1.0.0", + "description": "Stundenerfassungs-System für Mitarbeiter", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js", + "reset-db": "node reset-db.js" + }, + "dependencies": { + "express": "^4.18.2", + "express-session": "^1.17.3", + "bcryptjs": "^2.4.3", + "sqlite3": "^5.1.6", + "body-parser": "^1.20.2", + "ejs": "^3.1.9", + "pdfkit": "^0.13.0", + "ldapjs": "^3.0.7", + "node-cron": "^3.0.3", + "ping": "^0.4.4", + "archiver": "^7.0.1" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..9317247 --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,1054 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background-color: #f5f5f5; + color: #333; + line-height: 1.6; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +.dashboard-container { + max-width: 100% !important; + padding: 20px 30px; +} + +/* Navbar */ +.navbar { + background-color: #2c3e50; + color: white; + padding: 15px 0; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.navbar .container { + display: flex; + justify-content: space-between; + align-items: center; + max-width: 100%; + padding: 15px 30px; +} + +.navbar-brand { + display: flex; + align-items: center; + gap: 25px; +} + +.navbar-logo { + height: 40px; + width: auto; + object-fit: contain; +} + +.navbar h1 { + font-size: 24px; + margin: 0; +} + +.nav-right { + display: flex; + align-items: center; + gap: 15px; +} + +/* Buttons */ +.btn { + padding: 10px 20px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + text-decoration: none; + display: inline-block; + transition: background-color 0.3s; +} + +.btn:disabled, +.btn[disabled] { + background-color: #95a5a6; + color: #ecf0f1; + cursor: not-allowed; + opacity: 0.6; +} + +.btn:disabled:hover, +.btn[disabled]:hover { + background-color: #95a5a6; +} + +.btn-primary { + background-color: #3498db; + color: white; +} + +.btn-primary:hover { + background-color: #2980b9; +} + +.btn-secondary { + background-color: #95a5a6; + color: white; +} + +.btn-secondary:hover { + background-color: #7f8c8d; +} + +.btn-success { + background-color: #27ae60; + color: white; +} + +.btn-success:hover { + background-color: #229954; +} + +.btn-success:disabled, +.btn-success[disabled] { + background-color: #95a5a6; + color: #ecf0f1; + cursor: not-allowed; + opacity: 0.6; +} + +.btn-success:disabled:hover, +.btn-success[disabled]:hover { + background-color: #95a5a6; +} + +.btn-danger { + background-color: #e74c3c; + color: white; +} + +.btn-danger:hover { + background-color: #c0392b; +} + +.btn-sm { + padding: 5px 10px; + font-size: 12px; +} + +/* Login Page */ +.login-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.login-box { + background: white; + padding: 40px; + border-radius: 8px; + box-shadow: 0 10px 25px rgba(0,0,0,0.2); + width: 100%; + max-width: 400px; +} + +.login-box h1 { + text-align: center; + color: #2c3e50; + margin-bottom: 10px; +} + +.login-box h2 { + text-align: center; + color: #7f8c8d; + font-size: 18px; + margin-bottom: 30px; + font-weight: normal; +} + +/* Forms */ +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 5px; + color: #555; + font-weight: 500; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: #3498db; +} + +.form-help-text { + display: block; + margin-top: 8px; + color: #666; + font-size: 13px; + font-style: italic; +} + +/* Rollen-Checkboxen Styling */ +.roles-checkbox-group { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 8px; + padding: 15px; + background-color: #f9f9f9; + border: 1px solid #e0e0e0; + border-radius: 6px; +} + +.roles-checkbox-group-inline { + padding: 10px; + gap: 8px; +} + +.role-checkbox-label { + display: flex; + align-items: center; + cursor: pointer; + padding: 10px 12px; + background-color: white; + border: 2px solid #e0e0e0; + border-radius: 6px; + transition: all 0.2s ease; + user-select: none; +} + +.role-checkbox-label:hover { + border-color: #3498db; + background-color: #f0f8ff; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.role-checkbox-label-small { + padding: 6px 10px; + font-size: 13px; +} + +.role-checkbox-input { + width: 18px; + height: 18px; + margin-right: 12px; + cursor: pointer; + accent-color: #3498db; + flex-shrink: 0; +} + +.role-checkbox-input:checked + .role-checkbox-text { + font-weight: 600; + color: #2c3e50; +} + +.role-checkbox-label:has(.role-checkbox-input:checked) { + border-color: #3498db; + background-color: #e8f4f8; + box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1); +} + +.role-checkbox-text { + font-size: 14px; + color: #333; + transition: all 0.2s ease; +} + +.role-checkbox-label-small .role-checkbox-text { + font-size: 13px; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; +} + +/* Error Messages */ +.error-message { + background-color: #e74c3c; + color: white; + padding: 10px; + border-radius: 4px; + margin-bottom: 20px; + text-align: center; +} + +/* Dashboard Layout */ +.dashboard-layout { + display: flex; + gap: 20px; + align-items: flex-start; +} + +/* Dashboard */ +.dashboard { + background: white; + padding: 30px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + flex: 1; + min-width: 0; /* Ermöglicht Flexbox-Shrinking */ +} + +/* User Stats Panel */ +.user-stats-panel { + background: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + width: 280px; + flex-shrink: 0; + box-sizing: border-box; + overflow: hidden; +} + +.user-stats-panel h3 { + margin-top: 0; + margin-bottom: 15px; + color: #2c3e50; + font-size: 18px; + font-weight: 600; +} + +.stat-card { + background: #f8f9fa; + padding: 15px; + border-radius: 6px; + margin-bottom: 12px; + border-left: 4px solid #3498db; + box-sizing: border-box; + width: 100%; + overflow: hidden; +} + +.stat-card.stat-vacation { + border-left-color: #27ae60; +} + +.stat-card.stat-planned { + border-left-color: #f39c12; +} + +.stat-label { + font-size: 12px; + color: #666; + margin-bottom: 6px; + font-weight: 500; +} + +.stat-value { + font-size: 24px; + font-weight: bold; + color: #2c3e50; + margin-bottom: 4px; +} + +.stat-unit { + font-size: 11px; + color: #999; +} + +.week-selector { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; +} + +.week-selector h2 { + flex: 1; + text-align: center; + color: #2c3e50; +} + +/* Timesheet Table */ +.timesheet-grid { + overflow-x: auto; +} + +table { + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; +} + +table th, +table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #ddd; +} + +table th { + background-color: #f8f9fa; + font-weight: 600; + color: #555; +} + +table tr:hover { + background-color: #f8f9fa; +} + +table input[type="time"], +table input[type="number"], +table input[type="text"] { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; +} + +/* Summary */ +.summary { + background-color: #ecf0f1; + padding: 20px; + border-radius: 4px; + margin-bottom: 20px; +} + +.summary-item { + font-size: 18px; + margin-bottom: 10px; +} + +.summary-item strong { + margin-right: 5px; +} + +/* Actions */ +.actions { + text-align: center; +} + +.actions .help-text { + margin-top: 10px; + color: #7f8c8d; + font-size: 14px; +} + +/* Admin Panel */ +.admin-panel { + background: white; + padding: 30px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + width: 100%; +} + +/* Admin Container - volle Breite */ +.admin-container .container { + max-width: 100%; + padding: 20px; +} + +.add-user-form { + background-color: #f8f9fa; + padding: 20px; + border-radius: 4px; + margin-bottom: 30px; +} + +.add-user-form h3 { + margin-bottom: 20px; + color: #2c3e50; +} + +/* Role Badges */ +.role-badge { + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + text-transform: capitalize; +} + +.role-admin { + background-color: #e74c3c; + color: white; +} + +.role-verwaltung { + background-color: #3498db; + color: white; +} + +.role-mitarbeiter { + background-color: #27ae60; + color: white; +} + +/* Status Badges */ +.status-badge { + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.status-eingereicht { + background-color: #f39c12; + color: white; +} + +.status-genehmigt { + background-color: #27ae60; + color: white; +} + +.status-abgelehnt { + background-color: #e74c3c; + color: white; +} + +/* Version Badge */ +.version-badge { + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + background-color: #6c757d; + color: white; + display: inline-flex; + align-items: center; + gap: 5px; + margin-right: 10px; +} + +.version-info { + font-size: 11px; + opacity: 0.9; + font-weight: normal; +} + +/* PDF Download Marker */ +.pdf-downloaded-marker { + display: inline-block; + padding: 4px 10px; + border-radius: 12px; + font-size: 11px; + font-weight: 500; + background-color: #27ae60; + color: white; + margin-left: 5px; +} + +.pdf-not-downloaded-marker { + display: inline-block; + padding: 4px 10px; + border-radius: 12px; + font-size: 11px; + font-weight: 500; + background-color: #e0e0e0; + color: #7f8c8d; + margin-left: 5px; +} + +/* Verwaltung Container - Volle Breite */ +.verwaltung-container { + max-width: 100% !important; + padding: 20px 30px; +} + +/* Verwaltung Panel */ +.verwaltung-panel { + background: white; + padding: 30px 40px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + width: 100%; +} + +.empty-state { + text-align: center; + padding: 40px; + color: #7f8c8d; +} + +/* Timesheet Groups */ +.timesheet-groups { + display: flex; + flex-direction: column; + gap: 25px; + width: 100%; +} + +.timesheet-group { + border: 1px solid #e0e0e0; + border-radius: 8px; + overflow: hidden; + background-color: #fff; +} + +.group-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + background-color: #f8f9fa; + border-bottom: 2px solid #e0e0e0; + cursor: pointer; +} + +.group-header:hover { + background-color: #e9ecef; +} + +.group-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 5px; +} + +.group-employee { + font-size: 16px; + color: #2c3e50; +} + +.group-week { + font-size: 14px; + color: #555; +} + +.group-versions-info { + font-size: 12px; + color: #7f8c8d; +} + +.version-count { + display: inline-block; + padding: 2px 8px; + background-color: #3498db; + color: white; + border-radius: 12px; + font-size: 11px; + font-weight: 600; +} + +.toggle-versions-btn { + display: flex; + align-items: center; + gap: 5px; +} + +.toggle-icon { + font-size: 12px; + transition: transform 0.2s ease; +} + +.toggle-versions-btn.active .toggle-icon { + transform: rotate(180deg); +} + +.versions-container { + padding: 0; + background-color: #fff; + width: 100%; + overflow-x: auto; +} + +.versions-table { + margin: 0; + border-top: none; + width: 100%; + table-layout: auto; +} + +.versions-table th, +.versions-table td { + padding: 15px 25px; +} + +.versions-table tbody tr:hover { + background-color: #f8f9fa; +} + +/* Timesheet Table */ +.timesheet-table { + width: 100%; + border-collapse: collapse; + margin-top: 20px; + table-layout: auto; +} + +.timesheet-table th, +.timesheet-table td { + padding: 15px 20px; + text-align: left; + border-bottom: 1px solid #e0e0e0; +} + +.timesheet-table th { + background-color: #f8f9fa; + font-weight: 600; + color: #2c3e50; +} + +.timesheet-table tr:hover { + background-color: #f8f9fa; +} + +/* PDF Preview */ +.pdf-preview-row { + background-color: #f8f9fa; +} + +.pdf-preview-row td { + padding: 0 !important; +} + +.pdf-preview-container { + padding: 20px; + background-color: white; + border-top: 2px solid #3498db; +} + +.pdf-preview-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + padding-bottom: 15px; + border-bottom: 1px solid #e0e0e0; +} + +.pdf-preview-header h3 { + margin: 0; + color: #2c3e50; + font-size: 18px; +} + +.pdf-viewer-wrapper { + position: relative; + width: 100%; + min-height: 800px; + border: 1px solid #ddd; + border-radius: 4px; + background-color: #f5f5f5; +} + +.pdf-iframe { + width: 100%; + height: 800px; + border: none; + border-radius: 4px; + background-color: white; + display: block; +} + +.pdf-fallback { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + z-index: 1; +} + +.pdf-iframe:not([src=""]) ~ .pdf-fallback { + display: none; +} + +/* Toggle PDF Button */ +.toggle-pdf-btn { + margin-right: 10px; + display: inline-flex; + align-items: center; + gap: 5px; +} + +.toggle-pdf-btn .arrow-icon { + display: inline-block; + transition: transform 0.3s ease; + font-size: 12px; +} + +.toggle-pdf-btn.active .arrow-icon { + transform: rotate(90deg); +} + +.btn-info { + background-color: #17a2b8; + color: white; +} + +.btn-info:hover { + background-color: #138496; +} + +/* Utility Classes */ +.text-muted { + color: #7f8c8d; +} + +/* Activities/Tätigkeiten */ +.activities-row { + background-color: #f8f9fa; + border-top: 2px solid #e0e0e0; +} + +.activities-cell { + padding: 15px !important; +} + +.activities-form { + display: flex; + flex-direction: column; + gap: 10px; +} + +.activities-header { + margin-bottom: 8px; + color: #2c3e50; + font-size: 14px; +} + +.activity-row { + display: grid; + grid-template-columns: 1fr 150px 120px; + gap: 15px; + align-items: center; +} + +.activity-desc { + display: flex; + align-items: center; +} + +.activity-hours { + display: flex; + align-items: center; + gap: 5px; +} + +.activity-input, +.activity-hours-input { + padding: 8px 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; +} + +.activity-input { + width: 100%; +} + +.activity-hours-input { + width: 80px; +} + +.activity-hours-label { + font-size: 14px; + color: #555; +} + +.activity-project { + display: flex; + align-items: center; +} + +.activity-project-input { + padding: 8px 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + width: 100%; +} + +.overtime-vacation-controls { + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid #eee; + display: flex; + gap: 15px; + align-items: center; + flex-wrap: wrap; +} + +.overtime-control, +.vacation-control { + display: flex; + align-items: center; + gap: 5px; +} + +.overtime-input, +.vacation-select { + padding: 6px 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; +} + +.user-field-display { + display: inline-block; +} + +.user-field-edit { + display: none; +} + +/* Mitarbeiter-Gruppe (Level 1) */ +.employee-group { + background-color: white; + border: 1px solid #ddd; + border-radius: 8px; + padding: 25px 30px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + margin-bottom: 20px; +} + +.employee-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding-bottom: 20px; + border-bottom: 2px solid #3498db; + margin-bottom: 20px; +} + +.employee-info { + flex: 1; +} + +.employee-name { + font-size: 18px; + color: #2c3e50; + margin-bottom: 10px; +} + +.employee-details { + font-size: 14px; + color: #666; +} + +.toggle-employee-btn { + display: flex; + align-items: center; + gap: 5px; +} + +/* Wochen-Container (Level 2) */ +.weeks-container { + margin-top: 20px; + padding-left: 25px; + border-left: 3px solid #e0e0e0; + width: 100%; +} + +.week-group { + background-color: #f8f9fa; + border: 1px solid #e0e0e0; + border-radius: 6px; + padding: 20px 25px; + margin-bottom: 20px; +} + +.week-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding-bottom: 15px; + border-bottom: 1px solid #ddd; + margin-bottom: 15px; +} + +.week-info { + flex: 1; +} + +.week-dates { + font-size: 16px; + color: #2c3e50; + margin-bottom: 10px; +} + +.week-versions-info { + font-size: 12px; + color: #7f8c8d; + margin-top: 5px; +} + +/* Responsive */ +@media (max-width: 1024px) { + .dashboard-layout { + flex-direction: column; + } + + .user-stats-panel { + width: 100%; + display: flex; + gap: 15px; + flex-wrap: wrap; + } + + .stat-card { + flex: 1; + min-width: 200px; + margin-bottom: 0; + } +} + +@media (max-width: 768px) { + .dashboard-container { + padding: 15px; + } + + .form-row { + grid-template-columns: 1fr; + } + + .week-selector { + flex-direction: column; + gap: 15px; + } + + table { + font-size: 12px; + } + + table th, + table td { + padding: 8px; + } + + .user-stats-panel { + flex-direction: column; + } + + .stat-card { + width: 100%; + } + + .activity-row { + grid-template-columns: 1fr; + } + + .overtime-vacation-controls { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/public/images/favicon.png b/public/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c07aa8793cbb16f446f8c8a8f63a393b8223e0ef GIT binary patch literal 46071 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
    nTNEcLr10v{PYGW?-zU(icB#>heb=pYE-e*B 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¥4gv*fMnp4UX{x&lLVIqLphyb8blanj3Zp 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 literal 0 HcmV?d00001 diff --git a/public/js/admin.js b/public/js/admin.js new file mode 100644 index 0000000..db21727 --- /dev/null +++ b/public/js/admin.js @@ -0,0 +1,342 @@ +// Admin JavaScript + +document.addEventListener('DOMContentLoaded', function() { + // Benutzer-Formular + const form = document.getElementById('addUserForm'); + + form.addEventListener('submit', async function(e) { + e.preventDefault(); + + // Rollen aus Checkboxen sammeln + const roleCheckboxes = document.querySelectorAll('input[name="roles"]:checked'); + const roles = Array.from(roleCheckboxes).map(cb => cb.value); + + // Validierung: Mindestens eine Rolle muss ausgewählt sein + if (roles.length === 0) { + alert('Bitte wählen Sie mindestens eine Rolle aus.'); + return; + } + + const formData = { + username: document.getElementById('username').value, + password: document.getElementById('password').value, + firstname: document.getElementById('firstname').value, + lastname: document.getElementById('lastname').value, + roles: roles, + personalnummer: document.getElementById('personalnummer').value, + wochenstunden: document.getElementById('wochenstunden').value, + urlaubstage: document.getElementById('urlaubstage').value + }; + + try { + const response = await fetch('/admin/users', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }); + + const result = await response.json(); + + if (result.success) { + alert('Benutzer wurde erfolgreich angelegt!'); + location.reload(); + } else { + alert('Fehler: ' + (result.error || 'Benutzer konnte nicht angelegt werden')); + } + } catch (error) { + console.error('Fehler:', error); + alert('Fehler beim Anlegen des Benutzers'); + } + }); + + // LDAP-Konfiguration laden + loadLDAPConfig(); + + // LDAP-Konfigurationsformular + const ldapConfigForm = document.getElementById('ldapConfigForm'); + if (ldapConfigForm) { + ldapConfigForm.addEventListener('submit', async function(e) { + e.preventDefault(); + + const enabled = document.getElementById('ldapEnabled').checked; + const url = document.getElementById('ldapUrl').value; + const baseDn = document.getElementById('ldapBaseDn').value; + + // Validierung: URL und Base DN sind erforderlich wenn aktiviert + if (enabled && (!url || !baseDn)) { + alert('Bitte füllen Sie URL und Base DN aus, wenn LDAP aktiviert ist.'); + return; + } + + const formData = { + enabled: enabled, + url: url, + bind_dn: document.getElementById('ldapBindDn').value, + bind_password: document.getElementById('ldapBindPassword').value, + base_dn: baseDn, + user_search_filter: document.getElementById('ldapSearchFilter').value, + username_attribute: document.getElementById('ldapUsernameAttr').value, + firstname_attribute: document.getElementById('ldapFirstnameAttr').value, + lastname_attribute: document.getElementById('ldapLastnameAttr').value, + sync_interval: parseInt(document.getElementById('ldapSyncInterval').value) || 0 + }; + + try { + const response = await fetch('/admin/ldap/config', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }); + + const result = await response.json(); + + if (result.success) { + alert('LDAP-Konfiguration wurde erfolgreich gespeichert!'); + location.reload(); + } else { + alert('Fehler: ' + (result.error || 'Konfiguration konnte nicht gespeichert werden')); + } + } catch (error) { + console.error('Fehler:', error); + alert('Fehler beim Speichern der Konfiguration'); + } + }); + } + + // Sync-Button + const syncNowBtn = document.getElementById('syncNowBtn'); + if (syncNowBtn) { + syncNowBtn.addEventListener('click', async function() { + if (!confirm('Möchten Sie die LDAP-Synchronisation jetzt starten?')) { + return; + } + + const statusEl = document.getElementById('syncStatus'); + syncNowBtn.disabled = true; + statusEl.textContent = 'Synchronisation läuft...'; + statusEl.style.color = 'blue'; + + try { + const response = await fetch('/admin/ldap/sync', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + const result = await response.json(); + + if (result.success) { + statusEl.textContent = `Erfolgreich: ${result.synced} Benutzer synchronisiert`; + statusEl.style.color = 'green'; + + if (result.errors && result.errors.length > 0) { + alert('Synchronisation abgeschlossen mit Warnungen:\n' + result.errors.join('\n')); + } else { + alert(`Synchronisation erfolgreich abgeschlossen: ${result.synced} Benutzer synchronisiert`); + } + + // Seite neu laden nach kurzer Verzögerung + setTimeout(() => { + location.reload(); + }, 2000); + } else { + statusEl.textContent = 'Fehler: ' + (result.error || 'Synchronisation fehlgeschlagen'); + statusEl.style.color = 'red'; + alert('Fehler: ' + (result.error || 'Synchronisation fehlgeschlagen')); + syncNowBtn.disabled = false; + } + } catch (error) { + console.error('Fehler:', error); + statusEl.textContent = 'Fehler bei der Synchronisation'; + statusEl.style.color = 'red'; + alert('Fehler bei der Synchronisation'); + syncNowBtn.disabled = false; + } + }); + } +}); + +// LDAP-Konfiguration laden und Formular ausfüllen +async function loadLDAPConfig() { + try { + const response = await fetch('/admin/ldap/config'); + const result = await response.json(); + + if (result.config) { + const config = result.config; + + if (document.getElementById('ldapEnabled')) { + document.getElementById('ldapEnabled').checked = config.enabled === 1; + } + if (document.getElementById('ldapUrl')) { + document.getElementById('ldapUrl').value = config.url || ''; + } + if (document.getElementById('ldapBaseDn')) { + document.getElementById('ldapBaseDn').value = config.base_dn || ''; + } + if (document.getElementById('ldapBindDn')) { + document.getElementById('ldapBindDn').value = config.bind_dn || ''; + } + if (document.getElementById('ldapSearchFilter')) { + document.getElementById('ldapSearchFilter').value = config.user_search_filter || '(objectClass=person)'; + } + if (document.getElementById('ldapUsernameAttr')) { + document.getElementById('ldapUsernameAttr').value = config.username_attribute || 'cn'; + } + if (document.getElementById('ldapFirstnameAttr')) { + document.getElementById('ldapFirstnameAttr').value = config.firstname_attribute || 'givenName'; + } + if (document.getElementById('ldapLastnameAttr')) { + document.getElementById('ldapLastnameAttr').value = config.lastname_attribute || 'sn'; + } + if (document.getElementById('ldapSyncInterval')) { + document.getElementById('ldapSyncInterval').value = config.sync_interval || 0; + } + } + } catch (error) { + console.error('Fehler beim Laden der LDAP-Konfiguration:', error); + } +} + +async function deleteUser(userId, username) { + const confirmed = confirm(`Möchten Sie den Benutzer "${username}" wirklich löschen?`); + + if (!confirmed) return; + + try { + const response = await fetch(`/admin/users/${userId}`, { + method: 'DELETE' + }); + + const result = await response.json(); + + if (result.success) { + alert('Benutzer wurde erfolgreich gelöscht!'); + location.reload(); + } else { + alert('Fehler: ' + (result.error || 'Benutzer konnte nicht gelöscht werden')); + } + } catch (error) { + console.error('Fehler:', error); + alert('Fehler beim Löschen des Benutzers'); + } +} + +// User bearbeiten +function editUser(userId) { + const row = document.querySelector(`tr[data-user-id="${userId}"]`); + if (!row) return; + + // Alle Display-Felder ausblenden und Edit-Felder einblenden + row.querySelectorAll('.user-field-display').forEach(display => { + display.style.display = 'none'; + }); + row.querySelectorAll('.user-field-edit').forEach(edit => { + edit.style.display = 'inline-block'; + }); + + // Buttons umschalten + row.querySelector('.edit-user-btn').style.display = 'none'; + row.querySelector('.save-user-btn').style.display = 'inline-block'; + row.querySelector('.cancel-user-btn').style.display = 'inline-block'; +} + +// User speichern +async function saveUser(userId) { + const row = document.querySelector(`tr[data-user-id="${userId}"]`); + if (!row) return; + + const personalnummer = row.querySelector('input[data-field="personalnummer"]').value; + const wochenstunden = row.querySelector('input[data-field="wochenstunden"]').value; + const urlaubstage = row.querySelector('input[data-field="urlaubstage"]').value; + + // Rollen aus Checkboxen sammeln + const roleCheckboxes = row.querySelectorAll('.role-checkbox:checked'); + const roles = Array.from(roleCheckboxes).map(cb => cb.value); + + // Validierung: Mindestens eine Rolle erforderlich + if (roles.length === 0) { + alert('Mindestens eine Rolle muss ausgewählt sein.'); + return; + } + + try { + const response = await fetch(`/admin/users/${userId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + personalnummer: personalnummer || null, + wochenstunden: wochenstunden || null, + urlaubstage: urlaubstage || null, + roles: roles + }) + }); + + const result = await response.json(); + + if (result.success) { + // Werte in Display-Felder übernehmen + row.querySelector('span[data-field="personalnummer"]').textContent = personalnummer || '-'; + row.querySelector('span[data-field="wochenstunden"]').textContent = wochenstunden || '-'; + row.querySelector('span[data-field="urlaubstage"]').textContent = urlaubstage || '-'; + + // Rollen-Display aktualisieren + const rolesDisplay = row.querySelector('div[data-field="roles"]'); + if (rolesDisplay) { + const roleLabels = { 'mitarbeiter': 'Mitarbeiter', 'verwaltung': 'Verwaltung', 'admin': 'Admin' }; + rolesDisplay.innerHTML = roles.map(role => + `${roleLabels[role] || role}` + ).join(''); + } + + // Bearbeitung beenden + cancelEditUser(userId); + alert('Benutzerdaten wurden erfolgreich gespeichert!'); + // Seite neu laden um sicherzustellen dass alles korrekt ist + location.reload(); + } else { + alert('Fehler: ' + (result.error || 'Daten konnten nicht gespeichert werden')); + } + } catch (error) { + console.error('Fehler:', error); + alert('Fehler beim Speichern der Benutzerdaten'); + } +} + +// Bearbeitung abbrechen +function cancelEditUser(userId) { + const row = document.querySelector(`tr[data-user-id="${userId}"]`); + if (!row) return; + + // Alle Edit-Felder ausblenden und Display-Felder einblenden + row.querySelectorAll('.user-field-edit').forEach(edit => { + edit.style.display = 'none'; + // Wert zurücksetzen (nur für Input-Felder, nicht für Rollen) + const field = edit.dataset.field; + if (field !== 'roles') { + const display = row.querySelector(`span[data-field="${field}"]`); + if (display && edit.tagName === 'INPUT') { + edit.value = display.textContent === '-' ? '' : display.textContent; + } + } + }); + row.querySelectorAll('.user-field-display').forEach(display => { + if (display.tagName === 'DIV' || display.tagName === 'SPAN') { + display.style.display = 'block'; + } else { + display.style.display = 'inline'; + } + }); + + // Buttons umschalten + row.querySelector('.edit-user-btn').style.display = 'inline-block'; + row.querySelector('.save-user-btn').style.display = 'none'; + row.querySelector('.cancel-user-btn').style.display = 'none'; +} diff --git a/public/js/dashboard.js b/public/js/dashboard.js new file mode 100644 index 0000000..ae8d933 --- /dev/null +++ b/public/js/dashboard.js @@ -0,0 +1,1539 @@ +// Dashboard JavaScript + +let currentWeekStart = getMonday(new Date()); +let currentEntries = {}; +let currentHolidayDates = new Set(); // Feiertage der aktuellen Woche (YYYY-MM-DD) +let userWochenstunden = 0; // Wochenstunden des Users + +// Statistiken laden +async function loadUserStats() { + try { + const response = await fetch('/api/user/stats'); + if (!response.ok) { + throw new Error('Fehler beim Laden der Statistiken'); + } + const stats = await response.json(); + + // Überstunden anzeigen + const currentOvertimeEl = document.getElementById('currentOvertime'); + if (currentOvertimeEl) { + const overtime = stats.currentOvertime || 0; + currentOvertimeEl.textContent = overtime >= 0 ? `+${overtime.toFixed(2)}` : overtime.toFixed(2); + currentOvertimeEl.style.color = overtime >= 0 ? '#27ae60' : '#e74c3c'; + // Auch die Border-Farbe des Cards anpassen + const overtimeCard = currentOvertimeEl.closest('.stat-card'); + if (overtimeCard) { + overtimeCard.style.borderLeftColor = overtime >= 0 ? '#27ae60' : '#e74c3c'; + } + } + + // Urlaubstage anzeigen + const remainingVacationEl = document.getElementById('remainingVacation'); + if (remainingVacationEl) { + remainingVacationEl.textContent = (stats.remainingVacation || 0).toFixed(1); + } + + const totalVacationEl = document.getElementById('totalVacation'); + if (totalVacationEl) { + totalVacationEl.textContent = (stats.urlaubstage || 0).toFixed(1); + } + + // Verplante Urlaubstage anzeigen + const plannedVacationEl = document.getElementById('plannedVacation'); + if (plannedVacationEl) { + plannedVacationEl.textContent = (stats.plannedVacationDays || 0).toFixed(1); + } + + // Kalenderwochen anzeigen + const plannedWeeksEl = document.getElementById('plannedWeeks'); + if (plannedWeeksEl) { + if (stats.plannedWeeks && stats.plannedWeeks.length > 0) { + const weeksHTML = stats.plannedWeeks.map(w => { + const daysText = w.days === 1 ? '1 Tag' : `${w.days.toFixed(1)} Tage`; + return `${w.year} KW${String(w.week).padStart(2, '0')} (${daysText})`; + }).join('
    '); + plannedWeeksEl.innerHTML = weeksHTML; + plannedWeeksEl.style.display = 'block'; + } else { + plannedWeeksEl.textContent = ''; + plannedWeeksEl.style.display = 'none'; + } + } + } catch (error) { + console.error('Fehler beim Laden der Statistiken:', error); + // Fehlerbehandlung: Zeige "-" oder "Fehler" + const currentOvertimeEl = document.getElementById('currentOvertime'); + if (currentOvertimeEl) currentOvertimeEl.textContent = '-'; + const remainingVacationEl = document.getElementById('remainingVacation'); + if (remainingVacationEl) remainingVacationEl.textContent = '-'; + const totalVacationEl = document.getElementById('totalVacation'); + if (totalVacationEl) totalVacationEl.textContent = '-'; + const plannedVacationEl = document.getElementById('plannedVacation'); + if (plannedVacationEl) plannedVacationEl.textContent = '-'; + const plannedWeeksEl = document.getElementById('plannedWeeks'); + if (plannedWeeksEl) { + plannedWeeksEl.textContent = ''; + plannedWeeksEl.style.display = 'none'; + } + } +} + +// Beim Laden der Seite +document.addEventListener('DOMContentLoaded', async function() { + // Letzte Woche vom Server laden + try { + const response = await fetch('/api/user/last-week'); + const data = await response.json(); + if (data.last_week_start) { + // Prüfe ob last_week_start wirklich ein Montag ist + if (isMonday(data.last_week_start)) { + currentWeekStart = data.last_week_start; + } else { + // Korrigiere zu Montag falls nicht + console.warn('last_week_start war kein Montag, korrigiere:', data.last_week_start); + currentWeekStart = getMonday(data.last_week_start); + // Speichere die korrigierte Woche + saveLastWeek(); + } + } + } catch (error) { + console.warn('Konnte letzte Woche nicht vom Server laden:', error); + } + + // Stelle sicher, dass currentWeekStart immer ein Montag ist + if (currentWeekStart && !isMonday(currentWeekStart)) { + console.warn('currentWeekStart war kein Montag, korrigiere:', currentWeekStart); + currentWeekStart = getMonday(currentWeekStart); + saveLastWeek(); + } + + // Ping-IP laden + loadPingIP(); + + // Statistiken laden + loadUserStats(); + + loadWeek(); + + document.getElementById('prevWeek').addEventListener('click', function() { + // Parse als lokales Datum + const parts = currentWeekStart.split('-'); + const date = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); + date.setDate(date.getDate() - 7); + // Stelle sicher, dass es ein Montag ist + currentWeekStart = getMonday(date); + saveLastWeek(); + loadWeek(); + }); + + document.getElementById('nextWeek').addEventListener('click', function() { + // Parse als lokales Datum + const parts = currentWeekStart.split('-'); + const date = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); + date.setDate(date.getDate() + 7); + // Stelle sicher, dass es ein Montag ist + currentWeekStart = getMonday(date); + saveLastWeek(); + loadWeek(); + }); + + // Event-Listener für Submit-Button - mit Event-Delegation für bessere Zuverlässigkeit + document.addEventListener('click', function(e) { + if (e.target && e.target.id === 'submitWeek') { + e.preventDefault(); + e.stopPropagation(); + const submitButton = e.target; + console.log('Submit-Button wurde geklickt!'); + console.log('Button disabled?', submitButton.disabled); + console.log('Button element:', submitButton); + + if (!submitButton.disabled) { + submitWeek(); + } else { + console.warn('Submit-Button ist deaktiviert!'); + const tooltip = submitButton.title || 'Bitte füllen Sie alle Werktage aus.'; + alert(tooltip); + } + } + }); + + // Zusätzlicher direkter Event-Listener als Fallback + const submitButton = document.getElementById('submitWeek'); + if (submitButton) { + submitButton.onclick = function(e) { + e.preventDefault(); + e.stopPropagation(); + console.log('Submit-Button onclick wurde ausgelöst!'); + if (!this.disabled) { + submitWeek(); + } + return false; + }; + console.log('Submit-Button Event-Listener gesetzt'); + } else { + console.error('Submit-Button nicht gefunden beim Initialisieren!'); + } +}); + +// Letzte Woche auf dem Server speichern +async function saveLastWeek() { + try { + await fetch('/api/user/last-week', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + week_start: currentWeekStart + }) + }); + } catch (error) { + console.warn('Konnte letzte Woche nicht auf dem Server speichern:', error); + } +} + +// Prüft ob ein Datum ein Montag ist (1 = Montag) +function isMonday(dateStr) { + const d = new Date(dateStr + 'T00:00:00'); // Lokale Zeit verwenden, nicht UTC + return d.getDay() === 1; +} + +// Montag der aktuellen Woche ermitteln (robust gegen Zeitzonenprobleme) +function getMonday(date) { + // Wenn date bereits ein String ist (YYYY-MM-DD), parsen wir es als lokales Datum + let d; + if (typeof date === 'string') { + // Parse als lokales Datum, nicht UTC + const parts = date.split('-'); + d = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); + } else { + d = new Date(date); + } + + // Stelle sicher, dass wir mit lokaler Zeit arbeiten + const day = d.getDay(); // 0 = Sonntag, 1 = Montag, ..., 6 = Samstag + // Berechne Differenz zum Montag: Montag = 1, also diff = 1 - day + // Aber: Sonntag = 0, also für Sonntag: diff = 1 - 0 = 1, aber wir wollen -6 Tage zurück + const diff = day === 0 ? -6 : 1 - day; + d.setDate(d.getDate() + diff); + + // Format als YYYY-MM-DD in lokaler Zeit + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const dayOfMonth = String(d.getDate()).padStart(2, '0'); + return `${year}-${month}-${dayOfMonth}`; +} + +// Kalenderwoche berechnen (ISO 8601 - Woche beginnt Montag) +function getCalendarWeek(dateStr) { + const date = new Date(dateStr); + const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); + const dayNum = d.getUTCDay() || 7; + d.setUTCDate(d.getUTCDate() + 4 - dayNum); + const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7); + return weekNo; +} + +// Datum formatieren (YYYY-MM-DD) +function formatDate(date) { + const d = new Date(date); + return d.toISOString().split('T')[0]; +} + +// Datum formatieren (DD.MM.YYYY) +function formatDateDE(dateStr) { + const d = new Date(dateStr); + return d.toLocaleDateString('de-DE'); +} + +// Wochentag ermitteln +function getWeekday(dateStr) { + const days = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag']; + const d = new Date(dateStr); + return days[d.getDay()]; +} + +// Woche laden +async function loadWeek() { + try { + // User-Daten laden (Wochenstunden) + try { + const userResponse = await fetch('/api/user/data'); + const userData = await userResponse.json(); + userWochenstunden = userData.wochenstunden || 0; + } catch (error) { + console.warn('Konnte User-Daten nicht laden:', error); + userWochenstunden = 0; + } + + const parts = currentWeekStart.split('-'); + const weekEndDate = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10) + 6); + const weekEnd = weekEndDate.getFullYear() + '-' + String(weekEndDate.getMonth() + 1).padStart(2, '0') + '-' + String(weekEndDate.getDate()).padStart(2, '0'); + const [entriesResponse, holidaysResponse] = await Promise.all([ + fetch(`/api/timesheet/week/${currentWeekStart}`), + fetch(`/api/timesheet/holidays?week_start=${currentWeekStart}&week_end=${weekEnd}`) + ]); + const entries = await entriesResponse.json(); + const holidaysData = await holidaysResponse.json(); + currentHolidayDates = new Set(holidaysData.dates || []); + + // Entries in Object umwandeln für schnellen Zugriff + currentEntries = {}; + let weekIsSubmitted = false; + + entries.forEach(entry => { + currentEntries[entry.date] = entry; + // Prüfe ob die Woche bereits eingereicht wurde + if (entry.week_submitted) { + weekIsSubmitted = true; + } + }); + + // Speichere den Status ob die Woche bereits eingereicht wurde + currentEntries._weekSubmitted = weekIsSubmitted; + // Speichere ob bereits eine Version existiert (für Grund-Validierung) + if (entries.length > 0) { + currentEntries._hasExistingVersion = entries[0].has_existing_version || false; + currentEntries._latestVersion = entries[0].latest_version || 0; + } + + renderWeek(); + } catch (error) { + console.error('Fehler beim Laden der Woche:', error); + alert('Fehler beim Laden der Daten'); + } +} + +// Woche rendern +function renderWeek() { + const startDate = new Date(currentWeekStart); + const endDate = new Date(startDate); + endDate.setDate(endDate.getDate() + 6); + + const calendarWeek = getCalendarWeek(currentWeekStart); + document.getElementById('weekTitle').innerHTML = + `Kalenderwoche ${calendarWeek}
    ${formatDateDE(currentWeekStart)} - ${formatDateDE(formatDate(endDate))}`; + + let html = ` +

    + + + + + + + + + + + + + `; + + let totalHours = 0; + let allWeekdaysFilled = true; + + // Hilfsfunktion zum Rendern eines einzelnen Tages + function renderDay(i) { + const date = new Date(startDate); + date.setDate(date.getDate() + i); + const dateStr = formatDate(date); + const entry = currentEntries[dateStr] || {}; + + const startTime = entry.start_time || ''; + const endTime = entry.end_time || ''; + const breakMinutes = entry.break_minutes || 0; + const hours = entry.total_hours || 0; + const overtimeTaken = entry.overtime_taken_hours || ''; + const vacationType = entry.vacation_type || ''; + const sickStatus = entry.sick_status || false; + const isHoliday = currentHolidayDates.has(dateStr); + + // Tätigkeiten laden + const activities = [ + { desc: entry.activity1_desc || '', hours: entry.activity1_hours || 0, projectNumber: entry.activity1_project_number || '' }, + { desc: entry.activity2_desc || '', hours: entry.activity2_hours || 0, projectNumber: entry.activity2_project_number || '' }, + { desc: entry.activity3_desc || '', hours: entry.activity3_hours || 0, projectNumber: entry.activity3_project_number || '' }, + { desc: entry.activity4_desc || '', hours: entry.activity4_hours || 0, projectNumber: entry.activity4_project_number || '' }, + { desc: entry.activity5_desc || '', hours: entry.activity5_hours || 0, projectNumber: entry.activity5_project_number || '' } + ]; + + // Prüfen ob Werktag (Montag-Freitag, i < 5) ausgefüllt ist + // Bei Feiertag, ganztägigem Urlaub oder Krank gilt der Tag als ausgefüllt + // Bei 8 Überstunden (ganzer Tag) gilt der Tag auch als ausgefüllt + const overtimeValue = overtimeTaken ? parseFloat(overtimeTaken) : 0; + const fullDayHours = userWochenstunden ? (userWochenstunden / 5) : 8; + const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01; + + if (i < 5 && !isHoliday && vacationType !== 'full' && !sickStatus && !isFullDayOvertime && (!startTime || !endTime || startTime.trim() === '' || endTime.trim() === '')) { + allWeekdaysFilled = false; + } + + // Stunden zur Summe hinzufügen + // Bei ganztägigem Urlaub oder Krank sollten es bereits 8 Stunden sein (vom Backend gesetzt) + // Bei halbem Tag Urlaub werden die Urlaubsstunden später in der Überstunden-Berechnung hinzugezählt + totalHours += hours; + + // Bearbeitung ist immer möglich, auch nach Abschicken + // Bei ganztägigem Urlaub oder Krank werden Zeitfelder deaktiviert; Feiertag: Anzeige, Zeitfelder optional (Überstunden) + const isFullDayVacation = vacationType === 'full'; + const isSick = sickStatus === true || sickStatus === 1; + const timeFieldsDisabled = (isFullDayVacation || isSick) ? 'disabled' : ''; + const disabled = ''; + const holidayLabel = isHoliday ? ' (Feiertag)' : ''; + + return ` + + + + + + + + + + + + `; + } + + // Werktage (Montag bis Freitag) rendern + for (let i = 0; i < 5; i++) { + html += renderDay(i); + } + + // Wochenende (Samstag und Sonntag) in zusammenklappbarer Sektion + html += ` + + + + `; + + html += ` + +
    TagDatumStartEndePause (Min)Stunden
    ${getWeekday(dateStr)}${formatDateDE(dateStr)}${isFullDayVacation ? ' (Urlaub - ganzer Tag)' : ''}${isSick ? ' (Krank)' : ''}${holidayLabel} + + + + + + ${isFullDayVacation ? '8.00 h (Urlaub)' : isSick ? '8.00 h (Krank)' : isHoliday && !hours ? '8.00 h (Feiertag)' : hours.toFixed(2) + ' h'}
    +
    +
    Tätigkeiten:
    + ${activities.map((activity, idx) => ` +
    +
    + +
    +
    + +
    +
    + + h +
    +
    + `).join('')} +
    +
    +
    + +
    + + h +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +

    Wochenende

    + +
    + +
    +
    +
    + `; + + document.getElementById('timesheetTable').innerHTML = html; + document.getElementById('totalHours').textContent = totalHours.toFixed(2) + ' h'; + + // Überstunden-Berechnung (startDate und endDate sind bereits oben deklariert) + + // Anzahl Werktage berechnen (Montag-Freitag) + let workdays = 0; + for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { + const day = d.getDay(); + if (day >= 1 && day <= 5) { // Montag bis Freitag + workdays++; + } + } + + // Sollstunden berechnen + const sollStunden = (userWochenstunden / 5) * workdays; + + // Urlaubsstunden berechnen (Urlaub zählt als normale Arbeitszeit) + let vacationHours = 0; + Object.values(currentEntries).forEach(e => { + if (e.vacation_type === 'full') { + vacationHours += 8; // Ganzer Tag = 8 Stunden + } else if (e.vacation_type === 'half') { + vacationHours += 4; // Halber Tag = 4 Stunden + } + }); + + // Überstunden berechnen + let overtimeTaken = 0; + Object.values(currentEntries).forEach(e => { + if (e.overtime_taken_hours) { + overtimeTaken += parseFloat(e.overtime_taken_hours) || 0; + } + }); + + // Überstunden-Berechnung aufrufen + updateOvertimeDisplay(); + + // Nach dem Rendern die vollständige Validierung durchführen + // Dies prüft auch direkt die Input-Felder im DOM + checkWeekComplete(); +} + +// Überstunden-Anzeige aktualisieren (wird bei jeder Änderung aufgerufen) +function updateOvertimeDisplay() { + const startDate = new Date(currentWeekStart); + const endDate = new Date(startDate); + endDate.setDate(endDate.getDate() + 6); + + // Anzahl Werktage berechnen (Montag-Freitag) + let workdays = 0; + for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { + const day = d.getDay(); + if (day >= 1 && day <= 5) { // Montag bis Freitag + workdays++; + } + } + + // Sollstunden berechnen + const sollStunden = (userWochenstunden / 5) * workdays; + + // Gesamtstunden berechnen - direkt aus DOM-Elementen lesen für Echtzeit-Aktualisierung + let totalHours = 0; + let vacationHours = 0; + const fullDayHours = userWochenstunden ? (userWochenstunden / 5) : 8; + let fullDayOvertimeDays = 0; // Anzahl Tage mit 8 Überstunden (wie im Backend) + const startDateObj = new Date(startDate); + for (let i = 0; i < 7; i++) { + const date = new Date(startDateObj); + date.setDate(date.getDate() + i); + const dateStr = formatDate(date); + + // Prüfe Urlaub-Status und Krank-Status + const vacationSelect = document.querySelector(`select[data-date="${dateStr}"][data-field="vacation_type"]`); + const vacationType = vacationSelect ? vacationSelect.value : (currentEntries[dateStr]?.vacation_type || ''); + const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`); + const sickStatus = sickCheckbox ? sickCheckbox.checked : (currentEntries[dateStr]?.sick_status || false); + + // Prüfe ob 8 Überstunden (ganzer Tag) eingetragen sind + const overtimeInput = document.querySelector(`input[data-date="${dateStr}"][data-field="overtime_taken_hours"]`); + const overtimeValue = overtimeInput ? parseFloat(overtimeInput.value) || 0 : (currentEntries[dateStr]?.overtime_taken_hours ? parseFloat(currentEntries[dateStr].overtime_taken_hours) : 0); + const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01; + + // Zähle volle Überstundentage (wie im Backend) + if (isFullDayOvertime) { + fullDayOvertimeDays++; + } + + // Wenn Urlaub oder Krank, zähle nur diese Stunden (nicht zusätzlich Arbeitsstunden) + if (vacationType === 'full') { + vacationHours += 8; // Ganzer Tag Urlaub = 8 Stunden + } else if (vacationType === 'half') { + vacationHours += 4; // Halber Tag Urlaub = 4 Stunden + // Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein + const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`); + const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`); + const breakInput = document.querySelector(`input[data-date="${dateStr}"][data-field="break_minutes"]`); + + const startTime = startInput ? startInput.value : ''; + const endTime = endInput ? endInput.value : ''; + const breakMinutes = parseInt(breakInput ? breakInput.value : 0) || 0; + + if (startTime && endTime && !isFullDayOvertime) { + const start = new Date(`2000-01-01T${startTime}`); + const end = new Date(`2000-01-01T${endTime}`); + const diffMs = end - start; + const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60); + totalHours += hours; + } else if (currentEntries[dateStr]?.total_hours && !isFullDayOvertime) { + // Fallback auf gespeicherte Werte + totalHours += parseFloat(currentEntries[dateStr].total_hours) || 0; + } + } else if (sickStatus) { + totalHours += 8; // Krank = 8 Stunden + } else { + // Wenn 8 Überstunden (ganzer Tag) eingetragen sind, zählt der Tag als 0 Stunden + if (isFullDayOvertime) { + // Tag zählt als 0 Stunden (Überstunden werden separat abgezogen) + // totalHours bleibt unverändert (0 Stunden für diesen Tag) + } else { + // Berechne Stunden direkt aus Start-/Endzeit und Pause + const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`); + const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`); + const breakInput = document.querySelector(`input[data-date="${dateStr}"][data-field="break_minutes"]`); + + const startTime = startInput ? startInput.value : ''; + const endTime = endInput ? endInput.value : ''; + const breakMinutes = parseInt(breakInput ? breakInput.value : 0) || 0; + + if (startTime && endTime) { + const start = new Date(`2000-01-01T${startTime}`); + const end = new Date(`2000-01-01T${endTime}`); + const diffMs = end - start; + const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60); + totalHours += hours; + } else if (currentEntries[dateStr]?.total_hours) { + // Fallback auf gespeicherte Werte + totalHours += parseFloat(currentEntries[dateStr].total_hours) || 0; + } + } + } + } + + // Genommene Überstunden berechnen - direkt aus DOM lesen + let overtimeTaken = 0; + const startDateObj3 = new Date(startDate); + for (let i = 0; i < 7; i++) { + const date = new Date(startDateObj3); + date.setDate(date.getDate() + i); + const dateStr = formatDate(date); + + const overtimeInput = document.querySelector(`input[data-date="${dateStr}"][data-field="overtime_taken_hours"]`); + if (overtimeInput && overtimeInput.value) { + overtimeTaken += parseFloat(overtimeInput.value) || 0; + } else if (currentEntries[dateStr]?.overtime_taken_hours) { + overtimeTaken += parseFloat(currentEntries[dateStr].overtime_taken_hours) || 0; + } + } + + // Überstunden berechnen (wie im Backend: mit adjustedSollStunden) + // Wenn 8 Überstunden genommen wurden, zählen diese Tage als 0 Stunden + // Die negativen Stunden (wegen 0 statt Sollstunden) werden durch die verbrauchten Überstunden ausgeglichen + // Daher: adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours) + // So werden die Tage mit 8 Überstunden nicht zu negativen Überstunden führen + const totalHoursWithVacation = totalHours + vacationHours; + const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours); + // overtimeHours = Überstunden diese Woche (wie im Backend berechnet) + const overtimeHours = totalHoursWithVacation - adjustedSollStunden; + + // Überstunden-Anzeige aktualisieren + const overtimeSummaryItem = document.getElementById('overtimeSummaryItem'); + const overtimeHoursSpan = document.getElementById('overtimeHours'); + if (overtimeSummaryItem && overtimeHoursSpan) { + overtimeSummaryItem.style.display = 'block'; + const sign = overtimeHours >= 0 ? '+' : ''; + overtimeHoursSpan.textContent = `${sign}${overtimeHours.toFixed(2)} h`; + overtimeHoursSpan.style.color = overtimeHours >= 0 ? '#27ae60' : '#e74c3c'; + } +} + +// Überstunden-Änderung verarbeiten +function handleOvertimeChange(dateStr, overtimeHours) { + if (!userWochenstunden || userWochenstunden <= 0) { + console.warn('Wochenstunden nicht verfügbar, kann Überstunden-Logik nicht anwenden'); + return; + } + + const fullDayHours = userWochenstunden / 5; + const overtimeValue = parseFloat(overtimeHours) || 0; + + // 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 + const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`); + const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`); + + if (startInput) { + startInput.value = ''; + saveEntry(startInput); + } + + if (endInput) { + 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 + } + } +} + +// Eintrag speichern +async function saveEntry(input) { + const date = input.dataset.date; + const field = input.dataset.field; + const value = input.value; + + // Entferne Fehlermarkierung wenn Feld ausgefüllt wird + if (input.classList.contains('missing-field')) { + input.classList.remove('missing-field'); + input.style.borderColor = ''; + input.style.backgroundColor = ''; + } + + // Aktualisiere currentEntries + if (!currentEntries[date]) { + currentEntries[date] = { date }; + } + currentEntries[date][field] = value; + + // Lese alle aktuellen Werte direkt aus dem DOM, nicht nur aus currentEntries + // Das stellt sicher, dass auch Werte gespeichert werden, die noch nicht in currentEntries sind + // WICHTIG: Wenn das aktuelle Input-Element das Feld ist, das wir suchen, verwende direkt dessen Wert + const startInput = document.querySelector(`input[data-date="${date}"][data-field="start_time"]`); + const endInput = document.querySelector(`input[data-date="${date}"][data-field="end_time"]`); + const breakInput = document.querySelector(`input[data-date="${date}"][data-field="break_minutes"]`); + const notesInput = document.querySelector(`textarea[data-date="${date}"][data-field="notes"]`); + const vacationSelect = document.querySelector(`select[data-date="${date}"][data-field="vacation_type"]`); + const overtimeInput = document.querySelector(`input[data-date="${date}"][data-field="overtime_taken_hours"]`); + const sickCheckbox = document.querySelector(`input[data-date="${date}"][data-field="sick_status"]`); + + // Wenn das aktuelle Input-Element das gesuchte Feld ist, verwende dessen Wert direkt + // Das stellt sicher, dass der Wert auch bei oninput/onchange sofort verfügbar ist + const actualStartTime = (input.dataset.field === 'start_time' && input.value) ? input.value : + (startInput && startInput.value && startInput.value.trim() !== '') ? startInput.value : + (currentEntries[date].start_time || null); + const actualEndTime = (input.dataset.field === 'end_time' && input.value) ? input.value : + (endInput && endInput.value && endInput.value.trim() !== '') ? endInput.value : + (currentEntries[date].end_time || null); + + // Aktuelle Werte aus DOM lesen (falls vorhanden), sonst aus currentEntries + // Wichtig: Leere Strings werden zu null konvertiert, aber ein Wert sollte vorhanden sein + const start_time = actualStartTime; + const end_time = actualEndTime; + const break_minutes = breakInput && breakInput.value ? (parseInt(breakInput.value) || 0) : (parseInt(currentEntries[date].break_minutes) || 0); + const notes = notesInput ? (notesInput.value || '') : (currentEntries[date].notes || ''); + const vacation_type = vacationSelect && vacationSelect.value ? vacationSelect.value : (currentEntries[date].vacation_type || null); + const overtime_taken_hours = overtimeInput && overtimeInput.value ? overtimeInput.value : (currentEntries[date].overtime_taken_hours || null); + const sick_status = sickCheckbox ? (sickCheckbox.checked ? true : false) : (currentEntries[date].sick_status || false); + + // Activity-Felder aus DOM lesen + const activities = []; + for (let i = 1; i <= 5; i++) { + const descInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_desc"]`); + const hoursInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_hours"]`); + const projectInput = document.querySelector(`input[data-date="${date}"][data-field="activity${i}_project_number"]`); + + activities.push({ + desc: descInput ? (descInput.value || null) : (currentEntries[date][`activity${i}_desc`] || null), + hours: hoursInput ? (parseFloat(hoursInput.value) || 0) : (parseFloat(currentEntries[date][`activity${i}_hours`]) || 0), + projectNumber: projectInput ? (projectInput.value || null) : (currentEntries[date][`activity${i}_project_number`] || null) + }); + } + + // Aktualisiere currentEntries mit den DOM-Werten + currentEntries[date].start_time = start_time; + currentEntries[date].end_time = end_time; + currentEntries[date].break_minutes = break_minutes; + currentEntries[date].notes = notes; + currentEntries[date].vacation_type = vacation_type; + currentEntries[date].overtime_taken_hours = overtime_taken_hours; + currentEntries[date].sick_status = sick_status; + for (let i = 1; i <= 5; i++) { + currentEntries[date][`activity${i}_desc`] = activities[i-1].desc; + currentEntries[date][`activity${i}_hours`] = activities[i-1].hours; + currentEntries[date][`activity${i}_project_number`] = activities[i-1].projectNumber; + } + + const entry = currentEntries[date]; + + + try { + const response = await fetch('/api/timesheet/save', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + date: date, + start_time: start_time, + end_time: end_time, + break_minutes: break_minutes, + notes: notes, + activity1_desc: activities[0].desc, + activity1_hours: activities[0].hours, + activity1_project_number: activities[0].projectNumber, + activity2_desc: activities[1].desc, + activity2_hours: activities[1].hours, + activity2_project_number: activities[1].projectNumber, + activity3_desc: activities[2].desc, + activity3_hours: activities[2].hours, + activity3_project_number: activities[2].projectNumber, + activity4_desc: activities[3].desc, + activity4_hours: activities[3].hours, + activity4_project_number: activities[3].projectNumber, + activity5_desc: activities[4].desc, + activity5_hours: activities[4].hours, + activity5_project_number: activities[4].projectNumber, + overtime_taken_hours: overtime_taken_hours, + vacation_type: vacation_type, + sick_status: sick_status + }) + }); + + const result = await response.json(); + + if (result.success) { + // Aktualisiere Stunden-Anzeige + const hoursElement = document.getElementById(`hours_${date}`); + if (hoursElement && result.total_hours !== undefined) { + hoursElement.textContent = result.total_hours.toFixed(2) + ' h'; + currentEntries[date].total_hours = result.total_hours; + } + + // Gesamtstunden neu berechnen + let totalHours = 0; + Object.values(currentEntries).forEach(e => { + totalHours += e.total_hours || 0; + }); + document.getElementById('totalHours').textContent = totalHours.toFixed(2) + ' h'; + + // Überstunden-Anzeige aktualisieren (bei jeder Änderung) + updateOvertimeDisplay(); + + // Wenn vacation_type geändert wurde, Statistiken aktualisieren (für verplante Tage) + if (input.dataset.field === 'vacation_type') { + loadUserStats(); + } + + // Submit-Button Status prüfen (nach jedem Speichern) + checkWeekComplete(); + + // Visuelles Feedback + input.style.backgroundColor = '#d4edda'; + setTimeout(() => { + input.style.backgroundColor = ''; + }, 500); + + // Statistiken aktualisieren (nur wenn es sich um eingereichte Wochen handelt) + // Wir aktualisieren die Statistiken nicht bei jedem Speichern, da sie nur für eingereichte Wochen relevant sind + // Die Statistiken werden beim Laden der Seite und nach dem Abschicken aktualisiert + } + } catch (error) { + console.error('Fehler beim Speichern:', error); + alert('Fehler beim Speichern'); + } +} + +// Prüfen ob alle Werktage der Woche ausgefüllt sind +function checkWeekComplete() { + const startDate = new Date(currentWeekStart); + let allWeekdaysFilled = true; + const missingFields = []; + + // Prüfe nur Werktage (Montag-Freitag, i < 5) + // Samstag und Sonntag (i >= 5) sind optional + for (let i = 0; i < 5; i++) { + const date = new Date(startDate); + date.setDate(date.getDate() + i); + const dateStr = formatDate(date); + + // Prüfe Urlaub-Status und Krank-Status + const entry = currentEntries[dateStr] || {}; + const vacationType = entry.vacation_type; + const vacationSelect = document.querySelector(`select[data-date="${dateStr}"][data-field="vacation_type"]`); + const vacationValue = vacationSelect ? vacationSelect.value : (vacationType || ''); + const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`); + const sickStatus = sickCheckbox ? sickCheckbox.checked : (entry.sick_status || false); + + // Wenn ganzer Tag Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten + if (vacationValue === 'full' || sickStatus) { + continue; // Tag ist ausgefüllt (ganzer Tag Urlaub oder Krank) + } + + // Prüfe ob 8 Überstunden eingetragen sind (dann ist der Tag auch ausgefüllt) + const overtimeInput = document.querySelector(`input[data-date="${dateStr}"][data-field="overtime_taken_hours"]`); + const overtimeValue = overtimeInput ? parseFloat(overtimeInput.value) || 0 : (entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0); + + // Berechne fullDayHours (normalerweise 8 Stunden) + const fullDayHours = userWochenstunden ? (userWochenstunden / 5) : 8; + + // Wenn 8 Überstunden (ganzer Tag) eingetragen sind, ist der Tag ausgefüllt + if (overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01) { + continue; // Tag ist ausgefüllt (8 Überstunden = ganzer Tag) + } + + // Prüfe IMMER direkt die Input-Felder im DOM (das ist die zuverlässigste Quelle) + // Auch bei manueller Eingabe werden die Werte hier erkannt + const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`); + const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`); + + // Hole die Werte direkt aus den Input-Feldern + const startTime = startInput ? (startInput.value || '').trim() : ''; + const endTime = endInput ? (endInput.value || '').trim() : ''; + + // Debug-Ausgabe - zeigt auch den tatsächlichen Wert im Input-Feld + console.log(`Tag ${i + 1} (${dateStr}): Start="${startTime || 'LEER'}", Ende="${endTime || 'LEER'}", Urlaub="${vacationValue || 'KEIN'}", Überstunden="${overtimeValue}"`, { + startInputExists: !!startInput, + endInputExists: !!endInput, + startInputValue: startInput ? startInput.value : 'N/A', + endInputValue: endInput ? endInput.value : 'N/A', + vacationValue: vacationValue, + overtimeValue: overtimeValue, + fullDayHours: fullDayHours + }); + + // Bei halbem Tag Urlaub oder keinem Urlaub müssen Start- und Endzeit vorhanden sein + // (außer wenn 8 Überstunden eingetragen sind, dann sind Start/Ende nicht nötig) + if (!startTime || !endTime || startTime === '' || endTime === '') { + allWeekdaysFilled = false; + missingFields.push(formatDateDE(dateStr)); + } + } + + // Prüfe ob die Woche bereits eingereicht wurde (nicht der Status einzelner Einträge!) + const weekIsSubmitted = currentEntries._weekSubmitted === true; + const submitButton = document.getElementById('submitWeek'); + if (submitButton) { + submitButton.disabled = weekIsSubmitted || !allWeekdaysFilled; + + if (weekIsSubmitted) { + submitButton.title = 'Diese Woche wurde bereits eingereicht und kann nicht mehr geändert werden.'; + } else if (!allWeekdaysFilled) { + submitButton.title = `Bitte füllen Sie alle Werktage (Montag bis Freitag) aus (Start- und Endzeit). Wochenende ist optional. Fehlend: ${missingFields.join(', ')}`; + } else { + submitButton.title = ''; + } + + console.log(`Submit-Button Status: disabled=${submitButton.disabled}, allWeekdaysFilled=${allWeekdaysFilled}, weekIsSubmitted=${weekIsSubmitted}`); + } +} + +// Globaler Handler für onclick-Attribut (im HTML) +window.submitWeekHandler = function(e) { + e.preventDefault(); + e.stopPropagation(); + console.log('submitWeekHandler wurde aufgerufen'); + const submitButton = document.getElementById('submitWeek'); + + // Auch wenn der Button disabled ist, versuchen wir zu prüfen was fehlt + if (submitButton) { + if (!submitButton.disabled) { + submitWeek(); + } else { + // Button ist disabled - zeige was fehlt + console.warn('Button ist disabled - zeige fehlende Felder'); + // Rufe submitWeek auf, um die Validierung durchzuführen (wird wegen fehlender Felder abgebrochen) + submitWeek(); + } + } else { + console.error('Button nicht gefunden'); + alert('Fehler: Button nicht gefunden'); + } + return false; +}; + +// Woche abschicken +async function submitWeek() { + console.log('submitWeek() wurde aufgerufen'); + const startDate = new Date(currentWeekStart); + const endDate = new Date(startDate); + endDate.setDate(endDate.getDate() + 6); + + console.log('Prüfe Validierung für Woche:', currentWeekStart); + + // Frontend-Validierung: Prüfen ob alle Werktage (Montag-Freitag) ausgefüllt sind + let missingFields = []; + let firstMissingInput = null; + + // Entferne vorherige Fehlermarkierungen + document.querySelectorAll('.missing-field').forEach(el => { + el.classList.remove('missing-field'); + el.style.borderColor = ''; + el.style.backgroundColor = ''; + }); + + for (let i = 0; i < 5; i++) { + const date = new Date(startDate); + date.setDate(date.getDate() + i); + const dateStr = formatDate(date); + const weekday = getWeekday(dateStr); + const dateDisplay = formatDateDE(dateStr); + + // Prüfe Urlaub-Status und Krank-Status + const entry = currentEntries[dateStr] || {}; + const vacationSelect = document.querySelector(`select[data-date="${dateStr}"][data-field="vacation_type"]`); + const vacationValue = vacationSelect ? vacationSelect.value : (entry.vacation_type || ''); + const sickCheckbox = document.querySelector(`input[data-date="${dateStr}"][data-field="sick_status"]`); + const sickStatus = sickCheckbox ? sickCheckbox.checked : (entry.sick_status || false); + + // Wenn ganzer Tag Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten + if (vacationValue === 'full' || sickStatus) { + continue; // Tag ist ausgefüllt (ganzer Tag Urlaub oder Krank) + } + + // Prüfe ob 8 Überstunden eingetragen sind (dann ist der Tag auch ausgefüllt, Start/Ende nicht nötig) + const overtimeInput = document.querySelector(`input[data-date="${dateStr}"][data-field="overtime_taken_hours"]`); + const overtimeValue = overtimeInput ? parseFloat(overtimeInput.value) || 0 : (entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0); + const fullDayHours = userWochenstunden ? (userWochenstunden / 5) : 8; + + // Wenn 8 Überstunden (ganzer Tag) eingetragen sind, ist der Tag ausgefüllt + if (overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01) { + continue; // Tag ist ausgefüllt (8 Überstunden = ganzer Tag) + } + + // Prüfe IMMER direkt die Input-Felder im DOM - auch bei manueller Eingabe + const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`); + const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`); + + // Hole die Werte direkt aus den Input-Feldern (nicht aus currentEntries!) + const startTime = startInput ? (startInput.value || '').trim() : ''; + const endTime = endInput ? (endInput.value || '').trim() : ''; + + // Debug-Ausgabe mit detaillierten Informationen + console.log(`Tag ${i + 1} (${dateStr}): Start="${startTime || 'LEER'}", Ende="${endTime || 'LEER'}", Urlaub="${vacationValue || 'KEIN'}", Überstunden="${overtimeValue}"`, { + startInputExists: !!startInput, + endInputExists: !!endInput, + startInputValue: startInput ? `"${startInput.value}"` : 'N/A', + endInputValue: endInput ? `"${endInput.value}"` : 'N/A', + startInputType: startInput ? typeof startInput.value : 'N/A', + vacationValue: vacationValue, + overtimeValue: overtimeValue, + fullDayHours: fullDayHours + }); + + const missing = []; + if (!startTime || startTime === '') { + missing.push('Startzeit'); + if (startInput) { + startInput.classList.add('missing-field'); + startInput.style.borderColor = '#dc3545'; + startInput.style.backgroundColor = '#fff5f5'; + if (!firstMissingInput) firstMissingInput = startInput; + } + } + + if (!endTime || endTime === '') { + missing.push('Endzeit'); + if (endInput) { + endInput.classList.add('missing-field'); + endInput.style.borderColor = '#dc3545'; + endInput.style.backgroundColor = '#fff5f5'; + if (!firstMissingInput) firstMissingInput = endInput; + } + } + + if (missing.length > 0) { + missingFields.push(`${weekday} (${dateDisplay}): ${missing.join(' und ')}`); + } + } + + if (missingFields.length > 0) { + console.warn('Fehlende Felder:', missingFields); + + // Scroll zum ersten fehlenden Feld + if (firstMissingInput) { + firstMissingInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); + setTimeout(() => firstMissingInput.focus(), 300); + } + + // Detaillierte Fehlermeldung + const message = `❌ Bitte füllen Sie alle Werktage (Montag bis Freitag) vollständig aus!\n\n` + + `Fehlende Eingaben:\n${missingFields.map((field, index) => `\n${index + 1}. ${field}`).join('')}\n\n` + + `Die fehlenden Felder wurden rot markiert.\n` + + `Hinweis: Samstag und Sonntag sind optional.`; + + alert(message); + return; + } + + console.log('Alle Werktage sind ausgefüllt'); + + // Prüfe ob bereits eine Version existiert + const hasExistingVersion = currentEntries._hasExistingVersion || false; + + if (hasExistingVersion) { + // Zeige Modal für Grund-Eingabe + showVersionReasonModal((reason) => { + if (reason) { + submitWeekWithReason(reason); + } + }); + } else { + // Erste Version - normale Bestätigung + const confirmed = confirm( + `Möchten Sie die Stundenerfassung für die Woche vom ${formatDateDE(currentWeekStart)} bis ${formatDateDE(formatDate(endDate))} wirklich abschicken?` + ); + + if (!confirmed) { + console.log('Benutzer hat abgebrochen'); + return; + } + + submitWeekWithReason(null); + } +} + +// Modal für Grund-Eingabe anzeigen +function showVersionReasonModal(callback) { + // Erstelle Modal + const modal = document.createElement('div'); + modal.id = 'versionReasonModal'; + modal.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 10000; + `; + + const modalContent = document.createElement('div'); + modalContent.style.cssText = ` + background-color: white; + padding: 30px; + border-radius: 8px; + max-width: 500px; + width: 90%; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + `; + + modalContent.innerHTML = ` +

    Neue Version einreichen

    +

    + Es existiert bereits eine Version für diese Woche. Bitte geben Sie einen Grund an, warum Sie eine neue Version einreichen: +

    + +
    + + +
    + `; + + modal.appendChild(modalContent); + document.body.appendChild(modal); + + const reasonInput = document.getElementById('versionReasonInput'); + reasonInput.focus(); + + // Event-Handler + document.getElementById('cancelReasonBtn').addEventListener('click', () => { + document.body.removeChild(modal); + callback(null); + }); + + document.getElementById('submitReasonBtn').addEventListener('click', () => { + const reason = reasonInput.value.trim(); + if (!reason) { + alert('Bitte geben Sie einen Grund für die neue Version an.'); + reasonInput.focus(); + return; + } + document.body.removeChild(modal); + callback(reason); + }); + + // Enter-Taste im Textarea + reasonInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && e.ctrlKey) { + document.getElementById('submitReasonBtn').click(); + } + }); + + // ESC-Taste zum Schließen + modal.addEventListener('click', (e) => { + if (e.target === modal) { + document.body.removeChild(modal); + callback(null); + } + }); +} + +// Woche mit Grund abschicken +async function submitWeekWithReason(versionReason) { + const startDate = new Date(currentWeekStart); + const endDate = new Date(startDate); + endDate.setDate(endDate.getDate() + 6); + + console.log('Sende Anfrage an Server...'); + try { + const response = await fetch('/api/timesheet/submit', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + week_start: currentWeekStart, + week_end: formatDate(endDate), + version_reason: versionReason || null + }) + }); + + console.log('Server-Antwort erhalten, Status:', response.status); + const result = await response.json(); + console.log('Server-Antwort:', result); + + if (result.success) { + const versionText = result.version ? ` (Version ${result.version})` : ''; + alert(`Stundenzettel wurde erfolgreich eingereicht${versionText}!`); + loadWeek(); // Neu laden um Status zu aktualisieren + // Statistiken aktualisieren + loadUserStats(); + } else { + console.error('Fehler-Details:', result); + alert(result.error || 'Fehler beim Einreichen des Stundenzettels'); + } + } catch (error) { + console.error('Fehler beim Abschicken:', error); + alert('Fehler beim Abschicken: ' + error.message); + } +} + +// Überstunden-Eingabefeld ein-/ausblenden +function toggleOvertimeInput(dateStr) { + const inputDiv = document.getElementById(`overtime-input-${dateStr}`); + if (inputDiv) { + if (inputDiv.style.display === 'none' || !inputDiv.style.display) { + inputDiv.style.display = 'inline-block'; + const input = inputDiv.querySelector('input'); + if (input) { + input.focus(); + } + } else { + inputDiv.style.display = 'none'; + // Wert löschen wenn ausgeblendet + const input = inputDiv.querySelector('input'); + if (input) { + input.value = ''; + // Speichern + if (currentEntries[dateStr]) { + currentEntries[dateStr].overtime_taken_hours = null; + saveEntry(input); + } + } + } + } +} + +// Urlaub-Auswahl ein-/ausblenden +function toggleVacationSelect(dateStr) { + const selectDiv = document.getElementById(`vacation-select-${dateStr}`); + if (selectDiv) { + if (selectDiv.style.display === 'none' || !selectDiv.style.display) { + selectDiv.style.display = 'inline-block'; + const select = selectDiv.querySelector('select'); + if (select) { + select.focus(); + } + } else { + selectDiv.style.display = 'none'; + // Wert löschen wenn ausgeblendet + const select = selectDiv.querySelector('select'); + if (select) { + select.value = ''; + // Speichern + if (currentEntries[dateStr]) { + currentEntries[dateStr].vacation_type = null; + saveEntry(select); + } + } + } + } +} + +// Krank-Status ein-/ausblenden +function toggleSickStatus(dateStr) { + const checkboxDiv = document.getElementById(`sick-checkbox-${dateStr}`); + if (checkboxDiv) { + if (checkboxDiv.style.display === 'none' || !checkboxDiv.style.display) { + checkboxDiv.style.display = 'inline-block'; + const checkbox = checkboxDiv.querySelector('input[type="checkbox"]'); + if (checkbox) { + // Prüfe aktuellen Status aus currentEntries + const currentSickStatus = currentEntries[dateStr]?.sick_status || false; + checkbox.checked = currentSickStatus || true; // Wenn nicht gesetzt, auf true setzen + checkbox.focus(); + // Sofort speichern wenn aktiviert + if (!currentSickStatus) { + saveEntry(checkbox); + } + } + } else { + // Wert löschen wenn ausgeblendet + const checkbox = checkboxDiv.querySelector('input[type="checkbox"]'); + if (checkbox) { + checkbox.checked = false; + // Speichern + if (currentEntries[dateStr]) { + currentEntries[dateStr].sick_status = false; + saveEntry(checkbox); + } + } + checkboxDiv.style.display = 'none'; + } + } +} + +// Ping-IP laden +async function loadPingIP() { + try { + const response = await fetch('/api/user/ping-ip'); + if (!response.ok) { + throw new Error('Fehler beim Laden der IP-Adresse'); + } + const data = await response.json(); + const pingIpInput = document.getElementById('pingIpInput'); + if (pingIpInput) { + pingIpInput.value = data.ping_ip || ''; + } + } catch (error) { + console.error('Fehler beim Laden der Ping-IP:', error); + } +} + +// Client-IP ermitteln und eintragen (global für onclick) +window.detectClientIP = async function() { + const pingIpInput = document.getElementById('pingIpInput'); + const detectButton = document.querySelector('button[onclick*="detectClientIP"]'); + + if (!pingIpInput) { + return; + } + + // Button-Status während des Ladens + if (detectButton) { + const originalText = detectButton.textContent; + detectButton.textContent = 'Ermittle...'; + detectButton.disabled = true; + + try { + const response = await fetch('/api/user/client-ip'); + if (!response.ok) { + throw new Error('Fehler beim Abrufen der IP-Adresse'); + } + + const data = await response.json(); + + if (data.client_ip && data.client_ip !== 'unknown') { + // IP in das Eingabefeld eintragen + pingIpInput.value = data.client_ip; + + // Erfolgs-Feedback + detectButton.textContent = 'IP ermittelt!'; + detectButton.style.backgroundColor = '#27ae60'; + setTimeout(() => { + detectButton.textContent = originalText; + detectButton.style.backgroundColor = '#3498db'; + detectButton.disabled = false; + }, 2000); + } else { + alert('IP-Adresse konnte nicht ermittelt werden.'); + detectButton.textContent = originalText; + detectButton.disabled = false; + } + } catch (error) { + console.error('Fehler beim Ermitteln der Client-IP:', error); + alert('Fehler beim Ermitteln der IP-Adresse'); + if (detectButton) { + detectButton.textContent = originalText; + detectButton.disabled = false; + } + } + } +}; + +// Ping-IP speichern (global für onclick) +window.savePingIP = async function() { + const pingIpInput = document.getElementById('pingIpInput'); + if (!pingIpInput) { + return; + } + + const pingIp = pingIpInput.value.trim(); + + // Finde den Button (nächstes Geschwisterelement oder über Parent) + const button = pingIpInput.parentElement?.querySelector('button') || + document.querySelector('button[onclick*="savePingIP"]'); + + try { + const response = await fetch('/api/user/ping-ip', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ ping_ip: pingIp }) + }); + + const result = await response.json(); + + if (!response.ok) { + alert(result.error || 'Fehler beim Speichern der IP-Adresse'); + return; + } + + // Erfolgs-Feedback + if (button) { + const originalText = button.textContent; + button.textContent = 'Gespeichert!'; + button.style.backgroundColor = '#27ae60'; + setTimeout(() => { + button.textContent = originalText; + button.style.backgroundColor = ''; + }, 2000); + } + + console.log('Ping-IP gespeichert:', result.ping_ip); + } catch (error) { + console.error('Fehler beim Speichern der Ping-IP:', error); + alert('Fehler beim Speichern der IP-Adresse'); + } +}; diff --git a/reset-db.js b/reset-db.js new file mode 100644 index 0000000..61a6ccc --- /dev/null +++ b/reset-db.js @@ -0,0 +1,445 @@ +const sqlite3 = require('sqlite3').verbose(); +const bcrypt = require('bcryptjs'); +const path = require('path'); +const fs = require('fs'); +const { exec } = require('child_process'); +const { promisify } = require('util'); + +const execAsync = promisify(exec); +const dbPath = path.join(__dirname, 'stundenerfassung.db'); + +console.log('🔄 Setze Datenbank zurück...\n'); + +// Datenbank schließen falls offen +let db = null; +let savedLdapConfig = []; + +// Hilfsfunktion zum Warten +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +// Funktion zum Prüfen und Beenden von Prozessen auf bestimmten Ports +async function checkAndKillPorts(ports) { + const killedProcesses = []; + + for (const port of ports) { + try { + // Prüfe, ob der Port belegt ist (Windows) + const { stdout } = await execAsync(`netstat -ano | findstr :${port}`); + + if (stdout) { + // Extrahiere PID aus der Ausgabe + const lines = stdout.trim().split('\n'); + const pids = new Set(); + + for (const line of lines) { + const parts = line.trim().split(/\s+/); + if (parts.length > 0) { + const pid = parts[parts.length - 1]; + if (pid && !isNaN(pid)) { + pids.add(pid); + } + } + } + + // Beende alle Prozesse, die den Port verwenden + for (const pid of pids) { + try { + console.log(`🛑 Beende Prozess ${pid} auf Port ${port}...`); + await execAsync(`taskkill /F /PID ${pid}`); + killedProcesses.push({ port, pid }); + console.log(`✅ Prozess ${pid} beendet`); + } catch (err) { + // Prozess könnte bereits beendet sein oder keine Berechtigung + if (!err.message.includes('not found') && !err.message.includes('not running')) { + console.log(`⚠️ Konnte Prozess ${pid} nicht beenden: ${err.message}`); + } + } + } + } + } catch (err) { + // Port ist nicht belegt oder netstat hat nichts gefunden + if (!err.message.includes('findstr') && !err.message.includes('not found')) { + // Ignoriere Fehler, wenn der Port nicht belegt ist + } + } + } + + if (killedProcesses.length > 0) { + console.log(`\n✅ ${killedProcesses.length} Prozess(e) beendet\n`); + // Warte kurz, damit die Ports freigegeben werden + await sleep(1000); + } else { + console.log('ℹ️ Keine Prozesse auf Ports 3333 oder 3334 gefunden\n'); + } + + return killedProcesses.length > 0; +} + +// Funktion zum Löschen der Datenbankdatei mit Retry-Logik (async) +async function deleteDatabaseFile(retries = 10, initialDelay = 500) { + if (!fs.existsSync(dbPath)) { + return true; + } + + for (let i = 0; i < retries; i++) { + try { + fs.unlinkSync(dbPath); + console.log('✅ Datenbankdatei gelöscht\n'); + return true; + } catch (err) { + if (err.code === 'EBUSY' && i < retries - 1) { + // Exponentielle Backoff-Strategie + const waitTime = initialDelay * Math.pow(2, i); + console.log(`⏳ Datenbankdatei noch gesperrt (Versuch ${i + 1}/${retries}), warte ${waitTime}ms...`); + await sleep(waitTime); + continue; + } + if (i === retries - 1) { + console.error(`❌ Konnte Datenbankdatei nach ${retries} Versuchen nicht löschen.`); + console.error(' Bitte stellen Sie sicher, dass alle Prozesse geschlossen sind, die die Datenbank verwenden.'); + return false; + } + throw err; + } + } + return false; +} + +// Promise-Wrapper für sqlite3 Database.all +function dbAll(db, query, params = []) { + return new Promise((resolve, reject) => { + db.all(query, params, (err, rows) => { + if (err) reject(err); + else resolve(rows); + }); + }); +} + +// Promise-Wrapper für sqlite3 Database.close +function dbClose(db) { + return new Promise((resolve, reject) => { + db.close((err) => { + if (err) reject(err); + else resolve(); + }); + }); +} + +// Promise-Wrapper für sqlite3 Database-Konstruktor +function openDatabase(path, mode = sqlite3.OPEN_READONLY) { + return new Promise((resolve, reject) => { + const db = new sqlite3.Database(path, mode, (err) => { + if (err) reject(err); + else resolve(db); + }); + }); +} + +// Hauptfunktion als async +async function resetDatabase() { + try { + // Prüfe und beende Prozesse auf Ports 3333 und 3334 + console.log('🔍 Prüfe auf laufende Server auf Ports 3333 und 3334...\n'); + await checkAndKillPorts([3333, 3334]); + + // Prüfe ob Datenbank existiert und sichere ldap_config Daten + if (fs.existsSync(dbPath)) { + console.log('📁 Datenbankdatei gefunden...'); + + try { + // Temporäre Datenbankverbindung zum Lesen der ldap_config + const tempDb = await openDatabase(dbPath, sqlite3.OPEN_READONLY); + + try { + // Lese ldap_config Daten + const rows = await dbAll(tempDb, 'SELECT * FROM ldap_config'); + if (rows && rows.length > 0) { + savedLdapConfig = rows; + console.log(`💾 ${rows.length} LDAP-Konfiguration(en) gesichert\n`); + } else { + console.log('ℹ️ Keine LDAP-Konfiguration vorhanden\n'); + } + } catch (err) { + console.log('ℹ️ ldap_config Tabelle existiert nicht oder konnte nicht gelesen werden\n'); + } + + // Schließe die temporäre Datenbank + await dbClose(tempDb); + + // Warte etwas länger, damit die Datenbank wirklich geschlossen ist + await sleep(500); + + // Datenbank löschen + const success = await deleteDatabaseFile(); + if (success) { + createNewDatabase(); + } else { + console.error('❌ Konnte Datenbankdatei nicht löschen'); + process.exit(1); + } + } catch (err) { + console.log('⚠️ Konnte Datenbank nicht öffnen zum Lesen, fahre fort...\n'); + // Datenbank löschen + const success = await deleteDatabaseFile(); + if (success) { + createNewDatabase(); + } else { + console.error('❌ Konnte Datenbankdatei nicht löschen'); + process.exit(1); + } + } + } else { + console.log('ℹ️ Datenbankdatei existiert nicht, erstelle neue...\n'); + createNewDatabase(); + } + } catch (error) { + console.error('❌ Fehler beim Zurücksetzen der Datenbank:', error); + if (db) { + db.close(); + } + process.exit(1); + } +} + +// Starte das Reset +resetDatabase().catch((error) => { + console.error('❌ Unerwarteter Fehler:', error); + process.exit(1); +}); + + function createNewDatabase() { + // Neue Datenbank erstellen + db = new sqlite3.Database(dbPath); + + db.serialize(() => { + console.log('📊 Erstelle Tabellen...\n'); + + // Benutzer-Tabelle + db.run(`CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + firstname TEXT NOT NULL, + lastname TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'mitarbeiter', + last_week_start TEXT, + personalnummer TEXT, + wochenstunden REAL, + urlaubstage REAL, + overtime_offset_hours REAL DEFAULT 0, + ping_ip TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, (err) => { + if (err) console.error('Fehler bei users:', err); + else console.log('✅ Tabelle users erstellt'); + }); + + // Stundenerfassung-Tabelle + db.run(`CREATE TABLE timesheet_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + date TEXT NOT NULL, + start_time TEXT, + end_time TEXT, + break_minutes INTEGER DEFAULT 0, + total_hours REAL, + notes TEXT, + activity1_desc TEXT, + activity1_hours REAL, + activity1_project_number TEXT, + activity2_desc TEXT, + activity2_hours REAL, + activity2_project_number TEXT, + activity3_desc TEXT, + activity3_hours REAL, + activity3_project_number TEXT, + activity4_desc TEXT, + activity4_hours REAL, + activity4_project_number TEXT, + activity5_desc TEXT, + activity5_hours REAL, + activity5_project_number TEXT, + overtime_taken_hours REAL, + vacation_type TEXT, + sick_status INTEGER DEFAULT 0, + pause_start_time TEXT, + pause_end_time TEXT, + status TEXT DEFAULT 'offen', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) + )`, (err) => { + if (err) console.error('Fehler bei timesheet_entries:', err); + else console.log('✅ Tabelle timesheet_entries erstellt'); + }); + + // Wöchentliche Stundenzettel-Tabelle + db.run(`CREATE TABLE weekly_timesheets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + week_start TEXT NOT NULL, + week_end TEXT NOT NULL, + version INTEGER DEFAULT 1, + status TEXT DEFAULT 'eingereicht', + submitted_at DATETIME DEFAULT CURRENT_TIMESTAMP, + reviewed_by INTEGER, + reviewed_at DATETIME, + pdf_downloaded_at DATETIME, + pdf_downloaded_by INTEGER, + version_reason TEXT, + admin_comment TEXT, + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (reviewed_by) REFERENCES users(id), + FOREIGN KEY (pdf_downloaded_by) REFERENCES users(id) + )`, (err) => { + if (err) console.error('Fehler bei weekly_timesheets:', err); + else console.log('✅ Tabelle weekly_timesheets erstellt'); + }); + + // LDAP-Konfiguration-Tabelle + db.run(`CREATE TABLE ldap_config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + enabled INTEGER DEFAULT 0, + url TEXT, + bind_dn TEXT, + bind_password TEXT, + base_dn TEXT, + user_search_filter TEXT, + username_attribute TEXT DEFAULT 'cn', + firstname_attribute TEXT DEFAULT 'givenName', + lastname_attribute TEXT DEFAULT 'sn', + sync_interval INTEGER DEFAULT 0, + last_sync DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, (err) => { + if (err) console.error('Fehler bei ldap_config:', err); + else { + console.log('✅ Tabelle ldap_config erstellt'); + + // Stelle gesicherte LDAP-Konfiguration wieder her + if (savedLdapConfig.length > 0) { + console.log('🔄 Stelle LDAP-Konfiguration wieder her...'); + savedLdapConfig.forEach((config) => { + db.run(`INSERT INTO ldap_config ( + id, enabled, url, bind_dn, bind_password, base_dn, + user_search_filter, username_attribute, firstname_attribute, + lastname_attribute, sync_interval, last_sync, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ + config.id, + config.enabled, + config.url, + config.bind_dn, + config.bind_password, + config.base_dn, + config.user_search_filter, + config.username_attribute, + config.firstname_attribute, + config.lastname_attribute, + config.sync_interval, + config.last_sync, + config.created_at, + config.updated_at + ], (err) => { + if (err) { + console.error('Fehler beim Wiederherstellen der LDAP-Konfiguration:', err); + } else { + console.log(`✅ LDAP-Konfiguration (ID: ${config.id}) wiederhergestellt`); + } + }); + }); + } + } + }); + + // LDAP-Sync-Log-Tabelle + db.run(`CREATE TABLE ldap_sync_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sync_type TEXT NOT NULL, + status TEXT NOT NULL, + users_synced INTEGER DEFAULT 0, + error_message TEXT, + sync_started_at DATETIME DEFAULT CURRENT_TIMESTAMP, + sync_completed_at DATETIME + )`, (err) => { + if (err) console.error('Fehler bei ldap_sync_log:', err); + else console.log('✅ Tabelle ldap_sync_log erstellt'); + }); + + // Ping-Status-Tabelle für IP-basierte Zeiterfassung + db.run(`CREATE TABLE ping_status ( + user_id INTEGER NOT NULL, + date TEXT NOT NULL, + last_successful_ping DATETIME, + failed_ping_count INTEGER DEFAULT 0, + start_time_set INTEGER DEFAULT 0, + first_failed_ping_time DATETIME, + PRIMARY KEY (user_id, date), + FOREIGN KEY (user_id) REFERENCES users(id) + )`, (err) => { + if (err) console.error('Fehler bei ping_status:', err); + else console.log('✅ Tabelle ping_status erstellt'); + }); + + // Warte bis alle Tabellen erstellt sind + db.run('SELECT 1', (err) => { + if (err) { + console.error('Fehler beim Warten:', err); + return; + } + + console.log('\n👤 Erstelle Standard-Benutzer...\n'); + + // Standard Admin-Benutzer (Rolle als JSON-Array) + const adminPassword = bcrypt.hashSync('admin123', 10); + db.run(`INSERT INTO users (id, username, password, firstname, lastname, role) + VALUES (1, 'admin', ?, 'System', 'Administrator', ?)`, + [adminPassword, JSON.stringify(['admin'])], (err) => { + if (err) console.error('Fehler beim Erstellen des Admin-Users:', err); + else console.log('✅ Admin-User erstellt (admin / admin123)'); + }); + + // Standard Verwaltungs-Benutzer (Rolle als JSON-Array) + const verwaltungPassword = bcrypt.hashSync('verwaltung123', 10); + db.run(`INSERT INTO users (id, username, password, firstname, lastname, role) + VALUES (2, 'verwaltung', ?, 'Verwaltung', 'User', ?)`, + [verwaltungPassword, JSON.stringify(['verwaltung'])], (err) => { + if (err) console.error('Fehler beim Erstellen des Verwaltungs-Users:', err); + else console.log('✅ Verwaltungs-User erstellt (verwaltung / verwaltung123)'); + }); + + // Test-Mitarbeiter (optional, Rolle als JSON-Array) + const mitarbeiterPassword = bcrypt.hashSync('test123', 10); + db.run(`INSERT INTO users (id, username, password, firstname, lastname, role, wochenstunden, urlaubstage) + VALUES (3, 'test', ?, 'Test', 'Mitarbeiter', ?, 40, 25)`, + [mitarbeiterPassword, JSON.stringify(['mitarbeiter'])], (err) => { + if (err && !err.message.includes('UNIQUE constraint')) { + console.error('Fehler beim Erstellen des Test-Users:', err); + } else if (!err) { + console.log('✅ Test-Mitarbeiter erstellt (test / test123, 40h/Woche, 25 Urlaubstage)'); + } + }); + + // Warte bis alle Benutzer erstellt sind + setTimeout(() => { + console.log('\n✨ Datenbank erfolgreich zurückgesetzt!\n'); + console.log('📋 Standard-Zugangsdaten:'); + console.log(' Admin: admin / admin123'); + console.log(' Verwaltung: verwaltung / verwaltung123'); + console.log(' Test-User: test / test123\n'); + + db.close((err) => { + if (err) { + console.error('Fehler beim Schließen der Datenbank:', err); + process.exit(1); + } else { + console.log('✅ Datenbank geschlossen\n'); + process.exit(0); + } + }); + }, 500); + }); + }); + } diff --git a/routes/admin-ldap.js b/routes/admin-ldap.js new file mode 100644 index 0000000..4e84c2e --- /dev/null +++ b/routes/admin-ldap.js @@ -0,0 +1,167 @@ +// LDAP Admin Routes + +const { db } = require('../database'); +const LDAPService = require('../ldap-service'); +const { requireAdmin } = require('../middleware/auth'); + +// Routes registrieren +function registerAdminLDAPRoutes(app) { + // LDAP-Konfiguration abrufen + app.get('/admin/ldap/config', requireAdmin, (req, res) => { + db.get('SELECT * FROM ldap_config WHERE id = 1', (err, config) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Abrufen der Konfiguration' }); + } + + // Passwort nicht zurückgeben + if (config) { + delete config.bind_password; + } + + res.json({ config: config || null }); + }); + }); + + // LDAP-Konfiguration speichern + app.post('/admin/ldap/config', requireAdmin, (req, res) => { + const { + enabled, + url, + bind_dn, + bind_password, + base_dn, + user_search_filter, + username_attribute, + firstname_attribute, + lastname_attribute, + sync_interval + } = req.body; + + // Validierung - nur wenn aktiviert + if (enabled && (!url || !base_dn)) { + return res.status(400).json({ error: 'URL und Base DN sind erforderlich wenn LDAP aktiviert ist' }); + } + + // Prüfe ob Konfiguration bereits existiert + db.get('SELECT id FROM ldap_config WHERE id = 1', (err, existing) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Prüfen der Konfiguration' }); + } + + const configData = { + enabled: enabled ? 1 : 0, + url: url.trim(), + bind_dn: bind_dn ? bind_dn.trim() : null, + 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', + firstname_attribute: firstname_attribute ? firstname_attribute.trim() : 'givenName', + lastname_attribute: lastname_attribute ? lastname_attribute.trim() : 'sn', + sync_interval: parseInt(sync_interval) || 0, + updated_at: new Date().toISOString() + }; + + if (existing) { + // Update - Passwort nur aktualisieren wenn angegeben + if (configData.bind_password) { + db.run( + `UPDATE ldap_config SET + enabled = ?, url = ?, bind_dn = ?, bind_password = ?, base_dn = ?, + user_search_filter = ?, username_attribute = ?, firstname_attribute = ?, + lastname_attribute = ?, sync_interval = ?, updated_at = ? + WHERE id = 1`, + [ + configData.enabled, configData.url, configData.bind_dn, configData.bind_password, + configData.base_dn, configData.user_search_filter, configData.username_attribute, + configData.firstname_attribute, configData.lastname_attribute, configData.sync_interval, + configData.updated_at + ], + (err) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Speichern der Konfiguration' }); + } + res.json({ success: true }); + } + ); + } else { + // Passwort nicht ändern + db.run( + `UPDATE ldap_config SET + enabled = ?, url = ?, bind_dn = ?, base_dn = ?, + user_search_filter = ?, username_attribute = ?, firstname_attribute = ?, + lastname_attribute = ?, sync_interval = ?, updated_at = ? + WHERE id = 1`, + [ + configData.enabled, configData.url, configData.bind_dn, + configData.base_dn, configData.user_search_filter, configData.username_attribute, + configData.firstname_attribute, configData.lastname_attribute, configData.sync_interval, + configData.updated_at + ], + (err) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Speichern der Konfiguration' }); + } + res.json({ success: true }); + } + ); + } + } else { + // Insert + db.run( + `INSERT INTO ldap_config ( + enabled, url, bind_dn, bind_password, base_dn, user_search_filter, + username_attribute, firstname_attribute, lastname_attribute, sync_interval, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + configData.enabled, configData.url, configData.bind_dn, configData.bind_password, + configData.base_dn, configData.user_search_filter, configData.username_attribute, + configData.firstname_attribute, configData.lastname_attribute, configData.sync_interval, + configData.updated_at + ], + (err) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Erstellen der Konfiguration' }); + } + res.json({ success: true }); + } + ); + } + }); + }); + + // Manuelle LDAP-Synchronisation starten + app.post('/admin/ldap/sync', requireAdmin, (req, res) => { + LDAPService.performSync('manual', (err, result) => { + if (err) { + return res.status(500).json({ + error: err.message || 'Fehler bei der Synchronisation', + synced: result ? result.synced : 0, + errors: result ? result.errors : [] + }); + } + res.json({ + success: true, + synced: result.synced, + errors: result.errors || [] + }); + }); + }); + + // Sync-Log abrufen + app.get('/admin/ldap/sync/log', requireAdmin, (req, res) => { + const limit = parseInt(req.query.limit) || 10; + db.all( + 'SELECT * FROM ldap_sync_log ORDER BY sync_started_at DESC LIMIT ?', + [limit], + (err, logs) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Abrufen des Logs' }); + } + res.json({ logs: logs || [] }); + } + ); + }); +} + +module.exports = registerAdminLDAPRoutes; diff --git a/routes/admin.js b/routes/admin.js new file mode 100644 index 0000000..f8cbe54 --- /dev/null +++ b/routes/admin.js @@ -0,0 +1,154 @@ +// Admin Routes + +const bcrypt = require('bcryptjs'); +const { db } = require('../database'); +const { requireAdmin } = require('../middleware/auth'); + +// Routes registrieren +function registerAdminRoutes(app) { + // Admin-Bereich + app.get('/admin', requireAdmin, (req, res) => { + db.all('SELECT id, username, firstname, lastname, role, personalnummer, wochenstunden, urlaubstage, created_at FROM users ORDER BY created_at DESC', + (err, users) => { + // LDAP-Konfiguration und Sync-Log abrufen + db.get('SELECT * FROM ldap_config WHERE id = 1', (err, ldapConfig) => { + db.all('SELECT * FROM ldap_sync_log ORDER BY sync_started_at DESC LIMIT 10', (err, syncLogs) => { + // Parse Rollen für jeden User + const usersWithRoles = (users || []).map(u => { + let roles = []; + try { + roles = JSON.parse(u.role); + if (!Array.isArray(roles)) { + roles = [u.role]; + } + } catch (e) { + roles = [u.role || 'mitarbeiter']; + } + return { ...u, roles }; + }); + + res.render('admin', { + users: usersWithRoles, + ldapConfig: ldapConfig || null, + syncLogs: syncLogs || [], + user: { + firstname: req.session.firstname, + lastname: req.session.lastname, + roles: req.session.roles || [], + currentRole: req.session.currentRole || 'admin' + } + }); + }); + }); + }); + }); + + // Benutzer erstellen + app.post('/admin/users', requireAdmin, (req, res) => { + const { username, password, firstname, lastname, roles, personalnummer, wochenstunden, urlaubstage } = req.body; + const hashedPassword = bcrypt.hashSync(password, 10); + + // Normalisiere die optionalen Felder + const normalizedPersonalnummer = personalnummer && personalnummer.trim() !== '' ? personalnummer.trim() : null; + const normalizedWochenstunden = wochenstunden && wochenstunden !== '' ? parseFloat(wochenstunden) : null; + const normalizedUrlaubstage = urlaubstage && urlaubstage !== '' ? parseFloat(urlaubstage) : null; + + // Rollen verarbeiten: Erwarte Array, konvertiere zu JSON-String + let rolesArray = []; + if (Array.isArray(roles)) { + rolesArray = roles.filter(r => r && ['mitarbeiter', 'verwaltung', 'admin'].includes(r)); + } else if (roles) { + // Fallback: Einzelne Rolle als Array + rolesArray = [roles]; + } + + // Mindestens eine Rolle erforderlich + if (rolesArray.length === 0) { + rolesArray = ['mitarbeiter']; // Standard-Rolle + } + + const rolesJson = JSON.stringify(rolesArray); + + db.run('INSERT INTO users (username, password, firstname, lastname, role, personalnummer, wochenstunden, urlaubstage) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', + [username, hashedPassword, firstname, lastname, rolesJson, normalizedPersonalnummer, normalizedWochenstunden, normalizedUrlaubstage], + (err) => { + if (err) { + return res.status(400).json({ error: 'Benutzername existiert bereits' }); + } + res.json({ success: true }); + }); + }); + + // Benutzer löschen + app.delete('/admin/users/:id', requireAdmin, (req, res) => { + const userId = req.params.id; + + // Admin darf sich nicht selbst löschen + if (userId == req.session.userId) { + return res.status(400).json({ error: 'Sie können sich nicht selbst löschen' }); + } + + db.run('DELETE FROM users WHERE id = ?', [userId], (err) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Löschen' }); + } + res.json({ success: true }); + }); + }); + + // Benutzer aktualisieren (Personalnummer, Wochenstunden, Urlaubstage, Rollen) + app.put('/admin/users/:id', requireAdmin, (req, res) => { + const userId = req.params.id; + const { personalnummer, wochenstunden, urlaubstage, roles } = req.body; + + // Rollen verarbeiten falls vorhanden + let rolesJson = null; + if (roles !== undefined) { + let rolesArray = []; + if (Array.isArray(roles)) { + rolesArray = roles.filter(r => r && ['mitarbeiter', 'verwaltung', 'admin'].includes(r)); + } + // Mindestens eine Rolle erforderlich + if (rolesArray.length === 0) { + return res.status(400).json({ error: 'Mindestens eine Rolle ist erforderlich' }); + } + rolesJson = JSON.stringify(rolesArray); + } + + // SQL-Query dynamisch zusammenstellen + if (rolesJson !== null) { + // Aktualisiere auch Rollen + db.run('UPDATE users SET personalnummer = ?, wochenstunden = ?, urlaubstage = ?, role = ? WHERE id = ?', + [ + personalnummer || null, + wochenstunden ? parseFloat(wochenstunden) : null, + urlaubstage ? parseFloat(urlaubstage) : null, + rolesJson, + userId + ], + (err) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Aktualisieren' }); + } + res.json({ success: true }); + }); + } else { + // Nur andere Felder aktualisieren + db.run('UPDATE users SET personalnummer = ?, wochenstunden = ?, urlaubstage = ? WHERE id = ?', + [ + personalnummer || null, + wochenstunden ? parseFloat(wochenstunden) : null, + urlaubstage ? parseFloat(urlaubstage) : null, + userId + ], + (err) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Aktualisieren' }); + } + res.json({ success: true }); + }); + } + }); +} + +module.exports = registerAdminRoutes; diff --git a/routes/auth.js b/routes/auth.js new file mode 100644 index 0000000..3c530ae --- /dev/null +++ b/routes/auth.js @@ -0,0 +1,132 @@ +// Authentifizierungs-Routes + +const bcrypt = require('bcryptjs'); +const { db } = require('../database'); +const LDAPService = require('../ldap-service'); +const { getDefaultRole } = require('../helpers/utils'); + +// Helper-Funktion für erfolgreiche Anmeldung +function handleSuccessfulLogin(req, res, user, rememberMe = false) { + // Rollen als JSON-Array parsen + let roles = []; + try { + roles = JSON.parse(user.role); + if (!Array.isArray(roles)) { + // Fallback: Falls kein Array, erstelle Array mit vorhandener Rolle + roles = [user.role]; + } + } catch (e) { + // Fallback: Falls kein JSON, erstelle Array mit vorhandener Rolle + roles = [user.role || 'mitarbeiter']; + } + + // Standard-Rolle bestimmen: Immer "mitarbeiter" wenn vorhanden, sonst höchste Priorität + let defaultRole; + if (roles.includes('mitarbeiter')) { + defaultRole = 'mitarbeiter'; + } else { + defaultRole = getDefaultRole(roles); + } + + req.session.userId = user.id; + req.session.username = user.username; + req.session.roles = roles; + req.session.currentRole = defaultRole; + req.session.firstname = user.firstname; + req.session.lastname = user.lastname; + + // Session-Gültigkeit setzen: 30 Tage wenn "Angemeldet bleiben" aktiviert, sonst 24 Stunden + if (rememberMe) { + req.session.cookie.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 Tage + } else { + req.session.cookie.maxAge = 24 * 60 * 60 * 1000; // 24 Stunden + } + + // Redirect: Immer zu Dashboard wenn Mitarbeiter-Rolle vorhanden, sonst basierend auf Standard-Rolle + if (roles.includes('mitarbeiter')) { + res.redirect('/dashboard'); + } else if (defaultRole === 'admin') { + res.redirect('/admin'); + } else if (defaultRole === 'verwaltung') { + res.redirect('/verwaltung'); + } else { + res.redirect('/dashboard'); + } +} + +// Routes registrieren +function registerAuthRoutes(app) { + // Login-Seite + app.get('/login', (req, res) => { + res.render('login', { error: null }); + }); + + // Login-Verarbeitung + app.post('/login', (req, res) => { + const { username, password, remember_me } = req.body; + const rememberMe = remember_me === 'on' || remember_me === true; + + // Prüfe ob LDAP aktiviert ist + LDAPService.getConfig((err, ldapConfig) => { + if (err) { + console.error('Fehler beim Abrufen der LDAP-Konfiguration:', err); + } + + const isLDAPEnabled = ldapConfig && ldapConfig.enabled === 1; + + // Wenn LDAP aktiviert ist, authentifiziere gegen LDAP + if (isLDAPEnabled) { + LDAPService.authenticate(username, password, (authErr, authSuccess) => { + if (authErr || !authSuccess) { + // LDAP-Authentifizierung fehlgeschlagen - prüfe lokale Datenbank als Fallback + // 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 { + res.render('login', { error: 'Ungültiger Benutzername oder Passwort' }); + } + }); + } 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) => { + if (err || !user) { + return res.render('login', { error: 'Benutzer nicht in der Datenbank gefunden. Bitte führen Sie eine LDAP-Synchronisation durch.' }); + } + + handleSuccessfulLogin(req, res, user, rememberMe); + }); + } + }); + } else { + // LDAP nicht aktiviert - verwende lokale Authentifizierung + // 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' }); + } + + if (bcrypt.compareSync(password, user.password)) { + handleSuccessfulLogin(req, res, user, rememberMe); + } else { + res.render('login', { error: 'Ungültiger Benutzername oder Passwort' }); + } + }); + } + }); + }); + + // Logout + app.get('/logout', (req, res) => { + req.session.destroy(); + res.redirect('/login'); + }); +} + +module.exports = registerAuthRoutes; diff --git a/routes/dashboard.js b/routes/dashboard.js new file mode 100644 index 0000000..3f0e256 --- /dev/null +++ b/routes/dashboard.js @@ -0,0 +1,35 @@ +// Dashboard-Route + +const { hasRole } = require('../helpers/utils'); +const { requireAuth } = require('../middleware/auth'); + +// Routes registrieren +function registerDashboardRoutes(app) { + // Dashboard für Mitarbeiter + app.get('/dashboard', requireAuth, (req, res) => { + // Prüfe ob User Mitarbeiter-Rolle hat + if (!hasRole(req, 'mitarbeiter')) { + // Wenn User keine Mitarbeiter-Rolle hat, aber andere Rollen, redirecte entsprechend + if (hasRole(req, 'admin')) { + return res.redirect('/admin'); + } + if (hasRole(req, 'verwaltung')) { + return res.redirect('/verwaltung'); + } + return res.status(403).send('Zugriff verweigert'); + } + + res.render('dashboard', { + user: { + id: req.session.userId, + firstname: req.session.firstname, + lastname: req.session.lastname, + username: req.session.username, + roles: req.session.roles || [], + currentRole: req.session.currentRole || 'mitarbeiter' + } + }); + }); +} + +module.exports = registerDashboardRoutes; diff --git a/routes/timesheet.js b/routes/timesheet.js new file mode 100644 index 0000000..2050666 --- /dev/null +++ b/routes/timesheet.js @@ -0,0 +1,379 @@ +// Timesheet API Routes + +const { db } = require('../database'); +const { requireAuth, requireVerwaltung } = require('../middleware/auth'); +const { generatePDF } = require('../services/pdf-service'); +const { getHolidaysForDateRange } = require('../services/feiertage-service'); + +// Routes registrieren +function registerTimesheetRoutes(app) { + // API: Stundenerfassung speichern + app.post('/api/timesheet/save', requireAuth, (req, res) => { + const { + date, start_time, end_time, break_minutes, notes, + activity1_desc, activity1_hours, activity1_project_number, + activity2_desc, activity2_hours, activity2_project_number, + activity3_desc, activity3_hours, activity3_project_number, + activity4_desc, activity4_hours, activity4_project_number, + activity5_desc, activity5_hours, activity5_project_number, + overtime_taken_hours, vacation_type, sick_status + } = req.body; + const userId = req.session.userId; + + // Normalisiere end_time: Leere Strings werden zu null + const normalizedEndTime = (end_time && typeof end_time === 'string' && end_time.trim() !== '') ? end_time.trim() : (end_time || null); + const normalizedStartTime = (start_time && typeof start_time === 'string' && start_time.trim() !== '') ? start_time.trim() : (start_time || null); + + // Normalisiere sick_status: Boolean oder 1/0 zu Boolean + const isSick = sick_status === true || sick_status === 1 || sick_status === 'true' || sick_status === '1'; + + // User-Daten laden (für Überstunden-Berechnung) + db.get('SELECT wochenstunden FROM users WHERE id = ?', [userId], (err, user) => { + if (err) { + console.error('Fehler beim Laden der User-Daten:', err); + return res.status(500).json({ error: 'Fehler beim Laden der User-Daten' }); + } + + const wochenstunden = user?.wochenstunden || 0; + const overtimeValue = overtime_taken_hours ? parseFloat(overtime_taken_hours) : 0; + const fullDayHours = wochenstunden > 0 ? wochenstunden / 5 : 0; + + // Überstunden-Logik: Prüfe ob ganzer Tag oder weniger + let isFullDayOvertime = false; + if (overtimeValue > 0 && fullDayHours > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01) { + isFullDayOvertime = true; + } + + // Gesamtstunden berechnen (aus Start- und Endzeit, nicht aus Tätigkeiten) + // Wenn ganzer Tag Urlaub oder Krank, dann zählt dieser als 8 Stunden normale Arbeitszeit + let total_hours = 0; + let finalActivity1Desc = activity1_desc; + let finalActivity1Hours = parseFloat(activity1_hours) || 0; + let finalActivity2Desc = activity2_desc; + let finalActivity3Desc = activity3_desc; + let finalActivity4Desc = activity4_desc; + let finalActivity5Desc = activity5_desc; + let finalStartTime = normalizedStartTime; + let finalEndTime = normalizedEndTime; + + // Überstunden-Logik: Bei vollem Tag Überstunden + if (isFullDayOvertime) { + total_hours = 0; + finalStartTime = null; + finalEndTime = null; + // Keine Tätigkeit setzen - Überstunden werden über overtime_taken_hours in der PDF angezeigt + } else if (vacation_type === 'full') { + total_hours = 8; // Ganzer Tag Urlaub = 8 Stunden normale Arbeitszeit + } else if (isSick) { + total_hours = 8; // Krank = 8 Stunden normale Arbeitszeit + finalActivity1Desc = 'Krank'; + finalActivity1Hours = 8; + } else if (normalizedStartTime && normalizedEndTime) { + const start = new Date(`2000-01-01T${normalizedStartTime}`); + const end = new Date(`2000-01-01T${normalizedEndTime}`); + const diffMs = end - start; + total_hours = (diffMs / (1000 * 60 * 60)) - (break_minutes / 60); + // Bei halbem Tag Urlaub: total_hours bleibt die tatsächlich gearbeiteten Stunden + // Die 4 Stunden Urlaub werden nur in der Überstunden-Berechnung hinzugezählt + } + + // Überstunden werden nicht mehr als Tätigkeit hinzugefügt + // Sie werden über overtime_taken_hours in der PDF angezeigt + + // Prüfen ob Eintrag existiert - verwende den neuesten Eintrag falls mehrere existieren + db.get('SELECT id FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1', + [userId, date], (err, row) => { + if (row) { + // Update + db.run(`UPDATE timesheet_entries + SET start_time = ?, end_time = ?, break_minutes = ?, total_hours = ?, notes = ?, + activity1_desc = ?, activity1_hours = ?, activity1_project_number = ?, + activity2_desc = ?, activity2_hours = ?, activity2_project_number = ?, + activity3_desc = ?, activity3_hours = ?, activity3_project_number = ?, + activity4_desc = ?, activity4_hours = ?, activity4_project_number = ?, + activity5_desc = ?, activity5_hours = ?, activity5_project_number = ?, + overtime_taken_hours = ?, vacation_type = ?, sick_status = ?, + updated_at = CURRENT_TIMESTAMP + WHERE id = ?`, + [ + finalStartTime, finalEndTime, break_minutes, total_hours, notes, + finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null, + finalActivity2Desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null, + finalActivity3Desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null, + finalActivity4Desc || null, parseFloat(activity4_hours) || 0, activity4_project_number || null, + finalActivity5Desc || null, parseFloat(activity5_hours) || 0, activity5_project_number || null, + overtime_taken_hours ? parseFloat(overtime_taken_hours) : null, + vacation_type || null, + isSick ? 1 : 0, + row.id + ], + (err) => { + if (err) { + console.error('Fehler beim Update:', err); + return res.status(500).json({ error: 'Fehler beim Speichern: ' + err.message }); + } + res.json({ success: true, total_hours }); + }); + } else { + // Insert + db.run(`INSERT INTO timesheet_entries + (user_id, date, start_time, end_time, break_minutes, total_hours, notes, + activity1_desc, activity1_hours, activity1_project_number, + activity2_desc, activity2_hours, activity2_project_number, + activity3_desc, activity3_hours, activity3_project_number, + activity4_desc, activity4_hours, activity4_project_number, + activity5_desc, activity5_hours, activity5_project_number, + overtime_taken_hours, vacation_type, sick_status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + userId, date, finalStartTime, finalEndTime, break_minutes, total_hours, notes, + finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null, + finalActivity2Desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null, + finalActivity3Desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null, + finalActivity4Desc || null, parseFloat(activity4_hours) || 0, activity4_project_number || null, + finalActivity5Desc || null, parseFloat(activity5_hours) || 0, activity5_project_number || null, + overtime_taken_hours ? parseFloat(overtime_taken_hours) : null, + vacation_type || null, + isSick ? 1 : 0 + ], + (err) => { + if (err) { + console.error('Fehler beim Insert:', err); + return res.status(500).json({ error: 'Fehler beim Speichern: ' + err.message }); + } + res.json({ success: true, total_hours }); + }); + } + }); + }); + }); + + // API: Feiertage für einen Zeitraum (Dashboard-Anzeige) + app.get('/api/timesheet/holidays', requireAuth, (req, res) => { + const { week_start, week_end } = req.query; + if (!week_start || !week_end) { + return res.status(400).json({ error: 'week_start und week_end erforderlich' }); + } + getHolidaysForDateRange(week_start, week_end) + .then((set) => res.json({ dates: [...set] })) + .catch(() => res.json({ dates: [] })); + }); + + // API: Stundenerfassung für Woche laden + app.get('/api/timesheet/week/:weekStart', requireAuth, (req, res) => { + const userId = req.session.userId; + const weekStart = req.params.weekStart; + + // Berechne Wochenende + const startDate = new Date(weekStart); + const endDate = new Date(startDate); + endDate.setDate(endDate.getDate() + 6); + const weekEnd = endDate.toISOString().split('T')[0]; + + // Prüfe ob die Woche bereits eingereicht wurde (aber ermögliche Bearbeitung) + db.get(`SELECT id, version FROM weekly_timesheets + WHERE user_id = ? AND week_start = ? AND week_end = ? + ORDER BY version DESC LIMIT 1`, + [userId, weekStart, weekEnd], + (err, weeklySheet) => { + const hasSubmittedVersion = !!weeklySheet; + const latestVersion = weeklySheet ? weeklySheet.version : 0; + + // Lade alle Einträge für die Woche + db.all(`SELECT * FROM timesheet_entries + WHERE user_id = ? AND date >= ? AND date <= ? + ORDER BY date`, + [userId, weekStart, weekEnd], + (err, entries) => { + // Füge Status-Info hinzu (Bearbeitung ist immer möglich) + const entriesWithStatus = (entries || []).map(entry => ({ + ...entry, + week_submitted: false, // Immer false, damit Bearbeitung möglich ist + latest_version: latestVersion, + has_existing_version: latestVersion > 0 + })); + res.json(entriesWithStatus); + }); + }); + }); + + // API: Woche abschicken + app.post('/api/timesheet/submit', requireAuth, (req, res) => { + const { week_start, week_end, version_reason } = req.body; + const userId = req.session.userId; + + // Validierung: Prüfen ob alle 7 Tage der Woche ausgefüllt sind + db.all(`SELECT id, date, start_time, end_time, vacation_type, sick_status, overtime_taken_hours, updated_at FROM timesheet_entries + WHERE user_id = ? AND date >= ? AND date <= ? + ORDER BY date, updated_at DESC, id DESC`, + [userId, week_start, week_end], + (err, entries) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Prüfen der Daten' }); + } + + // Erstelle Set mit vorhandenen Daten + // WICHTIG: Wenn mehrere Einträge für denselben Tag existieren, nimm den neuesten + const entriesByDate = {}; + entries.forEach(entry => { + const existing = entriesByDate[entry.date]; + // Wenn noch kein Eintrag existiert oder dieser neuer ist, verwende ihn + if (!existing) { + entriesByDate[entry.date] = entry; + } else { + // Vergleiche updated_at (falls vorhanden) oder id (höhere ID = neuer) + const existingTime = existing.updated_at ? new Date(existing.updated_at).getTime() : 0; + const currentTime = entry.updated_at ? new Date(entry.updated_at).getTime() : 0; + if (currentTime > existingTime || (currentTime === existingTime && entry.id > existing.id)) { + entriesByDate[entry.date] = entry; + } + } + }); + + // Prüfe nur Werktage (Montag-Freitag, erste 5 Tage) + // Samstag und Sonntag sind optional + // Bei ganztägigem Urlaub (vacation_type = 'full') ist der Tag als ausgefüllt zu betrachten + // Bei 8 Überstunden (ganzer Tag) ist der Tag auch als ausgefüllt zu betrachten + // week_start ist bereits im Format YYYY-MM-DD + const startDateParts = week_start.split('-'); + const startYear = parseInt(startDateParts[0]); + const startMonth = parseInt(startDateParts[1]) - 1; // Monat ist 0-basiert + const startDay = parseInt(startDateParts[2]); + + // User-Daten laden für Überstunden-Berechnung + db.get('SELECT wochenstunden FROM users WHERE id = ?', [userId], (err, user) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Laden der User-Daten' }); + } + + const wochenstunden = user?.wochenstunden || 0; + const fullDayHours = wochenstunden > 0 ? wochenstunden / 5 : 8; + + // Feiertage laden: Feiertag zählt als ausgefüllt (kein Start/Ende nötig) + getHolidaysForDateRange(week_start, week_end) + .catch(() => new Set()) + .then((holidaySet) => { + let missingDays = []; + + for (let i = 0; i < 5; i++) { + // Datum direkt berechnen ohne Zeitzonenprobleme + const date = new Date(startYear, startMonth, startDay + i); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const dateStr = `${year}-${month}-${day}`; + const entry = entriesByDate[dateStr]; + + // Feiertag zählt als ausgefüllt + if (holidaySet.has(dateStr)) { + continue; // Tag ist ausgefüllt + } + + // Wenn ganztägiger Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten + const isSick = entry && (entry.sick_status === 1 || entry.sick_status === true); + if (entry && (entry.vacation_type === 'full' || isSick)) { + continue; // Tag ist ausgefüllt + } + + // Prüfe ob 8 Überstunden (ganzer Tag) eingetragen sind + const overtimeValue = entry && entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0; + const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01; + + if (isFullDayOvertime) { + continue; // Tag ist ausgefüllt (8 Überstunden = ganzer Tag) + } + + // Bei halbem Tag Urlaub oder keinem Urlaub müssen Start- und Endzeit vorhanden sein + // start_time und end_time könnten null, undefined oder leer strings sein + const hasStartTime = entry && entry.start_time && entry.start_time.toString().trim() !== ''; + const hasEndTime = entry && entry.end_time && entry.end_time.toString().trim() !== ''; + + if (!entry || !hasStartTime || !hasEndTime) { + missingDays.push(dateStr); + } + } + + if (missingDays.length > 0) { + return res.status(400).json({ + error: `Nicht alle Werktage (Montag bis Freitag) sind ausgefüllt. Fehlende Tage: ${missingDays.join(', ')}. Bitte füllen Sie alle Werktage mit Start- und Endzeit aus. Wochenende ist optional.` + }); + } + + // Alle Tage ausgefüllt - Woche abschicken (immer neue Version erstellen) + // Prüfe welche Version die letzte ist + db.get(`SELECT MAX(version) as max_version FROM weekly_timesheets + WHERE user_id = ? AND week_start = ? AND week_end = ?`, + [userId, week_start, week_end], + (err, result) => { + if (err) return res.status(500).json({ error: 'Fehler beim Prüfen der Version' }); + + const maxVersion = result && result.max_version ? result.max_version : 0; + const newVersion = maxVersion + 1; + + // Wenn bereits eine Version existiert, ist version_reason erforderlich + if (maxVersion > 0 && (!version_reason || version_reason.trim() === '')) { + return res.status(400).json({ + error: 'Bitte geben Sie einen Grund für die neue Version an.' + }); + } + + // Neue Version erstellen (nicht überschreiben) + db.run(`INSERT INTO weekly_timesheets (user_id, week_start, week_end, version, status, version_reason) + VALUES (?, ?, ?, ?, 'eingereicht', ?)`, + [userId, week_start, week_end, newVersion, version_reason ? version_reason.trim() : null], + (err) => { + if (err) return res.status(500).json({ error: 'Fehler beim Abschicken' }); + + // Status der Einträge aktualisieren (optional - für Nachverfolgung) + db.run(`UPDATE timesheet_entries + SET status = 'eingereicht' + WHERE user_id = ? AND date >= ? AND date <= ?`, + [userId, week_start, week_end], + (err) => { + if (err) return res.status(500).json({ error: 'Fehler beim Aktualisieren des Status' }); + res.json({ success: true, version: newVersion }); + }); + }); + }); + }); + }); + }); + }); + + // API: PDF Download-Info abrufen + app.get('/api/timesheet/download-info/:id', requireVerwaltung, (req, res) => { + const timesheetId = req.params.id; + + db.get(`SELECT wt.pdf_downloaded_at, + dl.firstname as downloaded_by_firstname, + dl.lastname as downloaded_by_lastname + FROM weekly_timesheets wt + LEFT JOIN users dl ON wt.pdf_downloaded_by = dl.id + WHERE wt.id = ?`, [timesheetId], (err, result) => { + + if (err) { + console.error('Fehler beim Abrufen der Download-Info:', err); + return res.status(500).json({ error: 'Fehler beim Abrufen der Informationen' }); + } + + if (!result) { + return res.status(404).json({ error: 'Stundenzettel nicht gefunden' }); + } + + res.json({ + downloaded: !!result.pdf_downloaded_at, + downloaded_at: result.pdf_downloaded_at, + downloaded_by_firstname: result.downloaded_by_firstname, + downloaded_by_lastname: result.downloaded_by_lastname + }); + }); + }); + + // API: PDF generieren + app.get('/api/timesheet/pdf/:id', requireVerwaltung, (req, res) => { + const timesheetId = req.params.id; + generatePDF(timesheetId, req, res); + }); +} + +module.exports = registerTimesheetRoutes; diff --git a/routes/user.js b/routes/user.js new file mode 100644 index 0000000..5fef85d --- /dev/null +++ b/routes/user.js @@ -0,0 +1,468 @@ +// User API Routes + +const { db } = require('../database'); +const { hasRole, getCurrentDate } = require('../helpers/utils'); +const { requireAuth } = require('../middleware/auth'); +const { getHolidaysForDateRange } = require('../services/feiertage-service'); + +// Routes registrieren +function registerUserRoutes(app) { + // API: Letzte bearbeitete Woche abrufen + app.get('/api/user/last-week', requireAuth, (req, res) => { + const userId = req.session.userId; + + db.get('SELECT last_week_start FROM users WHERE id = ?', [userId], (err, user) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Abrufen der letzten Woche' }); + } + + res.json({ last_week_start: user?.last_week_start || null }); + }); + }); + + // API: Letzte bearbeitete Woche speichern + app.post('/api/user/last-week', requireAuth, (req, res) => { + const userId = req.session.userId; + const { week_start } = req.body; + + if (!week_start) { + return res.status(400).json({ error: 'week_start ist erforderlich' }); + } + + db.run('UPDATE users SET last_week_start = ? WHERE id = ?', + [week_start, userId], + (err) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Speichern der letzten Woche' }); + } + res.json({ success: true }); + }); + }); + + // API: User-Daten abrufen (Wochenstunden) + app.get('/api/user/data', requireAuth, (req, res) => { + const userId = req.session.userId; + + db.get('SELECT wochenstunden FROM users WHERE id = ?', [userId], (err, user) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' }); + } + + res.json({ wochenstunden: user?.wochenstunden || 0 }); + }); + }); + + // API: Client-IP abrufen + app.get('/api/user/client-ip', requireAuth, (req, res) => { + // Versuche verschiedene Methoden, um die Client-IP zu erhalten + const clientIp = req.ip || + req.connection.remoteAddress || + req.socket.remoteAddress || + (req.headers['x-forwarded-for'] ? req.headers['x-forwarded-for'].split(',')[0].trim() : null) || + req.headers['x-real-ip'] || + 'unknown'; + + // Entferne IPv6-Präfix falls vorhanden (::ffff:192.168.1.1 -> 192.168.1.1) + const cleanIp = clientIp.replace(/^::ffff:/, ''); + + res.json({ client_ip: cleanIp }); + }); + + // API: Ping-IP abrufen + app.get('/api/user/ping-ip', requireAuth, (req, res) => { + const userId = req.session.userId; + + db.get('SELECT ping_ip FROM users WHERE id = ?', [userId], (err, user) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Abrufen der IP-Adresse' }); + } + + res.json({ ping_ip: user?.ping_ip || null }); + }); + }); + + // API: Ping-IP speichern + app.post('/api/user/ping-ip', requireAuth, (req, res) => { + const userId = req.session.userId; + const { ping_ip } = req.body; + + // Validierung: IPv4 Format (einfache Prüfung) + const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/; + if (ping_ip && ping_ip.trim() !== '' && !ipv4Regex.test(ping_ip.trim())) { + return res.status(400).json({ error: 'Ungültige IP-Adresse. Bitte geben Sie eine gültige IPv4-Adresse ein.' }); + } + + // Normalisiere: Leere Strings werden zu null + const normalizedPingIp = (ping_ip && ping_ip.trim() !== '') ? ping_ip.trim() : null; + + db.run('UPDATE users SET ping_ip = ? WHERE id = ?', [normalizedPingIp, userId], (err) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Speichern der IP-Adresse' }); + } + + // Wenn IP entfernt wurde, lösche auch den Ping-Status für heute + if (!normalizedPingIp) { + const currentDate = getCurrentDate(); + db.run('DELETE FROM ping_status WHERE user_id = ? AND date = ?', [userId, currentDate], (err) => { + // Fehler ignorieren + }); + } + + res.json({ success: true, ping_ip: normalizedPingIp }); + }); + }); + + // API: Rollenwechsel + app.post('/api/user/switch-role', requireAuth, (req, res) => { + const { role } = req.body; + + if (!role) { + return res.status(400).json({ error: 'Rolle ist erforderlich' }); + } + + // Prüfe ob User diese Rolle hat + if (!hasRole(req, role)) { + return res.status(403).json({ error: 'Sie haben keine Berechtigung für diese Rolle' }); + } + + // Validiere dass die Rolle eine gültige Rolle ist + const validRoles = ['mitarbeiter', 'verwaltung', 'admin']; + if (!validRoles.includes(role)) { + return res.status(400).json({ error: 'Ungültige Rolle' }); + } + + // Setze aktuelle Rolle + req.session.currentRole = role; + + res.json({ success: true, currentRole: role }); + }); + + // API: Verplante Urlaubstage (alle Wochen, auch nicht-eingereichte) + app.get('/api/user/planned-vacation', requireAuth, (req, res) => { + const userId = req.session.userId; + const { getCalendarWeek } = require('../helpers/utils'); + + db.all(`SELECT date, vacation_type FROM timesheet_entries + WHERE user_id = ? AND vacation_type IS NOT NULL AND vacation_type != ''`, + [userId], + (err, entries) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Abrufen der verplanten Tage' }); + } + + let plannedDays = 0; + const weeksMap = {}; // { KW: { year: YYYY, week: KW, days: X } } + + entries.forEach(entry => { + const dayValue = entry.vacation_type === 'full' ? 1 : 0.5; + plannedDays += dayValue; + + // Berechne Kalenderwoche + const date = new Date(entry.date); + const year = date.getFullYear(); + const week = getCalendarWeek(entry.date); + const weekKey = `${year}-KW${week}`; + + if (!weeksMap[weekKey]) { + weeksMap[weekKey] = { year, week, days: 0 }; + } + weeksMap[weekKey].days += dayValue; + }); + + // Konvertiere zu sortiertem Array + const weeks = Object.values(weeksMap).sort((a, b) => { + if (a.year !== b.year) return a.year - b.year; + return a.week - b.week; + }); + + res.json({ + plannedVacationDays: plannedDays, + weeks: weeks + }); + } + ); + }); + + // API: Gesamtstatistiken für Mitarbeiter (Überstunden und Urlaubstage) + app.get('/api/user/stats', requireAuth, (req, res) => { + const userId = req.session.userId; + + // User-Daten abrufen + db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours FROM users WHERE id = ?', [userId], (err, user) => { + if (err || !user) { + return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' }); + } + + const wochenstunden = user.wochenstunden || 0; + const urlaubstage = user.urlaubstage || 0; + const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0; + + // Verplante Urlaubstage berechnen (alle Wochen, auch nicht-eingereichte) + const { getCalendarWeek } = require('../helpers/utils'); + db.all(`SELECT date, vacation_type FROM timesheet_entries + WHERE user_id = ? AND vacation_type IS NOT NULL AND vacation_type != ''`, + [userId], + (err, allVacationEntries) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Abrufen der verplanten Tage' }); + } + + let plannedVacationDays = 0; + const weeksMap = {}; // { KW: { year: YYYY, week: KW, days: X } } + + (allVacationEntries || []).forEach(entry => { + const dayValue = entry.vacation_type === 'full' ? 1 : 0.5; + plannedVacationDays += dayValue; + + // Berechne Kalenderwoche + const date = new Date(entry.date); + const year = date.getFullYear(); + const week = getCalendarWeek(entry.date); + const weekKey = `${year}-KW${week}`; + + if (!weeksMap[weekKey]) { + weeksMap[weekKey] = { year, week, days: 0 }; + } + weeksMap[weekKey].days += dayValue; + }); + + // Konvertiere zu sortiertem Array + const plannedWeeks = Object.values(weeksMap).sort((a, b) => { + if (a.year !== b.year) return a.year - b.year; + return a.week - b.week; + }); + + // Alle eingereichten Wochen abrufen + db.all(`SELECT DISTINCT week_start, week_end + FROM weekly_timesheets + WHERE user_id = ? AND status = 'eingereicht' + ORDER BY week_start`, + [userId], + (err, weeks) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Abrufen der Wochen' }); + } + + // Wenn keine Wochen vorhanden + if (!weeks || weeks.length === 0) { + return res.json({ + currentOvertime: overtimeOffsetHours, + remainingVacation: urlaubstage, + totalOvertimeHours: 0, + totalOvertimeTaken: 0, + totalVacationDays: 0, + plannedVacationDays: plannedVacationDays, + plannedWeeks: plannedWeeks, + urlaubstage: urlaubstage, + overtimeOffsetHours: overtimeOffsetHours + }); + } + + let totalOvertimeHours = 0; + let totalOvertimeTaken = 0; + let totalVacationDays = 0; + let processedWeeks = 0; + let hasError = false; + + // Für jede Woche die Statistiken berechnen + weeks.forEach((week) => { + // Einträge für diese Woche abrufen (nur neueste pro Tag) + db.all(`SELECT id, date, total_hours, overtime_taken_hours, vacation_type, sick_status, start_time, end_time, updated_at + FROM timesheet_entries + WHERE user_id = ? AND date >= ? AND date <= ? + ORDER BY date, updated_at DESC, id DESC`, + [userId, week.week_start, week.week_end], + (err, allEntries) => { + if (hasError) return; // Wenn bereits ein Fehler aufgetreten ist, ignoriere weitere Ergebnisse + + if (err) { + hasError = true; + return res.status(500).json({ error: 'Fehler beim Abrufen der Einträge' }); + } + + // Filtere auf neuesten Eintrag pro Tag + const entriesByDate = {}; + (allEntries || []).forEach(entry => { + const existing = entriesByDate[entry.date]; + if (!existing) { + entriesByDate[entry.date] = entry; + } else { + // Vergleiche updated_at (falls vorhanden) oder id (höhere ID = neuer) + const existingTime = existing.updated_at ? new Date(existing.updated_at).getTime() : 0; + const currentTime = entry.updated_at ? new Date(entry.updated_at).getTime() : 0; + if (currentTime > existingTime || (currentTime === existingTime && entry.id > existing.id)) { + entriesByDate[entry.date] = entry; + } + } + }); + + // Konvertiere zurück zu Array + const entries = Object.values(entriesByDate); + + // Prüfe ob Woche vollständig ausgefüllt ist (alle 5 Werktage) + + // Feiertage für die Woche laden (Feiertag zählt als ausgefüllt) + getHolidaysForDateRange(week.week_start, week.week_end) + .catch(() => new Set()) + .then((holidaySet) => { + // Prüfe alle 5 Werktage (Montag-Freitag) + const startDate = new Date(week.week_start); + const endDate = new Date(week.week_end); + let workdays = 0; + let filledWorkdays = 0; + + for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { + const day = d.getDay(); + if (day >= 1 && day <= 5) { // Montag bis Freitag + workdays++; + const dateStr = d.toISOString().split('T')[0]; + if (holidaySet.has(dateStr)) { + filledWorkdays++; + continue; + } + const entry = entriesByDate[dateStr]; + + // Tag gilt als ausgefüllt wenn: + // - Ganzer Tag Urlaub (vacation_type = 'full') + // - Krank (sick_status = 1) + // - ODER Start- und End-Zeit vorhanden sind + if (entry) { + const isFullDayVacation = entry.vacation_type === 'full'; + const isSick = entry.sick_status === 1 || entry.sick_status === true; + const hasStartAndEnd = entry.start_time && entry.end_time && + entry.start_time.toString().trim() !== '' && + entry.end_time.toString().trim() !== ''; + + if (isFullDayVacation || isSick || hasStartAndEnd) { + filledWorkdays++; + } + } + } + } + + // Nur berechnen wenn alle Werktage ausgefüllt sind + if (filledWorkdays < workdays) { + // Woche nicht vollständig - überspringe diese Woche + processedWeeks++; + if (processedWeeks === weeks.length && !hasError) { + const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours; + const remainingVacation = urlaubstage - totalVacationDays; + + res.json({ + currentOvertime: currentOvertime, + remainingVacation: remainingVacation, + totalOvertimeHours: totalOvertimeHours, + totalOvertimeTaken: totalOvertimeTaken, + totalVacationDays: totalVacationDays, + plannedVacationDays: plannedVacationDays, + plannedWeeks: plannedWeeks, + urlaubstage: urlaubstage, + overtimeOffsetHours: overtimeOffsetHours + }); + } + return; // Überspringe diese Woche + } + + // Berechnungen für diese Woche (nur wenn vollständig ausgefüllt) + let weekTotalHours = 0; + let weekOvertimeTaken = 0; + let weekVacationDays = 0; + let weekVacationHours = 0; + + const fullDayHours = wochenstunden > 0 ? wochenstunden / 5 : 8; + let fullDayOvertimeDays = 0; // Anzahl Tage mit 8 Überstunden + + entries.forEach(entry => { + // Prüfe ob 8 Überstunden (ganzer Tag) eingetragen sind + const overtimeValue = entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0; + const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01; + + if (entry.overtime_taken_hours) { + weekOvertimeTaken += entry.overtime_taken_hours; + } + + // Wenn 8 Überstunden eingetragen sind, zählt der Tag als 0 Stunden + // Diese Tage werden separat gezählt, um die Sollstunden anzupassen + if (isFullDayOvertime) { + fullDayOvertimeDays++; + } + + // Urlaub hat Priorität - wenn Urlaub, zähle nur Urlaubsstunden, nicht zusätzlich Arbeitsstunden + if (entry.vacation_type === 'full') { + weekVacationDays += 1; + weekVacationHours += 8; // Ganzer Tag = 8 Stunden + // Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt + } else if (entry.vacation_type === 'half') { + weekVacationDays += 0.5; + weekVacationHours += 4; // Halber Tag = 4 Stunden + // Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein + if (entry.total_hours && !isFullDayOvertime) { + weekTotalHours += entry.total_hours; + } + } else { + // Kein Urlaub - zähle nur Arbeitsstunden (wenn nicht 8 Überstunden) + if (entry.total_hours && !isFullDayOvertime) { + weekTotalHours += entry.total_hours; + } + } + }); + + // Feiertagsstunden: 8h pro Werktag der ein Feiertag ist + let holidayHours = 0; + for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { + const day = d.getDay(); + if (day >= 1 && day <= 5) { + const dateStr = d.toISOString().split('T')[0]; + if (holidaySet.has(dateStr)) holidayHours += 8; + } + } + + // Sollstunden berechnen + const sollStunden = (wochenstunden / 5) * workdays; + + // Überstunden für diese Woche: (totalHours + vacationHours + holidayHours) - adjustedSollStunden + const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours; + const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours); + // weekOvertimeHours = Überstunden diese Woche (wie im Frontend berechnet) + const weekOvertimeHours = weekTotalHoursWithVacation - adjustedSollStunden; + + // Kumulativ addieren + // WICHTIG: weekOvertimeHours enthält bereits die Überstunden dieser Woche (kann negativ sein bei 8 Überstunden) + // weekOvertimeTaken enthält die verbrauchten Überstunden (8 Stunden pro Tag mit 8 Überstunden) + // Die aktuellen Überstunden = Summe aller Wochen-Überstunden - verbrauchte Überstunden + totalOvertimeHours += weekOvertimeHours; + totalOvertimeTaken += weekOvertimeTaken; + totalVacationDays += weekVacationDays; + + processedWeeks++; + + // Wenn alle Wochen verarbeitet wurden, Antwort senden + if (processedWeeks === weeks.length && !hasError) { + // Aktuelle Überstunden = Summe aller Wochen-Überstunden - verbrauchte Überstunden + Offset + // weekOvertimeHours enthält bereits die korrekte Berechnung pro Woche (wie im Frontend) + // weekOvertimeTaken enthält die verbrauchten Überstunden (8 Stunden pro Tag mit 8 Überstunden) + const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours; + const remainingVacation = urlaubstage - totalVacationDays; + + res.json({ + currentOvertime: currentOvertime, + remainingVacation: remainingVacation, + totalOvertimeHours: totalOvertimeHours, + totalOvertimeTaken: totalOvertimeTaken, + totalVacationDays: totalVacationDays, + plannedVacationDays: plannedVacationDays, + plannedWeeks: plannedWeeks, + urlaubstage: urlaubstage, + overtimeOffsetHours: overtimeOffsetHours + }); + } + }); + }); + }); + }); + }); + }); + }); +} + +module.exports = registerUserRoutes; diff --git a/routes/verwaltung.js b/routes/verwaltung.js new file mode 100644 index 0000000..9a0133c --- /dev/null +++ b/routes/verwaltung.js @@ -0,0 +1,391 @@ +// Verwaltung Routes + +const archiver = require('archiver'); +const { db } = require('../database'); +const { requireVerwaltung } = require('../middleware/auth'); +const { getWeekDatesFromCalendarWeek } = require('../helpers/utils'); +const { generatePDFToBuffer } = require('../services/pdf-service'); +const { getHolidaysForDateRange } = require('../services/feiertage-service'); + +// Routes registrieren +function registerVerwaltungRoutes(app) { + // Verwaltungs-Bereich + app.get('/verwaltung', requireVerwaltung, (req, res) => { + db.all(` + SELECT wt.*, u.firstname, u.lastname, u.username, u.personalnummer, u.wochenstunden, u.urlaubstage, u.overtime_offset_hours, + dl.firstname as downloaded_by_firstname, + dl.lastname as downloaded_by_lastname, + (SELECT COUNT(*) FROM weekly_timesheets wt2 + WHERE wt2.user_id = wt.user_id + AND wt2.week_start = wt.week_start + AND wt2.week_end = wt.week_end) as total_versions + FROM weekly_timesheets wt + JOIN users u ON wt.user_id = u.id + LEFT JOIN users dl ON wt.pdf_downloaded_by = dl.id + WHERE wt.status = 'eingereicht' + ORDER BY wt.week_start DESC, wt.user_id, wt.version DESC + `, (err, timesheets) => { + // Gruppiere nach Mitarbeiter, dann nach Kalenderwoche + // Struktur: { [user_id]: { user: {...}, weeks: { [week_key]: {...} } } } + const groupedByEmployee = {}; + + (timesheets || []).forEach(ts => { + const userId = ts.user_id; + const weekKey = `${ts.week_start}_${ts.week_end}`; + + // Level 1: Mitarbeiter + if (!groupedByEmployee[userId]) { + groupedByEmployee[userId] = { + user: { + id: ts.user_id, + firstname: ts.firstname, + lastname: ts.lastname, + username: ts.username, + personalnummer: ts.personalnummer, + wochenstunden: ts.wochenstunden, + urlaubstage: ts.urlaubstage, + overtime_offset_hours: ts.overtime_offset_hours + }, + weeks: {} + }; + } + + // Level 2: Kalenderwoche + if (!groupedByEmployee[userId].weeks[weekKey]) { + groupedByEmployee[userId].weeks[weekKey] = { + week_start: ts.week_start, + week_end: ts.week_end, + total_versions: ts.total_versions, + versions: [] + }; + } + + // Level 3: Versionen + groupedByEmployee[userId].weeks[weekKey].versions.push(ts); + }); + + // Sortierung: Mitarbeiter nach Name, Wochen nach Datum (neueste zuerst) + const sortedEmployees = Object.values(groupedByEmployee).map(employee => { + // Wochen innerhalb jedes Mitarbeiters sortieren + const sortedWeeks = Object.values(employee.weeks).sort((a, b) => { + return new Date(b.week_start) - new Date(a.week_start); + }); + + return { + ...employee, + weeks: sortedWeeks + }; + }).sort((a, b) => { + // Mitarbeiter nach Nachname, dann Vorname sortieren + const nameA = `${a.user.lastname} ${a.user.firstname}`.toLowerCase(); + const nameB = `${b.user.lastname} ${b.user.firstname}`.toLowerCase(); + return nameA.localeCompare(nameB); + }); + + res.render('verwaltung', { + groupedByEmployee: sortedEmployees, + user: { + firstname: req.session.firstname, + lastname: req.session.lastname, + roles: req.session.roles || [], + currentRole: req.session.currentRole || 'verwaltung' + } + }); + }); + }); + + // API: Überstunden-Offset für einen User setzen (positiv/negativ) + app.put('/api/verwaltung/user/:id/overtime-offset', requireVerwaltung, (req, res) => { + const userId = req.params.id; + const raw = req.body ? req.body.overtime_offset_hours : undefined; + + // Leere Eingabe => 0 + const normalized = (raw === '' || raw === null || raw === undefined) ? 0 : parseFloat(raw); + if (!Number.isFinite(normalized)) { + return res.status(400).json({ error: 'Ungültiger Überstunden-Offset' }); + } + + db.run('UPDATE users SET overtime_offset_hours = ? WHERE id = ?', [normalized, userId], (err) => { + if (err) { + console.error('Fehler beim Speichern des Überstunden-Offsets:', err); + return res.status(500).json({ error: 'Fehler beim Speichern des Überstunden-Offsets' }); + } + res.json({ success: true, overtime_offset_hours: normalized }); + }); + }); + + // API: Krankheitstage für einen User im aktuellen Jahr abrufen + app.get('/api/verwaltung/user/:id/sick-days', requireVerwaltung, (req, res) => { + const userId = req.params.id; + const currentYear = new Date().getFullYear(); + const yearStart = `${currentYear}-01-01`; + const yearEnd = `${currentYear}-12-31`; + + db.all(`SELECT DISTINCT date + FROM timesheet_entries + WHERE user_id = ? AND date >= ? AND date <= ? AND sick_status = 1 + ORDER BY date`, + [userId, yearStart, yearEnd], + (err, entries) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Abrufen der Krankheitstage' }); + } + + const sickDays = entries ? entries.length : 0; + res.json({ sickDays: sickDays, year: currentYear }); + } + ); + }); + + // API: Überstunden- und Urlaubsstatistiken für einen User abrufen + app.get('/api/verwaltung/user/:id/stats', requireVerwaltung, (req, res) => { + const userId = req.params.id; + const { week_start, week_end } = req.query; + + // User-Daten abrufen + db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours FROM users WHERE id = ?', [userId], (err, user) => { + if (err || !user) { + return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' }); + } + + const wochenstunden = user.wochenstunden || 0; + const urlaubstage = user.urlaubstage || 0; + const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0; + + // Einträge für die Woche abrufen + db.all(`SELECT date, total_hours, overtime_taken_hours, vacation_type, sick_status + FROM timesheet_entries + WHERE user_id = ? AND date >= ? AND date <= ? + ORDER BY date`, + [userId, week_start, week_end], + (err, entries) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Abrufen der Einträge' }); + } + + // Berechnungen + let totalHours = 0; + let overtimeTaken = 0; + let vacationDays = 0; + let vacationHours = 0; + let sickDays = 0; + + entries.forEach(entry => { + if (entry.overtime_taken_hours) { + overtimeTaken += entry.overtime_taken_hours; + } + + // Krankheitstage zählen + if (entry.sick_status === 1 || entry.sick_status === true) { + sickDays += 1; + } + + // Urlaub hat Priorität - wenn Urlaub, zähle nur Urlaubsstunden, nicht zusätzlich Arbeitsstunden + if (entry.vacation_type === 'full') { + vacationDays += 1; + vacationHours += 8; // Ganzer Tag = 8 Stunden + // Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt + } else if (entry.vacation_type === 'half') { + vacationDays += 0.5; + vacationHours += 4; // Halber Tag = 4 Stunden + // Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein + if (entry.total_hours) { + totalHours += entry.total_hours; + } + } else { + // Kein Urlaub - zähle nur Arbeitsstunden + if (entry.total_hours) { + totalHours += entry.total_hours; + } + } + }); + + // Feiertage für die Woche laden (8h pro Feiertag; Arbeit an Feiertag = Überstunden) + getHolidaysForDateRange(week_start, week_end) + .catch(() => new Set()) + .then((holidaySet) => { + // Anzahl Werktage berechnen (Montag-Freitag) + const startDate = new Date(week_start); + const endDate = new Date(week_end); + let workdays = 0; + let holidayHours = 0; + for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { + const day = d.getDay(); + if (day >= 1 && day <= 5) { // Montag bis Freitag + workdays++; + const dateStr = d.toISOString().split('T')[0]; + if (holidaySet.has(dateStr)) holidayHours += 8; + } + } + + // Sollstunden berechnen + const sollStunden = (wochenstunden / 5) * workdays; + + // Überstunden: (Tatsächliche Stunden + Urlaubsstunden + Feiertagsstunden) - Sollstunden + const totalHoursWithVacation = totalHours + vacationHours + holidayHours; + const overtimeHours = totalHoursWithVacation - sollStunden; + const remainingOvertime = overtimeHours - overtimeTaken; + const remainingOvertimeWithOffset = remainingOvertime + overtimeOffsetHours; + + // Verbleibende Urlaubstage + const remainingVacation = urlaubstage - vacationDays; + + res.json({ + wochenstunden, + urlaubstage, + totalHours, + sollStunden, + overtimeHours, + overtimeTaken, + remainingOvertime, + overtimeOffsetHours, + remainingOvertimeWithOffset, + vacationDays, + remainingVacation, + sickDays, + workdays + }); + }); + }); + }); + }); + + // API: Admin-Kommentar speichern + app.put('/api/verwaltung/timesheet/:id/comment', requireVerwaltung, (req, res) => { + const timesheetId = req.params.id; + const { comment } = req.body; + + db.run('UPDATE weekly_timesheets SET admin_comment = ? WHERE id = ?', + [comment ? comment.trim() : null, timesheetId], + (err) => { + if (err) { + console.error('Fehler beim Speichern des Kommentars:', err); + return res.status(500).json({ error: 'Fehler beim Speichern des Kommentars' }); + } + res.json({ success: true }); + }); + }); + + // API: Massendownload aller PDFs für eine Kalenderwoche + app.get('/api/verwaltung/bulk-download/:year/:week', requireVerwaltung, async (req, res) => { + const year = parseInt(req.params.year); + const week = parseInt(req.params.week); + const downloadedBy = req.session.userId; + + // Validierung + if (!year || year < 2000 || year > 2100) { + return res.status(400).json({ error: 'Ungültiges Jahr' }); + } + if (!week || week < 1 || week > 53) { + return res.status(400).json({ error: 'Ungültige Kalenderwoche (1-53)' }); + } + + try { + // Berechne week_start und week_end aus Jahr und KW + const { week_start, week_end } = getWeekDatesFromCalendarWeek(year, week); + + // Hole alle eingereichten Stundenzettel für diese KW + db.all(`SELECT wt.id, wt.user_id, wt.version, u.firstname, u.lastname + FROM weekly_timesheets wt + JOIN users u ON wt.user_id = u.id + WHERE wt.status = 'eingereicht' + AND wt.week_start = ? + AND wt.week_end = ? + ORDER BY wt.user_id, wt.version DESC`, + [week_start, week_end], + async (err, allTimesheets) => { + if (err) { + console.error('Fehler beim Abrufen der Stundenzettel:', err); + return res.status(500).json({ error: 'Fehler beim Abrufen der Stundenzettel' }); + } + + if (!allTimesheets || allTimesheets.length === 0) { + return res.status(404).json({ error: `Keine eingereichten Stundenzettel für KW ${week}/${year} gefunden` }); + } + + // Gruppiere nach user_id und wähle neueste Version pro User + const latestByUser = {}; + allTimesheets.forEach(ts => { + if (!latestByUser[ts.user_id] || ts.version > latestByUser[ts.user_id].version) { + latestByUser[ts.user_id] = ts; + } + }); + + const timesheetsToDownload = Object.values(latestByUser); + const timesheetIds = timesheetsToDownload.map(ts => ts.id); + + // Erstelle ZIP + res.setHeader('Content-Type', 'application/zip'); + res.setHeader('Content-Disposition', `attachment; filename="Stundenzettel_KW${String(week).padStart(2, '0')}_${year}.zip"`); + + const archive = archiver('zip', { zlib: { level: 9 } }); + archive.on('error', (err) => { + console.error('Fehler beim Erstellen des ZIP:', err); + if (!res.headersSent) { + res.status(500).json({ error: 'Fehler beim Erstellen des ZIP-Archivs' }); + } + }); + + archive.pipe(res); + + // Generiere PDFs sequenziell und füge sie zum ZIP hinzu + const errors = []; + for (const ts of timesheetsToDownload) { + try { + // Erstelle Mock-Request-Objekt für generatePDFToBuffer + const mockReq = { + session: { userId: downloadedBy }, + query: {} + }; + + const pdfBuffer = await generatePDFToBuffer(ts.id, mockReq); + + // Dateiname: Stundenzettel_KW{week}_{Nachname}{Vorname}_Version{version}.pdf + const employeeName = `${ts.lastname}${ts.firstname}`.replace(/\s+/g, ''); + const filename = `Stundenzettel_KW${String(week).padStart(2, '0')}_${employeeName}_Version${ts.version}.pdf`; + + archive.append(pdfBuffer, { name: filename }); + } catch (pdfError) { + console.error(`Fehler beim Generieren des PDFs für Timesheet ${ts.id}:`, pdfError); + errors.push(`Fehler bei ${ts.firstname} ${ts.lastname}: ${pdfError.message}`); + } + } + + // Warte auf ZIP-Finalisierung und markiere dann PDFs als heruntergeladen + archive.on('end', () => { + if (timesheetIds.length > 0 && downloadedBy) { + // Update alle betroffenen timesheets + const placeholders = timesheetIds.map(() => '?').join(','); + db.run(`UPDATE weekly_timesheets + SET pdf_downloaded_at = CURRENT_TIMESTAMP, + pdf_downloaded_by = ? + WHERE id IN (${placeholders})`, + [downloadedBy, ...timesheetIds], + (err) => { + if (err) { + console.error('Fehler beim Markieren der PDFs als heruntergeladen:', err); + } else { + console.log(`Massendownload: ${timesheetIds.length} PDFs als heruntergeladen markiert`); + } + }); + } + }); + + // Finalisiere ZIP (startet den Stream) + archive.finalize(); + + // Wenn Fehler aufgetreten sind, aber ZIP trotzdem erstellt wurde, logge sie + if (errors.length > 0) { + console.warn('Einige PDFs konnten nicht generiert werden:', errors); + } + }); + } catch (error) { + console.error('Fehler beim Massendownload:', error); + if (!res.headersSent) { + res.status(500).json({ error: 'Fehler beim Massendownload: ' + error.message }); + } + } + }); +} + +module.exports = registerVerwaltungRoutes; diff --git a/server.js b/server.js new file mode 100644 index 0000000..a9af914 --- /dev/null +++ b/server.js @@ -0,0 +1,92 @@ +const express = require('express'); +const session = require('express-session'); +const bodyParser = require('body-parser'); +const path = require('path'); +const { initDatabase } = require('./database'); +const { getDefaultRole } = require('./helpers/utils'); + +const app = express(); +const PORT = 3335; + +// Middleware +app.use(bodyParser.urlencoded({ extended: true })); +app.use(bodyParser.json()); +// Trust proxy für korrekte Client-IP-Erkennung (wichtig bei Proxies/Reverse Proxies) +app.set('trust proxy', true); +app.use(express.static('public')); +app.set('view engine', 'ejs'); +app.set('views', path.join(__dirname, 'views')); + +// Session-Konfiguration +// Standard: 24 Stunden, kann in der Login-Route auf 30 Tage erhöht werden wenn "Angemeldet bleiben" aktiviert ist +app.use(session({ + secret: 'stundenerfassung-geheim-2024', + resave: false, + saveUninitialized: false, + cookie: { maxAge: 24 * 60 * 60 * 1000 } // Standard: 24 Stunden +})); + +// Datenbank initialisieren +initDatabase(); + +// Routes importieren und registrieren +const registerAuthRoutes = require('./routes/auth'); +const registerDashboardRoutes = require('./routes/dashboard'); +const registerUserRoutes = require('./routes/user'); +const registerAdminRoutes = require('./routes/admin'); +const registerAdminLDAPRoutes = require('./routes/admin-ldap'); +const registerVerwaltungRoutes = require('./routes/verwaltung'); +const registerTimesheetRoutes = require('./routes/timesheet'); + +// Services importieren +const { setupPingService } = require('./services/ping-service'); +const { setupLDAPScheduler } = require('./services/ldap-scheduler'); + +// Routes registrieren +registerAuthRoutes(app); +registerDashboardRoutes(app); +registerUserRoutes(app); +registerAdminRoutes(app); +registerAdminLDAPRoutes(app); +registerVerwaltungRoutes(app); +registerTimesheetRoutes(app); + +// Start-Route +app.get('/', (req, res) => { + if (req.session.userId) { + // Redirect: Immer zu Dashboard wenn Mitarbeiter-Rolle vorhanden, sonst basierend auf currentRole + const roles = req.session.roles || []; + if (roles.includes('mitarbeiter')) { + res.redirect('/dashboard'); + } else { + const currentRole = req.session.currentRole || getDefaultRole(roles); + if (currentRole === 'admin') { + res.redirect('/admin'); + } else if (currentRole === 'verwaltung') { + res.redirect('/verwaltung'); + } else { + res.redirect('/dashboard'); + } + } + } else { + res.redirect('/login'); + } +}); + +// Server starten +app.listen(PORT, () => { + console.log(`Server läuft auf http://localhost:${PORT}`); + console.log('Standard-Zugangsdaten:'); + console.log('Admin: admin / admin123'); + console.log('Verwaltung: verwaltung / verwaltung123'); + + // LDAP-Scheduler starten + setupLDAPScheduler(); + + // Ping-Service starten + setupPingService(); + console.log('Ping-Service gestartet (prüft alle IPs jede Minute)'); +}); + +// Check-in-Server starten (separater Server auf Port 3334) +require('./checkin-server'); diff --git a/services/feiertage-service.js b/services/feiertage-service.js new file mode 100644 index 0000000..fbfb8b7 --- /dev/null +++ b/services/feiertage-service.js @@ -0,0 +1,109 @@ +// Feiertage-Service: Lädt Feiertage aus DB oder von feiertage-api.de (BW), speichert in public_holidays + +const https = require('https'); +const { db } = require('../database'); + +const API_BASE = 'https://feiertage-api.de/api/'; +const LAND = 'BW'; + +/** + * Holt Feiertage für ein Jahr von der API und speichert sie in der DB. + * @param {number} year + * @returns {Promise>} Set von Datums-Strings YYYY-MM-DD + */ +function fetchHolidaysFromAPI(year) { + return new Promise((resolve, reject) => { + const url = `${API_BASE}?jahr=${year}&nur_land=${LAND}`; + https.get(url, (res) => { + let data = ''; + res.on('data', (chunk) => { data += chunk; }); + res.on('end', () => { + try { + const json = JSON.parse(data); + const dates = new Set(); + const toInsert = []; + for (const [name, obj] of Object.entries(json)) { + if (obj && obj.datum) { + dates.add(obj.datum); + toInsert.push({ date: obj.datum, name: name || null }); + } + } + if (toInsert.length === 0) { + resolve(dates); + return; + } + let pending = toInsert.length; + toInsert.forEach(({ date, name }) => { + db.run('INSERT OR REPLACE INTO public_holidays (date, name) VALUES (?, ?)', [date, name], (err) => { + if (err) console.warn('Feiertage: Fehler beim Speichern:', err.message); + pending--; + if (pending === 0) resolve(dates); + }); + }); + } catch (e) { + reject(e); + } + }); + }).on('error', reject); + }); +} + +/** + * Liest Feiertage für ein Jahr aus der DB. + * @param {number} year + * @returns {Promise>} + */ +function getHolidaysFromDB(year) { + return new Promise((resolve, reject) => { + const pattern = `${year}-%`; + db.all('SELECT date FROM public_holidays WHERE date LIKE ?', [pattern], (err, rows) => { + if (err) return reject(err); + const set = new Set((rows || []).map((r) => r.date)); + resolve(set); + }); + }); +} + +/** + * Liefert alle Feiertage für ein Jahr. Zuerst DB; wenn Jahr fehlt, API aufrufen und in DB schreiben. + * @param {number} year + * @returns {Promise>} Set von YYYY-MM-DD + */ +function getHolidaysForYear(year) { + return getHolidaysFromDB(year).then((set) => { + if (set.size > 0) return set; + return fetchHolidaysFromAPI(year).catch((err) => { + console.warn('Feiertage API fehlgeschlagen für Jahr', year, err.message); + return new Set(); + }); + }); +} + +/** + * Liefert alle Feiertage im Datumsbereich [weekStart, weekEnd] (inklusive). + * Lädt ggf. fehlende Jahre aus der API und speichert sie in der DB. + * @param {string} weekStart YYYY-MM-DD + * @param {string} weekEnd YYYY-MM-DD + * @returns {Promise>} Set von YYYY-MM-DD + */ +function getHolidaysForDateRange(weekStart, weekEnd) { + const startYear = parseInt(weekStart.slice(0, 4), 10); + const endYear = parseInt(weekEnd.slice(0, 4), 10); + const years = []; + for (let y = startYear; y <= endYear; y++) years.push(y); + + return Promise.all(years.map((y) => getHolidaysForYear(y))).then((sets) => { + const combined = new Set(); + sets.forEach((s) => s.forEach((d) => combined.add(d))); + const inRange = new Set(); + combined.forEach((d) => { + if (d >= weekStart && d <= weekEnd) inRange.add(d); + }); + return inRange; + }); +} + +module.exports = { + getHolidaysForYear, + getHolidaysForDateRange, +}; diff --git a/services/ldap-scheduler.js b/services/ldap-scheduler.js new file mode 100644 index 0000000..b6e74c4 --- /dev/null +++ b/services/ldap-scheduler.js @@ -0,0 +1,34 @@ +// LDAP-Scheduler Service + +const { db } = require('../database'); +const LDAPService = require('../ldap-service'); + +// Automatische LDAP-Synchronisation einrichten +function setupLDAPScheduler() { + // Prüfe alle 5 Minuten, ob eine Synchronisation notwendig ist + setInterval(() => { + db.get('SELECT * FROM ldap_config WHERE id = 1 AND enabled = 1 AND sync_interval > 0', (err, config) => { + if (err || !config) { + return; // Keine aktive Konfiguration + } + + const now = new Date(); + const lastSync = config.last_sync ? new Date(config.last_sync) : null; + const syncIntervalMs = config.sync_interval * 60 * 1000; // Minuten in Millisekunden + + // Prüfe ob Synchronisation fällig ist + if (!lastSync || (now - lastSync) >= syncIntervalMs) { + console.log('Starte automatische LDAP-Synchronisation...'); + LDAPService.performSync('scheduled', (err, result) => { + if (err) { + console.error('Fehler bei automatischer LDAP-Synchronisation:', err.message); + } else { + console.log(`Automatische LDAP-Synchronisation abgeschlossen: ${result.synced} Benutzer synchronisiert`); + } + }); + } + }); + }, 5 * 60 * 1000); // Alle 5 Minuten prüfen +} + +module.exports = { setupLDAPScheduler }; diff --git a/services/pdf-service.js b/services/pdf-service.js new file mode 100644 index 0000000..975df4d --- /dev/null +++ b/services/pdf-service.js @@ -0,0 +1,531 @@ +// PDF-Generierung Service + +const PDFDocument = require('pdfkit'); +const { db } = require('../database'); +const { formatDate, formatDateTime } = require('../helpers/utils'); +const { getHolidaysForDateRange } = require('./feiertage-service'); + +// Kalenderwoche berechnen +function getCalendarWeek(dateStr) { + const date = new Date(dateStr); + const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); + const dayNum = d.getUTCDay() || 7; + d.setUTCDate(d.getUTCDate() + 4 - dayNum); + const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7); + return weekNo; +} + +// PDF generieren +function generatePDF(timesheetId, req, res) { + db.get(`SELECT wt.*, u.firstname, u.lastname, u.username, u.wochenstunden + FROM weekly_timesheets wt + JOIN users u ON wt.user_id = u.id + WHERE wt.id = ?`, [timesheetId], (err, timesheet) => { + + if (err || !timesheet) { + return res.status(404).send('Stundenzettel nicht gefunden'); + } + + // Hole Einträge die zum Zeitpunkt der Einreichung existierten + // Filtere nach submitted_at der Version, damit jede Version ihre eigenen Daten zeigt + // Logik: Wenn updated_at existiert, verwende das, sonst created_at, sonst zeige Eintrag (für alte Daten ohne Timestamps) + db.all(`SELECT * FROM timesheet_entries + WHERE user_id = ? AND date >= ? AND date <= ? + AND ( + (updated_at IS NOT NULL AND updated_at <= ?) OR + (updated_at IS NULL AND created_at IS NOT NULL AND created_at <= ?) OR + (updated_at IS NULL AND created_at IS NULL) + ) + ORDER BY date, updated_at DESC, id DESC`, + [timesheet.user_id, timesheet.week_start, timesheet.week_end, + timesheet.submitted_at, timesheet.submitted_at], + (err, allEntries) => { + if (err) { + return res.status(500).send('Fehler beim Abrufen der Einträge'); + } + + // Filtere auf neuesten Eintrag pro Tag (basierend auf updated_at oder id) + const entriesByDate = {}; + (allEntries || []).forEach(entry => { + const existing = entriesByDate[entry.date]; + if (!existing) { + entriesByDate[entry.date] = entry; + } else { + // Vergleiche updated_at (falls vorhanden) oder id (höhere ID = neuer) + const existingTime = existing.updated_at ? new Date(existing.updated_at).getTime() : 0; + const currentTime = entry.updated_at ? new Date(entry.updated_at).getTime() : 0; + if (currentTime > existingTime || (currentTime === existingTime && entry.id > existing.id)) { + entriesByDate[entry.date] = entry; + } + } + }); + + // Konvertiere zu Array und sortiere nach Datum + const entries = Object.values(entriesByDate).sort((a, b) => { + return new Date(a.date) - new Date(b.date); + }); + + // Feiertage für die Woche laden (8h pro Feiertag; Arbeit an Feiertag = Überstunden) + getHolidaysForDateRange(timesheet.week_start, timesheet.week_end) + .then((holidaySet) => { + let holidayHours = 0; + const start = new Date(timesheet.week_start); + const end = new Date(timesheet.week_end); + for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { + const day = d.getDay(); + if (day >= 1 && day <= 5) { + const dateStr = d.toISOString().split('T')[0]; + if (holidaySet.has(dateStr)) holidayHours += 8; + } + } + return { holidaySet, holidayHours }; + }) + .catch(() => ({ holidaySet: new Set(), holidayHours: 0 })) + .then(({ holidaySet, holidayHours }) => { + const doc = new PDFDocument({ margin: 50 }); + + // Prüfe ob inline angezeigt werden soll (für Vorschau) + const inline = req.query.inline === 'true'; + + // Dateinamen generieren: Stundenzettel_KWxxx_NameMitarbeiter_heutigesDatum.pdf + const calendarWeek = getCalendarWeek(timesheet.week_start); + const today = new Date(); + const todayStr = today.getFullYear() + '-' + + String(today.getMonth() + 1).padStart(2, '0') + '-' + + String(today.getDate()).padStart(2, '0'); + const employeeName = `${timesheet.firstname}${timesheet.lastname}`.replace(/\s+/g, ''); + const filename = `Stundenzettel_KW${String(calendarWeek).padStart(2, '0')}_${employeeName}_${todayStr}.pdf`; + + res.setHeader('Content-Type', 'application/pdf'); + res.setHeader('X-Content-Type-Options', 'nosniff'); + + if (inline) { + res.setHeader('Content-Disposition', `inline; filename="${filename}"`); + // Zusätzliche Header für iframe-Unterstützung + res.setHeader('X-Frame-Options', 'SAMEORIGIN'); + } else { + res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); + + // Marker setzen, dass PDF heruntergeladen wurde (nur bei Download, nicht bei Vorschau) + const downloadedBy = req.session.userId; // User der die PDF herunterlädt + console.log('PDF Download - User ID:', downloadedBy, 'Timesheet ID:', timesheetId); + + if (downloadedBy) { + db.run(`UPDATE weekly_timesheets + SET pdf_downloaded_at = CURRENT_TIMESTAMP, + pdf_downloaded_by = ? + WHERE id = ?`, + [downloadedBy, timesheetId], + (err) => { + if (err) { + console.error('Fehler beim Setzen des Download-Markers:', err); + } else { + console.log('Download-Marker erfolgreich gesetzt für User:', downloadedBy); + } + // Fehler wird ignoriert, damit PDF trotzdem generiert wird + }); + } else { + console.warn('PDF Download - Keine User ID in Session gefunden!'); + } + } + + doc.pipe(res); + + // Header (Kalenderwoche wurde bereits oben berechnet) + doc.fontSize(20).text(`Stundenzettel für KW ${calendarWeek}`, { align: 'center' }); + doc.moveDown(); + + // Mitarbeiter-Info + doc.fontSize(12); + doc.text(`Mitarbeiter: ${timesheet.firstname} ${timesheet.lastname}`); + doc.text(`Zeitraum: ${formatDate(timesheet.week_start)} - ${formatDate(timesheet.week_end)}`); + doc.text(`Eingereicht am: ${formatDateTime(timesheet.submitted_at)}`); + doc.moveDown(); + + // Tabelle - Basis-Informationen + const tableTop = doc.y; + const colWidths = [80, 80, 80, 60, 80]; + const headers = ['Datum', 'Start', 'Ende', 'Pause', 'Stunden']; + + // Tabellen-Header + doc.fontSize(10).font('Helvetica-Bold'); + let x = 50; + headers.forEach((header, i) => { + doc.text(header, x, tableTop, { width: colWidths[i], align: 'left' }); + x += colWidths[i]; + }); + + doc.moveDown(); + let y = doc.y; + doc.moveTo(50, y).lineTo(430, y).stroke(); + doc.moveDown(0.5); + + // Tabellen-Daten + doc.font('Helvetica'); + let totalHours = 0; + let vacationHours = 0; // Urlaubsstunden für Überstunden-Berechnung + + entries.forEach((entry) => { + y = doc.y; + x = 50; + + // Basis-Zeile + const rowData = [ + formatDate(entry.date), + entry.start_time || '-', + entry.end_time || '-', + entry.break_minutes ? `${entry.break_minutes} min` : '-', + entry.total_hours ? entry.total_hours.toFixed(2) + ' h' : '-' + ]; + + rowData.forEach((data, i) => { + doc.text(data, x, y, { width: colWidths[i], align: 'left' }); + x += colWidths[i]; + }); + + // Tätigkeiten sammeln + const activities = []; + for (let i = 1; i <= 5; i++) { + const desc = entry[`activity${i}_desc`]; + const hours = entry[`activity${i}_hours`]; + const projectNumber = entry[`activity${i}_project_number`]; + if (desc && desc.trim() && hours > 0) { + activities.push({ + desc: desc.trim(), + hours: parseFloat(hours), + projectNumber: projectNumber ? projectNumber.trim() : null + }); + } + } + + // Tätigkeiten anzeigen + if (activities.length > 0) { + doc.moveDown(0.3); + doc.fontSize(9).font('Helvetica-Oblique'); + doc.text('Tätigkeiten:', 60, doc.y, { width: 380 }); + doc.moveDown(0.2); + + activities.forEach((activity, idx) => { + let activityText = `${idx + 1}. ${activity.desc}`; + if (activity.projectNumber) { + activityText += ` (Projekt: ${activity.projectNumber})`; + } + activityText += ` - ${activity.hours.toFixed(2)} h`; + doc.fontSize(9).font('Helvetica'); + doc.text(activityText, 70, doc.y, { width: 360 }); + doc.moveDown(0.2); + }); + doc.fontSize(10); + } + + // Überstunden und Urlaub anzeigen + const overtimeInfo = []; + if (entry.overtime_taken_hours && parseFloat(entry.overtime_taken_hours) > 0) { + overtimeInfo.push(`Überstunden genommen: ${parseFloat(entry.overtime_taken_hours).toFixed(2)} h`); + } + if (entry.vacation_type) { + const vacationText = entry.vacation_type === 'full' ? 'Ganzer Tag' : 'Halber Tag'; + overtimeInfo.push(`Urlaub: ${vacationText}`); + } + + if (overtimeInfo.length > 0) { + doc.moveDown(0.2); + doc.fontSize(9).font('Helvetica-Oblique'); + overtimeInfo.forEach((info, idx) => { + doc.text(info, 70, doc.y, { width: 360 }); + doc.moveDown(0.15); + }); + doc.fontSize(10); + } + + // Urlaub hat Priorität - wenn Urlaub, zähle nur Urlaubsstunden, nicht zusätzlich Arbeitsstunden + if (entry.vacation_type === 'full') { + vacationHours += 8; // Ganzer Tag = 8 Stunden + // Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt + } else if (entry.vacation_type === 'half') { + vacationHours += 4; // Halber Tag = 4 Stunden + // Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein + if (entry.total_hours) { + totalHours += entry.total_hours; + } + } else { + // Kein Urlaub - zähle Arbeitsstunden; an Feiertagen zählt jede Stunde als Überstunde (8h Feiertag + Arbeit) + if (entry.total_hours) { + totalHours += entry.total_hours; + } + } + // Feiertag: 8h sind über holidayHours erfasst; gearbeitete Stunden oben bereits zu totalHours addiert + + doc.moveDown(0.5); + + // Trennlinie zwischen Einträgen + y = doc.y; + doc.moveTo(50, y).lineTo(430, y).stroke(); + doc.moveDown(0.3); + }); + + // Summe + y = doc.y; + doc.moveTo(50, y).lineTo(550, y).stroke(); + doc.moveDown(0.5); + doc.font('Helvetica-Bold'); + // Gesamtstunden = Arbeitsstunden + Urlaubsstunden + Feiertagsstunden (8h pro Feiertag) + const totalHoursWithVacation = totalHours + vacationHours + holidayHours; + doc.text(`Gesamtstunden: ${totalHoursWithVacation.toFixed(2)} h`, 50, doc.y); + + // Überstunden berechnen und anzeigen + const wochenstunden = timesheet.wochenstunden || 0; + // Überstunden = (Tatsächliche Stunden + Urlaubsstunden) - Wochenstunden + const overtimeHours = totalHoursWithVacation - wochenstunden; + + doc.moveDown(0.3); + doc.font('Helvetica-Bold'); + if (overtimeHours > 0) { + doc.text(`Überstunden: +${overtimeHours.toFixed(2)} h`, 50, doc.y); + } else if (overtimeHours < 0) { + doc.text(`Überstunden: ${overtimeHours.toFixed(2)} h`, 50, doc.y); + } else { + doc.text(`Überstunden: 0.00 h`, 50, doc.y); + } + + doc.end(); + }); + }); + }); +} + +// PDF als Buffer generieren (für ZIP-Downloads) +function generatePDFToBuffer(timesheetId, req) { + return new Promise((resolve, reject) => { + db.get(`SELECT wt.*, u.firstname, u.lastname, u.username, u.wochenstunden + FROM weekly_timesheets wt + JOIN users u ON wt.user_id = u.id + WHERE wt.id = ?`, [timesheetId], (err, timesheet) => { + + if (err || !timesheet) { + return reject(new Error('Stundenzettel nicht gefunden')); + } + + // Hole Einträge die zum Zeitpunkt der Einreichung existierten + db.all(`SELECT * FROM timesheet_entries + WHERE user_id = ? AND date >= ? AND date <= ? + AND ( + (updated_at IS NOT NULL AND updated_at <= ?) OR + (updated_at IS NULL AND created_at IS NOT NULL AND created_at <= ?) OR + (updated_at IS NULL AND created_at IS NULL) + ) + ORDER BY date, updated_at DESC, id DESC`, + [timesheet.user_id, timesheet.week_start, timesheet.week_end, + timesheet.submitted_at, timesheet.submitted_at], + (err, allEntries) => { + if (err) { + return reject(new Error('Fehler beim Abrufen der Einträge')); + } + + // Filtere auf neuesten Eintrag pro Tag + const entriesByDate = {}; + (allEntries || []).forEach(entry => { + const existing = entriesByDate[entry.date]; + if (!existing) { + entriesByDate[entry.date] = entry; + } else { + const existingTime = existing.updated_at ? new Date(existing.updated_at).getTime() : 0; + const currentTime = entry.updated_at ? new Date(entry.updated_at).getTime() : 0; + if (currentTime > existingTime || (currentTime === existingTime && entry.id > existing.id)) { + entriesByDate[entry.date] = entry; + } + } + }); + + const entries = Object.values(entriesByDate).sort((a, b) => { + return new Date(a.date) - new Date(b.date); + }); + + getHolidaysForDateRange(timesheet.week_start, timesheet.week_end) + .then((holidaySet) => { + let holidayHours = 0; + const start = new Date(timesheet.week_start); + const end = new Date(timesheet.week_end); + for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { + const day = d.getDay(); + if (day >= 1 && day <= 5) { + const dateStr = d.toISOString().split('T')[0]; + if (holidaySet.has(dateStr)) holidayHours += 8; + } + } + return { holidaySet, holidayHours }; + }) + .catch(() => ({ holidaySet: new Set(), holidayHours: 0 })) + .then(({ holidayHours }) => { + const doc = new PDFDocument({ margin: 50 }); + const buffers = []; + + doc.on('data', buffers.push.bind(buffers)); + doc.on('end', () => { + const pdfBuffer = Buffer.concat(buffers); + resolve(pdfBuffer); + }); + doc.on('error', reject); + + // Header + const calendarWeek = getCalendarWeek(timesheet.week_start); + doc.fontSize(20).text(`Stundenzettel für KW ${calendarWeek}`, { align: 'center' }); + doc.moveDown(); + + // Mitarbeiter-Info + doc.fontSize(12); + doc.text(`Mitarbeiter: ${timesheet.firstname} ${timesheet.lastname}`); + doc.text(`Zeitraum: ${formatDate(timesheet.week_start)} - ${formatDate(timesheet.week_end)}`); + doc.text(`Eingereicht am: ${formatDateTime(timesheet.submitted_at)}`); + doc.moveDown(); + + // Tabelle - Basis-Informationen + const tableTop = doc.y; + const colWidths = [80, 80, 80, 60, 80]; + const headers = ['Datum', 'Start', 'Ende', 'Pause', 'Stunden']; + + // Tabellen-Header + doc.fontSize(10).font('Helvetica-Bold'); + let x = 50; + headers.forEach((header, i) => { + doc.text(header, x, tableTop, { width: colWidths[i], align: 'left' }); + x += colWidths[i]; + }); + + doc.moveDown(); + let y = doc.y; + doc.moveTo(50, y).lineTo(430, y).stroke(); + doc.moveDown(0.5); + + // Tabellen-Daten + doc.font('Helvetica'); + let totalHours = 0; + let vacationHours = 0; + + entries.forEach((entry) => { + y = doc.y; + x = 50; + + const rowData = [ + formatDate(entry.date), + entry.start_time || '-', + entry.end_time || '-', + entry.break_minutes ? `${entry.break_minutes} min` : '-', + entry.total_hours ? entry.total_hours.toFixed(2) + ' h' : '-' + ]; + + rowData.forEach((data, i) => { + doc.text(data, x, y, { width: colWidths[i], align: 'left' }); + x += colWidths[i]; + }); + + // Tätigkeiten sammeln + const activities = []; + for (let i = 1; i <= 5; i++) { + const desc = entry[`activity${i}_desc`]; + const hours = entry[`activity${i}_hours`]; + const projectNumber = entry[`activity${i}_project_number`]; + if (desc && desc.trim() && hours > 0) { + activities.push({ + desc: desc.trim(), + hours: parseFloat(hours), + projectNumber: projectNumber ? projectNumber.trim() : null + }); + } + } + + // Tätigkeiten anzeigen + if (activities.length > 0) { + doc.moveDown(0.3); + doc.fontSize(9).font('Helvetica-Oblique'); + doc.text('Tätigkeiten:', 60, doc.y, { width: 380 }); + doc.moveDown(0.2); + + activities.forEach((activity, idx) => { + let activityText = `${idx + 1}. ${activity.desc}`; + if (activity.projectNumber) { + activityText += ` (Projekt: ${activity.projectNumber})`; + } + activityText += ` - ${activity.hours.toFixed(2)} h`; + doc.fontSize(9).font('Helvetica'); + doc.text(activityText, 70, doc.y, { width: 360 }); + doc.moveDown(0.2); + }); + doc.fontSize(10); + } + + // Überstunden und Urlaub anzeigen + const overtimeInfo = []; + if (entry.overtime_taken_hours && parseFloat(entry.overtime_taken_hours) > 0) { + overtimeInfo.push(`Überstunden genommen: ${parseFloat(entry.overtime_taken_hours).toFixed(2)} h`); + } + if (entry.vacation_type) { + const vacationText = entry.vacation_type === 'full' ? 'Ganzer Tag' : 'Halber Tag'; + overtimeInfo.push(`Urlaub: ${vacationText}`); + } + + if (overtimeInfo.length > 0) { + doc.moveDown(0.2); + doc.fontSize(9).font('Helvetica-Oblique'); + overtimeInfo.forEach((info, idx) => { + doc.text(info, 70, doc.y, { width: 360 }); + doc.moveDown(0.15); + }); + doc.fontSize(10); + } + + // Urlaub hat Priorität - wenn Urlaub, zähle nur Urlaubsstunden, nicht zusätzlich Arbeitsstunden + if (entry.vacation_type === 'full') { + vacationHours += 8; // Ganzer Tag = 8 Stunden + // Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt + } else if (entry.vacation_type === 'half') { + vacationHours += 4; // Halber Tag = 4 Stunden + // Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein + if (entry.total_hours) { + totalHours += entry.total_hours; + } + } else { + // Kein Urlaub - zähle nur Arbeitsstunden + if (entry.total_hours) { + totalHours += entry.total_hours; + } + } + + doc.moveDown(0.5); + + y = doc.y; + doc.moveTo(50, y).lineTo(430, y).stroke(); + doc.moveDown(0.3); + }); + + // Summe + y = doc.y; + doc.moveTo(50, y).lineTo(550, y).stroke(); + doc.moveDown(0.5); + doc.font('Helvetica-Bold'); + // Gesamtstunden = Arbeitsstunden + Urlaubsstunden + Feiertagsstunden + const totalHoursWithVacation = totalHours + vacationHours + holidayHours; + doc.text(`Gesamtstunden: ${totalHoursWithVacation.toFixed(2)} h`, 50, doc.y); + + const wochenstunden = timesheet.wochenstunden || 0; + const overtimeHours = totalHoursWithVacation - wochenstunden; + + doc.moveDown(0.3); + doc.font('Helvetica-Bold'); + if (overtimeHours > 0) { + doc.text(`Überstunden: +${overtimeHours.toFixed(2)} h`, 50, doc.y); + } else if (overtimeHours < 0) { + doc.text(`Überstunden: ${overtimeHours.toFixed(2)} h`, 50, doc.y); + } else { + doc.text(`Überstunden: 0.00 h`, 50, doc.y); + } + + doc.end(); + }); + }); + }); + }); +} + +module.exports = { generatePDF, generatePDFToBuffer }; diff --git a/services/ping-service.js b/services/ping-service.js new file mode 100644 index 0000000..90f10b6 --- /dev/null +++ b/services/ping-service.js @@ -0,0 +1,182 @@ +// Ping-Service für IP-basierte automatische Zeiterfassung + +const ping = require('ping'); +const { db } = require('../database'); +const { getCurrentDate, getCurrentTime, updateTotalHours } = require('../helpers/utils'); + +// Ping-Funktion für einen User +async function pingUserIP(userId, ip, currentDate, currentTime) { + try { + const result = await ping.promise.probe(ip, { + timeout: 3, + min_reply: 1 + }); + + const isReachable = result.alive; + const now = new Date().toISOString(); + + // Hole oder erstelle Ping-Status für heute + db.get('SELECT * FROM ping_status WHERE user_id = ? AND date = ?', + [userId, currentDate], (err, pingStatus) => { + if (err) { + console.error(`Fehler beim Abrufen des Ping-Status für User ${userId}:`, err); + return; + } + + // Hole aktuellen Eintrag für heute + db.get('SELECT * FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1', + [userId, currentDate], (err, entry) => { + if (err) { + console.error(`Fehler beim Abrufen des Eintrags für User ${userId}:`, err); + return; + } + + if (isReachable) { + // IP ist erreichbar + if (!pingStatus) { + // Erstelle neuen Ping-Status + db.run(`INSERT INTO ping_status (user_id, date, last_successful_ping, failed_ping_count, start_time_set) + VALUES (?, ?, ?, 0, 0)`, + [userId, currentDate, now], (err) => { + if (err) console.error(`Fehler beim Erstellen des Ping-Status:`, err); + }); + } else { + // Update Ping-Status: Reset failed_ping_count, update last_successful_ping + db.run(`UPDATE ping_status + SET last_successful_ping = ?, failed_ping_count = 0, first_failed_ping_time = NULL + WHERE user_id = ? AND date = ?`, + [now, userId, currentDate], (err) => { + if (err) console.error(`Fehler beim Aktualisieren des Ping-Status:`, err); + }); + } + + // Start-Zeit setzen wenn noch nicht vorhanden + if (entry && !entry.start_time) { + db.run('UPDATE timesheet_entries SET start_time = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', + [currentTime, entry.id], (err) => { + if (err) { + console.error(`Fehler beim Setzen der Start-Zeit für User ${userId}:`, err); + } else { + console.log(`Start-Zeit gesetzt für User ${userId} um ${currentTime}`); + // Markiere dass Start-Zeit gesetzt wurde + db.run('UPDATE ping_status SET start_time_set = 1 WHERE user_id = ? AND date = ?', + [userId, currentDate], (err) => { + if (err) console.error(`Fehler beim Aktualisieren von start_time_set:`, err); + }); + } + }); + } else if (!entry) { + // Kein Eintrag existiert → Erstelle neuen mit start_time + db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, updated_at) + VALUES (?, ?, ?, CURRENT_TIMESTAMP)`, + [userId, currentDate, currentTime], (err) => { + if (err) { + console.error(`Fehler beim Erstellen des Eintrags für User ${userId}:`, err); + } else { + console.log(`Eintrag erstellt und Start-Zeit gesetzt für User ${userId} um ${currentTime}`); + // Markiere dass Start-Zeit gesetzt wurde + db.run('UPDATE ping_status SET start_time_set = 1 WHERE user_id = ? AND date = ?', + [userId, currentDate], (err) => { + if (err) { + // Falls kein Ping-Status existiert, erstelle einen + db.run(`INSERT INTO ping_status (user_id, date, last_successful_ping, failed_ping_count, start_time_set) + VALUES (?, ?, ?, 0, 1)`, + [userId, currentDate, now], (err) => { + if (err) console.error(`Fehler beim Erstellen des Ping-Status:`, err); + }); + } + }); + } + }); + } + } else { + // IP ist nicht erreichbar + if (!pingStatus) { + // Erstelle neuen Ping-Status mit failed_ping_count = 1 + db.run(`INSERT INTO ping_status (user_id, date, failed_ping_count, first_failed_ping_time) + VALUES (?, ?, 1, ?)`, + [userId, currentDate, now], (err) => { + if (err) console.error(`Fehler beim Erstellen des Ping-Status:`, err); + }); + } else { + // Erhöhe failed_ping_count + const newFailedCount = (pingStatus.failed_ping_count || 0) + 1; + const firstFailedTime = pingStatus.first_failed_ping_time || now; + + db.run(`UPDATE ping_status + SET failed_ping_count = ?, first_failed_ping_time = ? + WHERE user_id = ? AND date = ?`, + [newFailedCount, firstFailedTime, userId, currentDate], (err) => { + if (err) console.error(`Fehler beim Aktualisieren des Ping-Status:`, err); + }); + + // Wenn 3 oder mehr fehlgeschlagene Pings UND Start-Zeit existiert UND keine End-Zeit + if (newFailedCount >= 3 && entry && entry.start_time && !entry.end_time) { + // Setze End-Zeit auf Zeit des ersten fehlgeschlagenen Pings + const firstFailedDate = new Date(firstFailedTime); + const endTime = `${String(firstFailedDate.getHours()).padStart(2, '0')}:${String(firstFailedDate.getMinutes()).padStart(2, '0')}`; + + // Berechne total_hours + const breakMinutes = entry.break_minutes || 0; + const totalHours = updateTotalHours(entry.start_time, endTime, breakMinutes); + + db.run('UPDATE timesheet_entries SET end_time = ?, total_hours = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', + [endTime, totalHours, entry.id], (err) => { + if (err) { + console.error(`Fehler beim Setzen der End-Zeit für User ${userId}:`, err); + } else { + console.log(`End-Zeit gesetzt für User ${userId} um ${endTime} (nach ${newFailedCount} fehlgeschlagenen Pings)`); + } + }); + } + } + } + }); + }); + } catch (error) { + console.error(`Fehler beim Ping für User ${userId} (IP: ${ip}):`, error); + // Behandle als nicht erreichbar + const now = new Date().toISOString(); + db.get('SELECT * FROM ping_status WHERE user_id = ? AND date = ?', + [userId, currentDate], (err, pingStatus) => { + if (!err && pingStatus) { + const newFailedCount = (pingStatus.failed_ping_count || 0) + 1; + const firstFailedTime = pingStatus.first_failed_ping_time || now; + + db.run(`UPDATE ping_status + SET failed_ping_count = ?, first_failed_ping_time = ? + WHERE user_id = ? AND date = ?`, + [newFailedCount, firstFailedTime, userId, currentDate], (err) => { + if (err) console.error(`Fehler beim Aktualisieren des Ping-Status:`, err); + }); + } + }); + } +} + +// Ping-Service Setup +function setupPingService() { + setInterval(async () => { + const currentDate = getCurrentDate(); + const currentTime = getCurrentTime(); + + // Hole alle User mit IP-Adresse + db.all('SELECT id, ping_ip FROM users WHERE ping_ip IS NOT NULL AND ping_ip != ""', (err, users) => { + if (err) { + console.error('Fehler beim Abrufen der User mit IP-Adressen:', err); + return; + } + + if (!users || users.length === 0) { + return; // Keine User mit IP-Adressen + } + + // Ping alle User parallel + users.forEach(user => { + pingUserIP(user.id, user.ping_ip, currentDate, currentTime); + }); + }); + }, 60000); // Jede Minute +} + +module.exports = { setupPingService }; diff --git a/views/admin.ejs b/views/admin.ejs new file mode 100644 index 0000000..34231f8 --- /dev/null +++ b/views/admin.ejs @@ -0,0 +1,419 @@ + + + + + + Admin - Stundenerfassung + + + + +
    + +
    +
    +
    +

    Benutzerverwaltung

    + + +
    +
    +

    Neuen Benutzer anlegen

    + +
    + + +
    + + +
    +
    +

    Benutzer-Liste

    + +
    + + +
    + +
    +
    +

    LDAP-Synchronisation

    + +
    + + +
    +
    +
    +
    + + + + + diff --git a/views/dashboard.ejs b/views/dashboard.ejs new file mode 100644 index 0000000..31c7fc9 --- /dev/null +++ b/views/dashboard.ejs @@ -0,0 +1,406 @@ + + + + + + + Dashboard - Stundenerfassung + + + + + + +
    +
    +
    +
    + +

    Kalenderwoche

    + +
    + +
    + +
    + +
    +
    + Gesamtstunden diese Woche: + 0.00 h +
    + +
    + +
    + +

    Stunden werden automatisch gespeichert. Am Ende der Woche können Sie die Stunden abschicken.

    +
    +
    + + +
    + +
    +
    + Aktuelle Überstunden + ? +
    +
    -
    +
    Stunden
    +
    +
    +
    + Verbleibende Urlaubstage + ? +
    +
    -
    +
    von - Tagen
    +
    +
    +
    + Verplante Urlaubstage + ? +
    +
    -
    +
    Tage
    +
    +
    + + +
    +

    + + Automatische Zeiterfassung +

    + +
    +
    +
    +
    + + + + + + + + + + diff --git a/views/login.ejs b/views/login.ejs new file mode 100644 index 0000000..2621dc9 --- /dev/null +++ b/views/login.ejs @@ -0,0 +1,46 @@ + + + + + + Login - Stundenerfassung + + + + + + + diff --git a/views/verwaltung.ejs b/views/verwaltung.ejs new file mode 100644 index 0000000..54e441e --- /dev/null +++ b/views/verwaltung.ejs @@ -0,0 +1,780 @@ + + + + + + Verwaltung - Stundenerfassung + + + + + + +
    +
    +

    Postfach - Eingereichte Stundenzettel

    + + +
    +

    Massendownload für Kalenderwoche

    +
    +
    + + +
    +
    + + +
    + +
    + +
    + + <% if (!groupedByEmployee || groupedByEmployee.length === 0) { %> +
    +

    Keine eingereichten Stundenzettel vorhanden.

    +
    + <% } else { %> +
    + <% groupedByEmployee.forEach(function(employee, employeeIndex) { %> + +
    +
    +
    +
    + <%= employee.user.firstname %> <%= employee.user.lastname %> + <% if (employee.user.personalnummer) { %> + (Personalnummer: <%= employee.user.personalnummer %>) + <% } %> +
    +
    +
    + Wochenstunden: <%= employee.user.wochenstunden || '-' %> +
    +
    + Urlaubstage: <%= employee.user.urlaubstage || '-' %> +
    +
    + Überstunden-Offset: + + +
    +
    + Kalenderwochen: <%= employee.weeks.length %> +
    +
    + Krankheitstage (<%= new Date().getFullYear() %>): + - +
    +
    +
    + +
    + + + +
    + <% }); %> +
    + <% } %> +
    +
    + + + + +