Django + Dash - New & Improved Relationship/Authentication

login_banner

Dash → Django → Dash : Unlock a New way of Building Fullstack Dash Applications

Hey Everyone, excited to announce this breakthrough. Honestly feel like this has a lot of potential as it could possibly allow an evolution within python full-stack development. Many trailblazers lead the way, Django-Plotly-Dash for instance was the first one to create a direct connection between the two frameworks.

My goal was to decouple the direct relationship setup within that package as with the introduction of pages and a need to maintain an up-to-date dash application independent from Django , my hypothesis is I would receive many benefits if I was able to connect them with an API instead of running dash in Django as has been the norm sense 2018.

While maintaining all the features and benefits of both frameworks.

The idea outlined in my project schema is to create two independent houses running separate applications instead of keeping them under the same project.

Ideally with everything in dash being designed to take advantage of .json what better way of setting up authentication, data fetching, and uploading than continuing from that basis?

This is why I’ve designed what is an advanced project, Django houses a few apps but for the purpose of this article and dash the only import one is the API. Building off FASTapi, I went with Ninja which is a powerful alternative that maintains the Django syntax along with a list of useful features.

Before we jump into the nit and gritty, let’s look at the Django application was setup and how the Dash app.py was setup.

Although this doesn’t cover everything… its a damn good start for getting Django hosted in a docker container with a Postgres database along with a bunch of useful optional features: https://youtu.be/oGHQCapKsac

The next step would be to install Ninja with pip install django-ninja & pip install django-ninja-jwt

Benchmarks of Ninja Vs other API's

With this, we need to make a few changes to our Django application first create an app to manage the API and connect it within the setting.py

Then we need to go the main urls.py and it should look like:

urls.py
from django.contrib import admin
from django.urls import path, include
from ninja_extra import NinjaExtraAPI, api_controller
from api.views import router as account_router
from ninja_jwt.controller import NinjaJWTDefaultController

# Starts the API with the NinjaExtraAPI
api = NinjaExtraAPI()
# Create a base rout for user management and authentication, you can add more routers here
api.add_router('/account/', account_router)

# Register the NinjaJWTDefaultController
api.register_controllers(NinjaJWTDefaultController)

urlpatterns = [
    path('admin/', admin.site.urls),

    path("api/", api.urls),
]

This next section has some fluff and a starting point for register but i still need to do some tweaking:

api/views.py
import pytz
from datetime import datetime
from ninja import NinjaAPI, Form
from ninja.security import HttpBasicAuth
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth import authenticate as auth_authenticate
from email_validator import validate_email, EmailNotValidError
from django.core.signing import Signer
from colorama import Fore as Color
from django.http import HttpResponse
from ninja import Router

router = Router()

class BasicAuth(HttpBasicAuth):
    def authenticate(self, request, username, password):
        # user = authenticate(request, username=username, password=password)
        # Request all Users on Application
        users = User.objects.all()
        # check username vs user in database
        user_in_users = users.values_list('username', flat=True)
        print('user_in_users', list(user_in_users))

        emails = users.values_list('email', flat=True)
        print('emails', list(emails))

        print('user provided', username)
        # check if username or email in database return login or none
        if username in list(user_in_users):
            user = auth_authenticate(username=username, password=password)
            if user is not None:

                def cookie_save_user_id_function(request, current_time):
                    signer = Signer()
                    user_id = User.objects.get(username=username).id
                    signed_obj = signer.sign_object({'username': username, 'email': user.email,'id': user_id, 'logged_in': True, 'time_logged_in': current_time})
                    return {'username': username, 'email': user.email,'id': user_id, 'logged_in': True, 'time_logged_in': current_time, 'signed_obj': signed_obj}

                user = authenticate(request, username=username, password=password)
                if user is not None:
                    # login user
                    login(request, user)
                    central_time = pytz.timezone('US/Central')
                    current_time = datetime.now(central_time)
                    current_time = current_time.strftime("%Y-%m-%d %H:%M:%S")
                    cookie = cookie_save_user_id_function(request, current_time)

                    # grab httpresponse
                    response = HttpResponse('You are logged in')
                    # set cookie
                    response.set_cookie('cookie', cookie['signed_obj'])


                    print(Color.GREEN+'Was able to login user, saved cookie of user_id to logged in', cookie)
                    print(Color.CYAN + 'User:', user, Color.RESET + f'Just logged into the application at: {current_time}')
                    # the credentials are valid
                    return cookie
                else:
                    print(Color.RED +'User was found but not able to authenticate on django')
                    # the credentials are invalid
                    return None
            else:
                # the credentials are invalid
                return None
        elif username in list(emails):
            print(Color.YELLOW + 'Wasn\'t able to find Username, trying email')
            print(Color.GREEN + 'Email in database')
            # gets username from email
            u = User.objects.get(email=username)

            def cookie_save_user_id_function(request, current_time):
                signer = Signer()
                user_id = User.objects.get(email=username).id
                signed_obj = signer.sign_object(
                    {'username': username, 'email': user.email, 'id': user_id, 'logged_in': True,
                     'time_logged_in': current_time})



                return {'username': u.username ,'email': username, 'id': user_id, 'logged_in': True, 'time_logged_in': current_time, 'signed_obj': signed_obj}


            # checks authentication but doesn't login
            # user = auth_authenticate(username=u, password=password)
            # authenticate
            user = authenticate(request, username=u, password=password)
            if user is not None:
                print('testing username')
                print(u.username)
                # login user
                login(request, user)
                central_time = pytz.timezone('US/Central')
                current_time = datetime.now(central_time)
                current_time = current_time.strftime("%Y-%m-%d %H:%M:%S")
                cookie = cookie_save_user_id_function(request, current_time)

                # grab httpresponse
                response = HttpResponse('You are logged in')
                # set cookie
                response.set_cookie('cookie', cookie['signed_obj'])

                print(Color.GREEN + 'Was able to login user, saved cookie of user_id to logged in', cookie['signed_obj'])
                print(Color.CYAN + 'User:', user, Color.RESET + f'Just logged into the application at: {current_time}')
                # the credentials are valid
                return cookie
            else:
                print(Color.RED + 'Username / Email not in database')
                # the credentials are invalid
                return None


@router.get("/login", auth=BasicAuth())
def account_login(request):
    return {"httpuser": request.auth}


@router.post("/register")
def register(request, username: str = Form(...), email: str = Form(...), password: str = Form(...), password_checker: str = Form(...)):
    # Request all Users on Application
    users = User.objects.all()
    # check username vs user in database
    user_in_users = users.values_list('username', flat=True)
    # print('user_in_users', list(user_in_users))

    emails = users.values_list('email', flat=True)

    email_checker = validate_email(email)
    print('Testing email_checker')
    print(email_checker)

    if username in list(user_in_users):
        print('Username already taken')
        return request.content_params == {'Username': 'already taken'}
    elif email in list(emails):
        print('Email already taken')
        return request.content_params == {'Email': 'already taken'}
    elif validate_email(email):
        if password == password_checker:
            print('Passwords match')
            user = User.objects.create_user(username, email, password)
            user.save()
            return {'message': 'User Created'}
        else:
            print('Passwords don\'t match')
            return request.content_params == {'Passwords': 'Do Not Match'}
    else:
        print('Email not valid')
        return request.content_params == {'Email': 'not valid'}

Once all this is setup, run the app and make sure you have http://localhost:8000/api/docs and everything is working.

Now with the project running, we can finally work in dash and the real fun starts. The idea is basically, we need to set up a data folder to house our requests to the Django database for organizational reasons. Within the app.py file, we will create a login forum, I decided to use a modal to display the forum & run the login logic on an @callback but you are welcome to change stuff and see what you can figure out. Then when the @callback is run it will send everything to a token that was created from the API to a cookie to save the users state within the application where we create another @callback to refer to this cookie on default @callback render and change aspects of the application showing the user has successfully logged in. For example, displaying navbar for the user vs the default anonymous user navbar.

First lets setup our data folder in dash:

data/ninja_test.py
import requests
import colorama


def login(username, password):

    url_login = f'http://{username}:{password}@127.0.0.1:8000/api/account/login'

    response = requests.get(url_login, auth=(username, password))

    if response.status_code == 200:
        print(colorama.Fore.GREEN + f"Login Successful: {response.json()['httpuser']}")
        print(colorama.Fore.YELLOW + f'{response.headers}')

        print(colorama.Fore.RESET)
        print(response.json())
        return response.json()
    else:
        print(colorama.Fore.RED + f"Login Failed: {username}")
        print(colorama.Fore.RESET)
        return False


def register(username, email, password, password_checker):
    print('Testing Register')
    url_register = 'http://127.0.0.1:8000/api/account/register'

    response = requests.post(url_register, data={'username': username, 'email': email, 'password': password, 'password_checker': password_checker})

    if response.status_code == 200:
        print(colorama.Fore.GREEN + f"Registration Successful: {response.json()['httpuser']}")
        print(colorama.Fore.RESET)
        return response.json()
    else:
        print(colorama.Fore.RED + f"Registration Failed: {username}")
        print(colorama.Fore.RESET)
        return False


def create_user_token(username, password):
    url_login = f'http://127.0.0.1:8000/api/token/pair'

    curl_body = {
        'password': password,
        'username': username
    }

    response = requests.post(url_login, json=curl_body)
    if response.status_code == 200:
        print(colorama.Fore.GREEN + f"Login Successful: {response.status_code}", colorama.Fore.RESET)
    else:
        print(colorama.Fore.RED + f"Login Failed: {username}", colorama.Fore.RESET)

    print('headers')
    print(response.headers)
    print('response')
    print(response.json())
    return response.json()


def refresh_user_token(refresh_token):
    refresh_token_url = f'http://127.0.0.1:8000/api/token/refresh'

    curl_body = {
        'refresh': refresh_token
    }

    response = requests.post(refresh_token_url, json=curl_body)
    if response.status_code == 200:
        print(colorama.Fore.GREEN + f"Login Successful: {response.status_code}", colorama.Fore.RESET)
    else:
        print(colorama.Fore.RED + f"Login Failed: {response.status_code}", colorama.Fore.RESET)
    print('headers')
    print(response.headers)
    print('access_token')
    print(response.json())
    return response.json()


def verify_user_token(access_token):
    verify_token_url = f'http://127.0.0.1:8000/api/token/verify'

    curl_body = {
        'token': access_token
    }

    response = requests.post(verify_token_url, json=curl_body)
    if response.status_code == 200:
        print(colorama.Fore.GREEN + f"Login Successful: {response.status_code}", colorama.Fore.RESET)
    else:
        print(colorama.Fore.RED + f"Login Failed: {response.status_code}", colorama.Fore.RESET)
    print('headers')
    print(response.headers)
    print('access_token')

    if response.json() == {}:
        return True
    else:
        return False



if __name__ == '__main__':
    # login('pip', 'NeverTellMeTheOdds')
    create_user_token(username='pip', password='NeverTellMeTheOdds')

    # refresh_user_token('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTY3Mjc1Mzc1OSwiaWF0IjoxNjcyNjY3MzU5LCJqdGkiOiJlZGQwMzkxMWFkNDY0MWI4OTBmOWJkMDEwYjE1ZTliNCIsInVzZXJfaWQiOjF9.t4QvVzxUja2TfiBw_qEkwypFeOoIaOA1GVngKpwK258')

    # print(verify_user_token('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjcyNjY3ODY0LCJpYXQiOjE2NzI2NjczNTksImp0aSI6ImQyYzBkMzU5ZTJkNDRiZTNiN2I4MjEyZmM2YjJkNDhkIiwidXNlcl9pZCI6MX0.y43WCcRgs5KnXJ5yAl-HFwKALyHIA2ZGMaxBrSeRzzM'))

Then we can look into setting up the main app.py file:

app.py
import dash
from dash import html, Dash, dcc
from dash.dependencies import Output, Input, State
import dash_mantine_components as dmc
import dash_bootstrap_components as dbc
from dash_iconify import DashIconify
from flask import Flask, render_template

from data.ninja_test import login, register, create_user_token, refresh_user_token, verify_user_token

from http.cookiejar import CookieJar
from http.cookiejar import Cookie

server = Flask(__name__)

#setup cookieJar
cookieJar = CookieJar()

app = Dash(
    __name__,
    assets_url_path="assets",
    external_stylesheets=[
        "https://use.fontawesome.com/releases/v6.2.1/css/all.css",
        dbc.themes.SKETCHY,
    ],
    external_scripts=[],
    use_pages=True,
    server=server,
)

login_button = dmc.Buton("Login",
                         id="login-modal-button",
                         )

login_form = dbc.Form(
    [
        dbc.Col(
            [
                dmc.Stack(
                    children=[
                        dmc.TextInput(
                            label="Your Username or Email:",
                            style={"width": "100%"},
                            id="login-username",
                        ),
                    ],
                )
            ]
        ),
        dbc.Col(
            [
                dmc.PasswordInput(
                    label="Your password:",
                    style={"width": "100%"},
                    placeholder="Your password",
                    icon=DashIconify(icon="bi:shield-lock"),
                    id="login-password",
                )
            ]
        ),
    ],
    className="mb-5",
)

# Create the layout for the login screen
login_modal = dmc.Card(
    children=[
        dmc.CardSection(
            dmc.Image(
                src=dash.get_asset_url("gif/login_banner.gif"),
                height=160,
            )
        ),
        dmc.Group(
            [
                dmc.Text("Access Account", weight=500),
            ],
            position="apart",
            mt="md",
            mb="xs",
        ),
        login_form,
        dmc.Button(
            "Login",
            variant="light",
            color="blue",
            fullWidth=True,
            mt="md",
            radius="md",
            id="login-button",
        ),
    ],
    withBorder=True,
    shadow="sm",
    radius="md",
    style={"width": 350},
    id="login-modal-form",
)

# modal login
login_to_account = dmc.Modal(
    children=[
        html.Div(id="welcome-back-alert"),
        html.Div(id="User-Avatar"),
        login_modal,
    ],
    id="login-account-modal",
    overflow="outside",
    opened=False,
    size="sm",
)

test_checking_user_active = html.Div(id='display_test')
test_checking_user_active_button = html.Button('Test', id='test-button', n_clicks=1)

app.layout = html.Div(
    [
        test_checking_user_active_button,
        login_to_account,
        test_checking_user_active,
        dash.page_container,

    ],
    style={"height": "100vh"},
)

@app.callback(Output('display_test', 'children'), [Input('test-button', 'n_clicks')])
def checking_user_active(n_clicks):
    print(n_clicks)
    if n_clicks % 2 != 0:
        keys = []
        for c in cookieJar:
            print(c)
            if c.name == 'account_cookie':
                print('found cookie')
                print(f'Refresh KEY: {c.value}')
                print(f'Secret KEY: {c.comment}')
                keys.append(c.comment)
        return dbc.Card(id='test-checking-user-active', children=[
        dbc.CardHeader(f"Testing Active Users"),
        dbc.CardBody([
            # html.H4(f"{}", className="card-title"),
            html.H4(id='active_users', className="card-title"),
            html.H4(f"Cookie Jar: {cookieJar}"),
            html.P(f"{keys}", className="card-text"),
            # html.H4(f"cookie_jar: {req_cookie_jar}", className="card-title"),
        ])])
    else:
        return None

# Store Data through Callbacks
@app.callback(
    # Output("store", "data"), hiding the modal on success
    [
        Output("welcome-back-alert", "children"),
        Output("login-modal-form", "hidden"),
        Output("User-Avatar", "children"),
    ],
    [
        Input("login-button", "n_clicks"),
        Input("login-username", "value"),
        Input("login-password", "value"),
    ],
    prevent_initial_call=True,
)
def get_data(login_button, username, password):
    if login_button is None:
        pass
    elif login_button:
        if username and password is not None:
            login_test = login(username, password)
            if login_test:
                user_token = create_user_token(username='pip', password='12a10l1k')
                c = Cookie(None, 'account_cookie', f'{user_token["refresh"]}', '8000', True, '127.0.0.1',
                           True, False, '/', True, False, '1370002304', False, f'{user_token["access"]}', None, None,
                           False)
                cookieJar.set_cookie(c)
                print('Cookie Set')
                print(f'Cookie: {c}')
                print(f'CookieJar: {cookieJar}')
                
                welcome_back_alert = dmc.Alert(
                    f'Hey, {login_test["httpuser"]["username"]} you just logged in. Best way to take full advantage of this powerful Ai is to build up your account and we will help get you new upgrades, tips, tools & cheatcodes.',
                    title="Welcome Back to Maply.io",
                    color="green",
                    id="welcome-back-alert",
                    withCloseButton=True,
                    hide=False,
                )

                return (
                    welcome_back_alert,
                    True,
                    html.Center(
                        html.Img(
                            src=dash.get_asset_url(
                                "https://yt3.ggpht.com/_7416qbIfG4-6jW_WIYnIzOzYxz-xiI13Bs6wAEMIHLxCqLL__n0chPeXPkS3r7O0N7SqEVGww=s600-c-k-c0x00ffffff-no-rj-rp-mo"
                            ),
                            style={"width": "100px", "height": "250px"},
                        )
                    ),
                )
            else:
                pass
    else:
        pass

if __name__ == "__main__":

    app.run_server(
        host=f"{host}",
        debug=True,
        port=f"{port}",
        threaded=True,
    )

Tried to keep it simple as possible as I took a lot of the code out of an existing project, which might need some tweaking. Regardless the end result is you are able to login with the @callback get_data then you can refer to the cookie in the @callback checking_user_active.

4 Likes

Hey @PipInstallPython, thanks for writing this up. If you don’t mind me asking, what is the benefit of having the Django app coupled with the Dash app?

1 Like

@RenaudLN , well Django is a powerful backend framework being used by Youtube, Instagram NASA ect…

The benefits are the ease of creating models within the framework. Out of the box, has users & authentication setup, has an admin portal for displaying everything and you can update the database directly from that admin portal and have it update on the app in real-time. Outside of all that with it supporting async you have a strong backend to handle the processing of advanced tasks that would take some time to run. You could set up celery to manage these tasks and store them internally to also improve load times.

Not only that you have a base application you can refer to in regard to any future smaller applications that get developed. You can create multiple full-stack dash apps and now that you have the capability of login authentication via dash/django you can relate all these different apps to one user endpoint. Thinking in terms of google, you have youtube, google docs, google analytics, … and countless other google providers. They all take in just one username and password that’s related to your overall google account. This setup would allow the same benefits of scalability.

Outside of all that my use case has been, Django makes an amazing blog and e-commerce app. Dash does not… wasn’t built for that its a graphing/dashboard tool. Keeping them separate but connected I get the best of both worlds. With dash pages, I can design layered complex dashboards. With Django I can design the schema, manage users, run a central API, host an admin portal and maintain the templates & apps that work best with that framework.

Needs updating, but this should scratch the surface of a visual idea of the two:

Dash App

Django App

Currently, authentication and everything in the blog post above haven’t been updated to production as I still am working out the kinks, just wanted to structure my work and make it public the progress I’ve made.

2 Likes