"""Web UI routes for blog application with authentication. This module provides HTML endpoints for the blog web interface with role-based access control and user authentication. """ from datetime import datetime from typing import Any from uuid import uuid4 from fastapi import APIRouter, HTTPException, Request from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates from app.infrastructure.auth import TokenInfo from app.presentation.web.deps import ( OptionalUserDep, RequireUserDep, can_create_post, can_delete_post, can_edit_post, can_see_draft, get_user_role, ) router = APIRouter(prefix="/web", tags=["web"]) templates = Jinja2Templates(directory="app/presentation/templates") class MockPost: """Mock post object for UI demonstration. This class simulates a Post entity for template rendering before integration with actual use cases. Attributes: id: Unique identifier for the post. title: Post title value object. content: Post content value object. slug: URL-friendly slug. author_id: Identifier of the post author. published: Publication status flag. tags: List of tags associated with the post. created_at: Timestamp when the post was created. updated_at: Timestamp when the post was last updated. """ def __init__( self, id: str, title: str, content: str, slug: str, author_id: str, published: bool, tags: list[str], created_at: datetime | None = None, ) -> None: """Initialize mock post with provided attributes. Args: id: Unique identifier for the post. title: Post title string. content: Post content string. slug: URL-friendly slug string. author_id: Author identifier string. published: Whether the post is published. tags: List of tag strings. created_at: Optional creation timestamp, defaults to now. """ self.id = id self.title = MockValueObject(title) self.content = MockValueObject(content) self.slug = MockValueObject(slug) self.author_id = author_id self.published = published self.tags = tags self.created_at = created_at or datetime.now() self.updated_at = self.created_at class MockValueObject: """Mock value object for simulating domain value objects. Wraps a raw value to simulate the interface of domain value objects like Title, Content, and Slug. Attributes: value: The wrapped string value. """ def __init__(self, value: str) -> None: """Initialize with a string value. Args: value: The string value to wrap. """ self.value = value MOCK_POSTS = [ MockPost( id=str(uuid4()), title="Getting Started with FastAPI", content="FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. It is designed to be easy to use while providing high performance.", slug="getting-started-with-fastapi", author_id="john_doe", published=True, tags=["python", "fastapi", "tutorial"], created_at=datetime(2026, 1, 15, 10, 30), ), MockPost( id=str(uuid4()), title="Understanding DDD Architecture", content="Domain-Driven Design (DDD) is an approach to software development that centers the development on programming a domain model that has a rich understanding of the processes and rules of a domain. The term was coined by Eric Evans in his book of the same title.", slug="understanding-ddd-architecture", author_id="jane_smith", published=True, tags=["ddd", "architecture", "software-design"], created_at=datetime(2026, 1, 14, 14, 45), ), MockPost( id=str(uuid4()), title="Draft Post Example", content="This is a draft post that hasn't been published yet. It demonstrates how unpublished posts appear in the UI.", slug="draft-post-example", author_id="john_doe", published=False, tags=["draft"], created_at=datetime(2026, 1, 13, 9, 0), ), ] def get_base_context(user: TokenInfo | None) -> dict[str, Any]: """Get base template context with user info and permissions. Args: user: Current user or None for guest. Returns: Dictionary with user, user_role, and can_create flags. """ user_role = get_user_role(user) return { "user": user, "user_role": user_role.value if user_role else None, "can_create": can_create_post(user), } def filter_visible_posts(posts: list[MockPost], user: TokenInfo | None) -> list[MockPost]: """Filter posts based on user permissions. Args: posts: List of all posts. user: Current user or None for guest. Returns: Filtered list of posts visible to the user. """ visible_posts = [] for post in posts: if post.published or can_see_draft(user, post.author_id): visible_posts.append(post) return visible_posts @router.get("/", response_class=HTMLResponse) async def home( request: Request, user: OptionalUserDep, ) -> HTMLResponse: """Render the home page with list of posts. Args: request: The HTTP request object for template context. user: Current user from dependency. Returns: HTMLResponse with rendered posts list template. """ context = get_base_context(user) visible_posts = filter_visible_posts(MOCK_POSTS, user) return templates.TemplateResponse( request, "pages/index.html", { **context, "posts": visible_posts, "active_page": "home", "current_page": 1, "has_prev": False, "has_next": False, }, ) @router.get("/posts", response_class=HTMLResponse) async def list_posts( request: Request, user: OptionalUserDep, ) -> HTMLResponse: """Render the posts listing page. Args: request: The HTTP request object for template context. user: Current user from dependency. Returns: HTMLResponse with rendered posts list template. """ context = get_base_context(user) visible_posts = filter_visible_posts(MOCK_POSTS, user) return templates.TemplateResponse( request, "pages/index.html", { **context, "posts": visible_posts, "active_page": "posts", "current_page": 1, "has_prev": False, "has_next": True, }, ) @router.get("/posts/new", response_class=HTMLResponse) async def new_post_form( request: Request, user: RequireUserDep, ) -> HTMLResponse: """Render the new post creation form. Args: request: The HTTP request object for template context. user: Current user (required). Returns: HTMLResponse with rendered post form template. """ context = get_base_context(user) return templates.TemplateResponse( request, "pages/post_form.html", { **context, "is_edit": False, "post": None, "active_page": "posts", }, ) @router.post("/posts/new", response_class=HTMLResponse) async def create_post( request: Request, user: RequireUserDep, ) -> HTMLResponse: """Handle new post creation form submission. Args: request: The HTTP request object containing form data. user: Current user (required). Returns: HTMLResponse redirecting to the home page. """ context = get_base_context(user) visible_posts = filter_visible_posts(MOCK_POSTS, user) return templates.TemplateResponse( request, "pages/index.html", { **context, "posts": visible_posts, "active_page": "home", "current_page": 1, "has_prev": False, "has_next": False, }, ) @router.get("/posts/{post_id}", response_class=HTMLResponse) async def post_detail( request: Request, post_id: str, user: OptionalUserDep, ) -> HTMLResponse: """Render a single post detail page. Args: request: The HTTP request object for template context. post_id: The unique identifier of the post to display. user: Current user from dependency. Returns: HTMLResponse with rendered post detail template. Raises: HTTPException: If post not found or not visible to user. """ post = next((p for p in MOCK_POSTS if str(p.id) == post_id), None) if not post: raise HTTPException(status_code=404, detail="Post not found") if not post.published and not can_see_draft(user, post.author_id): raise HTTPException(status_code=404, detail="Post not found") context = get_base_context(user) return templates.TemplateResponse( request, "pages/post_detail.html", { **context, "post": post, "active_page": "posts", "can_edit": can_edit_post(user, post.author_id), "can_delete": can_delete_post(user, post.author_id), }, ) @router.get("/posts/{post_id}/edit", response_class=HTMLResponse) async def edit_post_form( request: Request, post_id: str, user: RequireUserDep, ) -> HTMLResponse: """Render the post edit form. Args: request: The HTTP request object for template context. post_id: The unique identifier of the post to edit. user: Current user (required). Returns: HTMLResponse with rendered post form template. Raises: HTTPException: If post not found or user cannot edit it. """ post = next((p for p in MOCK_POSTS if str(p.id) == post_id), None) if not post: raise HTTPException(status_code=404, detail="Post not found") if not can_edit_post(user, post.author_id): raise HTTPException(status_code=403, detail="Not authorized to edit this post") context = get_base_context(user) return templates.TemplateResponse( request, "pages/post_form.html", { **context, "is_edit": True, "post": post, "active_page": "posts", }, ) @router.post("/posts/{post_id}/edit", response_class=HTMLResponse) async def update_post( request: Request, post_id: str, user: RequireUserDep, ) -> HTMLResponse: """Handle post update form submission. Args: request: The HTTP request object containing form data. post_id: The unique identifier of the post to update. user: Current user (required). Returns: HTMLResponse with rendered post detail template. Raises: HTTPException: If post not found or user cannot edit it. """ post = next((p for p in MOCK_POSTS if str(p.id) == post_id), None) if not post: raise HTTPException(status_code=404, detail="Post not found") if not can_edit_post(user, post.author_id): raise HTTPException(status_code=403, detail="Not authorized to edit this post") context = get_base_context(user) return templates.TemplateResponse( request, "pages/post_detail.html", { **context, "post": post, "active_page": "posts", "can_edit": True, "can_delete": can_delete_post(user, post.author_id), }, ) @router.post("/posts/{post_id}/delete", response_class=HTMLResponse) async def delete_post( request: Request, post_id: str, user: RequireUserDep, ) -> HTMLResponse: """Handle post deletion. Args: request: The HTTP request object. post_id: The unique identifier of the post to delete. user: Current user (required). Returns: HTMLResponse redirecting to the home page. Raises: HTTPException: If post not found or user cannot delete it. """ post = next((p for p in MOCK_POSTS if str(p.id) == post_id), None) if not post: raise HTTPException(status_code=404, detail="Post not found") if not can_delete_post(user, post.author_id): raise HTTPException(status_code=403, detail="Not authorized to delete this post") context = get_base_context(user) visible_posts = filter_visible_posts(MOCK_POSTS, user) return templates.TemplateResponse( request, "pages/index.html", { **context, "posts": visible_posts, "active_page": "home", "current_page": 1, "has_prev": False, "has_next": False, }, ) @router.get("/profile", response_class=HTMLResponse) async def profile( request: Request, user: RequireUserDep, ) -> HTMLResponse: """Render user profile page. Args: request: The HTTP request object for template context. user: Current user (required). Returns: HTMLResponse with rendered profile template. """ context = get_base_context(user) return templates.TemplateResponse( request, "pages/profile.html", { **context, "active_page": "profile", }, ) @router.get("/about", response_class=HTMLResponse) async def about( request: Request, user: OptionalUserDep, ) -> HTMLResponse: """Render the about page. Args: request: The HTTP request object for template context. user: Current user from dependency. Returns: HTMLResponse with rendered about page template. """ return HTMLResponse( content=f""" About - Blog

About

A modern blog built with FastAPI and DDD architecture.

User: {user.username if user else "Guest"}

Back to home """ )