Skip to content

ulysse-lacour/Jolify-back

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JOLIFY-back

Frontend : https://github.com/ulysse-lacour/Jolify-front




Process to create your django graphql backend API.


Related docs :



PIPENV

Activate/create env :

pipenv shell

Run command within shell :

pipenv run command_name

Install packages :

pipenv install package_name

You cand add --dev after package_name for packages not suited for production Or install all packages from Pipfile with :

pipenv install

To check your current dependency tree :

pipenv graph

Create .lock for production :

pipenv lock

Deactivate env :

exit

Documentation



Create Django project and apps

Make sure your virtualenv is activated !

Add needed packages (with version needed) to Pipfile then run pipenv install.

Create Django project with :

django-admin startproject backend

Then cd backend (once, same level as manage.py and create apps with :

django-admin startapp apps


Directories/files to add/modify

backend/
    schema.py

apps/
    models.py (NEED TO BE DELETED)
    views.py (NEED TO BE DELETED)

    api/
        __init__.py
        api_name.py

    models/
        __init__.py (NOT EMPTY)

    schema/
        __init__.py (NOT EMPTY)

        nodes/
            __init__.py
            node_name.py

        operations/
            __init__.py
            operation_name.py

logs/
    readme.txt

Example :

See image

Directory structure



Update backend/urls.py

Code
from django.contrib import admin
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
]

Update backend/settings.py

Code

Modify/add :

ALLOWED_HOSTS = ['domain_or_IP', 'other_domain_or_IP', . . ., 'localhost']
INSTALLED_APPS = [
    ...
    'graphene_django',
    'graphql_jwt.refresh_token.apps.RefreshTokenConfig',
    'apps',
    ...
]
MIDDLEWARE = [
    ...
    #after this
    'django.contrib.sessions.middleware.SessionMiddleware',
    # this is the line to add
    'corsheaders.middleware.CorsMiddleware',
    # before this
    'django.middleware.common.CommonMiddleware',
    ...
]
import os

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
GRAPHENE = {
    'SCHEMA': 'backend.schema.schema',
    'MIDDLEWARE': [
        'graphql_jwt.middleware.JSONWebTokenMiddleware',
    ],
}

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

CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",
    "http://127.0.0.1:3000"
]

CSRF_TRUSTED_ORIGINS = [
    "http://localhost:3000",
    "http://127.0.0.1:3000"
]

AUTH_USER_MODEL = 'apps.User'

Replace :

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

With

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'my_project',
        'USER': 'my_project_user',
        'PASSWORD': 'my_project_password',
        'HOST': 'localhost',
        'PORT': '',
    }
}

Update backend/schema.py

Code
import graphene
import graphql_jwt

import apps.schema as schema


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


class Mutation(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)

Replace apps/admin.py

Code
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from apps.models import User

admin.site.register(User, UserAdmin)

Update apps/schema/init.py

Code
from apps.schema.nodes.user import UserNode
from apps.schema.operations import user
from apps.schema.operations import spotify

TYPES = [
    UserNode
]

class Mutation(
    user.Mutation,
    spotify.Mutation,
):
    pass

class Query(
    user.Query,
):

    pass

Update apps/schema/operations/user.py

Code
import graphene

from graphene import ResolveInfo

from apps.schema.nodes.user import UserNode
from apps.api.user import UserAPI
from apps.models import User


class Query(graphene.ObjectType):
    whoami = graphene.Field(UserNode)
    users = graphene.List(UserNode)

    def resolve_whoami(self, info):
        user = info.context.user
        # Check to to ensure you're signed-in to see yourself
        if user.is_anonymous:
            raise Exception('Authentication Failure: Your must be signed in')
        return user

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


class CreateUser(graphene.Mutation):
    class Arguments:
        username = graphene.String()
        password = graphene.String()
        firstName = graphene.String()
        lastName = graphene.String()
        email = graphene.String()

    success = graphene.Boolean()
    token = graphene.String()
    refresh_token = graphene.String()
    details = graphene.String()

    def mutate(self, info: ResolveInfo, **kwargs):
        create_user = UserAPI.create_user(
            mutation_input=kwargs,
            context=info.context
        )

        return CreateUser(
            success=create_user.get("success"),
            token=create_user.get("token"),
            refresh_token=create_user.get("refresh_token"),
            details=create_user.get("details"),
        )


class LoginUser(graphene.Mutation):
    class Arguments:
        username = graphene.String()
        password = graphene.String()

    success = graphene.Boolean()
    token = graphene.String()
    refresh_token = graphene.String()
    details = graphene.String()

    def mutate(self, info: ResolveInfo, **kwargs):
        login_user = UserAPI.login_user(
            mutation_input=kwargs,
            context=info.context
        )

        return LoginUser(
            success=login_user.get("success"),
            token=login_user.get("token"),
            refresh_token=login_user.get("refresh_token"),
            details=login_user.get("details"),
        )


class LogoutUser(graphene.Mutation):
    success = graphene.Boolean()
    details = graphene.String()

    def mutate(self, info: ResolveInfo):
        logout_user = UserAPI.logout_user(context=info.context)

        return LogoutUser(
            success=logout_user.get("success"),
            details=logout_user.get("details"),
        )


class Mutation(graphene.ObjectType):
    create_user = CreateUser.Field()
    login_user = LoginUser.Field()
    logout_user = LogoutUser.Field()

Update apps/schema/nodes/user.py

Code
from graphene_django import DjangoObjectType
from apps.models import User


class UserNode(DjangoObjectType):
    """ Node for User model """
    class Meta:
        model = User
        name = 'User'

Update apps/models/init.py

Code
from django.db import models
from django.contrib.auth.models import AbstractUser


class User(AbstractUser):
    """ User model, extends basic User model with some custom fields. """
    nick_name = models.CharField(max_length=100, verbose_name='Surnom')
    profile_picture = models.ImageField(verbose_name='Photo de profil')

Update apps/api/user.py

Code
from django.contrib.auth import authenticate, login, logout
from typing import Any, Dict
from graphql_jwt.shortcuts import create_refresh_token, get_token

from django.http import HttpRequest

from apps.models import User


class UserAPI:

    @staticmethod
    def create_user(
        mutation_input: Dict[str, Any],
        context: HttpRequest,
    ) -> Dict[str, Any]:
        username = mutation_input.get("username")
        password = mutation_input.get("password")
        firstName = mutation_input.get("firstName")
        lastName = mutation_input.get("lastName")
        email = mutation_input.get("email")
        success = False
        token = None
        refresh_token = None

        if username is None or password is None:
            details = "Input malformed !"
            return {"success": success, "token": token, "refresh_token": refresh_token, "details": details}

        user = User.objects.filter(username=username).exists()

        if user:
            details = "User already exist !"
        else:
            user = User(username=username, first_name=firstName, last_name=lastName, email=email)
            user.set_password(password)
            user.save()
            token = get_token(user)
            refresh_token = create_refresh_token(user)
            success = True
            details = f"User {username} created successfully !"

        return {"success": success, "token": token, "refresh_token": refresh_token, "details": details}

    # TODO : improve error handling

    @staticmethod
    def login_user(
        mutation_input: Dict[str, Any],
        context: HttpRequest,
    ) -> Dict[str, Any]:
        username = mutation_input.get("username")
        password = mutation_input.get("password")

        user = authenticate(context, username=username, password=password)

        if user is not None:
            login(context, user)
            token = get_token(user)
            refresh_token = create_refresh_token(user)
            success = True
            details = f"User {username} logged in !"
        else:
            token = None
            refresh_token = None
            success = False
            details = "Error in either password or username !"

        return {"success": success, "token": token, "refresh_token": refresh_token, "details": details}

    @staticmethod
    def logout_user(
        context: HttpRequest,
    ) -> Dict[str, Any]:
        logout(context)

        return {"success": True, "details": "NONE"}


Database

Make sure backend/settings.py has the right config for our postgres database!


SERVER PROCEDURE :

Login to Postgres

sudo -u postgres psql

Create database for project

CREATE DATABASE my_project;

Create user

CREATE USER my_project_user WITH PASSWORD 'password';

Alter role with Django prerequisite

ALTER ROLE my_project_user SET client_encoding TO 'utf8';
ALTER ROLE my_project_user SET default_transaction_isolation TO 'read committed';
ALTER ROLE my_project_user SET timezone TO 'UTC';

And grant privileges to user on the database

GRANT ALL PRIVILEGES ON DATABASE my_project TO my_project_user;

Exit postgres

\q

Migrate

python manage.py makemigrations apps
python manage.py migrate


LOCAL PROCEDURE :

Create role/user

psql -d postgres -c "CREATE ROLE my_project_user WITH LOGIN PASSWORD 'password';"

Create database

psql -d postgres -c "CREATE DATABASE my_project WITH OWNER my_project_user;"

Migrate and create superuser

Make sure to be in your virtualenv !

python manage.py makemigrations apps
python manage.py migrate


View project

Inside you virtualenv !



Collect static

Make sure to have static config in settings.py.

import os

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')

You can then run collectstatic to allow nginx to serve those files.

python manage.py collectstatic


LOCAL PROCEDURE :

Launch local server :

python manage.py runserver

Acces it via : http://localhost:8000



SERVER PROCEDURE :

Launch dev server :

python ~/my_project_dir/manage.py runserver 0.0.0.0:8000

Acces it via : http://server_domain_or_IP:8000

Test gunicorn :

gunicorn --bind 0.0.0.0:8000 my_project.wsgi

With gunicorn test static folder won't be loaded.

Make sure to exit your python environment before dealing with nginx and gunicorn files.



Test GQL request

With graphiQL : http://localhost:8000/graphql/ or http://domain_name:8000/graphql/

Test mutation

mutation {
  createUser(password: "1234567890", username: "test1") {
    success
    details
    token
    refreshToken
  }
}

Test query

query {
  users {
    id
    password
    lastLogin
    isSuperuser
    username
    firstName
    lastName
    email
    isStaff
    isActive
    dateJoined
    nickName
    profilePicture
  }
}


Set up gunicorn

Create socket file :

sudo nano /etc/systemd/system/my_project.socket

Add :

[Unit]
Description=my_project socket

[Socket]
ListenStream=/run/my_project.sock

[Install]
WantedBy=sockets.target

Create service file

To find env name look for pipenv shell path

sudo nano /etc/systemd/system/my_project.service

Add :

[Unit]
Description=gunicorn daemon
Requires=my_project.socket
After=network.target

[Service]
User=linux_user
Group=www-data
WorkingDirectory=/home/linux_user/django/my_project/
ExecStart=/home/linux_user/.local/share/virtualenvs/my_project_ENV/bin/gunicorn \
          --access-logfile - \
          --workers 3 \
          --bind unix:/run/my_project.sock \
          my_project.wsgi:application

[Install]
WantedBy=multi-user.target

Start and enable socket file :

This will create the socket file at /run/my_project.sock. When a connection is made to that socket, systemd will automatically start the my_project.service to handle it.

sudo systemctl start my_project.socket
sudo systemctl enable my_project.socket

Check if it worked :

Get socket status :

sudo systemctl status my_project.socket

Should return something like :

● my_project.socket - my_project socket Loaded: loaded (/etc/systemd/system/my_project.socket; enabled; vendor prese> Active: active (listening) since Fri 2020-06-26 17:53:10 UTC; 14s ago Triggers: ● my_project.service Listen: /run/my_project.sock (Stream) Tasks: 0 (limit: 1137) Memory: 0B CGroup: /system.slice/my_project.socket

Check if .sock is there :

file /run/my_project.sock

Should return :

/run/my_project.sock: socket

If some errors appears check the socket log :

sudo journalctl -u my_project.socket

If everything's allright with socket check for .service mechanism :

sudo systemctl status my_project

Should return :

● my_project.service - my_project daemon Loaded: loaded (/etc/systemd/system/my_project.service; disabled; vendor preset: enabled) Active: inactive (dead)

To activate it request via :

curl --unix-socket /run/my_project.sock localhost

You should receive raw HTML of the project, check again .service status via :

sudo systemctl status my_project

Active: inactive (dead) Should now be : Active: active (running)

If you did any changes to either .socket or .service files :

sudo systemctl daemon-reload
sudo systemctl restart my_project


Set up nginx

Create server block :

sudo nano /etc/nginx/sites-available/my_project

And add :

server {
    listen 80;
    server_name IP adress or domain name;

    access_log /home/eljovial/django/my_project/logs/access.log;
    error_log /home/eljovial/django/my_project/logs/error.log error;

    location = /favicon.ico { access_log off; log_not_found off; }
    location /static/ {
        root /home/eljovial/django/my_project;
    }

    location / {
        proxy_pass http://unix:/run/my_project.sock;
        include proxy_params;
    }
}

You may need to comment (#) include proxy_params; for a subdomain.

Link to sites-enabled :

sudo ln -s /etc/nginx/sites-available/my_project /etc/nginx/sites-enabled

Check for syntax errors :

    sudo nginx -t

Restart nginx :

sudo systemctl restart nginx

Or you can restart nginx with bash ~/restart_nginx.sh



Bash scripts and alias

For each project you cand add to ~/.profile :

alias my_project-log="sudo journalctl -u my_project -n 150"
alias my_project-reboot="sudo systemctl daemon-reload && sudo systemctl restart my_project.socket my_project"
alias my_project-status="sudo systemctl status my_project"