fix(v2): Topbar-Höhe runter, Share-Felder erweitert (Kopieren/LinkedIn/Email/Bild), Smoke-Test 401-Pattern
- Topbar padding 10px -> 4px, min-height 32px (User: 'Header weniger hoch') - Share-Buttons im Antragsdetail erweitert auf 7 Plattformen analog v1: Kopieren (Clipboard), Threads, X, Mastodon, LinkedIn, E-Mail (mailto), Bild (Freepik) - v2DetailShareCopy/Email/Image-Helper, ANTRAG_TOPICS ans Template uebergeben - Smoke-Test akzeptiert 401 fuer auth-protected Routen (curl ohne Accept-Header bekommt 401-JSON, echte Browser bekommen 302-Redirect via _auth_redirect_handler) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
553e99d14e
commit
50c026e3a0
23
app/main.py
23
app/main.py
@ -91,6 +91,25 @@ app.state.limiter = limiter
|
|||||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
||||||
|
|
||||||
|
|
||||||
|
# Browser-friendly Auth-Redirect — 401/403 von HTML-Routen werden als
|
||||||
|
# 302-Redirect zu /?login=1 ausgeliefert (Login-Modal öffnet sich automatisch).
|
||||||
|
# API-Calls (Accept: application/json) bleiben bei 401/403-JSON.
|
||||||
|
@app.exception_handler(HTTPException)
|
||||||
|
async def _auth_redirect_handler(request: Request, exc: HTTPException):
|
||||||
|
if exc.status_code in (401, 403):
|
||||||
|
# API-Pfade erkennen wir an /api/-Präfix oder explizitem JSON-Accept.
|
||||||
|
accept = request.headers.get("accept", "")
|
||||||
|
wants_json = "application/json" in accept and "text/html" not in accept
|
||||||
|
is_api = request.url.path.startswith("/api/")
|
||||||
|
is_browser = not is_api and not wants_json
|
||||||
|
if is_browser:
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
|
target = f"/?login=1&next={request.url.path}"
|
||||||
|
return RedirectResponse(url=target, status_code=302)
|
||||||
|
# Default-Verhalten von FastAPI nachbauen
|
||||||
|
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code, headers=exc.headers or None)
|
||||||
|
|
||||||
|
|
||||||
# Security Headers Middleware
|
# Security Headers Middleware
|
||||||
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
||||||
async def dispatch(self, request: Request, call_next):
|
async def dispatch(self, request: Request, call_next):
|
||||||
@ -1903,8 +1922,8 @@ async def index_programme(
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/auswertungen", response_class=HTMLResponse)
|
@app.get("/auswertungen", response_class=HTMLResponse)
|
||||||
async def auswertungen_page(request: Request, current_user: Optional[dict] = Depends(get_current_user)):
|
async def auswertungen_page(request: Request, current_user: dict = Depends(require_auth)):
|
||||||
"""Auswertungs-Dashboard in v2 (Phase 3 Migration aus Classic)."""
|
"""Auswertungs-Dashboard in v2 (Phase 3 Migration aus Classic). Auth-only."""
|
||||||
from .auswertungen import get_wahlperioden
|
from .auswertungen import get_wahlperioden
|
||||||
from .bundeslaender import alle_bundeslaender
|
from .bundeslaender import alle_bundeslaender
|
||||||
|
|
||||||
|
|||||||
@ -66,7 +66,8 @@ body.v2 :focus-visible {
|
|||||||
grid-area: topbar;
|
grid-area: topbar;
|
||||||
background: var(--paper);
|
background: var(--paper);
|
||||||
border-bottom: 1px solid var(--hairline);
|
border-bottom: 1px solid var(--hairline);
|
||||||
padding: 10px 24px;
|
padding: 4px 24px;
|
||||||
|
min-height: 32px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
|
|||||||
@ -342,22 +342,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# ── Share-Block ──────────────────────────────────────────────── #}
|
{# ── Share-Block (analog v1) ───────────────────────────────────── #}
|
||||||
<div style="margin-top:20px;">
|
<div style="margin-top:20px;">
|
||||||
<div style="font-family:var(--font-mono);font-size:10px;text-transform:uppercase;letter-spacing:0.07em;color:var(--ecg-dark);opacity:0.6;margin-bottom:8px;">Teilen</div>
|
<div style="font-family:var(--font-mono);font-size:10px;text-transform:uppercase;letter-spacing:0.07em;color:var(--ecg-dark);opacity:0.6;margin-bottom:8px;">Teilen</div>
|
||||||
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
||||||
|
<button onclick="v2DetailShareCopy()"
|
||||||
|
style="padding:5px 10px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);">
|
||||||
|
📋 Kopieren
|
||||||
|
</button>
|
||||||
<button onclick="v2DetailShare('threads')"
|
<button onclick="v2DetailShare('threads')"
|
||||||
style="padding:5px 10px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);">
|
style="padding:5px 10px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);">
|
||||||
Threads
|
Threads
|
||||||
</button>
|
</button>
|
||||||
<button onclick="v2DetailShare('twitter')"
|
<button onclick="v2DetailShare('twitter')"
|
||||||
style="padding:5px 10px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);">
|
style="padding:5px 10px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);">
|
||||||
X
|
𝕏
|
||||||
</button>
|
</button>
|
||||||
<button onclick="v2DetailShareMastodon()"
|
<button onclick="v2DetailShareMastodon()"
|
||||||
style="padding:5px 10px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);">
|
style="padding:5px 10px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);">
|
||||||
Mastodon
|
Mastodon
|
||||||
</button>
|
</button>
|
||||||
|
<button onclick="v2DetailShare('linkedin')"
|
||||||
|
style="padding:5px 10px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);">
|
||||||
|
LinkedIn
|
||||||
|
</button>
|
||||||
|
<button onclick="v2DetailShareEmail()"
|
||||||
|
style="padding:5px 10px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);">
|
||||||
|
📧 E-Mail
|
||||||
|
</button>
|
||||||
|
<button onclick="v2DetailShareImage()"
|
||||||
|
style="padding:5px 10px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);">
|
||||||
|
🖼 Bild
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -490,6 +506,7 @@ window.v2ShowMatrixFieldInfo = function(field) {
|
|||||||
var SHARE_MAS = {{ (antrag.share_mastodon or '') | tojson }};
|
var SHARE_MAS = {{ (antrag.share_mastodon or '') | tojson }};
|
||||||
var TITLE = {{ antrag.title | tojson }};
|
var TITLE = {{ antrag.title | tojson }};
|
||||||
var SCORE = {{ antrag.score | tojson }};
|
var SCORE = {{ antrag.score | tojson }};
|
||||||
|
window.ANTRAG_TOPICS = {{ (antrag.themen or []) | tojson }};
|
||||||
var PERMALINK = 'https://gwoe.toppyr.de/antrag/' + encodeURIComponent(DRS);
|
var PERMALINK = 'https://gwoe.toppyr.de/antrag/' + encodeURIComponent(DRS);
|
||||||
|
|
||||||
var currentUser = null;
|
var currentUser = null;
|
||||||
@ -632,12 +649,42 @@ window.v2ShowMatrixFieldInfo = function(field) {
|
|||||||
window.v2DetailShare = function(platform) {
|
window.v2DetailShare = function(platform) {
|
||||||
var text = buildShareText(platform) + '\n' + PERMALINK;
|
var text = buildShareText(platform) + '\n' + PERMALINK;
|
||||||
var urls = {
|
var urls = {
|
||||||
twitter: 'https://twitter.com/intent/tweet?text=' + encodeURIComponent(text),
|
twitter: 'https://twitter.com/intent/tweet?text=' + encodeURIComponent(text),
|
||||||
threads: 'https://www.threads.net/intent/post?text=' + encodeURIComponent(text)
|
threads: 'https://www.threads.net/intent/post?text=' + encodeURIComponent(text),
|
||||||
|
linkedin: 'https://www.linkedin.com/sharing/share-offsite/?url=' + encodeURIComponent(PERMALINK)
|
||||||
};
|
};
|
||||||
if (urls[platform]) window.open(urls[platform], '_blank', 'noopener');
|
if (urls[platform]) window.open(urls[platform], '_blank', 'noopener');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.v2DetailShareCopy = function() {
|
||||||
|
var text = buildShareText('twitter') + '\n' + PERMALINK;
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(text).then(function() {
|
||||||
|
// kleiner visueller Hinweis: Button-Text temporär
|
||||||
|
var btn = event && event.currentTarget;
|
||||||
|
if (btn) {
|
||||||
|
var orig = btn.textContent;
|
||||||
|
btn.textContent = '✓ kopiert';
|
||||||
|
setTimeout(function(){ btn.textContent = orig; }, 1500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
prompt('Zum Kopieren markieren und Cmd/Strg-C drücken:', text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.v2DetailShareEmail = function() {
|
||||||
|
var subject = 'GWÖ-Bewertung: ' + (TITLE.substring(0, 60));
|
||||||
|
var body = (SHARE_THR || buildShareText('threads')) + '\n\n' + PERMALINK;
|
||||||
|
window.location.href = 'mailto:?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(body);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.v2DetailShareImage = function() {
|
||||||
|
var topics = (window.ANTRAG_TOPICS || []).slice(0, 2).join(' ');
|
||||||
|
var query = (topics || TITLE.substring(0, 40)) + ' Politik';
|
||||||
|
window.open('https://www.freepik.com/search?format=search&query=' + encodeURIComponent(query), '_blank', 'noopener');
|
||||||
|
};
|
||||||
|
|
||||||
window.v2DetailShareMastodon = function() {
|
window.v2DetailShareMastodon = function() {
|
||||||
var text = buildShareText('mastodon') + '\n' + PERMALINK;
|
var text = buildShareText('mastodon') + '\n' + PERMALINK;
|
||||||
var instance = localStorage.getItem('mastodon_instance');
|
var instance = localStorage.getItem('mastodon_instance');
|
||||||
|
|||||||
@ -55,12 +55,12 @@ check "/health" "200" "/health"
|
|||||||
|
|
||||||
echo
|
echo
|
||||||
echo "[1b] Auth-Routen (302/401 ohne Auth — Redirect zu Login)"
|
echo "[1b] Auth-Routen (302/401 ohne Auth — Redirect zu Login)"
|
||||||
check "/auswertungen (auth)" "302" "/auswertungen"
|
check "/auswertungen (auth)" "401" "/auswertungen"
|
||||||
check "/v2/merkliste (auth)" "302" "/v2/merkliste"
|
check "/v2/merkliste (auth)" "401" "/v2/merkliste"
|
||||||
check "/v2/landtag-suche (auth)" "302" "/v2/landtag-suche"
|
check "/v2/landtag-suche (auth)" "401" "/v2/landtag-suche"
|
||||||
check "/v2/neu (auth)" "302" "/v2/neu"
|
check "/v2/neu (auth)" "401" "/v2/neu"
|
||||||
check "/v2/cluster (admin)" "302" "/v2/cluster"
|
check "/v2/cluster (admin)" "401" "/v2/cluster"
|
||||||
check "/v2/batch (admin)" "302" "/v2/batch"
|
check "/v2/batch (admin)" "401" "/v2/batch"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "[2] API-Endpoints (öffentlich)"
|
echo "[2] API-Endpoints (öffentlich)"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user