diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72263c0..dd68e63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,37 +1,38 @@ -name: "CI" -on: push +name: "CI/CD Pipeline" +on: + pull_request: + branches: [main, 'blatt*'] + push: + branches: [main, 'blatt*'] permissions: contents: read jobs: - Test: + build: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 - - name: Install Docker Compose + + - name: Install Docker Compose (build job) run: | sudo apt-get update sudo apt-get install -y docker-compose + - name: Build the stack run: docker-compose -f project/compose.yaml build - # Container starten - - name: Start services - run: docker-compose -f project/compose.yaml up -d - # Healthcheck / Test - # - name: Run tests - # run: | - # docker ps - # curl -s --retry 10 --retry-connrefused http://localhost:8000/ - # - name: Test - # run: docker run --network container:webapp-frontend appropriate/curl -s --retry 10 --retry-connrefused http://localhost:5000/ + - name: Start services + run: | + # remove stale containers/volumes from previous runs that can cause compose to read invalid metadata + docker-compose -f project/compose.yaml down --remove-orphans --volumes || true + docker-compose -f project/compose.yaml up -d - name: Wait for frontend on :8000 run: | PORT=8000 attempts=30 - wait_seconds=3 + wait_seconds=1 + echo "Waiting for frontend..." for i in $(seq 1 $attempts); do if curl -fsSL "http://localhost:${PORT}/" >/dev/null 2>&1; then echo "ready" @@ -44,11 +45,62 @@ jobs: docker-compose -f project/compose.yaml ps || true docker-compose -f project/compose.yaml logs || true exit 1 + - name: Run smoke test frontend + run: curl -fsSL -o /dev/null http://localhost:8000/ + + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Docker Compose (tests job) + run: | + sudo apt-get update + sudo apt-get install -y docker-compose - - name: Run backend tests in container + - name: Pull images (if available) or build run: | - # startet einen einmaligen Container für den Service 'backend' und führt Unittests aus - docker-compose -f project/compose.yaml run --rm backend sh -c "python -m unittest discover -s tests -v" + # Versuche remote images zu ziehen; falls nicht vorhanden, baue lokal neu. + docker-compose -f project/compose.yaml pull || docker-compose -f project/compose.yaml build - - name: Smoke test - run: curl -fsSL -o /dev/null http://localhost:8000/ + - name: Start services + run: | + docker-compose -f project/compose.yaml down --remove-orphans --volumes || true + docker-compose -f project/compose.yaml up -d backend-dev + + - name: Wait for backend to be ready + run: | + for i in $(seq 1 20); do + docker-compose -f project/compose.yaml exec -T backend-dev sh -c "python -c 'print(\"ok\")'" >/dev/null 2>&1 && break + sleep 1 + done + + - name: Run backend tests + run: | + docker-compose -f project/compose.yaml exec -T backend-dev sh -c "python -m unittest discover -s tests -v" + +# lint: +# runs-on: ubuntu-latest +# needs: build +# steps: +# - name: Wait for frontend on :8000 +# run: | +# PORT=8000 +# attempts=30 +# wait_seconds=3 +# for i in $(seq 1 $attempts); do +# if curl -fsSL "http://localhost:${PORT}/" >/dev/null 2>&1; then +# echo "ready" +# exit 0 +# fi +# echo "waiting... ($i)" +# sleep $wait_seconds +# done +# echo "service not ready" >&2 +# docker-compose -f project/compose.yaml ps || true +# docker-compose -f project/compose.yaml logs || true +# exit 1 +# - name: Run linting tests +# run: | +# # startet einen einmaligen Container für den Service 'backend' und führt Linting-Tests aus (z.B. mit flake8) +# docker-compose -f project/compose.yaml run --rm backend sh -c "flake8 ." \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..9170a85 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$PROJECT_DIR$/Project/backend/LazyCookDB.sqlite3 + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/db-forest-config.xml b/.idea/db-forest-config.xml new file mode 100644 index 0000000..5a20425 --- /dev/null +++ b/.idea/db-forest-config.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..6ca4331 --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/docs/Images/CI_CD.drawio b/docs/Images/CI_CD.drawio new file mode 100644 index 0000000..303a9d7 --- /dev/null +++ b/docs/Images/CI_CD.drawio @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Images/CI_CD.drawio.png b/docs/Images/CI_CD.drawio.png new file mode 100644 index 0000000..a8bbf86 Binary files /dev/null and b/docs/Images/CI_CD.drawio.png differ diff --git a/docs/RMMM-Liste.pdf b/docs/RMMM-Liste.pdf new file mode 100644 index 0000000..25c55db Binary files /dev/null and b/docs/RMMM-Liste.pdf differ diff --git a/docs/Softwareanforderungsspezifikation (SRS).md b/docs/Softwareanforderungsspezifikation (SRS).md index 1754561..8a2c5d9 100644 --- a/docs/Softwareanforderungsspezifikation (SRS).md +++ b/docs/Softwareanforderungsspezifikation (SRS).md @@ -53,7 +53,8 @@ Eine Zutat soll aus ihren Namen, ihre Menge und Einheit der Menge bestehen. Die Der Nutzer muss angemeldet sein und sich auf der Seite befinden für Zutaten eintragen, um die Zutaten eintragen zu können. ##### Nachbedingungen: Die eingetragene Zutat werden gespeichert. -##### Geschätzter Aufwand: mittel/hoch +##### Geschätzter Aufwand: +mittel/hoch #### 2.03 Zutaten entfernen können @@ -71,7 +72,8 @@ Die jeweilige Zutat sollen im Browser-Cache entfernt werden. Der Nutzer muss angemeldet sein und sich auf der Seite befinden für Zutaten eintragen, um die Zutaten eintragen zu können und es muss mindestens eine Zutat bereits eingetragen sein. ##### Nachbedingungen: Die gelöschte Zutat wird aus dem Speicher gelöscht. -##### Geschätzter Aufwand: niedrig +##### Geschätzter Aufwand: +niedrig #### 2.04 Automatischer Rezeptvorschlag auf Grundlage der eingetragenen Zutaten @@ -92,7 +94,8 @@ Die Anzahl der Personen soll hier noch keine Auswirkung auf den automatischen Re Der Nutzer muss angemeldet sein und Zutaten, die er vorrätig hat, eingetragen haben. ##### Nachbedingungen: n/a -##### Geschätzter Aufwand: hoch +##### Geschätzter Aufwand: +hoch #### 2.05 Als Kunde mit neuem Konto registrieren können @@ -113,7 +116,8 @@ Die Eingabe für die Registrierung soll im Frontend und im Backend Validiert wer Ein Nutzer der LazyCook nutzen will ##### Nachbedingungen: n/a -##### Geschätzter Aufwand: mittel +##### Geschätzter Aufwand: +mittel #### 2.06 Als Kunde mit Konto anmelden können @@ -134,7 +138,8 @@ Als Nutzer soll man sich auf der Webseite Anmelden können um alle Funktionen de Der User muss mit dem Konto bereits registriert sein mit welchen er sich anmelden möchte. ##### Nachbedingungen: Der User ist mit dem Konto angemeldet, welches er eingegeben hat. -##### Geschätzter Aufwand: mittel +##### Geschätzter Aufwand: +mittel #### 2.07 Als Kunde mit Konto abmelden können @@ -152,7 +157,8 @@ Als Nutzer soll man sich auch abmelden können um sich z.B. auch mit anderen Kon Der Nutzer muss angemeldet sein mit einem Konto. ##### Nachbedingungen: Der Nutzer ist nicht angemeldet. -##### Geschätzter Aufwand: niedrig +##### Geschätzter Aufwand: +niedrig #### 2.08 Personenanzahl angegeben bei Zutaten eintragen @@ -170,7 +176,8 @@ Die angegebene Personenanzahl soll sich auf den automatischen Rezeptvorschlag au Der Nutzer muss angemeldet sein und sich auf der Seite befinden für Zutaten eintragen, um die Personenanzahl eintragen zu können. ##### Nachbedingungen: Die eingetragene Personenanzahl wir gespeichert. Bei dem automatischen Rezeptvorschlag wird die Personenanzahl berücksichtigt und passt entsprechend angepasst. -##### Geschätzter Aufwand: mittel +##### Geschätzter Aufwand: +mittel #### 2.09 Grundlegende Zutaten werden beim Eintragen empfohlen @@ -188,7 +195,8 @@ Dieser Vorschlag soll fest eingestellt sein. Der Nutzer muss angemeldet sein und sich auf der Seite befinden für Zutaten eintragen. ##### Nachbedingungen: Die hinzugefügten Grundzutaten müssen zu den bereits vorhandenen Zutaten hinzugefügt werden. -##### Geschätzter Aufwand: mittel +##### Geschätzter Aufwand: +mittel #### 2.10 Rezepte Suchen können ohne eingetragene Zutaten @@ -209,26 +217,38 @@ Rezepte sollen auch ohne den Eintrag von Zutaten suchbar sein. Der Nutzer muss angemeldet sein und sich auf der Seite befinden für Zutaten eintragen. ##### Nachbedingungen: -##### Geschätzter Aufwand: niedrig +##### Geschätzter Aufwand: +niedrig ___ ### 3. Nicht-funktionale Anforderungen -Das Projekt muss zum Ende des 4. Semesters abgegeben werden, genaueres wird noch bekannt gegeben. Es muss sich um eine webbasierte Anwendung handeln. LazyCook soll für die Benutzung/Bedingung am Laptop oder am Computer optimiert sein. - -#### 3.1 Verwendete Technologien -- Frontend: HTML, CSS, JavaScript/React -- Backend: Python -- DB: SQLite -- IDE: IntelliJ -- Projekt Management: GitHub Projects - -#### 3.2 Qualitätsmerkmale -- Wartbarkeit (Änderbarkeit): Neue Feature und Bug-Fixes innerhalb von 2 Tagen -- Effizient (Zeitverhalten): Rezepte werden in maximal 5 Sekunden nach Klick auf Filtern angezeigt -- Sicherheit der Nutzerdaten: Passwörter werden gehashed mit Salt in der Datnebnak gespeichert -- Benutzbarkeit (Bedienbarkeit & Ansehnlichkeit): Anmeldung & Registrierung mit möglichts wenigen Klicks; Rezept vorschläge werden als 3x3 Matrix mit max. 9 Stück auf einer Seite aangezeigt. -- Kompatibilität (Interoperatibilität): Die Anwendung soll auf unterscheidlihen Browsern fehlerfrei laufen (Chrome, Firefox, Safari) +Das Projekt muss zum Ende des 4. Semesters abgegeben werden, genaueres wird noch bekannt gegeben.
+Sollte die Zeit nicht ausreichen, um alle geplanten Features zu implementieren, sollen die Kernfunktionen (Zutaten hinzufügen/entfernen, Rezeptsuche und Rezeptvorschläge) Priorität haben. +#### 3.1 Technische Einschränkungen +- **TE-1 (Architektur):** Das System muss als Web-Applikation realisiert werden. Eine native Installation auf dem Endgerät des Nutzers ist nicht vorgesehen. +- **TE-2 (Laufzeitumgebung/Browser):** Die Anwendung muss auf den aktuellsten stabilen Versionen der Browser Google Chrome, Mozilla Firefox und Safari lauffähig sein. Eine Optimierung für veraltete Browser (z.B. Internet Explorer) oder Browser-Versionen erfolgt nicht. +- **TE-3 (Zielplattform):** Das Interface ist ausschließlich für die Nutzung an Desktop-Systemen (Laptops/PCs) mit Maus- und Tastatureingabe zu optimieren. Eine Smartphone-Optimierung ist im Rahmen dieses Projekts nicht gefordert. +- **TE-4 (Persistenz):** Zur Datenspeicherung ist eine lokale SQLite-Datenbank zu verwenden. Dies schließt den Einsatz von Cloud-Datenbanken oder extern verwalteten DBMS aus. +- **TE-5 (Sicherheit/Verschlüsselung):** Passwörter dürfen unter keinen Umständen im Klartext gespeichert werden. Es ist zwingend ein modernes Hashing-Verfahren (z.B. Argon2 oder bcrypt) mit Salt-Zusatz zu verwenden. +- **TE-6 (Entwicklungsumgebung):** Das Projekt muss unter Verwendung der IDE IntelliJ und der Versionsverwaltung GitHub entwickelt werden. + +#### 3.2 Verwendete Technologien +- **Frontend:** HTML, CSS, React/JavaScript +- **Backend:** Python mit FastAPI +- **DB:** SQLite +- **Containerization:** Docker +- **Tests:** Pytest/unittest +- **CI/CD:** GitHub Actions +- **IDE:** IntelliJ +- **Projekt Management:** GitHub Projects + +#### 3.3 Qualitätsmerkmale +- **Wartbarkeit (Änderbarkeit):** Neue Feature und Bug-Fixes innerhalb von 2 Tagen auf GitHub bereitstellen. +- **Effizient (Zeitverhalten):** Rezepte werden in maximal 5 Sekunden nach Klick auf Filtern angezeigt. +- **Sicherheit der Nutzerdaten:** Passwörter werden gehasht mit Salt in der Datenbank gespeichert. +- **Benutzbarkeit (Bedienbarkeit & Ansehnlichkeit):** Anmeldung & Registrierung mit möglichst wenigen Klicks; Rezept vorschläge werden als 3x3 Matrix mit max. 9 Stück auf einer Seite angezeigt. +- **Kompatibilität (Interoperabilität):** Die Anwendung soll auf unterschiedlichen Browsern fehlerfrei laufen (Chrome, Firefox, Safari). ___ ### 4. Unterstützende Informationen diff --git a/docs/Softwarearchitektur_2026-04-09.md b/docs/Softwarearchitektur_2026-04-09.md new file mode 100644 index 0000000..b9d6c71 --- /dev/null +++ b/docs/Softwarearchitektur_2026-04-09.md @@ -0,0 +1,462 @@ +# Einführung und Ziele + +## Aufgabenstellung + +**Inhalt** + +Die Anwendung LazyCook soll Nutzern das Filtern von Rezepten, die den vorhandenen Zutaten entsprechen, ermöglichen. Nutzer sollen Zutaten, Menge und Personenanzahl eingeben können. Rezepte, die auf diese Angaben möglichst genau passen, werden gesucht und angezeigt. + +Anforderungen: +- Registrierung und Anmeldung auf der Website +- Abmelden von der Website +- Eingabe von Zutaten, Menge und Personenanzahl +- Entfernen von falschen Eingaben +- Automatisches Suchen und Filtern der Rezepte anhand der Eingaben +- Anzeigen von 9 Rezepten auf einer Seite +- Anzeige von mehr Rezepten bei Bedarf +- Vorschläge von häufigen Zutaten +- Rezepte auch ohne eingetragenen Zutaten suchen können + +Die Anforderungen werden ausführlich im [SRS-Dokument](https://github.com/GalacticCodeGambit/LazyCook/blob/4710a641f177a29e59067edf688781efb3b03a3d/docs/Softwareanforderungsspezifikation%20(SRS).md) aufgeschlüsselt. + +**Motivation** + +Unsere Nutzer sollen schnell und bequem passende Rezepte zu den Zutaten finden, die sie zu Hause haben. Wir möchten die dafür nötige Anwendung zur Verfügung stellen. Für die einfache Nutzung soll die Anwendung performant, benutzerfreundlich und verfügbar sein. + +## Qualitätsziele + +**Inhalt** + +Die folgenden Qualitätsziele sind für LazyCook maßgeblich und beeinflussen die Architekturentscheidungen: + +| Priorität | Qualitätsziel | Beschreibung | +|-----------|---------------|--------------| +| 1 | Effizienz (Zeitverhalten) | Rezepte werden innerhalb von maximal 5 Sekunden nach Klick auf Filtern angezeigt | +| 2 | Sicherheit | Passwörter werden gehasht mit Salt (PBKDF2-HMAC mit SHA256) in der Datenbank gespeichert | +| 3 | Benutzbarkeit | Anmeldung & Registrierung mit möglichst wenigen Klicks; Rezeptvorschläge als 3x3 Matrix | +| 4 | Wartbarkeit (Änderbarkeit) | Neue Features und Bug-Fixes innerhalb von 2 Arbeitstagen umsetzbar | +| 5 | Kompatibilität | Die Anwendung läuft fehlerfrei auf Chrome, Firefox, Safari und Edge | + +**Motivation** + +Weil Qualitätsziele grundlegende Architekturentscheidungen oft maßgeblich beeinflussen, sollten Sie die für Ihre Stakeholder relevanten Qualitätsziele kennen, möglichst konkret und operationalisierbar. + +**Form** + +Unser [Qualitätsbaum](https://github.com/GalacticCodeGambit/LazyCook/blob/4710a641f177a29e59067edf688781efb3b03a3d/docs/Qualit%C3%A4tsmerkmale/Qualit%C3%A4tsbaum.md) + +Unsere [Qualitätsszenarien](https://github.com/GalacticCodeGambit/LazyCook/blob/4710a641f177a29e59067edf688781efb3b03a3d/docs/Qualit%C3%A4tsmerkmale/Qualit%C3%A4tsszenario.md) + +## Stakeholder + +| Rolle | Kontakt | Erwartungshaltung | +|-------|---------|-------------------| +| Product-Owner | Samuel Göbel: goebelsamuel@gmail.com | Funktionstüchtige Anwendung entsprechend der gesetzten Anforderungen | +| Kunde/Investor | Harald Ichters: ichters.harald@edu.dhbw-karlsruhe.de | Funktionstüchtige Anwendung mit allen gewünschten Features und den Qualitätsanforderungen. Schnelle Entwicklung | +| Entwickler | Eden Bernhard: etbernhard4@gmail.com, Samuel Göbel, Frederik Behne, Niclas Matzke, Alexander Groer | Klare Anforderungen, direkte Kommunikation, Feedback, Unterstützung im Gesamtprozess, Kommunikation auf Augenhöhe | + +--- + +# Randbedingungen + +**Inhalt** + +### Technische Randbedingungen + +| Randbedingung | Erläuterung | +|---------------|-------------| +| Webbasierte Anwendung | Das Projekt muss als Webanwendung umgesetzt werden | +| Abgabefrist | Das Projekt muss zum Ende des 4. Semesters abgegeben werden | +| Optimierung für Desktop | LazyCook soll für die Benutzung am Laptop oder Computer optimiert sein | +| Docker-Container | Die Anwendung wird mittels Docker Compose containerisiert ausgeliefert | + + +### Organisatorische Randbedingungen + +| Randbedingung | Erläuterung | +|---------------|-------------| +| Team | 5 Entwickler (Eden Bernhard, Niclas Matzke, Frederik Behne, Alexander Groer, Samuel Göbel) | +| Projektmanagement | GitHub Projects zur Aufgabenverwaltung | +| Versionsverwaltung | Git/GitHub | +| CI/CD | GitHub Actions für automatisierte Builds und Tests | + +--- + +# Kontextabgrenzung + +**Inhalt** + +Unser System ist in sich geschlossen und nur von den verwendeten Technologien abhängig. Die Verwendung einer externen Rezept-API ist noch in Überlegung und wäre dann eine externe Abhängigkeit. Das Frontend kommuniziert über HTTP und JSON mit dem Backend und die Daten werden in einer SQLite-Datenbank gespeichert. + +## Fachlicher Kontext + +Der Nutzer interagiert mit LazyCook über den Browser. Die fachliche Schnittstelle umfasst folgende Interaktionen: + +| Kommunikationspartner | Eingabe | Ausgabe | +|----------------------|---------|---------| +| Nutzer (nicht angemeldet) | Aufruf der Website | Startseite mit Login/Registrierung | +| Nutzer (angemeldet) | E-Mail, Passwort | Bestätigung der Anmeldung, Weiterleitung zum RecipeFinder | +| Nutzer (angemeldet) | Zutaten (Name, Menge, Einheit), Personenanzahl | Passende Rezepte als 3x3 Matrix | +| Nutzer (angemeldet) | Abmelde-Anfrage | Bestätigung, Weiterleitung zur Startseite | + +## Technischer Kontext + +Die Anwendung besteht aus einem Next.js/React-Frontend, das über eine REST-Schnittstelle mit einem FastAPI-Backend kommuniziert. Das Backend greift auf eine SQLite-Datenbank zur Speicherung von Rezepten und Nutzerdaten zu und kann optional externe Rezept-APIs einbinden. Die Kommunikation erfolgt über HTTPS und JSON. + +| Technische Schnittstelle | Technologie | Protokoll | +|--------------------------|-------------|-----------| +| Frontend → Backend | REST API | HTTP / JSON | +| Backend → Datenbank | SQLite3 (Python sqlite3-Modul) | Direkte Dateianbindung | +| Browser → Frontend | Next.js SSR/CSR | HTTP | +| CI/CD Pipeline | GitHub Actions → Docker Compose | HTTP (Smoke Test) | + +**Mapping fachlich → technisch:** + +- Frontend-Container (Port 8000): Next.js/React-Anwendung, dient als Benutzeroberfläche +- Backend-Container (Port 3000): FastAPI-Anwendung, verarbeitet Geschäftslogik und Datenzugriff +- SQLite-Datenbankdatei: Persistiert in einem Docker-Volume (`data`) + +--- + +# Lösungsstrategie + +**Inhalt** + +Technologieentscheidungen: +- Frontend: HTML, CSS, TypeScript/React (Next.js 16) +- Backend: Python (FastAPI mit Gunicorn/Uvicorn) +- DB: SQLite +- Tests: Pytest (Backend), unittest +- CI/CD: GitHub Actions +- Containerization: Docker / Docker Compose +- IDE: IntelliJ +- Projekt Management: GitHub Projects + +**Zentrale Entwurfsentscheidungen:** + +| Entscheidung | Begründung | +|--------------|------------| +| Schichtbasierter Architekturstil (ADR04) | Einfache Umsetzung, klare Trennung der Verantwortlichkeiten zwischen Frontend, Backend und Datenbank | +| PBKDF2-HMAC mit SHA256 für Passwort-Hashing (ADR01) | Hohe Sicherheit gegen Brute-Force-Angriffe, Unterstützung durch Python hashlib | +| Direkte Weiterleitung nach Registrierung (ADR02) | Erhöht Benutzerfreundlichkeit, vermeidet unnötige Schritte | +| 3x3 Matrix für Rezeptanzeige (ADR03) | Verhindert Informationsüberladung, Rezeptkacheln sind groß genug für Bilder und Infos | +| Liskov Substitution Principle (ADR05) | Ermöglicht späteren Austausch der Datenquelle ohne Änderung der Geschäftslogik | + +Unsere Architekturentscheidungen sind [hier](https://github.com/GalacticCodeGambit/LazyCook/tree/4710a641f177a29e59067edf688781efb3b03a3d/docs/adr) zu finden. + +--- + +# Bausteinsicht + +## Whitebox Gesamtsystem + +Das Gesamtsystem besteht aus drei Hauptbausteinen: dem Frontend, dem Backend und der Datenbank. Diese sind durch den schichtbasierten Architekturstil klar voneinander getrennt. + +**Übersichtsdiagramm:** + +``` +┌─────────────────────────────────────────────────────────┐ +│ Browser (Nutzer) │ +└────────────────────────┬────────────────────────────────┘ + │ HTTP (Port 8000) +┌────────────────────────▼────────────────────────────────┐ +│ Frontend (Next.js/React) │ +│ ┌──────────┐ ┌──────────────┐ ┌───────────────────┐ │ +│ │ Homepage │ │ Login/ │ │ RecipeFinder │ │ +│ │ │ │ Registrier. │ │ (Zutateneingabe) │ │ +│ └──────────┘ └──────────────┘ └───────────────────┘ │ +└────────────────────────┬────────────────────────────────┘ + │ REST API (JSON, Port 3000) +┌────────────────────────▼────────────────────────────────┐ +│ Backend (FastAPI/Python) │ +│ ┌──────────────────┐ ┌────────────────────────────┐ │ +│ │ LazyCookVerwaltung│ │ Geschäftslogik │ │ +│ │ (API-Endpunkte) │ │ (Person, Benutzer, Admin, │ │ +│ │ /api/login │ │ Rezept, Zutat) │ │ +│ │ /api/register │ └────────────────────────────┘ │ +│ └──────────┬───────┘ │ +│ │ │ +│ ┌──────────▼───────┐ │ +│ │ Datenbank.py │ │ +│ │ (Datenzugriff) │ │ +│ └──────────┬───────┘ │ +└─────────────┼───────────────────────────────────────────┘ + │ SQLite3 +┌─────────────▼───────────────────────────────────────────┐ +│ SQLite-Datenbank (LazyCookDB.sqlite3) │ +│ Tabellen: Konto, Nutzer, Rezept, Zutat, │ +│ Besteht_Aus, Verfasser │ +└─────────────────────────────────────────────────────────┘ +``` + +**Begründung:** +Die Zerlegung folgt dem schichtbasierten Architekturstil (ADR04). Das Frontend ist für die Darstellung zuständig, das Backend für die Geschäftslogik und den Datenzugriff, die Datenbank für die Persistenz. Diese klare Trennung ermöglicht unabhängige Entwicklung und einfache Wartbarkeit. + +**Enthaltene Bausteine:** + +| Name | Verantwortung | +|------|---------------| +| Frontend (Next.js/React) | Benutzeroberfläche, Zutateneingabe, Rezeptanzeige, Login/Registrierung | +| Backend (FastAPI/Python) | REST-API-Endpunkte, Authentifizierung, Rezeptfilterung, Geschäftslogik | +| Datenbank (SQLite) | Persistente Speicherung von Nutzerkonten, Rezepten und Zutaten | + +**Wichtige Schnittstellen:** + +| Schnittstelle | Beschreibung | +|---------------|--------------| +| POST /api/login | Anmeldung mit E-Mail und Passwort. Gibt Erfolgsmeldung oder Fehlermeldung zurück | +| POST /api/register | Registrierung mit E-Mail und Passwort. Passwort wird mit PBKDF2-HMAC gehasht | + +### Frontend (Next.js/React) + +**Zweck/Verantwortung:** Das Frontend ist die Präsentationsschicht der Anwendung. Es stellt die Benutzeroberfläche bereit und kommuniziert über REST-Aufrufe mit dem Backend. + +**Schnittstellen:** Kommuniziert über HTTP/JSON mit dem Backend auf Port 3000. Wird im Docker-Container auf Port 8000 ausgeliefert. + +**Technologie:** Next.js 16, React 19, TypeScript, Tailwind CSS, Radix UI, Lucide Icons. + +**Ablageort:** `project/frontend/` + +### Backend (FastAPI/Python) + +**Zweck/Verantwortung:** Das Backend bildet die Geschäftslogik-Schicht. Es verarbeitet API-Anfragen, führt Authentifizierung durch, filtert Rezepte und greift auf die Datenbank zu. + +**Schnittstellen:** Stellt REST-Endpunkte bereit (`/api/login`, `/api/register`). Kommuniziert direkt mit der SQLite-Datenbank. + +**Technologie:** Python, FastAPI, Gunicorn mit UvicornWorker (4 Worker-Prozesse), Passlib (bcrypt), hashlib (PBKDF2). + +**Ablageort:** `project/backend/` + +**Klassenstruktur:** +- `LazyCookVerwaltung` – Hauptmodul mit FastAPI-App und API-Endpunkten +- `Datenbank` – Datenzugriffsschicht (DAO), verwaltet SQLite-Verbindungen und CRUD-Operationen +- `Person` – Abstrakte Basisklasse für Nutzer (Name, E-Mail, Passwort, Rolle) +- `Benutzer` – Erbt von Person, Rolle "User" +- `Admin` – Erbt von Person, Rolle "Admin", kann Rezepte hinzufügen +- `Rezept` – Repräsentiert ein Rezept (Name, Zutaten, Zubereitung, Dauer, Bewertung) +- `Zutat` – Repräsentiert eine Zutat (Name, Menge) +- `models.py` – Pydantic-Modelle für API-Validierung (UserSignUpIn, UserResponse, SessionResponse) + +### Datenbank (SQLite) + +**Zweck/Verantwortung:** Persistente Speicherung aller Anwendungsdaten. + +**Datenbankschema:** + +| Tabelle | Spalten | Beschreibung | +|---------|---------|--------------| +| Konto | id, email, passwort, salt | Benutzerkonten mit gehashten Passwörtern | +| Nutzer | id, name, kid (FK → Konto) | Nutzerprofile | +| Rezept | id, name, vid (FK → Verfasser) | Rezepte | +| Zutat | id, name, mengenArt | Verfügbare Zutaten | +| Besteht_Aus | zid (FK → Zutat), rid (FK → Rezept), menge | N:M-Beziehung Zutat-Rezept | +| Verfasser | id, name | Rezeptautoren | + +**Ablageort:** `LazyCookDB.sqlite3` (persistiert in Docker-Volume `data`) + +## Ebene 2 + +### Whitebox Frontend + +Das Frontend ist als Next.js-Anwendung mit dem App-Router strukturiert: + +| Baustein | Verantwortung | +|----------|---------------| +| `app/homepage/` | Startseite mit Hero-Section, Funktionsweise-Erklärung und Login-Modal | +| `app/imports/Startseite.tsx` | Importierte Startseitenkomponenten | +| `app/components/ui/` | Wiederverwendbare UI-Komponenten (Buttons, Calendar, etc.) basierend auf Radix UI/shadcn | + +### Whitebox Backend + +Das Backend folgt einer dreischichtigen Struktur innerhalb des Backends: + +| Baustein | Verantwortung | +|----------|---------------| +| `LazyCookVerwaltung.py` | API-Schicht: FastAPI-Endpunkte, CORS-Middleware, Passwort-Hashing/Verifizierung | +| `Datenbank.py` | Datenzugriffsschicht: SQLite-Verbindung, Tabellenerstellung, CRUD-Operationen | +| `Person.py`, `Benutzer.py`, `Admin.py` | Domänenmodelle: Vererbungshierarchie für Nutzerrollen | +| `Rezept.py`, `Zutat.py` | Domänenmodelle: Rezept- und Zutaten-Entitäten | +| `models.py` | API-Modelle: Pydantic-Schemas für Request/Response-Validierung | + +--- + +# Laufzeitsicht + +## Szenario 1: Benutzerregistrierung + +1. Nutzer gibt E-Mail und Passwort auf der Registrierungsseite ein +2. Frontend sendet POST-Request an `/api/register` mit E-Mail und Passwort als JSON +3. Backend (`LazyCookVerwaltung.registrieren()`) erzeugt ein 16-Byte zufälliges Salt +4. Backend hasht das Passwort mit PBKDF2-HMAC (SHA256, 100.000 Iterationen) +5. Backend speichert E-Mail, gehashtes Passwort und Salt (jeweils Base64-kodiert) in der Konto-Tabelle via `Datenbank.addNutzer()` +6. Backend prüft ob E-Mail bereits existiert und gibt entsprechende Meldung zurück +7. Frontend zeigt Erfolgsmeldung und leitet direkt zum RecipeFinder weiter (ADR02) + +## Szenario 2: Benutzeranmeldung + +1. Nutzer gibt E-Mail und Passwort auf der Anmeldeseite ein +2. Frontend sendet POST-Request an `/api/login` +3. Backend ruft `Datenbank.anmeldenNutzer(email)` auf, um Salt und gehashtes Passwort aus der Datenbank zu laden +4. Backend hasht das eingegebene Passwort mit dem gespeicherten Salt und vergleicht das Ergebnis +5. Bei Übereinstimmung: Erfolgsmeldung; bei Fehler: Fehlermeldung ("Falsches Passwort" oder "Kein Konto hinterlegt") + +## Szenario 3: Rezeptfilterung nach Zutaten + +1. Nutzer gibt auf der RecipeFinder-Seite Zutaten (Name, Menge, Einheit) und Personenanzahl ein +2. Zutaten werden im Browser-Cache zwischengespeichert +3. Frontend sendet die Zutatenliste und Personenanzahl als JSON an das Backend +4. Backend filtert die Rezepte in der Datenbank anhand der eingegebenen Zutaten über die Besteht_Aus-Tabelle +5. Backend gibt passende Rezepte als JSON zurück +6. Frontend zeigt maximal 9 Rezepte als 3x3 Matrix an (ADR03) + +## Szenario 4: Benutzerabmeldung + +1. Nutzer klickt auf "Abmelden" +2. Frontend löscht die Sitzungsdaten +3. Nutzer wird zur Startseite weitergeleitet + +--- + +# Verteilungssicht + +## Infrastruktur Ebene 1 + +Die Anwendung wird mittels Docker Compose auf einem einzelnen Host deployed. Zwei Container kommunizieren über ein internes Docker-Netzwerk. + +``` +┌──────────────────────────────────────────────────────────┐ +│ Host-System │ +│ │ +│ ┌─────────────────────┐ ┌─────────────────────────┐ │ +│ │ frontend-dev │ │ backend │ │ +│ │ (Docker Container) │ │ (Docker Container) │ │ +│ │ │ │ │ │ +│ │ Base: node:24- │ │ Base: python:3.11-slim │ │ +│ │ alpine3.20 │ │ │ │ +│ │ Port: 8000 │ │ Port: 3000 │ │ +│ │ │ │ Workers: 4 (Gunicorn/ │ │ +│ │ Next.js SSR │ │ Uvicorn) │ │ +│ │ │ │ │ │ +│ └─────────────────────┘ │ ┌───────────────────┐ │ │ +│ │ │ Volume: /data │ │ │ +│ │ │ LazyCookDB.sqlite3│ │ │ +│ │ └───────────────────┘ │ │ +│ └─────────────────────────┘ │ +│ │ +│ Port-Mapping: │ +│ localhost:8000 → frontend:8000 │ +│ localhost:3000 → backend:3000 │ +└──────────────────────────────────────────────────────────┘ +``` + +**Begründung:** Docker Compose ermöglicht eine einfache, reproduzierbare Entwicklungs- und Deployment-Umgebung. Die Trennung in zwei Container spiegelt die Schichtarchitektur wider. + +**Zuordnung von Bausteinen zu Infrastruktur:** + +| Baustein | Container | Dockerfile | Technologie | +|----------|-----------|------------|-------------| +| Frontend | frontend-dev | Dockerfile-frontend | Node.js 24, Next.js 16, React 19 | +| Backend + DB | backend | Dockerfile-backend | Python 3.11, FastAPI, Gunicorn, SQLite | + +**CI/CD-Pipeline (GitHub Actions):** +Die CI-Pipeline baut die Docker-Container, startet die Services, wartet auf die Verfügbarkeit des Frontends (Port 8000), führt Backend-Unittests aus und prüft per Smoke-Test die Erreichbarkeit. + +--- + +# Querschnittliche Konzepte + +## Sicherheit / Authentifizierung + +Passwörter werden niemals im Klartext gespeichert. Stattdessen wird PBKDF2-HMAC mit SHA256 verwendet (ADR01). Pro Nutzer wird ein zufälliges 16-Byte Salt generiert. Der Hash wird mit 100.000 Iterationen berechnet. Salt und Hash werden Base64-kodiert in der Konto-Tabelle gespeichert. Bei der Anmeldung wird das eingegebene Passwort mit dem gespeicherten Salt erneut gehasht und mit dem gespeicherten Hash verglichen. + +## CORS (Cross-Origin Resource Sharing) + +Das Backend konfiguriert CORS-Middleware, um Anfragen vom Frontend (localhost:8000) an das Backend (localhost:3000) zu erlauben. Alle HTTP-Methoden und Header sind zugelassen. + +## Validierung + +Die Eingabevalidierung erfolgt auf zwei Ebenen: Im Frontend wird die Benutzereingabe vorab geprüft (z.B. E-Mail-Format), im Backend wird die Validierung durch Pydantic-Modelle (`UserSignUpIn` mit `EmailStr`) sichergestellt. Die Datenbank erzwingt zusätzlich Integritätsbedingungen (UNIQUE, NOT NULL, Foreign Keys). + +## Persistenz + +SQLite wird als eingebettete Datenbank verwendet. Die Datenbankdatei wird in einem Docker-Volume persistiert, sodass Daten Container-Neustarts überleben. Foreign Keys sind aktiviert (`PRAGMA foreign_keys = ON`). Für jeden Request wird eine neue Datenbankverbindung erstellt (`get_connection()`), um Thread-Safety zu gewährleisten. + +## Containerisierung + +Die Anwendung wird mit Docker Compose verwaltet. Das Frontend wird als Node.js-Container gebaut (`npm run build` → `npm start`), das Backend als Python-Container mit Gunicorn und 4 Uvicorn-Workern. Ein Docker-Volume (`data`) persistiert die SQLite-Datenbank. + +## Fehlerbehandlung + +API-Endpunkte verwenden Try-Catch-Blöcke und geben strukturierte JSON-Antworten mit `message`-Feld zurück. Datenbankoperationen verwenden Transaktionen mit Rollback bei Fehlern. Verbindungen werden im `finally`-Block geschlossen. + +--- + +# Architekturentscheidungen + +Die wichtigsten Architekturentscheidungen sind als ADRs (Architecture Decision Records) dokumentiert: + +| ADR | Titel | Entscheidung | +|-----|-------|--------------| +| ADR01 | Sichere Passwortspeicherung | PBKDF2-HMAC mit SHA256, 100.000 Iterationen, zufälliges Salt, Base64-Kodierung | +| ADR02 | Intuitive Nutzbarkeit | Direkte Weiterleitung von Registrierung zum RecipeFinder (ohne erneute Anmeldung) | +| ADR03 | Verhinderung von Informationsüberladung | 3x3-Matrix für Rezeptanzeige, keine maximale Begrenzung | +| ADR04 | Architekturstil | Schichtbasierter Architekturstil (Frontend → Backend → Datenbank) | +| ADR05 | Designprinzipien | Liskov Substitution Principle (LSP) für austauschbare Speicher-Implementierungen | + +Alle ADRs sind [hier](https://github.com/GalacticCodeGambit/LazyCook/tree/4710a641f177a29e59067edf688781efb3b03a3d/docs/adr) zu finden. + +--- + +# Qualitätsanforderungen + +## Übersicht der Qualitätsanforderungen + +| Qualitätsattribut | Verfeinerung | Qualitätsszenario | Geschäftl. Nutzen | Techn. Risiko | +|-------------------|-------------|-------------------|-------------------|---------------| +| Wartbarkeit | Änderbarkeit | Entwickler möchte neues Feature hinzufügen – Änderungen innerhalb von 2 Arbeitstagen | Hoch | Hoch | +| Effizienz | Zeitverhalten | Benutzer filtert Rezepte – Anzeige innerhalb von max. 5 Sekunden | Hoch | Hoch | +| Sicherheit | Sicherheit der Nutzerdaten | Passwort wird gehasht mit Salt in der Datenbank gespeichert | Hoch | Hoch | +| Benutzbarkeit | Bedienbarkeit & Ansehnlichkeit | Registrierung bis RecipeFinder mit möglichst wenigen Klicks; 3x3 Matrix Rezeptanzeige | Mittel | Gering–Mittel | +| Kompatibilität | Interoperabilität | Anwendung läuft auf Chrome, Firefox, Safari, Edge (neueste Version, Laptop) | Hoch | Mittel | + +## Qualitätsszenarien + +| ID | Q-Attribut | Quelle | Stimulus | Artefakt | Umgebung | Antwort | Antwortmaß | +|----|-----------|--------|----------|----------|----------|---------|------------| +| 01 | Sicherheit | Benutzer | Passwort generieren | Datenbank | Normalbetrieb | Konto korrekt erstellt | Passwort gehasht in der Datenbank vorhanden | +| 02 | Benutzbarkeit | Benutzer | Suchen nach neuem Rezept | System | Normalbetrieb | Benutzer benutzt die Website produktiv | Durchklicken innerhalb von 2 min | +| 03 | Änderbarkeit | Entwickler | Website ändern / neues Feature | Code | Entwicklung | Änderung vorgenommen und getestet | 14 Stunden (2 Arbeitstage) | +| 04 | Kompatibilität | Benutzer | Ruft LazyCook Seite auf | System/Frontend | Normalbetrieb | Seite wird angezeigt | Funktioniert auf Edge, Firefox, Chrome, Opera GX (neueste Version) | +| 05 | Geschwindigkeit | Benutzer | Rezept wird gesucht | System | Normalbetrieb | Rezepte werden angezeigt | Innerhalb von max. 5 Sekunden | + +--- + +# Risiken und technische Schulden + +| Risiko/Schuld | Beschreibung | Priorität | Maßnahme | +|---------------|-------------|-----------|----------| +| SQLite-Skalierbarkeit | SQLite unterstützt keine parallelen Schreibzugriffe; bei hoher Nutzerzahl kann es zu Engpässen kommen | Mittel | Mittelfristig Migration auf PostgreSQL oder MySQL möglich dank LSP (ADR05) | +| Fehlende maximale Rezeptbegrenzung | Bei vielen Rezepten in der Datenbank gibt es keine Begrenzung der Ergebnisanzahl, was zu langen Ladezeiten führen kann (ADR03) | Mittel | Pagination implementieren | +| TODO-Kommentare im Code | Mehrere offene TODOs: Datenbank-Speicherort, E-Mail-Validierung, Passwortstärke, Error Messages generalisieren | Mittel | Systematisch abarbeiten | +| Fehlende Frontend-Tests | Aktuell nur Backend-Unittests in der CI-Pipeline, keine Frontend-Tests | Mittel | Jest/React Testing Library integrieren | + +--- + +# Glossar + +| Begriff | Definition | +|---------|-----------| +| RecipeFinder | Die Hauptseite der Anwendung, auf der Nutzer Zutaten eingeben und passende Rezepte angezeigt bekommen | +| PBKDF2-HMAC | Password-Based Key Derivation Function 2 mit Hash-based Message Authentication Code; sicheres Passwort-Hashing-Verfahren | +| Salt | Zufällig generierter Wert, der vor dem Hashing an das Passwort angehängt wird, um Rainbow-Table-Angriffe zu verhindern | +| ADR | Architecture Decision Record; dokumentierte Architekturentscheidung mit Kontext, Alternativen und Konsequenzen | +| SRS | Software Requirements Specification; Softwareanforderungsspezifikation | +| REST | Representational State Transfer; Architekturstil für Web-APIs | +| CORS | Cross-Origin Resource Sharing; Mechanismus zum Erlauben von Cross-Origin-Anfragen im Browser | +| LSP | Liskov Substitution Principle; Designprinzip, das sicherstellt, dass Unterklassen die Basisklasse ersetzen können | +| Docker Compose | Tool zur Definition und Verwaltung von Multi-Container-Docker-Anwendungen | +| FastAPI | Modernes Python-Web-Framework für den Aufbau von APIs | +| Next.js | React-Framework für serverseitiges Rendering und statische Seitengenerierung | +| SQLite | Eingebettete relationale Datenbank ohne separaten Serverprozess | +| CI/CD | Continuous Integration / Continuous Deployment; automatisierte Build- und Deployment-Pipeline | +| Gunicorn | Python WSGI HTTP Server, hier mit Uvicorn-Workern für ASGI-Support | diff --git a/docs/UML/ERM-Diagramm.drawio b/docs/UML/ERM-Diagramm.drawio new file mode 100644 index 0000000..0589fdc --- /dev/null +++ b/docs/UML/ERM-Diagramm.drawio @@ -0,0 +1,370 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/UML/ERM-Diagramm.png b/docs/UML/ERM-Diagramm.png new file mode 100644 index 0000000..37bb9f5 Binary files /dev/null and b/docs/UML/ERM-Diagramm.png differ diff --git a/docs/UML/KlassenDiagramm.drawio b/docs/UML/KlassenDiagramm.drawio index 654fecc..d16f281 100644 --- a/docs/UML/KlassenDiagramm.drawio +++ b/docs/UML/KlassenDiagramm.drawio @@ -1,10 +1,10 @@ - + - + - + @@ -12,297 +12,470 @@ - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - + - - - - - - - - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - - - + - - + + - - + + - - + + - + - + - + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/UML/KlassenDiagramm.png b/docs/UML/KlassenDiagramm.png index 0cb568c..077e934 100644 Binary files a/docs/UML/KlassenDiagramm.png and b/docs/UML/KlassenDiagramm.png differ diff --git a/docs/UML/UML-Aktivitaetsdiagramm_Rezept-filtern.drawio b/docs/UML/UML-Aktivitaetsdiagramm_Rezept-filtern.drawio index cbfa2b4..46c06c2 100644 --- a/docs/UML/UML-Aktivitaetsdiagramm_Rezept-filtern.drawio +++ b/docs/UML/UML-Aktivitaetsdiagramm_Rezept-filtern.drawio @@ -1,166 +1,166 @@ - + - + - - + + - - + + - + - + - - + + - + - - + + - - + + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - + - + - - + + - - + + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - - + + - - + + - + - - + + - - + + - - + + - + - - + + - + - + @@ -169,119 +169,158 @@ - - + + - + - - + + - + - - + + - + - - + + - + - + - - + + - + - - + + - + - + - - + + - - + + - + - - + + - + - - + + - - + + - + - - + + - + - + - - + + - + - + - - + + - + - - + + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/UML/UML-Aktivitaetsdiagramm_Rezept-filtern.png b/docs/UML/UML-Aktivitaetsdiagramm_Rezept-filtern.png index 06372d9..71ffdb0 100644 Binary files a/docs/UML/UML-Aktivitaetsdiagramm_Rezept-filtern.png and b/docs/UML/UML-Aktivitaetsdiagramm_Rezept-filtern.png differ diff --git a/project/.dockerignore b/project/.dockerignore index 94dc0c8..5c50de3 100644 --- a/project/.dockerignore +++ b/project/.dockerignore @@ -32,7 +32,7 @@ docs *.log, # Frontend -node_modules +**/node_modules npm-debug.log* # Backend diff --git a/project/Dockerfile-backend b/project/Dockerfile-backend index 27a0211..42d6818 100644 --- a/project/Dockerfile-backend +++ b/project/Dockerfile-backend @@ -11,7 +11,8 @@ COPY backend/ /app ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 +ENV PYTHONPATH=/app #EXPOSE 3000 -CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:3000", "--workers", "4", "LazyCookVerwaltung:app"] +CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:3000", "--workers", "2", "LazyCookAdministration:app"] diff --git a/project/Dockerfile-frontend b/project/Dockerfile-frontend index cf1cc7a..fc2b0e3 100644 --- a/project/Dockerfile-frontend +++ b/project/Dockerfile-frontend @@ -8,7 +8,7 @@ COPY frontend/package*.json ./ RUN npm install # Copy the rest of the application files -COPY frontend/ . +COPY frontend/ ./ RUN npm run build diff --git a/project/backend/Admin.py b/project/backend/Admin.py index 2a5bdae..673bfad 100644 --- a/project/backend/Admin.py +++ b/project/backend/Admin.py @@ -1,13 +1,13 @@ -from project.backend.Person import Person -from project.backend.Rezept import Rezept +from Person import Person +from Recipe import Recipe class Admin(Person): - def __init__(self, name: str, email: str, passwort: str): - super().__init__(name, email, passwort) - self.__rezepte = [] + def __init__(self, name: str, email: str, password: str): + super().__init__(name, email, password) + self.__ricepes = [] super().setRole("Admin") - def fuegeRezeptHinzu(self, rezept: Rezept): - self.__rezepte.append(rezept) - print(f"Rezept '{rezept.getName()}' wurde zu Benutzer '{self.name}' hinzugefügt.") + def addRecipe(self, recipe: Recipe): + self.__ricepes.append(recipe) + \ No newline at end of file diff --git a/project/backend/Benutzer.py b/project/backend/Benutzer.py deleted file mode 100644 index b4b8a37..0000000 --- a/project/backend/Benutzer.py +++ /dev/null @@ -1,9 +0,0 @@ -from project.backend.Person import Person -from project.backend.Rezept import Rezept - - -class Benutzer(Person): - def __init__(self, name: str, email: str, passwort: str): - super().__init__(name, email, passwort) - super().setRole("User") - diff --git a/project/backend/Datenbank.py b/project/backend/Database.py similarity index 54% rename from project/backend/Datenbank.py rename to project/backend/Database.py index 6edb2ad..5784a5e 100644 --- a/project/backend/Datenbank.py +++ b/project/backend/Database.py @@ -2,22 +2,22 @@ from contextlib import contextmanager from pathlib import Path -DB_PATH = Path("LazyCookDB.sqlite3") +DB_PATH = Path("/data/LazyCookDB.sqlite3") -def get_connection() -> sqlite3.Connection: +def getConnection() -> sqlite3.Connection: """Erstellt eine neue SQLite-Connection mit Row-Factory.""" con = sqlite3.connect(str(DB_PATH), check_same_thread=False) con.row_factory = sqlite3.Row con.execute("PRAGMA foreign_keys = ON") - con.execute("PRAGMA journal_mode = WAL") + #con.execute("PRAGMA journal_mode = WAL") return con @contextmanager -def get_db(): +def getDB(): """Context-Manager: öffnet Connection, committed bei Erfolg, rollt bei Fehler zurück.""" - con = get_connection() + con = getConnection() try: yield con con.commit() @@ -28,104 +28,104 @@ def get_db(): con.close() -def init_db(): +def initDB(): """Erstellt alle Tabellen, falls sie noch nicht existieren.""" - with get_db() as con: + with getDB() as con: cur = con.cursor() cur.execute(""" - CREATE TABLE IF NOT EXISTS Konto ( + CREATE TABLE IF NOT EXISTS Account ( id INTEGER PRIMARY KEY AUTOINCREMENT, email VARCHAR(250) NOT NULL UNIQUE, name TEXT NOT NULL, - hashed_password TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + hashedPassword TEXT NOT NULL, + createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) - # Refresh Tokens – pro Konto können mehrere existieren (z.B. verschiedene Geräte) + # Refresh Tokens – pro Account können mehrere existieren (z.B. verschiedene Geräte) cur.execute(""" CREATE TABLE IF NOT EXISTS RefreshToken ( id INTEGER PRIMARY KEY AUTOINCREMENT, - konto_id INTEGER NOT NULL, + AccountID INTEGER NOT NULL, token TEXT NOT NULL UNIQUE, - expires_at TIMESTAMP NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (konto_id) REFERENCES Konto (id) ON DELETE CASCADE + expiresAt TIMESTAMP NOT NULL, + createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (AccountID) REFERENCES Account (id) ON DELETE CASCADE ) """) cur.execute(""" - CREATE TABLE IF NOT EXISTS Zutat ( + CREATE TABLE IF NOT EXISTS Ingridient ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, - mengenArt VARCHAR(30) NOT NULL + amountType VARCHAR(30) NOT NULL ) """) cur.execute(""" - CREATE TABLE IF NOT EXISTS Verfasser ( + CREATE TABLE IF NOT EXISTS Author ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL ) """) cur.execute(""" - CREATE TABLE IF NOT EXISTS Rezept ( + CREATE TABLE IF NOT EXISTS Recipe ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, vid INTEGER NOT NULL, - FOREIGN KEY (vid) REFERENCES Verfasser (id) ON DELETE CASCADE + FOREIGN KEY (vid) REFERENCES Author (id) ON DELETE CASCADE ) """) cur.execute(""" - CREATE TABLE IF NOT EXISTS Besteht_Aus ( + CREATE TABLE IF NOT EXISTS Exists_from ( zid INTEGER NOT NULL, rid INTEGER NOT NULL, - menge DECIMAL(10,2) NOT NULL, - FOREIGN KEY (zid) REFERENCES Zutat (id) ON DELETE CASCADE, - FOREIGN KEY (rid) REFERENCES Rezept (id) ON DELETE CASCADE, + amount DECIMAL(10,2) NOT NULL, + FOREIGN KEY (zid) REFERENCES Ingridient (id) ON DELETE CASCADE, + FOREIGN KEY (rid) REFERENCES Recipe (id) ON DELETE CASCADE, UNIQUE (zid, rid) ) """) cur.execute(""" - CREATE TABLE IF NOT EXISTS Favoriten ( - konto_id INTEGER NOT NULL, + CREATE TABLE IF NOT EXISTS Favorites ( + AccountID INTEGER NOT NULL, rid INTEGER NOT NULL, - FOREIGN KEY (konto_id) REFERENCES Konto (id) ON DELETE CASCADE, - FOREIGN KEY (rid) REFERENCES Rezept (id) ON DELETE CASCADE, - UNIQUE (konto_id, rid) + FOREIGN KEY (AccountID) REFERENCES Account (id) ON DELETE CASCADE, + FOREIGN KEY (rid) REFERENCES Recipe (id) ON DELETE CASCADE, + UNIQUE (AccountID, rid) ) """) print("✅ Datenbank-Tabellen erfolgreich initialisiert") -# ── Konto-Operationen ────────────────────────────────────────── +# ── Account-Operationen ────────────────────────────────────────── -def create_konto(email: str, name: str, hashed_password: str) -> dict | None: - """Legt ein neues Konto an. Gibt die Konto-Daten zurück oder None bei Duplikat.""" - with get_db() as con: +def createAccount(email: str, name: str, hashedPassword: str) -> dict | None: + """Legt ein neues Account an. Gibt die Account-Daten zurück oder None bei Duplikat.""" + with getDB() as con: cur = con.cursor() - cur.execute("SELECT 1 FROM Konto WHERE email = ? LIMIT 1", (email,)) + cur.execute("SELECT 1 FROM Account WHERE email = ? LIMIT 1", (email,)) if cur.fetchone(): return None cur.execute( - "INSERT INTO Konto (email, name, hashed_password) VALUES (?, ?, ?)", - (email, name, hashed_password), + "INSERT INTO Account (email, name, hashedPassword) VALUES (?, ?, ?)", + (email, name, hashedPassword), ) return {"id": cur.lastrowid, "email": email, "name": name} -def get_konto_by_email(email: str) -> dict | None: - """Gibt Konto-Daten inkl. hashed_password zurück, oder None.""" - con = get_connection() +def getAccountByEmail(email: str) -> dict | None: + """Gibt Account-Daten inkl. hashedPassword zurück, oder None.""" + con = getConnection() try: cur = con.cursor() cur.execute( - "SELECT id, email, name, hashed_password FROM Konto WHERE email = ?", + "SELECT id, email, name, hashedPassword FROM Account WHERE email = ?", (email,), ) row = cur.fetchone() @@ -136,24 +136,24 @@ def get_konto_by_email(email: str) -> dict | None: # ── Refresh-Token-Operationen ────────────────────────────────── -def save_refresh_token(konto_id: int, token: str, expires_at: str) -> None: +def saveRefreshToken(AccountID: int, token: str, expiresAt: str) -> None: """Speichert einen neuen Refresh Token in der Datenbank.""" - with get_db() as con: + with getDB() as con: cur = con.cursor() cur.execute( - "INSERT INTO RefreshToken (konto_id, token, expires_at) VALUES (?, ?, ?)", - (konto_id, token, expires_at), + "INSERT INTO RefreshToken (AccountID, token, expiresAt) VALUES (?, ?, ?)", + (AccountID, token, expiresAt), ) -def get_refresh_token(token: str) -> dict | None: +def getRefreshToken(token: str) -> dict | None: """Gibt den Refresh-Token-Eintrag zurück, oder None.""" - con = get_connection() + con = getConnection() try: cur = con.cursor() cur.execute( - "SELECT rt.id, rt.konto_id, rt.token, rt.expires_at, k.email, k.name " - "FROM RefreshToken rt JOIN Konto k ON rt.konto_id = k.id " + "SELECT rt.id, rt.AccountID, rt.token, rt.expiresAt, k.email, k.name " + "FROM RefreshToken rt JOIN Account k ON rt.AccountID = k.id " "WHERE rt.token = ?", (token,), ) @@ -163,22 +163,30 @@ def get_refresh_token(token: str) -> dict | None: con.close() -def delete_refresh_token(token: str) -> None: +def deleteRefreshToken(token: str) -> None: """Löscht einen einzelnen Refresh Token (Logout).""" - with get_db() as con: + with getDB() as con: cur = con.cursor() cur.execute("DELETE FROM RefreshToken WHERE token = ?", (token,)) -def delete_all_refresh_tokens(konto_id: int) -> None: - """Löscht alle Refresh Tokens eines Kontos (Logout von allen Geräten).""" - with get_db() as con: +def deleteAllRefreshTokens(AccountID: int) -> None: + """Löscht alle Refresh Tokens eines Accounts (Logout von allen Geräten).""" + with getDB() as con: cur = con.cursor() - cur.execute("DELETE FROM RefreshToken WHERE konto_id = ?", (konto_id,)) + cur.execute("DELETE FROM RefreshToken WHERE AccountID = ?", (AccountID,)) -def cleanup_expired_tokens() -> None: +def deleteAccount(email: str) -> bool: + """Löscht ein Account anhand der E-Mail. Refresh Tokens werden via CASCADE mitgelöscht.""" + with getDB() as con: + cur = con.cursor() + cur.execute("DELETE FROM Account WHERE email = ?", (email,)) + return cur.rowcount > 0 + + +def cleanupExpiredTokens() -> None: """Löscht alle abgelaufenen Refresh Tokens.""" - with get_db() as con: + with getDB() as con: cur = con.cursor() - cur.execute("DELETE FROM RefreshToken WHERE expires_at < datetime('now')") \ No newline at end of file + cur.execute("DELETE FROM RefreshToken WHERE expiresAt < datetime('now')") \ No newline at end of file diff --git a/project/backend/Ingridient.py b/project/backend/Ingridient.py new file mode 100644 index 0000000..896471e --- /dev/null +++ b/project/backend/Ingridient.py @@ -0,0 +1,19 @@ +from typing import List, Dict, Any + +class Ingridient: + def __init__(self, name: str, amount: float): + self.__name = name + self.__amount = amount + + def getName(self) -> str: + return self.__name + + def setName(self, name: str): + self.__name = name + + def getAmount(self) -> float: + return self.__amount + + def setAmount(self, amount: float): + self.__amount = amount + diff --git a/project/backend/LazyCookVerwaltung.py b/project/backend/LazyCookAdministration.py similarity index 86% rename from project/backend/LazyCookVerwaltung.py rename to project/backend/LazyCookAdministration.py index 5d18696..8f7df0d 100644 --- a/project/backend/LazyCookVerwaltung.py +++ b/project/backend/LazyCookAdministration.py @@ -3,13 +3,13 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from Datenbank import init_db -from routes import router +from Database import initDB +from Routes import router @asynccontextmanager async def lifespan(app: FastAPI): - init_db() + initDB() yield diff --git a/project/backend/Person.py b/project/backend/Person.py index 4e0cb00..970cb44 100644 --- a/project/backend/Person.py +++ b/project/backend/Person.py @@ -1,11 +1,11 @@ class Person: - def __init__(self, name: str, email: str, passwort: str): + def __init__(self, name: str, email: str, password: str): self.__name = name self.__email = email - self.__passwort = passwort + self.__password = password self.__role = "User" - def speichernInDB(self) -> bool: + def saveInDB(self) -> bool: return True def getName(self) -> str: @@ -20,11 +20,11 @@ def getEmail(self) -> str: def setEmail(self, email: str): self.__email = email - def getPasswort(self) -> str: - return self.__passwort + def getPassword(self) -> str: + return self.__password - def setPasswort(self, passwort: str): - self.__passwort = passwort + def setPassword(self, password: str): + self.__password = password def getRole(self) -> str: return self.__role diff --git a/project/backend/Recipe.py b/project/backend/Recipe.py new file mode 100644 index 0000000..6578c7b --- /dev/null +++ b/project/backend/Recipe.py @@ -0,0 +1,54 @@ +from Database import Database +from Ingridient import Ingridient + + +class Recipe: + def __init__(self, name: str, ingridients: list[Ingridient], description: str): + self.__name = name + self.__Ingridienten = ingridients + self.__description = description + self.__original = "" + self.__duration = "" + self.__rating = 0 + self.__countPersons = 1 + self.__database = Database() + + def saveInDB(self) -> bool: + return True + + def getName(self) -> str: + return self.name + + def setName(self, name: str): + self.name = name + + def getOriginal(self) -> str: + return self.original + + def setOriginal(self, original: str): + self.original = original + + def getDescription(self) -> str: + return self.description + + def setDescription(self, description: str): + self.description = description + + def getRating(self) -> int: + return self.rating + + def setRating(self, rating: int): + self.rating = rating + + def getDuration(self) -> str: + return self.duration + + def setDuration(self, duration: str): + self.duration = duration + + def getIngridient(self) -> list[Ingridient]: + return self.ingridients + + def setIngridient(self, ingridients: list[Ingridient]): + self.ingridients = ingridients + diff --git a/project/backend/Rezept.py b/project/backend/Rezept.py deleted file mode 100644 index a0c09b0..0000000 --- a/project/backend/Rezept.py +++ /dev/null @@ -1,54 +0,0 @@ -from project.backend.Datenbank import Datenbank -from project.backend.Zutat import Zutat - - -class Rezept: - def __init__(self, name: str, zutaten: list[Zutat], zubereitung: str): - self.__name = name - self.__zutaten = zutaten - self.__zubereitung = zubereitung - self.__original = "" - self.__dauer = "" - self.__bewertung = 0 - self.__anzPersonen = 1 - self.__datenbank = Datenbank() - - def speichernInDB(self) -> bool: - return True - - def getName(self) -> str: - return self.name - - def setName(self, name: str): - self.name = name - - def getOriginal(self) -> str: - return self.original - - def setOriginal(self, original: str): - self.original = original - - def getZubereitung(self) -> str: - return self.zubereitung - - def setZubereitung(self, zubereitung: str): - self.zubereitung = zubereitung - - def getBewertung(self) -> int: - return self.bewertung - - def setBewertung(self, bewertung: int): - self.bewertung = bewertung - - def getDauer(self) -> str: - return self.dauer - - def setDauer(self, dauer: str): - self.dauer = dauer - - def getZutaten(self) -> list[Zutat]: - return self.zutaten - - def setZutaten(self, zutaten: list[Zutat]): - self.zutaten = zutaten - diff --git a/project/backend/User.py b/project/backend/User.py new file mode 100644 index 0000000..622cf03 --- /dev/null +++ b/project/backend/User.py @@ -0,0 +1,9 @@ +from Person import Person +from Recipe import Recipe + + +class User(Person): + def __init__(self, name: str, email: str, password: str): + super().__init__(name, email, password) + super().setRole("User") + diff --git a/project/backend/Zutat.py b/project/backend/Zutat.py deleted file mode 100644 index 041d395..0000000 --- a/project/backend/Zutat.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import List, Dict, Any - -class Zutat: - def __init__(self, name: str, menge: float): - self.__name = name - self.__menge = menge - - def getName(self) -> str: - return self.__name - - def setName(self, name: str): - self.__name = name - - def getMenge(self) -> float: - return self.__menge - - def setMenge(self, menge: float): - self.__menge = menge - diff --git a/project/backend/auth.py b/project/backend/auth.py index d1529f1..6a67389 100644 --- a/project/backend/auth.py +++ b/project/backend/auth.py @@ -13,13 +13,13 @@ import bcrypt from pydantic import BaseModel -from Datenbank import get_konto_by_email, save_refresh_token, get_refresh_token +from Database import getAccountByEmail, saveRefreshToken, getRefreshToken # ── Konfiguration ────────────────────────────────────────────── SECRET_KEY = "dein-geheimer-schlüssel-hier-ändern" # In Produktion: aus Umgebungsvariable laden! ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 10 # kurze Laufzeit für Access Token -REFRESH_TOKEN_EXPIRE_DAYS = 7 # lange Laufzeit für Refresh Token +refreshToken_EXPIRE_DAYS = 7 # lange Laufzeit für Refresh Token # ── Modelle ──────────────────────────────────────────────────── class Token(BaseModel): @@ -43,20 +43,20 @@ class LogoutRequest(BaseModel): refresh_token: str # ── Passwort-Hashing ────────────────────────────────────────── -def hash_password(password: str) -> str: +def hashPassword(password: str) -> str: return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() -def verify_password(plain: str, hashed: str) -> bool: +def verifyPassword(plain: str, hashed: str) -> bool: return bcrypt.checkpw(plain.encode(), hashed.encode()) # ── Access Token (JWT) ───────────────────────────────────────── -def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str: - to_encode = data.copy() +def createAccessToken(data: dict, expires_delta: timedelta | None = None) -> str: + toEncode = data.copy() expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)) - to_encode.update({"exp": expire}) - return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + toEncode.update({"exp": expire}) + return jwt.encode(toEncode, SECRET_KEY, algorithm=ALGORITHM) -def decode_token(token: str) -> str | None: +def decodeToken(token: str) -> str | None: """Dekodiert ein JWT und gibt die E-Mail (sub) zurück, oder None.""" try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) @@ -65,44 +65,44 @@ def decode_token(token: str) -> str | None: return None # ── Refresh Token ────────────────────────────────────────────── -def create_refresh_token(konto_id: int) -> str: +def createRefreshToken(konto_id: int) -> str: """Erstellt einen kryptografisch sicheren Refresh Token und speichert ihn in der DB.""" token = secrets.token_urlsafe(64) - expires_at = datetime.now(timezone.utc) + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) - save_refresh_token(konto_id, token, expires_at.isoformat()) + expiresAt = datetime.now(timezone.utc) + timedelta(days=refreshToken_EXPIRE_DAYS) + saveRefreshToken(konto_id, token, expiresAt.isoformat()) return token -def validate_refresh_token(token: str) -> dict | None: +def validateRefreshToken(token: str) -> dict | None: """Prüft ob ein Refresh Token gültig und nicht abgelaufen ist. Gibt Konto-Daten zurück.""" - entry = get_refresh_token(token) + entry = getRefreshToken(token) if entry is None: return None - expires_at = datetime.fromisoformat(entry["expires_at"]) - if expires_at.tzinfo is None: - expires_at = expires_at.replace(tzinfo=timezone.utc) - if expires_at < datetime.now(timezone.utc): + expiresAt = datetime.fromisoformat(entry["expiresAt"]) + if expiresAt.tzinfo is None: + expiresAt = expiresAt.replace(tzinfo=timezone.utc) + if expiresAt < datetime.now(timezone.utc): return None return entry # ── Token-Paar erstellen ────────────────────────────────────── -def create_token_pair(konto: dict) -> Token: +def createTokenPair(konto: dict) -> Token: """Erstellt ein Access + Refresh Token-Paar für ein Konto.""" - access_token = create_access_token( + access_token = createAccessToken( data={"sub": konto["email"]}, expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES), ) - refresh_token = create_refresh_token(konto["id"]) - return Token(access_token=access_token, refresh_token=refresh_token, token_type="bearer") + refreshToken = createRefreshToken(konto["id"]) + return Token(access_token=access_token, refresh_token=refreshToken, token_type="bearer") # ── Validierung ──────────────────────────────────────────────── EMAIL_RE = re.compile(r"^[^\s@]+@[^\s@]+\.[^\s@]+$") -def validate_email(email: str) -> str | None: +def validateEmail(email: str) -> str | None: if not EMAIL_RE.match(email): return "Ungültige E-Mail-Adresse." return None -def validate_password(pw: str) -> str | None: +def validatePassword(pw: str) -> str | None: if len(pw) < 8: return "Passwort muss mindestens 8 Zeichen lang sein." if not re.search(r"[A-Z]", pw): @@ -118,17 +118,17 @@ def validate_password(pw: str) -> str | None: # ── Auth-Dependency ──────────────────────────────────────────── oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login") -async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> User: +async def getCurrentUser(token: Annotated[str, Depends(oauth2_scheme)]) -> User: credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Ungültige Anmeldedaten", headers={"WWW-Authenticate": "Bearer"}, ) - email = decode_token(token) + email = decodeToken(token) if email is None: raise credentials_exception - konto = get_konto_by_email(email) + konto = getAccountByEmail(email) if konto is None: raise credentials_exception diff --git a/project/backend/routes.py b/project/backend/routes.py index 1f94e23..cf4b8e0 100644 --- a/project/backend/routes.py +++ b/project/backend/routes.py @@ -3,28 +3,31 @@ from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm -from auth import ( +from Auth import ( Token, User, UserCreate, RefreshRequest, LogoutRequest, - create_token_pair, - get_current_user, - hash_password, - validate_email, - validate_password, - validate_refresh_token, - verify_password, - create_access_token, + createTokenPair, + getCurrentUser, + hashPassword, + validateEmail, + validatePassword, + validateRefreshToken, + verifyPassword, + createAccessToken, ) -from Datenbank import ( - create_konto, - get_konto_by_email, - delete_refresh_token, - delete_all_refresh_tokens, +from Database import ( + createAccount, + getAccountByEmail, + deleteRefreshToken, + deleteAllRefreshTokens, + deleteAccount, ) +from Models import User, Token, UserCreate + router = APIRouter() @@ -34,17 +37,17 @@ async def register(user: UserCreate): if not user.name.strip(): raise HTTPException(status_code=400, detail="Name darf nicht leer sein.") - email_error = validate_email(user.email) - if email_error: - raise HTTPException(status_code=400, detail=email_error) - pw_error = validate_password(user.password) - if pw_error: - raise HTTPException(status_code=400, detail=pw_error) - - konto = create_konto( + emailError = validateEmail(user.email) + if emailError: + raise HTTPException(status_code=400, detail=emailError) + pwError = validatePassword(user.password) + if pwError: + raise HTTPException(status_code=400, detail=pwError) + + konto = createAccount( email=user.email, name=user.name, - hashed_password=hash_password(user.password), + hashedPassword=hashPassword(user.password), ) if konto is None: raise HTTPException(status_code=400, detail="E-Mail bereits registriert") @@ -53,20 +56,20 @@ async def register(user: UserCreate): @router.post("/auth/login", response_model=Token) -async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): - konto = get_konto_by_email(form_data.username) - if not konto or not verify_password(form_data.password, konto["hashed_password"]): +async def login(formData: Annotated[OAuth2PasswordRequestForm, Depends()]): + konto = getAccountByEmail(formData.username) + if not konto or not verifyPassword(formData.password, konto["hashedPassword"]): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="E-Mail oder Passwort falsch", ) - return create_token_pair(konto) + return createTokenPair(konto) @router.post("/auth/refresh", response_model=Token) async def refresh(body: RefreshRequest): """Tauscht einen gültigen Refresh Token gegen ein neues Token-Paar.""" - entry = validate_refresh_token(body.refresh_token) + entry = validateRefreshToken(body.refresh_token) if entry is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -74,23 +77,29 @@ async def refresh(body: RefreshRequest): ) # Alten Refresh Token löschen (Rotation – jeder Token ist nur einmal verwendbar) - delete_refresh_token(body.refresh_token) + deleteRefreshToken(body.refresh_token) # Konto-Daten für neues Token-Paar zusammenstellen konto = {"id": entry["konto_id"], "email": entry["email"]} - return create_token_pair(konto) + return createTokenPair(konto) @router.post("/auth/logout") async def logout(body: LogoutRequest): """Löscht den Refresh Token serverseitig → Token wird ungültig.""" - delete_refresh_token(body.refresh_token) + deleteRefreshToken(body.refresh_token) return {"detail": "Erfolgreich abgemeldet"} # ── Geschützte Endpunkte ─────────────────────────────────────── @router.get("/users/me", response_model=User) -async def read_current_user(current_user: Annotated[User, Depends(get_current_user)]): +async def readCurrentUser(currentUser: Annotated[User, Depends(getCurrentUser)]): """Nur mit gültigem Access Token erreichbar.""" - return current_user \ No newline at end of file + return currentUser + + +@router.delete("/users/me", status_code=status.HTTP_204_NO_CONTENT) +async def deleteCurrentUser(currentUser: Annotated[User, Depends(getCurrentUser)]): + """Löscht das eigene Konto inkl. aller Refresh Tokens (CASCADE).""" + deleteAccount(currentUser.email) \ No newline at end of file diff --git a/project/compose.yaml b/project/compose.yaml index 87da66d..3861840 100644 --- a/project/compose.yaml +++ b/project/compose.yaml @@ -6,9 +6,9 @@ services: ports: - "8000:8000" depends_on: - - backend + - backend-dev - backend: + backend-dev: build: context: . dockerfile: Dockerfile-backend @@ -16,14 +16,13 @@ services: environment: - PYTHONUNBUFFERED=1 volumes: - - ./backend:/app - - data:/data + - ./data:/data ports: - "3000:3000" # Persist data for containers -volumes: - data: +#volumes: +# data: #networks: # default: diff --git a/project/data/LazyCookDB.sqlite3 b/project/data/LazyCookDB.sqlite3 new file mode 100644 index 0000000..193ec1b Binary files /dev/null and b/project/data/LazyCookDB.sqlite3 differ diff --git a/project/frontend/app/profile/page.tsx b/project/frontend/app/profile/page.tsx new file mode 100644 index 0000000..9ddec4b --- /dev/null +++ b/project/frontend/app/profile/page.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useState } from "react"; +import { useAuth, fetchWithAuth } from "@/lib/auth"; +import { useRouter } from "next/navigation"; +import { ChefHat } from "lucide-react"; +import { Button } from '../components/ui/button'; + +const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000"; + +export default function Profile() { + const { user, logout } = useAuth(); + const router = useRouter(); + const [showConfirm, setShowConfirm] = useState(false); + + if (!user) return null; + + async function handleAccountDeletion() { + const res = await fetchWithAuth(`${API_URL}/users/me`, { method: "DELETE" }); + if (!res.ok) throw new Error("Konto löschen fehlgeschlagen"); + logout(); + router.push("/"); + } + + return ( +
+ {/* Navbar */} +
+
+
+ + Lazy Cook +
+ + + + +
+
+ +
+
+

Profil

+

E-Mail: {user.email}

+

Name: {user.name}

+
+
+ + {/* Bestätigungs-Modal */} + {showConfirm && ( +
setShowConfirm(false)} + > +
e.stopPropagation()} + > +

Konto löschen

+

+ Möchtest du dein Konto wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden. +

+
+ + +
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/project/frontend/app/recipeFinder/page.tsx b/project/frontend/app/recipeFinder/page.tsx index 57cc20f..0300efb 100644 --- a/project/frontend/app/recipeFinder/page.tsx +++ b/project/frontend/app/recipeFinder/page.tsx @@ -1,15 +1,19 @@ "use client"; -import { useEffect } from "react"; +import {useEffect, useRef, useState} from "react"; import { useRouter } from "next/navigation"; import { useAuth } from "@/lib/auth"; -import { ChefHat } from "lucide-react"; -import { Button } from "@/app/components/ui/button"; +import {ChefHat, LogOut} from "lucide-react"; +import { User, UserCircle } from "lucide-react"; +import "./style.css" export default function RecipeFinder() { const { user, loading, logout } = useAuth(); const router = useRouter(); + const [open, setOpen] = useState(false); + const menuRef = useRef(null); + // Nicht eingeloggt → zurück zur Startseite useEffect(() => { if (!loading && !user) { @@ -17,6 +21,16 @@ export default function RecipeFinder() { } }, [loading, user, router]); + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setOpen(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + if (loading) { return (
@@ -27,6 +41,12 @@ export default function RecipeFinder() { if (!user) return null; + const handleProfil = () => { + setOpen(false); + console.log("Profil ansehen"); + router.push("/profile"); + }; + return (
{/* Navbar */} @@ -38,19 +58,43 @@ export default function RecipeFinder() {
- Hallo, {user.name} - +
+ + + {open && ( +
+ + +
+ )} +
@@ -61,12 +105,6 @@ export default function RecipeFinder() {

Willkommen zurück, {user.name}!

-
-

Profil

-

E-Mail: {user.email}

-

Name: {user.name}

-
-

Favoriten

Du hast noch keine Rezepte gespeichert.

diff --git a/project/frontend/app/recipeFinder/style.css b/project/frontend/app/recipeFinder/style.css new file mode 100644 index 0000000..888974b --- /dev/null +++ b/project/frontend/app/recipeFinder/style.css @@ -0,0 +1,74 @@ +.account-menu { + position: relative; + display: inline-block; +} + +.account-menu__trigger { + background: none; + border: none; + cursor: pointer; + padding: 6px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.15s ease; +} + +.account-menu__trigger:hover { + background-color: #f3f4f6; +} + +.account-menu__trigger:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; +} + +.account-menu__dropdown { + position: absolute; + right: 0; + top: calc(100% + 6px); + background: #ffffff; + border: 1px solid #e5e7eb; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + min-width: 180px; + z-index: 100; + overflow: hidden; + animation: account-menu-fade-in 0.12s ease-out; +} + +.account-menu__item { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 10px 14px; + background: none; + border: none; + text-align: left; + cursor: pointer; + font-size: 14px; + color: #111827; + transition: background-color 0.1s ease; +} + +.account-menu__item:hover { + background-color: #f3f4f6; +} + +.account-menu__item:focus-visible { + outline: none; + background-color: #eff6ff; +} + +@keyframes account-menu-fade-in { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} \ No newline at end of file diff --git a/project/frontend/package-lock.json b/project/frontend/package-lock.json index b40d411..d6aaf07 100644 --- a/project/frontend/package-lock.json +++ b/project/frontend/package-lock.json @@ -12,7 +12,7 @@ "cmdk": "^1.1.1", "embla-carousel-react": "^8.6.0", "lucide-react": "^0.553.0", - "next": "16.1.7", + "next": "16.2.3", "next-auth": "^4.24.13", "radix-ui": "^1.4.3", "react": "19.2.0", @@ -25,7 +25,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", - "@types/node": "^20", + "@types/node": "^24", "@types/react": "^19.2.3", "@types/react-dom": "^19", "cmdk": "^1.1.1", @@ -1097,10 +1097,9 @@ } }, "node_modules/@next/env": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.7.tgz", - "integrity": "sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==", - "license": "MIT" + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.3.tgz", + "integrity": "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==" }, "node_modules/@next/eslint-plugin-next": { "version": "16.0.1", @@ -1113,13 +1112,12 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.7.tgz", - "integrity": "sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.3.tgz", + "integrity": "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -1129,13 +1127,12 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.7.tgz", - "integrity": "sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.3.tgz", + "integrity": "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -1145,13 +1142,12 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.7.tgz", - "integrity": "sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.3.tgz", + "integrity": "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1161,13 +1157,12 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.7.tgz", - "integrity": "sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.3.tgz", + "integrity": "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1177,13 +1172,12 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.7.tgz", - "integrity": "sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.3.tgz", + "integrity": "sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1193,13 +1187,12 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.7.tgz", - "integrity": "sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.3.tgz", + "integrity": "sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -1209,13 +1202,12 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.7.tgz", - "integrity": "sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.3.tgz", + "integrity": "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -1225,13 +1217,12 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.7.tgz", - "integrity": "sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.3.tgz", + "integrity": "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -3276,13 +3267,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz", - "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "dev": true, - "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/react": { @@ -3502,11 +3492,10 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -3542,13 +3531,12 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -3905,11 +3893,10 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4199,11 +4186,10 @@ } }, "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==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5573,11 +5559,10 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true }, "node_modules/for-each": { "version": "0.3.5", @@ -6941,11 +6926,10 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -7012,12 +6996,11 @@ "license": "MIT" }, "node_modules/next": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/next/-/next-16.1.7.tgz", - "integrity": "sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg==", - "license": "MIT", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.3.tgz", + "integrity": "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==", "dependencies": { - "@next/env": "16.1.7", + "@next/env": "16.2.3", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -7031,15 +7014,15 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.1.7", - "@next/swc-darwin-x64": "16.1.7", - "@next/swc-linux-arm64-gnu": "16.1.7", - "@next/swc-linux-arm64-musl": "16.1.7", - "@next/swc-linux-x64-gnu": "16.1.7", - "@next/swc-linux-x64-musl": "16.1.7", - "@next/swc-win32-arm64-msvc": "16.1.7", - "@next/swc-win32-x64-msvc": "16.1.7", - "sharp": "^0.34.4" + "@next/swc-darwin-arm64": "16.2.3", + "@next/swc-darwin-x64": "16.2.3", + "@next/swc-linux-arm64-gnu": "16.2.3", + "@next/swc-linux-arm64-musl": "16.2.3", + "@next/swc-linux-x64-gnu": "16.2.3", + "@next/swc-linux-x64-musl": "16.2.3", + "@next/swc-win32-arm64-msvc": "16.2.3", + "@next/swc-win32-x64-msvc": "16.2.3", + "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -7426,11 +7409,10 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -8518,11 +8500,10 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -8737,11 +8718,10 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true }, "node_modules/unrs-resolver": { "version": "1.11.1", diff --git a/project/frontend/package.json b/project/frontend/package.json index 56a3338..49a205c 100644 --- a/project/frontend/package.json +++ b/project/frontend/package.json @@ -13,7 +13,7 @@ "cmdk": "^1.1.1", "embla-carousel-react": "^8.6.0", "lucide-react": "^0.553.0", - "next": "16.1.7", + "next": "16.2.3", "next-auth": "^4.24.13", "radix-ui": "^1.4.3", "react": "19.2.0", @@ -26,7 +26,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", - "@types/node": "^20", + "@types/node": "^24", "@types/react": "^19.2.3", "@types/react-dom": "^19", "cmdk": "^1.1.1",