Update configuration and enhance newsletter generation features

- Change WIKI_API_URL in .env.example and config to point to the new API endpoint.
- Set COOKIE_SECURE to false for local development in .env.example and config.
- Improve the newsletter generation logic to handle new and edited articles separately.
- Add display options for showing date, user, and category in the newsletter output.
- Enhance error handling for Wiki API requests and update templates for better user feedback.
- Update CSS for new UI elements and improve overall layout in dashboard and login pages.
This commit is contained in:
smueller
2026-07-03 12:47:35 +02:00
parent 0d7729287f
commit e5e4d812c7
13 changed files with 629 additions and 91 deletions

View File

@@ -9,6 +9,7 @@ from sqlalchemy import inspect, text
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.cookies import cookie_secure
from app.core.security import create_access_token, hash_password, verify_password
from app.db.session import Base, engine, get_db
from app.models.system import SendLog
@@ -17,9 +18,18 @@ from app.schemas.user import 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, validate_csrf
from app.services.newsletter import create_html, create_plain_text, filter_articles
from app.services.newsletter import (
DEFAULT_DISPLAY,
article_url,
create_html,
create_plain_text,
create_subject,
filter_articles,
parse_display_options,
split_articles_by_type,
)
from app.services.smtp_sender import get_smtp_settings, run_scheduled_send_if_due, send_newsletter
from app.services.wiki import WikiService
from app.services.wiki import WikiFetchError, WikiService
app = FastAPI(title=settings.app_name)
allowed_hosts = [h.strip() for h in settings.allowed_hosts.split(",") if h.strip()]
@@ -27,6 +37,7 @@ if allowed_hosts:
app.add_middleware(TrustedHostMiddleware, allowed_hosts=allowed_hosts)
app.mount("/static", StaticFiles(directory="app/static"), name="static")
templates = Jinja2Templates(directory="app/templates")
templates.env.globals["wiki_article_url"] = article_url
wiki_service = WikiService()
@@ -54,9 +65,9 @@ async def add_security_headers(request: Request, call_next):
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Referrer-Policy"] = "same-origin"
response.headers["Content-Security-Policy"] = "default-src 'self'; style-src 'self';"
response.headers["Content-Security-Policy"] = "default-src 'self'; style-src 'self'; script-src 'self';"
if not request.cookies.get(CSRF_COOKIE_NAME):
response.set_cookie(CSRF_COOKIE_NAME, ensure_csrf_cookie(request), httponly=True, secure=settings.cookie_secure, samesite="strict")
response.set_cookie(CSRF_COOKIE_NAME, ensure_csrf_cookie(request), httponly=True, secure=cookie_secure(request), samesite="strict")
return response
@@ -120,7 +131,10 @@ def login_page(request: Request, db: Session = Depends(get_db)):
return _render(
request,
"login.html",
{"csrf_token": request.cookies.get(CSRF_COOKIE_NAME) or ensure_csrf_cookie(request)},
{
"csrf_token": request.cookies.get(CSRF_COOKIE_NAME) or ensure_csrf_cookie(request),
"error_message": None,
},
)
@@ -132,13 +146,30 @@ def login(
password: str = Form(...),
db: Session = Depends(get_db),
):
validate_csrf(request, csrf_token)
try:
validate_csrf(request, csrf_token)
except HTTPException:
return _render(
request,
"login.html",
{
"csrf_token": request.cookies.get(CSRF_COOKIE_NAME) or ensure_csrf_cookie(request),
"error_message": "Sitzung abgelaufen. Bitte erneut anmelden.",
},
)
user = db.query(User).filter(User.email == email, User.is_active.is_(True)).first()
if not user or not verify_password(password, user.password_hash):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Ungültige Zugangsdaten.")
return _render(
request,
"login.html",
{
"csrf_token": request.cookies.get(CSRF_COOKIE_NAME) or ensure_csrf_cookie(request),
"error_message": "Ungültige Zugangsdaten.",
},
)
token = create_access_token(user.id)
response = RedirectResponse(url="/dashboard", status_code=status.HTTP_302_FOUND)
response.set_cookie("access_token", token, httponly=True, secure=settings.cookie_secure, samesite="strict")
response.set_cookie("access_token", token, httponly=True, secure=cookie_secure(request), samesite="strict")
return response
@@ -153,7 +184,20 @@ def logout(request: Request, csrf_token: str = Form(...)):
@app.get("/dashboard", response_class=HTMLResponse)
async def dashboard(request: Request, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
context = _base_context(request, current_user, db)
context.update({"articles": [], "raw_html": "", "plain_text": "", "filters": {"days": 30, "only_edited": False, "category": ""}})
context.update({
"articles": [],
"articles_new": [],
"articles_edited": [],
"raw_html": "",
"plain_text": "",
"filters": {
"days": 30,
"only_edited": False,
"category": "",
"display": DEFAULT_DISPLAY,
},
"wiki_error": None,
})
logs = db.query(SendLog).order_by(SendLog.created_at.desc()).limit(20).all()
context["send_logs"] = logs
return _render(request, "dashboard.html", context)
@@ -164,27 +208,50 @@ async def dashboard_generate(
request: Request,
csrf_token: str = Form(...),
days: int = Form(30),
only_edited: bool = Form(False),
only_edited: str | None = Form(None),
category: str = Form(""),
show_date: str | None = Form(None),
show_user: str | None = Form(None),
show_category: str | None = Form(None),
db: Session = Depends(get_db),
current_user: User = Depends(require_editor_or_admin),
):
validate_csrf(request, csrf_token)
days = max(1, min(days, 90))
articles = await wiki_service.get_recent_changes(days=days, only_edited=only_edited)
filtered = filter_articles(articles, category_filter=category or None)
title = f"Thomas-Krenn Wiki Newsletter ({days} Tage)"
plain_text = create_plain_text(filtered, title)
raw_html = create_html(filtered, title)
run_scheduled_send_if_due(db, title, raw_html, plain_text)
only_edited_flag = only_edited == "true"
display = parse_display_options(show_date, show_user, show_category)
wiki_error = None
filtered: list = []
articles_new: list = []
articles_edited: list = []
plain_text = ""
raw_html = ""
try:
articles = await wiki_service.get_recent_changes(days=days, only_edited=only_edited_flag)
filtered = filter_articles(articles, category_filter=category or None)
articles_new, articles_edited = split_articles_by_type(filtered)
title = create_subject(days, category)
plain_text = create_plain_text(filtered, days, display, category)
raw_html = create_html(filtered, days, display, category)
run_scheduled_send_if_due(db, title, raw_html, plain_text)
except WikiFetchError as exc:
wiki_error = exc.message
context = _base_context(request, current_user, db)
context.update(
{
"articles": filtered,
"articles_new": articles_new,
"articles_edited": articles_edited,
"raw_html": raw_html,
"plain_text": plain_text,
"filters": {"days": days, "only_edited": only_edited, "category": category},
"filters": {
"days": days,
"only_edited": only_edited_flag,
"category": category,
"display": display,
},
"send_logs": db.query(SendLog).order_by(SendLog.created_at.desc()).limit(20).all(),
"wiki_error": wiki_error,
}
)
return _render(request, "dashboard.html", context)
@@ -195,17 +262,25 @@ async def send_now(
request: Request,
csrf_token: str = Form(...),
days: int = Form(30),
only_edited: bool = Form(False),
only_edited: str | None = Form(None),
category: str = Form(""),
show_date: str | None = Form(None),
show_user: str | None = Form(None),
show_category: str | None = Form(None),
db: Session = Depends(get_db),
current_user: User = Depends(require_editor_or_admin),
):
validate_csrf(request, csrf_token)
articles = await wiki_service.get_recent_changes(days=max(1, min(days, 90)), only_edited=only_edited)
display = parse_display_options(show_date, show_user, show_category)
only_edited_flag = only_edited == "true"
try:
articles = await wiki_service.get_recent_changes(days=max(1, min(days, 90)), only_edited=only_edited_flag)
except WikiFetchError as exc:
raise HTTPException(status_code=502, detail=exc.message) from exc
filtered = filter_articles(articles, category_filter=category or None)
title = f"Thomas-Krenn Wiki Newsletter ({days} Tage)"
plain_text = create_plain_text(filtered, title)
raw_html = create_html(filtered, title)
title = create_subject(max(1, min(days, 90)), category)
plain_text = create_plain_text(filtered, max(1, min(days, 90)), display, category)
raw_html = create_html(filtered, max(1, min(days, 90)), display, category)
recipients = [r.strip() for r in get_config(db, "smtp.recipients", "").split(",") if r.strip()]
send_newsletter(db, title, raw_html, plain_text, recipients, scheduled=False)
return RedirectResponse(url="/dashboard", status_code=status.HTTP_302_FOUND)