Problems with trying to implement flask-login into my multi-page app

I apologize for the length of code - I pushed it to a GitHub repo to make it easier to work with.

The problems I have been running into (and have spent the last several days trying to solve):

  • When I log into the app, it only ‘recognizes’ the login after I manually refresh the page. Otherwise, I can enter the correct credentials repeatedly and it will just say on the login page
  • Page 1 has a nav menu along the left side. I would like to be able to show a different menu based on the username that was used during login. However, I don’t know how to get this variable outside of the callback function. FWIW, the app I am trying to build is ~10 pages, and all of them have this nav menu.

Any help with these would be EXTREMELY appreciated!

app.py:

import dash
import dash_bootstrap_components as dbc
import src.page_container as page_container
from src.pages.login_callback import server

app = dash.Dash(
    __name__,
    server=server,
    use_pages=True,
    pages_folder='src',
    external_stylesheets=[dbc.themes.BOOTSTRAP]
)

page_container.main_layout(app)

if __name__ == "__main__":
    app.run_server(debug=True)

login_callback.py:

from flask import Flask
from flask_login import login_user, LoginManager, UserMixin
import dash
from dash import Input, Output, State
from dash.exceptions import PreventUpdate

VALID_USERNAME_PASSWORD = {"user1": "pw", "user2": "pw"}
FLASK_SECRET_KEY='abcd1234'

server = Flask(__name__)

class User(UserMixin):
    def __init__(self, username):
        self.id = username

server.config.update(SECRET_KEY=FLASK_SECRET_KEY)

login_manager = LoginManager()
login_manager.init_app(server)
login_manager.login_view = "/login"


@login_manager.user_loader
def load_user(username):
    return User(username)


@dash.callback(
    Output("redirect", "href"),
    Output("output-state", "children"),
    Output("session_user", "data"),
    Input("login-button", "n_clicks"),
    State("username", "value"),
    State("password", "value"),
    prevent_initial_call=True,
)
def login_button_click(n_clicks, username, password):
    if n_clicks:
        if VALID_USERNAME_PASSWORD.get(username) is None:
            return '/login', 'please enter pw', None
        elif VALID_USERNAME_PASSWORD.get(username) == password:
            login_user(User(username))
            return '/page-1', '', username
        elif VALID_USERNAME_PASSWORD.get(username) != password:
            return '/login', 'wrong pw', None
    else: raise PreventUpdate

page_container.py:

import dash
from dash import dcc, html
import dash_bootstrap_components as dbc
from flask_login import current_user


def main_layout(app):
    app.layout = dbc.Container([
        dcc.Store(id="session_user", storage_type="session"),
        html.Div([dash.page_container]),
    ], fluid=True)

page-1.py:

import dash
from dash import html, dcc
from flask_login import current_user
import dash_bootstrap_components as dbc

dash.register_page(__name__, path="/page-1")

if username == user1:
    navigation = dbc.Nav([
        dbc.NavLink('user1 page-1', href='/page-1'),
        dbc.NavLink('user1 page-2', href='page-2'),
        dbc.NavLink('user1 logout', href='/logout')
    ], vertical=True, pills=True)
elif username == user2:
    navigation = dbc.Nav([
        dbc.NavLink('user2 page-1', href='/page-1'),
        dbc.NavLink('user2 page-2', href='page-2'),
        dbc.NavLink('user2 logout', href='/logout')
    ], vertical=True, pills=True)


def layout():
    if not current_user.is_authenticated:
        return html.Div([dcc.Link("login", href="/login"), " to continue"])
    return dbc.Container([
        dbc.Row([
            dbc.Col([
                navigation
            ]),
            dbc.Col([
                html.H1("Page 1"),
                dcc.Link("Go to page 2", href="/page-2")
            ]),
        ]),
    ])

page-2.py:

import dash
from dash import html, dcc
from flask_login import current_user

dash.register_page(__name__, path="/page-2")

def layout():
    if not current_user.is_authenticated:
        return html.Div([dcc.Link("login", href="/login"), " to continue"])
    return html.Div([
        html.H1("Page 2"),
        dcc.Link("Go to page 1", href="/page-1")
    ])

login.py:

import dash
from dash import html, dcc

dash.register_page(__name__, path="/login")

layout = html.Div([
    dcc.Input(id='username', placeholder='username'),
    dcc.Input(id='password', placeholder='password'),
    dcc.Link(
        html.Button("Login", id="login-button"),
        id='redirect',
        href='/page-1'
    ),
    html.Div(children="", id="output-state"),
])

logout.py:

import dash
from dash import html, dcc
from flask_login import logout_user, current_user

dash.register_page(__name__, path="/logout")

def layout():
    if current_user.is_authenticated:
        logout_user()
    return html.Div([
        html.Div(html.H2("Please login")),
        dcc.Link("Login", href="/login"),
    ])

@anarchocaps,

page1.py

import dash
from dash import html, dcc
from flask_login import current_user
import dash_bootstrap_components as dbc

dash.register_page(__name__, path="/page-1")

def layout():
    try:
        username = current_user.id
    except:
        username = None
    if username == 'user1':
        navigation = dbc.Nav([
            dbc.NavLink('user1 page-1', href='/page-1'),
            dbc.NavLink('user1 page-2', href='page-2'),
            dbc.NavLink('user1 logout', href='/logout')
        ], vertical=True, pills=True)
    elif username == 'user2':
        navigation = dbc.Nav([
            dbc.NavLink('user2 page-1', href='/page-1'),
            dbc.NavLink('user2 page-2', href='page-2'),
            dbc.NavLink('user2 logout', href='/logout')
        ], vertical=True, pills=True)
    else:
        navigation = dbc.Nav([dbc.NavLink('login', href='/login'), ])
    if not current_user.is_authenticated:
        return html.Div([dcc.Link("login", href="/login"), " to continue"])
    return dbc.Container([
        dbc.Row([
            dbc.Col([
                navigation
            ]),
            dbc.Col([
                html.H1("Page 1"),
                dcc.Link("Go to page 2", href="/page-2")
            ]),
        ]),
    ])

login.py

import dash
from dash import dcc, html
import dash_bootstrap_components as dbc


def main_layout(app):
    app.layout = dbc.Container([dcc.Location(id='redirect'),
        dcc.Store(id="session_user", storage_type="session"),
        html.Div([dash.page_container]),
    ], fluid=True)

I think these are all that I changed.

Your issue, the login had a link wrapped around the button that was redirecting to page one which resulted in the loop.

For page1, I adjusted the function to pull the username from the session cookie of flask, all inside of the layout function.

Let me know if this fixes your issues.

Thanks @jinnyzor! The different navigation menus are working perfect. However, the app still requires a manual refresh to make it complete the login process… Did this code work for you on your end?

@jinnyzor I commented the dcc.Link wrapper out and put an html.Button in instead and it works - but what I really was trying to achieve was the app auto-redirecting to /page-1 upon a successful login…

login.py updated:

import dash
from dash import html, dcc

dash.register_page(__name__, path="/login")

layout = html.Div([
    dcc.Input(id='username', placeholder='username'),
    dcc.Input(id='password', placeholder='password'),
    # dcc.Link(
    #     html.Button("Login", id="login-button"),
    #     id='redirect',
    #     #href='/login'
    #     href='/page-1'
    # ),
    html.Button(children="Login", n_clicks=0, type="submit", id="login-button"),
    html.Div(children="", id="output-state"),
])

@anarchocaps, it was working in mine, maybe pass a dcc.location as id=redirect in the page_container.py?

@jinnyzor - got it. Everything is working great now in my example app.

The only issue I am running into now is that when I include if not current_user.is_authenticated: before each page’s layout, I am getting this error:

AttributeError: 'NoneType' object has no attribute 'is_authenticated'

as an example for one page’s layout:

import dash_bootstrap_components as dbc
from dash import dcc, html
from flask_login import current_user

def layout():
    if not current_user.is_authenticated:
        return html.Div([dcc.Link("login", href="/login"), " to continue"])
    return dbc.Container([
        dcc.Download(id='psr_dwnld'),
        dcc.Download(id='prtflo_dwnld'),
        dcc.Download(id='sp_dwnld'),
        dcc.Download(id='sb_dwnld'),
        dcc.Download(id='sd_dwnld'),
        html.Div([
            dbc.Row([
                dbc.Col([
                    create_menu_column(),
                ]),

            ])
        ])
    ], fluid=True)

Thanks again for your help with this this. I appreciate it a lot.

@anarchocaps,

Yeah, the authentication can be a bit tricky. You are trying to mix the Flask_Login functionality into Dash. The flask_login looks for a cookie and user together, but if you havent logged in it has no user.

You can add a try except clause:

import dash_bootstrap_components as dbc
from dash import dcc, html
from flask_login import current_user

def layout():
    try:
        if not current_user.is_authenticated:
              return html.Div([dcc.Link("login", href="/login"), " to continue"])
    except:
        return html.Div([dcc.Link("login", href="/login"), " to continue"])
    return dbc.Container([
        dcc.Download(id='psr_dwnld'),
        dcc.Download(id='prtflo_dwnld'),
        dcc.Download(id='sp_dwnld'),
        dcc.Download(id='sb_dwnld'),
        dcc.Download(id='sd_dwnld'),
        html.Div([
            dbc.Row([
                dbc.Col([
                    create_menu_column(),
                ]),

            ])
        ])
    ], fluid=True)

There might be another way to work with this though.

@anarchocaps,

I’m also curious, can you add this to your login_callback.py and see if it works?

from flask import redirect

@server.after_request
def testLogin(response):
    if current_user:
        return response
    return redirect("/login")

Try this one before the try except stuff.

@jinnyzor - no luck with either unfortunately. The first suggestion just always returns the layout in the try/except even when authenticated.

What makes no sense to me is why using current_user.is_authenticated works fine in the example I initially posted, but in my actual project I continue to get 'NoneType' object has no attribute 'is_authenticated'.

Both scenarios are nearly identical at this point - including the dir structure…

Its due to an anonymous mixin, which is the result of the session cookie not being present.

Are you hosting it somewhere else and trying to use it?

no - I do plan on hosting it on aws at some point, but at the moment, I am merely running both the working and non-working examples on my local machine.

Why does the initial example I posted not have this anonymous mixin issue? I can’t find any real difference between the two examples at this point?

How are you initializing the non-working example?

I am initializing both examples the same way - code is in vscode, and I am doing $ python application.py in Git Bash while in their respective project folders

Can you run it in a virtual environment instead? Something might be going awry with using the Bash.

Plus, running it in a virtual environment helps with package dependencies and implementing on another server.

Sounds like something isnt working quite as expected with your path.

How’d you trigger it with the environment?

Just tied this and it is giving the same error. But also, when running both examples out of the global env, the initial example is working fine…

As far as triggering it with the venv, I am just selecting that project’s venv in it’s project folder (making this selection within vscode), and initializing the app with git bash as described before.

I would be content at this point just setting dcc.Store to ‘True’ when someone is logged in and saying before the layout:

if dcc.store == True:
page’s actual layout
else:
‘you must login’ layout

The problem again is getting the value of dcc.store outside of the callback…

Using a dcc.store data update for private info is not good.

I dont think that git bash takes into account that you want to run it in a venv. You can start the bash there, but then activate the venv by this:

Command Prompt (windows):

.\venv\Scripts\activate

Linux(bash?):

source ./venv/bin/activate

OK. I was not thinking of storing anyting private in it - merely just True/False for if someone is logged in. Then the layout is determined from this. Once they log out, it gets set to False.

I will give what you suggested a try, but why would git bash be working fine for the initial example?

Within VSCode, when I select the venv I would like to use, the integrated terminal will then reflect that is running from that venv:

What is the nexus_callback?

Is it returning a layout? Is the current_user bit inside where it returns the layout?