RBAC Example: Building Todo Apps with AuthTuna

This tutorial demonstrates how to integrate AuthTuna into your applications for authentication, authorization, and role-based access control (RBAC). We'll build two versions of a Todo app: a simple server-side rendered (SSR) app and an advanced single-page application (SPA) with a decoupled frontend.

Prerequisites

  • Basic knowledge of Python and FastAPI
  • Familiarity with SQL databases (for the simple app)
  • Understanding of React and Next.js (for the advanced app)
  • MongoDB setup (for the advanced app)

Tutorial 1: Simple SSR Todo Application

In this tutorial, we'll create a classic monolithic web application where the FastAPI backend handles authentication, business logic, and HTML rendering using Jinja2 templates. The key concept is storing application data in the same SQL database as AuthTuna.

Step 1: Set Up Your Database Model

Create a Todo model that inherits from AuthTuna's Base class. This ensures the table is managed by AuthTuna's database system. The ForeignKey links each todo to a user, enabling user-specific data scoping.

# simple/main.py

# 1. Define our custom Todo model
# We inherit from authtuna's 'Base' so it's managed by the same system
class Todo(Base):
    __tablename__ = "todos"
    id: Mapped[int] = Column(Integer, primary_key=True, index=True)
    content: Mapped[str] = Column(String, index=True)

    # This is the crucial link to the User model
    user_id: Mapped[str] = Column(String(64), ForeignKey("users.id"))

    # This relationship lets us access todo.user
    user: Mapped["User"] = relationship("User")

Step 2: Initialize AuthTuna

Use the init_app(app) function to automatically add authentication routes and session middleware to your FastAPI app.

# simple/main.py

# 2. Setup FastAPI and Jinja2
app = FastAPI(title="Simple Todo App")
templates = Jinja2Templates(directory="templates")

# 3. Initialize AuthTuna
# This is the magic. It adds all auth routes (/auth/login, /auth/signup)
# and the session middleware.
init_app(app)

Step 3: Protect Routes and Scope Data

Use FastAPI's dependency injection with AuthTuna's user dependencies to protect routes and ensure users can only access their own data.

# simple/main.py

@app.get("/todos")
async def get_todos(request: Request, user: User = Depends(get_current_user_optional)):
    """
    Protected route. Only logged-in users can access this.
    It fetches *only* the todos for the current user.
    """
    if not user:
        return RedirectResponse("/")
    todos = []
    async with db_manager.get_db() as db:
        stmt = select(Todo).where(Todo.user_id == user.id)
        result = await db.execute(stmt)
        todos = result.scalars().all()

    return templates.TemplateResponse("todos.html", {
        "request": request,
        "todos": todos,
        "username": user.username
    })

@app.post("/todos/add")
async def add_todo(content: str = Form(...), user: User = Depends(get_current_user)):
    """
    Protected route to add a new todo.
    """
    async with db_manager.get_db() as db:
        new_todo = Todo(content=content, user_id=user.id)
        db.add(new_todo)
        await db.commit()

    return RedirectResponse(url="/todos", status_code=303)

Step 4: Create the HTML Template

Use Jinja2 templates to render the UI. The template receives data from the backend and includes links to AuthTuna's built-in routes.

<!-- simple/templates/todos.html -->

<div class="container">
    <div class="header">
        <h1>Welcome, {{ username }}!</h1>
        <a href="/auth/logout">Logout</a>
    </div>

    <h2>Your Todos</h2>
    <ul>
        {% for todo in todos %}
            <li>
                <span>{{ todo.content }}</span>
                <a href="/todos/{{ todo.id }}/delete">Delete</a>
            </li>
        {% else %}
            <li>You have no todos yet!</li>
        {% endfor %}
    </ul>

    <form action="/todos/add" method="POST">
        <input type="text" name="content" placeholder="What needs to be done?" required>
        <button type="submit">Add Todo</button>
    </form>
</div>

Tutorial 2: Advanced SPA Todo Application

This tutorial covers a modern decoupled architecture with a FastAPI backend serving JSON APIs and a Next.js frontend. We'll demonstrate multi-tenancy by using AuthTuna for user management and MongoDB for application data.

Step 1: Set Up MongoDB Connection

Connect to MongoDB separately from AuthTuna's SQL database. This shows how AuthTuna can work with any database system.

# advanced/database.py

import os
import motor.motor_asyncio
import dotenv
dotenv.load_dotenv(os.getenv("ENV_FILE_PATH"))

# Create a client to connect to MongoDB
client = motor.motor_asyncio.AsyncIOMotorClient(os.getenv("MONGO_CONNECTION_STRING", "mongodb://localhost:27017"))
# ...
db = client[os.getenv("MONGO_DATABASE_NAME", "authtuna_todo_app")]

# Get a handle to our 'todos' collection
TodoCollection = db.get_collection("todos")

Step 2: Configure CORS and Initialize AuthTuna

Add CORS middleware to allow the frontend to communicate with the backend, and initialize AuthTuna.

# advanced/main.py

# 1. Add CORS Middleware
app.add_middleware(
    CORSMiddleware,
    allow_origin_regex=r"http://localhost(:[0-9]+)?",
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 2. Initialize AuthTuna
init_app(app)

Step 3: Define Pydantic Models

Create models for handling MongoDB data with proper serialization.

# advanced/main.py

class Todo(BaseModel):
    id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
    content: str
    user_id: str  # This ID comes from authtuna's User model
    org_id: str  # This ID comes from authtuna's Organization model

    model_config = ConfigDict(
        populate_by_name=True,
        arbitrary_types_allowed=True,
        json_encoders={ObjectId: str}
    )

Step 4: Implement Organization-Scoped API

Create an API endpoint that fetches todos based on the user's organization memberships, demonstrating multi-tenancy.

# advanced/main.py

@app.get("/api/todos", response_model=List[Todo])
async def get_all_todos_for_user(user: User = Depends(get_current_user)):
    """
    Get all Todos for the current user.
    This demonstrates the "advanced" logic:
    1. Get the current user from AuthTuna.
    2. Get all organizations this user belongs to from AuthTuna.
    3. Get all Todos from MongoDB that belong to any of those organizations.
    """
    # 1. Get orgs from authtuna's db
    user_orgs = await auth_service.orgs.get_user_orgs(user.id)
    org_ids = [org.id for org in user_orgs]

    if not org_ids:
        return []

    # 2. Query MongoDB for todos in those orgs
    todo_cursor = TodoCollection.find({"org_id": {"$in": org_ids}})
    todos = await todo_cursor.to_list(100)
    return todos

Step 5: Add RBAC-Protected Admin Route

Use RoleChecker to restrict access to admin-only operations, such as data cleanup.

# advanced/main.py

@app.post("/api/admin/run-cleanup-step",
          dependencies=[Depends(RoleChecker("Admin"))])
async def run_cleanup_step():
    """
    This is the advanced user deletion task you requested.
    It finds users in authtuna's 'DeletedUser' table with cleanup_counter=0,
    deletes their data from our MongoDB, and increments the counter.
    """
    users_processed = []
    async with db_manager.get_db() as db:
        # 1. Find users in authtuna's DB marked for deletion
        stmt = select(DeletedUser).where(DeletedUser.cleanup_counter == 0)
        # ... (implementation details)
        for user in users_to_cleanup:
            # 2. Delete their application data from MongoDB
            delete_result = await TodoCollection.delete_many(
                {"user_id": user.user_id}
            )
            # ... (update counter)
    return { ... }

Step 6: Set Up Frontend API Client

Create a wrapper for API calls that includes credentials for session management.

// advanced/todo_frontend/lib/api.ts

const API_BASE_URL = "http://localhost:5080";

async function apiFetch(endpoint: string, options: RequestInit = {}) {
  const url = `${API_BASE_URL}${endpoint}`;
  // ...
  const config: RequestInit = {
    ...options,
    headers: defaultHeaders,
    credentials: "include", // <-- THIS IS THE KEY!
  };
  // ...
  const response = await fetch(url, config);
  // ...
}

export const api = {
  get: (endpoint: string, options?: RequestInit) =>
    apiFetch(endpoint, { ...options, method: "GET" }),

  post: (endpoint: string, body: object, options?: RequestInit) =>
    apiFetch(endpoint, { ...options, method: "POST", body: JSON.stringify(body) }),
  // ...
};

Step 7: Implement Custom Login Page

Create a custom login form that calls AuthTuna's API endpoints.

// advanced/todo_frontend/app/login/page.tsx
'use client';
// ... imports
import { api } from '@/lib/api';

export default function LoginPage() {
  // ... state variables
  const router = useRouter();

  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault();
    // ...
    try {
      await api.post('/auth/login', {
        username_or_email: username,
        password: password,
      });

      router.push('/');
    } catch (err: unknown) {
      // ... error handling
    }
  };
  // ... return JSX
}

Step 8: Protect Client-Side Pages

Use client-side logic to check authentication and redirect if necessary. Include links to AuthTuna's UI for advanced features.

// advanced/todo_frontend/app/page.tsx
'use client';
// ... imports

export default function Home() {
  // ... state
  const router = useRouter();

  useEffect(() => {
    const fetchTodos = async () => {
      try {
        const responseData = await api.get('/api/todos');
        setTodos(responseData);
      } catch (err: unknown) {
        const error = err as { status?: number };
        if (error.status === 401) {
          router.push('/login');
        } else {
          setError('Failed to fetch todos. Is your backend running?');
        }
      } finally {
        setLoading(false);
      }
    };
    fetchTodos();
  }, [router]);
  // ... handlers for add/delete
  // ... return JSX with link to http://localhost:5080/ui/organizations
}

Next Steps

  • Clone the full example repository: Authtuna-todo
  • Explore AuthTuna's documentation for more features
  • You can config email settings and just explore.
  • Implement additional RBAC roles and permissions.