To implement something like Django's admin screen with Flask, it's convenient to use a library called Flask-Admin. However, if you just use Flask-Admin as it is, you can enter the administrator screen without typing a password (without logging in), which is very vulnerable in terms of security. In this article, we will use Flask-Admin and Flask-Login to ** implement a DB administrator screen with a login function **.
Since the amount of information is a little small, this is an article that I tried to increase the information of the explanation in Japanese. If you can speak English or are not good at verbose phrases, you can see the reference site from the link above.
You need to know about the Model-View-Controller model, as it only needs a little bit. Because in this article I'll use words like model and controller. I think the article below will be helpful. About MVC model
Ubuntu20.04LTS
MySQL 8.0.21
Python3.8.5
Flask==1.1.2
Flask-Admin==1.5.6
Flask-Login==0.5.0
Flask-SQLAlchemy==2.4.4
mysqlclient==2.0.1
Suddenly, I'll show you the whole final source code for those who don't have the time. Detailed explanation is below.
from flask import Flask, abort, jsonify, render_template, request, redirect, url_for
from wtforms import form, fields, validators
import flask_admin as admin
import flask_login as login
from flask_admin.contrib import sqla
from flask_admin import helpers, expose
from flask_admin.contrib.sqla import ModelView
from werkzeug.security import generate_password_hash, check_password_hash
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql://{user}:{password}@{host}/{db_name}".format(**{
'user': os.environ['RDS_USER'],
'password': os.environ['RDS_PASS'],
'host': os.environ['RDS_HOST'],
'db_name': os.environ['RDS_DB_NAME']
})
app.config['SECRET_KEY'] = os.environ['FLASK_SECRET_KEY']
db = SQLAlchemy(app)
class AdminUser(db.Model):
id = db.Column(db.Integer, primary_key=True)
login = db.Column(db.String(50), unique=True)
password = db.Column(db.String(250))
@property
def is_authenticated(self):
return True
@property
def is_active(self):
return True
@property
def is_anonymous(self):
return False
def get_id(self):
return self.id
def __unicode__(self):
return self.username
class LoginForm(form.Form):
login = fields.StringField(validators=[validators.required()])
password = fields.PasswordField(validators=[validators.required()])
def validate_login(self, field):
user = self.get_user()
if user is None:
raise validators.ValidationError('The user name or password is different.')
if not check_password_hash(user.password, self.password.data):
raise validators.ValidationError('The user name or password is different.')
def get_user(self):
return db.session.query(AdminUser).filter_by(login=self.login.data).first()
class RegistrationForm(form.Form):
login = fields.StringField(validators=[validators.required()])
password = fields.PasswordField(validators=[validators.required()])
def validate_login(self, field):
if db.session.query(AdminUser).filter_by(login=self.login.data).count() > 0:
raise validators.ValidationError('The same user name exists.')
def init_login():
login_manager = login.LoginManager()
login_manager.init_app(app)
@login_manager.user_loader
def load_user(user_id):
return db.session.query(AdminUser).get(user_id)
class MyModelView(sqla.ModelView):
def is_accessible(self):
return login.current_user.is_authenticated
class MyAdminIndexView(admin.AdminIndexView):
@expose('/')
def index(self):
if not login.current_user.is_authenticated:
return redirect(url_for('.login_view'))
return super(MyAdminIndexView, self).index()
@expose('/login/', methods=('GET', 'POST'))
def login_view(self):
form = LoginForm(request.form)
if helpers.validate_form_on_submit(form):
user = form.get_user()
login.login_user(user)
if login.current_user.is_authenticated:
return redirect(url_for('.index'))
link = '<p>For not creating an account<a href="' + url_for('.register_view') + '">click here</a></p>'
self._template_args['form'] = form
self._template_args['link'] = link
return super(MyAdminIndexView, self).index()
@expose('/register/', methods=('GET', 'POST'))
def register_view(self):
form = RegistrationForm(request.form)
if helpers.validate_form_on_submit(form):
user = AdminUser()
form.populate_obj(user)
user.password = generate_password_hash(form.password.data)
db.session.add(user)
db.session.commit()
login.login_user(user)
return redirect(url_for('.index'))
link = '<p>If you already have an account<a href="' + url_for('.login_view') + '">Click here to log in</a></p>'
self._template_args['form'] = form
self._template_args['link'] = link
return super(MyAdminIndexView, self).index()
@expose('/logout/')
def logout_view(self):
login.logout_user()
return redirect(url_for('.index'))
init_login()
admin = admin.Admin(app, 'Administrator screen', index_view=MyAdminIndexView(), base_template='my_master.html')
admin.add_view(MyModelView(AdminUser, db.session))
@app.route("/", methods=['GET'])
def index():
return "Hello, World!"
if __name__ == "__main__":
app.run()
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql://{user}:{password}@{host}/{db_name}".format(**{
'user': os.environ['RDS_USER'],
'password': os.environ['RDS_PASS'],
'host': os.environ['RDS_HOST'],
'db_name': os.environ['RDS_DB_NAME']
})
app.config['SECRET_KEY'] = os.environ['FLASK_SECRET_KEY']
db = SQLAlchemy(app)
Since there are many articles in Japanese on the Internet, the explanation here is omitted.
class AdminUser(db.Model):
id = db.Column(db.Integer, primary_key=True)
login = db.Column(db.String(50), unique=True)
password = db.Column(db.String(250))
@property
def is_authenticated(self):
return True
@property
def is_active(self):
return True
@property
def is_anonymous(self):
return False
def get_id(self):
return self.id
def __unicode__(self):
return self.username
login is your username. It defines the user name and password for logging in to the administrator screen. Each method has a property decorator so that when you write the login process later, you'll get information like whether you're logged in or not. If you want to know more about property decorators ↓ Properties
class LoginForm(form.Form):
login = fields.StringField(validators=[validators.required()])
password = fields.PasswordField(validators=[validators.required()])
def validate_login(self, field):
user = self.get_user()
if user is None:
raise validators.ValidationError('The user name or password is different.')
if not check_password_hash(user.password, self.password.data):
raise validators.ValidationError('The user name or password is different.')
def get_user(self):
return db.session.query(AdminUser).filter_by(login=self.login.data).first()
class RegistrationForm(form.Form):
login = fields.StringField(validators=[validators.required()])
password = fields.PasswordField(validators=[validators.required()])
def validate_login(self, field):
if db.session.query(AdminUser).filter_by(login=self.login.data).count() > 0:
raise validators.ValidationError('The same user name exists.')
It is a controller that describes the process when input is received from the form of the view (login screen or administrator account registration screen). What you should pay attention to here is the LoginForm class.
check_password_hash(user.password, self.password.data)
is. This is a convenient one that compares the real password that is hashed and saved and the hashed value entered from the login screen, and returns True if they match.
Although it is not recommended, if the password is saved in clear text in the DB, the conditional expression should be used.
if user.password != self.password.data:
I think you should change it to.
def init_login():
login_manager = login.LoginManager()
login_manager.init_app(app)
@login_manager.user_loader
def load_user(user_id):
return db.session.query(AdminUser).get(user_id)
class MyModelView(sqla.ModelView):
def is_accessible(self):
return login.current_user.is_authenticated
class MyAdminIndexView(admin.AdminIndexView):
@expose('/')
def index(self):
if not login.current_user.is_authenticated:
return redirect(url_for('.login_view'))
return super(MyAdminIndexView, self).index()
@expose('/login/', methods=('GET', 'POST'))
def login_view(self):
form = LoginForm(request.form)
if helpers.validate_form_on_submit(form):
user = form.get_user()
login.login_user(user)
if login.current_user.is_authenticated:
return redirect(url_for('.index'))
link = '<p>For not creating an account<a href="' + url_for('.register_view') + '">click here</a></p>'
self._template_args['form'] = form
self._template_args['link'] = link
return super(MyAdminIndexView, self).index()
@expose('/register/', methods=('GET', 'POST'))
def register_view(self):
form = RegistrationForm(request.form)
if helpers.validate_form_on_submit(form):
user = AdminUser()
form.populate_obj(user)
user.password = generate_password_hash(form.password.data)
db.session.add(user)
db.session.commit()
login.login_user(user)
return redirect(url_for('.index'))
link = '<p>If you already have an account<a href="' + url_for('.login_view') + '">Click here to log in</a></p>'
self._template_args['form'] = form
self._template_args['link'] = link
return super(MyAdminIndexView, self).index()
@expose('/logout/')
def logout_view(self):
login.logout_user()
return redirect(url_for('.index'))
init_login()
admin = admin.Admin(app, 'Administrator screen', index_view=MyAdminIndexView(), base_template='my_master.html')
admin.add_view(MyModelView(AdminUser, db.session))
It's a bit like doing it with Flask normally.
What you should pay attention to here is the MyModelView class. MyModelView inherits from sqla.ModelView and overrides the is_accessible method. (I need to) The is_accessible method returns whether the user is already logged in. By simply overriding the is_accessible method, you will be able to define access control rules in a later view class (here the MyAdminIndexView class).
init_login()
admin = admin.Admin(app, 'Administrator screen', index_view=MyAdminIndexView(), base_template='my_master.html')
admin.add_view(MyModelView(AdminUser, db.session))
Defines which view class is actually used in which model.
It doesn't make sense without HTML. Create a templates directory in the project root directory, and create files and directories with the following structure.
templates/
admin/
index.html
my_master.html
index.html
my_master.html
{% extends 'admin/base.html' %}
{% block access_control %}
{% if current_user.is_authenticated %}
<div class="btn-group pull-right">
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
<i class="icon-user"></i> {{ current_user.login }} <span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li><a href="{{ url_for('admin.logout_view') }}">Log out</a></li>
</ul>
</div>
{% endif %}
{% endblock %}
On the screen after login, if you press the user ID, a dropdown button will appear.
templates/index.html
<html>
<body>
<div>
<a href="{{ url_for('admin.index') }}">Go to admin!</a>
</div>
</body>
</html>
Any index page is fine. I will write it appropriately.
templates/admin/index.html
{% extends 'admin/master.html' %}
{% block body %}
{{ super() }}
<div class="row-fluid">
<div>
{% if current_user.is_authenticated %}
<h1>Civichat admin screen</h1>
<p class="lead">
Certified
</p>
<p>
You can manage the data from this screen. If you want to log out/admin/Please access logout.
</p>
{% else %}
<form method="POST" action="">
{{ form.hidden_tag() if form.hidden_tag }}
{% for f in form if f.type != 'CSRFTokenField' %}
<div>
{{ f.label }}
{{ f }}
{% if f.errors %}
<ul>
{% for e in f.errors %}
<li>{{ e }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endfor %}
<button class="btn" type="submit">Done</button>
</form>
{{ link | safe }}
{% endif %}
</div>
<a class="btn btn-primary" href="/"><i class="icon-arrow-left icon-white"></i>Return</a>
</div>
{% endblock body %}
It's like an index page on the admin screen after login.
In the first place, I think there is a need to restrict access by IP address before reaching the login form. I think the article below will be helpful.
If you have any mistakes, please let us know in the comments.
Recommended Posts