π¨ 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:
- Application: http://localhost:8000
- API Documentation: http://localhost:8000/api/docs
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 |
π Related DocumentationΒΆ
Authentication & SecurityΒΆ
- OAuth & JWT Reference - Comprehensive OAuth 2.0, OIDC, and JWT guide
- RBAC & Authorization Guide - Detailed RBAC implementation patterns
- Mario's Pizzeria Tutorial - Authentication - Full authentication setup
Architecture PatternsΒΆ
- CQRS Pattern - Command Query Responsibility Segregation
- Clean Architecture - Layered architecture principles
- MVC Controllers - Controller implementation guide
Full Implementation GuideΒΆ
- Simple UI Development Guide - Step-by-step implementation tutorial with complete code examples
π‘ 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ΒΆ
- Try the Sample: Run the simple-ui application and explore the code
- Study RBAC Guide: Deep dive into RBAC implementation patterns
- Review OAuth Reference: Understand JWT and OAuth 2.0 in depth
- Build Your Own: Follow the Simple UI Development Guide to create a custom app
- Integrate Keycloak: Migrate from in-memory auth to production-ready Keycloak integration
Questions or Issues? Check the GitHub repository for more examples and support.