Authentication with GraphQL & JWT

Django comes with Users built in so, first we build our user app.

Create Our First User

Create a new folder called /server/users at the root of server with a new file schema.py: /todo-app-graphql/server/users/schema.py

mkdir server/users
touch server/user/schema.py

In our new /server/user/schema.py file add the following:

# /todo-app-graphql/server/users/schema.py

from django.contrib.auth import get_user_model

import graphene
from graphene_django import DjangoObjectType

class UserType(DjangoObjectType):
  class Meta:
    model = get_user_model()

class CreateUser(graphene.Mutation):
  user = graphene.Field(UserType)

  class Arguments:
    username = graphene.String(required=True)
    password = graphene.String(required=True)
    email    = graphene.String(required=True)

  def mutate(self, info, username, password, email):
    user = get_user_model()(
      username=username,
      email=email,
    )
    user.set_password(password)
    user.save()

    return CreateUser(user=user)

class Mutation(graphene.ObjectType):
  create_user = CreateUser.Field()

Like our previous mutation CreateTodo we a CreateUser passing in the username, password, and email for our new user.

We are ready to update our todo-app-graphql/server/todo_proj/schema.py file with our new user mutation:

# todo-app-graphql/server/todo_proj/schema.py

import graphene

import todo_app.schema
import users.schema

class Query(
  todo_app.schema.Query,
  graphene.ObjectType
):
  pass

class Mutation(users.schema.Mutation, todo_app.schema.Mutation, graphene.ObjectType):
  pass

schema = graphene.Schema(query=Query, mutation=Mutation)

Create a New User with GraphiQL

Add the following to GraphiQL at http://localhost:5555/graphql:

mutation {
  createUser(
    username: "ron",
    email: "ron@example.com",
    password: "123456A!"
  ) {
    user {
    id,
    username,
    email
    }
  }
}

… which should create our new user.

Query the Users

Next we update the server/users/schema.py file with a query, (see highlighted lines), to list all our users in the database.

# todo-app-graphql/server/users/schema.py


from django.contrib.auth import get_user_model
import graphene
from graphene_django import DjangoObjectType

class UserType(DjangoObjectType):
  class Meta:
    model = get_user_model()

class CreateUser(graphene.Mutation):
  user = graphene.Field(UserType)

  class Arguments:
    username = graphene.String(required=True)
    password = graphene.String(required=True)
    email    = graphene.String(required=True)

  def mutate(self, info, username, password, email):
    user = get_user_model() (
      username=username, 
      email = email,
    )
    user.set_password(password)
    user.save()

    return CreateUser(user=user)

class Mutation(graphene.ObjectType):
  create_user = CreateUser.Field()

class Query(graphene.ObjectType):
  users = graphene.List(UserType)

  def resolve_users(self, info):
    return get_user_model().objects.all()

Update the main query class with our new users query:

# todo-app-graphql/server/todo_proj/schema.py

# ... code
class Query(
  users.schema.Query,
  todo_app.schema.Query,
  graphene.ObjectType
):
  pass

We can now add the following query to GraphiQL to query all our users in the database:

query {
  users{
    id
    username
    email
  }
}

… and voila! Our list of users.

Authenticating Our Users

Most of the web apps today are stateless so, we will use the django-graphql-jwt library to implement JWT Tokens in Graphene.

When a User signs up or logs in, a token will be returned: a string of data that identifies the User. This token must be sent by the User in the HTTP Authorization header with every request when authentication is needed. If you want to know more about Django JWT checkout the official docs.

Configuring django-graphql-jwt

If you cut and paste all the requirements into the requirements.txt file from part one of this tutorial, you already have django-graphql-jwt installed. If not you can install with the following

docker-compose run server pip install django-graphql-jwt

To configure add the following to the MIDDLEWARE variable the todo-app-graphql/server/todo_proj/settings.py file:

# todo-app-graphql/server/todo_proj/settings.py

# ... code

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
]

GRAPHENE = {
  'SCHEMA': 'todo_proj.schema.schema',
  'MIDDLEWARE': [
    'graphql_jwt.middleware.JSONWebTokenMiddleware',
   ],
}

The entire server/todo_proj/settings.py should look like this now with highlighted lines of updates:

# server/todo_proj/settings.py

"""
Django settings for todo_proj project.

Generated by 'django-admin startproject' using Django 3.2.5.

For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-z6a#15u#4bw092uhpnvhbc_h!p$!&&g&qvb5dq3vj7kj3@#%bg'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'todo_app',
    'mptt',
    'graphene_django',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
]

ROOT_URLCONF = 'todo_proj.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'todo_proj.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases

import os

DATABASES = { 
'default': 
  { 
    'ENGINE': 'django.db.backends.postgresql', 
    'NAME': os.environ.get('DATABASE_NAME'), 
    'USER': os.environ.get('DATABASE_USER'), 
    'PASSWORD': os.environ.get('DATABASE_PASSWORD'),    
    'HOST': os.environ.get('DATABASE_HOST'), 
    'PORT': os.environ.get('DATABASE_PORT'), 
  } 
}


# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/

STATIC_URL = '/static/'

# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

GRAPHENE = {
  'SCHEMA': 'todo_proj.schema.schema',
  'MIDDLEWARE': [
    'graphql_jwt.middleware.JSONWebTokenMiddleware',
   ],
}

Also in the todo-app-graphql/server/todo_proj/settings.py file add the AUTHENTICATION_BACKENDS setting:

# todo-app-graphql/server/todo_proj/settings.py

# ... code

AUTHENTICATION_BACKENDS = [
  'graphql_jwt.backends.JSONWebTokenBackend',
  'django.contrib.auth.backends.ModelBackend',
]

In the todo-app-graphql/server/todo_proj/schema.py file add:

# todo-app-graphql/server/todo_proj/schema.py

import graphene
import graphql_jwt

# ...code

Also in the todo-app-graphql/server/todo_proj/schema.py file change the Mutation class by replacing “pass” with the following variables:

# todo-app-graphql/server/todo_proj/schema.py

# ... code

class Mutation(
  users.schema.Mutation,
  todo_app.schema.Mutation,
  graphene.ObjectType
):
  token_auth = graphql_jwt.ObtainJSONWebToken.Field()
  verify_token = graphql_jwt.Verify.Field()
  refresh_token = graphql_jwt.Refresh.Field()

# ... code

Your todo-app-graphql/server/todo_proj/schema.py should look like this now. Note the highlighted lines for the updated code.

import graphene
import graphql_jwt
 
import todo_app.schema
import users.schema
 
class Query(
  users.schema.Query,
  todo_app.schema.Query,
  graphene.ObjectType
):
  pass
 
class Mutation(users.schema.Mutation, todo_app.schema.Mutation, graphene.ObjectType):
  token_auth = graphql_jwt.ObtainJSONWebToken.Field()
  verify_token = graphql_jwt.Verify.Field()
  refresh_token = graphql_jwt.Refresh.Field()
 
schema = graphene.Schema(query=Query, mutation=Mutation)

Since we updated our settings.py file we need to reboot:

docker-compose down
docker-compose up -d

We have three new Mutations. The first, token_auth uses the User with its username and password to obtain its JSON Web token.

Add the following mutation into graphiql:

mutation {
  tokenAuth(username: "ron", password: "123456A!") {
    token
  }
}

Using the returned token we will verify that the provided token is indeed for the user we requested.

The token that is returned for you will be different from mine in the example below so you will need to use your token below.

mutation {
  verifyToken(token:   "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InJvbiIsImV4cCI6MTYwMTg2NTU4Nywib3JpZ19pYXQiOjE2MDE4NjUyODd9.5sakOlY_sf3MJIEq_FXNtJexalgNi7VIejUTuo_Bvok") 
  {
  payload
  }
}

Voila! VerifyToken confirms that the token is valid which is passed in as an argument.

RefreshToken is used to obtain a new token within the renewed expiration time for non-expired tokens, if they are enabled to expire. We won’t be using this token.

For more information check the documentation here.

Test Our Authentication

In order to test to see if everything is working, we create a new Query called me. This query returns the User’s info if the user is logged in or an error.

Update server/users/schema.py file with the following highlighted lines:

# server/users/schema.py


from django.contrib.auth import get_user_model
import graphene
from graphene_django import DjangoObjectType

class UserType(DjangoObjectType):
  class Meta:
    model = get_user_model()

class CreateUser(graphene.Mutation):
  user = graphene.Field(UserType)

  class Arguments:
    username = graphene.String(required=True)
    password = graphene.String(required=True)
    email    = graphene.String(required=True)

  def mutate(self, info, username, password, email):
    user = get_user_model() (
      username=username, 
      email = email,
    )
    user.set_password(password)
    user.save()

    return CreateUser(user=user)

class Mutation(graphene.ObjectType):
  create_user = CreateUser.Field()

class Query(graphene.ObjectType):
  me    = graphene.Field(UserType)
  users = graphene.List(UserType)

  def resolve_users(self, info):
    return get_user_model().objects.all()

  def resolve_me(self, info):
    user = info.context.user
    if user.is_anonymous:
      raise Exception('Not logged in!')

    return user

Now we need to switch from GraphiQL UI to a another app called Insomnia which can accept custom HTTP headers. You can download Insomnia here.

Using Insomnia let’s get a new token for our user:

mutation {
  tokenAuth(username: "ron", password: "123456A!") {
    token
  }
}

… which gives us this.

Add the token under the Header tab on Insomnia, add the AUTHORIZATION HTTP header with our token prefixed with the word jwt.

Using our new token we can query the identity of our user that we just logged in.

query {
  me {
    id
    username
  }
}

Now we can create users and login with them.

Integrating Todo Tasks and Users

Ok, where are we? We can login and access via Insomnia who is logged in. Excellent. What’s next? We don’t want anonymous users creating and editing todo tasks. Only logged in users can have that privilege so we need to link our users to todo tasks.

We need update our todo model, todo-app-graphql/todo_app/models.py:

# todo-app-graphql/todo_app/models.py

from django.conf import settings
# ... code

Add a posted_by field:

# todo-app-graphql/todo_app/models.py

from django.conf import settings
# ... code

class Todo(models.Model):
  title = models.CharField(max_length=5000)
  task = models.TextField(blank=True)
  slug = AutoSlugField(populate_from='title', default='')
  category = TreeForeignKey('Category', on_delete=models.CASCADE, null=True, blank=True, related_name='todos')
  project = models.ForeignKey('Project', related_name='todo_project', on_delete=models.CASCADE, null=True, blank=True)
  is_completed = models.BooleanField(default=False)
  posted_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)
  modified = models.DateTimeField(auto_now=True, null=True, editable=False)
  created = models.DateField(default=timezone.now().strftime("%Y-%m-%d"))
  due_date = models.DateField(default=timezone.now().strftime("%Y-%m-%d"))

  def __str__(self):
    return self.title

Using docker-compose run makemigrations and migrate to update our database:

# Stop server
docker-compose down
# Start server so new migrations take effect
docker-compose up -d

# Migrations
docker-compose run server ./manage.py makemigrations
docker-compose run server ./manage.py migrate

In CreateTodo mutation, todo-app-graphql/todo_app/schema.py, return the User in the newly created field:

# todo-app-graphql/todo_app/schema.py

# ...code
from users.schema import UserType

# ...code
# Update CreateTodo mutation
class CreateTodo(graphene.Mutation):
  id = graphene.Int()
  title = graphene.String()
  task = graphene.String()
  is_completed = graphene.Boolean()
  slug = graphene.String()
  project = graphene.Field(ProjectType)
  category = graphene.Field(CategoryType)
  posted_by = graphene.Field(UserType)

  class Arguments:
    title = graphene.String()
    task = graphene.String()
    projId = graphene.Int()
    catId = graphene.Int()

  def mutate(self, info, title, task, projId=None, catId=None):
    user = info.context.user
    if user.is_anonymous:
      raise GraphQLError('You must be logged in to create new todos!')

    if projSlug:
      proj = Project.objects.filter(slug=projSlug).first()
    else:
      proj = None

    if catSlug:
      cat = Category.objects.get(slug=catSlug)
    else:
      cat = None
      
    todo = Todo(
      title=title, 
      task=task, 
      project=proj, 
      category=cat,
      posted_by=user,
    )
    todo.save()

    return CreateTodo(
      id=todo.id,
      title=todo.title,
      task=todo.task,
      slug=todo.slug,
      project=todo.project,
      category=todo.category,
      posted_by=todo.posted_by,
    )

Now we can test by sending a mutation to the server via GraphiQL, http://localhost:5555/graphql:

mutation {
  createTodo(
    title: "My Todo",
    task: "Do this task…",
    projId: 1,
    catId: 1
  ) {
      id
      title
      task
      postedBy {
        id
        username
        email
    }
    project {
      id
      name
      slug
    }
    category {
      id
      name
    }
  }
}

But wait we get an error!

If you didn’t get an error and you were successful in creating a todo, you may be logged into the django backend, (http://localhost:5555/admin), using the same browser.

Our create todo mutation is now password protected. In order to create a new task via GraphQL we need to use Insomnia which sends our user token in the header:

Note that since we are referencing projId and catId in the createTodo arguments each with id 1 so, confirm that you have a project and a category each with id = 1 in your db if not adjust the id parameter accordingly.

Next we need to update /server/todo_app/schema.py file by adding user authentication for update todo:

# server/todo_app/schema.py

# code...

class UpdateTodo(graphene.Mutation):
  id = graphene.Int()
  title = graphene.String()
  task = graphene.String()
  is_completed = graphene.Boolean()
  project = graphene.Field(ProjectType)
  category = graphene.Field(CategoryType)
  slug = graphene.String()
  posted_by = graphene.Field(UserType)

  class Arguments:
    id = graphene.Int()
    title = graphene.String()
    task = graphene.String()
    is_completed = graphene.Boolean()
    projId = graphene.Int()
    catId = graphene.Int()

  def mutate(self, info, id, title=None, task=None, is_completed=None, projId=None, catId=None):

    user = info.context.user
    if user.is_anonymous:
      raise GraphQLError('You must be logged in to execute this mutation!')

    todo = Todo.objects.get(pk=id)
    if projId:
      project = Project.objects.get(pk=projId)
      if not project:
        raise Exception('There is no project with this id.')

    if catId:
      category = Category.objects.get(pk=catId)
      if not category:
        raise Exception('No category with this id exists.')

    if todo:
      if title:
        todo.title = title
      if task:
        todo.task = task
      if is_completed:
        todo.is_completed = is_completed
      if projId:
        todo.project = project
      if catId:
        todo.category = category

      todo.save()

      return UpdateTodo(
        id = todo.id,
        title = todo.title,
        task = todo.task,
        is_completed = todo.is_completed,
        project = todo.project,
        category = todo.category,
        slug = todo.slug,
        posted_by=todo.posted_by,
      )
    else:
      raise Exception('There is no todo with this id.')

# code ...
class Mutation(graphene.ObjectType):
  # code...
  update_todo = UpdateTodo.Field()

Using Insomnia GraphQL we can update our Todo:

mutation{
  updateTodo(
    id: 1,
    title: "todo1 updated"
  ) {
    id
    title
    postedBy{
      id
      username
      email
    }
  }
}

So now both create and updating todos are protected by user authentication. That’s fine allowing any authenticated user the permission for creating a new todo, but we only want the user who originally created the todo to be able to edit it. For that we need to add an additional layer of authentication testing that the logged in user is indeed the same as the user who created the todo.

We can do this by comparing current user id and posted by id, e.g. user.id != posted_by.id. Lets add this to our UpdateTodo mutation below:

# server/todo_app/schema.py

# code...

class UpdateTodo(graphene.Mutation):
  id = graphene.Int()
  title = graphene.String()
  task = graphene.String()
  is_completed = graphene.Boolean()
  project = graphene.Field(ProjectType)
  category = graphene.Field(CategoryType)
  slug = graphene.String()
  posted_by = graphene.Field(UserType)

  class Arguments:
    id = graphene.Int()
    title = graphene.String()
    task = graphene.String()
    is_completed = graphene.Boolean()
    projId = graphene.Int()
    catId = graphene.Int()

  def mutate(self, info, id, title=None, task=None, is_completed=None, projId=None, catId=None):

    user = info.context.user
    if user.is_anonymous:
      raise GraphQLError('You must be logged in to execute this mutation!')

    todo = Todo.objects.get(pk=id)

    # test if logged in user is the same as posted_by
    if todo.posted_by.id != user.id:
      raise GraphQLError('You do not have permission to execute this mutation!')

    if projId:
      project = Project.objects.get(pk=projId)
      if not project:
        raise Exception('There is no project with this id.')

    if catId:
      category = Category.objects.get(pk=catId)
      if not category:
        raise Exception('No category with this id exists.')

    if todo:
      if title:
        todo.title = title
      if task:
        todo.task = task
      if is_completed:
        todo.is_completed = is_completed
      if projId:
        todo.project = project
      if catId:
        todo.category = category

      todo.save()

      return UpdateTodo(
        id = todo.id,
        title = todo.title,
        task = todo.task,
        is_completed = todo.is_completed,
        project = todo.project,
        category = todo.category,
        slug = todo.slug,
        posted_by=todo.posted_by,
      )
    else:
      raise Exception('There is no todo with this id.')

Now only the user who created the todo can edit it. Try it in Insomnia.

Next we need to add user authentication for all Crud operations which the updated todo app schema and model addresses below.

Updated for All Mutations for Todo App Schema

Below is updated schema including:

  • Delete Todo
  • Create Category
  • Update Category
  • Delete Category
  • Create Project
  • Update Project
  • Delete Project
# /server/todo_app/schema.py

import graphene
from graphene import Schema
from graphene_django import DjangoObjectType
from graphql import GraphQLError
from todo_app.models import Todo, Project, Category
from users.schema import UserType

class TodoType(DjangoObjectType):
  class Meta:
    model = Todo

class ProjectType(DjangoObjectType):
  class Meta:
    model = Project

class CategoryType(DjangoObjectType):
  class Meta:
    model = Category

class Query(graphene.ObjectType):
  
  # todo queries
  todos = graphene.List(TodoType)
  def resolve_todos(self, info, **kwargs):
    return Todo.objects.all()

  todo = graphene.Field(
    TodoType,
    id=graphene.Int(),
  ) 
  def resolve_todo(self, info, id):
    return Todo.objects.get(pk=id)

  # project queries
  projects = graphene.List(ProjectType)
  def resolve_projects(self, info, **kwargs):
    return Project.objects.all()

  project = graphene.Field(
    ProjectType,
    id=graphene.Int(),
  )
  def resolve_project(self, info, id):
    return Project.objects.get(pk=id)

  # category queries
  categories = graphene.List(CategoryType)
  def resolve_categories(self, info, **kwargs):
    return Category.objects.all()

  category = graphene.Field(
    CategoryType,
    id=graphene.Int(),
  )
  def resolve_category(self, info, id):
    return Category.objects.get(pk=id)

# Mutations
class CreateTodo(graphene.Mutation):
  id = graphene.Int()
  title = graphene.String()
  task = graphene.String()
  is_completed = graphene.Boolean()
  slug = graphene.String()
  project = graphene.Field(ProjectType)
  category = graphene.Field(CategoryType)
  posted_by = graphene.Field(UserType)

  class Arguments:
    title = graphene.String()
    task = graphene.String()
    projId = graphene.Int()
    catId = graphene.Int()

  def mutate(self, info, title, task, projId=None, catId=None):

    user = info.context.user
    if user.is_anonymous:
      raise GraphQLError('You must be logged in to execute this mutation!')

    if projId:
      proj = Project.objects.get(pk=projId)
    else:
      proj = None

    if catId:
      cat = Category.objects.get(pk=catId)
    else:
      cat = None
      
    todo = Todo.objects.create(
      title=title, 
      task=task, 
      project=proj, 
      category=cat,
      posted_by=user,
    )

    return CreateTodo(
      id=todo.id,
      title=todo.title,
      task=todo.task,
      slug=todo.slug,
      project=todo.project,
      category=todo.category,
      posted_by=todo.posted_by,
    )

class UpdateTodo(graphene.Mutation):
  id = graphene.Int()
  title = graphene.String()
  task = graphene.String()
  slug = graphene.String()
  is_completed = graphene.Boolean()
  project = graphene.Field(ProjectType)
  category = graphene.Field(CategoryType)
  posted_by = graphene.Field(UserType)

  class Arguments:
    id = graphene.Int()
    title = graphene.String()
    task = graphene.String()
    is_completed = graphene.Boolean()
    projId = graphene.Int()
    catId = graphene.Int()

  def mutate(self, info, id, title=None, task=None, is_completed=None, projId=None, catId=None):
    user = info.context.user
    if user.is_anonymous:
      raise GraphQLError('You must be logged in to execute this mutation!')

    todo = Todo.objects.get(pk=id)

    # test if logged in user is the same as posted_by
    if todo.posted_by.id != user.id:
      raise GraphQLError('You do not have permission to execute this mutation!')

    if projId:
      project = Project.objects.get(pk=projId)
      if not project:
        raise Exception('There is no project with this id.')

    if catId:
      category = Category.objects.get(pk=catId)
      if not category:
        raise Exception('No category with this id exists.')

    if todo:
      if title:
        todo.title = title
      if task:
        todo.task = task
      if is_completed:
        todo.is_completed = is_completed
      if projId:
        todo.project = project
      if catId:
        todo.category = category

      todo.save()

      return UpdateTodo(
        id = todo.id,
        title = todo.title,
        task = todo.task,
        slug = todo.slug,
        is_completed = todo.is_completed,
        project = todo.project,
        category = todo.category,
        posted_by=todo.posted_by,
      )
    else:
      raise Exception('There is no todo with this id.')

class DeleteTodo(graphene.Mutation):
  id = graphene.Int()
  title = graphene.String()
  ok = graphene.Boolean()

  class Arguments:
    id = graphene.Int()

  def mutate(self, info, id=id):
    user = info.context.user
    if user.is_anonymous:
      raise GraphQLError('You must be logged in to execute this mutation!')

    todo = Todo.objects.get(pk=id)
    
    # test if logged in user is the same as posted_by
    if todo.posted_by.id != user.id:
      raise GraphQLError('You do not have permission to execute this mutation!')

    if todo:
      todo.delete()
      return DeleteTodo(
        id = todo.id,
        title = todo.title,
        ok = True
      )

class UpdateTodo(graphene.Mutation):
  id = graphene.Int()
  title = graphene.String()
  task = graphene.String()
  is_completed = graphene.Boolean()
  project = graphene.Field(ProjectType)
  category = graphene.Field(CategoryType)
  slug = graphene.String()
  posted_by = graphene.Field(UserType)

  class Arguments:
    id = graphene.Int()
    title = graphene.String()
    task = graphene.String()
    is_completed = graphene.Boolean()
    projId = graphene.Int()
    catId = graphene.Int()

  def mutate(self, info, id, title=None, task=None, is_completed=None, projId=None, catId=None):

    user = info.context.user
    if user.is_anonymous:
      raise GraphQLError('You must be logged in to execute this mutation!')

    todo = Todo.objects.get(pk=id)

    # test if logged in user is the same as posted_by
    if todo.posted_by.id != user.id:
      raise GraphQLError('You do not have permission to execute this mutation!')

    if projId:
      project = Project.objects.get(pk=projId)
      if not project:
        raise Exception('There is no project with this id.')

    if catId:
      category = Category.objects.get(pk=catId)
      if not category:
        raise Exception('No category with this id exists.')

    if todo:
      if title:
        todo.title = title
      if task:
        todo.task = task
      if is_completed:
        todo.is_completed = is_completed
      if projId:
        todo.project = project
      if catId:
        todo.category = category

      todo.save()

      return UpdateTodo(
        id = todo.id,
        title = todo.title,
        task = todo.task,
        is_completed = todo.is_completed,
        project = todo.project,
        category = todo.category,
        slug = todo.slug,
        posted_by=todo.posted_by,
      )
    else:
      raise Exception('There is no todo with this id.')

class CreateProject(graphene.Mutation):
  id = graphene.Int()
  name = graphene.String()
  description = graphene.String()
  slug = graphene.String()
  posted_by = graphene.Field(UserType)

  class Arguments:
    name = graphene.String()
    description = graphene.String()

  def mutate(self, info, name, description=None):

    user = info.context.user
    if user.is_anonymous:
      raise GraphQLError('You must be logged in to execute this mutation!')

    project = Project.objects.create(
      name=name, 
      description=description,
      posted_by=user,
    )

    return CreateProject(
      id = project.id,
      name = project.name,
      description = project.description,
      slug = project.slug,
      posted_by = project.posted_by,
    )

class DeleteProject(graphene.Mutation):
  id = graphene.Int()
  name = graphene.String()
  ok = graphene.Boolean()

  class Arguments:
    id = graphene.Int()

  def mutate(self, info, id=id):

    user = info.context.user
    if user.is_anonymous:
      raise GraphQLError('You must be logged in to execute this mutation!')
    
    project = Project.objects.get(pk=id)

    # test if logged in user is the same as posted_by
    if project.posted_by.id != user.id:
      raise GraphQLError('You do not have permission to execute this mutation!')

    if project:
      project.delete()
      return DeleteProject(
        id = project.id,
        name = project.name,
        ok = True,
      )

class UpdateProject(graphene.Mutation):
  id = graphene.Int()
  name = graphene.String()
  description = graphene.String()
  slug = graphene.String()
  posted_by = graphene.Field(UserType)

  class Arguments:
    id = graphene.Int()
    name = graphene.String()
    description = graphene.String()
  
  def mutate(self, info, id=id, name=None, description=None):

    user = info.context.user
    if user.is_anonymous:
      raise GraphQLError('You must be logged in to execute this mutation!')

    project = Project.objects.get(pk=id)

    # test if logged in user is the same as posted_by
    if project.posted_by.id != user.id:
      raise GraphQLError('You do not have permission to execute this mutation!')

    if project:
      if name:
        project.name = name
      if description:
        project.description = description

      project.save()

      return UpdateProject(
        id = project.id,
        name = project.name,
        description = project.description,
        slug = project.slug, 
        posted_by = project.posted_by,
      )
    else:
      raise Exception('There is no project with this id.')

class CreateCategory(graphene.Mutation):
  id = graphene.Int()
  name = graphene.String()
  slug = graphene.String()
  parent = graphene.Field(CategoryType)
  posted_by = graphene.Field(UserType)

  class Arguments:
    name = graphene.String()
    parentId = graphene.Int()

  def mutate(self, info, name, parentId=None):

    user = info.context.user
    if user.is_anonymous:
      raise GraphQLError('You must be logged in to execute this mutation!')

    if parentId:
      parent = Category.objects.get(pk=parentId)
      category = Category.objects.create(
        name=name, 
        parent=parent,
        posted_by=user,
      )
    else:
      category = Category.objects.create(
        name=name,
        posted_by=user,
      )

    return CreateCategory(
      id=category.id,
      name=category.name,
      slug=category.slug,
      parent=category.parent,
      posted_by=category.posted_by,
    )

class DeleteCategory(graphene.Mutation):
  id = graphene.Int()
  name = graphene.String()
  slug = graphene.String()
  ok = graphene.Boolean()

  class Arguments:
    id = graphene.Int()

  def mutate(self, info, id=id):

    user = info.context.user
    if user.is_anonymous:
      raise GraphQLError('You must be logged in to execute this mutation!')

    category = Category.objects.get(pk=id)

    # test if logged in user is the same as posted_by
    if category.posted_by.id != user.id:
      raise GraphQLError('You do not have permission to execute this mutation!')

    if category:
      category.delete()
      return DeleteCategory(
        id = category.id,
        name = category.name,
        slug = category.slug,
        ok = True
      )

class UpdateCategory(graphene.Mutation):
  id = graphene.Int()
  name = graphene.String()
  parent = graphene.Field(CategoryType)
  slug = graphene.String()
  posted_by = graphene.Field(UserType)

  class Arguments:
    id = graphene.Int()
    name = graphene.String()
    parentId = graphene.Int()

  def mutate(self, info, id, name=None, parentId=None):
    
    user = info.context.user
    if user.is_anonymous:
      raise GraphQLError('You must be logged in to execute this mutation!')
    
    category = Category.objects.get(pk=id)

    # test if logged in user is the same as posted_by
    if category.posted_by.id != user.id:
      raise GraphQLError('You do not have permission to execute this mutation!')

    if parentId:
      parent = Category.objects.get(pk=parentId)
      if not parent:
        raise Exception('No category with this id to be used as the updated parent.')


    if category:
      if name:
        category.name = name
      if parentId:
        category.parent = parent
      
      category.save()

      return UpdateCategory(
        id = category.id,
        name = category.name,
        parent = category.parent,
        slug = category.slug,
        posted_by = category.posted_by,
      )
    else:
      raise Exception('No category with this id.')      



class Mutation(graphene.ObjectType):
  create_todo = CreateTodo.Field()
  update_todo = UpdateTodo.Field()
  delete_todo = DeleteTodo.Field()
  create_project = CreateProject.Field()
  delete_project = DeleteProject.Field()
  update_project = UpdateProject.Field()
  create_category = CreateCategory.Field()
  delete_category = DeleteCategory.Field()
  update_category = UpdateCategory.Field()

schema = Schema(query=Query, mutation=Mutation)

Whew! That was a lot of work but, we only have to do this work once because everytime we run tests we will know definitively if there are any errors. Beats running all these mutations in GraphiQL or Insomnia every time we make a change.

We will need to update server/todo_app/models.py by adding posted_by to Project and Category:

# server/todo_app/models.py

from django.db import models
from autoslug import AutoSlugField
from django.utils import timezone
from datetime import datetime
from django.conf import settings
from mptt.models import MPTTModel, TreeForeignKey

class Category(MPTTModel):
  name = models.CharField(max_length=50, unique=True)
  parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children')
  posted_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)
  slug = AutoSlugField(populate_from='name')
  modified = models.DateTimeField(auto_now=True, null=True, editable=False) 
  
  created = models.DateField(default=timezone.now().strftime("%Y-%m-%d")) 

  def __str__(self): 
    return self.name 

  class MPTTMeta: 
    order_insertion_by = ['name'] 
    unique_together = ('slug', 'parent') 

  class Meta: 
    verbose_name_plural = 'categories'

class Project(models.Model):
  name = models.CharField(max_length=500)
  description = models.TextField(max_length=2500, blank=True, null=True, help_text="Description of your project.")
  posted_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)
  slug = AutoSlugField(populate_from='name')
  modified = models.DateTimeField(auto_now=True, null=True, editable=False) 
  created = models.DateField(default=timezone.now().strftime("%Y-%m-%d")) 

  def __str__(self): 
    return self.name 

  class Meta: 
    verbose_name_plural = 'projects'

    
class Todo(models.Model):
  title = models.CharField(max_length=5000)
  task = models.TextField(blank=True)
  slug = AutoSlugField(populate_from='title')
  category = TreeForeignKey('Category', on_delete=models.CASCADE, null=True, blank=True, related_name='todos')

  project = models.ForeignKey('Project', on_delete=models.CASCADE, null=True, blank=True)
  posted_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)
  is_completed = models.BooleanField(default=False)

  modified = models.DateTimeField(auto_now=True, null=True, editable=False)
  created = models.DateField(default=timezone.now().strftime("%Y-%m-%d"))
  due_date = models.DateField(default=timezone.now().strftime("%Y-%m-%d"))

  def __str__(self):
    return self.title

Since we updated the model we need to run migrations:

docker-compose run server ./manage.py makemigrations
docker-compose run server ./manage.py migrate

We can now test all Crud operations on our app with GraphiQL.

What about running pytest which is much faster. Ah, yes, if we were to run pytest now all our mutation tests would fail because our mutation tests are not adding the JWT token as we were doing with insomnia. That is our next step.

Next chapter we dive into testing jwt mutations!