diff --git a/projects/tentacles/src/python/tentacles/__main__.py b/projects/tentacles/src/python/tentacles/__main__.py index b610dc8..12c1a68 100644 --- a/projects/tentacles/src/python/tentacles/__main__.py +++ b/projects/tentacles/src/python/tentacles/__main__.py @@ -8,6 +8,7 @@ from pathlib import Path import click from flask import Flask, request, session, current_app import tomllib +from datetime import datetime from tentacles.blueprints import user_ui, printer_ui, api from tentacles.store import Store @@ -30,6 +31,7 @@ def commit_db(resp): def create_j2_request_global(): current_app.jinja_env.globals["request"] = request + current_app.jinja_env.globals["datetime"] = datetime def user_session(): diff --git a/projects/tentacles/src/python/tentacles/blueprints/user_ui.py b/projects/tentacles/src/python/tentacles/blueprints/user_ui.py index 49633ee..8cd5c9e 100644 --- a/projects/tentacles/src/python/tentacles/blueprints/user_ui.py +++ b/projects/tentacles/src/python/tentacles/blueprints/user_ui.py @@ -2,9 +2,11 @@ """Blueprints for HTML serving 'ui'.""" +import datetime import logging -from datetime import timedelta +from datetime import timedelta, datetime from importlib.resources import files +import re from click import group from flask import ( @@ -113,11 +115,29 @@ def logout(): @BLUEPRINT.route("/user", methods=["GET", "POST"]) def settings(): - if is_logged_in(request): + if not is_logged_in(request): return redirect("/") elif request.method == "POST": - pass + if request.form["action"] == "add": + ttl_spec = request.form.get("ttl") + if ttl_spec == "forever": + ttl = None + elif m := re.fullmatch(r"(\d+)d", ttl_spec): + ttl = timedelta(days=int(m.group(1))) + else: + flash("Bad request", category="error") + return render_template("user.html.j2"), 400 - else: - return render_template("user.html.j2") + request.db.create_key(request.sid, ttl, request.form.get("name")) + flash("Key created", category="success") + + elif request.form["action"] == "revoke": + request.db.delete_key(request.uid, request.form.get("id")) + flash("Key revoked", category="success") + + else: + flash("Bad request", category="error") + return render_template("user.html.j2"), 400 + + return render_template("user.html.j2") diff --git a/projects/tentacles/src/python/tentacles/schema.sql b/projects/tentacles/src/python/tentacles/schema.sql index 261d4d2..a808341 100644 --- a/projects/tentacles/src/python/tentacles/schema.sql +++ b/projects/tentacles/src/python/tentacles/schema.sql @@ -37,6 +37,7 @@ CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS user_keys ( id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(48)))) , user_id INTEGER + , name TEXT , expiration TEXT ); diff --git a/projects/tentacles/src/python/tentacles/store.py b/projects/tentacles/src/python/tentacles/store.py index 83824b6..5f75b54 100644 --- a/projects/tentacles/src/python/tentacles/store.py +++ b/projects/tentacles/src/python/tentacles/store.py @@ -139,10 +139,12 @@ class Store(object): @fmap(one) @requires_conn - def _create_session(self, uid: int, ttl: Optional[timedelta]): + def _create_session( + self, uid: int, ttl: Optional[timedelta] = None, name: Optional[str] = None + ): return self._conn.execute( - "INSERT INTO user_keys (user_id, expiration) VALUES (?, ?) RETURNING (id)", - [uid, (datetime.now() + ttl).isoformat() if ttl else None], + "INSERT INTO user_keys (user_id, name, expiration) VALUES (?1, ?2, ?3) RETURNING (id)", + [uid, name, (datetime.now() + ttl).isoformat() if ttl else None], ).fetchone() @requires_conn @@ -166,14 +168,14 @@ class Store(object): uid, status = res if status > 0: - return self._create_session(uid, ttl) + return self._create_session(uid, ttl, "web session") else: _, status = self.fetch_user_status(status) raise LoginError(status) @requires_conn - def create_key(self, kid: str, ttl) -> Optional[str]: + def create_key(self, kid: str, ttl, name: Optional[str] = None) -> Optional[str]: """Given an _existing_ login session, create a new key. This allows the user to create more or less permanent API keys associated with their identity. @@ -181,11 +183,14 @@ class Store(object): """ if uid := self.try_key(kid): - return self._create_session(uid, ttl) + return self._create_session(uid, ttl, name) @requires_conn - def list_keys(self): - return self._conn.execute("SELECT id, user_id FROM user_keys").fetchall() + def list_keys(self, uid: int): + for id, name, exp in self._conn.execute( + "SELECT id, name, expiration FROM user_keys WHERE user_id = ?1", [uid] + ).fetchall(): + yield id, name, datetime.fromisoformat(exp) if exp else None @requires_conn def fetch_key(self, kid) -> tuple: @@ -217,10 +222,12 @@ class Store(object): ) @requires_conn - def delete_key(self, kid: str): + def delete_key(self, uid: int, kid: str): """Remove a session/key; equivalent to logout.""" - self._conn.execute("DELETE FROM user_keys WHERE id = ?", [kid]) + self._conn.execute( + "DELETE FROM user_keys WHERE user_id = ?1 and id = ?2", [uid, kid] + ) ################################################################################ # Printers diff --git a/projects/tentacles/src/python/tentacles/templates/base.html.j2 b/projects/tentacles/src/python/tentacles/templates/base.html.j2 index d5a5524..484b90b 100644 --- a/projects/tentacles/src/python/tentacles/templates/base.html.j2 +++ b/projects/tentacles/src/python/tentacles/templates/base.html.j2 @@ -12,8 +12,10 @@