fix(ui): render article images via authenticated proxy thumbnails
This commit is contained in:
parent
efaf132936
commit
910ca72c81
4 changed files with 69 additions and 3 deletions
|
|
@ -4,9 +4,10 @@ import json
|
|||
from pathlib import Path
|
||||
import re
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import Request as UrlRequest, urlopen
|
||||
|
||||
from fastapi import APIRouter, Form, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, Response
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from .auth import create_session_token, verify_credentials, verify_session_token
|
||||
|
|
@ -41,6 +42,7 @@ ALLOWED_TRANSITIONS: dict[str, tuple[str, ...]] = {
|
|||
"published": ("error",),
|
||||
"error": ("review", "rewrite"),
|
||||
}
|
||||
IMAGE_PROXY_USER_AGENT = "rss-news-admin/1.0"
|
||||
|
||||
|
||||
def _admin_user(request: Request) -> str | None:
|
||||
|
|
@ -134,6 +136,7 @@ def _build_image_entries(article: dict, extraction: dict, meta: dict) -> list[di
|
|||
entries.append(
|
||||
{
|
||||
"url": url,
|
||||
"proxy_url": f"/admin/images/proxy?{urlencode({'url': url})}",
|
||||
"is_selected": selected_url == url,
|
||||
"is_excluded": url in excluded_set,
|
||||
"is_irrelevant_hint": _is_probably_irrelevant_image(url),
|
||||
|
|
@ -276,6 +279,9 @@ def admin_dashboard(request: Request):
|
|||
article["image_entries"] = _build_image_entries(article, extraction, meta)
|
||||
image_review = meta.get("image_review") if isinstance(meta.get("image_review"), dict) else {}
|
||||
article["selected_image_url"] = image_review.get("selected_url") if isinstance(image_review.get("selected_url"), str) else None
|
||||
article["selected_image_proxy_url"] = (
|
||||
f"/admin/images/proxy?{urlencode({'url': article['selected_image_url']})}" if article.get("selected_image_url") else None
|
||||
)
|
||||
if not article.get("press_contact") and isinstance(extraction.get("press_contact"), str):
|
||||
article["press_contact"] = extraction.get("press_contact")
|
||||
article["extraction_error"] = extraction.get("extraction_error") if isinstance(extraction.get("extraction_error"), str) else None
|
||||
|
|
@ -323,6 +329,9 @@ def admin_article_detail(request: Request, article_id: int):
|
|||
article["image_entries"] = _build_image_entries(article, extraction, meta)
|
||||
image_review = meta.get("image_review") if isinstance(meta.get("image_review"), dict) else {}
|
||||
article["selected_image_url"] = image_review.get("selected_url") if isinstance(image_review.get("selected_url"), str) else None
|
||||
article["selected_image_proxy_url"] = (
|
||||
f"/admin/images/proxy?{urlencode({'url': article['selected_image_url']})}" if article.get("selected_image_url") else None
|
||||
)
|
||||
article["days_old"] = article_age_days(article.get("published_at"))
|
||||
article["relevance"] = article_relevance(article.get("published_at"))
|
||||
feed = get_feed_by_id(int(article["feed_id"])) if article.get("feed_id") else None
|
||||
|
|
@ -360,6 +369,28 @@ def admin_article_image_decision(
|
|||
return RedirectResponse(url=f"/admin/articles/{article_id}", status_code=303)
|
||||
|
||||
|
||||
@router.get("/admin/images/proxy")
|
||||
def admin_image_proxy(request: Request, url: str):
|
||||
user = _admin_user(request)
|
||||
if not user:
|
||||
return Response(status_code=401)
|
||||
|
||||
if not (url.startswith("http://") or url.startswith("https://")):
|
||||
return Response(status_code=400)
|
||||
|
||||
try:
|
||||
req = UrlRequest(url=url, headers={"User-Agent": IMAGE_PROXY_USER_AGENT, "Referer": url})
|
||||
with urlopen(req, timeout=10) as resp:
|
||||
body = resp.read()
|
||||
content_type = resp.headers.get("Content-Type", "application/octet-stream")
|
||||
except Exception:
|
||||
return Response(status_code=404)
|
||||
|
||||
if not content_type.lower().startswith("image/"):
|
||||
return Response(status_code=415)
|
||||
return Response(content=body, media_type=content_type)
|
||||
|
||||
|
||||
@router.post("/admin/articles/{article_id}/legal-review")
|
||||
def admin_article_legal_review(request: Request, article_id: int, approved: str = Form("0"), note: str = Form("")):
|
||||
user = _admin_user(request)
|
||||
|
|
|
|||
|
|
@ -70,13 +70,16 @@
|
|||
<p><strong>Bilder:</strong> {{ article.image_entries|length if article.image_entries else 0 }}</p>
|
||||
{% if article.selected_image_url %}
|
||||
<p><strong>Ausgewähltes Hauptbild:</strong> <a href="{{ article.selected_image_url }}" target="_blank" rel="noopener">{{ article.selected_image_url }}</a></p>
|
||||
{% if article.selected_image_proxy_url %}
|
||||
<img src="{{ article.selected_image_proxy_url }}" alt="Ausgewähltes Hauptbild" class="thumb" loading="lazy" />
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if article.image_entries %}
|
||||
<div class="image-grid">
|
||||
{% for image in article.image_entries %}
|
||||
<article class="image-card {{ 'image-selected' if image.is_selected else '' }} {{ 'image-excluded' if image.is_excluded else '' }}">
|
||||
<a href="{{ image.url }}" target="_blank" rel="noopener">
|
||||
<img src="{{ image.url }}" alt="Artikelbild" loading="lazy" />
|
||||
<img src="{{ image.proxy_url }}" alt="Artikelbild" loading="lazy" />
|
||||
</a>
|
||||
<div class="image-meta">
|
||||
{% if image.is_selected %}<span class="badge ok">Ausgewählt</span>{% endif %}
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@
|
|||
<div class="subtle">Legal: {{ "OK" if a.legal_checked else "offen" }}</div>
|
||||
{% if a.selected_image_url %}
|
||||
<div class="subtle">Hauptbild gesetzt</div>
|
||||
<a href="{{ a.selected_image_url }}" target="_blank" rel="noopener"><img src="{{ a.selected_image_url }}" alt="Hauptbild" class="thumb" loading="lazy" /></a>
|
||||
<a href="{{ a.selected_image_url }}" target="_blank" rel="noopener"><img src="{{ a.selected_image_proxy_url }}" alt="Hauptbild" class="thumb" loading="lazy" /></a>
|
||||
{% endif %}
|
||||
{% if a.summary %}
|
||||
<div><strong>Summary:</strong> {{ a.summary }}</div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import os
|
|||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
|
@ -139,6 +140,37 @@ class TestAdminUi(unittest.TestCase):
|
|||
self.assertIsNotNone(article)
|
||||
self.assertIn("selected_url", article.get("meta_json", ""))
|
||||
|
||||
@patch("backend.app.admin_ui.urlopen")
|
||||
def test_image_proxy_returns_image_data(self, mock_urlopen) -> None:
|
||||
class _FakeHeaders:
|
||||
def get(self, key: str, default=None):
|
||||
if key.lower() == "content-type":
|
||||
return "image/jpeg"
|
||||
return default
|
||||
|
||||
class _FakeResponse:
|
||||
headers = _FakeHeaders()
|
||||
|
||||
def read(self):
|
||||
return b"\xff\xd8\xff\xd9"
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
return False
|
||||
|
||||
mock_urlopen.return_value = _FakeResponse()
|
||||
|
||||
self.client.post(
|
||||
"/admin/login",
|
||||
data={"username": "admin", "password": "secret"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
res = self.client.get("/admin/images/proxy?url=https%3A%2F%2Fexample.org%2Fimg.jpg")
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertIn("image/jpeg", res.headers.get("content-type", ""))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue