fixed client login and changed qt6 lib
This commit is contained in:
@@ -33,7 +33,7 @@ def get_current_user(
|
||||
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()
|
||||
|
||||
if not user:
|
||||
|
||||
+2
-1
@@ -100,7 +100,8 @@ app = FastAPI(
|
||||
lifespan=lifespan,
|
||||
docs_url="/api/docs",
|
||||
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
|
||||
|
||||
@@ -18,8 +18,8 @@ class GatewayBase(BaseModel):
|
||||
@field_validator('vpn_subnet')
|
||||
@classmethod
|
||||
def validate_subnet(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
if v is None or v == "" or v == "None":
|
||||
return None
|
||||
try:
|
||||
ipaddress.ip_network(v, strict=False)
|
||||
return v
|
||||
@@ -46,8 +46,8 @@ class GatewayUpdate(BaseModel):
|
||||
@field_validator('vpn_subnet')
|
||||
@classmethod
|
||||
def validate_subnet(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
if v is None or v == "" or v == "None":
|
||||
return None
|
||||
try:
|
||||
ipaddress.ip_network(v, strict=False)
|
||||
return v
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb">
|
||||
<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>
|
||||
</ol>
|
||||
</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>
|
||||
<input type="text" class="form-control" name="username" required
|
||||
value="{{ user.username if user else '' }}"
|
||||
placeholder="benutzername"
|
||||
{{ 'readonly' if user else '' }}>
|
||||
placeholder="benutzername">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@@ -61,13 +60,15 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{% if current_user.is_super_admin and not user %}
|
||||
{% if current_user.is_super_admin %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Mandant</label>
|
||||
<select class="form-select" name="tenant_id">
|
||||
<option value="">Kein Mandant (Super Admin)</option>
|
||||
{% 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 %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@ def create_access_token(
|
||||
expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes)
|
||||
|
||||
to_encode = {
|
||||
"sub": user_id,
|
||||
"sub": str(user_id), # JWT spec requires sub to be a string
|
||||
"username": username,
|
||||
"role": role,
|
||||
"tenant_id": tenant_id,
|
||||
@@ -59,7 +59,7 @@ def create_refresh_token(
|
||||
expire = datetime.utcnow() + timedelta(days=settings.refresh_token_expire_days)
|
||||
|
||||
to_encode = {
|
||||
"sub": user_id,
|
||||
"sub": str(user_id), # JWT spec requires sub to be a string
|
||||
"exp": expire,
|
||||
"type": "refresh"
|
||||
}
|
||||
@@ -71,5 +71,8 @@ def decode_token(token: str) -> dict | None:
|
||||
try:
|
||||
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
|
||||
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
|
||||
|
||||
+172
-2
@@ -104,6 +104,35 @@ async def create_user(
|
||||
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)
|
||||
async def user_access(
|
||||
request: Request,
|
||||
@@ -126,10 +155,10 @@ async def user_access(
|
||||
else:
|
||||
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
|
||||
).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(
|
||||
"users/access.html",
|
||||
@@ -142,3 +171,144 @@ async def user_access(
|
||||
"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)
|
||||
|
||||
Reference in New Issue
Block a user