├── LICENSE ├── README.md ├── app.py ├── assets ├── dash-logo-stripe.svg └── flatly-custom.css ├── config.py ├── config.txt ├── db └── table_create_statement ├── demo.gif ├── index.py ├── users_mgt.py └── views ├── error.py ├── login.py ├── page1.py ├── page2.py ├── profile.py └── user_admin.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Chris3691 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dash-User-Management 2 | 3 | A template web app built in Plotly Dash for Python, which handles user creation, user authentication/login, user permissions (admin/non admin), and page navigation. This web app is a great starting point for developing a custom cloud based dashboard where the user base is relatively small e.g. accessible by team members or small organisations. 4 | 5 | Example of app in action with a demo layout on page 1: 6 | 7 | ![](demo.gif) 8 | 9 | The template uses Dash bootstrap components with the Flatly bootswatch theme - https://bootswatch.com/flatly/ 10 | 11 | The template has the following views: 12 | 13 | 1. `login.py` - User login page, displayed if a user tries to access any page and is not currently logged in 14 | 2. `page1.py` - Empty template page for your own Dash layout 15 | 3. `page2.py` - Another empty template page 16 | 4. `profile.py` - For users to update their own password 17 | 5. `user_admin.py` - A page for admin users only, with ability to create new users and view existing users 18 | 6. `404.py` - Simple 404 error message to catch requests for non-existent pages 19 | 20 | 21 | 22 | 23 | 24 | # Database Setup 25 | 26 | Default database setup is MySQL and the required table can be setup using the table_create_statement in `/db`. 27 | 28 | The database `username`, `password`, and `database_name` need be updated in the `con` parameter of `config.txt` to the correct details for your database. 29 | 30 | Other databases can be used - refer to SQLAlchemy for required connection statement and update the `con` parameter of `config.txt`. 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | # Dash app initialization 2 | import dash 3 | # User management initialization 4 | import os 5 | from flask_login import LoginManager, UserMixin 6 | from users_mgt import db, User as base 7 | from config import config 8 | 9 | 10 | app = dash.Dash(__name__) 11 | server = app.server 12 | app.config.suppress_callback_exceptions = True 13 | 14 | 15 | # config 16 | server.config.update( 17 | SECRET_KEY=os.urandom(12), 18 | SQLALCHEMY_DATABASE_URI=config.get('database', 'con'), 19 | SQLALCHEMY_TRACK_MODIFICATIONS=False 20 | ) 21 | 22 | db.init_app(server) 23 | 24 | # Setup the LoginManager for the server 25 | login_manager = LoginManager() 26 | login_manager.init_app(server) 27 | login_manager.login_view = '/login' 28 | 29 | # Create User class with UserMixin 30 | class User(UserMixin, base): 31 | pass 32 | 33 | # callback to reload the user object 34 | @login_manager.user_loader 35 | def load_user(user_id): 36 | return User.query.get(int(user_id)) 37 | -------------------------------------------------------------------------------- /assets/dash-logo-stripe.svg: -------------------------------------------------------------------------------- 1 | dash-logo-stripe -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | from sqlalchemy import create_engine 3 | 4 | config = configparser.ConfigParser() 5 | config.read('config.txt') 6 | 7 | engine = create_engine(config.get('database', 'con')) -------------------------------------------------------------------------------- /config.txt: -------------------------------------------------------------------------------- 1 | 2 | [database] 3 | con = mysql+pymysql://user:password@localhost/database_name 4 | -------------------------------------------------------------------------------- /db/table_create_statement: -------------------------------------------------------------------------------- 1 | CREATE TABLE `user` ( 2 | `id` int(11) NOT NULL AUTO_INCREMENT, 3 | `username` varchar(20) NOT NULL, 4 | `email` varchar(50) NOT NULL, 5 | `password` varchar(80) NOT NULL, 6 | `admin` tinyint(4) NOT NULL DEFAULT '0', 7 | PRIMARY KEY (`id`), 8 | UNIQUE KEY `username_UNIQUE` (`username`), 9 | UNIQUE KEY `email_UNIQUE` (`email`) 10 | ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 11 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chris3691/Dash-User-Management/9fbb0e1dfc9afaa4b03d77a43a84bac40aa50490/demo.gif -------------------------------------------------------------------------------- /index.py: -------------------------------------------------------------------------------- 1 | # index page 2 | import dash_core_components as dcc 3 | import dash_html_components as html 4 | from dash.dependencies import Input, Output 5 | import dash_bootstrap_components as dbc 6 | 7 | from app import app, server 8 | from flask_login import logout_user, current_user 9 | from views import login, error, page1, page2, profile, user_admin 10 | 11 | 12 | navBar = dbc.Navbar(id='navBar', 13 | children=[], 14 | sticky='top', 15 | color='primary', 16 | className='navbar navbar-expand-lg navbar-dark bg-primary', 17 | ) 18 | 19 | 20 | app.layout = html.Div([ 21 | dcc.Location(id='url', refresh=False), 22 | html.Div([ 23 | navBar, 24 | html.Div(id='pageContent') 25 | ]) 26 | ], id='table-wrapper') 27 | 28 | 29 | ################################################################################ 30 | # HANDLE PAGE ROUTING - IF USER NOT LOGGED IN, ALWAYS RETURN TO LOGIN SCREEN 31 | ################################################################################ 32 | @app.callback(Output('pageContent', 'children'), 33 | [Input('url', 'pathname')]) 34 | def displayPage(pathname): 35 | if pathname == '/': 36 | if current_user.is_authenticated: 37 | return page1.layout 38 | else: 39 | return login.layout 40 | 41 | elif pathname == '/logout': 42 | if current_user.is_authenticated: 43 | logout_user() 44 | return login.layout 45 | else: 46 | return login.layout 47 | 48 | if pathname == '/page1': 49 | if current_user.is_authenticated: 50 | return page1.layout 51 | else: 52 | return login.layout 53 | 54 | if pathname == '/page2': 55 | if current_user.is_authenticated: 56 | return page2.layout 57 | else: 58 | return login.layout 59 | 60 | if pathname == '/profile': 61 | if current_user.is_authenticated: 62 | return profile.layout 63 | else: 64 | return login.layout 65 | 66 | if pathname == '/admin': 67 | if current_user.is_authenticated: 68 | if current_user.admin == 1: 69 | return user_admin.layout 70 | else: 71 | return error.layout 72 | else: 73 | return login.layout 74 | 75 | 76 | else: 77 | return error.layout 78 | 79 | 80 | ################################################################################ 81 | # ONLY SHOW NAVIGATION BAR WHEN A USER IS LOGGED IN 82 | ################################################################################ 83 | @app.callback( 84 | Output('navBar', 'children'), 85 | [Input('pageContent', 'children')]) 86 | def navBar(input1): 87 | if current_user.is_authenticated: 88 | if current_user.admin == 1: 89 | navBarContents = [ 90 | dbc.NavItem(dbc.NavLink('Page 1', href='/page1')), 91 | dbc.NavItem(dbc.NavLink('Page 2', href='/page2')), 92 | dbc.DropdownMenu( 93 | nav=True, 94 | in_navbar=True, 95 | label=current_user.username, 96 | children=[ 97 | dbc.DropdownMenuItem('Profile', href='/profile'), 98 | dbc.DropdownMenuItem('Admin', href='/admin'), 99 | dbc.DropdownMenuItem(divider=True), 100 | dbc.DropdownMenuItem('Logout', href='/logout'), 101 | ], 102 | ), 103 | ] 104 | return navBarContents 105 | 106 | else: 107 | navBarContents = [ 108 | dbc.NavItem(dbc.NavLink('Page 1', href='/page1')), 109 | dbc.NavItem(dbc.NavLink('Page 2', href='/page2')), 110 | dbc.DropdownMenu( 111 | nav=True, 112 | in_navbar=True, 113 | label=current_user.username, 114 | children=[ 115 | dbc.DropdownMenuItem('Profile', href='/profile'), 116 | dbc.DropdownMenuItem(divider=True), 117 | dbc.DropdownMenuItem('Logout', href='/logout'), 118 | ], 119 | ), 120 | ] 121 | return navBarContents 122 | 123 | else: 124 | return '' 125 | 126 | 127 | 128 | if __name__ == '__main__': 129 | app.run_server(debug=True) 130 | -------------------------------------------------------------------------------- /users_mgt.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Table 2 | from sqlalchemy.sql import select 3 | from flask_sqlalchemy import SQLAlchemy 4 | from werkzeug.security import generate_password_hash 5 | from config import engine 6 | 7 | db = SQLAlchemy() 8 | 9 | 10 | class User(db.Model): 11 | id = db.Column(db.Integer, primary_key=True) 12 | username = db.Column(db.String(15), unique=True) 13 | email = db.Column(db.String(50), unique=True) 14 | password = db.Column(db.String(80)) 15 | admin = db.Column(db.Boolean) 16 | 17 | 18 | userTable = Table('user', User.metadata) 19 | 20 | 21 | def create_user_table(): 22 | User.metadata.create_all(engine) 23 | 24 | 25 | def add_user(username, password, email, admin): 26 | hashed_password = generate_password_hash(password, method='sha256') 27 | 28 | insert_stmt = userTable.insert().values( 29 | username=username, email=email, password=hashed_password, admin=admin 30 | ) 31 | 32 | conn = engine.connect() 33 | conn.execute(insert_stmt) 34 | conn.close() 35 | 36 | 37 | def update_password(username, password): 38 | hashed_password = generate_password_hash(password, method='sha256') 39 | 40 | update = userTable.update().\ 41 | values(password=hashed_password).\ 42 | where(userTable.c.username==username) 43 | 44 | conn = engine.connect() 45 | conn.execute(update) 46 | conn.close() 47 | 48 | 49 | def show_users(): 50 | select_stmt = select([userTable.c.id, 51 | userTable.c.username, 52 | userTable.c.email, 53 | userTable.c.admin]) 54 | 55 | conn = engine.connect() 56 | results = conn.execute(select_stmt) 57 | 58 | users = [] 59 | 60 | for result in results: 61 | users.append({ 62 | 'id' : result[0], 63 | 'username' : result[1], 64 | 'email' : result[2], 65 | 'admin' : str(result[3]) 66 | }) 67 | 68 | conn.close() 69 | 70 | return users 71 | -------------------------------------------------------------------------------- /views/error.py: -------------------------------------------------------------------------------- 1 | 2 | import dash_html_components as html 3 | import dash_core_components as dcc 4 | import dash_bootstrap_components as dbc 5 | 6 | from app import app 7 | 8 | layout = dbc.Container([ 9 | html.Br(), 10 | dbc.Container([ 11 | dcc.Location(id='err404', refresh=True), 12 | dbc.Container( 13 | html.Img( 14 | src='/assets/dash-logo-stripe.svg', 15 | className='center' 16 | ), 17 | ), 18 | dbc.Container([ 19 | dbc.Container(id='outputState', children='Error 404 - Page not found') 20 | ], className='form-group'), 21 | ], className='jumbotron') 22 | ]) 23 | -------------------------------------------------------------------------------- /views/login.py: -------------------------------------------------------------------------------- 1 | import dash_core_components as dcc 2 | import dash_html_components as html 3 | import dash_bootstrap_components as dbc 4 | from dash.dependencies import Input, Output, State 5 | 6 | from app import app, User 7 | from flask_login import login_user 8 | from werkzeug.security import check_password_hash 9 | 10 | 11 | layout = dbc.Container([ 12 | html.Br(), 13 | dbc.Container([ 14 | dcc.Location(id='urlLogin', refresh=True), 15 | html.Div([ 16 | dbc.Container( 17 | html.Img( 18 | src='/assets/dash-logo-stripe.svg', 19 | className='center' 20 | ), 21 | ), 22 | dbc.Container(id='loginType', children=[ 23 | dcc.Input( 24 | placeholder='Enter your username', 25 | type='text', 26 | id='usernameBox', 27 | className='form-control', 28 | n_submit=0, 29 | ), 30 | html.Br(), 31 | dcc.Input( 32 | placeholder='Enter your password', 33 | type='password', 34 | id='passwordBox', 35 | className='form-control', 36 | n_submit=0, 37 | ), 38 | html.Br(), 39 | html.Button( 40 | children='Login', 41 | n_clicks=0, 42 | type='submit', 43 | id='loginButton', 44 | className='btn btn-primary btn-lg' 45 | ), 46 | html.Br(), 47 | ], className='form-group'), 48 | ]), 49 | ], className='jumbotron') 50 | ]) 51 | 52 | 53 | 54 | ################################################################################ 55 | # LOGIN BUTTON CLICKED / ENTER PRESSED - REDIRECT TO PAGE1 IF LOGIN DETAILS ARE CORRECT 56 | ################################################################################ 57 | @app.callback(Output('urlLogin', 'pathname'), 58 | [Input('loginButton', 'n_clicks'), 59 | Input('usernameBox', 'n_submit'), 60 | Input('passwordBox', 'n_submit')], 61 | [State('usernameBox', 'value'), 62 | State('passwordBox', 'value')]) 63 | def sucess(n_clicks, usernameSubmit, passwordSubmit, username, password): 64 | user = User.query.filter_by(username=username).first() 65 | if user: 66 | if check_password_hash(user.password, password): 67 | login_user(user) 68 | return '/page1' 69 | else: 70 | pass 71 | else: 72 | pass 73 | 74 | 75 | ################################################################################ 76 | # LOGIN BUTTON CLICKED / ENTER PRESSED - RETURN RED BOXES IF LOGIN DETAILS INCORRECT 77 | ################################################################################ 78 | @app.callback(Output('usernameBox', 'className'), 79 | [Input('loginButton', 'n_clicks'), 80 | Input('usernameBox', 'n_submit'), 81 | Input('passwordBox', 'n_submit')], 82 | [State('usernameBox', 'value'), 83 | State('passwordBox', 'value')]) 84 | def update_output(n_clicks, usernameSubmit, passwordSubmit, username, password): 85 | if (n_clicks > 0) or (usernameSubmit > 0) or (passwordSubmit) > 0: 86 | user = User.query.filter_by(username=username).first() 87 | if user: 88 | if check_password_hash(user.password, password): 89 | return 'form-control' 90 | else: 91 | return 'form-control is-invalid' 92 | else: 93 | return 'form-control is-invalid' 94 | else: 95 | return 'form-control' 96 | 97 | 98 | ################################################################################ 99 | # LOGIN BUTTON CLICKED / ENTER PRESSED - RETURN RED BOXES IF LOGIN DETAILS INCORRECT 100 | ################################################################################ 101 | @app.callback(Output('passwordBox', 'className'), 102 | [Input('loginButton', 'n_clicks'), 103 | Input('usernameBox', 'n_submit'), 104 | Input('passwordBox', 'n_submit')], 105 | [State('usernameBox', 'value'), 106 | State('passwordBox', 'value')]) 107 | def update_output(n_clicks, usernameSubmit, passwordSubmit, username, password): 108 | if (n_clicks > 0) or (usernameSubmit > 0) or (passwordSubmit) > 0: 109 | user = User.query.filter_by(username=username).first() 110 | if user: 111 | if check_password_hash(user.password, password): 112 | return 'form-control' 113 | else: 114 | return 'form-control is-invalid' 115 | else: 116 | return 'form-control is-invalid' 117 | else: 118 | return 'form-control' 119 | -------------------------------------------------------------------------------- /views/page1.py: -------------------------------------------------------------------------------- 1 | 2 | # Dash packages 3 | import dash_bootstrap_components as dbc 4 | import dash_html_components as html 5 | 6 | from app import app 7 | 8 | 9 | ############################################################################### 10 | ########### LANDING PAGE LAYOUT ########### 11 | ############################################################################### 12 | layout = dbc.Container([ 13 | 14 | html.H2('Page 1 Layout'), 15 | html.Hr(), 16 | 17 | 18 | ], className="mt-4") 19 | -------------------------------------------------------------------------------- /views/page2.py: -------------------------------------------------------------------------------- 1 | 2 | # Dash packages 3 | import dash_bootstrap_components as dbc 4 | import dash_html_components as html 5 | 6 | from app import app 7 | 8 | ############################################################################### 9 | ########### PAGE 2 LAYOUT ########### 10 | ############################################################################### 11 | layout = dbc.Container([ 12 | 13 | html.H2('Page 2 Layout'), 14 | html.Hr(), 15 | 16 | 17 | ], className="mt-4") 18 | -------------------------------------------------------------------------------- /views/profile.py: -------------------------------------------------------------------------------- 1 | import dash_core_components as dcc 2 | import dash_html_components as html 3 | import dash_bootstrap_components as dbc 4 | from dash.dependencies import Input, Output, State 5 | 6 | from app import app 7 | from flask_login import current_user 8 | from users_mgt import update_password 9 | from werkzeug.security import check_password_hash 10 | 11 | 12 | layout = dbc.Container([ 13 | html.Br(), 14 | dbc.Container([ 15 | dcc.Location(id='urlProfile', refresh=True), 16 | html.H3('Profile Management'), 17 | html.Hr(), 18 | dbc.Row([ 19 | 20 | dbc.Col([ 21 | dbc.Label('Username:'), 22 | html.Br(), 23 | html.Br(), 24 | dbc.Label('Email:'), 25 | ], md=2), 26 | 27 | dbc.Col([ 28 | dbc.Label(id='username', className='text-success'), 29 | html.Br(), 30 | html.Br(), 31 | dbc.Label(id='email', className='text-success'), 32 | ], md=4), 33 | 34 | dbc.Col([ 35 | dbc.Label('Old Password: '), 36 | dcc.Input( 37 | id='oldPassword', 38 | type='password', 39 | className='form-control', 40 | placeholder='old password', 41 | n_submit=0, 42 | style={ 43 | 'width' : '40%' 44 | }, 45 | ), 46 | html.Br(), 47 | dbc.Label('New Password: '), 48 | dcc.Input( 49 | id='newPassword1', 50 | type='password', 51 | className='form-control', 52 | placeholder='new password', 53 | n_submit=0, 54 | style={ 55 | 'width' : '40%' 56 | }, 57 | ), 58 | html.Br(), 59 | dbc.Label('Retype New Password: '), 60 | dcc.Input( 61 | id='newPassword2', 62 | type='password', 63 | className='form-control', 64 | placeholder='retype new password', 65 | n_submit=0, 66 | style={ 67 | 'width' : '40%' 68 | }, 69 | ), 70 | html.Br(), 71 | html.Button( 72 | children='Update Password', 73 | id='updatePasswordButton', 74 | n_clicks=0, 75 | type='submit', 76 | className='btn btn-primary btn-lg' 77 | ), 78 | html.Br(), 79 | html.Div(id='updateSuccess') 80 | ], md=6), 81 | ]), 82 | ], className='jumbotron') 83 | ]) 84 | 85 | 86 | @app.callback( 87 | Output('username', 'children'), 88 | [Input('pageContent', 'children')]) 89 | def currentUserName(pageContent): 90 | try: 91 | username = current_user.username 92 | return username 93 | except AttributeError: 94 | return '' 95 | 96 | 97 | @app.callback( 98 | Output('email', 'children'), 99 | [Input('pageContent', 'children')]) 100 | def currentUserEmail(pageContent): 101 | try: 102 | email = current_user.email 103 | return email 104 | except AttributeError: 105 | return '' 106 | 107 | ################################################################################ 108 | # UPDATE PWD BUTTON CLICKED / ENTER PRESSED - RETURN RED BOXES IF OLD PWD IS NOT CURR PWD 109 | ################################################################################ 110 | @app.callback(Output('oldPassword', 'className'), 111 | [Input('updatePasswordButton', 'n_clicks'), 112 | Input('newPassword1', 'n_submit'), 113 | Input('newPassword2', 'n_submit')], 114 | [State('pageContent', 'children'), 115 | State('oldPassword', 'value'), 116 | State('newPassword1', 'value'), 117 | State('newPassword2', 'value')]) 118 | def validateOldPassword(n_clicks, newPassword1Submit, newPassword2Submit, pageContent, 119 | oldPassword, newPassword1, newPassword2): 120 | if (n_clicks > 0) or (newPassword1Submit > 0) or (newPassword2Submit) > 0: 121 | if check_password_hash(current_user.password, oldPassword): 122 | return 'form-control is-valid' 123 | else: 124 | return 'form-control is-invalid' 125 | else: 126 | return 'form-control' 127 | 128 | 129 | 130 | # ############################################################################### 131 | # UPDATE PWD BUTTON CLICKED / ENTER PRESSED - RETURN RED BOXES IF NEW PASSWORDS ARE NOT THE SAME 132 | # ############################################################################### 133 | @app.callback(Output('newPassword1', 'className'), 134 | [Input('updatePasswordButton', 'n_clicks'), 135 | Input('newPassword1', 'n_submit'), 136 | Input('newPassword2', 'n_submit')], 137 | [State('newPassword1', 'value'), 138 | State('newPassword2', 'value')]) 139 | def validatePassword1(n_clicks, newPassword1Submit, newPassword2Submit, newPassword1, newPassword2): 140 | if (n_clicks > 0) or (newPassword1Submit > 0) or (newPassword2Submit) > 0: 141 | if newPassword1 == newPassword2: 142 | return 'form-control is-valid' 143 | else: 144 | return 'form-control is-invalid' 145 | else: 146 | return 'form-control' 147 | 148 | 149 | 150 | # ############################################################################### 151 | # UPDATE PWD BUTTON CLICKED / ENTER PRESSED - RETURN RED BOXES IF NEW PASSWORDS ARE NOT THE SAME 152 | # ############################################################################### 153 | @app.callback(Output('newPassword2', 'className'), 154 | [Input('updatePasswordButton', 'n_clicks'), 155 | Input('newPassword1', 'n_submit'), 156 | Input('newPassword2', 'n_submit')], 157 | [State('newPassword1', 'value'), 158 | State('newPassword2', 'value')]) 159 | def validatePassword2(n_clicks, newPassword1Submit, newPassword2Submit, newPassword1, newPassword2): 160 | if (n_clicks > 0) or (newPassword1Submit > 0) or (newPassword2Submit) > 0: 161 | if newPassword1 == newPassword2: 162 | return 'form-control is-valid' 163 | else: 164 | return 'form-control is-invalid' 165 | else: 166 | return 'form-control' 167 | 168 | 169 | 170 | ################################################################################ 171 | # UPDATE PWD BUTTON CLICKED / ENTER PRESSED - UPDATE DATABASE WITH NEW PASSWORD 172 | ################################################################################ 173 | @app.callback(Output('updateSuccess', 'children'), 174 | [Input('updatePasswordButton', 'n_clicks'), 175 | Input('newPassword1', 'n_submit'), 176 | Input('newPassword2', 'n_submit')], 177 | [State('pageContent', 'children'), 178 | State('oldPassword', 'value'), 179 | State('newPassword1', 'value'), 180 | State('newPassword2', 'value')]) 181 | def changePassword(n_clicks, newPassword1Submit, newPassword2Submit, pageContent, 182 | oldPassword, newPassword1, newPassword2): 183 | if (n_clicks > 0) or (newPassword1Submit > 0) or (newPassword2Submit) > 0: 184 | if check_password_hash(current_user.password, oldPassword) and newPassword1 == newPassword2: 185 | try: 186 | update_password(current_user.username, newPassword1) 187 | return html.Div(children=['Update Successful'], className='text-success') 188 | except Exception as e: 189 | return html.Div(children=['Update Not Successful: {e}'.format(e=e)], className='text-danger') 190 | else: 191 | return html.Div(children=['Old Password Invalid'], className='text-danger') 192 | -------------------------------------------------------------------------------- /views/user_admin.py: -------------------------------------------------------------------------------- 1 | import dash_core_components as dcc 2 | import dash_html_components as html 3 | import dash_bootstrap_components as dbc 4 | import dash_table as dt 5 | from dash.dependencies import Input, Output, State 6 | 7 | from app import app 8 | from users_mgt import show_users, add_user 9 | 10 | 11 | layout = dbc.Container([ 12 | html.Br(), 13 | dbc.Container([ 14 | dcc.Location(id='urlUserAdmin', refresh=True), 15 | html.H3('Add New User'), 16 | html.Hr(), 17 | dbc.Row([ 18 | dbc.Col([ 19 | dbc.Label('Username: '), 20 | dcc.Input( 21 | id='newUsername', 22 | className='form-control', 23 | n_submit=0, 24 | style={ 25 | 'width' : '90%' 26 | }, 27 | ), 28 | html.Br(), 29 | dbc.Label('Password: '), 30 | dcc.Input( 31 | id='newPwd1', 32 | type='password', 33 | className='form-control', 34 | n_submit=0, 35 | style={ 36 | 'width' : '90%' 37 | }, 38 | ), 39 | html.Br(), 40 | dbc.Label('Retype New Password: '), 41 | dcc.Input( 42 | id='newPwd2', 43 | type='password', 44 | className='form-control', 45 | n_submit=0, 46 | style={ 47 | 'width' : '90%' 48 | }, 49 | ), 50 | html.Br(), 51 | ], md=4), 52 | 53 | dbc.Col([ 54 | dbc.Label('Email: '), 55 | dcc.Input( 56 | id='newEmail', 57 | className='form-control', 58 | n_submit=0, 59 | style={ 60 | 'width' : '90%' 61 | }, 62 | ), 63 | html.Br(), 64 | dbc.Label('Admin? '), 65 | dcc.Dropdown( 66 | id='admin', 67 | style={ 68 | 'width' : '90%' 69 | }, 70 | options=[ 71 | {'label' : 'Yes', 'value' : 1}, 72 | {'label' : 'No', 'value' : 0}, 73 | ], 74 | value=0, 75 | clearable=False 76 | ), 77 | html.Br(), 78 | html.Br(), 79 | html.Button( 80 | children='Create User', 81 | id='createUserButton', 82 | n_clicks=0, 83 | type='submit', 84 | className='btn btn-primary btn-lg' 85 | ), 86 | html.Br(), 87 | html.Div(id='createUserSuccess') 88 | ], md=4), 89 | 90 | dbc.Col([ 91 | 92 | ], md=4) 93 | 94 | ]), 95 | ], className='jumbotron'), 96 | 97 | dbc.Container([ 98 | html.H3('View Users'), 99 | html.Hr(), 100 | dbc.Row([ 101 | dbc.Col([ 102 | dt.DataTable( 103 | id='users', 104 | columns = [{'name' : 'ID', 'id' : 'id'}, 105 | {'name' : 'Username', 'id' : 'username'}, 106 | {'name' : 'Email', 'id' : 'email'}, 107 | {'name' : 'Admin', 'id' : 'admin'}], 108 | data=show_users(), 109 | ), 110 | ], md=12), 111 | ]), 112 | ], className='jumbotron') 113 | ]) 114 | 115 | 116 | ################################################################################ 117 | # CREATE USER BUTTON CLICK / FORM SUBMIT - VALIDATE USERNAME 118 | ################################################################################ 119 | @app.callback(Output('newUsername', 'className'), 120 | 121 | [Input('createUserButton', 'n_clicks'), 122 | Input('newUsername', 'n_submit'), 123 | Input('newPwd1', 'n_submit'), 124 | Input('newPwd2', 'n_submit'), 125 | Input('newEmail', 'n_submit')], 126 | 127 | [State('newUsername', 'value')]) 128 | 129 | def validateUsername(n_clicks, usernameSubmit, newPassword1Submit, 130 | newPassword2Submit, newEmailSubmit, newUsername): 131 | 132 | if (n_clicks > 0) or (usernameSubmit > 0) or (newPassword1Submit > 0) or \ 133 | (newPassword2Submit > 0) or (newEmailSubmit > 0): 134 | 135 | if newUsername == None or newUsername == '': 136 | return 'form-control is-invalid' 137 | else: 138 | return 'form-control is-valid' 139 | else: 140 | return 'form-control' 141 | 142 | 143 | 144 | ################################################################################ 145 | # CREATE USER BUTTON CLICK / FORM SUBMIT - RED BOX IF PASSWORD DOES NOT MATCH 146 | ################################################################################ 147 | @app.callback(Output('newPwd1', 'className'), 148 | 149 | [Input('createUserButton', 'n_clicks'), 150 | Input('newUsername', 'n_submit'), 151 | Input('newPwd1', 'n_submit'), 152 | Input('newPwd2', 'n_submit'), 153 | Input('newEmail', 'n_submit')], 154 | 155 | [State('newPwd1', 'value'), 156 | State('newPwd2', 'value')]) 157 | 158 | def validatePassword1(n_clicks, usernameSubmit, newPassword1Submit, 159 | newPassword2Submit, newEmailSubmit, newPassword1, newPassword2): 160 | 161 | if (n_clicks > 0) or (usernameSubmit > 0) or (newPassword1Submit > 0) or \ 162 | (newPassword2Submit > 0) or (newEmailSubmit > 0): 163 | 164 | if newPassword1 == newPassword2 and len(newPassword1) > 7: 165 | return 'form-control is-valid' 166 | else: 167 | return 'form-control is-invalid' 168 | else: 169 | return 'form-control' 170 | 171 | 172 | ################################################################################ 173 | # CREATE USER BUTTON CLICK / FORM SUBMIT - RED BOX IF PASSWORD DOES NOT MATCH 174 | ################################################################################ 175 | @app.callback(Output('newPwd2', 'className'), 176 | 177 | [Input('createUserButton', 'n_clicks'), 178 | Input('newUsername', 'n_submit'), 179 | Input('newPwd1', 'n_submit'), 180 | Input('newPwd2', 'n_submit'), 181 | Input('newEmail', 'n_submit')], 182 | 183 | [State('newPwd1', 'value'), 184 | State('newPwd2', 'value')]) 185 | 186 | def validatePassword2(n_clicks, usernameSubmit, newPassword1Submit, 187 | newPassword2Submit, newEmailSubmit, newPassword1, newPassword2): 188 | 189 | if (n_clicks > 0) or (usernameSubmit > 0) or (newPassword1Submit > 0) or \ 190 | (newPassword2Submit > 0) or (newEmailSubmit > 0): 191 | 192 | if newPassword1 == newPassword2 and len(newPassword2) > 7: 193 | return 'form-control is-valid' 194 | else: 195 | return 'form-control is-invalid' 196 | else: 197 | return 'form-control' 198 | 199 | 200 | 201 | ################################################################################ 202 | # CREATE USER BUTTON CLICK / FORM SUBMIT - VALIDATE EMAIL 203 | ################################################################################ 204 | @app.callback(Output('newEmail', 'className'), 205 | 206 | [Input('createUserButton', 'n_clicks'), 207 | Input('newUsername', 'n_submit'), 208 | Input('newPwd1', 'n_submit'), 209 | Input('newPwd2', 'n_submit'), 210 | Input('newEmail', 'n_submit')], 211 | 212 | [State('newEmail', 'value')]) 213 | 214 | def validateEmail(n_clicks, usernameSubmit, newPassword1Submit, 215 | newPassword2Submit, newEmailSubmit, newEmail): 216 | 217 | if (n_clicks > 0) or (usernameSubmit > 0) or (newPassword1Submit > 0) or \ 218 | (newPassword2Submit > 0) or (newEmailSubmit > 0): 219 | 220 | if newEmail == None or newEmail == '': 221 | return 'form-control is-invalid' 222 | else: 223 | return 'form-control is-valid' 224 | else: 225 | return 'form-control' 226 | 227 | 228 | 229 | 230 | 231 | ################################################################################ 232 | # CREATE USER BUTTON CLICKED / ENTER PRESSED - UPDATE DATABASE WITH NEW USER 233 | ################################################################################ 234 | @app.callback(Output('createUserSuccess', 'children'), 235 | 236 | [Input('createUserButton', 'n_clicks'), 237 | Input('newUsername', 'n_submit'), 238 | Input('newPwd1', 'n_submit'), 239 | Input('newPwd2', 'n_submit'), 240 | Input('newEmail', 'n_submit')], 241 | 242 | [State('pageContent', 'children'), 243 | State('newUsername', 'value'), 244 | State('newPwd1', 'value'), 245 | State('newPwd2', 'value'), 246 | State('newEmail', 'value'), 247 | State('admin', 'value')]) 248 | 249 | def createUser(n_clicks, usernameSubmit, newPassword1Submit, newPassword2Submit, 250 | newEmailSubmit, pageContent, newUser, newPassword1, newPassword2, newEmail, admin): 251 | if (n_clicks > 0) or (usernameSubmit > 0) or (newPassword1Submit > 0) or \ 252 | (newPassword2Submit > 0) or (newEmailSubmit > 0): 253 | 254 | 255 | if newUser and newPassword1 and newPassword2 and newEmail != '': 256 | if newPassword1 == newPassword2: 257 | if len(newPassword1) > 7: 258 | try: 259 | add_user(newUser, newPassword1, newEmail, admin) 260 | return html.Div(children=['New User created'], className='text-success') 261 | except Exception as e: 262 | return html.Div(children=['New User not created: {e}'.format(e=e)], className='text-danger') 263 | else: 264 | return html.Div(children=['New Password Must Be Minimum 8 Characters'], className='text-danger') 265 | else: 266 | return html.Div(children=['Passwords do not match'], className='text-danger') 267 | else: 268 | return html.Div(children=['Invalid details submitted'], className='text-danger') 269 | --------------------------------------------------------------------------------