dev #5
111
app/main.py
111
app/main.py
@@ -16,7 +16,7 @@ from app.core.security import create_access_token, hash_password, verify_passwor
|
|||||||
from app.db.session import Base, engine, get_db
|
from app.db.session import Base, engine, get_db
|
||||||
from app.models.system import SendLog
|
from app.models.system import SendLog
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.schemas.user import UserCreate
|
from app.schemas.user import PasswordChange, ProfileUpdate, UserCreate
|
||||||
from app.services.auth import get_admin_user, get_current_user, get_optional_user, require_editor_or_admin
|
from app.services.auth import get_admin_user, get_current_user, get_optional_user, require_editor_or_admin
|
||||||
from app.services.config_store import get_config, set_config
|
from app.services.config_store import get_config, set_config
|
||||||
from app.services.csrf import CSRF_COOKIE_NAME, ensure_csrf_cookie, generate_csrf_token, validate_csrf
|
from app.services.csrf import CSRF_COOKIE_NAME, ensure_csrf_cookie, generate_csrf_token, validate_csrf
|
||||||
@@ -123,6 +123,8 @@ def _base_context(request: Request, user: User, db: Session) -> dict:
|
|||||||
active_nav = "dashboard"
|
active_nav = "dashboard"
|
||||||
if path.startswith("/admin/users"):
|
if path.startswith("/admin/users"):
|
||||||
active_nav = "users"
|
active_nav = "users"
|
||||||
|
elif path.startswith("/profil"):
|
||||||
|
active_nav = "profile"
|
||||||
return {
|
return {
|
||||||
"user": user,
|
"user": user,
|
||||||
"csrf_token": ensure_csrf_cookie(request),
|
"csrf_token": ensure_csrf_cookie(request),
|
||||||
@@ -533,3 +535,110 @@ def create_user(
|
|||||||
db.add(user)
|
db.add(user)
|
||||||
db.commit()
|
db.commit()
|
||||||
return RedirectResponse(url="/admin/users?created=1", status_code=status.HTTP_302_FOUND)
|
return RedirectResponse(url="/admin/users?created=1", status_code=status.HTTP_302_FOUND)
|
||||||
|
|
||||||
|
|
||||||
|
def _profile_context(request: Request, user: User, db: Session, **extra) -> dict:
|
||||||
|
context = _base_context(request, user, db)
|
||||||
|
context.setdefault("error_message", None)
|
||||||
|
context.setdefault("pw_error_message", None)
|
||||||
|
context.setdefault("success_message", None)
|
||||||
|
context.update(extra)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/profil", response_class=HTMLResponse)
|
||||||
|
def profile_page(request: Request, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||||
|
success = None
|
||||||
|
if request.query_params.get("updated") == "1":
|
||||||
|
success = "Profil wurde aktualisiert."
|
||||||
|
elif request.query_params.get("pw") == "1":
|
||||||
|
success = "Passwort wurde geändert."
|
||||||
|
return _render(request, "profile.html", _profile_context(request, current_user, db, success_message=success))
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/profil", response_class=HTMLResponse)
|
||||||
|
def update_profile(
|
||||||
|
request: Request,
|
||||||
|
csrf_token: str = Form(...),
|
||||||
|
email: str = Form(...),
|
||||||
|
full_name: str = Form(...),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
validate_csrf(request, csrf_token)
|
||||||
|
except HTTPException:
|
||||||
|
return _render(
|
||||||
|
request,
|
||||||
|
"profile.html",
|
||||||
|
_profile_context(request, current_user, db, error_message="Sitzung abgelaufen. Bitte Seite neu laden und erneut versuchen."),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = ProfileUpdate(email=email.strip(), full_name=full_name.strip())
|
||||||
|
except ValidationError as exc:
|
||||||
|
return _render(
|
||||||
|
request,
|
||||||
|
"profile.html",
|
||||||
|
_profile_context(request, current_user, db, error_message=_format_validation_error(exc)),
|
||||||
|
)
|
||||||
|
|
||||||
|
clash = db.query(User).filter(User.email == payload.email, User.id != current_user.id).first()
|
||||||
|
if clash:
|
||||||
|
return _render(
|
||||||
|
request,
|
||||||
|
"profile.html",
|
||||||
|
_profile_context(request, current_user, db, error_message="Diese E-Mail-Adresse wird bereits verwendet."),
|
||||||
|
)
|
||||||
|
|
||||||
|
current_user.email = payload.email
|
||||||
|
current_user.full_name = payload.full_name
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse(url="/profil?updated=1", status_code=status.HTTP_302_FOUND)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/profil/passwort", response_class=HTMLResponse)
|
||||||
|
def change_password(
|
||||||
|
request: Request,
|
||||||
|
csrf_token: str = Form(...),
|
||||||
|
current_password: str = Form(...),
|
||||||
|
new_password: str = Form(...),
|
||||||
|
confirm_password: str = Form(...),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
validate_csrf(request, csrf_token)
|
||||||
|
except HTTPException:
|
||||||
|
return _render(
|
||||||
|
request,
|
||||||
|
"profile.html",
|
||||||
|
_profile_context(request, current_user, db, pw_error_message="Sitzung abgelaufen. Bitte Seite neu laden und erneut versuchen."),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not verify_password(current_password, current_user.password_hash):
|
||||||
|
return _render(
|
||||||
|
request,
|
||||||
|
"profile.html",
|
||||||
|
_profile_context(request, current_user, db, pw_error_message="Das aktuelle Passwort ist nicht korrekt."),
|
||||||
|
)
|
||||||
|
|
||||||
|
if new_password != confirm_password:
|
||||||
|
return _render(
|
||||||
|
request,
|
||||||
|
"profile.html",
|
||||||
|
_profile_context(request, current_user, db, pw_error_message="Die neuen Passwörter stimmen nicht überein."),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = PasswordChange(new_password=new_password)
|
||||||
|
except ValidationError as exc:
|
||||||
|
return _render(
|
||||||
|
request,
|
||||||
|
"profile.html",
|
||||||
|
_profile_context(request, current_user, db, pw_error_message=_format_validation_error(exc)),
|
||||||
|
)
|
||||||
|
|
||||||
|
current_user.password_hash = hash_password(payload.new_password)
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse(url="/profil?pw=1", status_code=status.HTTP_302_FOUND)
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ class UserCreate(BaseModel):
|
|||||||
role: str = Field(default="reader", pattern="^(admin|editor|reader)$")
|
role: str = Field(default="reader", pattern="^(admin|editor|reader)$")
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileUpdate(BaseModel):
|
||||||
|
email: EmailStr
|
||||||
|
full_name: str = Field(min_length=2, max_length=255)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordChange(BaseModel):
|
||||||
|
new_password: str = Field(min_length=10, max_length=255)
|
||||||
|
|
||||||
|
|
||||||
class UserOut(BaseModel):
|
class UserOut(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
email: EmailStr
|
email: EmailStr
|
||||||
|
|||||||
@@ -115,6 +115,15 @@ a:hover { color: var(--tk-orange-dark); text-decoration: underline; }
|
|||||||
color: #CCCCCC;
|
color: #CCCCCC;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.user-chip-link {
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.user-chip-link:hover {
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
.topbar .btn-logout {
|
.topbar .btn-logout {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{% block title %}{{ title if title else "Wiki Newsletter Admin" }}{% endblock %} · Thomas-Krenn.AG</title>
|
<title>{% block title %}{{ title if title else "Wiki Newsletter Admin" }}{% endblock %} · Thomas-Krenn.AG</title>
|
||||||
<link rel="stylesheet" href="/static/css/main.css?v=4">
|
<link rel="stylesheet" href="/static/css/main.css?v=5">
|
||||||
</head>
|
</head>
|
||||||
<body class="{% block body_class %}{% endblock %}">
|
<body class="{% block body_class %}{% endblock %}">
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
@@ -22,11 +22,12 @@
|
|||||||
{% if user.role == "admin" %}
|
{% if user.role == "admin" %}
|
||||||
<a href="/admin/users" class="{% if active_nav == 'users' %}active{% endif %}">Benutzer</a>
|
<a href="/admin/users" class="{% if active_nav == 'users' %}active{% endif %}">Benutzer</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<a href="/profil" class="{% if active_nav == 'profile' %}active{% endif %}">Profil</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="user-chip">
|
<a href="/profil" class="user-chip user-chip-link">
|
||||||
<span>{{ user.full_name }}</span>
|
<span>{{ user.full_name }}</span>
|
||||||
<span class="badge badge-{{ user.role }}">{{ user.role }}</span>
|
<span class="badge badge-{{ user.role }}">{{ user.role }}</span>
|
||||||
</div>
|
</a>
|
||||||
<form method="post" action="/logout">
|
<form method="post" action="/logout">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
<button type="submit" class="btn-logout">Abmelden</button>
|
<button type="submit" class="btn-logout">Abmelden</button>
|
||||||
|
|||||||
74
app/templates/profile.html
Normal file
74
app/templates/profile.html
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Mein Profil{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>Mein Profil</h2>
|
||||||
|
<p class="page-lead"><a href="/dashboard">← Zurück zum Dashboard</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if success_message %}
|
||||||
|
<p class="hint" style="color:var(--tk-success);font-weight:600;">{{ success_message }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="layout-2col">
|
||||||
|
<div class="main-col">
|
||||||
|
<section class="card card-accent-orange">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Profil bearbeiten</h3>
|
||||||
|
</div>
|
||||||
|
{% if error_message %}
|
||||||
|
<p class="error">{{ error_message }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<form method="post" action="/profil" class="form-grid">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Vollständiger Name
|
||||||
|
<input type="text" name="full_name" required minlength="2" value="{{ user.full_name }}">
|
||||||
|
</label>
|
||||||
|
<label>E-Mail
|
||||||
|
<input type="email" name="email" required value="{{ user.email }}">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-primary">Profil speichern</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Passwort ändern</h3>
|
||||||
|
</div>
|
||||||
|
{% if pw_error_message %}
|
||||||
|
<p class="error">{{ pw_error_message }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<form method="post" action="/profil/passwort" class="form-grid">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<label>Aktuelles Passwort
|
||||||
|
<input type="password" name="current_password" required autocomplete="current-password">
|
||||||
|
</label>
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Neues Passwort
|
||||||
|
<input type="password" name="new_password" required minlength="10" autocomplete="new-password" placeholder="Mindestens 10 Zeichen">
|
||||||
|
</label>
|
||||||
|
<label>Neues Passwort bestätigen
|
||||||
|
<input type="password" name="confirm_password" required minlength="10" autocomplete="new-password">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-primary">Passwort ändern</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class="sidebar-col">
|
||||||
|
<div class="sidebar-card">
|
||||||
|
<h4>Konto</h4>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Rolle:</strong> {{ user.role }}</li>
|
||||||
|
<li><strong>Status:</strong> {{ "Aktiv" if user.is_active else "Inaktiv" }}</li>
|
||||||
|
</ul>
|
||||||
|
<p class="hint">Die Rolle kann nur ein Administrator ändern.</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user