Get API key management wired up

This commit is contained in:
Reid 'arrdem' McKenzie 2023-05-26 23:54:36 -06:00
parent adf9e28274
commit 2977d88e4f
6 changed files with 87 additions and 17 deletions

View file

@ -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():

View file

@ -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
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")

View file

@ -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
);

View file

@ -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

View file

@ -12,8 +12,10 @@
<body>
<nav class="navbar">
<span class="logo">
<a href="https://tentacles.tirefireind.us"><img src="/static/tentacles.svg" alt="Tentacles"></a>
<a href="/">
<img src="/static/tentacles.svg" alt="Tentacles">
<span class="color-yellow">Tentacles</span>
</a>
</span>
<input id="menu-toggle" type="checkbox" />

View file

@ -0,0 +1,38 @@
{% extends "base.html.j2" %}
{% block content %}
<h1>User settings</h1>
<div class="">
<h2>API keys</h2>
{% with keys = request.db.list_keys(request.uid) %}
<ul>
{% for id, name, exp in keys if name != 'web session' %}
<li>
<span class="key-name">{{ name }}</span>
<span class="key-key">{{ id }}</span>
<span class="key-expiration">{{ 'Expires in' if exp else ''}} {{ exp - datetime.now() if exp else 'Never' }}</span>
<form method="post">
<input type="hidden" name="action" value="revoke">
<input type="hidden" name="id" value="{{ id }}">
<span><input id="submit" type="submit" value="Revoke"/></span>
</form>
</li>
{% endfor %}
</ul>
{% endwith %}
<h2>Add a key</h2>
<form method="post">
<input type="hidden" name="action" value="add">
<span class="form-input"><span class="form-label">API key name</span><input type="text" name="name" /></span>
<span class="form-input"><span class="form-label">Key lifetime</span>
<select name="ttl">
<option value="30d">30 days</option>
<option value="90d">90 days</option>
<option value="365d">1y</option>
<option value="forever">Forever (not recommended)</option>
</select>
</span>
<span><input id="submit" type="submit" value="Add" onclick="maybeSubmit();" /></span>
</form>
</div>
{% endblock %}