Merge branch 'feature/auth-token-flask-login'

This commit is contained in:
Paul Brinkmeier 2023-12-05 15:52:01 +01:00
commit 1b6d0c40f3
11 changed files with 113 additions and 57 deletions

View File

@ -4,6 +4,11 @@
## Setup ## Setup
`jon` is a Python WSGI application written using Flask.
This means you'll have to install a bunch of Python packages to get up and running.
### Dependencies
``` ```
pip install -r requirements.txt pip install -r requirements.txt
``` ```
@ -11,6 +16,14 @@ pip install -r requirements.txt
You should probably use a virtualenv for that. You should probably use a virtualenv for that.
I develop `jon` using Python 3.10 but it should work with older versions as well. I develop `jon` using Python 3.10 but it should work with older versions as well.
#### Arch Linux
If you're on Arch, these packages are required for running the server:
```
python python-flask python-flask-login python-psycopg2
```
### Building Frontend JS ### Building Frontend JS
Most of jon works without JS but there are some features that require it. Most of jon works without JS but there are some features that require it.

View File

@ -22,12 +22,7 @@ def create_app():
app.config.from_file("config.json", load=json.load, silent=True) app.config.from_file("config.json", load=json.load, silent=True)
db.init_app(app) db.init_app(app)
auth.init_app(app)
# This function denies every request until `auth.ACCESS_TOKEN`
# is passed using `?token=` to authenticate the session.
@app.before_request
def before_req_fun():
return auth.before_request()
@app.context_processor @app.context_processor
def utility_processor(): def utility_processor():

View File

@ -1,7 +1,10 @@
import secrets import secrets
import string import string
from flask import Blueprint, request, redirect, render_template, session from flask import Blueprint, request, redirect, render_template
from flask_login import current_user, login_user, logout_user, LoginManager
from typing import Any, Dict, List, Optional
bp = Blueprint("auth", __name__, url_prefix="/auth") bp = Blueprint("auth", __name__, url_prefix="/auth")
@ -15,27 +18,67 @@ ALLOWED_PATHS = [
] ]
# A poor man's replacement for memory-backed session solution.
# We keep exactly one User (and the corresponding UserData) in
# memory and use that to store session data.
class UserData:
location: Optional[Dict[str, Any]]
orders: List[Dict[str, Any]]
def __init__(self):
self.location = None
self.orders = []
class User:
is_authenticated: bool
is_active: bool
is_anonymous: bool
data: UserData
def __init__(self):
self.is_authenticated = True
self.is_active = True
self.is_anonymous = False
self.data = UserData()
def get_id(self) -> str:
return ""
def init_app(app):
login_manager = LoginManager(app)
the_one_and_only_user = User()
@login_manager.user_loader
def load_user(user_id: str) -> User:
assert user_id == ""
return the_one_and_only_user
# This function denies every request until `auth.ACCESS_TOKEN`
# is passed using `?token=` to authenticate the user.
# We use this instead of @login_required because otherwise we'd have
# to add that annotation to all routes.
# See also: https://flask-login.readthedocs.io/en/latest/#flask_login.login_required
@app.before_request
def before_request(): def before_request():
"""
If the correct token query parameter is passed along with any request,
we mark this session authenticated by setting `session["authenticated"]`.
Unless the session is authenticated, all requests result in a 403 FORBIDDEN.
"""
if "token" in request.args: if "token" in request.args:
if request.args["token"] == ACCESS_TOKEN: if request.args["token"] == ACCESS_TOKEN:
session["authenticated"] = () login_user(the_one_and_only_user)
# Reload the page without query parameters # Reload the page without query parameters
return redirect(request.path) return redirect(request.path)
# Don't deny any paths in `ALLOWED_PATHS` # Never deny any paths in `ALLOWED_PATHS`
if request.path in ALLOWED_PATHS: if request.path in ALLOWED_PATHS:
return return
if not "authenticated" in session: if not current_user.is_authenticated:
return render_template("auth/denied.html"), 403 return render_template("auth/denied.html"), 403
@bp.get("/logout") @bp.get("/logout")
def logout(): def logout():
session.pop("authenticated", None) logout_user()
return redirect("/") return redirect("/")

View File

@ -1,5 +1,5 @@
from flask import Blueprint, flash, redirect, render_template, request, session from flask import Blueprint, flash, redirect, render_template, request
from flask_login import current_user
from . import db from . import db
@ -9,22 +9,18 @@ bp = Blueprint("entry", __name__, url_prefix="/entry")
@bp.route("/", methods=["GET", "POST"]) @bp.route("/", methods=["GET", "POST"])
def index(): def index():
cart = session.get("cart", default=[])
return render_template( return render_template(
"entry/index.html", "entry/index.html"
cart=cart
) )
@bp.post("/add-new-items") @bp.post("/add-new-items")
def add_new_entries(): def add_new_entries():
print(session)
i_know_what_im_doing = "i-know-what-im-doing" in request.form i_know_what_im_doing = "i-know-what-im-doing" in request.form
if not i_know_what_im_doing: if not i_know_what_im_doing:
return "Du weißt nicht was du tust", 400 return "Du weißt nicht was du tust", 400
orders = session.get("cart", default=[]) orders = current_user.data.orders
if not orders: if not orders:
return "Keine Aufträge", 404 return "Keine Aufträge", 404
@ -36,7 +32,7 @@ def add_new_entries():
db.get_db().commit() db.get_db().commit()
# Reset the cart # Reset the cart
session["cart"] = [] current_user.data.orders = []
return redirect(request.referrer) return redirect(request.referrer)
@ -48,9 +44,7 @@ def delete_order():
except: except:
return "Incomplete or mistyped form", 400 return "Incomplete or mistyped form", 400
cart = session.get("cart", default=[]) del current_user.data.orders[order_index]
del cart[order_index]
session["cart"] = cart
return redirect(request.referrer) return redirect(request.referrer)
@ -76,9 +70,7 @@ def new_order():
except: except:
return f"Incomplete or mistyped form", 400 return f"Incomplete or mistyped form", 400
cart = session.get("cart", default=[]) current_user.data.orders.append({
print(cart)
cart.append({
"barcode": barcode, "barcode": barcode,
"name": name, "name": name,
"sales_units": sales_units, "sales_units": sales_units,
@ -91,7 +83,6 @@ def new_order():
"net_unit_price": net_unit_price, "net_unit_price": net_unit_price,
"gross_unit_price": gross_unit_price "gross_unit_price": gross_unit_price
}) })
session["cart"] = cart
return redirect("/entry") return redirect("/entry")
with db.run_query("entry/get_groups.sql") as cursor: with db.run_query("entry/get_groups.sql") as cursor:
@ -122,10 +113,9 @@ def api_search_items():
except: except:
return {"error": "Missing query parameter `search-term`"}, 400 return {"error": "Missing query parameter `search-term`"}, 400
location = session.get("location", None) location = current_user.data.location
with db.run_query("search_items.sql", { with db.run_query("search_items.sql", {
"location_id": None if location is None else location["location_id"], "location_id": location["location_id"] if location else None,
"search_term": search_term "search_term": search_term
}) as cursor: }) as cursor:
items = cursor.fetchall() items = cursor.fetchall()

View File

@ -1,4 +1,5 @@
from flask import Blueprint, redirect, render_template, request, session from flask import Blueprint, redirect, render_template, request
from flask_login import current_user
from . import db from . import db
@ -8,7 +9,7 @@ bp = Blueprint("inventory", __name__, url_prefix="/inventory")
@bp.get("/") @bp.get("/")
def index(): def index():
location = session.get("location", None) location = current_user.data.location
items = db.run_query("get_inventory_overview.sql", { items = db.run_query("get_inventory_overview.sql", {
"location_id": None if location is None else location["location_id"] "location_id": None if location is None else location["location_id"]
}).fetchall() }).fetchall()
@ -20,7 +21,7 @@ def index():
@bp.get("/report") @bp.get("/report")
def read_report(): def read_report():
location = session.get("location", None) location = current_user.data.location
items = db.run_query("get_inventory_report.sql", { items = db.run_query("get_inventory_report.sql", {
"location_id": None if location is None else location["location_id"] "location_id": None if location is None else location["location_id"]
}).fetchall() }).fetchall()

View File

@ -1,4 +1,5 @@
from flask import Blueprint, render_template, request, session from flask import Blueprint, render_template, request
from flask_login import current_user
from . import db from . import db
@ -11,13 +12,12 @@ def index():
if request.method == "POST": if request.method == "POST":
location_id = request.form.get("location_id", "") location_id = request.form.get("location_id", "")
if location_id == "": if location_id == "":
session.pop("location", None) current_user.data.location = None
else: else:
location = db.run_query("get_location_by_id.sql", { location = db.run_query("get_location_by_id.sql", {
"location_id": location_id} "location_id": location_id
).fetchone() }).fetchone()
session["location"] = location current_user.data.location = location
locations = db.run_query("get_locations.sql").fetchall() locations = db.run_query("get_locations.sql").fetchall()

View File

@ -71,3 +71,6 @@ th {
display: block; display: block;
width: 8em; width: 8em;
} }
details {
font-size: 0.8em;
}

View File

@ -15,10 +15,10 @@
<li{{ " class=current-page" if request.path.startswith("/entry") else "" }}><a href="/entry">Eintragen</a></li> <li{{ " class=current-page" if request.path.startswith("/entry") else "" }}><a href="/entry">Eintragen</a></li>
<li{{ " class=current-page" if request.path.startswith("/location") else "" }}> <li{{ " class=current-page" if request.path.startswith("/location") else "" }}>
<a href="/location"> <a href="/location">
{% if "location" not in session %} {% if not current_user.data.location %}
Raum wählen Raum wählen
{% else %} {% else %}
Raum: {{ session.location.location_name }} Raum: {{ current_user.data.location.location_name }}
{% endif %} {% endif %}
</a> </a>
</li> </li>
@ -30,6 +30,16 @@
<details> <details>
<summary><code>config</code></summary> <summary><code>config</code></summary>
<pre>{% for key, value in config.items() %}{{ key }} = {{ value }} <pre>{% for key, value in config.items() %}{{ key }} = {{ value }}
{% endfor %}</pre>
</details>
<details>
<summary><code>session</code></summary>
<pre>{% for key, value in session.items() %}{{ key }} = {{ value }}
{% endfor %}</pre>
</details>
<details>
<summary><code>current_user.data</code></summary>
<pre>{% for key, value in current_user.data.__dict__.items() %}{{ key }} = {{ value }}
{% endfor %}</pre> {% endfor %}</pre>
</details> </details>
{% endif %} {% endif %}

View File

@ -15,7 +15,7 @@
<th>VK-Preis (Brutto)</th> <th>VK-Preis (Brutto)</th>
<th>Aktionen</th> <th>Aktionen</th>
</tr> </tr>
{% for cart_item in cart %} {% for cart_item in current_user.data.orders %}
<tr> <tr>
<td><code>{{ cart_item.barcode }}</code></td> <td><code>{{ cart_item.barcode }}</code></td>
<td>{{ cart_item.name }}</td> <td>{{ cart_item.name }}</td>

View File

@ -3,9 +3,9 @@
{% block content %} {% block content %}
<form method="POST"> <form method="POST">
<select name="location_id"> <select name="location_id">
<option value="" {{ "selected" if "location" not in session else ""}}>-</option> <option value="" {{ "selected" if not current_user.data.location else ""}}>-</option>
{% for location in locations %} {% for location in locations %}
<option value="{{ location.location_id }}" {{ "selected" if "location" in session and session.location.location_id == location.location_id else "" }}>{{ location.location_name }}</option> <option value="{{ location.location_id }}" {{ "selected" if current_user.data.location.location_id == location.location_id else "" }}>{{ location.location_name }}</option>
{% endfor %} {% endfor %}
</select> </select>

View File

@ -1,6 +1,7 @@
blinker==1.6.2 blinker==1.6.2
click==8.1.3 click==8.1.3
Flask==2.3.2 Flask==2.3.2
Flask-Login==0.6.2
itsdangerous==2.1.2 itsdangerous==2.1.2
Jinja2==3.1.2 Jinja2==3.1.2
MarkupSafe==2.1.2 MarkupSafe==2.1.2