From d02b6eedaef31d6e9527749b5171b5812924623c Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Wed, 17 Feb 2021 22:18:18 -0800 Subject: [PATCH] Add authentication and hide private info --- poetry.lock | 39 +++++++++++++++++- pyproject.toml | 4 +- redstring/server.py | 70 ++++++++++++++++++++++++++++++--- redstring/templates/doc.jinja | 16 ++++++-- redstring/templates/login.jinja | 20 ++++++++++ 5 files changed, 137 insertions(+), 12 deletions(-) create mode 100644 redstring/templates/login.jinja diff --git a/poetry.lock b/poetry.lock index 30b1d35..bfeffbb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -36,6 +36,19 @@ version = "0.5.0" [package.dependencies] Flask = "*" +[[package]] +category = "main" +description = "Simple integration of Flask and WTForms." +name = "flask-wtf" +optional = false +python-versions = "*" +version = "0.14.3" + +[package.dependencies] +Flask = "*" +WTForms = "*" +itsdangerous = "*" + [[package]] category = "main" description = "Various helpers to pass data to untrusted environments and back." @@ -118,8 +131,24 @@ version = "1.0.1" dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] watchdog = ["watchdog"] +[[package]] +category = "main" +description = "A flexible forms validation and rendering library for Python web development." +name = "wtforms" +optional = false +python-versions = "*" +version = "2.3.3" + +[package.dependencies] +MarkupSafe = "*" + +[package.extras] +email = ["email-validator"] +ipaddress = ["ipaddress"] +locale = ["Babel (>=1.3)"] + [metadata] -content-hash = "27f45d27293b2411af59f2d60572508a045af3d996d09cd45001f73388f721fd" +content-hash = "4ecee2e19e0576a331bb8c4e9f0de82f74a31d41c5daafed8dce5eb73b541281" lock-version = "1.0" python-versions = "^3.8" @@ -136,6 +165,10 @@ flask-login = [ {file = "Flask-Login-0.5.0.tar.gz", hash = "sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b"}, {file = "Flask_Login-0.5.0-py2.py3-none-any.whl", hash = "sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0"}, ] +flask-wtf = [ + {file = "Flask-WTF-0.14.3.tar.gz", hash = "sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720"}, + {file = "Flask_WTF-0.14.3-py2.py3-none-any.whl", hash = "sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2"}, +] itsdangerous = [ {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"}, {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"}, @@ -248,3 +281,7 @@ werkzeug = [ {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, ] +wtforms = [ + {file = "WTForms-2.3.3-py2.py3-none-any.whl", hash = "sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c"}, + {file = "WTForms-2.3.3.tar.gz", hash = "sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c"}, +] diff --git a/pyproject.toml b/pyproject.toml index 1275917..47dafe8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,13 +8,15 @@ authors = ["Your Name "] python = "^3.8" flask = "^1.1.2" flask-login = "^0.5.0" +flask_wtf = "^0.14.3" [tool.poetry.dev-dependencies] mypy = "^0.800" [tool.poetry.scripts] redstring-check = "redstring.parser:main" -redstring-server = "redstring.server:main" +redstring-server = "redstring.server:cli" +redstring-backend = "redstring.server:wsgi" [build-system] requires = ["poetry>=0.12"] diff --git a/redstring/server.py b/redstring/server.py index c8e701e..c3152c3 100644 --- a/redstring/server.py +++ b/redstring/server.py @@ -2,7 +2,10 @@ Logic for serving a collection of documents through a web frontend. """ import argparse +import json import os +import random +import string from flask import ( abort, @@ -14,6 +17,10 @@ from flask import ( safe_join, url_for, ) +from flask_login import login_user, logout_user, login_required, LoginManager, UserMixin +from flask_wtf import FlaskForm +from wtforms import PasswordField, SubmitField +from wtforms.validators import DataRequired from redstring.library import generate_index_document, generate_default_document from redstring.parser import ( @@ -28,9 +35,28 @@ from redstring.parser import ( ) +class Admin(UserMixin): + def get_id(self): + return 'admin' + + +class LoginForm(FlaskForm): + password = PasswordField('Password', validators=[DataRequired()]) + submit = SubmitField('Submit') + + CONFIG_ENVVAR = 'REDSTRING_CONFIG' app = Flask(__name__) +app.secret_key = ''.join(random.choices(string.ascii_uppercase + string.digits, k=32)) + +login_manager = LoginManager() +login_manager.login_view = 'login' +login_manager.init_app(app) + +@login_manager.user_loader +def load_user(user_id): + return Admin() if user_id == 'admin' else None @app.route('/', methods=['GET']) @@ -54,7 +80,25 @@ def document(document_id): return render_template('doc.jinja', document=doc, index=False) +@app.route('/login/', methods=['GET', 'POST']) +def login(): + form = LoginForm() + if form.validate_on_submit(): + if form.password.data == app.config['login']: + login_user(Admin()) + return redirect(url_for('index')) + return render_template('login.jinja', form=form) + + +@app.route('/logout/') +@login_required +def logout(): + logout_user() + return redirect(url_for('index')) + + @app.route('/new/', methods=['GET']) +@login_required def new(): document_id = 'new' new_doc = generate_default_document(document_id) @@ -65,6 +109,7 @@ def new(): @app.route('/edit/', methods=['GET', 'POST']) +@login_required def edit(document_id): doc_path = safe_join(current_app.config['root'], f'{document_id}.json') @@ -144,15 +189,28 @@ def edit(document_id): return render_template('edit.jinja', document=doc, index=False) -def main(): +def read_config(path): + with open(path) as f: + config = json.load(f) + return config + + +def cli(): parser = argparse.ArgumentParser(description="Run the redstring server.") parser.add_argument("--config", help="Config file path.") + parser.add_argument("--debug", action="store_true") + parser.add_argument("--port", type=int, default=5000) args = parser.parse_args() - config_path = args.config or os.environ.get(CONFIG_ENVVAR) or '/etc/redstring.conf' - # TODO - document_folder = args.config # TODO + config = read_config(config_path) + app.config['root'] = config['root'] + app.config['login'] = config['login'] + app.run(debug=args.debug, port=args.port) - app.config['root'] = document_folder - app.run(debug=True, port=5000) +def wsgi(): + config_path = os.environ.get(CONFIG_ENVVAR) or '/etc/redstring.conf' + config = read_config(config_path) + app.config['root'] = config['root'] + app.config['login'] = config['login'] + return app diff --git a/redstring/templates/doc.jinja b/redstring/templates/doc.jinja index ccf787f..807bce3 100644 --- a/redstring/templates/doc.jinja +++ b/redstring/templates/doc.jinja @@ -1,6 +1,6 @@ {% extends 'base.jinja' %} -{% set page_title = document.get_tag_value('title', document.get_tag('id').value) -%} +{% set page_title = document.get_tag_value('title', document.get_tag_value('id', 'redstring')) -%} {% set page_summary = document.get_tag_value('summary', '') %} {% block page_scripts %} @@ -38,14 +38,14 @@ window.onload = function () {
{% for tag in tab.tags %} -{%- if not tag.options.private -%} +{%- if not tag.options.private or current_user.is_authenticated -%} {%- endif -%} {% for subtag in tag.subtags %} -{%- if not tag.options.private and not subtag.options.private -%} +{%- if (not tag.options.private and not subtag.options.private) or current_user.is_authenticated -%} @@ -69,15 +69,23 @@ window.onload = function () { {# TODO: tab.priority support #} {% block page_content %}
+{% if index and current_user.is_authenticated %} + +{% endif %} + {%- for tab in document -%} -{%- if not tab.options.private -%} +{%- if not tab.options.private or current_user.is_authenticated-%} {{ make_content_tab(tab, loop.first) }} {%- endif -%} {%- endfor -%} + {%- if not index -%} +{% if current_user.is_authenticated %} +{% endif %} {%- endif -%} +
{% for tab in document -%} {{ make_tab_page(tab, loop.first) }} diff --git a/redstring/templates/login.jinja b/redstring/templates/login.jinja new file mode 100644 index 0000000..972c1af --- /dev/null +++ b/redstring/templates/login.jinja @@ -0,0 +1,20 @@ +{% extends 'base.jinja' %} + +{% set page_title = 'Login' -%} + +{% block page_content %} +
+
login
+ +
+
+
+ {{ form.hidden_tag() }} +

{{ form.password.label }}
{{ form.password(size=32) }} + {% for error in form.password.errors %} +
{{ error }} + {% endfor %}

+

{{ form.submit() }}

+ +
+{% endblock page_content %} \ No newline at end of file
{{ tag.name }} {{ make_tag_value(tag) }}
{% if loop.last %}└{% else %}├{% endif %} {{ subtag.name }} {{ make_tag_value(subtag) }}