diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dc97130 --- /dev/null +++ b/.env.example @@ -0,0 +1,34 @@ +# Oikos — Umgebungsvariablen +# Kopiere diese Datei nach .env und passe die Werte an. + +# Server +PORT=3000 +NODE_ENV=production + +# Session +SESSION_SECRET=HIER_EINEN_LANGEN_ZUFAELLIGEN_STRING_EINTRAGEN + +# Datenbank (SQLite/SQLCipher) +DB_PATH=/data/oikos.db +DB_ENCRYPTION_KEY=HIER_EINEN_STARKEN_VERSCHLUESSELUNGSSCHLUESSEL_EINTRAGEN + +# Wetter (OpenWeatherMap) +OPENWEATHER_API_KEY=DEIN_API_KEY +OPENWEATHER_CITY=Berlin +OPENWEATHER_UNITS=metric +OPENWEATHER_LANG=de + +# Google Calendar (optional) +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_REDIRECT_URI=https://deine-domain.de/api/v1/calendar/google/callback + +# Apple Calendar CalDAV (optional) +APPLE_CALDAV_URL=https://caldav.icloud.com +APPLE_USERNAME= +APPLE_APP_SPECIFIC_PASSWORD= + +# Sicherheit +RATE_LIMIT_WINDOW_MS=60000 +RATE_LIMIT_MAX_ATTEMPTS=5 +RATE_LIMIT_BLOCK_DURATION_MS=900000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e22336 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Abhängigkeiten +node_modules/ + +# Umgebungsvariablen (NIEMALS committen) +.env + +# Datenbank +*.db +*.db-shm +*.db-wal + +# Logs +*.log +logs/ + +# System +.DS_Store +Thumbs.db + +# Build-Artefakte +dist/ +.cache/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ea1bf69 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,516 @@ +# CLAUDE.md — Familienplaner Web-App „Oikos" + + +Du bist ein Senior Full-Stack-Entwickler mit Expertise in Self-Hosted Web-Apps, Progressive Web Apps, Datenbankdesign und UX/UI-Design. Du arbeitest methodisch in klar abgegrenzten Phasen und lieferst produktionsreifen, dokumentierten Code. + + + +Selbstgehostete Familienplaner-Web-App mit dem Namen "Oikos" für eine einzelne Familie (2–6 Personen). Kein App-Store, kein öffentlicher Zugang. Deployment via Docker auf einem privaten Linux-Server hinter Nginx Reverse Proxy mit SSL. Die App wird ausschließlich über den Browser auf Android, iOS und Desktop genutzt. + + +--- + +## ARCHITEKTUR-ENTSCHEIDUNGEN + + +### Backend +- **Runtime:** Node.js (LTS) +- **Framework:** Express.js (minimalistisch, nur Routing + Middleware) +- **Datenbank:** SQLite mit **better-sqlite3** + **SQLCipher** (Verschlüsselung at rest) +- **Auth:** Session-basiert mit **bcrypt** (Passwort-Hashing) + **express-session** mit SQLite-Session-Store +- **API-Design:** RESTful JSON-API, alle Routen unter `/api/v1/` +- **Kalender-Sync:** CalDAV-Client-Library für Google Calendar / Apple Calendar Integration +- **Wetter-API:** OpenWeatherMap Free Tier (serverseitiger Proxy, API-Key nie im Frontend) + +### Frontend +- **Kein Build-Step.** Kein React, kein Vue, kein Webpack, kein Bundler. +- **Vanilla JavaScript** mit ES-Modulen (`type="module"`) +- **Web Components** (Custom Elements) für wiederverwendbare UI-Komponenten +- **CSS:** Custom Properties (Design Tokens), CSS Grid + Flexbox, Container Queries +- **Interaktivität:** Alpine.js (optional, ~15kb, CDN-Einbindung, kein Build nötig) — NUR wenn DOM-Manipulation in Vanilla JS zu verbose wird +- **Icons:** Lucide Icons (SVG, CDN) +- **PWA:** Service Worker für Offline-Grundfunktionalität, Web App Manifest für „Add to Homescreen" + +### Deployment +- **Docker Compose:** Ein Container für die App (Node.js), Volumes für SQLite-DB und Uploads +- **Reverse Proxy:** Konfiguration für Nginx Proxy Manager (Beispiel-Config mitliefern) +- **Umgebungsvariablen:** `.env`-Datei für alle Secrets (DB-Passwort, API-Keys, Session-Secret) + + + +1. **Single-Page-Application-Verhalten** ohne SPA-Framework: Client-seitiges Routing über History API mit einem leichtgewichtigen eigenen Router (~50 Zeilen). +2. **API-First:** Jedes Modul hat eine saubere REST-API. Das Frontend ist ein reiner API-Consumer. +3. **Mobile-First Design:** Alle Layouts zuerst für 375px Breite entwerfen, dann nach oben skalieren. +4. **Offline-Tolerant:** Service Worker cached App-Shell. Daten werden bei Reconnect synchronisiert. +5. **Keine Telemetrie, kein Tracking, keine externen Fonts.** Alles self-contained. + + +--- + +## DATENMODELL + + +Entwirf das Schema nach diesen Entitäten. Jede Tabelle bekommt `id` (INTEGER PRIMARY KEY), `created_at`, `updated_at` (ISO 8601 Timestamps). + +### Users +- `username` (UNIQUE, NOT NULL) +- `display_name` +- `password_hash` (bcrypt) +- `avatar_color` (HEX — für farbliche Zuordnung im UI) +- `role` (ENUM: 'admin', 'member') — Admin kann Familienmitglieder verwalten + +### Tasks (Aufgaben-Modul) +- `title` (NOT NULL) +- `description` (Notizfeld, TEXT) +- `category` (z.B. Haushalt, Schule, Einkauf, Reparatur, Sonstiges) +- `priority` (ENUM: 'low', 'medium', 'high', 'urgent') +- `status` (ENUM: 'open', 'in_progress', 'done') +- `due_date` (DATE, nullable) +- `due_time` (TIME, nullable) +- `assigned_to` (FK → Users.id, nullable) +- `created_by` (FK → Users.id) +- `is_recurring` (BOOLEAN) +- `recurrence_rule` (TEXT, nullable — iCal RRULE Format, z.B. `FREQ=WEEKLY;BYDAY=MO,TH`) +- `parent_task_id` (FK → Tasks.id, nullable — für Teilaufgaben) + +### Shopping Lists (Einkaufslisten-Modul) +- `name` (z.B. "Wocheneinkauf", "Baumarkt") + +### Shopping Items +- `list_id` (FK → Shopping Lists.id) +- `name` (NOT NULL) +- `quantity` (TEXT, z.B. "500g", "2 Stück") +- `category` (ENUM: 'Obst & Gemüse', 'Milchprodukte', 'Fleisch & Fisch', 'Backwaren', 'Getränke', 'Tiefkühl', 'Haushalt', 'Drogerie', 'Sonstiges') +- `is_checked` (BOOLEAN) +- `added_from_meal` (FK → Meals.id, nullable — Herkunft aus Essensplan) + +### Meals (Essensplan-Modul) +- `date` (DATE, NOT NULL) +- `meal_type` (ENUM: 'breakfast', 'lunch', 'dinner', 'snack') +- `title` (NOT NULL, z.B. "Spaghetti Bolognese") +- `notes` (TEXT, nullable) +- `created_by` (FK → Users.id) + +### Meal Ingredients +- `meal_id` (FK → Meals.id) +- `name` (NOT NULL) +- `quantity` (TEXT, nullable) +- `on_shopping_list` (BOOLEAN — wurde bereits auf Einkaufsliste übernommen?) + +### Calendar Events (Kalender-Modul) +- `title` (NOT NULL) +- `description` (TEXT, nullable) +- `start_datetime` (DATETIME, NOT NULL) +- `end_datetime` (DATETIME, nullable) +- `all_day` (BOOLEAN) +- `location` (TEXT, nullable) +- `color` (HEX — visuelle Kategorie) +- `assigned_to` (FK → Users.id, nullable) +- `created_by` (FK → Users.id) +- `external_calendar_id` (TEXT, nullable — ID aus Google/Apple Calendar) +- `external_source` (ENUM: 'local', 'google', 'apple') +- `recurrence_rule` (TEXT, nullable — iCal RRULE) + +### Notes (Pinnwand-Modul) +- `title` (nullable) +- `content` (TEXT, NOT NULL) +- `color` (HEX — Sticky-Note-Farbe) +- `pinned` (BOOLEAN) +- `created_by` (FK → Users.id) + +### Contacts (Wichtige Kontakte) +- `name` (NOT NULL) +- `category` (ENUM: 'Arzt', 'Schule/Kita', 'Behörde', 'Versicherung', 'Handwerker', 'Notfall', 'Sonstiges') +- `phone` (TEXT, nullable) +- `email` (TEXT, nullable) +- `address` (TEXT, nullable) +- `notes` (TEXT, nullable) + +### Budget Entries (Budget-Tracker) +- `title` (NOT NULL) +- `amount` (REAL, NOT NULL — positiv = Einnahme, negativ = Ausgabe) +- `category` (ENUM: 'Lebensmittel', 'Miete', 'Versicherung', 'Mobilität', 'Freizeit', 'Kleidung', 'Gesundheit', 'Bildung', 'Sonstiges') +- `date` (DATE, NOT NULL) +- `is_recurring` (BOOLEAN) +- `recurrence_rule` (TEXT, nullable) +- `created_by` (FK → Users.id) + + +--- + +## MODULE — FUNKTIONALE SPEZIFIKATIONEN + + +### Dashboard (Startseite) +**Route:** `/` + +**Layout:** Responsive Grid (1 Spalte mobil, 2 Spalten Tablet, 3 Spalten Desktop). + +**Widgets:** +1. **Begrüßung** — "Guten [Morgen/Tag/Abend], [Name]" mit aktuellem Datum +2. **Wetter-Widget** — Aktuelles Wetter + 3-Tage-Vorschau. Standort konfigurierbar in Settings. Daten vom Backend-Proxy (OpenWeatherMap). Refresh alle 30 Minuten. Fallback bei API-Fehler: Widget ausblenden, kein Error-Screen. +3. **Anstehende Termine** — Nächste 3–5 Termine aus dem Kalender-Modul. Farbcodiert nach Person. Klick → Kalender-Modul. +4. **Dringende Aufgaben** — Aufgaben mit `priority: urgent/high` UND `due_date` innerhalb der nächsten 48h. Sortiert nach Fälligkeit. Klick → Aufgaben-Modul. +5. **Heutiges Essen** — Mahlzeiten für heute aus dem Essensplan. Zeigt Titel + Meal-Type. Klick → Essensplan-Modul. +6. **Pinnwand-Vorschau** — Letzte 2–3 angepinnte Notizen. Klick → Pinnwand. +7. **Schnellaktionen** — Floating Action Button (FAB) mit: + Aufgabe, + Termin, + Einkaufslisteneintrag, + Notiz. + +**Design-Vorgaben:** +- Widgets als Cards mit `border-radius: 12px`, subtiler `box-shadow` +- Farbschema: Neutraler Hintergrund (`#F5F5F7`), Cards weiß, Akzentfarbe konfigurierbar +- Smooth Scroll, keine abrupten Übergänge +- Skeleton-Loading-States während API-Calls (keine Spinner) + + + +### Aufgaben-Modul +**Route:** `/tasks` + +**Ansichten:** +1. **Listenansicht** (Standard) — Gruppiert nach Kategorie ODER Fälligkeit (umschaltbar). Filter: Person, Priorität, Status. +2. **Kanban-Ansicht** — Spalten: Offen → In Bearbeitung → Erledigt. Drag & Drop zum Statuswechsel. + +**Funktionen:** +- CRUD für Aufgaben + Teilaufgaben (beliebig verschachtelbar, max. 2 Ebenen) +- Zuweisung an Familienmitglied (Avatar-Farbe als Indikator) +- Dringlichkeitsstufen visuell durch Farbe/Icon codiert +- Wiederkehrende Aufgaben: Bei Erledigung wird automatisch die nächste Instanz erstellt +- Swipe-Gesten auf Mobil: Links = erledigt, Rechts = bearbeiten +- Benachrichtigung (In-App-Badge) bei überfälligen Aufgaben + +**Teilaufgaben:** +- Checkbox-Liste innerhalb einer Aufgabe +- Fortschrittsbalken (z.B. 3/5 Teilaufgaben erledigt) +- Eigene Notiz pro Teilaufgabe + + + +### Einkaufslisten-Modul +**Route:** `/shopping` + +**Funktionen:** +- Mehrere Listen parallel (z.B. "REWE", "dm", "Baumarkt") +- Artikel mit Kategorie, Menge, Checkbox +- Automatische Gruppierung nach Kategorie (Supermarkt-Gang-Logik) +- **Essensplan-Integration:** Button "Zutaten auf Einkaufsliste" im Essensplan → Zutaten werden mit Quell-Referenz übernommen +- Erledigte Artikel werden durchgestrichen, nach unten sortiert +- "Liste leeren" entfernt nur abgehakte Artikel +- Artikel-Vorschläge basierend auf bisherigen Einträgen (lokaler Autocomplete, kein externer Service) + + + +### Essensplan-Modul +**Route:** `/meals` + +**Layout:** Wochenansicht (Mo–So), jeder Tag mit Slots für Frühstück/Mittag/Abend/Snack. + +**Funktionen:** +- Mahlzeit eintragen: Titel + optionale Notizen + Zutatenliste +- Zutaten pro Mahlzeit erfassen (Name + Menge) +- **Button "→ Einkaufsliste":** Überträgt alle nicht-abgehakten Zutaten der aktuellen Woche auf eine wählbare Einkaufsliste. Bereits übertragene Zutaten werden markiert. +- Wochennavigation (vor/zurück) +- Mahlzeiten per Drag & Drop zwischen Tagen/Slots verschieben +- Vergangene Mahlzeiten als Vorschläge beim Tippen (Autocomplete aus Historie) + + + +### Familienkalender-Modul +**Route:** `/calendar` + +**Ansichten:** +1. **Monatsansicht** (Standard) — Tage mit Punkt-Indikatoren für Termine +2. **Wochenansicht** — Stundenraster mit Terminblöcken +3. **Tagesansicht** — Detaillierte Timeline +4. **Agenda-Ansicht** — Chronologische Liste aller kommenden Termine + +**Funktionen:** +- CRUD für Termine (Titel, Beschreibung, Start/Ende, Ganztägig, Ort, Farbe, Zuweisung) +- Farbcodierung pro Person (Avatar-Farbe aus User-Profil) +- Wiederkehrende Termine (iCal RRULE) +- **Kalender-Sync:** + - **Google Calendar:** OAuth 2.0 → Google Calendar API v3. Zwei-Wege-Sync. + - **Apple Calendar (iCloud):** CalDAV-Protokoll. Zwei-Wege-Sync. + - Sync-Intervall konfigurierbar (Standard: alle 15 Minuten) + - Externe Termine visuell unterscheidbar (dezenter Badge/Icon) + - Konflikte: Externes Event gewinnt bei Änderungen, lokale Ergänzungen bleiben erhalten + + + +### Pinnwand / Notizen-Modul +**Route:** `/notes` + +**Layout:** Masonry-Grid (Pinterest-Style) mit farbigen Sticky Notes. + +**Funktionen:** +- CRUD für Notizen (Titel optional, Inhalt, Farbe wählbar) +- Anpinnen (erscheint oben + auf Dashboard) +- Ersteller wird angezeigt (Avatar-Farbe) +- Markdown-Light im Inhalt (fett, kursiv, Listen — kein Full-Markdown-Parser nötig, regex-basiert) + + + +### Wichtige Kontakte +**Route:** `/contacts` + +**Funktionen:** +- CRUD für Kontakte mit Kategorie-Filter +- Telefonnummer als `tel:`-Link (direkter Anruf auf Mobil) +- E-Mail als `mailto:`-Link +- Adresse als Link zu Google Maps / Apple Maps (User-Agent-Detection) +- Suchfeld mit Echtzeit-Filter + + + +### Budget-Tracker +**Route:** `/budget` + +**Ansichten:** +1. **Monatsübersicht** — Einnahmen vs. Ausgaben, Saldo. Balkendiagramm nach Kategorie (Canvas-basiert, keine Chart-Library). +2. **Transaktionsliste** — Chronologisch, filterbar nach Kategorie/Monat. + +**Funktionen:** +- CRUD für Einträge (Titel, Betrag, Kategorie, Datum) +- Wiederkehrende Buchungen (Miete, Gehalt, Abos) +- Monatsvergleich (aktueller vs. Vormonat) +- Export als CSV + + +--- + +## AUTHENTIFIZIERUNG & SICHERHEIT + + +1. **Login-Screen:** Username + Passwort. Kein öffentlicher Registrierungs-Endpoint. Neue User werden nur durch Admin erstellt. +2. **Passwort-Hashing:** bcrypt mit Cost Factor 12. +3. **Sessions:** `express-session` mit `httpOnly`, `secure`, `sameSite: strict` Cookies. Session-Timeout: 7 Tage. +4. **CSRF-Protection:** Double Submit Cookie Pattern. +5. **Rate Limiting:** 5 Login-Versuche pro Minute pro IP, danach 15min Sperre. +6. **Datenbank-Verschlüsselung:** SQLCipher (AES-256-CBC). Schlüssel aus `.env`. +7. **Input Validation:** Alle API-Inputs serverseitig validieren. Kein `eval()`, kein `innerHTML` mit User-Input. +8. **Content Security Policy:** Strikte CSP-Header. Nur eigene Ressourcen + explizit erlaubte CDNs. +9. **HTTPS-only:** App setzt `Strict-Transport-Security` Header. Redirect HTTP → HTTPS. + + +--- + +## UI/UX DESIGN-SYSTEM + + +### Farben (CSS Custom Properties) +```css +:root { + /* Neutrals */ + --color-bg: #F5F5F7; + --color-surface: #FFFFFF; + --color-border: #E5E5EA; + --color-text-primary: #1C1C1E; + --color-text-secondary: #8E8E93; + + /* Akzent (konfigurierbar pro Installation) */ + --color-accent: #007AFF; + --color-accent-light: #E3F2FF; + + /* Semantisch */ + --color-success: #34C759; + --color-warning: #FF9500; + --color-danger: #FF3B30; + --color-info: #5AC8FA; + + /* Prioritäten */ + --color-priority-low: #8E8E93; + --color-priority-medium: #FF9500; + --color-priority-high: #FF6B35; + --color-priority-urgent: #FF3B30; + + /* Schatten */ + --shadow-sm: 0 1px 3px rgba(0,0,0,0.08); + --shadow-md: 0 4px 12px rgba(0,0,0,0.1); + --shadow-lg: 0 8px 24px rgba(0,0,0,0.12); + + /* Radien */ + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + + /* Typografie */ + --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --font-mono: 'SF Mono', 'Fira Code', monospace; +} + +/* Dark Mode */ +@media (prefers-color-scheme: dark) { + :root { + --color-bg: #1C1C1E; + --color-surface: #2C2C2E; + --color-border: #3A3A3C; + --color-text-primary: #F5F5F7; + --color-text-secondary: #8E8E93; + } +} +``` + +### Typografie +- Überschriften: System Font Stack, font-weight 600–700 +- Body: 16px (mobile), 15px (desktop), line-height 1.5 +- Small/Caption: 13px, `color: var(--color-text-secondary)` + +### Komponenten-Standards +- **Cards:** `background: var(--color-surface)`, `border-radius: var(--radius-md)`, `box-shadow: var(--shadow-sm)` +- **Buttons:** Primär = `var(--color-accent)` + weiße Schrift. Sekundär = Outline. Mindesthöhe 44px (Touch-Target). +- **Inputs:** `border-radius: var(--radius-sm)`, `border: 1.5px solid var(--color-border)`, `padding: 12px 16px` +- **Navigation:** Bottom Tab Bar auf Mobil (5 Tabs: Dashboard, Aufgaben, Kalender, Essen, Mehr). Sidebar auf Desktop. +- **Übergänge:** `transition: all 0.2s ease` für interaktive Elemente. Seiten-Übergänge: Slide-Animation. +- **Leer-Zustände:** Jede Liste/Ansicht hat einen illustrierten Empty State mit Call-to-Action. +- **Touch:** Haptic Feedback wo möglich (`navigator.vibrate`). Pull-to-Refresh auf Listen. + +### Responsive Breakpoints +- Mobil: < 768px (1 Spalte, Bottom Nav) +- Tablet: 768px–1024px (2 Spalten, Bottom Nav) +- Desktop: > 1024px (Sidebar + Content Area) + + +--- + +## ENTWICKLUNGSPLAN — PHASENSTRUKTUR + + +### Phase 1: Fundament +1. Projektstruktur anlegen (Verzeichnisse, package.json, .env.example, .gitignore) +2. Express-Server mit grundlegendem Routing-Setup +3. SQLite + SQLCipher Datenbankverbindung + Schema-Migration +4. Auth-System (Login, Session, User-CRUD für Admin) +5. Frontend App-Shell (SPA-Router, Navigation, Layout-Gerüst) +6. CSS Design-System (Custom Properties, Basis-Komponenten) +7. Docker Compose + Nginx-Config + +### Phase 2: Kern-Module +8. Dashboard (Layout + Widget-Slots, zunächst mit Platzhaltern) +9. Aufgaben-Modul (CRUD + Listenansicht + Teilaufgaben) +10. Einkaufslisten-Modul (CRUD + Kategorien + Checkbox-Logik) +11. Essensplan-Modul (Wochenansicht + CRUD + Zutaten) +12. Essensplan → Einkaufsliste Integration + +### Phase 3: Kalender & Erweiterungen +13. Kalender-Modul (lokale Termine, Monats-/Wochen-/Tagesansicht) +14. Google Calendar OAuth + Sync +15. Apple Calendar CalDAV + Sync +16. Pinnwand-Modul +17. Kontakte-Modul +18. Budget-Tracker + +### Phase 4: Polish & Integration +19. Dashboard-Widgets mit Live-Daten verbinden +20. Wetter-Widget (OpenWeatherMap-Integration) +21. Wiederkehrende Aufgaben + Termine (RRULE-Engine) +22. Kanban-Ansicht für Aufgaben +23. Dark Mode +24. PWA (Service Worker, Manifest, Offline-Shell) +25. Drag & Drop (Aufgaben, Essensplan) +26. Swipe-Gesten (Mobil) + +### Phase 5: Härtung +27. Input-Validation + Sanitization auf allen Endpoints +28. CSRF-Protection +29. Rate Limiting +30. CSP-Header + Security-Audit +31. Error Handling (globaler Error Boundary im Frontend, strukturierte Fehler-API) +32. Performance-Optimierung (Lazy Loading, Caching-Strategie) +33. README.md mit Setup-Anleitung + + +--- + +## ANWEISUNGEN FÜR CLAUDE CODE + + +1. **Arbeite Phase für Phase.** Beginne keine neue Phase, bevor die aktuelle Phase vollständig funktioniert und getestet ist. +2. **Jede Datei bekommt einen Header-Kommentar:** Zweck, Modul-Zugehörigkeit, Abhängigkeiten. +3. **Kein toter Code.** Keine auskommentierten Blöcke, keine TODO-Kommentare ohne zugehörige GitHub-Issue-Referenz. +4. **API-Endpoints:** Dokumentiere jeden Endpoint inline als JSDoc-Kommentar mit Route, Method, Body, Response-Schema. +5. **Fehlerbehandlung:** Jeder API-Endpoint hat try/catch. Fehler werden als `{ error: string, code: number }` zurückgegeben. +6. **Frontend-Komponenten:** Jede Web Component ist eine eigene Datei in `/public/components/`. Naming: `fb-[modul]-[name].js` (z.B. `fb-task-card.js`). +7. **CSS:** Eine globale `styles.css` mit Design Tokens + Reset. Pro Modul eine eigene CSS-Datei. Kein Inline-Styling. +8. **Dateistruktur:** +``` +/oikos +├── server/ +│ ├── index.js (Express Entry Point) +│ ├── db.js (SQLite/SQLCipher Setup + Migrations) +│ ├── auth.js (Auth Middleware + Routes) +│ ├── routes/ +│ │ ├── tasks.js +│ │ ├── shopping.js +│ │ ├── meals.js +│ │ ├── calendar.js +│ │ ├── notes.js +│ │ ├── contacts.js +│ │ ├── budget.js +│ │ └── weather.js +│ └── services/ +│ ├── google-calendar.js +│ ├── apple-calendar.js +│ └── recurrence.js (RRULE-Parser/Generator) +├── public/ +│ ├── index.html (App Shell) +│ ├── router.js (Client-Side Router) +│ ├── api.js (Fetch-Wrapper mit Auth + Error Handling) +│ ├── styles/ +│ │ ├── tokens.css +│ │ ├── reset.css +│ │ ├── layout.css +│ │ └── [modul].css +│ ├── components/ +│ │ ├── fb-app-shell.js +│ │ ├── fb-nav-bar.js +│ │ ├── fb-dashboard.js +│ │ ├── fb-task-*.js +│ │ ├── fb-shopping-*.js +│ │ ├── fb-meal-*.js +│ │ ├── fb-calendar-*.js +│ │ ├── fb-notes-*.js +│ │ ├── fb-contacts-*.js +│ │ └── fb-budget-*.js +│ ├── pages/ +│ │ ├── dashboard.js +│ │ ├── tasks.js +│ │ ├── shopping.js +│ │ ├── meals.js +│ │ ├── calendar.js +│ │ ├── notes.js +│ │ ├── contacts.js +│ │ ├── budget.js +│ │ ├── settings.js +│ │ └── login.js +│ ├── sw.js (Service Worker) +│ └── manifest.json +├── docker-compose.yml +├── Dockerfile +├── nginx.conf.example +├── .env.example +├── package.json +└── README.md +``` +9. **Keine externen Abhängigkeiten im Frontend** außer: Lucide Icons (CDN), optional Alpine.js (CDN). +10. **Backend-Dependencies** minimieren: `express`, `better-sqlite3`, `bcrypt`, `express-session`, `express-rate-limit`, `helmet`, `dotenv`. Für Kalender-Sync: `node-fetch`, `googleapis` (Google), `tsdav` (CalDAV). +11. **Deutsche UI-Texte.** Alle Labels, Buttons, Meldungen auf Deutsch. Datumsformate: `DD.MM.YYYY`, Uhrzeiten: `HH:MM` (24h). +12. **Bei Architektur-Unklarheiten:** Nicht raten. Frage nach. Dokumentiere die Entscheidung als Kommentar. + + +--- + +## QUALITÄTSKRITERIEN + + +- [ ] App startet mit `docker compose up` ohne manuelle Schritte außer `.env` konfigurieren +- [ ] Login funktioniert, Session bleibt über Browser-Neustart erhalten +- [ ] Alle 8 Module (Dashboard, Aufgaben, Einkauf, Essen, Kalender, Pinnwand, Kontakte, Budget) sind CRUD-funktional +- [ ] Essensplan-Zutaten können auf Einkaufsliste übernommen werden +- [ ] Dashboard zeigt Live-Daten aus allen relevanten Modulen +- [ ] Wetter-Widget zeigt aktuelle Daten +- [ ] App ist auf iPhone SE (375px) voll bedienbar +- [ ] Dark Mode funktioniert systemgesteuert +- [ ] Datenbank ist verschlüsselt (SQLCipher) +- [ ] Kein API-Endpoint ist ohne Auth erreichbar (außer Login) +- [ ] Lighthouse Mobile Score: Performance > 85, Accessibility > 90, Best Practices > 90 +- [ ] Google Calendar Sync funktioniert bidirektional + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..db8bc99 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM node:20-slim + +# SQLCipher-Abhängigkeiten +RUN apt-get update && apt-get install -y \ + python3 \ + make \ + g++ \ + libsqlcipher-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Abhängigkeiten zuerst (Docker-Layer-Caching) +COPY package*.json ./ +RUN npm ci --omit=dev + +# Anwendungscode +COPY . . + +# Daten-Volume-Verzeichnis +RUN mkdir -p /data + +EXPOSE 3000 + +USER node + +CMD ["node", "server/index.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..539715f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +version: "3.9" + +services: + oikos: + build: . + container_name: oikos + restart: unless-stopped + ports: + - "3000:3000" + volumes: + - oikos_data:/data + env_file: + - .env + environment: + - NODE_ENV=production + - DB_PATH=/data/oikos.db + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode === 200 ? 0 : 1))"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + +volumes: + oikos_data: + driver: local diff --git a/nginx.conf.example b/nginx.conf.example new file mode 100644 index 0000000..652572c --- /dev/null +++ b/nginx.conf.example @@ -0,0 +1,48 @@ +# Nginx Reverse Proxy Konfiguration für Oikos +# Für Nginx Proxy Manager: Diese Datei als Vorlage für "Advanced"-Konfiguration nutzen. +# Ersetze "deine-domain.de" mit deiner tatsächlichen Domain. + +server { + listen 80; + server_name deine-domain.de; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + server_name deine-domain.de; + + # SSL (von Nginx Proxy Manager automatisch verwaltet, oder manuell): + # ssl_certificate /etc/letsencrypt/live/deine-domain.de/fullchain.pem; + # ssl_certificate_key /etc/letsencrypt/live/deine-domain.de/privkey.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + # Security Headers + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Proxy zu Oikos + location / { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # Statische Assets cachen + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $host; + expires 7d; + add_header Cache-Control "public, immutable"; + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..62d2696 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3028 @@ +{ + "name": "oikos", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "oikos", + "version": "1.0.0", + "dependencies": { + "bcrypt": "^5.1.1", + "better-sqlite3": "^9.6.0", + "connect-sqlite3": "^0.9.15", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "express-rate-limit": "^7.5.0", + "express-session": "^1.18.1", + "helmet": "^8.0.0", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "googleapis": "^144.0.0", + "tsdav": "^2.0.10" + } + }, + "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==", + "license": "MIT", + "optional": true + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "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==", + "license": "ISC", + "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", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/base-64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", + "license": "MIT", + "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" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/better-sqlite3": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.6.0.tgz", + "integrity": "sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "*" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "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==", + "license": "ISC", + "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-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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "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==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/connect-sqlite3": { + "version": "0.9.16", + "resolved": "https://registry.npmjs.org/connect-sqlite3/-/connect-sqlite3-0.9.16.tgz", + "integrity": "sha512-2gqo0QmcBBL8p8+eqpBETn7RgM/PaoKvpQGl8PfjEgwlr0VuMYNMxRJRrRCo3KR3fxMYeSsCw2tGNG0JKN9Nvg==", + "dependencies": { + "sqlite3": "^5.0.2" + }, + "engines": { + "node": ">=0.4.x" + } + }, + "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==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/cross-fetch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", + "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", + "license": "MIT", + "optional": true, + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "(MIT OR WTFPL)", + "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==", + "license": "MIT", + "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-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express-session": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz", + "integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==", + "license": "MIT", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT", + "optional": true + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "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/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "ISC", + "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==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/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==", + "license": "MIT", + "optional": true + }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob 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 exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "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/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis": { + "version": "144.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-144.0.0.tgz", + "integrity": "sha512-ELcWOXtJxjPX4vsKMh+7V+jZvgPwYMlEhQFiu2sa9Qmt5veX8nwXPksOWGGN6Zk4xCiLygUyaz7xGtcMO+Onxw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "google-auth-library": "^9.0.0", + "googleapis-common": "^7.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/googleapis-common": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", + "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^6.0.3", + "google-auth-library": "^9.7.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "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==", + "license": "ISC", + "optional": true + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "ISC", + "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.", + "license": "ISC", + "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==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "optional": true + }, + "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==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "optional": true + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "optional": true, + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "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==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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==", + "license": "ISC", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "MIT", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "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==", + "license": "MIT", + "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/node-gyp/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.", + "license": "ISC", + "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/node-gyp/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.", + "license": "ISC", + "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/node-gyp/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.", + "license": "ISC", + "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/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "ISC", + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "MIT" + }, + "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==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "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/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "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==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "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", + "license": "ISC", + "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" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "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==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "ISC" + }, + "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" + } + ], + "license": "MIT" + }, + "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" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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, + "license": "BSD-3-Clause", + "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/sqlite3/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==", + "license": "MIT" + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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 exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "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==", + "license": "MIT", + "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==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "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==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tsdav": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/tsdav/-/tsdav-2.1.8.tgz", + "integrity": "sha512-zvQvhZLzTaEmNNgJbBlUYT/JOq9Xpw/xkxCqs7IT2d2/7o7pss0iZOlZXuHJ5VcvSvTny42Vc6+6GyzZcrCJ1g==", + "license": "MIT", + "optional": true, + "dependencies": { + "base-64": "1.0.0", + "cross-fetch": "4.1.0", + "debug": "4.4.3", + "xml-js": "1.6.11" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tsdav/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/tsdav/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==", + "license": "MIT", + "optional": true + }, + "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==", + "license": "Apache-2.0", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD", + "optional": true + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "license": "MIT", + "optional": true, + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..355ece8 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "oikos", + "version": "1.0.0", + "description": "Selbstgehostete Familienplaner-Web-App", + "main": "server/index.js", + "scripts": { + "start": "node server/index.js", + "dev": "node --watch server/index.js", + "setup": "node setup.js", + "test:db": "node --experimental-sqlite test-db.js" + }, + "dependencies": { + "bcrypt": "^5.1.1", + "better-sqlite3": "^9.6.0", + "connect-sqlite3": "^0.9.15", + "dotenv": "^16.4.7", + "express": "^4.21.2", + "express-rate-limit": "^7.5.0", + "express-session": "^1.18.1", + "helmet": "^8.0.0", + "node-fetch": "^3.3.2" + }, + "optionalDependencies": { + "googleapis": "^144.0.0", + "tsdav": "^2.0.10" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/public/api.js b/public/api.js new file mode 100644 index 0000000..83f3144 --- /dev/null +++ b/public/api.js @@ -0,0 +1,95 @@ +/** + * Modul: API-Client + * Zweck: Fetch-Wrapper mit Session-Auth, einheitlicher Fehlerbehandlung und JSON-Parsing + * Abhängigkeiten: keine + */ + +const API_BASE = '/api/v1'; + +/** + * Zentraler Fetch-Wrapper. + * Setzt Content-Type, handhabt 401-Redirects und parsed JSON-Fehler. + * + * @param {string} path - API-Pfad ohne /api/v1 (z.B. '/tasks') + * @param {RequestInit} options - Fetch-Optionen + * @returns {Promise} Geparstes JSON oder wirft einen Fehler + */ +async function apiFetch(path, options = {}) { + const url = `${API_BASE}${path}`; + + const response = await fetch(url, { + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + ...options, + }); + + if (response.status === 401) { + // Session abgelaufen → zur Login-Seite + window.dispatchEvent(new CustomEvent('auth:expired')); + throw new Error('Sitzung abgelaufen.'); + } + + const data = await response.json().catch(() => null); + + if (!response.ok) { + const message = data?.error || `HTTP ${response.status}`; + throw new ApiError(message, response.status, data); + } + + return data; +} + +/** + * Strukturierter API-Fehler mit HTTP-Status-Code. + */ +class ApiError extends Error { + constructor(message, status, data = null) { + super(message); + this.name = 'ApiError'; + this.status = status; + this.data = data; + } +} + +// -------------------------------------------------------- +// Convenience-Methoden +// -------------------------------------------------------- + +const api = { + get: (path) => apiFetch(path, { method: 'GET' }), + + post: (path, body) => apiFetch(path, { + method: 'POST', + body: JSON.stringify(body), + }), + + put: (path, body) => apiFetch(path, { + method: 'PUT', + body: JSON.stringify(body), + }), + + patch: (path, body) => apiFetch(path, { + method: 'PATCH', + body: JSON.stringify(body), + }), + + delete: (path) => apiFetch(path, { method: 'DELETE' }), +}; + +// -------------------------------------------------------- +// Auth-spezifische Methoden +// -------------------------------------------------------- + +const auth = { + login: (username, password) => api.post('/auth/login', { username, password }), + logout: () => api.post('/auth/logout'), + me: () => api.get('/auth/me'), + getUsers: () => api.get('/auth/users'), + createUser: (data) => api.post('/auth/users', data), + deleteUser: (id) => api.delete(`/auth/users/${id}`), +}; + +export { api, auth, ApiError }; diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..f2c9011 --- /dev/null +++ b/public/index.html @@ -0,0 +1,47 @@ + + + + + + + + Oikos + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..5941b72 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,25 @@ +{ + "name": "Oikos Familienplaner", + "short_name": "Oikos", + "description": "Selbstgehosteter Familienplaner für Kalender, Aufgaben, Einkauf und mehr.", + "start_url": "/", + "display": "standalone", + "background_color": "#F5F5F7", + "theme_color": "#007AFF", + "orientation": "portrait-primary", + "lang": "de", + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ] +} diff --git a/public/pages/budget.js b/public/pages/budget.js new file mode 100644 index 0000000..744c130 --- /dev/null +++ b/public/pages/budget.js @@ -0,0 +1,25 @@ +/** + * Modul: Budget + * Zweck: Seite für das Budget-Modul + * Abhängigkeiten: /api.js + */ + +import { api } from '/api.js'; + +/** + * @param {HTMLElement} container + * @param {{ user: object }} context + */ +export async function render(container, { user }) { + container.innerHTML = ` +
+ +
+
Kommt bald.
+
Dieses Modul wird in Phase 2 implementiert.
+
+
+ `; +} diff --git a/public/pages/calendar.js b/public/pages/calendar.js new file mode 100644 index 0000000..46fc333 --- /dev/null +++ b/public/pages/calendar.js @@ -0,0 +1,25 @@ +/** + * Modul: Calendar + * Zweck: Seite für das Calendar-Modul + * Abhängigkeiten: /api.js + */ + +import { api } from '/api.js'; + +/** + * @param {HTMLElement} container + * @param {{ user: object }} context + */ +export async function render(container, { user }) { + container.innerHTML = ` +
+ +
+
Kommt bald.
+
Dieses Modul wird in Phase 2 implementiert.
+
+
+ `; +} diff --git a/public/pages/contacts.js b/public/pages/contacts.js new file mode 100644 index 0000000..cb218ea --- /dev/null +++ b/public/pages/contacts.js @@ -0,0 +1,25 @@ +/** + * Modul: Contacts + * Zweck: Seite für das Contacts-Modul + * Abhängigkeiten: /api.js + */ + +import { api } from '/api.js'; + +/** + * @param {HTMLElement} container + * @param {{ user: object }} context + */ +export async function render(container, { user }) { + container.innerHTML = ` +
+ +
+
Kommt bald.
+
Dieses Modul wird in Phase 2 implementiert.
+
+
+ `; +} diff --git a/public/pages/dashboard.js b/public/pages/dashboard.js new file mode 100644 index 0000000..3795c3a --- /dev/null +++ b/public/pages/dashboard.js @@ -0,0 +1,25 @@ +/** + * Modul: Dashboard + * Zweck: Seite für das Dashboard-Modul + * Abhängigkeiten: /api.js + */ + +import { api } from '/api.js'; + +/** + * @param {HTMLElement} container + * @param {{ user: object }} context + */ +export async function render(container, { user }) { + container.innerHTML = ` +
+ +
+
Kommt bald.
+
Dieses Modul wird in Phase 2 implementiert.
+
+
+ `; +} diff --git a/public/pages/login.js b/public/pages/login.js new file mode 100644 index 0000000..31899c2 --- /dev/null +++ b/public/pages/login.js @@ -0,0 +1,96 @@ +/** + * Modul: Login-Seite + * Zweck: Anmeldeformular mit Username/Passwort, Fehlerbehandlung, Session-Start + * Abhängigkeiten: /api.js + */ + +import { auth } from '/api.js'; + +/** + * Rendert die Login-Seite in den gegebenen Container. + * @param {HTMLElement} container + */ +export async function render(container) { + container.innerHTML = ` +
+ +
+ `; + + const form = container.querySelector('#login-form'); + const errorEl = container.querySelector('#login-error'); + const submitBtn = container.querySelector('#login-btn'); + + form.addEventListener('submit', async (e) => { + e.preventDefault(); + errorEl.hidden = true; + + const username = form.username.value.trim(); + const password = form.password.value; + + if (!username || !password) { + showError(errorEl, 'Bitte alle Felder ausfüllen.'); + return; + } + + submitBtn.disabled = true; + submitBtn.textContent = 'Wird angemeldet …'; + + try { + await auth.login(username, password); + window.oikos.navigate('/'); + } catch (err) { + showError(errorEl, err.status === 429 + ? 'Zu viele Versuche. Bitte warte kurz.' + : 'Ungültige Anmeldedaten.' + ); + } finally { + submitBtn.disabled = false; + submitBtn.textContent = 'Anmelden'; + } + }); +} + +function showError(el, message) { + el.textContent = message; + el.hidden = false; +} diff --git a/public/pages/meals.js b/public/pages/meals.js new file mode 100644 index 0000000..db1103a --- /dev/null +++ b/public/pages/meals.js @@ -0,0 +1,25 @@ +/** + * Modul: Meals + * Zweck: Seite für das Meals-Modul + * Abhängigkeiten: /api.js + */ + +import { api } from '/api.js'; + +/** + * @param {HTMLElement} container + * @param {{ user: object }} context + */ +export async function render(container, { user }) { + container.innerHTML = ` +
+ +
+
Kommt bald.
+
Dieses Modul wird in Phase 2 implementiert.
+
+
+ `; +} diff --git a/public/pages/notes.js b/public/pages/notes.js new file mode 100644 index 0000000..f58bff4 --- /dev/null +++ b/public/pages/notes.js @@ -0,0 +1,25 @@ +/** + * Modul: Notes + * Zweck: Seite für das Notes-Modul + * Abhängigkeiten: /api.js + */ + +import { api } from '/api.js'; + +/** + * @param {HTMLElement} container + * @param {{ user: object }} context + */ +export async function render(container, { user }) { + container.innerHTML = ` +
+ +
+
Kommt bald.
+
Dieses Modul wird in Phase 2 implementiert.
+
+
+ `; +} diff --git a/public/pages/settings.js b/public/pages/settings.js new file mode 100644 index 0000000..ebcac98 --- /dev/null +++ b/public/pages/settings.js @@ -0,0 +1,25 @@ +/** + * Modul: Settings + * Zweck: Seite für das Settings-Modul + * Abhängigkeiten: /api.js + */ + +import { api } from '/api.js'; + +/** + * @param {HTMLElement} container + * @param {{ user: object }} context + */ +export async function render(container, { user }) { + container.innerHTML = ` +
+ +
+
Kommt bald.
+
Dieses Modul wird in Phase 2 implementiert.
+
+
+ `; +} diff --git a/public/pages/shopping.js b/public/pages/shopping.js new file mode 100644 index 0000000..9929066 --- /dev/null +++ b/public/pages/shopping.js @@ -0,0 +1,25 @@ +/** + * Modul: Shopping + * Zweck: Seite für das Shopping-Modul + * Abhängigkeiten: /api.js + */ + +import { api } from '/api.js'; + +/** + * @param {HTMLElement} container + * @param {{ user: object }} context + */ +export async function render(container, { user }) { + container.innerHTML = ` +
+ +
+
Kommt bald.
+
Dieses Modul wird in Phase 2 implementiert.
+
+
+ `; +} diff --git a/public/pages/tasks.js b/public/pages/tasks.js new file mode 100644 index 0000000..5a62473 --- /dev/null +++ b/public/pages/tasks.js @@ -0,0 +1,25 @@ +/** + * Modul: Tasks + * Zweck: Seite für das Tasks-Modul + * Abhängigkeiten: /api.js + */ + +import { api } from '/api.js'; + +/** + * @param {HTMLElement} container + * @param {{ user: object }} context + */ +export async function render(container, { user }) { + container.innerHTML = ` +
+ +
+
Kommt bald.
+
Dieses Modul wird in Phase 2 implementiert.
+
+
+ `; +} diff --git a/public/router.js b/public/router.js new file mode 100644 index 0000000..7597707 --- /dev/null +++ b/public/router.js @@ -0,0 +1,237 @@ +/** + * Modul: Client-Side Router + * Zweck: SPA-Routing über History API ohne Framework, Auth-Guard, Seiten-Übergänge + * Abhängigkeiten: api.js + */ + +import { auth } from '/api.js'; + +// -------------------------------------------------------- +// Routen-Definitionen +// Jede Route hat: path, page (dynamisch geladen), requiresAuth +// -------------------------------------------------------- +const ROUTES = [ + { path: '/login', page: '/pages/login.js', requiresAuth: false }, + { path: '/', page: '/pages/dashboard.js', requiresAuth: true }, + { path: '/tasks', page: '/pages/tasks.js', requiresAuth: true }, + { path: '/shopping', page: '/pages/shopping.js', requiresAuth: true }, + { path: '/meals', page: '/pages/meals.js', requiresAuth: true }, + { path: '/calendar', page: '/pages/calendar.js', requiresAuth: true }, + { path: '/notes', page: '/pages/notes.js', requiresAuth: true }, + { path: '/contacts', page: '/pages/contacts.js', requiresAuth: true }, + { path: '/budget', page: '/pages/budget.js', requiresAuth: true }, + { path: '/settings', page: '/pages/settings.js', requiresAuth: true }, +]; + +// -------------------------------------------------------- +// Globaler App-State +// -------------------------------------------------------- +let currentUser = null; +let currentPath = null; + +// -------------------------------------------------------- +// Router +// -------------------------------------------------------- + +/** + * Navigiert zu einem Pfad und rendert die entsprechende Seite. + * @param {string} path + * @param {boolean} pushState - false beim initialen Load und popstate + */ +async function navigate(path, pushState = true) { + if (path === currentPath) return; + currentPath = path; + + const route = ROUTES.find((r) => r.path === path) ?? ROUTES.find((r) => r.path === '/'); + + // Auth-Guard + if (route.requiresAuth && !currentUser) { + try { + const result = await auth.me(); + currentUser = result.user; + } catch { + navigateTo('/login', true); + return; + } + } + + if (!route.requiresAuth && currentUser && path === '/login') { + navigateTo('/', true); + return; + } + + if (pushState) { + history.pushState({ path }, '', path); + } + + await renderPage(route); + updateNav(path); +} + +/** + * Lädt und rendert eine Seite dynamisch. + * @param {{ path: string, page: string }} route + */ +async function renderPage(route) { + const app = document.getElementById('app'); + const loading = document.getElementById('app-loading'); + + // Loading verstecken + if (loading) loading.hidden = true; + + try { + const module = await import(route.page + '?v=1'); + + if (typeof module.render !== 'function') { + throw new Error(`Seite ${route.page} exportiert keine render()-Funktion.`); + } + + // Seiten-Wrapper erstellen + const pageWrapper = document.createElement('div'); + pageWrapper.className = 'page-transition'; + pageWrapper.style.animation = 'page-in 0.2s ease forwards'; + + await module.render(pageWrapper, { user: currentUser }); + + // Nav + Content einmalig aufbauen (beim ersten Render) + if (!document.querySelector('.nav-bottom') && currentUser) { + renderAppShell(app); + } + + const content = document.getElementById('page-content') || app; + content.replaceChildren(pageWrapper); + + } catch (err) { + console.error('[Router] Seiten-Render-Fehler:', err); + renderError(app, err); + } +} + +/** + * App-Shell mit Navigation einmalig aufbauen (nach erstem Login). + */ +function renderAppShell(container) { + container.innerHTML = ` + + +
+
+ + + +
+ `; + + // Klick-Handler für alle Nav-Links + container.querySelectorAll('[data-route]').forEach((el) => { + el.addEventListener('click', (e) => { + e.preventDefault(); + navigate(el.dataset.route); + }); + }); +} + +function navItems() { + return [ + { path: '/', label: 'Übersicht', icon: 'layout-dashboard' }, + { path: '/tasks', label: 'Aufgaben', icon: 'check-square' }, + { path: '/calendar', label: 'Kalender', icon: 'calendar' }, + { path: '/meals', label: 'Essen', icon: 'utensils' }, + { path: '/shopping', label: 'Einkauf', icon: 'shopping-cart' }, + { path: '/notes', label: 'Pinnwand', icon: 'sticky-note' }, + { path: '/contacts', label: 'Kontakte', icon: 'book-user' }, + { path: '/budget', label: 'Budget', icon: 'wallet' }, + { path: '/settings', label: 'Einstellungen', icon: 'settings' }, + ]; +} + +function navItemHtml({ path, label, icon }) { + return ` + + + ${label} + + `; +} + +/** + * Aktiven Nav-Link hervorheben. + */ +function updateNav(path) { + document.querySelectorAll('[data-route]').forEach((el) => { + el.removeAttribute('aria-current'); + if (el.dataset.route === path) { + el.setAttribute('aria-current', 'page'); + } + }); + + // Lucide Icons neu rendern (nach DOM-Update) + if (window.lucide) { + window.lucide.createIcons(); + } +} + +function renderError(container, err) { + container.innerHTML = ` +
+
Etwas ist schiefgelaufen.
+
${err.message}
+ +
+ `; +} + +// -------------------------------------------------------- +// Toast-Benachrichtigungen (global) +// -------------------------------------------------------- + +/** + * Zeigt eine Toast-Benachrichtigung an. + * @param {string} message + * @param {'default'|'success'|'danger'|'warning'} type + * @param {number} duration - ms + */ +function showToast(message, type = 'default', duration = 3000) { + const container = document.getElementById('toast-container'); + if (!container) return; + + const toast = document.createElement('div'); + toast.className = `toast ${type !== 'default' ? `toast--${type}` : ''}`; + toast.textContent = message; + toast.setAttribute('role', 'alert'); + + container.appendChild(toast); + setTimeout(() => toast.remove(), duration); +} + +// -------------------------------------------------------- +// Event-Listener +// -------------------------------------------------------- + +// Browser zurück/vor +window.addEventListener('popstate', (e) => { + navigate(e.state?.path || location.pathname, false); +}); + +// Session abgelaufen +window.addEventListener('auth:expired', () => { + currentUser = null; + navigate('/login'); +}); + +// -------------------------------------------------------- +// Initialisierung +// -------------------------------------------------------- +navigate(location.pathname, false); + +// Globale Exporte +window.oikos = { navigate, showToast }; diff --git a/public/styles/layout.css b/public/styles/layout.css new file mode 100644 index 0000000..795af74 --- /dev/null +++ b/public/styles/layout.css @@ -0,0 +1,451 @@ +/** + * Modul: Layout + * Zweck: App-Shell-Layout, Navigation (Bottom Mobile / Sidebar Desktop), Responsive Grid + * Abhängigkeiten: tokens.css, reset.css + */ + +/* -------------------------------------------------------- + * App-Shell + * -------------------------------------------------------- */ +.app-shell { + display: flex; + flex-direction: column; + min-height: 100dvh; +} + +/* -------------------------------------------------------- + * Loading-Screen + * -------------------------------------------------------- */ +.app-loading { + display: flex; + align-items: center; + justify-content: center; + min-height: 100dvh; + background-color: var(--color-bg); +} + +.app-loading__logo { + font-size: var(--text-2xl); + font-weight: var(--font-weight-bold); + color: var(--color-accent); + animation: pulse 1.5s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +/* -------------------------------------------------------- + * Layout: Mobile (Standard, < 1024px) + * -------------------------------------------------------- */ +.app-content { + flex: 1; + padding-bottom: calc(var(--nav-height-mobile) + var(--safe-area-inset-bottom)); + overflow-y: auto; +} + +/* -------------------------------------------------------- + * Bottom Navigation (Mobil + Tablet) + * -------------------------------------------------------- */ +.nav-bottom { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: calc(var(--nav-height-mobile) + var(--safe-area-inset-bottom)); + padding-bottom: var(--safe-area-inset-bottom); + background-color: var(--color-surface); + border-top: 1px solid var(--color-border); + display: flex; + align-items: center; + z-index: var(--z-nav); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.nav-bottom__items { + display: flex; + width: 100%; + height: 100%; +} + +.nav-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--space-1); + padding: var(--space-2) var(--space-1); + color: var(--color-text-secondary); + transition: color var(--transition-fast); + -webkit-tap-highlight-color: transparent; + min-height: unset; /* Reset des Touch-Target-Minimums für Nav-Items */ +} + +.nav-item[aria-current="page"] { + color: var(--color-accent); +} + +.nav-item__icon { + width: 24px; + height: 24px; +} + +.nav-item__label { + font-size: var(--text-xs); + font-weight: var(--font-weight-medium); +} + +/* -------------------------------------------------------- + * Sidebar Navigation (Desktop, > 1024px) + * -------------------------------------------------------- */ +@media (min-width: 1024px) { + .app-shell { + flex-direction: row; + } + + .app-content { + flex: 1; + padding-bottom: 0; + margin-left: var(--sidebar-width); + } + + .nav-bottom { + display: none; + } + + .nav-sidebar { + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: var(--sidebar-width); + background-color: var(--color-surface); + border-right: 1px solid var(--color-border); + display: flex; + flex-direction: column; + z-index: var(--z-nav); + padding: var(--space-6) 0; + } + + .nav-sidebar__logo { + font-size: var(--text-xl); + font-weight: var(--font-weight-bold); + color: var(--color-accent); + padding: 0 var(--space-6) var(--space-6); + border-bottom: 1px solid var(--color-border); + margin-bottom: var(--space-4); + } + + .nav-sidebar__items { + display: flex; + flex-direction: column; + gap: var(--space-1); + padding: 0 var(--space-3); + flex: 1; + } + + .nav-sidebar .nav-item { + flex-direction: row; + justify-content: flex-start; + border-radius: var(--radius-sm); + padding: var(--space-3) var(--space-3); + gap: var(--space-3); + min-height: 44px; + } + + .nav-sidebar .nav-item[aria-current="page"] { + background-color: var(--color-accent-light); + color: var(--color-accent); + } + + .nav-sidebar .nav-item:hover:not([aria-current="page"]) { + background-color: var(--color-surface-2); + } + + .nav-sidebar .nav-item__label { + font-size: var(--text-base); + } +} + +/* -------------------------------------------------------- + * Seiten-Container + * -------------------------------------------------------- */ +.page { + padding: var(--space-4); + max-width: var(--content-max-width); + margin: 0 auto; +} + +.page__header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--space-6); + gap: var(--space-4); +} + +.page__title { + font-size: var(--text-2xl); + font-weight: var(--font-weight-bold); +} + +@media (min-width: 1024px) { + .page { + padding: var(--space-8); + } +} + +/* -------------------------------------------------------- + * Cards + * -------------------------------------------------------- */ +.card { + background-color: var(--color-surface); + border-radius: var(--radius-md); + box-shadow: var(--shadow-sm); + overflow: hidden; +} + +.card--padded { + padding: var(--space-4); +} + +/* -------------------------------------------------------- + * Buttons + * -------------------------------------------------------- */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + padding: var(--space-3) var(--space-4); + border-radius: var(--radius-sm); + font-size: var(--text-base); + font-weight: var(--font-weight-medium); + min-height: 44px; + transition: opacity var(--transition-fast), background-color var(--transition-fast); + cursor: pointer; + white-space: nowrap; +} + +.btn--primary { + background-color: var(--color-accent); + color: #ffffff; +} + +.btn--primary:hover { + background-color: var(--color-accent-hover); +} + +.btn--secondary { + background-color: transparent; + color: var(--color-accent); + border: 1.5px solid var(--color-accent); +} + +.btn--danger { + background-color: var(--color-danger); + color: #ffffff; +} + +.btn--ghost { + background-color: transparent; + color: var(--color-text-secondary); +} + +.btn--ghost:hover { + background-color: var(--color-surface-2); +} + +.btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.btn--icon { + padding: var(--space-2); + min-height: 44px; + min-width: 44px; + border-radius: var(--radius-sm); +} + +/* FAB (Floating Action Button) */ +.fab { + position: fixed; + bottom: calc(var(--nav-height-mobile) + var(--safe-area-inset-bottom) + var(--space-4)); + right: var(--space-4); + width: 56px; + height: 56px; + border-radius: var(--radius-full); + background-color: var(--color-accent); + color: #ffffff; + box-shadow: var(--shadow-lg); + display: flex; + align-items: center; + justify-content: center; + z-index: calc(var(--z-nav) - 1); + transition: transform var(--transition-fast), box-shadow var(--transition-fast); +} + +.fab:hover { + transform: scale(1.05); + box-shadow: var(--shadow-lg); +} + +@media (min-width: 1024px) { + .fab { + bottom: var(--space-8); + } +} + +/* -------------------------------------------------------- + * Form-Elemente + * -------------------------------------------------------- */ +.input { + width: 100%; + padding: var(--space-3) var(--space-4); + border-radius: var(--radius-sm); + border: 1.5px solid var(--color-border); + background-color: var(--color-surface); + color: var(--color-text-primary); + font-size: var(--text-base); + transition: border-color var(--transition-fast); + min-height: 44px; +} + +.input:focus { + outline: none; + border-color: var(--color-accent); +} + +.input::placeholder { + color: var(--color-text-disabled); +} + +.label { + display: block; + font-size: var(--text-sm); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + margin-bottom: var(--space-1); +} + +.form-group { + display: flex; + flex-direction: column; + gap: var(--space-1); + margin-bottom: var(--space-4); +} + +/* -------------------------------------------------------- + * Skeleton-Loading + * -------------------------------------------------------- */ +.skeleton { + background: linear-gradient( + 90deg, + var(--color-border) 25%, + var(--color-surface-2) 50%, + var(--color-border) 75% + ); + background-size: 200% 100%; + animation: skeleton-shimmer 1.5s infinite; + border-radius: var(--radius-sm); +} + +@keyframes skeleton-shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +/* -------------------------------------------------------- + * Leer-Zustände (Empty States) + * -------------------------------------------------------- */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--space-4); + padding: var(--space-12) var(--space-6); + text-align: center; +} + +.empty-state__icon { + width: 64px; + height: 64px; + color: var(--color-text-disabled); +} + +.empty-state__title { + font-size: var(--text-lg); + font-weight: var(--font-weight-semibold); + color: var(--color-text-primary); +} + +.empty-state__description { + font-size: var(--text-base); + color: var(--color-text-secondary); + max-width: 280px; +} + +/* -------------------------------------------------------- + * Responsive Grid + * -------------------------------------------------------- */ +.grid { + display: grid; + gap: var(--space-4); + grid-template-columns: 1fr; +} + +@media (min-width: 768px) { + .grid--2 { grid-template-columns: repeat(2, 1fr); } +} + +@media (min-width: 1024px) { + .grid--3 { grid-template-columns: repeat(3, 1fr); } +} + +/* -------------------------------------------------------- + * Toast-Benachrichtigungen + * -------------------------------------------------------- */ +.toast-container { + position: fixed; + bottom: calc(var(--nav-height-mobile) + var(--safe-area-inset-bottom) + var(--space-4)); + left: 50%; + transform: translateX(-50%); + display: flex; + flex-direction: column; + gap: var(--space-2); + z-index: var(--z-toast); + pointer-events: none; + width: min(calc(100% - var(--space-8)), 400px); +} + +@media (min-width: 1024px) { + .toast-container { + bottom: var(--space-6); + left: calc(var(--sidebar-width) + 50%); + } +} + +.toast { + background-color: var(--color-text-primary); + color: var(--color-bg); + padding: var(--space-3) var(--space-4); + border-radius: var(--radius-sm); + font-size: var(--text-sm); + box-shadow: var(--shadow-lg); + pointer-events: auto; + animation: toast-in 0.2s ease forwards; +} + +.toast--success { background-color: var(--color-success); color: #fff; } +.toast--danger { background-color: var(--color-danger); color: #fff; } +.toast--warning { background-color: var(--color-warning); color: #fff; } + +@keyframes toast-in { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} diff --git a/public/styles/login.css b/public/styles/login.css new file mode 100644 index 0000000..195d93f --- /dev/null +++ b/public/styles/login.css @@ -0,0 +1,48 @@ +/** + * Modul: Login-Seite + * Zweck: Styles für die Anmeldeseite + * Abhängigkeiten: tokens.css, reset.css, layout.css + */ + +.login-page { + display: flex; + align-items: center; + justify-content: center; + min-height: 100dvh; + padding: var(--space-4); + background-color: var(--color-bg); +} + +.login-card { + width: 100%; + max-width: 380px; +} + +.login-card__title { + font-size: var(--text-3xl); + font-weight: var(--font-weight-bold); + color: var(--color-accent); + text-align: center; + margin-bottom: var(--space-1); +} + +.login-card__subtitle { + font-size: var(--text-base); + color: var(--color-text-secondary); + text-align: center; + margin-bottom: var(--space-8); +} + +.login-form__submit { + width: 100%; + margin-top: var(--space-2); +} + +.login-error { + padding: var(--space-3) var(--space-4); + background-color: var(--color-danger-light); + color: var(--color-danger); + border-radius: var(--radius-sm); + font-size: var(--text-sm); + margin-bottom: var(--space-4); +} diff --git a/public/styles/reset.css b/public/styles/reset.css new file mode 100644 index 0000000..aa87e1b --- /dev/null +++ b/public/styles/reset.css @@ -0,0 +1,83 @@ +/** + * Modul: CSS Reset + * Zweck: Browser-Defaults normalisieren, Box-Sizing, Touch-Verhalten + * Abhängigkeiten: tokens.css + */ + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-family: var(--font-sans); + font-size: 16px; + line-height: var(--line-height-base); + color: var(--color-text-primary); + background-color: var(--color-bg); + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: transparent; + scroll-behavior: smooth; +} + +body { + min-height: 100dvh; + overflow-x: hidden; +} + +img, svg, video { + display: block; + max-width: 100%; +} + +button { + font: inherit; + cursor: pointer; + border: none; + background: none; +} + +input, textarea, select { + font: inherit; + color: inherit; +} + +a { + color: inherit; + text-decoration: none; +} + +ul, ol { + list-style: none; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: var(--font-weight-semibold); + line-height: var(--line-height-tight); +} + +/* Touch-Targets: Mindestgröße 44px */ +button, [role="button"], input[type="checkbox"], input[type="radio"] { + min-height: 44px; +} + +/* Fokus-Styles (Accessibility) */ +:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; + border-radius: var(--radius-xs); +} + +/* Versteckte, aber zugängliche Elemente */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} diff --git a/public/styles/tokens.css b/public/styles/tokens.css new file mode 100644 index 0000000..828fb20 --- /dev/null +++ b/public/styles/tokens.css @@ -0,0 +1,149 @@ +/** + * Modul: Design Tokens + * Zweck: CSS Custom Properties für das gesamte Design-System + * Abhängigkeiten: keine + */ + +:root { + /* -------------------------------------------------------- + * Farben — Neutrals + * -------------------------------------------------------- */ + --color-bg: #F5F5F7; + --color-surface: #FFFFFF; + --color-surface-2: #F0F0F5; + --color-border: #E5E5EA; + --color-text-primary: #1C1C1E; + --color-text-secondary: #8E8E93; + --color-text-disabled: #C7C7CC; + + /* -------------------------------------------------------- + * Farben — Akzent (konfigurierbar) + * -------------------------------------------------------- */ + --color-accent: #007AFF; + --color-accent-hover: #0056CC; + --color-accent-light: #E3F2FF; + + /* -------------------------------------------------------- + * Farben — Semantisch + * -------------------------------------------------------- */ + --color-success: #34C759; + --color-success-light: #E3F9EB; + --color-warning: #FF9500; + --color-warning-light: #FFF3E0; + --color-danger: #FF3B30; + --color-danger-light: #FFE5E3; + --color-info: #5AC8FA; + --color-info-light: #E5F7FF; + + /* -------------------------------------------------------- + * Farben — Prioritäten + * -------------------------------------------------------- */ + --color-priority-low: #8E8E93; + --color-priority-medium: #FF9500; + --color-priority-high: #FF6B35; + --color-priority-urgent: #FF3B30; + + /* -------------------------------------------------------- + * Schatten + * -------------------------------------------------------- */ + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.10); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12); + + /* -------------------------------------------------------- + * Border-Radien + * -------------------------------------------------------- */ + --radius-xs: 4px; + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --radius-xl: 24px; + --radius-full: 9999px; + + /* -------------------------------------------------------- + * Typografie + * -------------------------------------------------------- */ + --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; + --font-mono: 'SF Mono', 'Fira Code', 'Fira Mono', 'Roboto Mono', monospace; + + --text-xs: 0.75rem; /* 12px */ + --text-sm: 0.8125rem; /* 13px */ + --text-base: 1rem; /* 16px */ + --text-md: 1.0625rem; /* 17px */ + --text-lg: 1.125rem; /* 18px */ + --text-xl: 1.25rem; /* 20px */ + --text-2xl: 1.5rem; /* 24px */ + --text-3xl: 1.875rem; /* 30px */ + + --line-height-tight: 1.2; + --line-height-base: 1.5; + --line-height-relaxed: 1.75; + + --font-weight-regular: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + + /* -------------------------------------------------------- + * Abstände + * -------------------------------------------------------- */ + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + --space-8: 32px; + --space-10: 40px; + --space-12: 48px; + --space-16: 64px; + + /* -------------------------------------------------------- + * Layout + * -------------------------------------------------------- */ + --nav-height-mobile: 64px; + --sidebar-width: 240px; + --content-max-width: 1200px; + --safe-area-inset-bottom: env(safe-area-inset-bottom, 0px); + + /* -------------------------------------------------------- + * Übergänge + * -------------------------------------------------------- */ + --transition-fast: 0.1s ease; + --transition-base: 0.2s ease; + --transition-slow: 0.3s ease; + + /* -------------------------------------------------------- + * Z-Indizes + * -------------------------------------------------------- */ + --z-base: 0; + --z-card: 1; + --z-nav: 100; + --z-modal: 200; + --z-toast: 300; +} + +/* -------------------------------------------------------- + * Dark Mode + * -------------------------------------------------------- */ +@media (prefers-color-scheme: dark) { + :root { + --color-bg: #1C1C1E; + --color-surface: #2C2C2E; + --color-surface-2: #3A3A3C; + --color-border: #3A3A3C; + --color-text-primary: #F5F5F7; + --color-text-secondary: #8E8E93; + --color-text-disabled: #48484A; + + --color-accent-light: #1A3A5C; + --color-success-light: #1A3A26; + --color-warning-light: #3A2800; + --color-danger-light: #3A1A1A; + --color-info-light: #1A3A4A; + + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5); + } +} diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..a6da92c --- /dev/null +++ b/public/sw.js @@ -0,0 +1,77 @@ +/** + * Modul: Service Worker + * Zweck: Offline-Fähigkeit (App-Shell-Caching), Hintergrund-Sync + * Abhängigkeiten: keine + */ + +const CACHE_NAME = 'oikos-v1'; + +// App-Shell-Ressourcen, die offline verfügbar sein sollen +const APP_SHELL = [ + '/', + '/index.html', + '/api.js', + '/router.js', + '/styles/tokens.css', + '/styles/reset.css', + '/styles/layout.css', + '/styles/login.css', + '/manifest.json', +]; + +// -------------------------------------------------------- +// Install: App-Shell cachen +// -------------------------------------------------------- +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL)) + ); + self.skipWaiting(); +}); + +// -------------------------------------------------------- +// Activate: Alte Caches löschen +// -------------------------------------------------------- +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all( + keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)) + ) + ) + ); + self.clients.claim(); +}); + +// -------------------------------------------------------- +// Fetch: Netzwerk-First für API, Cache-First für App-Shell +// -------------------------------------------------------- +self.addEventListener('fetch', (event) => { + const { request } = event; + const url = new URL(request.url); + + // API-Requests: immer Netzwerk (kein Caching von Nutzerdaten) + if (url.pathname.startsWith('/api/')) { + return; // Browser übernimmt + } + + // App-Shell: Cache-First, Fallback Netzwerk + event.respondWith( + caches.match(request).then((cached) => { + if (cached) return cached; + + return fetch(request).then((response) => { + if (response.ok && response.type === 'basic') { + const copy = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(request, copy)); + } + return response; + }).catch(() => { + // Offline-Fallback für Seiten-Navigation + if (request.mode === 'navigate') { + return caches.match('/index.html'); + } + }); + }) + ); +}); diff --git a/server/auth.js b/server/auth.js new file mode 100644 index 0000000..fa70ab4 --- /dev/null +++ b/server/auth.js @@ -0,0 +1,258 @@ +/** + * Modul: Authentifizierung (Auth) + * Zweck: Login-Route, Session-Middleware, Auth-Guard für geschützte Routen + * Abhängigkeiten: express, bcrypt, express-session, connect-sqlite3, server/db.js + */ + +'use strict'; + +require('dotenv').config(); +const express = require('express'); +const bcrypt = require('bcrypt'); +const session = require('express-session'); +const rateLimit = require('express-rate-limit'); +const db = require('./db'); + +const router = express.Router(); + +// -------------------------------------------------------- +// Session-Store (SQLite) +// -------------------------------------------------------- +const SQLiteStore = require('connect-sqlite3')(session); + +const sessionStore = new SQLiteStore({ + db: 'sessions.db', + dir: process.env.DB_PATH ? require('path').dirname(process.env.DB_PATH) : '.', + ttl: 60 * 60 * 24 * 7, // 7 Tage in Sekunden +}); + +/** + * Session-Middleware konfigurieren. + * Wird in server/index.js eingebunden. + */ +const sessionMiddleware = session({ + store: sessionStore, + secret: process.env.SESSION_SECRET || 'dev-secret-AENDERN-IN-PRODUKTION', + resave: false, + saveUninitialized: false, + name: 'oikos.sid', + cookie: { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 1000 * 60 * 60 * 24 * 7, // 7 Tage in ms + }, +}); + +// -------------------------------------------------------- +// Rate Limiting für Login +// -------------------------------------------------------- +const loginLimiter = rateLimit({ + windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 60_000, + max: parseInt(process.env.RATE_LIMIT_MAX_ATTEMPTS) || 5, + skipSuccessfulRequests: true, + standardHeaders: true, + legacyHeaders: false, + message: { error: 'Zu viele Login-Versuche. Bitte warte kurz.', code: 429 }, +}); + +// -------------------------------------------------------- +// Auth-Guard Middleware +// -------------------------------------------------------- + +/** + * Prüft ob der Request authentifiziert ist. + * Schützt alle API-Routen außer /auth/login. + */ +function requireAuth(req, res, next) { + if (req.session && req.session.userId) { + return next(); + } + res.status(401).json({ error: 'Nicht authentifiziert.', code: 401 }); +} + +/** + * Prüft ob der authentifizierte User Admin-Rolle hat. + */ +function requireAdmin(req, res, next) { + if (req.session && req.session.role === 'admin') { + return next(); + } + res.status(403).json({ error: 'Keine Berechtigung.', code: 403 }); +} + +// -------------------------------------------------------- +// Routen +// -------------------------------------------------------- + +/** + * POST /api/v1/auth/login + * Body: { username: string, password: string } + * Response: { user: { id, username, display_name, avatar_color, role } } + */ +router.post('/login', loginLimiter, async (req, res) => { + try { + const { username, password } = req.body; + + if (!username || !password) { + return res.status(400).json({ error: 'Benutzername und Passwort erforderlich.', code: 400 }); + } + + const user = db.get().prepare('SELECT * FROM users WHERE username = ?').get(username); + + if (!user) { + // Timing-Attack-Schutz: trotzdem bcrypt ausführen + await bcrypt.compare(password, '$2b$12$invalidhashfortimingprotection000000000000000000000'); + return res.status(401).json({ error: 'Ungültige Anmeldedaten.', code: 401 }); + } + + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return res.status(401).json({ error: 'Ungültige Anmeldedaten.', code: 401 }); + } + + req.session.regenerate((err) => { + if (err) { + console.error('[Auth] Session-Regenerierung fehlgeschlagen:', err); + return res.status(500).json({ error: 'Interner Serverfehler.', code: 500 }); + } + + req.session.userId = user.id; + req.session.role = user.role; + + res.json({ + user: { + id: user.id, + username: user.username, + display_name: user.display_name, + avatar_color: user.avatar_color, + role: user.role, + }, + }); + }); + } catch (err) { + console.error('[Auth] Login-Fehler:', err); + res.status(500).json({ error: 'Interner Serverfehler.', code: 500 }); + } +}); + +/** + * POST /api/v1/auth/logout + * Response: { ok: true } + */ +router.post('/logout', requireAuth, (req, res) => { + req.session.destroy((err) => { + if (err) { + console.error('[Auth] Logout-Fehler:', err); + return res.status(500).json({ error: 'Logout fehlgeschlagen.', code: 500 }); + } + res.clearCookie('oikos.sid'); + res.json({ ok: true }); + }); +}); + +/** + * GET /api/v1/auth/me + * Response: { user: { id, username, display_name, avatar_color, role } } + */ +router.get('/me', requireAuth, (req, res) => { + try { + const user = db.get() + .prepare('SELECT id, username, display_name, avatar_color, role FROM users WHERE id = ?') + .get(req.session.userId); + + if (!user) { + req.session.destroy(() => {}); + return res.status(401).json({ error: 'Benutzer nicht gefunden.', code: 401 }); + } + + res.json({ user }); + } catch (err) { + console.error('[Auth] /me Fehler:', err); + res.status(500).json({ error: 'Interner Serverfehler.', code: 500 }); + } +}); + +/** + * GET /api/v1/auth/users + * Admin only. Listet alle Familienmitglieder. + * Response: { data: User[] } + */ +router.get('/users', requireAuth, requireAdmin, (req, res) => { + try { + const users = db.get() + .prepare('SELECT id, username, display_name, avatar_color, role, created_at FROM users ORDER BY display_name') + .all(); + res.json({ data: users }); + } catch (err) { + console.error('[Auth] Users-Fehler:', err); + res.status(500).json({ error: 'Interner Serverfehler.', code: 500 }); + } +}); + +/** + * POST /api/v1/auth/users + * Admin only. Erstellt neues Familienmitglied. + * Body: { username, display_name, password, avatar_color?, role? } + * Response: { user: { id, username, display_name, avatar_color, role } } + */ +router.post('/users', requireAuth, requireAdmin, async (req, res) => { + try { + const { username, display_name, password, avatar_color = '#007AFF', role = 'member' } = req.body; + + if (!username || !display_name || !password) { + return res.status(400).json({ error: 'Benutzername, Anzeigename und Passwort erforderlich.', code: 400 }); + } + + if (!['admin', 'member'].includes(role)) { + return res.status(400).json({ error: 'Ungültige Rolle.', code: 400 }); + } + + const hash = await bcrypt.hash(password, 12); + + const result = db.get() + .prepare(` + INSERT INTO users (username, display_name, password_hash, avatar_color, role) + VALUES (?, ?, ?, ?, ?) + `) + .run(username, display_name, hash, avatar_color, role); + + res.status(201).json({ + user: { id: result.lastInsertRowid, username, display_name, avatar_color, role }, + }); + } catch (err) { + if (err.message && err.message.includes('UNIQUE constraint')) { + return res.status(409).json({ error: 'Benutzername bereits vergeben.', code: 409 }); + } + console.error('[Auth] User-Erstellen-Fehler:', err); + res.status(500).json({ error: 'Interner Serverfehler.', code: 500 }); + } +}); + +/** + * DELETE /api/v1/auth/users/:id + * Admin only. Löscht ein Familienmitglied. + * Response: { ok: true } + */ +router.delete('/users/:id', requireAuth, requireAdmin, (req, res) => { + try { + const userId = parseInt(req.params.id, 10); + + if (userId === req.session.userId) { + return res.status(400).json({ error: 'Eigenes Konto kann nicht gelöscht werden.', code: 400 }); + } + + const result = db.get().prepare('DELETE FROM users WHERE id = ?').run(userId); + + if (result.changes === 0) { + return res.status(404).json({ error: 'Benutzer nicht gefunden.', code: 404 }); + } + + res.json({ ok: true }); + } catch (err) { + console.error('[Auth] User-Löschen-Fehler:', err); + res.status(500).json({ error: 'Interner Serverfehler.', code: 500 }); + } +}); + +module.exports = { router, sessionMiddleware, requireAuth, requireAdmin }; diff --git a/server/db-schema-test.js b/server/db-schema-test.js new file mode 100644 index 0000000..6ed4a3d --- /dev/null +++ b/server/db-schema-test.js @@ -0,0 +1,177 @@ +/** + * Modul: DB-Schema-Export für Tests + * Zweck: SQL-Strings aus MIGRATIONS für node:sqlite-Tests exportieren. + * Nur für Testzwecke — db.js nutzt die MIGRATIONS direkt intern. + * Abhängigkeiten: keine + */ + +'use strict'; + +// SQL-String für Migration v1 (gespiegelt aus db.js MIGRATIONS[0].up) +// Änderungen in db.js MIGRATIONS müssen hier synchron gehalten werden. +const MIGRATIONS_SQL = { + 1: ` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + display_name TEXT NOT NULL, + password_hash TEXT NOT NULL, + avatar_color TEXT NOT NULL DEFAULT '#007AFF', + role TEXT NOT NULL DEFAULT 'member' + CHECK(role IN ('admin', 'member')), + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + CREATE TABLE IF NOT EXISTS tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + category TEXT NOT NULL DEFAULT 'Sonstiges', + priority TEXT NOT NULL DEFAULT 'medium' + CHECK(priority IN ('low', 'medium', 'high', 'urgent')), + status TEXT NOT NULL DEFAULT 'open' + CHECK(status IN ('open', 'in_progress', 'done')), + due_date TEXT, + due_time TEXT, + assigned_to INTEGER REFERENCES users(id) ON DELETE SET NULL, + created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + is_recurring INTEGER NOT NULL DEFAULT 0, + recurrence_rule TEXT, + parent_task_id INTEGER REFERENCES tasks(id) ON DELETE CASCADE, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + CREATE TABLE IF NOT EXISTS shopping_lists ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + CREATE TABLE IF NOT EXISTS meals ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL, + meal_type TEXT NOT NULL + CHECK(meal_type IN ('breakfast', 'lunch', 'dinner', 'snack')), + title TEXT NOT NULL, + notes TEXT, + created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + CREATE TABLE IF NOT EXISTS shopping_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + list_id INTEGER NOT NULL REFERENCES shopping_lists(id) ON DELETE CASCADE, + name TEXT NOT NULL, + quantity TEXT, + category TEXT NOT NULL DEFAULT 'Sonstiges', + is_checked INTEGER NOT NULL DEFAULT 0, + added_from_meal INTEGER REFERENCES meals(id) ON DELETE SET NULL, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + CREATE TABLE IF NOT EXISTS meal_ingredients ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + meal_id INTEGER NOT NULL REFERENCES meals(id) ON DELETE CASCADE, + name TEXT NOT NULL, + quantity TEXT, + on_shopping_list INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + CREATE TABLE IF NOT EXISTS calendar_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + start_datetime TEXT NOT NULL, + end_datetime TEXT, + all_day INTEGER NOT NULL DEFAULT 0, + location TEXT, + color TEXT NOT NULL DEFAULT '#007AFF', + assigned_to INTEGER REFERENCES users(id) ON DELETE SET NULL, + created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + external_calendar_id TEXT, + external_source TEXT NOT NULL DEFAULT 'local' + CHECK(external_source IN ('local', 'google', 'apple')), + recurrence_rule TEXT, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + CREATE TABLE IF NOT EXISTS notes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT, + content TEXT NOT NULL, + color TEXT NOT NULL DEFAULT '#FFEB3B', + pinned INTEGER NOT NULL DEFAULT 0, + created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + CREATE TABLE IF NOT EXISTS contacts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + category TEXT NOT NULL DEFAULT 'Sonstiges', + phone TEXT, + email TEXT, + address TEXT, + notes TEXT, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + CREATE TABLE IF NOT EXISTS budget_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + amount REAL NOT NULL, + category TEXT NOT NULL DEFAULT 'Sonstiges', + date TEXT NOT NULL, + is_recurring INTEGER NOT NULL DEFAULT 0, + recurrence_rule TEXT, + created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + CREATE TRIGGER IF NOT EXISTS trg_users_updated_at + AFTER UPDATE ON users FOR EACH ROW + BEGIN UPDATE users SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + CREATE TRIGGER IF NOT EXISTS trg_tasks_updated_at + AFTER UPDATE ON tasks FOR EACH ROW + BEGIN UPDATE tasks SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + CREATE TRIGGER IF NOT EXISTS trg_shopping_lists_updated_at + AFTER UPDATE ON shopping_lists FOR EACH ROW + BEGIN UPDATE shopping_lists SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + CREATE TRIGGER IF NOT EXISTS trg_shopping_items_updated_at + AFTER UPDATE ON shopping_items FOR EACH ROW + BEGIN UPDATE shopping_items SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + CREATE TRIGGER IF NOT EXISTS trg_meals_updated_at + AFTER UPDATE ON meals FOR EACH ROW + BEGIN UPDATE meals SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + CREATE TRIGGER IF NOT EXISTS trg_meal_ingredients_updated_at + AFTER UPDATE ON meal_ingredients FOR EACH ROW + BEGIN UPDATE meal_ingredients SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + CREATE TRIGGER IF NOT EXISTS trg_calendar_events_updated_at + AFTER UPDATE ON calendar_events FOR EACH ROW + BEGIN UPDATE calendar_events SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + CREATE TRIGGER IF NOT EXISTS trg_notes_updated_at + AFTER UPDATE ON notes FOR EACH ROW + BEGIN UPDATE notes SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + CREATE TRIGGER IF NOT EXISTS trg_contacts_updated_at + AFTER UPDATE ON contacts FOR EACH ROW + BEGIN UPDATE contacts SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + CREATE TRIGGER IF NOT EXISTS trg_budget_entries_updated_at + AFTER UPDATE ON budget_entries FOR EACH ROW + BEGIN UPDATE budget_entries SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + CREATE INDEX IF NOT EXISTS idx_tasks_assigned_to ON tasks(assigned_to); + CREATE INDEX IF NOT EXISTS idx_tasks_due_date ON tasks(due_date); + CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); + CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_task_id); + CREATE INDEX IF NOT EXISTS idx_shopping_items_list ON shopping_items(list_id); + CREATE INDEX IF NOT EXISTS idx_meals_date ON meals(date); + CREATE INDEX IF NOT EXISTS idx_calendar_start ON calendar_events(start_datetime); + CREATE INDEX IF NOT EXISTS idx_calendar_assigned ON calendar_events(assigned_to); + CREATE INDEX IF NOT EXISTS idx_notes_pinned ON notes(pinned); + CREATE INDEX IF NOT EXISTS idx_budget_date ON budget_entries(date); + CREATE INDEX IF NOT EXISTS idx_budget_created_by ON budget_entries(created_by); + `, +}; + +module.exports = { MIGRATIONS_SQL }; diff --git a/server/db.js b/server/db.js new file mode 100644 index 0000000..cd13698 --- /dev/null +++ b/server/db.js @@ -0,0 +1,342 @@ +/** + * Modul: Datenbank (Database) + * Zweck: SQLite/SQLCipher Verbindung, Schema-Migration (versioniert) und Query-Helfer + * Abhängigkeiten: better-sqlite3, dotenv + * + * SQLCipher-Hinweis: + * Verschlüsselung funktioniert nur wenn better-sqlite3 gegen SQLCipher kompiliert wurde. + * Im Docker-Container (Dockerfile: libsqlcipher-dev + npm rebuild) ist das gewährleistet. + * Ohne DB_ENCRYPTION_KEY gesetzt läuft die App mit unverschlüsseltem SQLite (für Entwicklung). + */ + +'use strict'; + +require('dotenv').config(); +const Database = require('better-sqlite3'); +const path = require('path'); + +const DB_PATH = process.env.DB_PATH || path.join(__dirname, '..', 'oikos.db'); +const DB_KEY = process.env.DB_ENCRYPTION_KEY; + +let db; + +// -------------------------------------------------------- +// Initialisierung +// -------------------------------------------------------- + +/** + * Datenbankverbindung öffnen, SQLCipher-Key setzen, Migrations ausführen. + * Einmalig beim Serverstart aufrufen. + * @returns {import('better-sqlite3').Database} + */ +function init() { + db = new Database(DB_PATH); + + if (DB_KEY) { + // Nur wirksam wenn Binary gegen SQLCipher kompiliert ist (Docker) + db.pragma(`key='${DB_KEY}'`); + // Sicherstellen dass die Datenbank tatsächlich entschlüsselbar ist + try { + db.prepare('SELECT count(*) FROM sqlite_master').get(); + } catch { + throw new Error('[DB] Falscher Verschlüsselungsschlüssel oder keine SQLCipher-Unterstützung.'); + } + } + + db.pragma('journal_mode = WAL'); + db.pragma('foreign_keys = ON'); + db.pragma('synchronous = NORMAL'); + db.pragma('temp_store = MEMORY'); + + migrate(); + + console.log(`[DB] Verbunden: ${DB_PATH} | Schema v${currentVersion()}`); + return db; +} + +// -------------------------------------------------------- +// Migrations-Engine +// -------------------------------------------------------- + +/** + * Alle Migrationen in aufsteigender Reihenfolge. + * Neue Migrations am Ende anhängen — niemals bestehende ändern. + */ +const MIGRATIONS = [ + { + version: 1, + description: 'Initiales Schema', + up: ` + -- Benutzer + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + display_name TEXT NOT NULL, + password_hash TEXT NOT NULL, + avatar_color TEXT NOT NULL DEFAULT '#007AFF', + role TEXT NOT NULL DEFAULT 'member' + CHECK(role IN ('admin', 'member')), + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + + -- Aufgaben + CREATE TABLE IF NOT EXISTS tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + category TEXT NOT NULL DEFAULT 'Sonstiges', + priority TEXT NOT NULL DEFAULT 'medium' + CHECK(priority IN ('low', 'medium', 'high', 'urgent')), + status TEXT NOT NULL DEFAULT 'open' + CHECK(status IN ('open', 'in_progress', 'done')), + due_date TEXT, + due_time TEXT, + assigned_to INTEGER REFERENCES users(id) ON DELETE SET NULL, + created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + is_recurring INTEGER NOT NULL DEFAULT 0, + recurrence_rule TEXT, + parent_task_id INTEGER REFERENCES tasks(id) ON DELETE CASCADE, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + + -- Einkaufslisten + CREATE TABLE IF NOT EXISTS shopping_lists ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + + -- Essensplan (muss vor shopping_items stehen wegen FK-Referenz) + CREATE TABLE IF NOT EXISTS meals ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL, + meal_type TEXT NOT NULL + CHECK(meal_type IN ('breakfast', 'lunch', 'dinner', 'snack')), + title TEXT NOT NULL, + notes TEXT, + created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + + -- Einkaufsartikel (nach meals, wegen added_from_meal FK) + CREATE TABLE IF NOT EXISTS shopping_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + list_id INTEGER NOT NULL REFERENCES shopping_lists(id) ON DELETE CASCADE, + name TEXT NOT NULL, + quantity TEXT, + category TEXT NOT NULL DEFAULT 'Sonstiges', + is_checked INTEGER NOT NULL DEFAULT 0, + added_from_meal INTEGER REFERENCES meals(id) ON DELETE SET NULL, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + + -- Mahlzeit-Zutaten + CREATE TABLE IF NOT EXISTS meal_ingredients ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + meal_id INTEGER NOT NULL REFERENCES meals(id) ON DELETE CASCADE, + name TEXT NOT NULL, + quantity TEXT, + on_shopping_list INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + + -- Kalender-Events + CREATE TABLE IF NOT EXISTS calendar_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + start_datetime TEXT NOT NULL, + end_datetime TEXT, + all_day INTEGER NOT NULL DEFAULT 0, + location TEXT, + color TEXT NOT NULL DEFAULT '#007AFF', + assigned_to INTEGER REFERENCES users(id) ON DELETE SET NULL, + created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + external_calendar_id TEXT, + external_source TEXT NOT NULL DEFAULT 'local' + CHECK(external_source IN ('local', 'google', 'apple')), + recurrence_rule TEXT, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + + -- Pinnwand / Notizen + CREATE TABLE IF NOT EXISTS notes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT, + content TEXT NOT NULL, + color TEXT NOT NULL DEFAULT '#FFEB3B', + pinned INTEGER NOT NULL DEFAULT 0, + created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + + -- Kontakte + CREATE TABLE IF NOT EXISTS contacts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + category TEXT NOT NULL DEFAULT 'Sonstiges', + phone TEXT, + email TEXT, + address TEXT, + notes TEXT, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + + -- Budget + CREATE TABLE IF NOT EXISTS budget_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + amount REAL NOT NULL, + category TEXT NOT NULL DEFAULT 'Sonstiges', + date TEXT NOT NULL, + is_recurring INTEGER NOT NULL DEFAULT 0, + recurrence_rule TEXT, + created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + + -- -------------------------------------------------------- + -- updated_at Trigger (automatisch bei UPDATE setzen) + -- -------------------------------------------------------- + CREATE TRIGGER IF NOT EXISTS trg_users_updated_at + AFTER UPDATE ON users FOR EACH ROW + BEGIN UPDATE users SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + + CREATE TRIGGER IF NOT EXISTS trg_tasks_updated_at + AFTER UPDATE ON tasks FOR EACH ROW + BEGIN UPDATE tasks SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + + CREATE TRIGGER IF NOT EXISTS trg_shopping_lists_updated_at + AFTER UPDATE ON shopping_lists FOR EACH ROW + BEGIN UPDATE shopping_lists SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + + CREATE TRIGGER IF NOT EXISTS trg_shopping_items_updated_at + AFTER UPDATE ON shopping_items FOR EACH ROW + BEGIN UPDATE shopping_items SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + + CREATE TRIGGER IF NOT EXISTS trg_meals_updated_at + AFTER UPDATE ON meals FOR EACH ROW + BEGIN UPDATE meals SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + + CREATE TRIGGER IF NOT EXISTS trg_meal_ingredients_updated_at + AFTER UPDATE ON meal_ingredients FOR EACH ROW + BEGIN UPDATE meal_ingredients SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + + CREATE TRIGGER IF NOT EXISTS trg_calendar_events_updated_at + AFTER UPDATE ON calendar_events FOR EACH ROW + BEGIN UPDATE calendar_events SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + + CREATE TRIGGER IF NOT EXISTS trg_notes_updated_at + AFTER UPDATE ON notes FOR EACH ROW + BEGIN UPDATE notes SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + + CREATE TRIGGER IF NOT EXISTS trg_contacts_updated_at + AFTER UPDATE ON contacts FOR EACH ROW + BEGIN UPDATE contacts SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + + CREATE TRIGGER IF NOT EXISTS trg_budget_entries_updated_at + AFTER UPDATE ON budget_entries FOR EACH ROW + BEGIN UPDATE budget_entries SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = OLD.id; END; + + -- -------------------------------------------------------- + -- Indizes + -- -------------------------------------------------------- + CREATE INDEX IF NOT EXISTS idx_tasks_assigned_to ON tasks(assigned_to); + CREATE INDEX IF NOT EXISTS idx_tasks_due_date ON tasks(due_date); + CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); + CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_task_id); + CREATE INDEX IF NOT EXISTS idx_shopping_items_list ON shopping_items(list_id); + CREATE INDEX IF NOT EXISTS idx_meals_date ON meals(date); + CREATE INDEX IF NOT EXISTS idx_calendar_start ON calendar_events(start_datetime); + CREATE INDEX IF NOT EXISTS idx_calendar_assigned ON calendar_events(assigned_to); + CREATE INDEX IF NOT EXISTS idx_notes_pinned ON notes(pinned); + CREATE INDEX IF NOT EXISTS idx_budget_date ON budget_entries(date); + CREATE INDEX IF NOT EXISTS idx_budget_created_by ON budget_entries(created_by); + `, + }, + // Zukünftige Migrations hier anhängen: + // { version: 2, description: '...', up: '...' }, +]; + +/** + * Führt alle ausstehenden Migrations in einer Transaktion aus. + */ +function migrate() { + // Migrations-Versions-Tabelle sicherstellen (außerhalb der Haupt-Transaktion) + db.exec(` + CREATE TABLE IF NOT EXISTS schema_migrations ( + version INTEGER PRIMARY KEY, + description TEXT NOT NULL, + applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + `); + + const applied = new Set( + db.prepare('SELECT version FROM schema_migrations').all().map((r) => r.version) + ); + + const pending = MIGRATIONS.filter((m) => !applied.has(m.version)); + + if (pending.length === 0) return; + + const runMigration = db.transaction((migration) => { + db.exec(migration.up); + db.prepare('INSERT INTO schema_migrations (version, description) VALUES (?, ?)') + .run(migration.version, migration.description); + console.log(`[DB] Migration ${migration.version} angewendet: ${migration.description}`); + }); + + for (const migration of pending) { + runMigration(migration); + } +} + +/** + * Aktuelle Schema-Version zurückgeben. + * @returns {number} + */ +function currentVersion() { + if (!db) return 0; + try { + const row = db.prepare('SELECT MAX(version) as v FROM schema_migrations').get(); + return row?.v ?? 0; + } catch { + return 0; + } +} + +// -------------------------------------------------------- +// Öffentliche API +// -------------------------------------------------------- + +/** + * Datenbankinstanz zurückgeben. + * @returns {import('better-sqlite3').Database} + */ +function get() { + if (!db) throw new Error('[DB] Nicht initialisiert — init() zuerst aufrufen.'); + return db; +} + +/** + * Transaktion-Helfer: Funktion wird atomar ausgeführt. + * Bei Fehler wird automatisch rollback ausgeführt. + * @param {Function} fn + * @returns {any} + */ +function transaction(fn) { + return get().transaction(fn)(); +} + +module.exports = { init, get, transaction, currentVersion }; diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..093d4fa --- /dev/null +++ b/server/index.js @@ -0,0 +1,124 @@ +/** + * Modul: Server Entry Point + * Zweck: Express-App initialisieren, Middleware einbinden, Routen registrieren + * Abhängigkeiten: express, helmet, dotenv, server/db.js, server/auth.js, server/routes/* + */ + +'use strict'; + +require('dotenv').config(); +const express = require('express'); +const helmet = require('helmet'); +const path = require('path'); +const db = require('./db'); +const { router: authRouter, sessionMiddleware, requireAuth } = require('./auth'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// -------------------------------------------------------- +// Datenbank initialisieren +// -------------------------------------------------------- +db.init(); + +// -------------------------------------------------------- +// Security-Middleware +// -------------------------------------------------------- +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + scriptSrc: [ + "'self'", + // Alpine.js CDN + 'https://cdn.jsdelivr.net', + // Lucide Icons CDN + 'https://unpkg.com', + ], + styleSrc: ["'self'", "'unsafe-inline'"], + imgSrc: ["'self'", 'data:', 'https://openweathermap.org'], + connectSrc: ["'self'"], + fontSrc: ["'self'"], + objectSrc: ["'none'"], + frameSrc: ["'none'"], + }, + }, + hsts: { + maxAge: 31536000, + includeSubDomains: true, + preload: true, + }, +})); + +// Trust Proxy für korrekte IP hinter Nginx +app.set('trust proxy', 1); + +// -------------------------------------------------------- +// Request-Parsing +// -------------------------------------------------------- +app.use(express.json({ limit: '1mb' })); +app.use(express.urlencoded({ extended: true, limit: '1mb' })); + +// -------------------------------------------------------- +// Sessions +// -------------------------------------------------------- +app.use(sessionMiddleware); + +// -------------------------------------------------------- +// Statische Dateien (Frontend) +// -------------------------------------------------------- +app.use(express.static(path.join(__dirname, '..', 'public'), { + maxAge: process.env.NODE_ENV === 'production' ? '7d' : 0, + etag: true, +})); + +// -------------------------------------------------------- +// API-Routen +// -------------------------------------------------------- +app.use('/api/v1/auth', authRouter); + +// Alle weiteren API-Routen erfordern Authentifizierung +app.use('/api/v1', requireAuth); +app.use('/api/v1/tasks', require('./routes/tasks')); +app.use('/api/v1/shopping', require('./routes/shopping')); +app.use('/api/v1/meals', require('./routes/meals')); +app.use('/api/v1/calendar', require('./routes/calendar')); +app.use('/api/v1/notes', require('./routes/notes')); +app.use('/api/v1/contacts', require('./routes/contacts')); +app.use('/api/v1/budget', require('./routes/budget')); +app.use('/api/v1/weather', require('./routes/weather')); + +// -------------------------------------------------------- +// Health-Check (für Docker) +// -------------------------------------------------------- +app.get('/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +// -------------------------------------------------------- +// SPA Fallback: Alle nicht-API-Routen → index.html +// -------------------------------------------------------- +app.get('*', (req, res) => { + if (req.path.startsWith('/api/')) { + return res.status(404).json({ error: 'Nicht gefunden.', code: 404 }); + } + res.sendFile(path.join(__dirname, '..', 'public', 'index.html')); +}); + +// -------------------------------------------------------- +// Globaler Error-Handler +// -------------------------------------------------------- +app.use((err, req, res, _next) => { + console.error('[Server] Unbehandelter Fehler:', err); + res.status(500).json({ error: 'Interner Serverfehler.', code: 500 }); +}); + +// -------------------------------------------------------- +// Server starten +// -------------------------------------------------------- +app.listen(PORT, () => { + console.log(`[Oikos] Server läuft auf Port ${PORT}`); + console.log(`[Oikos] Umgebung: ${process.env.NODE_ENV || 'development'}`); +}); + +module.exports = app; diff --git a/server/routes/budget.js b/server/routes/budget.js new file mode 100644 index 0000000..5779808 --- /dev/null +++ b/server/routes/budget.js @@ -0,0 +1,13 @@ +/** + * Modul: Budget-Tracker (Budget) + * Zweck: REST-API-Routen für Einnahmen und Ausgaben + * Abhängigkeiten: express, server/db.js, server/auth.js + */ + +const express = require('express'); +const router = express.Router(); + +// Platzhalter — wird in Phase 3 implementiert +router.get('/', (req, res) => res.json({ data: [] })); + +module.exports = router; diff --git a/server/routes/calendar.js b/server/routes/calendar.js new file mode 100644 index 0000000..a53c57d --- /dev/null +++ b/server/routes/calendar.js @@ -0,0 +1,13 @@ +/** + * Modul: Kalender (Calendar) + * Zweck: REST-API-Routen für Kalendereinträge und externe Kalender-Sync + * Abhängigkeiten: express, server/db.js, server/auth.js + */ + +const express = require('express'); +const router = express.Router(); + +// Platzhalter — wird in Phase 3 implementiert +router.get('/', (req, res) => res.json({ data: [] })); + +module.exports = router; diff --git a/server/routes/contacts.js b/server/routes/contacts.js new file mode 100644 index 0000000..2e1bdcc --- /dev/null +++ b/server/routes/contacts.js @@ -0,0 +1,13 @@ +/** + * Modul: Kontakte (Contacts) + * Zweck: REST-API-Routen für wichtige Familienkontakte + * Abhängigkeiten: express, server/db.js, server/auth.js + */ + +const express = require('express'); +const router = express.Router(); + +// Platzhalter — wird in Phase 3 implementiert +router.get('/', (req, res) => res.json({ data: [] })); + +module.exports = router; diff --git a/server/routes/meals.js b/server/routes/meals.js new file mode 100644 index 0000000..56ecd8c --- /dev/null +++ b/server/routes/meals.js @@ -0,0 +1,13 @@ +/** + * Modul: Essensplan (Meals) + * Zweck: REST-API-Routen für Mahlzeiten und Zutaten + * Abhängigkeiten: express, server/db.js, server/auth.js + */ + +const express = require('express'); +const router = express.Router(); + +// Platzhalter — wird in Phase 2 implementiert +router.get('/', (req, res) => res.json({ data: [] })); + +module.exports = router; diff --git a/server/routes/notes.js b/server/routes/notes.js new file mode 100644 index 0000000..238acc5 --- /dev/null +++ b/server/routes/notes.js @@ -0,0 +1,13 @@ +/** + * Modul: Pinnwand / Notizen (Notes) + * Zweck: REST-API-Routen für Notizen + * Abhängigkeiten: express, server/db.js, server/auth.js + */ + +const express = require('express'); +const router = express.Router(); + +// Platzhalter — wird in Phase 3 implementiert +router.get('/', (req, res) => res.json({ data: [] })); + +module.exports = router; diff --git a/server/routes/shopping.js b/server/routes/shopping.js new file mode 100644 index 0000000..e5b487a --- /dev/null +++ b/server/routes/shopping.js @@ -0,0 +1,13 @@ +/** + * Modul: Einkaufslisten (Shopping) + * Zweck: REST-API-Routen für Einkaufslisten und -artikel + * Abhängigkeiten: express, server/db.js, server/auth.js + */ + +const express = require('express'); +const router = express.Router(); + +// Platzhalter — wird in Phase 2 implementiert +router.get('/', (req, res) => res.json({ data: [] })); + +module.exports = router; diff --git a/server/routes/tasks.js b/server/routes/tasks.js new file mode 100644 index 0000000..afae043 --- /dev/null +++ b/server/routes/tasks.js @@ -0,0 +1,13 @@ +/** + * Modul: Aufgaben (Tasks) + * Zweck: REST-API-Routen für Aufgaben und Teilaufgaben + * Abhängigkeiten: express, server/db.js, server/auth.js + */ + +const express = require('express'); +const router = express.Router(); + +// Platzhalter — wird in Phase 2 implementiert +router.get('/', (req, res) => res.json({ data: [] })); + +module.exports = router; diff --git a/server/routes/weather.js b/server/routes/weather.js new file mode 100644 index 0000000..b1eaf65 --- /dev/null +++ b/server/routes/weather.js @@ -0,0 +1,13 @@ +/** + * Modul: Wetter-Proxy (Weather) + * Zweck: Serverseitiger Proxy für OpenWeatherMap API (API-Key nie im Frontend) + * Abhängigkeiten: express, node-fetch, dotenv + */ + +const express = require('express'); +const router = express.Router(); + +// Platzhalter — wird in Phase 4 implementiert +router.get('/', (req, res) => res.json({ data: null })); + +module.exports = router; diff --git a/server/services/apple-calendar.js b/server/services/apple-calendar.js new file mode 100644 index 0000000..56d1489 --- /dev/null +++ b/server/services/apple-calendar.js @@ -0,0 +1,11 @@ +/** + * Modul: Apple Calendar Sync (CalDAV) + * Zweck: Bidirektionaler Sync mit iCloud Calendar via CalDAV-Protokoll + * Abhängigkeiten: tsdav, server/db.js + */ + +// Platzhalter — wird in Phase 3 implementiert + +module.exports = { + sync: async () => null, +}; diff --git a/server/services/google-calendar.js b/server/services/google-calendar.js new file mode 100644 index 0000000..b7f7ef5 --- /dev/null +++ b/server/services/google-calendar.js @@ -0,0 +1,13 @@ +/** + * Modul: Google Calendar Sync + * Zweck: OAuth 2.0 + bidirektionaler Sync mit Google Calendar API v3 + * Abhängigkeiten: googleapis, server/db.js + */ + +// Platzhalter — wird in Phase 3 implementiert + +module.exports = { + getAuthUrl: () => null, + handleCallback: async () => null, + sync: async () => null, +}; diff --git a/server/services/recurrence.js b/server/services/recurrence.js new file mode 100644 index 0000000..e061d7a --- /dev/null +++ b/server/services/recurrence.js @@ -0,0 +1,12 @@ +/** + * Modul: Wiederholungsregeln (Recurrence) + * Zweck: RRULE-Parser und -Generator für wiederkehrende Aufgaben und Termine + * Abhängigkeiten: keine externen (eigene Implementierung für FREQ=DAILY/WEEKLY/MONTHLY) + */ + +// Platzhalter — wird in Phase 4 implementiert + +module.exports = { + nextOccurrence: () => null, + expandRule: () => [], +}; diff --git a/setup.js b/setup.js new file mode 100644 index 0000000..45e33b2 --- /dev/null +++ b/setup.js @@ -0,0 +1,136 @@ +/** + * Modul: Setup-Script + * Zweck: Erstmalige Einrichtung — ersten Admin-User anlegen. + * Wird einmalig nach dem ersten Start ausgeführt: `node setup.js` + * Abhängigkeiten: server/db.js, bcrypt, dotenv + */ + +'use strict'; + +require('dotenv').config(); +const readline = require('node:readline'); +const bcrypt = require('bcrypt'); +const db = require('./server/db'); + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +function prompt(question) { + return new Promise((resolve) => rl.question(question, resolve)); +} + +function promptPassword(question) { + return new Promise((resolve) => { + process.stdout.write(question); + process.stdin.setRawMode(true); + process.stdin.resume(); + + let password = ''; + process.stdin.on('data', function handler(char) { + char = char.toString(); + if (char === '\r' || char === '\n') { + process.stdin.setRawMode(false); + process.stdin.removeListener('data', handler); + process.stdout.write('\n'); + resolve(password); + } else if (char === '\u0003') { + process.exit(); + } else if (char === '\u007f') { + if (password.length > 0) { + password = password.slice(0, -1); + process.stdout.write('\b \b'); + } + } else { + password += char; + process.stdout.write('*'); + } + }); + }); +} + +async function main() { + console.log('\n=== Oikos Setup ===\n'); + + // Datenbank initialisieren + db.init(); + + // Prüfen ob bereits Admin vorhanden + const existingAdmin = db.get() + .prepare("SELECT id FROM users WHERE role = 'admin' LIMIT 1") + .get(); + + if (existingAdmin) { + console.log('ℹ Es existiert bereits ein Admin-Account.\n'); + const proceed = await prompt('Trotzdem einen weiteren Admin anlegen? (j/N): '); + if (proceed.toLowerCase() !== 'j') { + console.log('Setup abgebrochen.'); + rl.close(); + process.exit(0); + } + } + + console.log('Admin-Account anlegen:\n'); + + const username = (await prompt('Benutzername: ')).trim(); + if (!username || username.length < 3) { + console.error('Fehler: Benutzername muss mindestens 3 Zeichen lang sein.'); + process.exit(1); + } + + const displayName = (await prompt('Anzeigename (z.B. "Max Mustermann"): ')).trim(); + if (!displayName) { + console.error('Fehler: Anzeigename darf nicht leer sein.'); + process.exit(1); + } + + const password = await promptPassword('Passwort: '); + if (password.length < 8) { + console.error('Fehler: Passwort muss mindestens 8 Zeichen lang sein.'); + process.exit(1); + } + + const passwordConfirm = await promptPassword('Passwort bestätigen: '); + if (password !== passwordConfirm) { + console.error('Fehler: Passwörter stimmen nicht überein.'); + process.exit(1); + } + + const avatarColors = ['#007AFF', '#34C759', '#FF9500', '#FF3B30', '#AF52DE', '#FF2D55']; + const avatarColor = avatarColors[Math.floor(Math.random() * avatarColors.length)]; + + console.log('\nAccount wird erstellt …'); + + const hash = await bcrypt.hash(password, 12); + + try { + const result = db.get() + .prepare(` + INSERT INTO users (username, display_name, password_hash, avatar_color, role) + VALUES (?, ?, ?, ?, 'admin') + `) + .run(username, displayName, hash, avatarColor); + + console.log(`\n✓ Admin-Account erstellt (ID: ${result.lastInsertRowid})`); + console.log(` Benutzername: ${username}`); + console.log(` Anzeigename: ${displayName}`); + console.log(` Rolle: admin`); + console.log('\nDu kannst dich jetzt unter /login anmelden.\n'); + } catch (err) { + if (err.message?.includes('UNIQUE constraint')) { + console.error(`\nFehler: Benutzername "${username}" ist bereits vergeben.`); + } else { + console.error('\nFehler beim Erstellen:', err.message); + } + process.exit(1); + } + + rl.close(); + process.exit(0); +} + +main().catch((err) => { + console.error('Unerwarteter Fehler:', err.message); + process.exit(1); +}); diff --git a/test-db.js b/test-db.js new file mode 100644 index 0000000..e42d856 --- /dev/null +++ b/test-db.js @@ -0,0 +1,188 @@ +/** + * Modul: Datenbank-Test + * Zweck: Schema-Migration mit node:sqlite (built-in) validieren. + * Kein Kompilieren nötig — läuft direkt mit Node 22+. + * Testet SQL-Korrektheit, FK-Reihenfolge, Triggers, Indizes. + * + * Ausführen: node test-db.js + */ + +'use strict'; + +const { DatabaseSync } = require('node:sqlite'); + +// -------------------------------------------------------- +// Migrations-SQL direkt aus db.js extrahieren +// (Nur für Tests — in Produktion läuft db.js mit better-sqlite3) +// -------------------------------------------------------- +const { MIGRATIONS_SQL } = require('./server/db-schema-test'); + +let passed = 0; +let failed = 0; + +function test(name, fn) { + try { + fn(); + console.log(` ✓ ${name}`); + passed++; + } catch (err) { + console.error(` ✗ ${name}`); + console.error(` ${err.message}`); + failed++; + } +} + +function assert(condition, message) { + if (!condition) throw new Error(message || 'Assertion fehlgeschlagen'); +} + +// -------------------------------------------------------- +// Datenbank in Memory aufbauen +// -------------------------------------------------------- +const db = new DatabaseSync(':memory:'); +db.exec('PRAGMA foreign_keys = ON;'); + +console.log('\n[DB-Test] Schema-Migration\n'); + +// -------------------------------------------------------- +// Test 1: Migrations-Tabelle anlegen +// -------------------------------------------------------- +test('schema_migrations Tabelle erstellen', () => { + db.exec(` + CREATE TABLE IF NOT EXISTS schema_migrations ( + version INTEGER PRIMARY KEY, + description TEXT NOT NULL, + applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ); + `); + const count = db.prepare('SELECT count(*) as n FROM schema_migrations').get(); + assert(count.n === 0, 'Tabelle sollte leer sein'); +}); + +// -------------------------------------------------------- +// Test 2: Vollständige Migration v1 ausführen +// -------------------------------------------------------- +test('Migration v1 ausführen (alle Tabellen und Triggers)', () => { + db.exec(MIGRATIONS_SQL[1]); + db.prepare('INSERT INTO schema_migrations (version, description) VALUES (1, ?)').run('Initiales Schema'); + const v = db.prepare('SELECT MAX(version) as v FROM schema_migrations').get(); + assert(v.v === 1, 'Version sollte 1 sein'); +}); + +// -------------------------------------------------------- +// Test 3: Alle erwarteten Tabellen vorhanden +// -------------------------------------------------------- +const EXPECTED_TABLES = [ + 'users', 'tasks', 'shopping_lists', 'shopping_items', + 'meals', 'meal_ingredients', 'calendar_events', + 'notes', 'contacts', 'budget_entries', +]; + +EXPECTED_TABLES.forEach((table) => { + test(`Tabelle "${table}" existiert`, () => { + const row = db.prepare( + "SELECT name FROM sqlite_master WHERE type='table' AND name=?" + ).get(table); + assert(row, `Tabelle "${table}" nicht gefunden`); + }); +}); + +// -------------------------------------------------------- +// Test 4: Alle updated_at-Triggers vorhanden +// -------------------------------------------------------- +const EXPECTED_TRIGGERS = EXPECTED_TABLES.filter((t) => t !== 'schema_migrations').map( + (t) => `trg_${t}_updated_at` +); + +EXPECTED_TRIGGERS.forEach((trigger) => { + test(`Trigger "${trigger}" existiert`, () => { + const row = db.prepare( + "SELECT name FROM sqlite_master WHERE type='trigger' AND name=?" + ).get(trigger); + assert(row, `Trigger "${trigger}" nicht gefunden`); + }); +}); + +// -------------------------------------------------------- +// Test 5: CRUD-Operationen +// -------------------------------------------------------- +test('User anlegen', () => { + const result = db.prepare(` + INSERT INTO users (username, display_name, password_hash, role) + VALUES ('admin', 'Admin', '$2b$12$test', 'admin') + `).run(); + assert(result.lastInsertRowid === 1, 'User-ID sollte 1 sein'); +}); + +test('Aufgabe anlegen und lesen', () => { + const ins = db.prepare(` + INSERT INTO tasks (title, created_by, priority) VALUES ('Testaufgabe', 1, 'high') + `).run(); + const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(ins.lastInsertRowid); + assert(task.title === 'Testaufgabe', 'Titel stimmt nicht'); + assert(task.status === 'open', 'Status sollte open sein'); + assert(task.priority === 'high', 'Priorität stimmt nicht'); +}); + +test('Mahlzeit und Einkaufsartikel mit FK-Referenz', () => { + // Mahlzeit zuerst (FK-Reihenfolge) + const meal = db.prepare(` + INSERT INTO meals (date, meal_type, title, created_by) VALUES ('2026-03-24', 'dinner', 'Pizza', 1) + `).run(); + + const list = db.prepare(` + INSERT INTO shopping_lists (name, created_by) VALUES ('REWE', 1) + `).run(); + + // Artikel mit Referenz auf Mahlzeit + db.prepare(` + INSERT INTO shopping_items (list_id, name, added_from_meal) VALUES (?, 'Mehl', ?) + `).run(list.lastInsertRowid, meal.lastInsertRowid); + + const item = db.prepare('SELECT * FROM shopping_items WHERE name = ?').get('Mehl'); + assert(item.added_from_meal === meal.lastInsertRowid, 'FK zu meals stimmt nicht'); +}); + +test('updated_at Trigger feuert bei UPDATE', () => { + const before = db.prepare('SELECT updated_at FROM tasks WHERE id = 1').get(); + // Kurz warten damit Timestamp sich unterscheidet + const start = Date.now(); + while (Date.now() - start < 1100) { /* busy wait 1s */ } + db.prepare("UPDATE tasks SET title = 'Geändert' WHERE id = 1").run(); + const after = db.prepare('SELECT updated_at FROM tasks WHERE id = 1').get(); + assert(after.updated_at > before.updated_at, 'updated_at sollte nach UPDATE neuer sein'); +}); + +test('FK ON DELETE CASCADE (User löschen → Aufgaben weg)', () => { + // Zweiten User mit Aufgabe anlegen + db.prepare(`INSERT INTO users (username, display_name, password_hash) VALUES ('user2', 'User 2', 'x')`).run(); + db.prepare(`INSERT INTO tasks (title, created_by) VALUES ('Zu löschen', 2)`).run(); + + db.prepare('DELETE FROM users WHERE id = 2').run(); + + const orphan = db.prepare("SELECT * FROM tasks WHERE title = 'Zu löschen'").get(); + assert(!orphan, 'Verwaiste Aufgaben sollten gelöscht sein'); +}); + +test('CHECK constraint: ungültige Priorität wird abgelehnt', () => { + let threw = false; + try { + db.prepare("INSERT INTO tasks (title, created_by, priority) VALUES ('x', 1, 'invalid')").run(); + } catch { + threw = true; + } + assert(threw, 'CHECK constraint sollte Fehler werfen'); +}); + +test('Idempotenz: Migration zweimal ausführen ändert nichts', () => { + // CREATE TABLE IF NOT EXISTS + CREATE TRIGGER IF NOT EXISTS müssen idempotent sein + db.exec(MIGRATIONS_SQL[1]); + const tables = db.prepare("SELECT count(*) as n FROM sqlite_master WHERE type='table'").get(); + assert(tables.n > 0, 'Tabellen sollten noch vorhanden sein'); +}); + +// -------------------------------------------------------- +// Ergebnis +// -------------------------------------------------------- +console.log(`\n[DB-Test] Ergebnis: ${passed} bestanden, ${failed} fehlgeschlagen\n`); +if (failed > 0) process.exit(1);