small performance and security changes
This commit is contained in:
@@ -6,6 +6,7 @@ from fastapi import FastAPI
|
|||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
from starlette.middleware.gzip import GZipMiddleware
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from src.config import settings
|
from src.config import settings
|
||||||
@@ -49,6 +50,36 @@ async def lifespan(app: FastAPI):
|
|||||||
app = FastAPI(title="LinkedIn Post Creation System", lifespan=lifespan)
|
app = FastAPI(title="LinkedIn Post Creation System", lifespan=lifespan)
|
||||||
|
|
||||||
|
|
||||||
|
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
||||||
|
"""Add security headers to every response."""
|
||||||
|
|
||||||
|
# CSP allows inline scripts/styles (app uses them extensively in Jinja2 templates).
|
||||||
|
# frame-ancestors 'none' replaces X-Frame-Options for modern browsers.
|
||||||
|
_CSP = (
|
||||||
|
"default-src 'self'; "
|
||||||
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
|
||||||
|
"style-src 'self' 'unsafe-inline'; "
|
||||||
|
"img-src 'self' data: blob: https://*.supabase.co https://*.linkedin.com https://media.licdn.com; "
|
||||||
|
"connect-src 'self' https://*.supabase.co; "
|
||||||
|
"font-src 'self' data:; "
|
||||||
|
"frame-ancestors 'none'; "
|
||||||
|
"base-uri 'self'; "
|
||||||
|
"form-action 'self';"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def dispatch(self, request, call_next):
|
||||||
|
response = await call_next(request)
|
||||||
|
h = response.headers
|
||||||
|
h["X-Frame-Options"] = "DENY"
|
||||||
|
h["X-Content-Type-Options"] = "nosniff"
|
||||||
|
h["X-XSS-Protection"] = "1; mode=block"
|
||||||
|
h["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
||||||
|
h["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()"
|
||||||
|
h["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
|
||||||
|
h["Content-Security-Policy"] = self._CSP
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
class StaticCacheMiddleware(BaseHTTPMiddleware):
|
class StaticCacheMiddleware(BaseHTTPMiddleware):
|
||||||
"""Set long-lived Cache-Control headers on static assets."""
|
"""Set long-lived Cache-Control headers on static assets."""
|
||||||
|
|
||||||
@@ -64,7 +95,11 @@ class StaticCacheMiddleware(BaseHTTPMiddleware):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
# Middleware executes in reverse registration order (last added = outermost).
|
||||||
|
# Order: GZip compresses → StaticCache sets headers → SecurityHeaders sets headers.
|
||||||
|
app.add_middleware(SecurityHeadersMiddleware)
|
||||||
app.add_middleware(StaticCacheMiddleware)
|
app.add_middleware(StaticCacheMiddleware)
|
||||||
|
app.add_middleware(GZipMiddleware, minimum_size=500)
|
||||||
|
|
||||||
# Static files
|
# Static files
|
||||||
app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static")
|
app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static")
|
||||||
|
|||||||
@@ -171,10 +171,19 @@
|
|||||||
<h1 class="text-2xl font-bold text-white mb-1">Meine Posts</h1>
|
<h1 class="text-2xl font-bold text-white mb-1">Meine Posts</h1>
|
||||||
<p class="text-gray-400 text-sm">Ziehe Posts zwischen den Spalten um den Status zu ändern</p>
|
<p class="text-gray-400 text-sm">Ziehe Posts zwischen den Spalten um den Status zu ändern</p>
|
||||||
</div>
|
</div>
|
||||||
<a href="/create" class="px-4 py-2.5 btn-primary rounded-lg font-medium flex items-center gap-2">
|
<div class="flex items-center gap-3">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
|
{% if archived_count and archived_count > 0 %}
|
||||||
Neuer Post
|
<a href="/posts/archive" class="px-4 py-2.5 rounded-lg font-medium flex items-center gap-2 text-sm border border-gray-600 text-gray-300 hover:border-gray-400 hover:text-white transition-colors">
|
||||||
</a>
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8l1 12a2 2 0 002 2h8a2 2 0 002-2L19 8M10 12v4M14 12v4"/></svg>
|
||||||
|
Archiv
|
||||||
|
<span class="bg-gray-700 text-gray-300 text-xs px-1.5 py-0.5 rounded-full">{{ archived_count }}</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="/create" class="px-4 py-2.5 btn-primary rounded-lg font-medium flex items-center gap-2">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
|
||||||
|
Neuer Post
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if error %}
|
{% if error %}
|
||||||
@@ -253,8 +262,17 @@
|
|||||||
<div class="w-20 h-20 bg-brand-bg rounded-2xl flex items-center justify-center mx-auto mb-6">
|
<div class="w-20 h-20 bg-brand-bg rounded-2xl flex items-center justify-center mx-auto mb-6">
|
||||||
<svg class="w-10 h-10 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
<svg class="w-10 h-10 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
|
{% if archived_count and archived_count > 0 %}
|
||||||
|
<h3 class="text-xl font-semibold text-white mb-2">Alle Posts veröffentlicht</h3>
|
||||||
|
<p class="text-gray-400 mb-6 max-w-md mx-auto">Keine aktiven Posts. {{ archived_count }} Post{{ 's' if archived_count != 1 else '' }} im Archiv.</p>
|
||||||
|
<a href="/posts/archive" class="inline-flex items-center gap-2 px-6 py-3 border border-gray-600 text-gray-300 hover:text-white font-medium rounded-lg transition-colors mr-3">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8l1 12a2 2 0 002 2h8a2 2 0 002-2L19 8M10 12v4M14 12v4"/></svg>
|
||||||
|
Archiv ansehen
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
<h3 class="text-xl font-semibold text-white mb-2">Noch keine Posts</h3>
|
<h3 class="text-xl font-semibold text-white mb-2">Noch keine Posts</h3>
|
||||||
<p class="text-gray-400 mb-6 max-w-md mx-auto">Erstelle deinen ersten LinkedIn Post mit KI-Unterstützung.</p>
|
<p class="text-gray-400 mb-6 max-w-md mx-auto">Erstelle deinen ersten LinkedIn Post mit KI-Unterstützung.</p>
|
||||||
|
{% endif %}
|
||||||
<a href="/create" class="inline-flex items-center gap-2 px-6 py-3 btn-primary font-medium rounded-lg transition-colors">
|
<a href="/create" class="inline-flex items-center gap-2 px-6 py-3 btn-primary font-medium rounded-lg transition-colors">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
|
||||||
Post erstellen
|
Post erstellen
|
||||||
|
|||||||
118
src/web/templates/user/posts_archive.html
Normal file
118
src/web/templates/user/posts_archive.html
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Post-Archiv - LinkedIn Posts{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="mb-6 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-3 mb-1">
|
||||||
|
<a href="/posts" class="text-gray-400 hover:text-white transition-colors flex items-center gap-1 text-sm">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
|
||||||
|
Zurück
|
||||||
|
</a>
|
||||||
|
<h1 class="text-2xl font-bold text-white">Post-Archiv</h1>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-400 text-sm">Veröffentlichte, geplante und abgelehnte Posts ({{ total }} gesamt)</p>
|
||||||
|
</div>
|
||||||
|
<a href="/create" class="px-4 py-2.5 btn-primary rounded-lg font-medium flex items-center gap-2">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/></svg>
|
||||||
|
Neuer Post
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<div class="bg-red-900/50 border border-red-500 text-red-200 px-4 py-3 rounded-lg mb-6">
|
||||||
|
<strong>Error:</strong> {{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not posts and not error %}
|
||||||
|
<div class="card-bg rounded-xl border p-12 text-center">
|
||||||
|
<div class="w-20 h-20 bg-brand-bg rounded-2xl flex items-center justify-center mx-auto mb-6">
|
||||||
|
<svg class="w-10 h-10 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8l1 12a2 2 0 002 2h8a2 2 0 002-2L19 8M10 12v4M14 12v4"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-white mb-2">Kein Archiv vorhanden</h3>
|
||||||
|
<p class="text-gray-400 mb-6 max-w-md mx-auto">Noch keine veröffentlichten oder abgelehnten Posts.</p>
|
||||||
|
<a href="/posts" class="inline-flex items-center gap-2 px-6 py-3 btn-primary font-medium rounded-lg transition-colors">
|
||||||
|
Zurück zu den Posts
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
{% for post in posts %}
|
||||||
|
<a href="/posts/{{ post.id }}" class="block card-bg rounded-xl border hover:border-gray-500 transition-colors p-4">
|
||||||
|
<div class="flex items-start justify-between gap-4">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center gap-2 mb-1">
|
||||||
|
{% if post.status == 'published' %}
|
||||||
|
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-green-900/50 text-green-300 border border-green-800">
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
||||||
|
Veröffentlicht
|
||||||
|
</span>
|
||||||
|
{% elif post.status == 'scheduled' %}
|
||||||
|
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-blue-900/50 text-blue-300 border border-blue-800">
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||||
|
Geplant
|
||||||
|
</span>
|
||||||
|
{% elif post.status == 'rejected' %}
|
||||||
|
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-red-900/50 text-red-300 border border-red-800">
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||||
|
Abgelehnt
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
<h4 class="font-medium text-white truncate">{{ post.topic_title or 'Untitled' }}</h4>
|
||||||
|
</div>
|
||||||
|
{% if post.post_content %}
|
||||||
|
<p class="text-gray-400 text-sm line-clamp-2">{{ post.post_content[:200] }}{% if post.post_content | length > 200 %}...{% endif %}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0 text-right text-xs text-gray-500 space-y-1">
|
||||||
|
{% if post.status == 'published' and post.published_at %}
|
||||||
|
<div>veröffentlicht {{ post.published_at.strftime('%d.%m.%Y') }}</div>
|
||||||
|
{% elif post.status == 'scheduled' and post.scheduled_at %}
|
||||||
|
<div>geplant für {{ post.scheduled_at.strftime('%d.%m.%Y %H:%M') }}</div>
|
||||||
|
{% else %}
|
||||||
|
<div>erstellt {{ post.created_at.strftime('%d.%m.%Y') if post.created_at else 'N/A' }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if post.critic_feedback and post.critic_feedback | length > 0 and post.critic_feedback[-1].get('overall_score') is not none %}
|
||||||
|
{% set score = post.critic_feedback[-1].get('overall_score', 0) %}
|
||||||
|
<div class="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-semibold
|
||||||
|
{{ 'bg-green-900/50 text-green-300' if score >= 85 else 'bg-yellow-900/50 text-yellow-300' if score >= 70 else 'bg-red-900/50 text-red-300' }}">
|
||||||
|
{{ score }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if total_pages > 1 %}
|
||||||
|
<div class="flex items-center justify-center gap-2 mt-8">
|
||||||
|
{% if current_page > 1 %}
|
||||||
|
<a href="/posts/archive?page={{ current_page - 1 }}" class="px-3 py-2 rounded-lg border border-gray-600 text-gray-300 hover:text-white hover:border-gray-400 transition-colors text-sm">
|
||||||
|
← Zurück
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for p in range(1, total_pages + 1) %}
|
||||||
|
{% if p == current_page %}
|
||||||
|
<span class="px-3 py-2 rounded-lg bg-brand-bg text-white text-sm font-medium border border-yellow-500">{{ p }}</span>
|
||||||
|
{% elif p <= 2 or p >= total_pages - 1 or (p >= current_page - 2 and p <= current_page + 2) %}
|
||||||
|
<a href="/posts/archive?page={{ p }}" class="px-3 py-2 rounded-lg border border-gray-600 text-gray-300 hover:text-white hover:border-gray-400 transition-colors text-sm">{{ p }}</a>
|
||||||
|
{% elif p == 3 or p == total_pages - 2 %}
|
||||||
|
<span class="text-gray-500 px-1">…</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if current_page < total_pages %}
|
||||||
|
<a href="/posts/archive?page={{ current_page + 1 }}" class="px-3 py-2 rounded-lg border border-gray-600 text-gray-300 hover:text-white hover:border-gray-400 transition-colors text-sm">
|
||||||
|
Weiter →
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<p class="text-center text-xs text-gray-500 mt-2">Seite {{ current_page }} von {{ total_pages }} ({{ total }} Posts)</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -229,7 +229,8 @@ def set_user_session(response: Response, session: UserSession, access_token: str
|
|||||||
value=session.to_cookie_value(),
|
value=session.to_cookie_value(),
|
||||||
httponly=True,
|
httponly=True,
|
||||||
max_age=60 * 60 * 24 * 7,
|
max_age=60 * 60 * 24 * 7,
|
||||||
samesite="lax"
|
samesite="lax",
|
||||||
|
secure=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1338,9 +1338,13 @@ async def dashboard(request: Request):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
_ACTIVE_STATUSES = {"draft", "approved", "ready"}
|
||||||
|
_ARCHIVE_STATUSES = {"scheduled", "published", "rejected"}
|
||||||
|
|
||||||
|
|
||||||
@user_router.get("/posts", response_class=HTMLResponse)
|
@user_router.get("/posts", response_class=HTMLResponse)
|
||||||
async def posts_page(request: Request):
|
async def posts_page(request: Request):
|
||||||
"""View user's own posts."""
|
"""View user's own posts (active Kanban only)."""
|
||||||
session = require_user_session(request)
|
session = require_user_session(request)
|
||||||
if not session:
|
if not session:
|
||||||
return RedirectResponse(url="/login", status_code=302)
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
@@ -1348,7 +1352,9 @@ async def posts_page(request: Request):
|
|||||||
try:
|
try:
|
||||||
user_id = UUID(session.user_id)
|
user_id = UUID(session.user_id)
|
||||||
profile = await db.get_profile(user_id)
|
profile = await db.get_profile(user_id)
|
||||||
posts = await db.get_generated_posts(user_id)
|
all_posts = await db.get_generated_posts(user_id)
|
||||||
|
active_posts = [p for p in all_posts if p.status in _ACTIVE_STATUSES]
|
||||||
|
archived_count = sum(1 for p in all_posts if p.status in _ARCHIVE_STATUSES)
|
||||||
profile_picture = await get_user_avatar(session, user_id)
|
profile_picture = await get_user_avatar(session, user_id)
|
||||||
|
|
||||||
return templates.TemplateResponse("posts.html", {
|
return templates.TemplateResponse("posts.html", {
|
||||||
@@ -1356,8 +1362,9 @@ async def posts_page(request: Request):
|
|||||||
"page": "posts",
|
"page": "posts",
|
||||||
"session": session,
|
"session": session,
|
||||||
"profile": profile,
|
"profile": profile,
|
||||||
"posts": posts,
|
"posts": active_posts,
|
||||||
"total_posts": len(posts),
|
"total_posts": len(active_posts),
|
||||||
|
"archived_count": archived_count,
|
||||||
"profile_picture": profile_picture
|
"profile_picture": profile_picture
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1370,6 +1377,61 @@ async def posts_page(request: Request):
|
|||||||
"session": session,
|
"session": session,
|
||||||
"posts": [],
|
"posts": [],
|
||||||
"total_posts": 0,
|
"total_posts": 0,
|
||||||
|
"archived_count": 0,
|
||||||
|
"error": str(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@user_router.get("/posts/archive", response_class=HTMLResponse)
|
||||||
|
async def posts_archive_page(request: Request, page: int = 1):
|
||||||
|
"""View archived posts (published, scheduled, rejected)."""
|
||||||
|
session = require_user_session(request)
|
||||||
|
if not session:
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_id = UUID(session.user_id)
|
||||||
|
all_posts = await db.get_generated_posts(user_id)
|
||||||
|
archived_posts = [p for p in all_posts if p.status in _ARCHIVE_STATUSES]
|
||||||
|
# Sort: scheduled first (upcoming), then by published_at/created_at desc
|
||||||
|
archived_posts.sort(
|
||||||
|
key=lambda p: (
|
||||||
|
p.status != "scheduled",
|
||||||
|
-(p.published_at or p.created_at or datetime.min.replace(tzinfo=timezone.utc)).timestamp()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
per_page = 20
|
||||||
|
total = len(archived_posts)
|
||||||
|
total_pages = max(1, (total + per_page - 1) // per_page)
|
||||||
|
page = max(1, min(page, total_pages))
|
||||||
|
start = (page - 1) * per_page
|
||||||
|
page_posts = archived_posts[start:start + per_page]
|
||||||
|
|
||||||
|
profile_picture = await get_user_avatar(session, user_id)
|
||||||
|
|
||||||
|
return templates.TemplateResponse("posts_archive.html", {
|
||||||
|
"request": request,
|
||||||
|
"page": "posts",
|
||||||
|
"session": session,
|
||||||
|
"posts": page_posts,
|
||||||
|
"total": total,
|
||||||
|
"current_page": page,
|
||||||
|
"total_pages": total_pages,
|
||||||
|
"per_page": per_page,
|
||||||
|
"profile_picture": profile_picture
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error loading posts archive: {e}")
|
||||||
|
return templates.TemplateResponse("posts_archive.html", {
|
||||||
|
"request": request,
|
||||||
|
"page": "posts",
|
||||||
|
"session": session,
|
||||||
|
"posts": [],
|
||||||
|
"total": 0,
|
||||||
|
"current_page": 1,
|
||||||
|
"total_pages": 1,
|
||||||
|
"per_page": 20,
|
||||||
"error": str(e)
|
"error": str(e)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1633,6 +1695,7 @@ async def post_detail_page(request: Request, post_id: str):
|
|||||||
"post_type_analysis": post_type_analysis,
|
"post_type_analysis": post_type_analysis,
|
||||||
"final_feedback": final_feedback,
|
"final_feedback": final_feedback,
|
||||||
"profile_picture_url": profile_picture_url,
|
"profile_picture_url": profile_picture_url,
|
||||||
|
"profile_picture": profile_picture_url,
|
||||||
"media_items_dict": media_items_dict,
|
"media_items_dict": media_items_dict,
|
||||||
"limit_reached": limit_reached,
|
"limit_reached": limit_reached,
|
||||||
"limit_message": limit_message
|
"limit_message": limit_message
|
||||||
|
|||||||
Reference in New Issue
Block a user