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
|
import click
|
||||||
from flask import Flask, request, session, current_app
|
from flask import Flask, request, session, current_app
|
||||||
import tomllib
|
import tomllib
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from tentacles.blueprints import user_ui, printer_ui, api
|
from tentacles.blueprints import user_ui, printer_ui, api
|
||||||
from tentacles.store import Store
|
from tentacles.store import Store
|
||||||
|
@ -30,6 +31,7 @@ def commit_db(resp):
|
||||||
|
|
||||||
def create_j2_request_global():
|
def create_j2_request_global():
|
||||||
current_app.jinja_env.globals["request"] = request
|
current_app.jinja_env.globals["request"] = request
|
||||||
|
current_app.jinja_env.globals["datetime"] = datetime
|
||||||
|
|
||||||
|
|
||||||
def user_session():
|
def user_session():
|
||||||
|
|
|
@ -2,9 +2,11 @@
|
||||||
|
|
||||||
"""Blueprints for HTML serving 'ui'."""
|
"""Blueprints for HTML serving 'ui'."""
|
||||||
|
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta, datetime
|
||||||
from importlib.resources import files
|
from importlib.resources import files
|
||||||
|
import re
|
||||||
|
|
||||||
from click import group
|
from click import group
|
||||||
from flask import (
|
from flask import (
|
||||||
|
@ -113,11 +115,29 @@ def logout():
|
||||||
|
|
||||||
@BLUEPRINT.route("/user", methods=["GET", "POST"])
|
@BLUEPRINT.route("/user", methods=["GET", "POST"])
|
||||||
def settings():
|
def settings():
|
||||||
if is_logged_in(request):
|
if not is_logged_in(request):
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
|
||||||
elif request.method == "POST":
|
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:
|
else:
|
||||||
|
flash("Bad request", category="error")
|
||||||
|
return render_template("user.html.j2"), 400
|
||||||
|
|
||||||
return render_template("user.html.j2")
|
return render_template("user.html.j2")
|
||||||
|
|
|
@ -37,6 +37,7 @@ CREATE TABLE IF NOT EXISTS users (
|
||||||
CREATE TABLE IF NOT EXISTS user_keys (
|
CREATE TABLE IF NOT EXISTS user_keys (
|
||||||
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(48))))
|
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(48))))
|
||||||
, user_id INTEGER
|
, user_id INTEGER
|
||||||
|
, name TEXT
|
||||||
, expiration TEXT
|
, expiration TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -139,10 +139,12 @@ class Store(object):
|
||||||
|
|
||||||
@fmap(one)
|
@fmap(one)
|
||||||
@requires_conn
|
@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(
|
return self._conn.execute(
|
||||||
"INSERT INTO user_keys (user_id, expiration) VALUES (?, ?) RETURNING (id)",
|
"INSERT INTO user_keys (user_id, name, expiration) VALUES (?1, ?2, ?3) RETURNING (id)",
|
||||||
[uid, (datetime.now() + ttl).isoformat() if ttl else None],
|
[uid, name, (datetime.now() + ttl).isoformat() if ttl else None],
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
||||||
@requires_conn
|
@requires_conn
|
||||||
|
@ -166,14 +168,14 @@ class Store(object):
|
||||||
|
|
||||||
uid, status = res
|
uid, status = res
|
||||||
if status > 0:
|
if status > 0:
|
||||||
return self._create_session(uid, ttl)
|
return self._create_session(uid, ttl, "web session")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
_, status = self.fetch_user_status(status)
|
_, status = self.fetch_user_status(status)
|
||||||
raise LoginError(status)
|
raise LoginError(status)
|
||||||
|
|
||||||
@requires_conn
|
@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.
|
"""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.
|
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):
|
if uid := self.try_key(kid):
|
||||||
return self._create_session(uid, ttl)
|
return self._create_session(uid, ttl, name)
|
||||||
|
|
||||||
@requires_conn
|
@requires_conn
|
||||||
def list_keys(self):
|
def list_keys(self, uid: int):
|
||||||
return self._conn.execute("SELECT id, user_id FROM user_keys").fetchall()
|
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
|
@requires_conn
|
||||||
def fetch_key(self, kid) -> tuple:
|
def fetch_key(self, kid) -> tuple:
|
||||||
|
@ -217,10 +222,12 @@ class Store(object):
|
||||||
)
|
)
|
||||||
|
|
||||||
@requires_conn
|
@requires_conn
|
||||||
def delete_key(self, kid: str):
|
def delete_key(self, uid: int, kid: str):
|
||||||
"""Remove a session/key; equivalent to logout."""
|
"""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
|
# Printers
|
||||||
|
|
|
@ -12,8 +12,10 @@
|
||||||
<body>
|
<body>
|
||||||
<nav class="navbar">
|
<nav class="navbar">
|
||||||
<span class="logo">
|
<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>
|
<span class="color-yellow">Tentacles</span>
|
||||||
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<input id="menu-toggle" type="checkbox" />
|
<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