How to create an One to Many Relationship with Auth in Python with Masonite
December 07, 2021
To Get Started with Masonite, Start Here
Let’s Get Started
Assuming you have a basic primer in the Masonite framework using the resources above let’s get started with our setup.
- create and activate a virtual environment using your preferred method for doing so.
- install masonite
pip install masonite
- confirm it installed correct and run command
craft help
which should list all of masonite’s craft cli commands. - create a new project
craft new my_project
- cd into the myproject folder and run `craftinstall`
Setting Up Authentication
Out of the gate a fresh masonite project comes with a Users migratation and model.
The Migration is found in databases/migrations
from masoniteorm.migrations import Migration
class CreateUsersTable(Migration):
def up(self):
"""Run the migrations."""
with self.schema.create("users") as table:
table.increments("id")
table.string("name")
table.string("email").unique()
table.string("password")
table.string("remember_token").nullable()
table.timestamp("verified_at").nullable()
table.timestamps()
def down(self):
"""Revert the migrations."""
self.schema.drop("users")
You can add or rename any fields in here that you’d like, let’s rename “name” to “username”.
class CreateUsersTable(Migration):
def up(self):
"""Run the migrations."""
with self.schema.create("users") as table:
table.increments("id")
table.string("username")
table.string("email").unique()
table.string("password")
table.string("remember_token").nullable()
table.timestamp("verified_at").nullable()
table.timestamps()
def down(self):
"""Revert the migrations."""
self.schema.drop("users")
The model is found inside app/User.py
"""User Model."""
from masoniteorm.models import Model
class User(Model):
"""User Model."""
__fillable__ = ["name", "email", "password"]
__auth__ = "email"
We want to update this so that users login based on username instead of email.
"""User Model."""
from masoniteorm.models import Model
class User(Model):
"""User Model."""
__fillable__ = ["username", "email", "password"]
__auth__ = "username"
Before we migrate we need to setup our database settings in the .env
file, if you don’t have a ready to go mysql or postgres database we can use sqlite by just commenting out the database settings from the .env
file.
#DB_CONNECTION=mysql
#DB_HOST=127.0.0.1
#DB_PORT=3306
#DB_DATABASE=masonite
#DB_USERNAME=root
#DB_PASSWORD=root
#DB_LOG=True
Once that is done, run craft migrate
to run your migrations and create the users table.
Auth Settings
How the authentication functions will work are based on the settings in config/auth.py
AUTH = {
"defaults": {"guard": env("AUTH_GUARD", "web")},
"guards": {
"web": {
"driver": "cookie",
"model": User,
"drivers": { # 'cookie', 'jwt'
"jwt": {"reauthentication": True, "lifetime": "5 minutes"}
},
},
},
}
By default it will use session cookies but can easily be switched to JWT tokens by changing the driver property above to “jwt”, either is fine (JWT ideal for APIs).
Creating Auth Routes
Now we just need routes for logging in and signing up, let’s create a new controller craft controller Auth
Also while your at it let’s turn off CSRF protection for the time being, go to app/http/middleware/CsrfMiddleware.py
and exampt all urls like:
exempt = [
"*"
]
In app/http/controllers/AuthController.py
add the following.
"""A AuthController Module."""
from masonite.request import Request
from masonite.view import View
from masonite.controllers import Controller
from masonite.auth import Auth
class AuthController(Controller):
def __init__(self, request: Request, auth: Auth):
## Add Request and Auth as instance properties each route can use
self.request = request
self.auth = auth
def login(self):
username = self.request.input("username")
password = self.request.input("password")
result = self.auth.login(username, password)
return result
def signup(self):
username = self.request.input("username")
email = self.request.input("email")
password = self.request.input("password")
result = self.auth.register({"username": username, "email": email, "password": password})
return result
def logout(self):
self.auth.logout()
return "Logged Out"
Now we just need to attach some routes to this controller so head over to routes/web.py
and add the following:
"""Web Routes."""
from masonite.routes import Get, Post, Put, Delete, RouteGroup
ROUTES = [
Get("/", "WelcomeController@show").name("welcome"),
RouteGroup([
Post("/login", "AuthController@login").name("login"),
Post("/signup", "AuthController@signup").name("signup"),
Post("/logout", "AuthController@logout").name("logout"),
], prefix="/auth")
]
There you go, the auth routes are all set.
If you make a post request to /auth/signup
with username/email/password it should return a newly created user.
If you submit a valid username and password with a post request to /auth/login
it will create the cookie and login in the user, returning the data on the logged in user.
Now let’s start building our other models.
Owner and Dog
We are going to allow logged in users to be able to:
add owners add dogs associate a dog with an owner
First, let’s create our tables. Since this is a one to many relationship (Owners can have many dogs, but dogs have one owner), the foreign key (id of the related owner) should exist on the dogs table.
- create your migrations
craft migration Owner --create owners
craft migration Dog --create dogs
- both tables will have a one to many relationship to users (users can have many owners and dogs, but each owner and dog below to one user)
- the dog table will denote the one to many relationship to owners
- let’s give owners an name field in their migration
"""Owner Migration."""
from masoniteorm.migrations import Migration
class Owner(Migration):
def up(self):
"""
Run the migrations.
"""
with self.schema.create("owners") as table:
table.increments("id")
table.string("name")
## Field to track which user created the item
table.integer("user_id")
## Defining the field as a foreign key
table.foreign("user_id").references("id").on("users")
table.timestamps()
def down(self):
"""
Revert the migrations.
"""
self.schema.drop("owners")
- let’s give dogs and name and owner_id (foreign key) field.
"""Dog Migration."""
from masoniteorm.migrations import Migration
class Dog(Migration):
def up(self):
"""
Run the migrations.
"""
with self.schema.create("dogs") as table:
table.increments("id")
table.string("name")
## Field to track which user created the item
table.integer("user_id")
## Defining the field as a foreign key
table.foreign("user_id").references("id").on("users")
## Create the field that will be the foreign key
table.integer("owner_id")
## Foreign Key Field tracking id of related owner
table.foreign("owner_id").references("id").on("owners")
table.timestamps()
def down(self):
"""
Revert the migrations.
"""
self.schema.drop("dogs")
- run your migrations
craft migrate
The models
So the tables are setup for handling a traditional one-to-many relationship but now we need model classes that are aware of the relationship.
- create your models
craft model Owner
craft model Dog
- edit the
app/User.py
model to relate users to dogs and owners
"""User Model."""
from masoniteorm.models import Model
from masoniteorm.relationships import has_many
class User(Model):
"""User Model."""
__fillable__ = ["username", "email", "password"]
__auth__ = "username"
## Establish that users can have many dogs
@has_many("id", "user_id")
def dogs(self):
from app.Dog import Dog
return Dog
## Establish that users can have many owners
@has_many("id", "user_id")
def owners(self):
from app.Owner import Owner
return Owner
Now a user will be able to access associated owners user.owners
and its associated dogs user.dogs
.
- Now let’s edit the owners table so it can have a similar relationship with dogs.
"""Owner Model."""
from masoniteorm.models import Model
from masoniteorm.relationships import has_many
class Owner(Model):
@has_many("id", "owner_id")
def dogs(self):
from app.Dog import Dog
return Dog
Creating the Routes
Now let’s create a new controller so we can actually make dogs and owners associated with the logged in user. This new route group will have the auth middleware added which will check if the user exists and if it does will add the user as a property on the request object so we can use it in our routes.
craft controller DogOwner
"""A DogOwnerController Module."""
from masonite.request import Request
from masonite.view import View
from masonite.controllers import Controller
from app.Dog import Dog
from app.Owner import Owner
class DogOwnerController(Controller):
"""DogOwnerController Controller Class."""
def __init__(self, request: Request):
"""DogOwnerController Initializer
Arguments:
request {masonite.request.Request} -- The Masonite Request class.
"""
self.request = request
def get_user_owners(self):
return self.request.user().owners
def get_user_dogs(self):
return self.request.user().dogs
def create_owner(self):
user = self.request.user()
name = self.request.input("name")
print(user)
owner = Owner.create(name=name, user_id=user["id"])
return owner
def create_dog(self):
user = self.request.user()
name = self.request.input("name")
owner_id = self.request.input("owner_id")
dog = Dog.create(name=name, owner_id=owner_id, user_id=user.id)
return dog
- then we add the routes to web.py
"""Web Routes."""
from masonite.routes import Get, Post, Put, Delete, RouteGroup
ROUTES = [
Get("/", "WelcomeController@show").name("welcome"),
RouteGroup([
Post("/login", "AuthController@login").name("login"),
Post("/signup", "AuthController@signup").name("signup"),
Post("/logout", "AuthController@logout").name("logout"),
], prefix="/auth"),
RouteGroup([
Get("/dog", "DogOwnerController@get_user_dogs").name("get_dogs"),
Get("/owners", "DogOwnerController@get_user_owners").name("get_owners"),
Post("/owner", "DogOwnerController@create_owner").name("create_owner"),
Post("/dog", "DogOwnerController@create_dog").name("create_dog"),
], prefix="/dogowner",middleware=["auth"])
]
Now if your logged in you can make a post request to /dogowner/owner
with a name and it will create a new owner associated with the loggedIn users id and then if you pass a name and owner_id in a post request to /dogowner/dog
it will create a new dog associated with the loggedIn users id and the specified owners id.
If you make a get request to /dogowner/owners
it should return a list of owners created by that user and a get request to /dogowner/dog
should return a list of dogs created by that user. (You can similarly gets all the dogs of an owners with owner.dogs
)
There is a lot more you can do, but it is recommended to do some reading up on database relationships and on the masonite ORM.