Get API key management wired up
This commit is contained in:
parent
aacc9f5c1c
commit
bedad7d86b
6 changed files with 87 additions and 17 deletions
|
@ -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():
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
<span class="color-yellow">Tentacles</span>
|
||||
<a href="/">
|
||||
<img src="/static/tentacles.svg" alt="Tentacles">
|
||||
<span class="color-yellow">Tentacles</span>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<input id="menu-toggle" type="checkbox" />
|
||||
|
|
|
@ -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 %}
|
Loading…
Reference in a new issue