diff --git a/.gitignore b/.gitignore
index 0848898..4254f23 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
### Custom Files ###
backend/assets/bundle.js
-backend/staticfiles/*
+frontend/staticfiles/*
+frontend/dist/
node_modules
backend/*.env
.idea
diff --git a/API_TEST_GUIDE.md b/API_TEST_GUIDE.md
new file mode 100644
index 0000000..7fecff2
--- /dev/null
+++ b/API_TEST_GUIDE.md
@@ -0,0 +1,88 @@
+# 🔧 Test-Anleitung: Richtiges API-Endpoint testen
+
+## Das Problem
+
+```bash
+curl http://localhost:8005/api/
+# 404 Not Found HTML
+```
+
+Das ist **eigentlich OK** - `/api/` ist nur ein Prefix, kein echtes Endpoint!
+
+## Die Lösung
+
+Testen Sie mit einem echten API-Endpoint:
+
+```bash
+# ✅ RICHTIG - Login Endpoint
+curl -X POST http://localhost:8005/api/login \
+ -H "Content-Type: application/json" \
+ -d '{"username":"test","password":"test"}'
+
+# ✅ RICHTIG - User Data Endpoint
+curl http://localhost:8005/api/me
+
+# ✅ RICHTIG - Projects Endpoint
+curl http://localhost:8005/api/projects
+
+# ✅ RICHTIG - Health Check
+curl http://localhost:8005/check_health
+```
+
+## Warum `/api/` allein 404 gibt
+
+In den URLs ist `/api/` nur ein Prefix:
+
+```python
+path(webserver_path, include("webserver.urls"))
+# webserver_path = "api/"
+# → /api/ ist nur ein PREFIX!
+
+# Echte Endpoints sind:
+# /api/login ← include("webserver.urls") → path("login")
+# /api/me ← include("webserver.urls") → path("me")
+# /api/projects ← include("webserver.urls") → path("projects")
+```
+
+## Richtige Test-Kommandos
+
+```bash
+# Test 1: Login (POST)
+curl -X POST http://localhost:8005/api/login \
+ -H "Content-Type: application/json" \
+ -d '{"username":"secure-user@acme.de","password":"secure"}'
+
+# Test 2: Authentifizierte Anfrage (braucht Cookie/Token)
+curl -i http://localhost:8005/api/me
+
+# Test 3: Health Check (kein Auth nötig)
+curl http://localhost:8005/check_health
+
+# Test 4: Projects (mit Auth)
+curl http://localhost:8005/api/projects
+```
+
+## Success Criteria
+
+✅ Endpoints geben JSON zurück (nicht HTML!)
+✅ 404 für `/api/` allein ist OK (es ist nur Prefix)
+✅ 401 bei Auth-Endpoints ohne Auth ist OK
+✅ 200 mit JSON für echte Requests
+
+## Überprüfung: Backend serviert KEINE HTML mehr
+
+```bash
+# ❌ SCHLECHT: HTML Response
+
+
Not Found
+...
+
+# ✅ GUT: JSON Response
+{"detail":"Authentication credentials were not provided."}
+
+# ✅ GUT: JSON Response
+{"username":"...","email":"..."}
+```
+
+Testen Sie jetzt mit echten Endpoints!
+
diff --git "a/PROJEKT\303\234BERSICHT_3TIER.md" "b/PROJEKT\303\234BERSICHT_3TIER.md"
new file mode 100644
index 0000000..37cafde
--- /dev/null
+++ "b/PROJEKT\303\234BERSICHT_3TIER.md"
@@ -0,0 +1,203 @@
+# SecureCheckPlus Projekt-Übersicht & 3Tier-Refactoring
+
+## Projektstruktur
+
+```
+SecureCheckPlus.git/
+├── backend/ # Django Backend (3Tier)
+│ ├── analyzer/ # Sicherheitsanalyse App
+│ ├── webserver/ # Web API & User Management
+│ ├── securecheckplus/ # Django Settings & URLs
+│ ├── utilities/ # Shared Constants & Helpers
+│ ├── templates/ # Email Templates
+│ └── Dockerfile
+│
+├── frontend/ # React TypeScript Frontend
+│ └── src/
+│ ├── components/
+│ ├── page/
+│ ├── queries/ # API Client
+│ └── style/
+│
+├── adapter/ # Zusätzlicher Adapter Service
+│
+└── docker-compose-preview.yml # Testumgebung
+```
+
+## 3Tier-Architektur
+
+Die Anwendung folgt einer **3-Tier-Architektur**:
+
+### Tier 1: Presentation Layer (Frontend)
+- **React + TypeScript**
+- Container: `securecheckplus_frontend`
+- Port: 3000
+- **Komponenten:**
+ - Login-Seite
+ - Project Dashboard
+ - Report Viewer
+ - CVSS Calculator
+
+### Tier 2: Application Layer (Backend)
+- **Django 5.1.2** + REST Framework
+- Container: `securecheckplus_server`
+- Port: 8000 (intern) → 8005 (extern)
+- **Apps:**
+ - `analyzer`: CVE-Analyse und Dependency-Management
+ - `webserver`: REST API, User & Project Management
+ - `utilities`: Gemeinsame Konstanten und Helper
+
+### Tier 3: Data Layer (Database)
+- **PostgreSQL**
+- Container: `securecheckplus_db`
+- Port: 5432
+
+## Hauptkomponenten
+
+### Backend: Analyzer App (`analyzer/`)
+**Modelle:**
+- `Project`: Verwaltete Projekte
+- `Dependency`: Dependencies mit Versionierung
+- `CVEObject`: CVE Informationen (CVSS, EPSS, etc.)
+- `Report`: Verbindung zwischen Dependency und CVE mit Status
+
+**Eigenschaften:**
+- CVE-Daten-Integration (NVD API)
+- Risk-Score Berechnung basierend auf EPSS
+- Dependency Tracking pro Projekt
+- Status-Management (REVIEW, NO_THREAT, THREAT_FIXED, etc.)
+
+### Backend: Webserver App (`webserver/`)
+**Modelle:**
+- `User`: Authentifizierung & Benachrichtigungen
+- `UserWatchProject`: Many-to-Many Beziehung User ↔ Project
+
+**Funktionalitäten:**
+- REST API für Frontend
+- User Management (Favoriten, Verlauf)
+- LDAP Integration (optional)
+
+## Docker Compose Services
+
+### Development Setup (docker-compose-preview.yml)
+
+1. **securecheckplus_frontend**
+ - Build: `./frontend` (Dev-Modus)
+ - Environment: `REACT_APP_API_URL=http://localhost:8005`
+ - Exposes: Port 3000
+
+2. **securecheckplus_server**
+ - Build: `./backend` (target: dev)
+ - Environment: Django Config (SECRET_KEY, DB-Credentials, etc.)
+ - Volumes: `./backend:/backend` (Hot-Reload)
+ - Exposes: Port 8005 → 8000 (intern)
+ - Dependencies: PostgreSQL, SMTP Mailserver
+
+3. **securecheckplus_db**
+ - Image: postgres (latest)
+ - Environment: DB Credentials
+ - Exposes: Port 5432
+
+4. **smtp_mailserver**
+ - Image: maildev/maildev
+ - Purpose: Email Testing
+ - Exposes: Port 1080 (MailDev UI)
+
+## Häufige Kommandos
+
+### Backend starten/stoppen
+```bash
+# Mit Preview Compose
+docker-compose -f docker-compose-preview.yml up --build
+
+# Mit Logs
+docker-compose -f docker-compose-preview.yml logs -f securecheckplus_server
+
+# Backend-Shell betreten
+docker exec -it securecheckplus_server sh
+```
+
+### Datenbank-Operationen
+```bash
+# In der Container-Shell:
+python manage.py migrate
+python manage.py makemigrations
+python manage.py createsuperuser
+python manage.py showmigrations
+```
+
+### Tests ausführen
+```bash
+# Im Backend Container
+python manage.py test
+
+# Mit Coverage
+pytest --cov=analyzer --cov=webserver
+```
+
+## Konfiguration
+
+### Umgebungsvariablen (backend)
+- `IS_DEV`: Development-Modus (True/False)
+- `FULLY_QUALIFIED_DOMAIN_NAME`: Frontend URL (für CORS)
+- `DJANGO_SECRET_KEY`: Sicherheitsschlüssel
+- `POSTGRES_*`: Datenbankzugangsdaten
+- `NVD_API_KEY`: National Vulnerability Database API-Key
+- `ADMIN_USERNAME/PASSWORD`: Superuser-Credentials
+- `USER_USERNAME/PASSWORD`: Normaler User
+
+### LDAP Integration (Optional)
+- `LDAP_HOST`: LDAP Server
+- `LDAP_ADMIN_DN`: Admin Distinguished Name
+- `LDAP_ADMIN_PASSWORD`: Admin Passwort
+- `LDAP_USER_BASE_DN`: User Search Base
+- `LDAP_ADMIN_GROUP_DN`: Admin Group DN
+- etc.
+
+## Problembehebung
+
+### Issue: "Your models in app(s): 'analyzer' have changes"
+**Lösung:**
+```bash
+python manage.py makemigrations
+python manage.py migrate
+```
+
+### Issue: "The directory '/backend/assets' does not exist"
+**Lösung:**
+```bash
+mkdir -p backend/assets
+touch backend/assets/.gitkeep
+```
+
+### Issue: CSRF deactivated warnings
+**Erklärung:** Normal in DEV-Modus, da HTTP verwendet wird. In PROD automatisch HTTPS erzwungen.
+
+## Migrationen Management
+
+### Migrationsdatei Struktur
+- `analyzer/migrations/0001_initial.py`: Erste Migrationsdatei
+- `analyzer/migrations/0002_initial.py`: ForeignKey & Relations Setup
+
+Beim Refactoring müssen neue Migrationen erstellt werden:
+```bash
+python manage.py makemigrations analyzer webserver
+python manage.py migrate
+```
+
+## Best Practices für Entwicklung
+
+1. **Immer in der Container-Shell arbeiten** für Datenbank-Operationen
+2. **Hot-Reload aktiviert**: Änderungen am Backend werden automatisch neu geladen
+3. **Migrationen versionieren**: Nach Model-Änderungen `makemigrations` ausführen
+4. **Environment-Variablen nutzen**: Für Dev/Prod-Unterschiede
+5. **Tests schreiben**: Vor Production-Deployment testen
+
+## Nächste Schritte (3Tier-Refactoring)
+
+- [ ] Alle Migrationen generieren und testen
+- [ ] API-Layer vollständig separieren
+- [ ] Frontend-Komponenten refaktorieren
+- [ ] Authentifizierung erweitern
+- [ ] Performance-Testing durchführen
+
diff --git a/QUICK_START.md b/QUICK_START.md
new file mode 100644
index 0000000..186d782
--- /dev/null
+++ b/QUICK_START.md
@@ -0,0 +1,261 @@
+# 🚀 QUICK START: 3Tier Testing
+
+## Phase 1: Frontend Build (5 Min)
+
+```bash
+cd frontend
+npm install
+npm run build
+ls -la dist/ # Verify: index.html, app.js, login.js vorhanden
+```
+
+✅ **Erwartet:** `frontend/dist/` existiert mit Content
+
+---
+
+## Phase 2: Docker Start (3 Min)
+
+```bash
+# Alte Container stoppen
+docker-compose -f docker-compose-preview.yml down
+
+# Neu starten mit Build
+docker-compose -f docker-compose-preview.yml up --build -d
+
+# Logs anschauen
+docker-compose -f docker-compose-preview.yml logs -f
+```
+
+✅ **Erwartet:** Alle Container starten ohne Fehler
+
+---
+
+## Phase 3: Schnelle Tests (2 Min)
+
+### Frontend erreichbar?
+```bash
+curl -I http://localhost:3000/
+# Erwartet: HTTP/1.1 200 OK
+```
+
+### Backend API erreichbar?
+```bash
+curl -I http://localhost:8005/api/
+# Erwartet: HTTP/1.1 200 OK oder 401 (AUTH OK!)
+```
+
+### Backend serviert KEINE Assets?
+```bash
+curl -I http://localhost:8005/static/app.js
+# Erwartet: HTTP/1.1 404 Not Found ✅
+```
+
+### Frontend Assets vorhanden?
+```bash
+curl -I http://localhost:3000/app.js
+# Erwartet: HTTP/1.1 200 OK
+```
+
+### Migrations OK?
+```bash
+docker logs securecheckplus_server | grep -i migration
+# Erwartet: "Migrations wurden angewendet" oder "No migrations to apply"
+```
+
+---
+
+## Phase 4: Browser Test (1 Min)
+
+```
+1. Browser: http://localhost:3000
+2. Login mit:
+ - Username: secure-user@acme.de
+ - Password: secure
+3. Dashboard sollte laden
+4. F12 → Network Tab → Überprüfe Requests
+ - HTML/JS/CSS von localhost:3000 ✅
+ - API calls zu localhost:8005/api/ ✅
+```
+
+---
+
+## 🎯 Checkliste: Alles OK?
+
+- [ ] `frontend/dist/` existiert
+- [ ] Frontend Container läuft
+- [ ] Backend Container läuft
+- [ ] Keine STATICFILES_DIRS Warnings
+- [ ] Frontend erreichbar (Port 3000)
+- [ ] Backend API erreichbar (Port 8005)
+- [ ] Backend serviert KEINE Assets (404)
+- [ ] Migrations angewendet
+- [ ] Browser-Test erfolgreich
+- [ ] Logs sauber (keine Errors)
+
+---
+
+## ❌ Problem? Schnelle Fixes
+
+### Frontend nicht erreichbar (Port 3000)
+```bash
+docker logs securecheckplus_frontend
+# → Nginx Fehler? Dockerfile OK? dist/ kopiert?
+docker exec securecheckplus_frontend ls -la /usr/share/nginx/html/
+```
+
+### Backend zeigt 404 für API
+```bash
+docker logs securecheckplus_server | tail -50
+# → Django Fehler? URLs OK?
+docker exec securecheckplus_server python manage.py check
+```
+
+### Backend zeigt STATICFILES Warning
+```bash
+# Das sollte NICHT vorkommen (wir haben es entfernt)
+# Falls trotzdem: settings.py überprüfen
+docker exec securecheckplus_server grep STATICFILES_DIRS /backend/securecheckplus/settings.py
+```
+
+### Migrations fehlgeschlagen
+```bash
+docker exec securecheckplus_server python manage.py showmigrations
+docker exec securecheckplus_server python manage.py makemigrations
+docker exec securecheckplus_server python manage.py migrate
+```
+
+### Alles neu starten
+```bash
+docker-compose -f docker-compose-preview.yml down
+docker system prune -f
+docker-compose -f docker-compose-preview.yml up --build -d
+```
+
+---
+
+## 📊 Expected Output
+
+### Docker Logs (gut)
+```
+securecheckplus_server | Waiting for postgres...
+securecheckplus_server | PostgreSQL started
+securecheckplus_server | System check identified no issues.
+securecheckplus_server | No migrations to apply.
+securecheckplus_server | 0 static files copied
+securecheckplus_server | Django version 5.1.2, running development server...
+securecheckplus_frontend | nginx: master process started
+```
+
+### Docker Logs (gut - Migrations)
+```
+securecheckplus_server | Migrations created:
+securecheckplus_server | 0003_xyz.py (analyzer)
+securecheckplus_server | Running migrations...
+securecheckplus_server | Applying analyzer.0003_xyz... OK
+```
+
+### Docker Logs (NICHT OK - alte Fehler)
+```
+❌ staticfiles.W004: The directory '/backend/assets' does not exist
+❌ Your models in app(s): 'analyzer' have changes
+❌ STATICFILES_DIRS = [...]
+```
+
+---
+
+## 📋 Datei-Überprüfung
+
+```bash
+# Frontend webpack Config korrekt?
+cat frontend/webpack.common.js | grep -A2 "output:"
+
+# Frontend TypeScript Config korrekt?
+cat frontend/tsconfig.json | grep outDir
+
+# Backend Settings korrekt?
+cat backend/securecheckplus/settings.py | grep -A2 "Static files"
+
+# Backend URLs korrekt?
+cat backend/securecheckplus/urls.py | grep -A5 "urlpatterns ="
+
+# .gitignore korrekt?
+cat .gitignore | grep "frontend/dist"
+```
+
+---
+
+## 📞 Support Commands
+
+```bash
+# Status überprüfen
+docker-compose -f docker-compose-preview.yml ps
+
+# Spezifischen Container anschauen
+docker logs securecheckplus_server -n 100 # Letzte 100 Zeilen
+
+# In Container gehen
+docker exec -it securecheckplus_server sh
+
+# Backend testen
+docker exec securecheckplus_server python manage.py check
+
+# Frontend testen
+docker exec securecheckplus_frontend nginx -t
+
+# DB verbindung testen
+docker exec securecheckplus_db psql -U securecheckplus -d some-db-name -c "SELECT version();"
+
+# Alle Logs
+docker-compose -f docker-compose-preview.yml logs --tail=200
+```
+
+---
+
+## ✅ Success Criteria
+
+```
+🟢 GRÜN (Alles OK):
+✅ Frontend Port 3000 antwortet mit 200
+✅ Backend Port 8005 antwortet mit 200/401
+✅ Backend antwortet mit 404 auf /static/app.js
+✅ Migrations wurden angewendet
+✅ Keine STATICFILES_DIRS Warnings
+✅ Browser-Test erfolgreich
+
+🔴 ROT (Nicht OK):
+❌ Frontend nicht erreichbar
+❌ Backend Errors in Logs
+❌ STATICFILES_DIRS Warning vorhanden
+❌ Browser zeigt 404er
+❌ Migrations fehlgeschlagen
+```
+
+---
+
+## 🎯 Nächste Schritte nach erfolgreichem Test
+
+```bash
+# 1. Cleanup (optional)
+rm -rf backend/assets/
+
+# 2. Docker Cleanup
+docker system prune -a
+
+# 3. Alle Änderungen committen
+git add .
+git commit -m "refactor: implement 3tier architecture with separate frontend/backend assets"
+
+# 4. Production Test
+docker-compose -f docker-compose.yml build
+docker-compose -f docker-compose.yml up -d
+
+# 5. Feiern! 🎉
+echo "3Tier Architektur erfolgreich implementiert!"
+```
+
+---
+
+**Zeitschätzung für komplette Validierung: ~30 Minuten**
+
+Viel Erfolg! 🚀
+
diff --git a/README-DEV-INSTALLATION.md b/README-DEV-INSTALLATION.md
new file mode 100644
index 0000000..01888f0
--- /dev/null
+++ b/README-DEV-INSTALLATION.md
@@ -0,0 +1,25 @@
+
+

+
+
+This page describes how to install and run the application SecureCheckPlus by Accso locally
+for further development and testing SecureCheckPlus or just to have a look at it and try it out.
+
+# Running the Application using docker-compose
+
+## Prerequisites
+
+Your development environment has to meet the following criteria:
+
+* You must have a local docker demon running. This is usually done by installing
+ [Docker Desktop](https://www.docker.com/products/docker-desktop/) under Windows and macOS or a
+ [native Docker daemon](https://docs.docker.com/get-started/get-docker/) under Linux.
+* You must have [docker-compose](https://docs.docker.com/compose/install/) installed
+* You must be able to start a Docker container in your local environment.
+* You require to obtain a registration key from https://nvd.nist.gov/ if you don't have one already at hand. This is
+ necessary to download the vulnerability data from the NVD database. The registration key is free of charge.
+
+## Configuration
+
+You need to edit the docker-compose file [docker-compose-dev.yml](docker-compose-dev.yml) to set the
+nvd registration key. To do so, set the environment variable `NVD_API_KEY` in the `backend` service to you
diff --git a/README-INSTALLATION.md b/README-PROD-INSTALLATION.md
similarity index 93%
rename from README-INSTALLATION.md
rename to README-PROD-INSTALLATION.md
index b437d45..1aec5da 100644
--- a/README-INSTALLATION.md
+++ b/README-PROD-INSTALLATION.md
@@ -2,6 +2,8 @@
+This page describes how to install and run the application SecureCheckPlus by Accso in a production environment.
+
# Running the Application as Docker Container
The application SecureCheckPlus is provided as Docker image at https://hub.docker.com/u/accso. The name of the image
diff --git a/backend/.dockerignore b/backend/.dockerignore
index 2fca5d5..933df71 100644
--- a/backend/.dockerignore
+++ b/backend/.dockerignore
@@ -1,3 +1,3 @@
*.env
.coverage
-staticfiles/*
\ No newline at end of file
+../frontend/staticfiles/*
\ No newline at end of file
diff --git a/backend/analyzer/management/commands/seed_preview_data.py b/backend/analyzer/management/commands/seed_preview_data.py
new file mode 100644
index 0000000..7b6c8f9
--- /dev/null
+++ b/backend/analyzer/management/commands/seed_preview_data.py
@@ -0,0 +1,228 @@
+"""
+Django management command: seed_preview_data
+--------------------------------------------
+Creates a demo project "SecureCheckPlus" with realistic dependency and CVE
+data so the preview environment shows a populated dashboard out of the box.
+
+The command is idempotent: running it multiple times is safe.
+It is called automatically by entrypoint.sh when IS_DEV=True.
+"""
+import datetime
+
+from django.core.management.base import BaseCommand
+
+from analyzer.models import Project, Dependency, CVEObject, Report
+from utilities.constants import (
+ BaseSeverity, Status, Solution, Threshold,
+ AttackVector, AttackComplexity, PrivilegesRequired,
+ UserInteraction, ConfidentialityImpact, IntegrityImpact,
+ AvailabilityImpact, Scope,
+)
+
+
+# ---------------------------------------------------------------------------
+# Static demo data
+# ---------------------------------------------------------------------------
+
+DEMO_PROJECT = {
+ "project_id": "securecheckplus",
+ "project_name": "SecureCheckPlus",
+ "deployment_threshold": Threshold.MEDIUM.name,
+}
+
+# (dependency_name, version, package_manager, license, path)
+DEMO_DEPENDENCIES = [
+ # --- Frontend: vulnerable packages (used in test fixtures) ---
+ ("bootstrap", "3.3.6", "javascript", "MIT", "frontend/node_modules/bootstrap"),
+ ("jquery", "1.11.1", "javascript", "MIT", "frontend/node_modules/jquery"),
+ ("axios", "0.26.0", "javascript", "MIT", "frontend/node_modules/axios"),
+ # --- Frontend: clean packages ---
+ ("react", "17.0.2", "javascript", "MIT", "frontend/node_modules/react"),
+ ("react-dom", "17.0.2", "javascript", "MIT", "frontend/node_modules/react-dom"),
+ ("react-query", "3.34.16","javascript", "MIT", "frontend/node_modules/react-query"),
+ ("react-router-dom", "6.2.2", "javascript", "MIT", "frontend/node_modules/react-router-dom"),
+ ("webpack", "5.70.0", "javascript", "MIT", "frontend/node_modules/webpack"),
+ # --- Backend: Python packages ---
+ ("Django", "5.1.2", "python", "BSD-3", "backend/requirements.txt"),
+ ("djangorestframework","3.15.2", "python", "BSD-3", "backend/requirements.txt"),
+ ("django-cors-headers","4.5.0", "python", "MIT", "backend/requirements.txt"),
+ ("requests", "2.32.3", "python", "Apache-2.0", "backend/requirements.txt"),
+ ("lxml", "5.3.0", "python", "BSD-3", "backend/requirements.txt"),
+ ("whitenoise", "6.7.0", "python", "MIT", "backend/requirements.txt"),
+]
+
+# CVE data: (cve_id, base_severity, cvss, epss, description,
+# attack_vector, attack_complexity, privileges_required,
+# user_interaction, confidentiality, integrity, availability, scope,
+# recommended_url, published)
+DEMO_CVES = [
+ # --- bootstrap 3.3.6 ---
+ (
+ "CVE-2016-10735", BaseSeverity.MEDIUM.name, 6.1, 0.00384,
+ "In Bootstrap 3.x before 3.4.0, XSS is possible in the data-target attribute.",
+ AttackVector.NETWORK.value, AttackComplexity.LOW.value, PrivilegesRequired.NONE.value,
+ UserInteraction.Required.value, ConfidentialityImpact.LOW.value,
+ IntegrityImpact.LOW.value, AvailabilityImpact.NONE.value, Scope.CHANGED.value,
+ "https://github.com/twbs/bootstrap/pull/27033",
+ datetime.datetime(2019, 1, 9, tzinfo=datetime.timezone.utc),
+ ),
+ (
+ "CVE-2018-14040", BaseSeverity.MEDIUM.name, 6.1, 0.00461,
+ "In Bootstrap before 4.1.2, XSS is possible in the collapse data-parent attribute.",
+ AttackVector.NETWORK.value, AttackComplexity.LOW.value, PrivilegesRequired.NONE.value,
+ UserInteraction.Required.value, ConfidentialityImpact.LOW.value,
+ IntegrityImpact.LOW.value, AvailabilityImpact.NONE.value, Scope.CHANGED.value,
+ "https://github.com/twbs/bootstrap/issues/26630",
+ datetime.datetime(2018, 7, 13, tzinfo=datetime.timezone.utc),
+ ),
+ (
+ "CVE-2019-8331", BaseSeverity.MEDIUM.name, 6.1, 0.00512,
+ "In Bootstrap before 3.4.1 and 4.3.x before 4.3.1, XSS is possible in the "
+ "tooltip or popover data-template attribute.",
+ AttackVector.NETWORK.value, AttackComplexity.LOW.value, PrivilegesRequired.NONE.value,
+ UserInteraction.Required.value, ConfidentialityImpact.LOW.value,
+ IntegrityImpact.LOW.value, AvailabilityImpact.NONE.value, Scope.CHANGED.value,
+ "https://blog.getbootstrap.com/2019/02/13/bootstrap-4-3-1-and-3-4-1/",
+ datetime.datetime(2019, 2, 20, tzinfo=datetime.timezone.utc),
+ ),
+ # --- jquery 1.11.1 ---
+ (
+ "CVE-2015-9251", BaseSeverity.MEDIUM.name, 6.1, 0.00698,
+ "jQuery before 3.0.0 is vulnerable to Cross-site Scripting (XSS) attacks when a "
+ "cross-domain Ajax request is performed without the dataType option.",
+ AttackVector.NETWORK.value, AttackComplexity.LOW.value, PrivilegesRequired.NONE.value,
+ UserInteraction.Required.value, ConfidentialityImpact.LOW.value,
+ IntegrityImpact.LOW.value, AvailabilityImpact.NONE.value, Scope.CHANGED.value,
+ "https://github.com/jquery/jquery/commit/753d591",
+ datetime.datetime(2018, 1, 18, tzinfo=datetime.timezone.utc),
+ ),
+ (
+ "CVE-2019-11358", BaseSeverity.MEDIUM.name, 6.1, 0.01174,
+ "jQuery before 3.4.0 mishandles jQuery.extend(true, {}, ...) because of "
+ "Object.prototype pollution, allowing attackers to modify Object.prototype.",
+ AttackVector.NETWORK.value, AttackComplexity.LOW.value, PrivilegesRequired.NONE.value,
+ UserInteraction.Required.value, ConfidentialityImpact.LOW.value,
+ IntegrityImpact.LOW.value, AvailabilityImpact.NONE.value, Scope.CHANGED.value,
+ "https://github.com/jquery/jquery/commit/753d591",
+ datetime.datetime(2019, 4, 20, tzinfo=datetime.timezone.utc),
+ ),
+ (
+ "CVE-2020-11022", BaseSeverity.MEDIUM.name, 6.9, 0.01987,
+ "In jQuery versions greater than or equal to 1.2 and before 3.5.0, "
+ "passing HTML from untrusted sources to jQuery's DOM manipulation methods may execute untrusted code.",
+ AttackVector.NETWORK.value, AttackComplexity.LOW.value, PrivilegesRequired.NONE.value,
+ UserInteraction.Required.value, ConfidentialityImpact.LOW.value,
+ IntegrityImpact.LOW.value, AvailabilityImpact.NONE.value, Scope.CHANGED.value,
+ "https://github.com/jquery/jquery/security/advisories/GHSA-gxr4-xjj5-5px2",
+ datetime.datetime(2020, 4, 29, tzinfo=datetime.timezone.utc),
+ ),
+ # --- axios 0.26.0 ---
+ (
+ "CVE-2021-3749", BaseSeverity.HIGH.name, 7.5, 0.00433,
+ "axios before 0.21.2 is vulnerable to Regular Expression Denial of Service (ReDoS) "
+ "via the trim function.",
+ AttackVector.NETWORK.value, AttackComplexity.LOW.value, PrivilegesRequired.NONE.value,
+ UserInteraction.NONE.value, ConfidentialityImpact.NONE.value,
+ IntegrityImpact.NONE.value, AvailabilityImpact.HIGH.value, Scope.UNCHANGED.value,
+ "https://github.com/axios/axios/releases/tag/v0.21.2",
+ datetime.datetime(2021, 8, 31, tzinfo=datetime.timezone.utc),
+ ),
+]
+
+# Maps cve_id → (dep_name, dep_version, status, solution, comment)
+DEMO_REPORTS = [
+ ("CVE-2016-10735", "bootstrap", "3.3.6", Status.THREAT.name, Solution.CHANGE_VERSION, "Upgrade to bootstrap >= 3.4.1."),
+ ("CVE-2018-14040", "bootstrap", "3.3.6", Status.REVIEW.name, Solution.NO_SOLUTION_NEEDED, ""),
+ ("CVE-2019-8331", "bootstrap", "3.3.6", Status.THREAT_FIXED.name, Solution.CHANGE_VERSION, "Fixed by upgrading to 4.x in next sprint."),
+ ("CVE-2015-9251", "jquery", "1.11.1", Status.THREAT.name, Solution.CHANGE_VERSION, "Upgrade to jquery >= 3.5.0."),
+ ("CVE-2019-11358", "jquery", "1.11.1", Status.REVIEW.name, Solution.NO_SOLUTION_NEEDED, ""),
+ ("CVE-2020-11022", "jquery", "1.11.1", Status.THREAT_WIP.name, Solution.CHANGE_VERSION, "Migration to jquery 3.x in progress."),
+ ("CVE-2021-3749", "axios", "0.26.0", Status.THREAT.name, Solution.CHANGE_VERSION, "Upgrade to axios >= 0.21.2."),
+]
+
+
+class Command(BaseCommand):
+ help = "Seeds the preview database with a demo 'SecureCheckPlus' project and sample vulnerability data."
+
+ def handle(self, *args, **options):
+ self.stdout.write("Seeding preview data …")
+
+ # --- Project ---
+ project, created = Project.objects.get_or_create(
+ project_id=DEMO_PROJECT["project_id"],
+ defaults={
+ "project_name": DEMO_PROJECT["project_name"],
+ "deployment_threshold": DEMO_PROJECT["deployment_threshold"],
+ },
+ )
+ if created:
+ self.stdout.write(f" ✓ Created project '{project.project_id}'")
+ else:
+ self.stdout.write(f" · Project '{project.project_id}' already exists – skipping.")
+
+ # --- Dependencies ---
+ dep_map: dict[tuple, Dependency] = {}
+ for dep_name, version, pkg_mgr, lic, path in DEMO_DEPENDENCIES:
+ dep, dep_created = Dependency.objects.get_or_create(
+ project=project,
+ dependency_name=dep_name,
+ version=version,
+ defaults={
+ "package_manager": pkg_mgr,
+ "license": lic,
+ "path": path,
+ "in_use": True,
+ },
+ )
+ dep_map[(dep_name, version)] = dep
+ if dep_created:
+ self.stdout.write(f" ✓ Dependency {dep_name}@{version}")
+
+ # --- CVE Objects ---
+ cve_map: dict[str, CVEObject] = {}
+ for (cve_id, severity, cvss, epss, desc,
+ av, ac, pr, ui, ci, ii, ai, scope, url, published) in DEMO_CVES:
+ cve, cve_created = CVEObject.objects.get_or_create(
+ cve_id=cve_id,
+ defaults={
+ "base_severity": severity,
+ "cvss": cvss,
+ "epss": epss,
+ "description": desc,
+ "attack_vector": av,
+ "attack_complexity": ac,
+ "privileges_required": pr,
+ "user_interaction": ui,
+ "confidentiality_impact": ci,
+ "integrity_impact": ii,
+ "availability_impact": ai,
+ "scope": scope,
+ "recommended_url": url,
+ "published": published,
+ "updated": published,
+ },
+ )
+ cve_map[cve_id] = cve
+ if cve_created:
+ self.stdout.write(f" ✓ CVE {cve_id}")
+
+ # --- Reports ---
+ for cve_id, dep_name, dep_version, status, solution, comment in DEMO_REPORTS:
+ dep = dep_map.get((dep_name, dep_version))
+ cve = cve_map.get(cve_id)
+ if not dep or not cve:
+ continue
+ _, report_created = Report.objects.get_or_create(
+ dependency=dep,
+ cve_object=cve,
+ defaults={
+ "status": status,
+ "solution": solution,
+ "comment": comment,
+ },
+ )
+ if report_created:
+ self.stdout.write(f" ✓ Report {dep_name}@{dep_version} → {cve_id} [{status}]")
+
+ self.stdout.write(self.style.SUCCESS("Preview data seeding complete."))
+
diff --git a/backend/assets/.gitkeep b/backend/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/backend/assets/icons/GitHubIcon.svg b/backend/assets/icons/GitHubIcon.svg
deleted file mode 100644
index 37fa923..0000000
--- a/backend/assets/icons/GitHubIcon.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/backend/assets/icons/arrowDoubleDownIcon.svg b/backend/assets/icons/arrowDoubleDownIcon.svg
deleted file mode 100644
index 0c44194..0000000
--- a/backend/assets/icons/arrowDoubleDownIcon.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/backend/assets/icons/eye.svg b/backend/assets/icons/eye.svg
deleted file mode 100644
index 8f2a6fd..0000000
--- a/backend/assets/icons/eye.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/backend/assets/icons/eyeCrossed.svg b/backend/assets/icons/eyeCrossed.svg
deleted file mode 100644
index 373b38a..0000000
--- a/backend/assets/icons/eyeCrossed.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/backend/assets/icons/favicon.ico b/backend/assets/icons/favicon.ico
deleted file mode 100644
index a45571e..0000000
Binary files a/backend/assets/icons/favicon.ico and /dev/null differ
diff --git a/backend/assets/icons/flag_de.svg b/backend/assets/icons/flag_de.svg
deleted file mode 100644
index 60e5efc..0000000
--- a/backend/assets/icons/flag_de.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/backend/assets/icons/flag_gb.svg b/backend/assets/icons/flag_gb.svg
deleted file mode 100644
index 82b1fad..0000000
--- a/backend/assets/icons/flag_gb.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/backend/assets/images/SecureCheckPlusLogoHorizontal.png b/backend/assets/images/SecureCheckPlusLogoHorizontal.png
deleted file mode 100644
index 253205c..0000000
Binary files a/backend/assets/images/SecureCheckPlusLogoHorizontal.png and /dev/null differ
diff --git a/backend/assets/images/SecureCheckPlusLogoVertical.svg b/backend/assets/images/SecureCheckPlusLogoVertical.svg
deleted file mode 100644
index 210dbe4..0000000
--- a/backend/assets/images/SecureCheckPlusLogoVertical.svg
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
\ No newline at end of file
diff --git a/backend/assets/images/SecureCheckPlusLogoVertical500x500.png b/backend/assets/images/SecureCheckPlusLogoVertical500x500.png
deleted file mode 100644
index 3913dba..0000000
Binary files a/backend/assets/images/SecureCheckPlusLogoVertical500x500.png and /dev/null differ
diff --git a/backend/assets/images/error404.gif b/backend/assets/images/error404.gif
deleted file mode 100644
index cd58daf..0000000
Binary files a/backend/assets/images/error404.gif and /dev/null differ
diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh
index 3049d70..deb75bc 100644
--- a/backend/entrypoint.sh
+++ b/backend/entrypoint.sh
@@ -5,8 +5,15 @@ while ! nc -z $POSTGRES_HOST $POSTGRES_PORT; do sleep 1; done;
echo "PostgreSQL started"
python manage.py createcachetable rate_limit
+python manage.py makemigrations
python manage.py migrate
+# Collect Django admin staticfiles (not frontend assets - those are served by Nginx in the frontend container)
python manage.py collectstatic --no-input
+# In dev/preview mode: seed a demo project so the dashboard is not empty on first start
+if [ "$IS_DEV" = "True" ]; then
+ python manage.py seed_preview_data
+fi
+
exec "$@"
diff --git a/backend/securecheckplus/settings.py b/backend/securecheckplus/settings.py
index 13df1d2..4ea95ae 100644
--- a/backend/securecheckplus/settings.py
+++ b/backend/securecheckplus/settings.py
@@ -214,8 +214,9 @@ def get_env_variable_or_shutdown_gracefully(var_name):
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
+# Note: In 3Tier architecture, static files are served by the frontend container (Nginx)
+# The backend only serves Django admin staticfiles
-STATICFILES_DIRS = [os.path.join(BASE_DIR, "assets")]
STATIC_URL = f"/{BASE_URL}/static/" if BASE_URL else "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") # Location where the staticfiles will be collected
@@ -296,6 +297,9 @@ def format(self, record):
CSRF_TRUSTED_ORIGINS = [
FULLY_QUALIFIED_DOMAIN_NAME,
]
+# Required for cross-origin requests with credentials (cookies/session).
+# Needed when REACT_APP_API_URL points directly to the backend port (preview setup).
+CORS_ALLOW_CREDENTIALS = True
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_SAVE_EVERY_REQUEST = True
diff --git a/backend/securecheckplus/urls.py b/backend/securecheckplus/urls.py
index d34eacb..fd654d4 100644
--- a/backend/securecheckplus/urls.py
+++ b/backend/securecheckplus/urls.py
@@ -16,15 +16,22 @@
import os
from django.conf import settings
-from django.conf.urls.static import static
-from django.contrib.staticfiles.urls import staticfiles_urlpatterns
-from django.http import HttpResponse
+from django.http import HttpResponse, JsonResponse
from django.urls import path, include, re_path
from django.views.generic.base import TemplateView
from analyzer.views import AnalyzeReport, health_endpoint
from securecheckplus.settings import BASE_URL
-from webserver.views.misc_views import AppView, HtmlView
+
+
+def api_404_view(request, exception=None):
+ """Return JSON 404 for API requests instead of HTML"""
+ return JsonResponse(
+ {"detail": "Not found."},
+ status=404,
+ content_type="application/json"
+ )
+
webserver_path = f"{BASE_URL}/api/" if BASE_URL else "api/"
analyzer_path = f"{BASE_URL}/analyzer/api" if BASE_URL else "analyzer/api"
@@ -35,13 +42,15 @@
path("check_health", health_endpoint),
path(webserver_path, include("webserver.urls")),
path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")),
- re_path(rf'{base_url_pattern}html/(?P[-a-z_A-Z0-9]+)\.html$', HtmlView.as_view()),
- re_path(rf'{base_url_pattern}(?:.*)/?$', AppView.as_view()),
+ # HTML views removed in 3Tier architecture - Frontend is now served by Nginx, not Django
+ # re_path(rf'{base_url_pattern}html/(?P[-a-z_A-Z0-9]+)\.html$', HtmlView.as_view()),
+ # re_path(rf'{base_url_pattern}(?:.*)/?$', AppView.as_view()),
]
-# Serving the media files in development mode
-if settings.IS_DEV:
- urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
- urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
-else:
- urlpatterns += staticfiles_urlpatterns()
+# Serving static files is removed in 3Tier architecture
+# Frontend container (Nginx) serves all frontend assets
+# Backend only serves Django admin static files via collectstatic
+# (no additional static() URL patterns needed)
+
+handler404 = api_404_view
+
diff --git a/backend/webserver/views/misc_views.py b/backend/webserver/views/misc_views.py
index bb71ea4..b761dcc 100644
--- a/backend/webserver/views/misc_views.py
+++ b/backend/webserver/views/misc_views.py
@@ -2,7 +2,6 @@
import os
import traceback
-from django.shortcuts import render
from django.views import View
from rest_framework.exceptions import APIException
from rest_framework.permissions import IsAuthenticated
@@ -21,51 +20,9 @@
logger = logging.getLogger(__name__)
-class HtmlView(View):
-
-
- def get(self, request, template_name):
-
- PREFIX = "/"
-
- if not IS_DEV and BASE_URL:
- PREFIX = f"/{BASE_URL}/static/"
- elif not IS_DEV and not BASE_URL:
- PREFIX = "/static/"
-
- context = {
- 'IS_DEV': IS_DEV,
- 'BASE_URL': "/" + BASE_URL if BASE_URL else "",
- 'PREFIX': PREFIX
- }
-
- if request.user.is_authenticated:
- return render(request, f"includes/{template_name}.html", context)
- else:
- return render(request, "login.html", context)
-
-
-class AppView(View):
- def get(self, request):
-
- PREFIX = "/"
-
- if not IS_DEV and BASE_URL:
- PREFIX = f"/{BASE_URL}/static/"
- elif not IS_DEV and not BASE_URL:
- PREFIX = "/static/"
-
- context = {
- 'IS_DEV': IS_DEV,
- 'BASE_URL': BASE_URL,
- 'PREFIX': PREFIX
- }
-
- if request.user.is_authenticated:
- return render(request, "app.html", context)
- else:
- return render(request, "login.html", context)
-
+# HtmlView and AppView have been removed as part of the 2-Tier → 3-Tier migration.
+# The frontend is now served by the Nginx container (frontend/Dockerfile).
+# Django exclusively provides REST endpoints.
class DependenciesAPI(APIView):
permission_classes = [IsAuthenticated]
diff --git a/docker-compose-preview.yml b/docker-compose-preview.yml
new file mode 100644
index 0000000..3037e1c
--- /dev/null
+++ b/docker-compose-preview.yml
@@ -0,0 +1,66 @@
+services:
+
+ securecheckplus_frontend:
+ container_name: securecheckplus_frontend
+ build:
+ context: ./frontend
+ args:
+ # Leave empty so the frontend uses the Nginx proxy (same origin, port 3000).
+ # Set to "http://localhost:8005" only if you want to bypass Nginx and call
+ # the backend directly (requires CORS_ALLOW_CREDENTIALS=True on the backend).
+ REACT_APP_API_URL: ""
+ ports:
+ - "3000:80"
+ depends_on:
+ - securecheckplus_server
+
+ securecheckplus_server:
+ container_name: securecheckplus_server
+ user: "${RUNNER_UID}:${GID}"
+ build:
+ context: ./backend
+ target: dev
+ environment:
+ - IS_DEV=True
+ # The frontend is served on http://localhost:3000 in this compose preview setup.
+ # Set FULLY_QUALIFIED_DOMAIN_NAME to the frontend origin so Django's
+ # CORS_ALLOWED_ORIGINS and CSRF_TRUSTED_ORIGINS include the browser origin.
+ - FULLY_QUALIFIED_DOMAIN_NAME=http://localhost:3000
+ - USER_USERNAME=secure-user@acme.de
+ - USER_PASSWORD=secure
+ - ADMIN_USERNAME=secure-admin@acme.de
+ - ADMIN_PASSWORD=secure
+ - POSTGRES_HOST=securecheckplus_db
+ - POSTGRES_USER=securecheckplus
+ - POSTGRES_DB=some-db-name
+ - POSTGRES_PORT=5432
+ - POSTGRES_PASSWORD="some-secure-password"
+ - NVD_API_KEY="PLACEHOLDER_FOR_NVD_API_KEY"
+ - DJANGO_SECRET_KEY="SOME-RANDOM-KEY"
+ - SALT="SOME-RANDOM-SALT"
+ volumes:
+ - "./backend:/backend"
+ ports:
+ - "8005:8000"
+ depends_on:
+ - securecheckplus_db
+ - smtp_mailserver
+
+ securecheckplus_db:
+ image: postgres
+ restart: always
+ container_name: securecheckplus_db
+ environment:
+ - POSTGRES_HOST=securecheckplus_db
+ - POSTGRES_USER=securecheckplus
+ - POSTGRES_DB=some-db-name
+ - POSTGRES_PORT=5432
+ - POSTGRES_PASSWORD="some-secure-password"
+ ports:
+ - "5432:5432"
+
+ smtp_mailserver:
+ image: maildev/maildev
+ container_name: mailserver
+ ports:
+ - "1080:80"
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
new file mode 100644
index 0000000..d374d3c
--- /dev/null
+++ b/frontend/Dockerfile
@@ -0,0 +1,27 @@
+# Stage 1: Build
+FROM node:18-alpine AS build
+WORKDIR /app
+
+# Accept REACT_APP_API_URL as build arg and expose as env for webpack DefinePlugin
+ARG REACT_APP_API_URL=''
+ENV REACT_APP_API_URL=${REACT_APP_API_URL}
+
+COPY package.json yarn.lock* package-lock.json* ./
+RUN yarn install --frozen-lockfile || npm install
+COPY . .
+RUN yarn build || npm run build
+
+# Stage 2: Serve
+FROM nginx:alpine
+# Copy build output from Webpack (dist directory)
+COPY --from=build /app/dist /usr/share/nginx/html
+
+# Also copy Django-managed staticfiles (icons, images, rest_framework) into the nginx html root
+# The build context for this Dockerfile is the repo root, so backend/staticfiles is available.
+COPY /staticfiles/icons /usr/share/nginx/html/static/icons
+COPY /staticfiles/images /usr/share/nginx/html/static/images
+COPY /staticfiles/rest_framework /usr/share/nginx/html/static/rest_framework
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/frontend/nginx.conf b/frontend/nginx.conf
new file mode 100644
index 0000000..cd1ccad
--- /dev/null
+++ b/frontend/nginx.conf
@@ -0,0 +1,37 @@
+server {
+ listen 80;
+ server_name localhost;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ # Login page – serve the dedicated login bundle
+ location = /login {
+ try_files /login.html =404;
+ }
+ location = /login.html {
+ # No caching so the browser always gets the latest login page
+ add_header Cache-Control "no-cache, no-store, must-revalidate";
+ }
+
+ # All authenticated app routes – serve the main app bundle (SPA fallback)
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ # Proxy API requests to the backend service inside the docker network
+ location /api/ {
+ # Keep the /api prefix so Django routes to webserver.urls correctly.
+ proxy_pass http://securecheckplus_server:8000;
+ proxy_http_version 1.1;
+ 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_buffering off;
+ }
+
+ error_page 500 502 503 504 /50x.html;
+ location = /50x.html {
+ root /usr/share/nginx/html;
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
index c083bb8..1072372 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -5,7 +5,8 @@
"main": "index.js",
"scripts": {
"prod": "webpack --config webpack.prod.js --mode production",
- "dev": "webpack-dev-server --config webpack.dev.js --mode development"
+ "dev": "webpack-dev-server --config webpack.dev.js --mode development",
+ "build": "webpack --config webpack.prod.js --mode production"
},
"repository": {
"type": "git",
diff --git a/frontend/src/components/LoginBox.tsx b/frontend/src/components/LoginBox.tsx
index 06900ab..bac6f60 100644
--- a/frontend/src/components/LoginBox.tsx
+++ b/frontend/src/components/LoginBox.tsx
@@ -11,6 +11,7 @@ import Typography from "@mui/material/Typography";
import {localStorageItemKeys, urlAddress} from "../utilities/constants";
import ImageDropDown from "./ImageDropDown";
import {getSupportedLanguages} from "../utilities/supportedLanguages";
+import apiClient from "../queries/apiClient";
/**
* The login box at the login page. Takes the user inputs and requests an authentication process
@@ -27,46 +28,83 @@ const LoginBox: React.FunctionComponent = () => {
let allLanguages = getSupportedLanguages();
let defaultLanguageIndex = allLanguages.abbreviations.indexOf(defaultLanguage ? defaultLanguage : "");
+ // Debug: component mounted
+ console.debug && console.debug("LoginBox mounted", { usernameInit: username, passwordInit: password });
+
/**
* If submit button is pressed redirect request to AuthProvider.
* Submits on enter or on click.
* @param clicked
* @param e
*/
- const onConfirm = (clicked: boolean = false, e?: React.KeyboardEvent): void => {
+ const onConfirm = async (clicked: boolean = false, e?: React.KeyboardEvent): Promise => {
+ console.debug("LoginBox.onConfirm called", { clicked, key: e?.key, username, password, stayLoggedIn });
// @ts-ignore handled by first statement
- if ((e === undefined && clicked) || (e.key === "Enter")) {
- if (username.includes("@")) {
- login({
- username: username,
- password: password,
- keepMeLoggedIn: stayLoggedIn,
- }).then(reloadPage).catch(() => {
- notification.error(localization.notificationMessage.incorrectLogin)
- })
+ if ((e === undefined && clicked) || (e && e.key === "Enter")) {
+ // Previously we required an '@' in the username (email). Some deployments/users use plain
+ // usernames — allow any non-empty username so the request is triggered. Keep a minimal check.
+ if (username && username.trim().length > 0) {
+ // CSRF-Token vorab holen – Fehler werden ignoriert,
+ // da der Login-POST auch ohne vorheriges Cookie funktioniert.
+ // GET /api/login liefert 405 (nur POST erlaubt), setzt aber das csrftoken-Cookie.
+ try {
+ console.debug("Fetching CSRF token via GET api/login ...");
+ await apiClient.get(urlAddress.api.login);
+ } catch (csrfErr: any) {
+ // 405 Method Not Allowed ist erwartet – csrftoken-Cookie wird trotzdem gesetzt.
+ console.debug("CSRF pre-fetch finished (status ignored):", csrfErr?.response?.status);
+ }
+
+ try {
+ console.debug("Calling login() with", { username, stayLoggedIn });
+ await login({
+ username: username,
+ password: password,
+ keepMeLoggedIn: stayLoggedIn,
+ });
+ console.debug("login() successful");
+ reloadPage();
+ } catch (err: any) {
+ console.error("Login failed:", err);
+ if (err?.response?.status === 401 || err?.response?.status === 403) {
+ notification.error(localization.notificationMessage.incorrectLogin);
+ } else {
+ notification.error(localization.notificationMessage.serverError || "Server error");
+ }
+ }
} else {
+ console.debug("username empty", username);
notification.warn(localization.notificationMessage.usernameIsNotMail)
}
}
}
const reloadPage = () => {
- window.location.reload();
+ // Navigate to the main app (loads app.js bundle via nginx → index.html)
+ window.location.href = '/';
}
- return onConfirm(false, e)}
- >
-
-
-
+ // New: submit handler for semantic form submit
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ console.debug('Login form submitted', { username, password, stayLoggedIn });
+ // Call existing onConfirm to run the login flow (clicked=true -> triggers login attempt)
+ await onConfirm(true);
+ }
+
+ return (
+
+ )
}
diff --git a/frontend/src/context/ApiClientProvider.tsx b/frontend/src/context/ApiClientProvider.tsx
index 275a3fd..a172079 100644
--- a/frontend/src/context/ApiClientProvider.tsx
+++ b/frontend/src/context/ApiClientProvider.tsx
@@ -7,7 +7,19 @@ const ApiClientProvider: React.FC<{ children: React.ReactNode }> = ({ children }
const [loading, setLoading] = React.useState(true);
useEffect(() => {
- apiClient.defaults.baseURL = location.protocol + '//' + location.host + "/" + (baseUrl ? `${baseUrl}/api/` : "api/");
+ const envApiUrlRaw = process.env.REACT_APP_API_URL || '';
+ // Normalize env var: remove any trailing /api or /api/ and ensure trailing slash
+ const envApiUrl = envApiUrlRaw.replace(/\/api\/?$/,'').trim();
+ if (envApiUrl) {
+ apiClient.defaults.baseURL = envApiUrl.endsWith('/') ? envApiUrl : envApiUrl + '/';
+ } else {
+ // Use origin (with optional baseUrl) but DO NOT include the 'api' segment here —
+ // endpoints defined in urlAddress already include the 'api/' prefix.
+ apiClient.defaults.baseURL = location.protocol + '//' + location.host + "/" + (baseUrl ? `${baseUrl}/` : "");
+ }
+ // Expose for debugging in browser devtools
+ try { (window as any).__API_BASE__ = apiClient.defaults.baseURL } catch (e) { /* ignore in non-browser env */ }
+ console.debug && console.debug("ApiClientProvider: resolved api base url:", apiClient.defaults.baseURL);
setLoading(false);
}, [baseUrl]);
diff --git a/frontend/src/context/UserContext.tsx b/frontend/src/context/UserContext.tsx
index e9b301f..2da5746 100644
--- a/frontend/src/context/UserContext.tsx
+++ b/frontend/src/context/UserContext.tsx
@@ -14,16 +14,22 @@ function UserContextProvider({children}: { children: React.ReactNode }) {
const [userGroups, setUserGroups] = React.useState([groups.basic.id]);
const [username, setUsername] = React.useState("");
- const {data: userData, isError, isSuccess} = useQuery("userData", getUserData)
+ const {data: userData, error, isError, isSuccess} = useQuery("userData", getUserData)
useEffect(() => {
if (isSuccess) {
setUsername(userData?.data.username);
setUserGroups(userData?.data.groups);
} else if (isError) {
- notification.error(localization.notificationMessage.errorUserDataFetch);
+ // 401 = not authenticated → redirect to login page
+ const status = (error as any)?.response?.status;
+ if (status === 401 || status === 403) {
+ window.location.href = '/login';
+ } else {
+ notification.error(localization.notificationMessage.errorUserDataFetch);
+ }
}
- }, [userData])
+ }, [userData, isError])
/**
* Checks if the user has at least one of the given group
diff --git a/frontend/src/index.html b/frontend/src/index.html
new file mode 100644
index 0000000..52645fc
--- /dev/null
+++ b/frontend/src/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+ SecureCheckPlus
+
+
+
+
+
+
diff --git a/frontend/src/login.html b/frontend/src/login.html
new file mode 100644
index 0000000..c61b7f1
--- /dev/null
+++ b/frontend/src/login.html
@@ -0,0 +1,11 @@
+
+
+
+
+ SecureCheckPlus – Login
+
+
+
+
+
+
diff --git a/frontend/src/page/ReportOverview.tsx b/frontend/src/page/ReportOverview.tsx
index 3f45dd9..4bee49c 100644
--- a/frontend/src/page/ReportOverview.tsx
+++ b/frontend/src/page/ReportOverview.tsx
@@ -219,7 +219,11 @@ const ReportOverview: React.FunctionComponent = () => {
description: localization.ReportPage.toolTips.epss,
renderCell: (params: GridRenderCellParams) => (
- {params.value === 0 ? "N/A" : (params.value * 100).toFixed(2) + "%"}
+
+ {typeof params.value === "number" && params.value !== undefined && params.value !== 0
+ ? (params.value * 100).toFixed(2) + "%"
+ : "N/A"}
+
),
valueGetter: params => params.row.cveObject.epss,
diff --git a/frontend/src/queries/apiClient.ts b/frontend/src/queries/apiClient.ts
new file mode 100644
index 0000000..0608336
--- /dev/null
+++ b/frontend/src/queries/apiClient.ts
@@ -0,0 +1,9 @@
+import axios from 'axios';
+
+const apiClient = axios.create({
+ withCredentials: true, // Wichtig für CSRF-Token-Cookie-Handling
+ xsrfCookieName: 'csrftoken', // Django's Standard-CSRF-Cookie-Name
+ xsrfHeaderName: 'X-CSRFToken', // Django's Standard-CSRF-Header-Name
+});
+
+export default apiClient;
diff --git a/frontend/src/utilities/constants.tsx b/frontend/src/utilities/constants.tsx
index 9aedcb9..0fa6e53 100644
--- a/frontend/src/utilities/constants.tsx
+++ b/frontend/src/utilities/constants.tsx
@@ -11,25 +11,25 @@ export const localStorageItemKeys = {
export const urlAddress = {
api: {
- login: "login",
- logout: "logout",
- me: "me",
- myFavorite: "myFavorites",
- projectsFlat: "projectsFlat",
- projects: "projects",
- deleteProjects: "deleteProjects",
- projectGroups: "projectGroups",
- createProject: (projectId: string) => "projects/" + projectId,
- project: (projectId: string) => "projects/" + projectId,
- projectAPIKey: (projectId: string) => "projects/" + projectId + "/apiKey",
- projectDependencies: (projectId: string) => "projects/" + projectId + "/dependencies",
- projectReports: (projectId: string) => "projects/" + projectId + "/reports",
- projectUpdateCVEs: (projectId: string) => "projects/" + projectId + "/updateCVE",
+ login: "api/login",
+ logout: "api/logout",
+ me: "api/me",
+ myFavorite: "api/myFavorites",
+ projectsFlat: "api/projectsFlat",
+ projects: "api/projects",
+ deleteProjects: "api/deleteProjects",
+ projectGroups: "api/projectGroups",
+ createProject: (projectId: string) => "api/projects/" + projectId,
+ project: (projectId: string) => "api/projects/" + projectId,
+ projectAPIKey: (projectId: string) => "api/projects/" + projectId + "/apiKey",
+ projectDependencies: (projectId: string) => "api/projects/" + projectId + "/dependencies",
+ projectReports: (projectId: string) => "api/projects/" + projectId + "/reports",
+ projectUpdateCVEs: (projectId: string) => "api/projects/" + projectId + "/updateCVE",
report: (projectId: string,
- reportId: string) => "projects/" + projectId + "/reports/" + reportId,
- updateCVE: (cveId: string) => "cveObject/" + cveId + "/update",
- updateAllCVEs: "cveObjects/update",
- unknownPage: "error404"
+ reportId: string) => "api/projects/" + projectId + "/reports/" + reportId,
+ updateCVE: (cveId: string) => "api/cveObject/" + cveId + "/update",
+ updateAllCVEs: "api/cveObjects/update",
+ unknownPage: "api/error404"
},
media: {
rootUrlWithBase: "", // gets set by ConfigContext Provider
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index 6cdd469..c51099d 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -19,7 +19,7 @@
"isolatedModules": true,
"noEmit": false,
"jsx": "react-jsx",
- "outDir": "../backend/assets"
+ "outDir": "./dist"
},
"include": [
"src",
diff --git a/frontend/webpack.common.js b/frontend/webpack.common.js
index 26ea6d6..9aa4182 100644
--- a/frontend/webpack.common.js
+++ b/frontend/webpack.common.js
@@ -1,4 +1,6 @@
const path = require("path");
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const webpack = require('webpack');
module.exports = {
entry: {
app: "./src/App.tsx",
@@ -43,8 +45,24 @@ module.exports = {
},
output: {
filename: "[name].js",
- path: path.join(__dirname, "../backend/assets"),
+ path: path.join(__dirname, "./dist"),
publicPath: '/'
},
-
+ plugins: [
+ // Main app bundle – served by nginx for all authenticated routes
+ new HtmlWebpackPlugin({
+ template: './src/index.html',
+ filename: 'index.html',
+ chunks: ['app'],
+ }),
+ // Login bundle – served by nginx at /login
+ new HtmlWebpackPlugin({
+ template: './src/login.html',
+ filename: 'login.html',
+ chunks: ['login'],
+ }),
+ new webpack.DefinePlugin({
+ 'process.env.REACT_APP_API_URL': JSON.stringify(process.env.REACT_APP_API_URL || ''),
+ })
+ ],
}