Создание учетных записей пользователей и защита доступа к информационным панелям

Защитите свои данные

Не так давно было заявлено, что данные - это новая нефть в цифровой экономике [1]. После этого заявления понимание кибербезопасности и способов защиты данных приобрело первостепенное значение для предприятий по всему миру. Как человек, который работает с конфиденциальными данными об образовании каждый день, я не понаслышке понимаю, насколько серьезно защищать записи.

Я занимаюсь аналитикой и использую Dash от Plotly. Dash - это фреймворк для Python, написанный поверх Flask, Plotly.js и React.js. Он абстрагирует сложность каждой из этих технологий до простых в применении компонентов. Любой, кто имеет небольшой опыт работы с Python и HTML, почувствует, что Dash дает им возможность создавать мощные и интерактивные веб-панели мониторинга, не беспокоясь об этом.

Dash & Plotly загружается 4 миллиона раз в месяц. Это то, как мир продвигает аналитику Python. - п lotly.com

В этой статье я собираюсь рассмотреть два метода защиты приложений Dash с помощью пользовательской аутентификации:

  1. Как пользоваться Dash-Auth (простой пример).
  2. Как пользоваться Flask-Login (расширенный пример).

Полный код для примера 2 доступен внизу статьи.

Обновление Dash Framework

Я кратко освежу основные концепции Dash, описывая пример приложения и начальный код. Однако, если вы новичок в Dash или нуждаетесь в углубленном освежении основ, начните с моего руководства Введение в Dash или посетите мой веб-сайт pythondashboards.com .



Перед созданием приложения начните с установки зависимостей.

pip install dash
pip install dash-auth

Поскольку основное внимание в этой статье уделяется обеспечению безопасности панели инструментов, я начну с простого примера приложения Dash, состоящего из списка и диаграммы. Оттуда я добавлю аутентификацию.

Если вам нужен пример расширенного приложения Dash, посмотрите на панель инструментов, которую я использую, чтобы доминировать на фондовом рынке!



Dash Apps

Приложения Dash состоят из макета и обратных вызовов:

Макет

Макет состоит из дерева компонентов, которые описывают, как выглядит приложение и как пользователи воспринимают контент. Dash поставляется с несколькими библиотеками компонентов из коробки.

Обратные вызовы

Обратные вызовы делают приложения Dash интерактивными. Обратные вызовы - это функции Python, которые автоматически вызываются при изменении свойства input.

Начальный код приложения Dash

Вот стартовый код приложения Dash для прохождения аутентификации. Расширенный пример будет использовать те же функции раскрывающегося списка и графиков, но значительно усложнит.

#dependencies 
import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly
#instantiate Dash
app = dash.Dash(__name__)
#Set the layout
app.layout = html.Div([
    html.H1('A Simple Dash App')
    , dcc.Dropdown(
        id='dropdown'
        , options=[{'label': i, 'value': i} 
                for i in ['Day 1', 'Day 2']]
        , value='Day 1')
    , dcc.Graph(id='graph')
    ]) #end div
#set the callback for the dropdown interactivity
@app.callback(
    dash.dependencies.Output('graph', 'figure')
    , [dash.dependencies.Input('dropdown', 'value')])
def update_graph(dropdown_value):
    if dropdown_value == 'Day 1':
        return {'layout': {'title': 'Graph of Day 1'}
                , 'data': [{'x': [1, 2, 3, 4]
                    , 'y': [4, 1, 2, 1]}]}
    else:
        return {'layout': {'title': 'Graph of Day 2'}
                ,'data': [{'x': [1, 2, 3, 4]
                    , 'y': [2, 3, 2, 4]}]}
if __name__ == '__main__':
    app.run_server(debug=True)

Обратите внимание, что макет приложения состоит из трех компонентов (html.H1, dcc.Dropdown, dcc.Graph) и обратный вызов, чтобы сделать его немного интерактивным. Пользователь может выбрать один из двух элементов в списке, и график обновится соответствующим образом.

Когда приложение запускается в терминале, к нему можно получить доступ локально через браузер по следующему URL-адресу: http://127.0.0.1:8050/. Если он где-то размещен, любой может получить к нему доступ и просмотреть график. Поскольку я хочу, чтобы к панели управления имели доступ только определенные люди, я использую Dash-Auth для настройки базовой HTTP-аутентификации.

Базовая аутентификация с использованием Dash-Auth

Использовать библиотеку Dash-Auth просто, но у нее есть некоторые ограничения. Например, используя базовую аутентификацию, я должен жестко закодировать имена пользователей и пароли в словаре и отправить пару пользователь / пароль пользователям, которым я хочу получить доступ к приложению. Пользователи не могут создать свою учетную запись или изменить пароль, и пользователи не могут выйти из системы. В расширенном примере с использованием Flask-Login я позволю пользователю создать собственное имя пользователя и пароль!

ПРЕДУПРЕЖДЕНИЕ: относитесь к парам имени пользователя и пароля как к ключам API и не допускайте их попадания в репозиторий исходного кода! Не забудьте добавить файл в свой .gitignore.

Файловая структура

Приложение использует два файла. Я храню пары имя пользователя / пароль, хранящиеся в файле Python в виде текста, отдельно от кода приложения. В расширенном примере я буду использовать базу данных SQLite для хранения пользовательских данных.

Файл users.py

Файл users.py содержит словарь имени пользователя и пароля. Если мне нужно позволить трем людям получить доступ к приложению Dash, мне нужно создать три пары имени пользователя и пароля, например:

#user dictionary
USERNAME_PASSWORD_PAIRS = {
     'user1': 'test1'
    , 'user2': 'test2'
    , 'user3': 'test3'
}

Файл app.py

Файл app.py содержит код приложения Dash. Для зависимостей импортируйте библиотеку dash_auth и пользовательский словарь из файла users.py.

#import dash-auth and users dictionary
import dash_auth
from users import USERNAME_PASSWORD_PAIRS

Затем добавьте этот небольшой фрагмент шаблонного кода после создания приложения в файле app.py:

auth = dash_auth.BasicAuth(app, USERNAME_PASSWORD_PAIRS)

Должно получиться так:

#import dependencies
import dash_auth
from users import USERNAME_PASSWORD_PAIRS
#instantiate Dash
app = dash.Dash(__name__)
auth = dash_auth.BasicAuth(
    app,
    USERNAME_PASSWORD_PAIRS
)
### layout and callbacks ###
...

Вот и все! Теперь базовая аутентификация настроена. При открытии приложения Dash пользователю будет предложено ввести имя пользователя и пароль.

Введите одну из комбинаций пользователя и пароля из файла users.py, чтобы получить доступ к панели управления.

За кулисами структура HTTP-аутентификации бросает вызов клиентскому запросу, вынуждая его предоставить информацию для аутентификации. Процесс начинается с того, что сервер отвечает клиенту со статусом «Неавторизованный ответ» (401). Клиент, который хочет пройти аутентификацию на сервере, должен включить заголовок запроса авторизации с учетными данными.

Примечание: для обеспечения безопасности обмен должен происходить через соединение HTTPS (TLS). Этот процесс определен в RFC 7235.

Расширенная аутентификация с использованием Flask-Login

Библиотека Dash-Auth предоставляет пользователям простой способ доступа к панели управления, но мало что дает о способах управления сеансами. Для более надежного решения для аутентификации я рекомендую Библиотеку Flask-Login. Он хранит идентификатор активных пользователей в сеансе, позволяя им входить и выходить, защищая сеансы пользователей от кражи похитителями файлов cookie. Согласно документации:

Он выполняет стандартные задачи входа в систему, выхода из системы и запоминания сеансов ваших пользователей в течение продолжительных периодов времени.

Несмотря на свою мощь, Flask-Login имеет несколько ограничений. У него нет встроенного способа регистрации пользователей или восстановления учетных записей. Я объясню, как создавать эти функции. Кроме того, библиотека обрабатывает только вход или выход, но никаких других разрешений.

Зависимости

В этом сложном примере используется несколько мощных библиотек Python. Подробное объяснение их всех выходит за рамки этого руководства, но я стараюсь объяснить каждый компонент на высоком уровне.

Используйте pip для установки этих необходимых библиотек: sqlalchemy, flask-sqlalchemy, flask-login, werkzeug и configparser.

#import dependencies
#manage database and users
import sqlite3
from sqlalchemy import Table, create_engine
from sqlalchemy.sql import select
from flask_sqlalchemy import SQLAlchemy
from flask_login import login_user, logout_user, current_user, LoginManager, UserMixin
#manage password hashing
from werkzeug.security import generate_password_hash, check_password_hash
#use to config server
import warnings
import configparser
import os
#dash dependencies
import dash_core_components as dcc
import dash_html_components as html
import dash
from dash.dependencies import Input, Output, State

Файловая структура

Файловая структура аналогична первому примеру тем, что содержит только два файла. Файл app.py содержит код для приложения Dash и будет состоять из нескольких различных макетов для создания учетной записи пользователя, входа в систему и отображения графических данных.

Файл data.sqlite

Я рекомендую создать базу данных с таблицей Users перед созданием приложения Dash.

Файл data.sqlite представляет собой базу данных и хранит имя пользователя, пароль и адрес электронной почты в таблице Пользователи. Для защиты паролей пользователей пароль будет хеширован с помощью библиотеки werkzeug. Werkzeug - это расширенная библиотека утилит интерфейса шлюза веб-сервера (WSGI).

Настройка базы данных и таблицы пользователей

Если база данных еще не существует, она будет создана автоматически при подключении.

conn = sqlite3.connect('data.sqlite')

Как видно на изображении рабочего процесса аутентификации выше, я хочу, чтобы пользователи создавали свои собственные учетные записи. Соединение с базой данных и оператор вставки, используемые для записи учетной записи пользователя в базу данных, будут управляться библиотекой SQLAlchemy. Кроме того, поскольку Dash построен на основе Flask, я буду использовать Flask-SQLAlchemy, чтобы настроить сервер для взаимодействия с базой данных.

SQLAlchemy - это мощный и гибкий набор инструментов SQL корпоративного уровня и Object Relational Mapper для Python. Flask-SQLAlchemy - это расширение для Flask, которое добавляет поддержку SQLAlchemy в приложение Flask. Поскольку Dash построен на Flask, все отлично работает вместе.

Создать таблицу пользователей

Для хранения учетных записей пользователей в базе данных мне понадобится таблица со столбцами для имени пользователя, пароля и электронной почты. SQLAlchemy может обрабатывать создание таблицы. Используйте следующий шаблон для создания таблицы Пользователи.

Обратите внимание: это можно сделать с помощью необработанного SQL вместо SQLAlchemy.

conn = sqlite3.connect('data.sqlite')
#connect to the database
engine = create_engine('sqlite:///data.sqlite')
db = SQLAlchemy()
#class for the table Users
class Users(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(15), unique=True, nullable = False)
    email = db.Column(db.String(50), unique=True)
    password = db.Column(db.String(80))
Users_tbl = Table('users', Users.metadata)
#fuction to create table using Users class
def create_users_table():
    Users.metadata.create_all(engine)
#create the table
create_users_table()

Обратите внимание, что таблица создается с помощью функции create_users_table.

Я считаю, что проще всего проверить данные таблицы с помощью библиотеки pandas, потому что фреймы данных красиво представляют данные в столбцах и строках.

import pandas as pd
c = conn.cursor()
df = pd.read_sql('select * from users', conn)
df

Теперь, когда база данных и таблица настроены, пора создать приложение Dash!

Настроить сервер приложений Dash

Настроить приложение немного сложнее, чем простое приложение Dash, поскольку я использую Flask и SQLAlchemy. Используйте этот фрагмент шаблонного кода, чтобы настроить сервер Flask для приложения Dash и заставить его взаимодействовать с базой данных через SQLAlchemy. Большая часть этого кода должна быть вам знакома, если вы использовали SQLAlchemy для построения таблицы Users.

warnings.filterwarnings("ignore")
#connect to SQLite database
conn = sqlite3.connect('data.sqlite')
engine = create_engine('sqlite:///data.sqlite')
db = SQLAlchemy()
config = configparser.ConfigParser()
#create users class for interacting with users table
class Users(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(15), unique=True, nullable = False)
    email = db.Column(db.String(50), unique=True)
    password = db.Column(db.String(80))
Users_tbl = Table('users', Users.metadata)
#instantiate dash app
app = dash.Dash(__name__)
server = app.server
app.config.suppress_callback_exceptions = True
#config the server to interact with the database
#Secret Key is used for user sessions
server.config.update(
    SECRET_KEY=os.urandom(12),
    SQLALCHEMY_DATABASE_URI='sqlite:///data.sqlite',
    SQLALCHEMY_TRACK_MODIFICATIONS=False
)
db.init_app(server)

Обратите внимание, что os.urandom используется для создания SECRET_KEY. Это будет объяснено в следующем разделе, поскольку это относится к функциям Flask-Login.

Для получения более подробной информации о настройке приложения я рекомендую просмотреть документацию по настройке Flask-SQLAlchemy.



Настройте Flask-Login в приложении Dash

Flask-Login - мощная библиотека, и в этом руководстве я лишь поверхностно затронул ее. Прочтите документацию, чтобы получить список всех расширенных функций, поставляемых с Flask-Login.

Используйте класс LoginManager, чтобы приложение Dash могло работать с Flask-Login. Flask-Login по умолчанию использует сеансы для аутентификации. Это означает, что конфигурация должна включать секретный ключ. Поэтому SECERET_KEY установлен в предыдущем разделе.

login_manager = LoginManager()

Диспетчер входа в систему позволяет приложению Dash выполнять такие действия, как загрузка пользователя по идентификатору и проверка пользователя в процессе входа в систему.

#This provides default implementations for the methods that Flask-#Login expects user objects to have
login_manager.init_app(app)
class Users(UserMixin, Users):
    pass

Обратный вызов login_manager необходим для завершения процессов входа в систему. Этот обратный вызов будет использоваться с остальными обратными вызовами Dash.

# callback to reload the user object
@login_manager.user_loader
def load_user(user_id):
    return Users.query.get(int(user_id))

Поздравляю! Flask-Login настроен. Пришло время создать остальную часть приложения Dash.

Создайте макеты приложений Dash

Каждый шаг рабочего процесса проверки подлинности соответствует макету в файле app.py. Между аутентификацией и содержимым приложения Dash используется всего шесть макетов:

Создать макет

Этот макет используется, чтобы позволить пользователю создать свою учетную запись. Он полагается на Input Dash Core Component, чтобы принимать вводимые пользователем данные. Когда пользователь нажимает кнопку Создать пользователя, срабатывает обратный вызов, и вводимые пользователем данные записываются в базу данных.

create = html.Div([ html.H1('Create User Account')
        , dcc.Location(id='create_user', refresh=True)
        , dcc.Input(id="username"
            , type="text"
            , placeholder="user name"
            , maxLength =15)
        , dcc.Input(id="password"
            , type="password"
            , placeholder="password")
        , dcc.Input(id="email"
            , type="email"
            , placeholder="email"
            , maxLength = 50)
        , html.Button('Create User', id='submit-val', n_clicks=0)
        , html.Div(id='container-button-basic')
    ])#end div

Создать обратный звонок

Используя обратный вызов, ввод записывается в базу данных. Входные данные сохраняются с использованием состояния зависимости тире. Состояние позволяет вводить значения без активации обратного вызова, пока не будет нажата кнопка Создать пользователя.

Обратите внимание на несколько вещей в обратном вызове:

  • Я использую SQLAlchemy для вставки значений вместо необработанного SQL.
ins = Users_tbl.insert().values(username=un,  password=hashed_password, email=em,)
@app.callback(
   [Output('container-button-basic', "children")]
    , [Input('submit-val', 'n_clicks')]
    , [State('username', 'value'), State('password', 'value'), State('email', 'value')])
def insert_users(n_clicks, un, pw, em):
    hashed_password = generate_password_hash(pw, method='sha256')
    if un is not None and pw is not None and em is not None:
        ins = Users_tbl.insert().values(username=un,  password=hashed_password, email=em,)
        conn = engine.connect()
        conn.execute(ins)
        conn.close()
        return [login]
    else:
        return [html.Div([html.H2('Already have a user account?'), dcc.Link('Click here to Log In', href='/login')])]

Кроме того, обратите внимание, что макет входа в систему возвращается при создании учетной записи пользователя, что упрощает вход пользователя в систему.

Схема входа в систему

Макет входа используется, чтобы позволить пользователю войти в систему после создания своей учетной записи. Он использует поля Ввод и кнопку, как и макет Создать.

login =  html.Div([dcc.Location(id='url_login', refresh=True)
            , html.H2('''Please log in to continue:''', id='h1')
            , dcc.Input(placeholder='Enter your username',
                    type='text',
                    id='uname-box')
            , dcc.Input(placeholder='Enter your password',
                    type='password',
                    id='pwd-box')
            , html.Button(children='Login',
                    n_clicks=0,
                    type='submit',
                    id='login-button')
            , html.Div(children='', id='output-state')
        ]) #end div

Обратные вызовы при входе

Есть два обратных вызова, связанных с входом в систему. Первый переводит пользователя на страницу успеха. Второй обратный вызов сообщает пользователю, что его имя пользователя или пароль неверны, если его информация неверна или не найдена в базе данных.

Обратные вызовы используют функцию check_password_hash из библиотеки Werkzeug.

@app.callback(
    Output('url_login', 'pathname')
    , [Input('login-button', 'n_clicks')]
    , [State('uname-box', 'value'), State('pwd-box', 'value')])
def successful(n_clicks, input1, input2):
    user = Users.query.filter_by(username=input1).first()
    if user:
        if check_password_hash(user.password, input2):
            login_user(user)
            return '/success'
        else:
            pass
    else:
        pass
@app.callback(
    Output('output-state', 'children')
    , [Input('login-button', 'n_clicks')]
    , [State('uname-box', 'value'), State('pwd-box', 'value')])
def update_output(n_clicks, input1, input2):
    if n_clicks > 0:
        user = Users.query.filter_by(username=input1).first()
        if user:
            if check_password_hash(user.password, input2):
                return ''
            else:
                return 'Incorrect username or password'
        else:
            return 'Incorrect username or password'
    else:
        return ''

Обратите внимание, что обратные вызовы используют SQLAlchemy для запроса таблицы Users по имени пользователя. Следующий код описывает, как SQLAlchemy извлекает данные пользователя, чтобы можно было проверить пароль.

Users.query.filter_by(username=input1).first()

Макет успеха

Макет Успех отображает доступные наборы данных, к которым пользователь может получить доступ после аутентификации. Единственный набор данных, доступный в приложении, - это тот, который я сделал для графика в первом примере.

success = html.Div([dcc.Location(id='url_login_success', refresh=True)
            , html.Div([html.H2('Login successful.')
                    , html.Br()
                    , html.P('Select a Dataset')
                    , dcc.Link('Data', href = '/data')
                ]) #end div
            , html.Div([html.Br()
                    , html.Button(id='back-button', children='Go back', n_clicks=0)
                ]) #end div
        ]) #end div

Неудачный макет

Макет "Неудачный" отображается, когда пользователю не удается войти в систему. Обратите внимание, что макет Вход встроен в макет Не удалось, что позволяет пользователю повторно ввести свои учетные данные.

failed = html.Div([ dcc.Location(id='url_login_df', refresh=True)
            , html.Div([html.H2('Log in Failed. Please try again.')
                    , html.Br()
                    , html.Div([login])
                    , html.Br()
                    , html.Button(id='back-button', children='Go back', n_clicks=0)
                ]) #end div
        ]) #end div

Макет выхода

Макет выхода отображается, когда пользователь вышел из системы. Как и макет «Неудачный», макет Вход встроен, поэтому пользователь может легко снова войти в систему.

logout = html.Div([dcc.Location(id='logout', refresh=True)
        , html.Br()
        , html.Div(html.H2('You have been logged out - Please login'))
        , html.Br()
        , html.Div([login])
        , html.Button(id='back-button', children='Go back', n_clicks=0)
    ])#end div

Схема данных

Этот макет отображает график, который был настроен в первом примере.

data = html.Div([dcc.Dropdown(
                    id='dropdown',
                    options=[{'label': i, 'value': i} for i in ['Day 1', 'Day 2']],
                    value='Day 1')
                , html.Br()
                , html.Div([dcc.Graph(id='graph')])
            ]) #end div

Обратный вызов данных

Обратный вызов данных выводит график в зависимости от того, какая опция выбрана в раскрывающемся меню.

#set the callback for the dropdown interactivity
@app.callback(
    [Output('graph', 'figure')]
    , [Input('dropdown', 'value')])
def update_graph(dropdown_value):
    if dropdown_value == 'Day 1':
        return [{'layout': {'title': 'Graph of Day 1'}
                , 'data': [{'x': [1, 2, 3, 4]
                    , 'y': [4, 1, 2, 1]}]}]
    else:
        return [{'layout': {'title': 'Graph of Day 2'}
                ,'data': [{'x': [1, 2, 3, 4]
                    , 'y': [2, 3, 2, 4]}]}]

Обратные вызовы кнопки "Назад"

На каждой из страниц есть кнопка возврата, которая возвращает пользователя к экрану входа в систему. Эти обратные вызовы обновляют соответствующий макет и возвращают пользователя на экран входа в систему.

@app.callback(
    Output('url_login_success', 'pathname')
    , [Input('back-button', 'n_clicks')])
def logout_dashboard(n_clicks):
    if n_clicks > 0:
        return '/'
@app.callback(
    Output('url_login_df', 'pathname')
    , [Input('back-button', 'n_clicks')])
def logout_dashboard(n_clicks):
    if n_clicks > 0:
        return '/'
# Create callbacks
@app.callback(
    Output('url_logout', 'pathname')
    , [Input('back-button', 'n_clicks')])
def logout_dashboard(n_clicks):
    if n_clicks > 0:
        return '/'

Отображение макетов

Теперь, когда макеты созданы, я спроектирую app.layout для использования html.Div, который может отображать соответствующий макет для соответствующего шага в качестве дочернего элемента html.Div. Обратный вызов используется для управления тем, какой макет будет возвращен как дочерний для компонента html.Div с идентификатором page-content.

app.layout= html.Div([
            html.Div(id='page-content', className='content')
            ,  dcc.Location(id='url', refresh=False)
        ])

#callback to determine layout to return
@app.callback(
    Output('page-content', 'children')
    , [Input('url', 'pathname')])
def display_page(pathname):
    if pathname == '/':
        return create
    elif pathname == '/login':
        return login
    elif pathname == '/success':
        if current_user.is_authenticated:
            return success
        else:
            return failed
    elif pathname =='/data':
        if current_user.is_authenticated:
            return data
    elif pathname == '/logout':
        if current_user.is_authenticated:
            logout_user()
            return logout
        else:
            return logout
    else:
        return '404'

Код готов! При сборке начните с кода конфигурации, затем добавьте макеты, а затем обратные вызовы. Ниже приведен полный код расширенного примера.

Заключительные мысли и приложение Complete Dash

Защита пользовательских данных важна как никогда, и теперь вы можете использовать Dash-Auth и Flask-Login для настройки безопасности и аутентификации пользователей для ваших приложений Dash. У обоих инструментов есть свои плюсы и минусы. Dash-Auth идеально подходит для простой базовой аутентификации HTTP, в то время как Flask-Login предоставляет расширенные функции для управления аутентификацией и пользовательскими сеансами.

Полный код

Когда все отдельные части закодированы, пора показать полное приложение Dash! Решил заказать конфигурацию, макеты и обратные вызовы в 1 файл. Однако вполне нормально разбить макеты на отдельные файлы, чтобы упорядочить их. Если вам понравился этот урок и вы хотите узнать больше о Dash, ознакомьтесь с моими другими уроками по Dash!

3 продвинутых примера для начинающих
Обновления в реальном времени и потоковая передача данных в информационную панель
Адаптивные мобильные информационные панели с компонентами Bootstrap CSS
Создание таблицы данных с использованием данных из Reddit
Экспорт данных из панели управления

import dash_core_components as dcc
import dash_html_components as html
import dash
from dash.dependencies import Input, Output, State
from sqlalchemy import Table, create_engine
from sqlalchemy.sql import select
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
import sqlite3
import warnings
import os
from flask_login import login_user, logout_user, current_user, LoginManager, UserMixin
import configparser
warnings.filterwarnings("ignore")
conn = sqlite3.connect('data.sqlite')
engine = create_engine('sqlite:///data.sqlite')
db = SQLAlchemy()
config = configparser.ConfigParser()
class Users(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(15), unique=True, nullable = False)
    email = db.Column(db.String(50), unique=True)
    password = db.Column(db.String(80))
Users_tbl = Table('users', Users.metadata)
app = dash.Dash(__name__)
server = app.server
app.config.suppress_callback_exceptions = True
# config
server.config.update(
    SECRET_KEY=os.urandom(12),
    SQLALCHEMY_DATABASE_URI='sqlite:///data.sqlite',
    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'
#User as base
# Create User class with UserMixin
class Users(UserMixin, Users):
    pass
create = html.Div([ html.H1('Create User Account')
        , dcc.Location(id='create_user', refresh=True)
        , dcc.Input(id="username"
            , type="text"
            , placeholder="user name"
            , maxLength =15)
        , dcc.Input(id="password"
            , type="password"
            , placeholder="password")
        , dcc.Input(id="email"
            , type="email"
            , placeholder="email"
            , maxLength = 50)
        , html.Button('Create User', id='submit-val', n_clicks=0)
        , html.Div(id='container-button-basic')
    ])#end div
login =  html.Div([dcc.Location(id='url_login', refresh=True)
            , html.H2('''Please log in to continue:''', id='h1')
            , dcc.Input(placeholder='Enter your username',
                    type='text',
                    id='uname-box')
            , dcc.Input(placeholder='Enter your password',
                    type='password',
                    id='pwd-box')
            , html.Button(children='Login',
                    n_clicks=0,
                    type='submit',
                    id='login-button')
            , html.Div(children='', id='output-state')
        ]) #end div
success = html.Div([dcc.Location(id='url_login_success', refresh=True)
            , html.Div([html.H2('Login successful.')
                    , html.Br()
                    , html.P('Select a Dataset')
                    , dcc.Link('Data', href = '/data')
                ]) #end div
            , html.Div([html.Br()
                    , html.Button(id='back-button', children='Go back', n_clicks=0)
                ]) #end div
        ]) #end div
data = html.Div([dcc.Dropdown(
                    id='dropdown',
                    options=[{'label': i, 'value': i} for i in ['Day 1', 'Day 2']],
                    value='Day 1')
                , html.Br()
                , html.Div([dcc.Graph(id='graph')])
            ]) #end div
failed = html.Div([ dcc.Location(id='url_login_df', refresh=True)
            , html.Div([html.H2('Log in Failed. Please try again.')
                    , html.Br()
                    , html.Div([login])
                    , html.Br()
                    , html.Button(id='back-button', children='Go back', n_clicks=0)
                ]) #end div
        ]) #end div
logout = html.Div([dcc.Location(id='logout', refresh=True)
        , html.Br()
        , html.Div(html.H2('You have been logged out - Please login'))
        , html.Br()
        , html.Div([login])
        , html.Button(id='back-button', children='Go back', n_clicks=0)
    ])#end div
app.layout= html.Div([
            html.Div(id='page-content', className='content')
            ,  dcc.Location(id='url', refresh=False)
        ])
# callback to reload the user object
@login_manager.user_loader
def load_user(user_id):
    return Users.query.get(int(user_id))
@app.callback(
    Output('page-content', 'children')
    , [Input('url', 'pathname')])
def display_page(pathname):
    if pathname == '/':
        return create
    elif pathname == '/login':
        return login
    elif pathname == '/success':
        if current_user.is_authenticated:
            return success
        else:
            return failed
    elif pathname =='/data':
        if current_user.is_authenticated:
            return data
    elif pathname == '/logout':
        if current_user.is_authenticated:
            logout_user()
            return logout
        else:
            return logout
    else:
        return '404'
#set the callback for the dropdown interactivity
@app.callback(
    [Output('graph', 'figure')]
    , [Input('dropdown', 'value')])
def update_graph(dropdown_value):
    if dropdown_value == 'Day 1':
        return [{'layout': {'title': 'Graph of Day 1'}
                , 'data': [{'x': [1, 2, 3, 4]
                    , 'y': [4, 1, 2, 1]}]}]
    else:
        return [{'layout': {'title': 'Graph of Day 2'}
                ,'data': [{'x': [1, 2, 3, 4]
                    , 'y': [2, 3, 2, 4]}]}]
@app.callback(
   [Output('container-button-basic', "children")]
    , [Input('submit-val', 'n_clicks')]
    , [State('username', 'value'), State('password', 'value'), State('email', 'value')])
def insert_users(n_clicks, un, pw, em):
    hashed_password = generate_password_hash(pw, method='sha256')
    if un is not None and pw is not None and em is not None:
        ins = Users_tbl.insert().values(username=un,  password=hashed_password, email=em,)
        conn = engine.connect()
        conn.execute(ins)
        conn.close()
        return [login]
    else:
        return [html.Div([html.H2('Already have a user account?'), dcc.Link('Click here to Log In', href='/login')])]
@app.callback(
    Output('url_login', 'pathname')
    , [Input('login-button', 'n_clicks')]
    , [State('uname-box', 'value'), State('pwd-box', 'value')])
def successful(n_clicks, input1, input2):
    user = Users.query.filter_by(username=input1).first()
    if user:
        if check_password_hash(user.password, input2):
            login_user(user)
            return '/success'
        else:
            pass
    else:
        pass
@app.callback(
    Output('output-state', 'children')
    , [Input('login-button', 'n_clicks')]
    , [State('uname-box', 'value'), State('pwd-box', 'value')])
def update_output(n_clicks, input1, input2):
    if n_clicks > 0:
        user = Users.query.filter_by(username=input1).first()
        if user:
            if check_password_hash(user.password, input2):
                return ''
            else:
                return 'Incorrect username or password'
        else:
            return 'Incorrect username or password'
    else:
        return ''
@app.callback(
    Output('url_login_success', 'pathname')
    , [Input('back-button', 'n_clicks')])
def logout_dashboard(n_clicks):
    if n_clicks > 0:
        return '/'
@app.callback(
    Output('url_login_df', 'pathname')
    , [Input('back-button', 'n_clicks')])
def logout_dashboard(n_clicks):
    if n_clicks > 0:
        return '/'
# Create callbacks
@app.callback(
    Output('url_logout', 'pathname')
    , [Input('back-button', 'n_clicks')])
def logout_dashboard(n_clicks):
    if n_clicks > 0:
        return '/'
if __name__ == '__main__':
    app.run_server(debug=True)

Благодарю вас!

- Эрик Клеппен