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.models.system import SendLog
|
||||
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.config_store import get_config, set_config
|
||||
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"
|
||||
if path.startswith("/admin/users"):
|
||||
active_nav = "users"
|
||||
elif path.startswith("/profil"):
|
||||
active_nav = "profile"
|
||||
return {
|
||||
"user": user,
|
||||
"csrf_token": ensure_csrf_cookie(request),
|
||||
@@ -533,3 +535,110 @@ def create_user(
|
||||
db.add(user)
|
||||
db.commit()
|
||||
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)$")
|
||||
|
||||
|
||||
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):
|
||||
id: int
|
||||
email: EmailStr
|
||||
|
||||
@@ -115,6 +115,15 @@ a:hover { color: var(--tk-orange-dark); text-decoration: underline; }
|
||||
color: #CCCCCC;
|
||||
}
|
||||
|
||||
a.user-chip-link {
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a.user-chip-link:hover {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.topbar .btn-logout {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<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>
|
||||
<link rel="stylesheet" href="/static/css/main.css?v=4">
|
||||
<link rel="stylesheet" href="/static/css/main.css?v=5">
|
||||
</head>
|
||||
<body class="{% block body_class %}{% endblock %}">
|
||||
<header class="topbar">
|
||||
@@ -22,11 +22,12 @@
|
||||
{% if user.role == "admin" %}
|
||||
<a href="/admin/users" class="{% if active_nav == 'users' %}active{% endif %}">Benutzer</a>
|
||||
{% endif %}
|
||||
<a href="/profil" class="{% if active_nav == 'profile' %}active{% endif %}">Profil</a>
|
||||
</nav>
|
||||
<div class="user-chip">
|
||||
<a href="/profil" class="user-chip user-chip-link">
|
||||
<span>{{ user.full_name }}</span>
|
||||
<span class="badge badge-{{ user.role }}">{{ user.role }}</span>
|
||||
</div>
|
||||
</a>
|
||||
<form method="post" action="/logout">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<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