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:
119
app/main.py
119
app/main.py
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user