2
votes

UPDATE

It appears that the Flask redirect (response code 302) below is being passed as the response to the _dash-update-component request:

b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n<title>Redirecting...</title>\n<h1>Redirecting...</h1>\n<p>You should be redirected automatically to target URL: <a href="/login">/login</a>.  If not click the link.'

This explains the SyntaxError thrown by dash_renderer below, so this led me to add the following in server.py:

@server.after_request
def check_response(response):

    redirecting = 'Redirecting...' in response.get_data().decode('utf-8')
    dash_response = request.path=='/_dash-update-component'

    return make_response('', 204) if redirecting and dash_response else response

Now I can emulate a Dash-like PreventUpdate by returning a "204 No-Content" response to the dash component, but then I am not receiving the additional request for the redirect back to the login page. Commenting out the after_request function and then tracking the requests seen by before_request, it's actually shown that the login() route is invoked and render_template('login.html') is returned, but it's simply not rendered in the browser....

ORIGINAL POST BELOW

I've spent the better part of the last few days attempting to overhaul our login procedures to add some quality of life update and modifications. For the purposes of this question, I'm interested in logging out our users after a certain period of inactivity in the main Dash application.

My approach was to register routes for our Login page, and then point a Flask route for /dashapp to the response returned by app.index() where app points to the Dash application. Once they are logged into the Dash application, I have a before_request decorator that will update the session modified attribute and the session expiration (5 seconds for the purposes of testing). I've also applied the @login_required decorator to this invoked function, so that login_manager.unauthorized_handler is invoked if the user is no longer authenticated when triggering the before_request decorator. I think my logic is sound here, but I am still having issues which I will describe below.

I am able to login my users and redirect them to the main Dash application at /dashapp, and I can use the application without issues. Now when I wait the 5 seconds to allow for the session to expire, clicking on a component in my Dash application that triggers a dash callback produces the following error in the console:

dash_renderer.v1_7_0m1602118443.min.js:20 SyntaxError: Unexpected token < in JSON at position 0

I'm aware that some function is expecting a JSON response, and has apparently received an HTML response instead, but I can't pin down what that is. It's also preventing my redirection back to the login page that I expected to be invoked when the user was no longer authenticated and triggered the before_request decorator.

My code structure is below (not that config.py is simply my SQL connection):

application.py

from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html

from server import app, server as application, User, login_manager
from flask_login import logout_user, current_user, login_user, login_required
from flask import session, redirect, render_template, url_for, request
from views import main

app.layout = html.Div([

    dcc.Location(id='url', refresh=False),

    html.Div(id='page-content')

])

@application.route('/login')
def login():

    return render_template('login.html')

@application.route('/login', methods=['POST'])
def login_post():
    
    if current_user.is_authenticated:

        return redirect('/dashapp')

    user = User.query.filter_by(username=request.form['username']).first()

    #Check if user exists
    if user:

        #Check if password is correct
        if user.password==request.form['password']:

            login_user(user, remember=False)
            return redirect('/dashapp')

@login_manager.unauthorized_handler
def unauthorized():
    
    if request.path!='/login':
        return redirect('/login')

@application.route('/logout')
@login_required
def logout():

    logout_user()
    return redirect('/login')

@application.route('/dashapp')
@login_required
def main_page():

    return app.index()

@app.callback(
    Output('page-content', 'children'),
    [Input('url', 'pathname')])
def display_page(pathname):

    if current_user.is_authenticated:
        content = main.get_layout()
    else:
        content = dcc.Location(pathname='/login', id='redirect-id')

    return content

if __name__ == '__main__':
    app.run_server()

views/login.html

<html>
  <head>
    <title>Flask Intro - login page</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="static/bootstrap.min.css" rel="stylesheet" media="screen">
  </head>
  <body>
    <div class="container">
      <h1>Please login</h1>
      <br>
      <form action="" method="post">
        <input type="text" placeholder="Username" name="username" value="{{
          request.form.username }}">
         <input type="password" placeholder="Password" name="password" value="{{
          request.form.password }}">
        <input class="btn btn-default" type="submit" value="Login">
      </form>
      {% if error %}
        <p class="error"><strong>Error:</strong> {{ error }}
      {% endif %}
    </div>
  </body>
</html>

server.py

import dash, os, datetime
from flask_login import LoginManager, UserMixin, current_user, login_required
from config import connection_string
import dash_bootstrap_components as dbc
from credentials import db, User as base
from flask import session, g, redirect, url_for, request, flash, render_template
import flask

external_stylesheets = [dbc.themes.BOOTSTRAP]

app_flask = flask.Flask(__name__)

app = dash.Dash(
    __name__,
    server=app_flask,
    external_stylesheets=external_stylesheets,
    update_title=None,
    url_base_pathname='/'
)

app.title = 'Login Testing Interface'
server = app_flask
app.config.suppress_callback_exceptions = True

server.config.update(
    SECRET_KEY=os.urandom(12),
    SQLALCHEMY_DATABASE_URI=connection_string,
    SQLALCHEMY_TRACK_MODIFICATIONS=False
)

db.init_app(server)

#Setup the LoginManager for the server
login_manager = LoginManager()
login_manager.init_app(server)
login_manager.login_view = 'login'

#Create User class with UserMixin
class User(UserMixin, base):

    def get_id(self):

        return self.user_id

#Reload the user object
@login_manager.user_loader
def load_user(user_id):

    return User.query.get(user_id)

@server.before_request
@login_required
def check_authentication():

    session.permanent = True
    server.permanent_session_lifetime = datetime.timedelta(seconds=5)
    session.modified = True
    g.user = current_user

main.py

from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html
import dash_bootstrap_components as dbc

from flask_login import current_user

from server import app, server

def get_layout():

    return html.Div([

        dcc.Location(id='url-main', refresh=False),

        dbc.Button('Click me', id='test-click', n_clicks_timestamp=0),

        html.Div(id='testing')

    ])

@app.callback(
    Output('testing', 'children'),
    [Input('test-click', 'n_clicks_timestamp')])
def update_test_div(clicks):

    return f'Last clicked: {clicks}'

credentials.py

from flask_sqlalchemy import SQLAlchemy
from config import engine

db = SQLAlchemy()

db.Model.metadata.reflect(engine)

class User(db.Model):

    __table__ = db.Model.metadata.tables['my_sql_table_with_user_details']

Thank you in advance for any guidance here!

1

1 Answers

1
votes

I suggest writing your login and login post route as a single fuunction

@app.route('/login', methods=['POST','GET'])
def login():
    if current_user.is_authenticated :
        return redirect('/')
    if request.method == 'POST':
        user_name = request.form.get('username')
        password_entered =request.form.get('password')
        
        present_user=User.query.filter_by(username=user_name).first()
        if present_user.password == password_entered:
            login_user(present_user)
            next_page= request.args.get('next')
            print(next_page)
            return redirect(next_page)  if next_page else redirect('/')
        else:
            flash('Incorrect  Password',category='danger')
            return render_template('user_login.html')
    else:
        return render_template('user_login.html')

If you redirected from the login_required function to the Login page, you might notice that the link /url on top says

/login?next=%2FpathofFunction

When we write

next_page= request.args.get('next')

We get the remaining URL after ?next and then redirect the user to where it came from