Use authentication for web app (#321)

* Require authentication for web app index page

* Got template working for login page

* Got fake login working

* Add logout endpoint

* Remove old comment

* Got login working

* Remove log lines
This commit is contained in:
Jonathan Zernik 2020-10-24 23:13:16 -04:00 committed by GitHub
parent 8422654513
commit 79d7a45e32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 170 additions and 9 deletions

View file

@ -32,6 +32,8 @@ rpc_port=8994
enabled=true
host=0.0.0.0
port=12994
username=devuser
password=devpass
use_ssl=false
[postgresql]

View file

@ -9,3 +9,5 @@ requests
SQLAlchemy
Flask
protobuf
flask-login
Flask-WTF

View file

@ -0,0 +1,12 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import ValidationError, DataRequired, EqualTo
from squeakserver.admin.squeak_admin_web_user import User
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Remember Me')
submit = SubmitField('Sign In')

View file

@ -3,6 +3,14 @@ import logging
from flask import Flask
from flask import request
from flask import redirect, url_for
from flask import render_template
from flask import flash
from flask_login import LoginManager
from flask_login import current_user, login_user
from flask_login import login_required
from flask_login import logout_user
from google.protobuf import json_format
from google.protobuf import message
@ -10,17 +18,38 @@ from google.protobuf import message
from proto import squeak_admin_pb2, squeak_admin_pb2_grpc
from proto import lnd_pb2, lnd_pb2_grpc
from squeakserver.admin.squeak_admin_web_user import User
from squeakserver.admin.forms import LoginForm
logger = logging.getLogger(__name__)
def create_app(handler):
def create_app(handler, username, password):
# create and configure the app
logger.info("Starting flask app from directory: {}".format(os.getcwd()))
app = Flask(__name__, static_folder='/app/static/build', static_url_path='/')
app = Flask(
__name__,
static_folder='/app/static/build',
static_url_path='/',
template_folder='/app/squeakserver/admin/templates',
)
app.config.from_mapping(
SECRET_KEY='dev',
)
login = LoginManager(app)
valid_user = User(
username,
password,
)
@login.user_loader
def load_user(id):
return valid_user.get_user_by_username(id)
@login.unauthorized_handler
def unauthorized_callback():
return redirect('/login')
def handle_request(request_message, handle_rpc_request):
data = request.get_data()
@ -28,11 +57,30 @@ def create_app(handler):
reply = handle_rpc_request(request_message)
return reply.SerializeToString(reply)
@app.route('/login', methods=['GET', 'POST'])
def login():
logger.info("Trying to login")
if current_user.is_authenticated:
return redirect(url_for('index'))
form = LoginForm()
if form.validate_on_submit():
user = valid_user.get_user_by_username(form.username.data)
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data)
return redirect(url_for('index'))
return render_template('login.html', title='Sign In', form=form)
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('index'))
@app.route('/')
@login_required
def index():
logger.info("Getting index route.")
logger.info("os.getcwd(): {}".format(os.getcwd()))
logger.info("os.listdir(os.getcwd()): {}".format(os.listdir(os.getcwd())))
return app.send_static_file('index.html')
@app.route('/hello')
@ -297,12 +345,11 @@ def create_app(handler):
class SqueakAdminWebServer():
def __init__(self, host, port, use_ssl, handler):
def __init__(self, host, port, username, password, use_ssl, handler):
self.host = host
self.port = port
self.use_ssl = use_ssl
self.handler = handler
self.app = create_app(handler)
self.app = create_app(handler, username, password)
def serve(self):
self.app.run(

View file

@ -0,0 +1,34 @@
import logging
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
logger = logging.getLogger(__name__)
class User(UserMixin):
def __init__(self, username, password):
self.username = username
self.password_hash = generate_password_hash(password)
def is_authenticated(self):
return True
def is_active(self):
return True
def is_anonymous(self):
return False
def get_id(self):
return self.username
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def get_user_by_username(self, username):
if self.username == username:
return self

View file

@ -0,0 +1,31 @@
<html>
<head>
{% if title %}
<title>{{ title }} - Microblog</title>
{% else %}
<title>Welcome to Microblog</title>
{% endif %}
</head>
<body>
<div>
Microblog:
<a href="{{ url_for('index') }}">Home</a>
{% if current_user.is_anonymous %}
<a href="{{ url_for('login') }}">Login</a>
{% else %}
<a href="{{ url_for('logout') }}">Logout</a>
{% endif %}
</div>
<hr>
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</body>
</html>

View file

@ -0,0 +1,7 @@
<!doctype html>
<title>Hello from Flask</title>
{% if name %}
<h1>Hello {{ name }}!</h1>
{% else %}
<h1>Hello, World!</h1>
{% endif %}

View file

@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block content %}
<h1>Sign In</h1>
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}<br>
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}

View file

@ -65,6 +65,8 @@ def load_admin_web_server(config, handler) -> SqueakAdminWebServer:
return SqueakAdminWebServer(
config["webadmin"]["host"],
config["webadmin"]["port"],
config["webadmin"]["username"],
config["webadmin"]["password"],
config["webadmin"].getboolean("use_ssl"),
handler,
)

View file

@ -18,7 +18,7 @@ class SqueakBlockVerifier:
block_info = self._get_block_info_for_height(squeak.nBlockHeight)
except Exception as e:
logger.error(
"Failed to sync because unable to get blockchain info.", exc_info=True
"Failed to sync because unable to get blockchain info.", exc_info=False
)
return

View file

@ -54,7 +54,7 @@ class SqueakSyncController:
block_height = block_info.block_height
except Exception as e:
logger.error(
"Failed to sync because unable to get blockchain info.", exc_info=True
"Failed to sync because unable to get blockchain info.", exc_info=False
)
return
self._download_from_peers(peers, block_height)