Login and Registration¶
A login form and a registration form, with the checks that only your app can make (is the password right? is the username free?) shown in the form like any other error.
models.py
from typing import Literal
from pydantic import EmailStr, ValidationInfo, field_validator
from pydantic_schemaforms import Field, FormModel
class LoginForm(FormModel):
username: str = Field(title='Username', ui_autofocus=True)
password: str = Field(title='Password', ui_element='password')
remember_me: bool = Field(False, title='Keep me signed in')
class RegistrationForm(FormModel):
username: str = Field(
title='Username',
min_length=3,
max_length=30,
pattern=r'^[a-z0-9_]+$',
description='Lowercase letters, numbers and underscores.',
)
email: EmailStr = Field(title='Email')
password: str = Field(title='Password', ui_element='password', min_length=12)
confirm_password: str = Field(title='Confirm password', ui_element='password')
accept_terms: Literal[True] = Field(
title='I accept the terms of service',
unchecked_message='You must accept the terms to create an account',
)
@field_validator('username', mode='before')
@classmethod
def normalize_username(cls, value: str) -> str:
return value.strip().lower()
@field_validator('confirm_password')
@classmethod
def passwords_match(cls, value: str, info: ValidationInfo) -> str:
if value != info.data.get('password'):
raise ValueError('Passwords do not match')
return value
main.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from models import LoginForm, RegistrationForm
from pydantic_schemaforms import render_form_html_async
app = FastAPI()
ASSETS = {'include_framework_assets': True, 'asset_mode': 'cdn'}
def authenticate(username: str, password: str) -> bool:
return False # look the user up and verify the password hash
def username_taken(username: str) -> bool:
return username == 'admin'
def create_user(data: dict) -> None: ... # hash the password, then store the user
@app.get('/login', response_class=HTMLResponse)
async def show_login():
return await render_form_html_async(LoginForm, submit_url='/login', **ASSETS)
@app.post('/login', response_class=HTMLResponse)
async def login(request: Request):
result = LoginForm.validate(await request.form(), submit_url='/login', **ASSETS)
if not result.is_valid:
return await result.render_with_errors_async()
if not authenticate(result.data['username'], result.data['password']):
return await render_form_html_async(
LoginForm,
form_data={'username': result.data['username']},
errors={'_errors': 'Wrong username or password'},
submit_url='/login',
**ASSETS,
)
return RedirectResponse('/', status_code=303)
@app.get('/register', response_class=HTMLResponse)
async def show_register():
return await render_form_html_async(RegistrationForm, submit_url='/register', **ASSETS)
@app.post('/register', response_class=HTMLResponse)
async def register(request: Request):
form = await request.form()
result = RegistrationForm.validate(form, submit_url='/register', **ASSETS)
if not result.is_valid:
return await result.render_with_errors_async()
if username_taken(result.data['username']):
return await render_form_html_async(
RegistrationForm,
form_data=dict(form),
errors={'username': 'That username is taken'},
submit_url='/register',
**ASSETS,
)
create_user(result.data)
return RedirectResponse('/login', status_code=303)
main.py
from flask import Flask, redirect, request
from models import LoginForm, RegistrationForm
from pydantic_schemaforms import render_form_html
app = Flask(__name__)
ASSETS = {'include_framework_assets': True, 'asset_mode': 'cdn'}
def authenticate(username: str, password: str) -> bool:
return False # look the user up and verify the password hash
def username_taken(username: str) -> bool:
return username == 'admin'
def create_user(data: dict) -> None: ... # hash the password, then store the user
@app.get('/login')
def show_login():
return render_form_html(LoginForm, submit_url='/login', **ASSETS)
@app.post('/login')
def login():
result = LoginForm.validate(request.form, submit_url='/login', **ASSETS)
if not result.is_valid:
return result.render_with_errors()
if not authenticate(result.data['username'], result.data['password']):
return render_form_html(
LoginForm,
form_data={'username': result.data['username']},
errors={'_errors': 'Wrong username or password'},
submit_url='/login',
**ASSETS,
)
return redirect('/', code=303)
@app.get('/register')
def show_register():
return render_form_html(RegistrationForm, submit_url='/register', **ASSETS)
@app.post('/register')
def register():
result = RegistrationForm.validate(request.form, submit_url='/register', **ASSETS)
if not result.is_valid:
return result.render_with_errors()
if username_taken(result.data['username']):
return render_form_html(
RegistrationForm,
form_data=request.form.to_dict(),
errors={'username': 'That username is taken'},
submit_url='/register',
**ASSETS,
)
create_user(result.data)
return redirect('/login', code=303)
How it works¶
- The confirmation must match.
passwords_matchreads the password frominfo.dataand puts its error under the confirmation field. - The terms box must be ticked.
Literal[True]accepts only a ticked box, andunchecked_messageis the error shown when it's left unticked. - The username is cleaned up first.
mode="before"lowercases and trims it beforepatternchecks it. - Errors from your own checks go through
render_form_html()'serrors=:{"username": "..."}shows under that field, and"_errors"shows a message for the whole form in the summary at the top. Passform_data=so the user's input stays filled in. The login form only puts back the username, never the password. - After success, redirect (303) so reloading the page doesn't send the form again.
Before going live
Store password hashes, never passwords (use a library such as argon2-cffi
or bcrypt). Protect both forms with a CSRF token once users are logged
in; see CSRF Protection.