feat(ui): add error handling, flash messages and SEO optimization
- Add custom error pages (404, 403, 500) with user-friendly messages
- Add flash message system with signed cookies for security
- Add toast notifications with auto-dismiss and manual close
- Add comprehensive SEO meta tags (description, keywords, OG, Twitter)
- Add canonical URLs for SEO
- Update routes to use slug-based URLs (/posts/{slug} instead of /posts/{id})
- Add Open Graph and Twitter Card meta tags for social sharing
- Add favicon SVG
- Update all templates with proper meta tags and URLs
- Add error handlers registration in main.py
- Add flash middleware for request handling
- Install itsdangerous dependency
This commit is contained in:
188
app/presentation/web/error_handlers.py
Normal file
188
app/presentation/web/error_handlers.py
Normal file
@@ -0,0 +1,188 @@
|
||||
"""Error handlers and middleware for web UI.
|
||||
|
||||
This module provides custom error pages and flash message middleware
|
||||
for the web interface.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from app.presentation.web.flash import FlashManager, get_flash_messages
|
||||
|
||||
templates = Jinja2Templates(directory="app/presentation/templates")
|
||||
|
||||
|
||||
async def setup_flash_manager(request: Request) -> None:
|
||||
"""Setup flash manager on request state.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object.
|
||||
"""
|
||||
request.state.flash_manager = FlashManager(request)
|
||||
|
||||
|
||||
async def add_flash_to_response(request: Request, response: HTMLResponse) -> None:
|
||||
"""Add flash cookie to response if needed.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object.
|
||||
response: FastAPI response object.
|
||||
"""
|
||||
if hasattr(request.state, "flash_manager"):
|
||||
request.state.flash_manager.set_cookie(response)
|
||||
|
||||
|
||||
def get_template_context(request: Request) -> dict[str, Any]:
|
||||
"""Get base template context with flash messages.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object.
|
||||
|
||||
Returns:
|
||||
Template context dictionary.
|
||||
"""
|
||||
from app.presentation.web.deps import can_create_post, get_user_role
|
||||
|
||||
user = getattr(request.state, "user", None)
|
||||
user_role = get_user_role(user)
|
||||
|
||||
return {
|
||||
"request": request,
|
||||
"user": user,
|
||||
"user_role": user_role.value if user_role else None,
|
||||
"can_create": can_create_post(user),
|
||||
"flash_messages": get_flash_messages(request),
|
||||
}
|
||||
|
||||
|
||||
async def http_exception_handler(request: Request, exc: HTTPException) -> HTMLResponse:
|
||||
"""Handle HTTP exceptions with custom error pages.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object.
|
||||
exc: HTTPException instance.
|
||||
|
||||
Returns:
|
||||
HTMLResponse with error page.
|
||||
"""
|
||||
# Handle redirects (307, 308)
|
||||
if exc.status_code in (307, 308) and "Location" in exc.headers:
|
||||
return RedirectResponse(url=exc.headers["Location"], status_code=exc.status_code)
|
||||
|
||||
error_pages = {
|
||||
403: ("Access Denied", "You don't have permission to access this page."),
|
||||
404: ("Page Not Found", "The page you're looking for doesn't exist."),
|
||||
500: ("Server Error", "Something went wrong on our end. Please try again later."),
|
||||
}
|
||||
|
||||
error_title, error_message = error_pages.get(
|
||||
exc.status_code, ("Error", exc.detail or "An unexpected error occurred.")
|
||||
)
|
||||
|
||||
context = get_template_context(request)
|
||||
context.update(
|
||||
{
|
||||
"error_code": exc.status_code,
|
||||
"error_title": error_title,
|
||||
"error_message": error_message,
|
||||
}
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"pages/error.html",
|
||||
context,
|
||||
status_code=exc.status_code,
|
||||
)
|
||||
|
||||
|
||||
async def not_found_handler(request: Request, exc: HTTPException) -> HTMLResponse:
|
||||
"""Handle 404 Not Found errors.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object.
|
||||
exc: HTTPException instance.
|
||||
|
||||
Returns:
|
||||
HTMLResponse with 404 page.
|
||||
"""
|
||||
context = get_template_context(request)
|
||||
context.update(
|
||||
{
|
||||
"error_code": 404,
|
||||
"error_title": "Page Not Found",
|
||||
"error_message": "The page you're looking for doesn't exist or has been moved.",
|
||||
}
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"pages/error.html",
|
||||
context,
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
|
||||
async def forbidden_handler(request: Request, exc: HTTPException) -> HTMLResponse:
|
||||
"""Handle 403 Forbidden errors.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object.
|
||||
exc: HTTPException instance.
|
||||
|
||||
Returns:
|
||||
HTMLResponse with 403 page.
|
||||
"""
|
||||
context = get_template_context(request)
|
||||
context.update(
|
||||
{
|
||||
"error_code": 403,
|
||||
"error_title": "Access Denied",
|
||||
"error_message": "You don't have permission to access this resource. Please sign in or contact an administrator.",
|
||||
}
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"pages/error.html",
|
||||
context,
|
||||
status_code=403,
|
||||
)
|
||||
|
||||
|
||||
async def server_error_handler(request: Request, exc: Exception) -> HTMLResponse:
|
||||
"""Handle 500 Internal Server Error.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object.
|
||||
exc: Exception instance.
|
||||
|
||||
Returns:
|
||||
HTMLResponse with 500 page.
|
||||
"""
|
||||
context = get_template_context(request)
|
||||
context.update(
|
||||
{
|
||||
"error_code": 500,
|
||||
"error_title": "Server Error",
|
||||
"error_message": "Something went wrong on our end. Please try again later or contact support if the problem persists.",
|
||||
}
|
||||
)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"pages/error.html",
|
||||
context,
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
|
||||
def register_error_handlers(app) -> None:
|
||||
"""Register error handlers with FastAPI app.
|
||||
|
||||
Args:
|
||||
app: FastAPI application instance.
|
||||
"""
|
||||
app.add_exception_handler(404, not_found_handler)
|
||||
app.add_exception_handler(403, forbidden_handler)
|
||||
app.add_exception_handler(500, server_error_handler)
|
||||
app.add_exception_handler(HTTPException, http_exception_handler)
|
||||
Reference in New Issue
Block a user