fix(templates): replace all TemplateResponse with _render to bypass Jinja2 unhashable dict cache bug

This commit is contained in:
2026-07-18 09:03:55 +08:00
parent b80a1859a0
commit 1c53209acf
+44 -54
View File
@@ -94,6 +94,18 @@ from app.i18n import get_languages as _get_languages
templates.env.globals["get_languages"] = _get_languages
def _render(request: Request, name: str, ctx: dict) -> HTMLResponse:
"""Render a Jinja2 template and return HTMLResponse.
Bypasses Starlette TemplateResponse to avoid a Jinja2 cache bug where
the context dict is passed as ``globals`` and used as an unhashable
cache key (``TypeError: unhashable type: 'dict'``).
"""
ctx["request"] = request
html = templates.get_template(name).render(ctx)
return HTMLResponse(html)
def _get_copyright() -> str:
from app.models import SystemConfig as _SC
from app.database import SessionLocal
@@ -151,8 +163,7 @@ def index_page(request: Request, db: Session = Depends(get_db)):
if not lang or lang not in avail:
lang = "zh"
return templates.TemplateResponse("index.html", {
"request": request, "postcards": all_postcards[:20], "lang": lang,
return _render(request, "index.html", {"postcards": all_postcards[:20], "lang": lang,
"has_more": len(all_postcards) > 20,
})
@@ -248,7 +259,7 @@ def login_page(request: Request, db: Session = Depends(get_db)):
user = redirect_if_not_logged_in(request, db)
if user:
return _redirect("/dashboard")
return templates.TemplateResponse("login.html", {"request": request})
return _render(request, "login.html", {"request": request})
@router.post("/login")
@@ -263,8 +274,7 @@ def login_submit(
cleanup_expired_sessions(db)
user = db.query(User).filter(User.username == username).first()
if not user or not verify_password(password, user.password_hash):
return templates.TemplateResponse(
"login.html", {"request": request, "error": "用户名或密码错误"}, status_code=401
return _render(request, "login.html", {"error": "用户名或密码错误"}, status_code=401
)
is_remember = remember_me == "on"
token = create_session(user.id, db, remember_me=is_remember)
@@ -283,7 +293,7 @@ def register_page(request: Request, db: Session = Depends(get_db)):
user = redirect_if_not_logged_in(request, db)
if user:
return _redirect("/dashboard")
return templates.TemplateResponse("register.html", {"request": request})
return _render(request, "register.html", {"request": request})
@router.post("/register")
@@ -298,30 +308,24 @@ def register_submit(
# Validate invite code
code_obj = db.query(InviteCode).filter(InviteCode.code == invite_code).first()
if not code_obj:
return templates.TemplateResponse(
"register.html", {"request": request, "error": "邀请码无效"}, status_code=400
return _render(request, "register.html", {"error": "邀请码无效"}, status_code=400
)
if code_obj.expires_at and code_obj.expires_at < datetime.utcnow():
return templates.TemplateResponse(
"register.html", {"request": request, "error": "邀请码已过期"}, status_code=400
return _render(request, "register.html", {"error": "邀请码已过期"}, status_code=400
)
if code_obj.max_uses is not None and code_obj.use_count >= code_obj.max_uses:
return templates.TemplateResponse(
"register.html", {"request": request, "error": "邀请码使用次数已用尽"}, status_code=400
return _render(request, "register.html", {"error": "邀请码使用次数已用尽"}, status_code=400
)
# Validate passwords
if password != confirm_password:
return templates.TemplateResponse(
"register.html", {"request": request, "error": "两次输入的密码不一致"}, status_code=400
return _render(request, "register.html", {"error": "两次输入的密码不一致"}, status_code=400
)
if len(password) < 6:
return templates.TemplateResponse(
"register.html", {"request": request, "error": "密码至少6个字符"}, status_code=400
return _render(request, "register.html", {"error": "密码至少6个字符"}, status_code=400
)
# Check username
if db.query(User).filter(User.username == username).first():
return templates.TemplateResponse(
"register.html", {"request": request, "error": "用户名已存在"}, status_code=400
return _render(request, "register.html", {"error": "用户名已存在"}, status_code=400
)
# Create user
new_user = User(username=username, password_hash=hash_password(password), invited_by_code_id=code_obj.id)
@@ -359,7 +363,9 @@ def _admin_context(user, db, active_tab="invites", **extra):
def _admin_render(request, user, db, active_tab="invites", **extra):
return templates.TemplateResponse("admin.html", _admin_context(user, db, active_tab, request=request, **extra))
ctx = _admin_context(user, db, active_tab, **extra)
ctx["request"] = request
return _render(request, "admin.html", ctx)
@router.get("/admin", response_class=HTMLResponse)
@@ -501,8 +507,7 @@ def settings_page(
for p in profiles:
profile_cards[p.id] = db.query(Postcard).filter(Postcard.profile_id == p.id).all()
api_keys = db.query(ApiKey).filter(ApiKey.user_id == user.id).order_by(ApiKey.created_at.desc()).all()
return templates.TemplateResponse("settings.html", {
"request": request, "user": user, "profiles": profiles,
return _render(request, "settings.html", {"user": user, "profiles": profiles,
"profile_cards": profile_cards, "active_tab": active_tab,
"api_keys": api_keys,
})
@@ -516,13 +521,11 @@ def change_username(
db: Session = Depends(get_db),
):
if db.query(User).filter(User.username == new_username, User.id != user.id).first():
return templates.TemplateResponse(
"settings.html", {"request": request, "user": user, "error": "用户名已存在"}, status_code=400
return _render(request, "settings.html", {"user": user, "error": "用户名已存在"}, status_code=400
)
user.username = new_username
db.commit()
return templates.TemplateResponse(
"settings.html", {"request": request, "user": user, "success": "用户名修改成功"}
return _render(request, "settings.html", {"user": user, "success": "用户名修改成功"}
)
@@ -536,21 +539,17 @@ def change_password(
db: Session = Depends(get_db),
):
if not verify_password(old_password, user.password_hash):
return templates.TemplateResponse(
"settings.html", {"request": request, "user": user, "error": "当前密码错误"}, status_code=400
return _render(request, "settings.html", {"user": user, "error": "当前密码错误"}, status_code=400
)
if new_password != confirm_password:
return templates.TemplateResponse(
"settings.html", {"request": request, "user": user, "error": "两次输入的新密码不一致"}, status_code=400
return _render(request, "settings.html", {"user": user, "error": "两次输入的新密码不一致"}, status_code=400
)
if len(new_password) < 6:
return templates.TemplateResponse(
"settings.html", {"request": request, "user": user, "error": "新密码长度至少6位"}, status_code=400
return _render(request, "settings.html", {"user": user, "error": "新密码长度至少6位"}, status_code=400
)
user.password_hash = hash_password(new_password)
db.commit()
return templates.TemplateResponse(
"settings.html", {"request": request, "user": user, "success": "密码修改成功"}
return _render(request, "settings.html", {"user": user, "success": "密码修改成功"}
)
@@ -626,8 +625,7 @@ def dashboard(request: Request, user: User = Depends(get_current_user_web), db:
from datetime import datetime, timezone as _tz
return templates.TemplateResponse("dashboard.html", {
"request": request, "user": user, "profiles": profiles,
return _render(request, "dashboard.html", {"user": user, "profiles": profiles,
"total": total, "sent": sent, "delivered": delivered, "received": received,
"countries": countries, "recent": recent,
"sent_pct": sent_pct, "delivered_pct": delivered_pct, "received_pct": received_pct,
@@ -651,7 +649,7 @@ def profile_list(request: Request, user: User = Depends(get_current_user_web), d
"received": sum(1 for c in cards if c.status == "received"),
"countries": len({c.country_to for c in cards if c.country_to}),
}
return templates.TemplateResponse("profiles.html", {"request": request, "user": user, "profiles": profiles, "extras": extras})
return _render(request, "profiles.html", {"user": user, "profiles": profiles, "extras": extras})
@router.post("/profiles")
@@ -719,9 +717,7 @@ def postcard_list(
sent = [pc for pc in postcards if pc.status in ("pending", "sent", "delivered")]
received = [pc for pc in postcards if pc.status == "received"]
return templates.TemplateResponse(
"postcards.html",
{"request": request, "user": user, "profile": profile, "sent": sent, "received": received, "countries": all_countries, "country": country},
return _render(request, "postcards.html", {"user": user, "profile": profile, "sent": sent, "received": received, "countries": all_countries, "country": country},
)
@@ -731,7 +727,7 @@ def postcard_new(request: Request, profile_id: int, create_type: str = Query("se
if not profile:
return _redirect("/profiles")
today = datetime.now().strftime("%Y-%m-%d")
return templates.TemplateResponse("postcard_form.html", {"request": request, "user": user, "profile": profile, "postcard": None, "create_type": create_type, "today": today})
return _render(request, "postcard_form.html", {"user": user, "profile": profile, "postcard": None, "create_type": create_type, "today": today})
@router.post("/profiles/{profile_id}/postcards")
@@ -760,8 +756,7 @@ def postcard_create(
Postcard.card_number == cn, Profile.user_id == user.id
).first()
if dup:
return templates.TemplateResponse("postcard_form.html", {
"request": request, "user": user, "profile": profile, "postcard": None,
return _render(request, "postcard_form.html", {"user": user, "profile": profile, "postcard": None,
"error": _t("pc_list.duplicate", user.language),
})
@@ -805,7 +800,7 @@ def postcard_detail(request: Request, postcard_id: int, user: User = Depends(get
)
if not pc:
return _redirect("/profiles")
return templates.TemplateResponse("postcard_detail.html", {"request": request, "user": user, "postcard": pc})
return _render(request, "postcard_detail.html", {"user": user, "postcard": pc})
@router.get("/postcards/{postcard_id}/edit", response_class=HTMLResponse)
@@ -818,7 +813,7 @@ def postcard_edit(request: Request, postcard_id: int, user: User = Depends(get_c
)
if not pc:
return _redirect("/profiles")
return templates.TemplateResponse("postcard_form.html", {"request": request, "user": user, "profile": pc.profile, "postcard": pc})
return _render(request, "postcard_form.html", {"user": user, "profile": pc.profile, "postcard": pc})
@router.post("/postcards/{postcard_id}/edit")
@@ -853,8 +848,7 @@ def postcard_update(
Profile.user_id == user.id
).first()
if dup:
return templates.TemplateResponse("postcard_form.html", {
"request": request, "user": user, "profile": pc.profile, "postcard": pc,
return _render(request, "postcard_form.html", {"user": user, "profile": pc.profile, "postcard": pc,
"error": _t("pc_list.duplicate", user.language),
})
@@ -1000,8 +994,7 @@ def profile_showcase_page(
if not p:
return _redirect("/settings?tab=showcase")
cards = db.query(Postcard).filter(Postcard.profile_id == profile_id).all()
return templates.TemplateResponse("profile_showcase.html", {
"request": request, "user": user, "profile": p, "cards": cards,
return _render(request, "profile_showcase.html", {"user": user, "profile": p, "cards": cards,
})
@@ -1073,8 +1066,7 @@ def public_showcase(request: Request, username: str, db: Session = Depends(get_d
profile_groups.append({"profile": p, "sent": p_sent, "received": p_received})
sent_all.sort(key=lambda c: c.send_time or c.created_at, reverse=True)
received_all.sort(key=lambda c: c.receive_time or c.created_at, reverse=True)
resp = templates.TemplateResponse("showcase.html", {
"request": request, "profile_user": user, "mode": user.showcase_mode,
resp = _render(request, "showcase.html", {"profile_user": user, "mode": user.showcase_mode,
"profile_groups": profile_groups, "sent_all": sent_all, "received_all": received_all,
"lang": visitor_lang, "available_languages": _avail,
})
@@ -1104,8 +1096,7 @@ def apikey_create(
db.commit()
db.refresh(ak)
api_keys = db.query(ApiKey).filter(ApiKey.user_id == user.id).order_by(ApiKey.created_at.desc()).all()
return templates.TemplateResponse("settings.html", {
"request": request, "user": user, "active_tab": "apikeys",
return _render(request, "settings.html", {"user": user, "active_tab": "apikeys",
"api_keys": api_keys, "new_key": ak.key, "success": "API Key created",
})
@@ -1153,6 +1144,5 @@ def api_docs_page(
user: User = Depends(get_current_user_web),
db: Session = Depends(get_db),
):
return templates.TemplateResponse("api_docs.html", {
"request": request, "user": user,
return _render(request, "api_docs.html", {"user": user,
})