Message!
This is an old work, for updated version please visit Updated FastAPI WebApp Course
In the previous post we implemented HttpOnly Cookie and tried to secure our web app. In this article, we are going to provide login functionality. Once someone logins in our web app, we would store an HttpOnly cookie in their browser which will be used to identify the user for future requests.
Lets first create a class which will act as form validator for us. Everytime any user submits a HTML form in the UI, It will get encapsulated in a POST request and we need to validate the input before trying to log them in. Create a new file webapps > auth > forms.py and put the form logic in this file and try to understand:
from typing import List
from typing import Optional
from fastapi import Request
class LoginForm:
def __init__(self, request: Request):
self.request: Request = request
self.errors: List = []
self.username: Optional[str] = None
self.password: Optional[str] = None
async def load_data(self):
form = await self.request.form()
self.username = form.get(
"email"
) # since outh works on username field we are considering email as username
self.password = form.get("password")
async def is_valid(self):
if not self.username or not (self.username.__contains__("@")):
self.errors.append("Email is required")
if not self.password or not len(self.password) >= 4:
self.errors.append("A valid password is required")
if not self.errors:
return True
return False
Obviously, you can make the validation much better e.g. by validating the email using a Regex or by checking password constraints. But, here I am doing a simple validation in the is_valid method. If you have gone through Registration e.g. you would understand this pattern much more. I believe this class is self-explanatory. Moving forward, Now we need to handle the get and post request from users. A get request will ask for an empty form so, Its logic is pretty simple but for a post request we will be using LoginForm class to valid user inputs. Again, we need to create a new file webapps > auth > route_login.py
from apis.version1.route_login import login_for_access_token
from db.session import get_db
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from fastapi import Request
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from webapps.auth.forms import LoginForm
templates = Jinja2Templates(directory="templates")
router = APIRouter(include_in_schema=False)
@router.get("/login/")
def login(request: Request):
return templates.TemplateResponse("auth/login.html", {"request": request})
@router.post("/login/")
async def login(request: Request, db: Session = Depends(get_db)):
form = LoginForm(request)
await form.load_data()
if await form.is_valid():
try:
form.__dict__.update(msg="Login Successful :)")
response = templates.TemplateResponse("auth/login.html", form.__dict__)
login_for_access_token(response=response, form_data=form, db=db)
return response
except HTTPException:
form.__dict__.update(msg="")
form.__dict__.get("errors").append("Incorrect Email or Password")
return templates.TemplateResponse("auth/login.html", form.__dict__)
return templates.TemplateResponse("auth/login.html", form.__dict__)
Woahh, this is a big one, but I feel the code is pretty straightforward. In the POST method we are basically,
login_for_access_token
validates if the username and password is correct.Time to have a template for our implementation. create a new file templates > auth > login.html
{% extends "shared/base.html" %}
{% block title %}
<title>Login</title>
{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<h5 class="display-5">Login to JobBoard</h5>
<div class="text-danger font-weight-bold">
{% for error in errors %}
<li>{{error}}</li>
{% endfor %}
</div>
<div class="text-success font-weight-bold">
{% if msg %}
<div class="badge bg-success text-wrap font-weight-bold" style="font-size: large;">
{{msg}}
</div>
{% endif %}
</div>
</div>
<div class="row my-5">
<form method="POST">
<div class="mb-3">
<label>Email</label>
<input type="text" required placeholder="Your email" name="email" value="{{email}}" class="form-control">
</div>
<div class="mb-3">
<label>Password</label>
<input type="password" required placeholder="Choose a secure password" value="{{password}}" name="password" class="form-control">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
{% endblock %}
All done, Now we need to inform the app in the main.py file. That we have created something like this. update webapps > base.py file
from fastapi import APIRouter
from webapps.auth import route_login #new
from webapps.jobs import route_jobs
from webapps.users import route_users
api_router = APIRouter()
api_router.include_router(route_jobs.router, prefix="", tags=["job-webapp"])
api_router.include_router(route_users.router, prefix="", tags=["users-webapp"])
api_router.include_router(route_login.router, prefix="", tags=["auth-webapp"]) #new
Time to test our implementation. Fingers crossed 🤞
Final git commit: https://github.com/nofoobar/JobBoard-Fastapi/commit/0e1bdf676a66e3f341d00c590b3fd07b269488f4
Brige the gap between Tutorial hell and Industry. We want to bring in the culture of Clean Code, Test Driven Development.
We know, we might make it hard for you but definitely worth the efforts.
© Copyright 2022-23 Team FastAPITutorial