# Setup visualization for this notebook
from fasthtml.jupyter import HTMX, JupyUvi
import uuid
# Create visualization app and server
= fast_app(hdrs=Theme.blue.headers())
_viz_app, _viz_rt = JupyUvi(_viz_app, port=8003)
_viz_server
def Show(component, height='auto'):
"""Display a MonsterUI component in the notebook."""
= f'/show/{uuid.uuid4().hex}'
path
@_viz_app.route(path)
def show_component():
return component
return HTMX(path, app=_viz_app, port=8003, height=height)
routes.auth
Overview
This module provides pre-built authentication routes that follow FastHTML patterns and the Answer.AI design philosophy:
- Simple by default:
login_route(rt)
gives you a working login page - Progressively customizable: Override forms, handlers, or paths as needed
- No magic: All behavior is explicit and visible
- MonsterUI integration: Beautiful forms out of the box
Quick Start
from ship_kit.routes.auth import login_route, signup_route, logout_route
from fasthtml.common import *
= fast_app()
app, rt
# Add all auth routes with defaults
login_route(rt)
signup_route(rt) logout_route(rt)
Component Visualization
To visualize MonsterUI components in this notebook during development, run the setup cell below. This will create a JupyUvi server that can render components with proper MonsterUI styling.
Note: The Show function requires a running JupyUvi server. If components aren’t displaying, make sure you’ve run the setup cell and that the server is still running on port 8003.
# Remember to stop the server to avoid dangling thread
_viz_server.stop()
Login Route
login_route
login_route (rt, path='/login', redirect_to='/', login_form=None, authenticate=None, session_key='auth', **kwargs)
*Create a login route with customizable form and authentication.
Following FastHTML patterns - pass the router instance, not the app.
Args: rt: FastHTML router instance (from fast_app()) path: Route path (default: ‘/login’) redirect_to: Where to redirect after successful login (default: ‘/’) login_form: Custom form component (callable returning FT) authenticate: Custom authentication function(email, password) -> user_dict or None session_key: Session key for auth data (default: ‘auth’) **kwargs: Additional arguments passed to form component*
Type | Default | Details | |
---|---|---|---|
rt | FastHTML router instance | ||
path | str | /login | |
redirect_to | str | / | |
login_form | NoneType | None | |
authenticate | NoneType | None | |
session_key | str | auth | |
kwargs | VAR_KEYWORD |
def default_login_form(error=None, **kwargs):
return Card(
'Login'),
H2(
Form('Email',
LabelInput(='email',
nameid='email',
type='email',
=True,
required='you@example.com'),
placeholder'Password',
LabelInput(='password',
nameid='password',
type='password',
=True),
required='destructive') if error else None,
Alert(error, variant'Sign In', type='submit', cls='w-full'),
Button(='post',
method='space-y-4'
cls
),
P("Don't have an account? ",
'Sign up', href='/signup', cls='text-primary hover:underline'),
A(='text-center text-sm mt-4'
cls
),='max-w-md mx-auto mt-8'
cls )
# Uncomment to Visualize the default login form
#Show(default_login_form(), height='500px')
Login Examples
Basic Usage
The default login form includes: - Email and password fields with proper input types - MonsterUI styling with Card container - Error handling with Alert component - HTMX integration for seamless form submission - Link to signup page
# Simple login with all defaults
login_route(rt)
Custom Form Example
def branded_login_form(error=None, **kwargs):
return Container(
Card(
Div('Welcome Back!', cls='text-3xl font-bold'),
H1('Sign in to continue to your account', cls='text-muted-foreground'),
P(='text-center mb-6'
cls
),
Form('Email Address',
LabelInput(='email',
nametype='email',
=True,
required='Enter your email'),
placeholder
Div('Password',
LabelInput(='password',
nametype='password',
=True),
required'Forgot password?', href='/forgot-password',
A(='text-sm text-primary hover:underline mt-1 block'),
cls='space-y-1'
cls
),='destructive', cls='mb-4') if error else None,
Alert(error, variant'Sign In', type='submit', cls='w-full', size='lg'),
Button('Or'),
DividerSplit(
P("Don't have an account? ",
'Sign up', href='/signup', cls='text-primary hover:underline'),
A(='text-center text-sm'
cls
),='post',
method='space-y-4'
cls
),='max-w-md w-full'
cls
),='min-h-screen flex items-center justify-center'
cls
)
# Use the custom form
# login_route(rt, login_form=branded_login_form)
# Uncomment to Visualize the branded login form
#Show(branded_login_form(), height='700px')
Custom Authentication
# Example with database authentication
def db_authenticate(email, password):
"""Authenticate user against database."""
# This is pseudocode - implement your actual database logic
# user = db.query("SELECT * FROM users WHERE email = ?", email)
# if user and verify_password(password, user['password_hash']):
# return {'id': user['id'], 'email': user['email'], 'name': user['name']}
# return None
pass
# login_route(rt, authenticate=db_authenticate)
Signup Route
signup_route
signup_route (rt, path='/signup', redirect_to='/', signup_form=None, create_user=None, session_key='auth', **kwargs)
*Create a signup route with customizable form and user creation.
Following FastHTML patterns - pass the router instance, not the app.
Args: rt: FastHTML router instance (from fast_app()) path: Route path (default: ‘/signup’) redirect_to: Where to redirect after successful signup (default: ‘/’) signup_form: Custom form component (callable returning FT) create_user: Custom user creation function(form_data) -> user_dict or error_string session_key: Session key for auth data (default: ‘auth’) **kwargs: Additional arguments passed to form component*
Type | Default | Details | |
---|---|---|---|
rt | FastHTML router instance | ||
path | str | /signup | |
redirect_to | str | / | |
signup_form | NoneType | None | |
create_user | NoneType | None | |
session_key | str | auth | |
kwargs | VAR_KEYWORD |
The default signup form includes: - Name, email, password, and password confirmation fields - Form validation with error messages - Preserves form values on validation errors - HTMX integration for seamless submission - Link to login page for existing users
# Visualize the default signup form
def default_signup_form(error=None, values=None, **kwargs):
= values or {}
values return Card(
'Create Account'),
H2(
Form('Name',
LabelInput(='name',
nameid='name',
=True,
required=values.get('name', ''),
value='John Doe'),
placeholder'Email',
LabelInput(='email',
nameid='email',
type='email',
=True,
required=values.get('email', ''),
value='you@example.com'),
placeholder'Password',
LabelInput(='password',
nameid='password',
type='password',
=True,
required='At least 8 characters'),
placeholder'Confirm Password',
LabelInput(='password_confirm',
nameid='password_confirm',
type='password',
=True),
required='destructive') if error else None,
Alert(error, variant'Create Account', type='submit', cls='w-full'),
Button(='post',
method='space-y-4'
cls
),
P("Already have an account? ",
'Sign in', href='/login', cls='text-primary hover:underline'),
A(='text-center text-sm mt-4'
cls
),='max-w-md mx-auto mt-8'
cls )
# Uncomment to Visualize the default signup form
#Show(default_signup_form(), height='600px')
Logout Route
logout_route
logout_route (rt, path='/logout', redirect_to='/login', session_key='auth', before_logout=None)
*Create a logout route that clears the session.
Following FastHTML patterns - pass the router instance, not the app.
Args: rt: FastHTML router instance (from fast_app()) path: Route path (default: ‘/logout’) redirect_to: Where to redirect after logout (default: ‘/login’) session_key: Session key to clear (default: ‘auth’) before_logout: Optional callback function(session) called before logout*
Type | Default | Details | |
---|---|---|---|
rt | FastHTML router instance | ||
path | str | /logout | |
redirect_to | str | /login | |
session_key | str | auth | |
before_logout | NoneType | None |
Complete Example
Here’s a complete example showing how to use all the auth routes together with custom authentication:
# Example of complete auth setup
def setup_auth(rt, db):
"""Setup authentication routes with database integration."""
def authenticate_user(email, password):
"""Check credentials against database."""
= db.get_user_by_email(email)
user if user and verify_password(password, user['password_hash']):
return {
'id': user['id'],
'email': user['email'],
'name': user['name']
}return None
def create_new_user(form_data):
"""Create new user in database."""
# Validation
if form_data['password'] != form_data['password_confirm']:
return "Passwords don't match"
if len(form_data['password']) < 8:
return "Password must be at least 8 characters"
# Check if email exists
if db.get_user_by_email(form_data['email']):
return "Email already registered"
# Create user
= db.create_user(
user_id =form_data['email'],
email=form_data['name'],
name=hash_password(form_data['password'])
password_hash
)
return {
'id': user_id,
'email': form_data['email'],
'name': form_data['name']
}
# Add all routes
=authenticate_user)
login_route(rt, authenticate=create_new_user)
signup_route(rt, create_user
logout_route(rt)
# Usage:
# app, rt = fast_app()
# setup_auth(rt, database)
Interactive Demo
You can create a working demo using JupyUvi:
# Demo app with all auth routes
# Note: This demo uses JupyUvi to run interactively in the notebook
from fasthtml.jupyter import JupyUvi
# Create demo app
= fast_app(hdrs=Theme.blue.headers())
demo_app, rt
# Simple in-memory user store for demo
= {}
demo_users
def demo_authenticate(email, password):
"""Demo authentication."""
= demo_users.get(email)
user if user and user['password'] == password: # Don't do this in production!
return {'email': email, 'name': user['name']}
return None
def demo_create_user(form_data):
"""Demo user creation."""
if form_data['password'] != form_data['password_confirm']:
return "Passwords don't match"
if form_data['email'] in demo_users:
return "Email already exists"
'email']] = {
demo_users[form_data['name': form_data['name'],
'password': form_data['password'] # Don't store plain passwords in production!
}
return {'email': form_data['email'], 'name': form_data['name']}
# Add auth routes using rt pattern
=demo_authenticate)
login_route(rt, authenticate=demo_create_user)
signup_route(rt, create_user
logout_route(rt)
# Add a protected home page
@rt('/')
def home(sess):
= sess.get('auth')
user if not user:
return RedirectResponse('/login', status_code=303)
return Container(
Card(f"Welcome, {user.get('name', 'User')}!"),
H1(f"You are logged in as {user['email']}"),
P('Logout', hx_get='/logout', hx_push_url='true'),
Button(='max-w-2xl mx-auto mt-8'
cls
)
)
# Run the demo server
print("Starting demo server...")
print("\nDemo app routes:")
print("- http://localhost:8000/login - Login page")
print("- http://localhost:8000/signup - Signup page")
print("- http://localhost:8000/logout - Logout")
print("- http://localhost:8000/ - Protected home page")
print("\nTry creating an account and logging in!")
# Start the server
= JupyUvi(demo_app) server
# Remember to stop the server when done:
server.stop()
Advanced Customization
Custom Session Management
# Use different session keys for different user types
# login_route(rt, session_key='admin_auth', path='/admin/login')
# login_route(rt, session_key='user_auth', path='/user/login')
Multi-tenant Authentication
def tenant_authenticate(email, password, tenant_id=None):
"""Authenticate within a specific tenant."""
# Your multi-tenant logic here
pass
# Create tenant-specific login
# login_route(rt,
# path='/tenant/{tenant_id}/login',
# authenticate=lambda e, p: tenant_authenticate(e, p, req.path_params['tenant_id'])
# )
OAuth Login Form
def oauth_login_form(error=None, **kwargs):
"""Login form with OAuth options."""
return Card(
'Sign In'),
H2(
Div(
Button('github'), 'Continue with GitHub',
UkIcon(="window.location.href='/auth/github'",
onclick='w-full mb-2'
cls
),
Button('Continue with Google',
="window.location.href='/auth/google'",
onclick='w-full'
cls
),='mb-4'
cls
),'OR'),
DividerSplit(
Form(# Regular email/password form
'Email',
LabelInput(='email',
nametype='email',
=True),
required'Password',
LabelInput(='password',
nametype='password',
=True),
required='destructive') if error else None,
Alert(error, variant'Sign In with Email', type='submit', cls='w-full'),
Button(='post',
method='space-y-4'
cls
),='max-w-md mx-auto mt-8'
cls
)
# Use OAuth-enabled form
# login_route(rt, login_form=oauth_login_form)
Testing Auth Routes
Here’s how to test the auth routes:
# Test with httpx
import httpx
def test_auth_flow(base_url='http://localhost:8000'):
"""Test the complete auth flow."""
with httpx.Client(base_url=base_url, follow_redirects=True) as client:
# Test signup
= client.post('/signup', data={
response 'name': 'Test User',
'email': 'test@example.com',
'password': 'testpass123',
'password_confirm': 'testpass123'
})assert response.status_code == 200
# Test login
= client.post('/login', data={
response 'email': 'test@example.com',
'password': 'testpass123'
})assert response.status_code == 200
# Test protected route
= client.get('/')
response assert 'Welcome' in response.text
# Test logout
= client.get('/logout')
response assert response.url.path == '/login'
print("All tests passed!")
# Run tests against your app
# test_auth_flow()
# Uncomment to Visualize the OAuth login form
# Show(oauth_login_form(), height='600px')
Summary
The auth routes module provides:
- Simple defaults - Just call
login_route(rt)
to get started - Progressive customization - Override forms, handlers, or paths as needed
- FastHTML patterns - Uses standard rt() decorator, req/sess parameters, and HTMX
- MonsterUI integration - Beautiful forms out of the box
- No magic - All code is explicit and understandable
The routes handle both regular and HTMX requests, include CSRF protection via POST methods, and integrate seamlessly with FastHTML’s session management.
Breaking Changes in v2.0
- Functions now take
rt
parameter instead ofapp
- This follows FastHTML best practices - Old:
login_route(app)
- New:
login_route(rt)