Skip to content

🎨 Simple UI - SubApp Pattern & JWT Authentication¢

The Simple UI sample demonstrates how to build a modern single-page application (SPA) integrated with a FastAPI backend using Neuroglia's SubApp pattern, stateless JWT authentication, and role-based access control (RBAC).

🎯 Overview¢

What You'll Learn:

  • SubApp Pattern: Mount separate FastAPI applications for UI and API concerns
  • Stateless JWT Authentication: Pure token-based auth without server-side sessions
  • RBAC Implementation: Role-based access control at the query/command level
  • Frontend Integration: Bootstrap 5 UI with Parcel bundler
  • Clean Separation: API vs UI controllers with proper boundaries

Use Cases:

  • Internal dashboards and admin tools
  • Task management applications
  • Content management systems
  • Any application requiring role-based UI and API

πŸ—οΈ Architecture OverviewΒΆ

SubApp PatternΒΆ

The Simple UI sample uses FastAPI's SubApp mounting pattern to create clean separation between UI and API concerns:

from fastapi import FastAPI
from neuroglia.hosting.web import WebApplicationBuilder
from neuroglia.mediation import Mediator
from neuroglia.mapping import Mapper

def create_app():
    builder = WebApplicationBuilder()

    # Configure core services using .configure() methods
    Mediator.configure(builder, ["application.commands", "application.queries"])
    Mapper.configure(builder, ["application.mapping", "api.dtos"])

    # Add SubApp for API with controllers
    builder.add_sub_app(
        SubAppConfig(
            path="/api",
            name="api",
            title="Simple UI API",
            controllers=["api.controllers"],
            docs_url="/docs"
        )
    )

    # Add SubApp for UI
    builder.add_sub_app(
        SubAppConfig(
            path="/",
            name="ui",
            title="Simple UI",
            controllers=["ui.controllers"],
            static_files=[("/static", "static")]
        )
    )

    # Build the application
    app = builder.build()

    return app

Architecture DiagramΒΆ

graph TB
    subgraph "Client Browser"
        UI["πŸ–₯️ Bootstrap UI<br/>(HTML/JS/CSS)"]
        JWT_Storage["πŸ’Ύ JWT Token<br/>(localStorage)"]
    end

    subgraph "FastAPI Application"
        MainApp["πŸš€ Main FastAPI App<br/>(Port 8000)"]

        subgraph "UI SubApp (Mounted at /)"
            UIController["🎨 UI Controller<br/>/login, /dashboard"]
            StaticFiles["πŸ“ Static Files<br/>/static/dist/*"]
            Templates["πŸ“„ Jinja2 Templates<br/>index.html"]
        end

        subgraph "API SubApp (Mounted at /api)"
            AuthController["πŸ” Auth Controller<br/>/api/auth/login"]
            TasksController["πŸ“‹ Tasks Controller<br/>/api/tasks"]
            JWTMiddleware["πŸ”’ JWT Middleware<br/>(Bearer Token Validation)"]
        end

        subgraph "Application Layer (CQRS)"
            Commands["πŸ“ Commands<br/>CreateTaskCommand"]
            Queries["πŸ” Queries<br/>GetTasksQuery"]
            Handlers["βš™οΈ Handlers<br/>(with RBAC Logic)"]
        end

        subgraph "Domain Layer"
            Entities["πŸ›οΈ Entities<br/>Task, User"]
            Repositories["πŸ“¦ Repositories<br/>TaskRepository"]
        end
    end

    UI -->|"HTTP Requests"| UIController
    UI -->|"API Calls + JWT"| AuthController
    UI -->|"API Calls + JWT"| TasksController
    UI <-->|"Store/Retrieve"| JWT_Storage

    UIController --> Templates
    UIController --> StaticFiles

    AuthController -->|"Validate Credentials"| Handlers
    TasksController -->|"Via Mediator"| Commands
    TasksController -->|"Via Mediator"| Queries

    Commands --> Handlers
    Queries --> Handlers
    Handlers -->|"RBAC Check"| Entities
    Handlers --> Repositories

    JWTMiddleware -->|"Validate"| AuthController
    JWTMiddleware -->|"Validate"| TasksController

    style UI fill:#e1f5ff
    style UIController fill:#fff9c4
    style AuthController fill:#c8e6c9
    style TasksController fill:#c8e6c9
    style JWT_Storage fill:#ffe0b2
    style JWTMiddleware fill:#ffccbc

πŸ” Authentication ArchitectureΒΆ

Stateless JWT AuthenticationΒΆ

The Simple UI sample implements pure JWT-based authentication without server-side sessions:

Benefits:

βœ… Stateless: No session storage required βœ… Scalable: Easy horizontal scaling βœ… Microservices-Ready: JWT works across service boundaries βœ… No CSRF: Token stored in localStorage (not cookies) βœ… Simple: No session management complexity

Authentication FlowΒΆ

sequenceDiagram
    participant User as πŸ‘€ User
    participant UI as 🎨 UI (Browser)
    participant UICtrl as πŸ–₯️ UI Controller
    participant AuthAPI as πŸ” Auth API
    participant Handlers as βš™οΈ Command Handlers
    participant Repo as πŸ“¦ Repository

    Note over User,Repo: 1. Initial Page Load

    User->>+UI: Navigate to app
    UI->>+UICtrl: GET /
    UICtrl->>-UI: Serve index.html + assets
    UI->>UI: Check localStorage for JWT
    UI-->>User: Show login form (no token)

    Note over User,Repo: 2. Login Flow

    User->>UI: Enter username/password
    UI->>+AuthAPI: POST /api/auth/login<br/>{username, password}
    AuthAPI->>+Handlers: LoginCommand
    Handlers->>+Repo: Validate credentials
    Repo-->>-Handlers: User found
    Handlers->>Handlers: Generate JWT token<br/>(user_id, username, roles)
    Handlers-->>-AuthAPI: OperationResult[LoginResponseDto]
    AuthAPI-->>-UI: 200 OK<br/>{token, username, roles}

    UI->>UI: Store JWT in localStorage
    UI->>UI: Update UI (show dashboard)
    UI-->>User: Dashboard displayed

    Note over User,Repo: 3. Authenticated API Call

    User->>UI: Request tasks
    UI->>UI: Retrieve JWT from localStorage
    UI->>+TaskAPI: GET /api/tasks<br/>Authorization: Bearer {JWT}
    TaskAPI->>TaskAPI: Validate JWT signature
    TaskAPI->>TaskAPI: Extract user info & roles from JWT
    TaskAPI->>+Handlers: GetTasksQuery(user_info)
    Handlers->>Handlers: Apply RBAC filter based on roles
    Handlers->>+Repo: Get tasks (filtered by role)
    Repo-->>-Handlers: Task list
    Handlers-->>-TaskAPI: OperationResult[List[TaskDto]]
    TaskAPI-->>-UI: 200 OK<br/>{tasks: [...]}
    UI-->>User: Display filtered tasks

    Note over User,Repo: 4. Logout

    User->>UI: Click logout
    UI->>UI: Remove JWT from localStorage
    UI->>UI: Redirect to login
    UI-->>User: Login form displayed

JWT Token StructureΒΆ

Example JWT payload for Simple UI:

{
  "username": "john_admin",
  "user_id": "550e8400-e29b-41d4-a716-446655440000",
  "roles": ["admin"],
  "exp": 1730494800,
  "iat": 1730491200
}

Token Generation (Backend):

from datetime import datetime, timedelta
import jwt

class AuthService:
    SECRET_KEY = "your-secret-key-here"  # Use environment variable
    ALGORITHM = "HS256"

    def create_access_token(self, user: User) -> str:
        """Generate JWT token for authenticated user."""
        payload = {
            "username": user.username,
            "user_id": str(user.id),
            "roles": user.roles,
            "exp": datetime.utcnow() + timedelta(hours=24),
            "iat": datetime.utcnow()
        }
        return jwt.encode(payload, self.SECRET_KEY, algorithm=self.ALGORITHM)

    def decode_token(self, token: str) -> dict:
        """Validate and decode JWT token."""
        try:
            return jwt.decode(token, self.SECRET_KEY, algorithms=[self.ALGORITHM])
        except jwt.ExpiredSignatureError:
            raise UnauthorizedException("Token expired")
        except jwt.InvalidTokenError:
            raise UnauthorizedException("Invalid token")

Token Storage (Frontend):

// Store token after successful login
async function login(username, password) {
  const response = await fetch("/api/auth/login", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ username, password }),
  });

  const data = await response.json();

  if (data.token) {
    // Store JWT in localStorage (NOT cookies)
    localStorage.setItem("jwt_token", data.token);
    localStorage.setItem("username", data.username);
    localStorage.setItem("roles", JSON.stringify(data.roles));
  }
}

// Include token in all API requests
async function apiRequest(url, options = {}) {
  const token = localStorage.getItem("jwt_token");

  const headers = {
    "Content-Type": "application/json",
    ...options.headers,
  };

  if (token) {
    headers["Authorization"] = `Bearer ${token}`;
  }

  return fetch(url, { ...options, headers });
}

// Logout: simply remove token
function logout() {
  localStorage.removeItem("jwt_token");
  localStorage.removeItem("username");
  localStorage.removeItem("roles");
  window.location.href = "/";
}

πŸ›‘οΈ Role-Based Access Control (RBAC)ΒΆ

The Simple UI sample demonstrates RBAC implementation at the query and command handler level, not at the controller/endpoint level.

RBAC ArchitectureΒΆ

Key Principle: Authorization happens in the application layer (handlers), allowing fine-grained control based on business rules.

from neuroglia.mediation import QueryHandler, Query
from dataclasses import dataclass

@dataclass
class GetTasksQuery(Query[List[TaskDto]]):
    """Query to retrieve tasks with role-based filtering."""
    user_info: dict  # Contains username, user_id, roles from JWT

class GetTasksQueryHandler(QueryHandler[GetTasksQuery, OperationResult[List[TaskDto]]]):
    def __init__(self, task_repository: TaskRepository):
        super().__init__()
        self.task_repository = task_repository

    async def handle_async(self, query: GetTasksQuery) -> OperationResult[List[TaskDto]]:
        """Handle task retrieval with role-based filtering."""
        user_roles = query.user_info.get("roles", [])

        # RBAC Logic: Filter tasks based on user role
        if "admin" in user_roles:
            # Admins see ALL tasks
            tasks = await self.task_repository.get_all_async()
        elif "manager" in user_roles:
            # Managers see their department tasks
            tasks = await self.task_repository.get_by_department_async(
                query.user_info.get("department")
            )
        else:
            # Regular users see only their assigned tasks
            tasks = await self.task_repository.get_by_assignee_async(
                query.user_info.get("user_id")
            )

        task_dtos = [self.mapper.map(task, TaskDto) for task in tasks]
        return self.ok(task_dtos)

Controller IntegrationΒΆ

Controllers extract user information from JWT and pass it to handlers:

from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

security = HTTPBearer()

class TasksController(ControllerBase):

    def _get_user_info(self, credentials: HTTPAuthorizationCredentials) -> dict:
        """Extract user information from JWT token."""
        token = credentials.credentials
        try:
            # Decode JWT and extract user info
            payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
            return {
                "username": payload.get("username"),
                "user_id": payload.get("user_id"),
                "roles": payload.get("roles", []),
                "department": payload.get("department")
            }
        except jwt.InvalidTokenError:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid authentication credentials"
            )

    @get("/", response_model=List[TaskDto])
    async def get_tasks(
        self,
        credentials: HTTPAuthorizationCredentials = Depends(security)
    ) -> List[TaskDto]:
        """Get tasks with role-based filtering."""
        user_info = self._get_user_info(credentials)

        query = GetTasksQuery(user_info=user_info)
        result = await self.mediator.execute_async(query)

        return self.process(result)

RBAC PatternsΒΆ

1. Permission-Based Access:

class CreateOrderCommand(Command[OperationResult[OrderDto]]):
    user_info: dict
    order_data: dict

class CreateOrderHandler(CommandHandler[CreateOrderCommand, OperationResult[OrderDto]]):
    async def handle_async(self, command: CreateOrderCommand) -> OperationResult[OrderDto]:
        # Check permissions
        if not self._has_permission(command.user_info, "orders:create"):
            return self.forbidden("Insufficient permissions")

        # Process command...

2. Resource-Level Access:

class UpdateTaskCommand(Command[OperationResult[TaskDto]]):
    task_id: str
    user_info: dict
    updates: dict

class UpdateTaskHandler(CommandHandler[UpdateTaskCommand, OperationResult[TaskDto]]):
    async def handle_async(self, command: UpdateTaskCommand) -> OperationResult[TaskDto]:
        task = await self.task_repository.get_by_id_async(command.task_id)

        # Check ownership or admin role
        if not (task.assignee_id == command.user_info["user_id"] or
                "admin" in command.user_info["roles"]):
            return self.forbidden("Cannot update tasks assigned to others")

        # Process update...

3. Multi-Role Authorization:

def _check_authorization(self, user_info: dict, required_roles: list[str]) -> bool:
    """Check if user has any of the required roles."""
    user_roles = set(user_info.get("roles", []))
    required = set(required_roles)
    return bool(user_roles & required)  # Intersection check

# Usage
if not self._check_authorization(command.user_info, ["admin", "manager"]):
    return self.forbidden("Access denied")

πŸ“¦ Project StructureΒΆ

samples/simple-ui/
β”œβ”€β”€ main.py                          # Application entry point with SubApp mounting
β”œβ”€β”€ settings.py                      # Configuration (JWT secret, etc.)
β”‚
β”œβ”€β”€ api/                             # API Layer (JSON endpoints)
β”‚   └── controllers/
β”‚       β”œβ”€β”€ auth_controller.py       # POST /api/auth/login, /logout
β”‚       └── tasks_controller.py      # GET/POST/PUT/DELETE /api/tasks/*
β”‚
β”œβ”€β”€ ui/                              # UI Layer (HTML/Templates)
β”‚   β”œβ”€β”€ controllers/
β”‚   β”‚   └── ui_controller.py         # GET /, /dashboard
β”‚   β”œβ”€β”€ templates/
β”‚   β”‚   └── index.html               # Jinja2 SPA template
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ scripts/
β”‚   β”‚   β”‚   └── main.js              # Frontend logic (fetch API, JWT handling)
β”‚   β”‚   └── styles/
β”‚   β”‚       └── main.scss            # SASS styles (compiled by Parcel)
β”‚   └── package.json                 # Node dependencies (Bootstrap, Parcel)
β”‚
β”œβ”€β”€ application/                     # Application Layer (CQRS)
β”‚   β”œβ”€β”€ commands/
β”‚   β”‚   β”œβ”€β”€ create_task_command.py
β”‚   β”‚   β”œβ”€β”€ update_task_command.py
β”‚   β”‚   └── login_command.py
β”‚   β”œβ”€β”€ queries/
β”‚   β”‚   └── get_tasks_query.py
β”‚   └── handlers/
β”‚       β”œβ”€β”€ create_task_handler.py   # With RBAC logic
β”‚       β”œβ”€β”€ get_tasks_handler.py     # With role-based filtering
β”‚       └── login_handler.py         # JWT generation
β”‚
β”œβ”€β”€ domain/                          # Domain Layer
β”‚   β”œβ”€β”€ entities/
β”‚   β”‚   β”œβ”€β”€ task.py                  # Task entity
β”‚   β”‚   └── user.py                  # User entity
β”‚   └── repositories/
β”‚       β”œβ”€β”€ task_repository.py       # Abstract repository
β”‚       └── user_repository.py
β”‚
β”œβ”€β”€ integration/                     # Infrastructure Layer
β”‚   └── repositories/
β”‚       β”œβ”€β”€ in_memory_task_repository.py
β”‚       └── in_memory_user_repository.py
β”‚
└── static/                          # Generated static assets
    └── dist/                        # Parcel build output
        β”œβ”€β”€ main.js                  # Bundled JavaScript
        └── main.css                 # Compiled CSS

πŸš€ Getting StartedΒΆ

Quick StartΒΆ

# Navigate to simple-ui sample
cd samples/simple-ui

# Install Python dependencies (from project root)
poetry install

# Install frontend dependencies
cd ui
npm install
npm run build  # Build assets

# Return to sample directory
cd ..

# Start the application
poetry run python main.py

Access the application:

Development ModeΒΆ

For frontend development with hot-reload:

# Terminal 1: Watch and rebuild frontend assets
cd ui
npm run dev

# Terminal 2: Start backend with hot-reload
cd ..
poetry run uvicorn main:app --reload

Test UsersΒΆ

The in-memory implementation includes test users:

Username Password Roles Can See
admin admin123 admin All tasks
manager manager123 manager Department tasks
user user123 user Only assigned tasks

Authentication & SecurityΒΆ

Architecture PatternsΒΆ

Full Implementation GuideΒΆ

πŸ’‘ Key TakeawaysΒΆ

SubApp Pattern BenefitsΒΆ

βœ… Clean Separation: UI and API concerns are isolated βœ… Independent Scaling: Can deploy UI and API separately βœ… Clear Boundaries: Different routers, middleware, and static file handling βœ… Flexible Deployment: Easy to split into microservices later

Stateless JWT BenefitsΒΆ

βœ… No Server-Side Sessions: Eliminates session storage complexity βœ… Horizontal Scaling: Any server can validate any token βœ… Microservices-Ready: Tokens work across service boundaries βœ… Simplicity: No session synchronization needed

RBAC Best PracticesΒΆ

βœ… Application Layer Authorization: RBAC in handlers, not controllers βœ… Fine-Grained Control: Business rules determine access βœ… Testable: Easy to unit test authorization logic βœ… Flexible: Can combine role, permission, and resource-level checks

πŸŽ“ Next StepsΒΆ

  1. Try the Sample: Run the simple-ui application and explore the code
  2. Study RBAC Guide: Deep dive into RBAC implementation patterns
  3. Review OAuth Reference: Understand JWT and OAuth 2.0 in depth
  4. Build Your Own: Follow the Simple UI Development Guide to create a custom app
  5. Integrate Keycloak: Migrate from in-memory auth to production-ready Keycloak integration

Questions or Issues? Check the GitHub repository for more examples and support.