Compare commits

..

1 Commits

Author SHA1 Message Date
Tim Van Baak 94e91d6b3e Add more logging 2021-02-18 22:34:38 -08:00
6 changed files with 158 additions and 29 deletions

54
poetry.lock generated
View File

@ -25,6 +25,30 @@ dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxco
docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
dotenv = ["python-dotenv"] dotenv = ["python-dotenv"]
[[package]]
category = "main"
description = "User session management for Flask"
name = "flask-login"
optional = false
python-versions = "*"
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]] [[package]]
category = "main" category = "main"
description = "WSGI HTTP Server for UNIX" description = "WSGI HTTP Server for UNIX"
@ -124,11 +148,27 @@ version = "1.0.1"
dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"]
watchdog = ["watchdog"] 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)"]
[extras] [extras]
dev = ["gunicorn"] dev = ["gunicorn"]
[metadata] [metadata]
content-hash = "4a6df8f74dbda5e091bc6a4f2240fa4276812f9830369848df1a5813e7a81b83" content-hash = "e4930aaf62df0a261424129ab0d89af96ea4e19f80b76c7c6fbcb25c491a213d"
lock-version = "1.0" lock-version = "1.0"
python-versions = "^3.8" python-versions = "^3.8"
@ -141,6 +181,14 @@ flask = [
{file = "Flask-1.1.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"}, {file = "Flask-1.1.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"},
{file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"}, {file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"},
] ]
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"},
]
gunicorn = [ gunicorn = [
{file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"}, {file = "gunicorn-20.0.4-py2.py3-none-any.whl", hash = "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"},
{file = "gunicorn-20.0.4.tar.gz", hash = "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626"}, {file = "gunicorn-20.0.4.tar.gz", hash = "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626"},
@ -257,3 +305,7 @@ werkzeug = [
{file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"},
{file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, {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"},
]

View File

@ -7,6 +7,8 @@ authors = ["Your Name <you@example.com>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8" python = "^3.8"
flask = "^1.1.2" flask = "^1.1.2"
flask-login = "^0.5.0"
flask_wtf = "^0.14.3"
gunicorn = {version = "^20.0.4", optional = true} gunicorn = {version = "^20.0.4", optional = true}
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]

View File

@ -18,6 +18,10 @@ from flask import (
safe_join, safe_join,
url_for, 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.library import generate_index_document, generate_default_document
from redstring.parser import ( from redstring.parser import (
@ -32,16 +36,49 @@ from redstring.parser import (
) )
class Admin(UserMixin):
def get_id(self):
return 'admin'
class LoginForm(FlaskForm):
password = PasswordField('Password')
submit = SubmitField('Submit')
CONFIG_ENVVAR = 'REDSTRING_CONFIG' CONFIG_ENVVAR = 'REDSTRING_CONFIG'
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
app = Flask(__name__) 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)
@app.context_processor def check_password(app, password):
def inject_edit(): """
return {'edit': current_app.config['edit']} Checks if a password is correct
"""
password_file = app.config['password_file']
if not os.path.isfile(password_file):
logger.debug('Authentication failed: no password file found')
with open(password_file) as f:
real_password = f.read().strip()
correct = password == real_password
del real_password
if correct:
logger.debug('Authentication successful')
else:
logger.debug('Authentication failed: password incorrect')
return correct
@login_manager.user_loader
def load_user(user_id):
return Admin() if user_id == 'admin' else None
@app.route('/', methods=['GET']) @app.route('/', methods=['GET'])
@ -65,11 +102,35 @@ def document(document_id):
return render_template('doc.jinja', document=doc, index=False) return render_template('doc.jinja', document=doc, index=False)
@app.route('/new/', methods=['GET']) @app.route('/login/', methods=['GET', 'POST'])
def new(): def login():
if not current_app.config.get('edit'): form = LoginForm()
return abort(404) s = form.is_submitted()
logger.debug(f's={s}')
if s:
logger.debug(f'pw={form.password.data}')
logger.debug(f'headers={request.headers}')
v = form.validate()
logger.debug(f'v={v}')
logger.debug(f'e={form.errors}')
if v:
valid = check_password(current_app, form.password.data)
if valid:
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' document_id = 'new'
new_doc = generate_default_document(document_id) new_doc = generate_default_document(document_id)
doc_path = safe_join(current_app.config['root'], f'{document_id}.json') doc_path = safe_join(current_app.config['root'], f'{document_id}.json')
@ -79,29 +140,23 @@ def new():
@app.route('/edit/<document_id>', methods=['GET', 'POST']) @app.route('/edit/<document_id>', methods=['GET', 'POST'])
@login_required
def edit(document_id): def edit(document_id):
if not current_app.config.get('edit'):
return abort(404)
doc_path = safe_join(current_app.config['root'], f'{document_id}.json') doc_path = safe_join(current_app.config['root'], f'{document_id}.json')
# Load the document to edit
if not os.path.isfile(doc_path):
return abort(404)
with open(doc_path) as f:
doc: Document = load(f)
# Check for content updates # Check for content updates
if request.method == 'POST': if request.method == 'POST':
sent_json = request.json sent_json = request.json
new_doc: Document = parse_document_from_json(sent_json) new_doc: Document = parse_document_from_json(sent_json)
with open(doc_path, 'w') as f: with open(doc_path, 'w') as f:
dump(new_doc, f) dump(new_doc, f)
new_id = new_doc.get_tag_value('id') return {}
if document_id != new_id:
new_path = safe_join(current_app.config['root'], f'{new_id}.json') # Load the document to edit
os.rename(doc_path, new_path) if not os.path.isfile(doc_path):
return { 'href': url_for('edit', document_id=new_id) } return abort(404)
with open(doc_path) as f:
doc: Document = load(f)
# Check for structural updates # Check for structural updates
if add := request.args.get('add'): if add := request.args.get('add'):
@ -169,7 +224,7 @@ def read_config(app, path):
with open(path) as f: with open(path) as f:
config = json.load(f) config = json.load(f)
app.config['root'] = config['root'] app.config['root'] = config['root']
app.config['edit'] = config['edit'] app.config['password_file'] = config['password_file']
return config return config
@ -188,5 +243,5 @@ def wsgi():
config_path = os.environ.get(CONFIG_ENVVAR) or '/etc/redstring.conf' config_path = os.environ.get(CONFIG_ENVVAR) or '/etc/redstring.conf'
config = read_config(app, config_path) config = read_config(app, config_path)
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
logger.debug(f'Loaded config from {config_path}: {config}') logger.debug(f'Lloaded config from {config_path}: {config}')
return app return app

View File

@ -38,14 +38,14 @@ window.onload = function () {
<div id="{{ tab.name }}-page" class="tab-page{% if selected %} tab-page-selected{% endif %}"> <div id="{{ tab.name }}-page" class="tab-page{% if selected %} tab-page-selected{% endif %}">
<table id="{{ tab.name }}-page-table" class="page-table"> <table id="{{ tab.name }}-page-table" class="page-table">
{% for tag in tab.tags %} {% for tag in tab.tags %}
{%- if not tag.options.private or edit -%} {%- if not tag.options.private or current_user.is_authenticated -%}
<tr {% if tab.options.hide_names %}class="hide-tag-name"{% endif %}> <tr {% if tab.options.hide_names %}class="hide-tag-name"{% endif %}>
<td>{{ tag.name }}</td> <td>{{ tag.name }}</td>
<td>{{ make_tag_value(tag) }}</td> <td>{{ make_tag_value(tag) }}</td>
</tr> </tr>
{%- endif -%} {%- endif -%}
{% for subtag in tag.subtags %} {% for subtag in tag.subtags %}
{%- if (not tag.options.private and not subtag.options.private) or edit -%} {%- if (not tag.options.private and not subtag.options.private) or current_user.is_authenticated -%}
<tr {% if tab.options.hide_names %}class="hide-tag-name"{% endif %}> <tr {% if tab.options.hide_names %}class="hide-tag-name"{% endif %}>
<td>{% if loop.last %}&#9492;{% else %}&#9500;{% endif %} {{ subtag.name }}</td> <td>{% if loop.last %}&#9492;{% else %}&#9500;{% endif %} {{ subtag.name }}</td>
<td>{{ make_tag_value(subtag) }}</td> <td>{{ make_tag_value(subtag) }}</td>
@ -69,15 +69,18 @@ window.onload = function () {
{# TODO: tab.priority support #} {# TODO: tab.priority support #}
{% block page_content %} {% block page_content %}
<div id="tabs"> <div id="tabs">
{% if index and current_user.is_authenticated %}
<div id="logout" class="tab tab-right"><a href="/logout/">logout</a></div>
{% endif %}
{%- for tab in document -%} {%- for tab in document -%}
{%- if not tab.options.private or edit-%} {%- if not tab.options.private or current_user.is_authenticated-%}
{{ make_content_tab(tab, loop.first) }} {{ make_content_tab(tab, loop.first) }}
{%- endif -%} {%- endif -%}
{%- endfor -%} {%- endfor -%}
{%- if not index -%} {%- if not index -%}
{% if edit %} {% if current_user.is_authenticated %}
<div id="edit" class="tab tab-right"><a href="/edit/{{ document.get_tag_value('id') }}">edit</a></div> <div id="edit" class="tab tab-right"><a href="/edit/{{ document.get_tag_value('id') }}">edit</a></div>
{% endif %} {% endif %}
<div id="index" class="tab tab-right"><a href="/index/">index</a></div> <div id="index" class="tab tab-right"><a href="/index/">index</a></div>

View File

@ -121,7 +121,7 @@ function save() {
req.responseType = "json"; req.responseType = "json";
req.onreadystatechange = function () { req.onreadystatechange = function () {
if (req.readyState == 4 && req.status == 200) { if (req.readyState == 4 && req.status == 200) {
window.location.href = req.response.href; window.location.reload();
} }
}; };
req.send(JSON.stringify(doc)); req.send(JSON.stringify(doc));

View File

@ -0,0 +1,17 @@
{% extends 'base.jinja' %}
{% set page_title = 'Login' -%}
{% block page_content %}
<div id="tabs">
<div id="tab-login" class="tab tab-down">login</div>
<div id="index" class="tab tab-right"><a href="/index/">index</a></div>
</div>
<div class="tab-page tab-page-selected">
<form method="post" novalidate>
{{ form.hidden_tag() }}
<p>{{ form.password.label }}<br>{{ form.password(size=32) }}
<p>{{ form.submit() }}</p>
</form>
</div>
{% endblock page_content %}