Files
blog.pyaqa.ru/AGENTS.md
Sergey Vanyushkin 0cb706e54b feat(auth): implement web authentication with Keycloak OAuth2
- Add auth routes: /auth/login, /auth/callback, /auth/logout
- Add OAuth2 flow with Keycloak using HTTP-only cookies
- Add web auth dependencies with role checking
- Add profile page (read-only) at /web/profile
- Update header with user menu (sign in/out, profile)
- Filter posts based on user permissions (hide drafts from guests)
- Conditionally show/hide create/edit/delete buttons
- Add authorization rules documentation to AGENTS.md
- Secure post editing/deletion endpoints with auth checks
- Add can_edit, can_delete flags to templates
2026-05-02 15:39:49 +03:00

11 KiB

Blog AGENTS.md

Stack

  • Python 3.13+, FastAPI, pydantic, uvicorn
  • SQLAlchemy 2.0 (async), aiosqlite
  • Package manager: uv
  • CI: Woodpecker (lint, test, type on push/PR to dev)

Commands

uv sync --group dev         # Install all dev dependencies
uv run pytest               # Run tests (coverage >= 70% required)
uv run pytest tests/unit/   # Run single test directory
uv run ruff check . --fix   # Lint
uv run ruff format          # Format
uv run isort .              # Sort imports
uv run mypy .               # Type check (strict mode)
uv run blog                 # Start dev server (port 8000)

Pre-commit order

ruff check --fixruff formatisortmypy

DDD Architecture

Layer Structure

app/
├── domain/                   # Domain Layer - business logic, no dependencies
│   ├── entities/            # Domain entities (Post, User, etc.)
│   │   ├── base.py          # Base entity class
│   │   └── post.py          # Post entity with business logic
│   ├── value_objects/       # Value objects (Title, Content, Slug)
│   │   ├── base.py
│   │   ├── title.py
│   │   ├── content.py
│   │   └── slug.py
│   ├── repositories/        # Repository interfaces (abstract)
│   │   ├── base.py
│   │   └── post.py
│   └── exceptions.py        # Domain exceptions
│
├── application/             # Application Layer - use cases
│   ├── dtos/               # Data Transfer Objects
│   │   └── post.py
│   ├── interfaces/         # Abstract interfaces (UoW)
│   │   └── unit_of_work.py
│   └── use_cases/          # Use cases (CQRS-like)
│       ├── create_post.py
│       ├── get_post.py
│       ├── update_post.py
│       ├── delete_post.py
│       ├── list_posts.py
│       └── publish_post.py
│
├── infrastructure/          # Infrastructure Layer - external concerns
│   ├── config/             # Configuration
│   │   └── settings.py
│   ├── database/           # Database connection & ORM models
│   │   ├── connection.py
│   │   └── models.py
│   ├── repositories/       # Repository implementations
│   │   ├── post.py         # SQLAlchemyPostRepository
│   │   └── unit_of_work.py # SQLAlchemyUnitOfWork
│   ├── di/                 # Dependency Injection
│   │   └── container.py
│   └── middleware/         # Exception handlers
│       └── error_handler.py
│
├── presentation/            # Presentation Layer - API
│   ├── api/                # FastAPI routes
│   │   ├── v1/            # API version 1
│   │   │   ├── __init__.py
│   │   │   └── posts.py   # Posts endpoints
│   │   ├── deps.py        # FastAPI dependencies
│   │   └── __init__.py
│   └── schemas/           # Pydantic schemas
│       └── post.py
│
└── main.py                # Application entry point

tests/
├── unit/                  # Unit tests (domain, use cases)
│   ├── domain/           # Domain layer tests
│   ├── application/      # Application layer tests
│   └── infrastructure/   # Infrastructure tests
├── integration/          # Integration tests (DB, repos)
├── api/                  # API endpoint tests
└── e2e/                  # End-to-end tests

Key Conventions

Dependency Rule

  • Domain layer has NO dependencies on other layers
  • Application layer depends only on Domain
  • Infrastructure depends on Domain and Application
  • Presentation depends on all other layers

Testing

  • Unit tests: Test domain logic without DB/external services
  • Integration tests: Test repository implementations with real DB
  • API tests: Test endpoints with mocked use cases
  • E2E tests: Full workflow testing

Code Patterns

  • Use dataclasses for entities and value objects
  • Use frozen dataclasses for value objects (immutable)
  • Use Unit of Work pattern for transactions
  • Use Repository pattern for data access
  • Use Dependency Injection via FastAPI's Depends()

AI Code Generation Requirements

Documentation Standards

  • Write self-documenting code - use clear, descriptive variable names and function names
  • NO inline comments - code should be readable without explanatory comments
  • Google-style docstrings are REQUIRED for all modules, classes, and functions

Docstring Requirements

Modules

Every module must have a module-level docstring:

"""Module for managing blog posts.

This module provides use cases for creating, updating, and deleting
blog posts in the application layer.
"""

Classes

Every class must have a detailed docstring:

class CreatePostUseCase:
    """Use case for creating a new blog post.

    This class encapsulates the business logic for creating posts,
    including validation and slug generation.

    Attributes:
        uow: Unit of Work for database transactions.
        slug_service: Service for generating URL-friendly slugs.

    Example:
        >>> use_case = CreatePostUseCase(uow, slug_service)
        >>> result = await use_case.execute(dto)
    """

Functions/Methods

Every function must have a detailed docstring with Args, Returns, Raises:

async def execute(self, dto: CreatePostDTO) -> PostDTO:
    """Execute the use case to create a new post.

    Args:
        dto: Data transfer object containing post creation data
            including title, content, and author information.

    Returns:
        PostDTO: The created post data transfer object with
            generated ID and slug.

    Raises:
        TitleValidationError: If the title is empty or too long.
        ContentValidationError: If the content is empty.
        DuplicateSlugError: If a post with the same slug exists.

    Note:
        This method is idempotent - calling it multiple times with
        the same data will create separate posts with unique slugs.
    """

Google-Style Docstring Format

Use the following sections as appropriate:

  • Args - Parameter descriptions with types
  • Returns - Return value description with type
  • Raises - Exceptions that may be raised
  • Yields - For generator functions
  • Example - Usage examples
  • Note - Additional important information
  • Warning - Critical warnings
  • Attributes - For class attributes
  • See Also - References to related code

UI Development Requirements

HTML Templates (Jinja2)

  • All HTML templates use Jinja2 templating engine
  • Templates are located in app/presentation/templates/
  • Base template: base.html with theme support (light/dark)

data-testid Attributes (REQUIRED)

Every interactive and significant HTML element MUST have a data-testid attribute for automated testing.

Required Elements:

  • Navigation: data-testid="nav-link-{name}", data-testid="nav-logo"
  • Buttons: data-testid="btn-{action}" (e.g., btn-create, btn-save, btn-delete)
  • Forms: data-testid="form-{name}", data-testid="input-{field}", data-testid="submit-{action}"
  • Cards/Posts: data-testid="post-card-{id}", data-testid="post-title", data-testid="post-content"
  • Lists: data-testid="list-{name}", data-testid="list-item-{index}"
  • Theme Switcher: data-testid="theme-toggle", data-testid="theme-{light|dark}"
  • Messages/Alerts: data-testid="alert-{type}", `data-testid="alert-message"

Example:

<button data-testid="btn-create-post" class="btn btn-primary">
    Create Post
</button>

<article data-testid="post-card-{{ post.id }}" class="card">
    <h2 data-testid="post-title">{{ post.title }}</h2>
    <p data-testid="post-content">{{ post.content }}</p>
</article>

CSS Architecture (Gitea-inspired)

  • Theme files: static/css/themes/theme-{light|dark}.css with CSS variables
  • Base styles: static/css/base.css - reset, typography, CSS variables usage
  • Components: static/css/components.css - buttons, cards, forms, inputs
  • Layout: static/css/layout.css - grid, navigation, containers

Theme Support

  • Light and dark themes based on Gitea color scheme
  • Theme switching via data-theme attribute on <html> element
  • LocalStorage persistence for user preference
  • All colors use CSS custom properties (variables)

Static Assets

  • All assets are local - no external CDN dependencies
  • Location: static/ directory at project root
  • Served via FastAPI StaticFiles middleware

Authentication & Authorization

Web UI Authentication

  • Token storage: HTTP-only secure cookies
  • Login flow: Redirect to Keycloak login page → Callback → Set cookie → Redirect back
  • Registration: Only through Keycloak admin interface
  • Profile: Read-only display of user info

Authorization Rules

Post Visibility

Role Published Posts Own Drafts Other Drafts
GUEST (unauthenticated)
USER
ADMIN

UI Elements by Role

Element GUEST USER ADMIN
"New Post" button
"Edit" button on own posts
"Edit" button on other posts
"Delete" button on own posts
"Delete" button on other posts
Draft badges Own only All
User menu in header
Profile page access

Auth Routes

  • GET /auth/login - Redirect to Keycloak
  • GET /auth/callback - OAuth callback handler
  • GET /auth/logout - Clear cookie and logout
  • GET /profile - User profile page (read-only)
response.set_cookie(
    key="access_token",
    value=token,
    httponly=True,
    secure=True,  # In production
    samesite="lax",
    max_age=3600,  # 1 hour
)

DDD Concepts Used

Entities

  • Have identity (UUID)
  • Mutable state
  • Business logic methods (publish, update_title, etc.)
  • Example: Post entity

Value Objects

  • Immutable
  • Defined by attributes
  • Validated on creation
  • Examples: Title, Content, Slug

Aggregates & Repositories

  • Post is an aggregate root
  • PostRepository interface in Domain
  • SQLAlchemyPostRepository implementation in Infrastructure

Domain Events

  • Placeholder for future implementation
  • Can be added via event bus in application layer

Configuration

  • .env file loaded by pydantic-settings
  • Settings available via app.infrastructure.config.settings

Database

  • SQLAlchemy 2.0 with async support
  • SQLite by default (aiosqlite)
  • Tables auto-created on startup
  • Use init_db() and close_db() in lifespan