fixed client login and changed qt6 lib
This commit is contained in:
parent
6901dc369b
commit
1de7f5b593
|
|
@ -0,0 +1,38 @@
|
||||||
|
# ==============================================
|
||||||
|
# mGuard VPN Endpoint Server Configuration
|
||||||
|
# ==============================================
|
||||||
|
# Copy this file to .env and adjust the values
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
DB_ROOT_PASSWORD=change_me_root_password
|
||||||
|
DB_NAME=mguard_vpn
|
||||||
|
DB_USER=mguard
|
||||||
|
DB_PASSWORD=change_me_db_password
|
||||||
|
|
||||||
|
# API/Web Configuration
|
||||||
|
# Generate with: openssl rand -hex 32
|
||||||
|
SECRET_KEY=change_me_generate_with_openssl_rand_hex_32
|
||||||
|
|
||||||
|
# Admin User (created on first startup)
|
||||||
|
ADMIN_USERNAME=admin
|
||||||
|
ADMIN_PASSWORD=change_me_admin_password
|
||||||
|
ADMIN_EMAIL=admin@example.com
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# OpenVPN Container
|
||||||
|
# ==============================================
|
||||||
|
# Der OpenVPN-Container läuft im Host-Netzwerk-Modus und verwaltet
|
||||||
|
# automatisch alle VPN-Server-Instanzen, die in der Datenbank als
|
||||||
|
# aktiv markiert sind.
|
||||||
|
#
|
||||||
|
# Workflow:
|
||||||
|
# 1. Container starten mit: docker-compose up -d
|
||||||
|
# 2. Im Browser: CA erstellen unter http://localhost:8000/ca/new
|
||||||
|
# 3. VPN-Server erstellen unter http://localhost:8000/vpn-servers/new
|
||||||
|
# 4. Der Container erkennt neue Server automatisch (Polling alle 30s)
|
||||||
|
#
|
||||||
|
# Keine Anpassungen in docker-compose.yml nötig!
|
||||||
|
# VPN-Server können jederzeit über die Web-UI hinzugefügt/entfernt werden.
|
||||||
|
#
|
||||||
|
# Optional: Polling-Intervall anpassen (Standard: 30 Sekunden)
|
||||||
|
# POLL_INTERVAL=30
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
# Environment and credentials
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
*.env
|
||||||
|
secrets/
|
||||||
|
credentials/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.project
|
||||||
|
.pydevproject
|
||||||
|
.settings/
|
||||||
|
*.sublime-project
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
data/
|
||||||
|
db_data/
|
||||||
|
|
||||||
|
# Docker volumes (local data)
|
||||||
|
mariadb_data/
|
||||||
|
openvpn_data/
|
||||||
|
openvpn_logs/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Certificates and keys (sensitive!)
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.crt
|
||||||
|
*.csr
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
|
ca/
|
||||||
|
certs/
|
||||||
|
keys/
|
||||||
|
pki/
|
||||||
|
easy-rsa/
|
||||||
|
|
||||||
|
# OpenVPN specific
|
||||||
|
*.ovpn
|
||||||
|
ta.key
|
||||||
|
dh*.pem
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Test and coverage
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
htmlcov/
|
||||||
|
.pytest_cache/
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
|
||||||
|
# Jupyter
|
||||||
|
.ipynb_checkpoints/
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
*.tar.gz
|
||||||
|
*.zip
|
||||||
|
|
||||||
|
# Node (if frontend exists)
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
242
README.md
242
README.md
|
|
@ -531,6 +531,248 @@ docker-compose up -d
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Desktop Client (Techniker-Anwendung)
|
||||||
|
|
||||||
|
Der Desktop Client ist eine PyQt6-Anwendung für Techniker, um sich zu VPN-Gateways zu verbinden.
|
||||||
|
|
||||||
|
### Voraussetzungen
|
||||||
|
|
||||||
|
- **Python 3.10+**
|
||||||
|
- **OpenVPN** (muss installiert sein)
|
||||||
|
- Zugang zum mGuard VPN Server
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
#### Linux (Debian/Ubuntu)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# System-Abhängigkeiten installieren
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install python3 python3-pip python3-venv openvpn
|
||||||
|
|
||||||
|
# In das Client-Verzeichnis wechseln
|
||||||
|
cd client/
|
||||||
|
|
||||||
|
# Virtuelle Umgebung erstellen (empfohlen)
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Abhängigkeiten installieren
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Linux (Fedora/RHEL)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# System-Abhängigkeiten installieren
|
||||||
|
sudo dnf install python3 python3-pip openvpn
|
||||||
|
|
||||||
|
# In das Client-Verzeichnis wechseln
|
||||||
|
cd client/
|
||||||
|
|
||||||
|
# Virtuelle Umgebung erstellen (empfohlen)
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Abhängigkeiten installieren
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Linux (Arch)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# System-Abhängigkeiten installieren
|
||||||
|
sudo pacman -S python python-pip openvpn
|
||||||
|
|
||||||
|
# In das Client-Verzeichnis wechseln
|
||||||
|
cd client/
|
||||||
|
|
||||||
|
# Virtuelle Umgebung erstellen (empfohlen)
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Abhängigkeiten installieren
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Windows
|
||||||
|
|
||||||
|
1. **Python installieren:**
|
||||||
|
- Download von https://www.python.org/downloads/
|
||||||
|
- Bei Installation "Add Python to PATH" aktivieren
|
||||||
|
|
||||||
|
2. **OpenVPN installieren:**
|
||||||
|
- Download von https://openvpn.net/community-downloads/
|
||||||
|
- Standard-Installation in `C:\Program Files\OpenVPN`
|
||||||
|
|
||||||
|
3. **Client installieren:**
|
||||||
|
```powershell
|
||||||
|
# PowerShell oder CMD
|
||||||
|
cd client
|
||||||
|
|
||||||
|
# Virtuelle Umgebung erstellen (empfohlen)
|
||||||
|
python -m venv venv
|
||||||
|
venv\Scripts\activate
|
||||||
|
|
||||||
|
# Abhängigkeiten installieren
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
#### macOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Homebrew installieren (falls nicht vorhanden)
|
||||||
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||||
|
|
||||||
|
# Python und OpenVPN installieren
|
||||||
|
brew install python openvpn
|
||||||
|
|
||||||
|
# In das Client-Verzeichnis wechseln
|
||||||
|
cd client/
|
||||||
|
|
||||||
|
# Virtuelle Umgebung erstellen (empfohlen)
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Abhängigkeiten installieren
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Client starten
|
||||||
|
|
||||||
|
#### Linux / macOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In das Client-Verzeichnis wechseln
|
||||||
|
cd client/
|
||||||
|
|
||||||
|
# Virtuelle Umgebung aktivieren (falls verwendet)
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Client starten
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hinweis:** Für VPN-Verbindungen sind Root-Rechte erforderlich:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mit sudo starten (für VPN-Verbindungen)
|
||||||
|
sudo venv/bin/python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Windows
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# In das Client-Verzeichnis wechseln
|
||||||
|
cd client
|
||||||
|
|
||||||
|
# Virtuelle Umgebung aktivieren (falls verwendet)
|
||||||
|
venv\Scripts\activate
|
||||||
|
|
||||||
|
# Client starten
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hinweis:** Als Administrator ausführen für VPN-Verbindungen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Erste Schritte im Client
|
||||||
|
|
||||||
|
1. **Server konfigurieren:**
|
||||||
|
- Beim ersten Start Server-URL eingeben (z.B. `https://vpn.meinefirma.de:8000`)
|
||||||
|
|
||||||
|
2. **Anmelden:**
|
||||||
|
- Mit Techniker-Zugangsdaten einloggen
|
||||||
|
|
||||||
|
3. **Gateway wählen:**
|
||||||
|
- Liste der zugewiesenen Gateways wird angezeigt
|
||||||
|
- Online-Status wird angezeigt (grün = erreichbar)
|
||||||
|
|
||||||
|
4. **Verbinden:**
|
||||||
|
- Gateway auswählen und "Verbinden" klicken
|
||||||
|
- VPN-Verbindung wird automatisch hergestellt
|
||||||
|
|
||||||
|
5. **Endpunkte nutzen:**
|
||||||
|
- Nach Verbindung sind die Endpunkte (SPS, HMI, etc.) erreichbar
|
||||||
|
- IP-Adressen und Ports werden angezeigt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Konfiguration
|
||||||
|
|
||||||
|
Der Client speichert Einstellungen in:
|
||||||
|
|
||||||
|
| OS | Pfad |
|
||||||
|
|----|------|
|
||||||
|
| Linux/macOS | `~/.mguard-vpn/settings.json` |
|
||||||
|
| Windows | `C:\Users\<User>\.mguard-vpn\settings.json` |
|
||||||
|
|
||||||
|
OpenVPN-Konfigurationen werden gespeichert in:
|
||||||
|
|
||||||
|
| OS | Pfad |
|
||||||
|
|----|------|
|
||||||
|
| Linux/macOS | `~/.openvpn/` |
|
||||||
|
| Windows | `C:\Users\<User>\OpenVPN\config\` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fehlerbehebung Client
|
||||||
|
|
||||||
|
#### "OpenVPN nicht gefunden"
|
||||||
|
|
||||||
|
**Linux:**
|
||||||
|
```bash
|
||||||
|
# OpenVPN Pfad prüfen
|
||||||
|
which openvpn
|
||||||
|
# Sollte /usr/sbin/openvpn ausgeben
|
||||||
|
|
||||||
|
# Falls woanders installiert, symlink erstellen
|
||||||
|
sudo ln -s /pfad/zu/openvpn /usr/sbin/openvpn
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
- Prüfen ob OpenVPN in `C:\Program Files\OpenVPN\bin\` installiert ist
|
||||||
|
- Falls anderer Pfad: `client/config.py` anpassen
|
||||||
|
|
||||||
|
**macOS:**
|
||||||
|
```bash
|
||||||
|
# OpenVPN Pfad prüfen
|
||||||
|
which openvpn
|
||||||
|
# Meist /opt/homebrew/bin/openvpn oder /usr/local/bin/openvpn
|
||||||
|
|
||||||
|
# Symlink erstellen falls nötig
|
||||||
|
sudo ln -s $(which openvpn) /usr/sbin/openvpn
|
||||||
|
```
|
||||||
|
|
||||||
|
#### "Permission denied" bei VPN-Verbindung
|
||||||
|
|
||||||
|
VPN-Verbindungen erfordern erhöhte Rechte:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux/macOS: Mit sudo starten
|
||||||
|
sudo python main.py
|
||||||
|
|
||||||
|
# Windows: Als Administrator ausführen
|
||||||
|
# Rechtsklick → "Als Administrator ausführen"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### PyQt6 Fehler auf Linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fehlende Qt-Bibliotheken installieren
|
||||||
|
sudo apt install libxcb-xinerama0 libxkbcommon-x11-0
|
||||||
|
|
||||||
|
# Oder für Wayland
|
||||||
|
sudo apt install qtwayland5
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Lizenz
|
## Lizenz
|
||||||
|
|
||||||
Proprietär - Nur für internen Gebrauch.
|
Proprietär - Nur für internen Gebrauch.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
# PyQt GUI
|
# PyQt GUI
|
||||||
PyQt6==6.6.1
|
PyQt6>=6.4.0,<6.7.0
|
||||||
|
PyQt6-Qt6>=6.4.0,<6.7.0
|
||||||
|
|
||||||
# HTTP Client
|
# HTTP Client
|
||||||
httpx==0.26.0
|
httpx==0.26.0
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ class APIClient:
|
||||||
self.base_url = base_url.rstrip('/')
|
self.base_url = base_url.rstrip('/')
|
||||||
self.access_token: Optional[str] = None
|
self.access_token: Optional[str] = None
|
||||||
self.refresh_token: Optional[str] = None
|
self.refresh_token: Optional[str] = None
|
||||||
self.client = httpx.Client(timeout=30.0)
|
self.client = httpx.Client(timeout=30.0, follow_redirects=True)
|
||||||
|
|
||||||
def _headers(self) -> dict:
|
def _headers(self) -> dict:
|
||||||
"""Get request headers with auth token."""
|
"""Get request headers with auth token."""
|
||||||
|
|
@ -64,8 +64,10 @@ class APIClient:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
self.access_token = data["access_token"]
|
self.access_token = data["access_token"]
|
||||||
self.refresh_token = data["refresh_token"]
|
self.refresh_token = data["refresh_token"]
|
||||||
|
print(f"Login successful, token: {self.access_token[:50]}...")
|
||||||
return True
|
return True
|
||||||
except httpx.HTTPError:
|
except httpx.HTTPError as e:
|
||||||
|
print(f"Login error: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def logout(self):
|
def logout(self):
|
||||||
|
|
@ -100,7 +102,8 @@ class APIClient:
|
||||||
def get_gateways(self) -> list[Gateway]:
|
def get_gateways(self) -> list[Gateway]:
|
||||||
"""Get list of accessible gateways."""
|
"""Get list of accessible gateways."""
|
||||||
try:
|
try:
|
||||||
data = self._request("GET", "/api/gateways")
|
print(f"Fetching gateways with token: {self.access_token[:30] if self.access_token else 'NONE'}...")
|
||||||
|
data = self._request("GET", "/api/gateways/")
|
||||||
return [
|
return [
|
||||||
Gateway(
|
Gateway(
|
||||||
id=g["id"],
|
id=g["id"],
|
||||||
|
|
@ -113,7 +116,8 @@ class APIClient:
|
||||||
)
|
)
|
||||||
for g in data
|
for g in data
|
||||||
]
|
]
|
||||||
except httpx.HTTPError:
|
except httpx.HTTPError as e:
|
||||||
|
print(f"Error fetching gateways: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_gateways_status(self) -> list[dict]:
|
def get_gateways_status(self) -> list[dict]:
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ def get_current_user(
|
||||||
headers={"WWW-Authenticate": "Bearer"}
|
headers={"WWW-Authenticate": "Bearer"}
|
||||||
)
|
)
|
||||||
|
|
||||||
user_id = payload.get("sub")
|
user_id = int(payload.get("sub")) # sub is stored as string per JWT spec
|
||||||
user = db.query(User).filter(User.id == user_id).first()
|
user = db.query(User).filter(User.id == user_id).first()
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,8 @@ app = FastAPI(
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
docs_url="/api/docs",
|
docs_url="/api/docs",
|
||||||
redoc_url="/api/redoc",
|
redoc_url="/api/redoc",
|
||||||
openapi_url="/api/openapi.json"
|
openapi_url="/api/openapi.json",
|
||||||
|
redirect_slashes=False # Prevent 307 redirects that lose auth headers
|
||||||
)
|
)
|
||||||
|
|
||||||
# Session middleware for web UI
|
# Session middleware for web UI
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ class GatewayBase(BaseModel):
|
||||||
@field_validator('vpn_subnet')
|
@field_validator('vpn_subnet')
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_subnet(cls, v: str | None) -> str | None:
|
def validate_subnet(cls, v: str | None) -> str | None:
|
||||||
if v is None:
|
if v is None or v == "" or v == "None":
|
||||||
return v
|
return None
|
||||||
try:
|
try:
|
||||||
ipaddress.ip_network(v, strict=False)
|
ipaddress.ip_network(v, strict=False)
|
||||||
return v
|
return v
|
||||||
|
|
@ -46,8 +46,8 @@ class GatewayUpdate(BaseModel):
|
||||||
@field_validator('vpn_subnet')
|
@field_validator('vpn_subnet')
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_subnet(cls, v: str | None) -> str | None:
|
def validate_subnet(cls, v: str | None) -> str | None:
|
||||||
if v is None:
|
if v is None or v == "" or v == "None":
|
||||||
return v
|
return None
|
||||||
try:
|
try:
|
||||||
ipaddress.ip_network(v, strict=False)
|
ipaddress.ip_network(v, strict=False)
|
||||||
return v
|
return v
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<nav aria-label="breadcrumb" class="mb-3">
|
<nav aria-label="breadcrumb" class="mb-3">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="/users">Benutzer</a></li>
|
<li class="breadcrumb-item"><a href="/users">Benutzer</a></li>
|
||||||
<li class="breadcrumb-item"><a href="/users/{{ user.id }}/edit">{{ user.username }}</a></li>
|
<li class="breadcrumb-item"><a href="/users/{{ user.id }}">{{ user.username }}</a></li>
|
||||||
<li class="breadcrumb-item active">Zugriffe</li>
|
<li class="breadcrumb-item active">Zugriffe</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ user.username }} - mGuard VPN Manager{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<nav aria-label="breadcrumb" class="mb-3">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="/users">Benutzer</a></li>
|
||||||
|
<li class="breadcrumb-item active">{{ user.username }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<h1><i class="bi bi-person"></i> {{ user.username }}</h1>
|
||||||
|
<p class="text-muted mb-0">{{ user.email }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/users" class="btn btn-outline-secondary">
|
||||||
|
<i class="bi bi-arrow-left"></i> Zurück
|
||||||
|
</a>
|
||||||
|
<a href="/users/{{ user.id }}/edit" class="btn btn-outline-primary">
|
||||||
|
<i class="bi bi-pencil"></i> Bearbeiten
|
||||||
|
</a>
|
||||||
|
<a href="/users/{{ user.id }}/access" class="btn btn-primary">
|
||||||
|
<i class="bi bi-key"></i> Zugriffe verwalten
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-person-badge"></i> Benutzerinformationen</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-borderless">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 40%;">Benutzername</th>
|
||||||
|
<td>{{ user.username }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>E-Mail</th>
|
||||||
|
<td>{{ user.email }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Voller Name</th>
|
||||||
|
<td>{{ user.full_name or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Rolle</th>
|
||||||
|
<td>
|
||||||
|
{% if user.role.value == 'super_admin' %}
|
||||||
|
<span class="badge bg-danger">Super Admin</span>
|
||||||
|
{% elif user.role.value == 'admin' %}
|
||||||
|
<span class="badge bg-primary">Admin</span>
|
||||||
|
{% elif user.role.value == 'technician' %}
|
||||||
|
<span class="badge bg-success">Techniker</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">{{ user.role.value }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Mandant</th>
|
||||||
|
<td>
|
||||||
|
{% if user.tenant %}
|
||||||
|
<a href="/tenants/{{ user.tenant.id }}">{{ user.tenant.name }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">- (Global)</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
|
<td>
|
||||||
|
{% if user.is_active %}
|
||||||
|
<span class="badge bg-success">Aktiv</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Inaktiv</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Erstellt am</th>
|
||||||
|
<td>{{ user.created_at.strftime('%d.%m.%Y %H:%M') if user.created_at else '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Letzte Anmeldung</th>
|
||||||
|
<td>{{ user.last_login.strftime('%d.%m.%Y %H:%M') if user.last_login else 'Noch nie' }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0"><i class="bi bi-router"></i> Gateway-Zugriffe</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if user.role.value in ['super_admin', 'admin'] %}
|
||||||
|
<div class="alert alert-info mb-0">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
{% if user.role.value == 'super_admin' %}
|
||||||
|
Super-Admins haben automatisch Zugriff auf alle Gateways aller Mandanten.
|
||||||
|
{% else %}
|
||||||
|
Admins haben automatisch Zugriff auf alle Gateways ihres Mandanten.
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{% if user.gateway_access %}
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{% for access in user.gateway_access %}
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
<span>
|
||||||
|
<a href="/gateways/{{ access.gateway.id }}">{{ access.gateway.name }}</a>
|
||||||
|
{% if access.gateway.location %}
|
||||||
|
<small class="text-muted ms-2">{{ access.gateway.location }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
{% if access.gateway.is_online %}
|
||||||
|
<span class="badge bg-success">Online</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Offline</span>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted mb-0">Keine Gateway-Zugriffe zugewiesen.</p>
|
||||||
|
{% endif %}
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="/users/{{ user.id }}/access" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-pencil"></i> Zugriffe bearbeiten
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -25,8 +25,7 @@
|
||||||
<label class="form-label">Benutzername *</label>
|
<label class="form-label">Benutzername *</label>
|
||||||
<input type="text" class="form-control" name="username" required
|
<input type="text" class="form-control" name="username" required
|
||||||
value="{{ user.username if user else '' }}"
|
value="{{ user.username if user else '' }}"
|
||||||
placeholder="benutzername"
|
placeholder="benutzername">
|
||||||
{{ 'readonly' if user else '' }}>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
|
@ -61,13 +60,15 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if current_user.is_super_admin and not user %}
|
{% if current_user.is_super_admin %}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Mandant</label>
|
<label class="form-label">Mandant</label>
|
||||||
<select class="form-select" name="tenant_id">
|
<select class="form-select" name="tenant_id">
|
||||||
<option value="">Kein Mandant (Super Admin)</option>
|
<option value="">Kein Mandant (Super Admin)</option>
|
||||||
{% for tenant in tenants %}
|
{% for tenant in tenants %}
|
||||||
<option value="{{ tenant.id }}">{{ tenant.name }}</option>
|
<option value="{{ tenant.id }}" {{ 'selected' if user and user.tenant_id == tenant.id }}>
|
||||||
|
{{ tenant.name }}
|
||||||
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ def create_access_token(
|
||||||
expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes)
|
expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes)
|
||||||
|
|
||||||
to_encode = {
|
to_encode = {
|
||||||
"sub": user_id,
|
"sub": str(user_id), # JWT spec requires sub to be a string
|
||||||
"username": username,
|
"username": username,
|
||||||
"role": role,
|
"role": role,
|
||||||
"tenant_id": tenant_id,
|
"tenant_id": tenant_id,
|
||||||
|
|
@ -59,7 +59,7 @@ def create_refresh_token(
|
||||||
expire = datetime.utcnow() + timedelta(days=settings.refresh_token_expire_days)
|
expire = datetime.utcnow() + timedelta(days=settings.refresh_token_expire_days)
|
||||||
|
|
||||||
to_encode = {
|
to_encode = {
|
||||||
"sub": user_id,
|
"sub": str(user_id), # JWT spec requires sub to be a string
|
||||||
"exp": expire,
|
"exp": expire,
|
||||||
"type": "refresh"
|
"type": "refresh"
|
||||||
}
|
}
|
||||||
|
|
@ -71,5 +71,8 @@ def decode_token(token: str) -> dict | None:
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
|
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
|
||||||
return payload
|
return payload
|
||||||
except JWTError:
|
except JWTError as e:
|
||||||
|
print(f"JWT decode error: {e}")
|
||||||
|
print(f"Token (first 50 chars): {token[:50]}...")
|
||||||
|
print(f"Secret key (first 10 chars): {settings.secret_key[:10]}...")
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,35 @@ async def create_user(
|
||||||
return RedirectResponse(url="/users", status_code=303)
|
return RedirectResponse(url="/users", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/users/{user_id}", response_class=HTMLResponse)
|
||||||
|
async def user_detail(
|
||||||
|
request: Request,
|
||||||
|
user_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(require_admin_web)
|
||||||
|
):
|
||||||
|
"""User detail page."""
|
||||||
|
user = db.query(User).filter(User.id == user_id).first()
|
||||||
|
if not user:
|
||||||
|
flash(request, "Benutzer nicht gefunden", "danger")
|
||||||
|
return RedirectResponse(url="/users", status_code=303)
|
||||||
|
|
||||||
|
# Check tenant access
|
||||||
|
if current_user.role != UserRole.SUPER_ADMIN and user.tenant_id != current_user.tenant_id:
|
||||||
|
flash(request, "Zugriff verweigert", "danger")
|
||||||
|
return RedirectResponse(url="/users", status_code=303)
|
||||||
|
|
||||||
|
return request.app.state.templates.TemplateResponse(
|
||||||
|
"users/detail.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"current_user": current_user,
|
||||||
|
"user": user,
|
||||||
|
"flash_messages": get_flashed_messages(request)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/users/{user_id}/access", response_class=HTMLResponse)
|
@router.get("/users/{user_id}/access", response_class=HTMLResponse)
|
||||||
async def user_access(
|
async def user_access(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
@ -126,10 +155,10 @@ async def user_access(
|
||||||
else:
|
else:
|
||||||
gateways = db.query(Gateway).filter(Gateway.tenant_id == current_user.tenant_id).all()
|
gateways = db.query(Gateway).filter(Gateway.tenant_id == current_user.tenant_id).all()
|
||||||
|
|
||||||
user_access = db.query(UserGatewayAccess).filter(
|
user_access_list = db.query(UserGatewayAccess).filter(
|
||||||
UserGatewayAccess.user_id == user_id
|
UserGatewayAccess.user_id == user_id
|
||||||
).all()
|
).all()
|
||||||
access_gateway_ids = [a.gateway_id for a in user_access]
|
access_gateway_ids = [a.gateway_id for a in user_access_list]
|
||||||
|
|
||||||
return request.app.state.templates.TemplateResponse(
|
return request.app.state.templates.TemplateResponse(
|
||||||
"users/access.html",
|
"users/access.html",
|
||||||
|
|
@ -142,3 +171,144 @@ async def user_access(
|
||||||
"flash_messages": get_flashed_messages(request)
|
"flash_messages": get_flashed_messages(request)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/users/{user_id}/access")
|
||||||
|
async def save_user_access(
|
||||||
|
request: Request,
|
||||||
|
user_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(require_admin_web)
|
||||||
|
):
|
||||||
|
"""Save user gateway access."""
|
||||||
|
from ..models.gateway import Gateway
|
||||||
|
from ..models.access import UserGatewayAccess
|
||||||
|
|
||||||
|
user = db.query(User).filter(User.id == user_id).first()
|
||||||
|
if not user:
|
||||||
|
flash(request, "Benutzer nicht gefunden", "danger")
|
||||||
|
return RedirectResponse(url="/users", status_code=303)
|
||||||
|
|
||||||
|
# Get form data
|
||||||
|
form_data = await request.form()
|
||||||
|
selected_gateway_ids = [int(gw_id) for gw_id in form_data.getlist("gateway_ids")]
|
||||||
|
|
||||||
|
# Get available gateways
|
||||||
|
if current_user.role == UserRole.SUPER_ADMIN:
|
||||||
|
available_gateways = db.query(Gateway).all()
|
||||||
|
else:
|
||||||
|
available_gateways = db.query(Gateway).filter(Gateway.tenant_id == current_user.tenant_id).all()
|
||||||
|
available_gateway_ids = [g.id for g in available_gateways]
|
||||||
|
|
||||||
|
# Remove existing access for available gateways
|
||||||
|
db.query(UserGatewayAccess).filter(
|
||||||
|
UserGatewayAccess.user_id == user_id,
|
||||||
|
UserGatewayAccess.gateway_id.in_(available_gateway_ids)
|
||||||
|
).delete(synchronize_session=False)
|
||||||
|
|
||||||
|
# Add new access
|
||||||
|
for gateway_id in selected_gateway_ids:
|
||||||
|
if gateway_id in available_gateway_ids:
|
||||||
|
access = UserGatewayAccess(
|
||||||
|
user_id=user_id,
|
||||||
|
gateway_id=gateway_id,
|
||||||
|
granted_by_id=current_user.id
|
||||||
|
)
|
||||||
|
db.add(access)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
flash(request, f"Zugriffe für '{user.username}' aktualisiert", "success")
|
||||||
|
return RedirectResponse(url=f"/users/{user_id}/access", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/users/{user_id}/edit", response_class=HTMLResponse)
|
||||||
|
async def edit_user_form(
|
||||||
|
request: Request,
|
||||||
|
user_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(require_admin_web)
|
||||||
|
):
|
||||||
|
"""Edit user form."""
|
||||||
|
from ..models.tenant import Tenant
|
||||||
|
|
||||||
|
user = db.query(User).filter(User.id == user_id).first()
|
||||||
|
if not user:
|
||||||
|
flash(request, "Benutzer nicht gefunden", "danger")
|
||||||
|
return RedirectResponse(url="/users", status_code=303)
|
||||||
|
|
||||||
|
# Check tenant access
|
||||||
|
if current_user.role != UserRole.SUPER_ADMIN and user.tenant_id != current_user.tenant_id:
|
||||||
|
flash(request, "Zugriff verweigert", "danger")
|
||||||
|
return RedirectResponse(url="/users", status_code=303)
|
||||||
|
|
||||||
|
tenants = db.query(Tenant).filter(Tenant.is_active == True).all()
|
||||||
|
|
||||||
|
return request.app.state.templates.TemplateResponse(
|
||||||
|
"users/form.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"current_user": current_user,
|
||||||
|
"user": user,
|
||||||
|
"tenants": tenants,
|
||||||
|
"roles": [r.value for r in UserRole if r != UserRole.SUPER_ADMIN or current_user.role == UserRole.SUPER_ADMIN],
|
||||||
|
"flash_messages": get_flashed_messages(request)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/users/{user_id}/edit")
|
||||||
|
async def update_user(
|
||||||
|
request: Request,
|
||||||
|
user_id: int,
|
||||||
|
username: str = Form(...),
|
||||||
|
email: str = Form(...),
|
||||||
|
password: str = Form(None),
|
||||||
|
role: str = Form(...),
|
||||||
|
full_name: str = Form(None),
|
||||||
|
tenant_id: int = Form(None),
|
||||||
|
is_active: str = Form(None),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(require_admin_web)
|
||||||
|
):
|
||||||
|
"""Update user."""
|
||||||
|
user = db.query(User).filter(User.id == user_id).first()
|
||||||
|
if not user:
|
||||||
|
flash(request, "Benutzer nicht gefunden", "danger")
|
||||||
|
return RedirectResponse(url="/users", status_code=303)
|
||||||
|
|
||||||
|
# Check tenant access
|
||||||
|
if current_user.role != UserRole.SUPER_ADMIN and user.tenant_id != current_user.tenant_id:
|
||||||
|
flash(request, "Zugriff verweigert", "danger")
|
||||||
|
return RedirectResponse(url="/users", status_code=303)
|
||||||
|
|
||||||
|
# Check if username is taken by another user
|
||||||
|
existing = db.query(User).filter(User.username == username, User.id != user_id).first()
|
||||||
|
if existing:
|
||||||
|
flash(request, "Benutzername bereits vergeben", "danger")
|
||||||
|
return RedirectResponse(url=f"/users/{user_id}/edit", status_code=303)
|
||||||
|
|
||||||
|
# Check if email is taken by another user
|
||||||
|
existing = db.query(User).filter(User.email == email, User.id != user_id).first()
|
||||||
|
if existing:
|
||||||
|
flash(request, "E-Mail bereits vergeben", "danger")
|
||||||
|
return RedirectResponse(url=f"/users/{user_id}/edit", status_code=303)
|
||||||
|
|
||||||
|
user.username = username
|
||||||
|
user.email = email
|
||||||
|
user.role = UserRole(role)
|
||||||
|
user.full_name = full_name or None
|
||||||
|
user.is_active = is_active is not None
|
||||||
|
|
||||||
|
# Only update password if provided
|
||||||
|
if password:
|
||||||
|
user.password_hash = get_password_hash(password)
|
||||||
|
|
||||||
|
# Update tenant
|
||||||
|
if current_user.role == UserRole.SUPER_ADMIN:
|
||||||
|
user.tenant_id = tenant_id if UserRole(role) != UserRole.SUPER_ADMIN else None
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
flash(request, f"Benutzer '{username}' aktualisiert", "success")
|
||||||
|
return RedirectResponse(url=f"/users/{user_id}", status_code=303)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue