Get a user approval flow working
This commit is contained in:
parent
42ad82d96e
commit
d6d9a348b1
16 changed files with 396 additions and 116 deletions
|
@ -11,10 +11,10 @@ import cherrypy
|
|||
import click
|
||||
from flask import Flask, request
|
||||
from tentacles.blueprints import (
|
||||
admin_ui,
|
||||
api,
|
||||
file_ui,
|
||||
job_ui,
|
||||
printer_ui,
|
||||
user_ui,
|
||||
)
|
||||
from tentacles.db import Db
|
||||
|
@ -39,8 +39,7 @@ def custom_ctx(app, wsgi_app):
|
|||
store = db_factory(app)
|
||||
token = _ctx.set(Ctx(store))
|
||||
try:
|
||||
with store.savepoint():
|
||||
return wsgi_app(environ, start_response)
|
||||
return wsgi_app(environ, start_response)
|
||||
finally:
|
||||
store.close()
|
||||
_ctx.reset(token)
|
||||
|
@ -88,11 +87,11 @@ def make_app():
|
|||
app.before_request(user_session)
|
||||
|
||||
# Blueprints
|
||||
app.register_blueprint(user_ui.BLUEPRINT)
|
||||
app.register_blueprint(printer_ui.BLUEPRINT)
|
||||
app.register_blueprint(job_ui.BLUEPRINT)
|
||||
app.register_blueprint(file_ui.BLUEPRINT)
|
||||
app.register_blueprint(admin_ui.BLUEPRINT)
|
||||
app.register_blueprint(api.BLUEPRINT)
|
||||
app.register_blueprint(file_ui.BLUEPRINT)
|
||||
app.register_blueprint(job_ui.BLUEPRINT)
|
||||
app.register_blueprint(user_ui.BLUEPRINT)
|
||||
|
||||
# Shove our middleware in there
|
||||
app.wsgi_app = custom_ctx(app, app.wsgi_app)
|
||||
|
@ -106,6 +105,8 @@ def make_app():
|
|||
@click.option("--trace/--no-trace", "trace", default=False)
|
||||
@click.option("--config", type=Path)
|
||||
def serve(hostname: str, port: int, config: Path, trace: bool):
|
||||
logging.addLevelName(logging.DEBUG - 5, "TRACE")
|
||||
logging.TRACE = logging.DEBUG - 5
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s %(threadName)s - %(name)s - %(levelname)s - %(message)s",
|
||||
level=logging.INFO,
|
||||
|
@ -113,7 +114,7 @@ def serve(hostname: str, port: int, config: Path, trace: bool):
|
|||
|
||||
logging.getLogger("tentacles").setLevel(logging.DEBUG)
|
||||
if trace:
|
||||
logging.getLogger("tentacles.db").setLevel(logging.DEBUG - 1)
|
||||
logging.getLogger("tentacles.db").setLevel(logging.TRACE)
|
||||
|
||||
app = make_app()
|
||||
|
||||
|
|
123
projects/tentacles/src/python/tentacles/blueprints/admin_ui.py
Normal file
123
projects/tentacles/src/python/tentacles/blueprints/admin_ui.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import logging
|
||||
|
||||
from .api import requires_admin
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
flash,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
)
|
||||
from tentacles.globals import ctx
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
BLUEPRINT = Blueprint("admin", __name__)
|
||||
|
||||
|
||||
@BLUEPRINT.route("/admin", methods=["GET"])
|
||||
@requires_admin
|
||||
def list_files():
|
||||
if request.method == "POST":
|
||||
flash("Not supported yet", category="warning")
|
||||
|
||||
return render_template("admin.html.j2")
|
||||
|
||||
|
||||
@BLUEPRINT.route("/admin/users", methods=["POST"])
|
||||
@requires_admin
|
||||
def manipulate_users():
|
||||
uid = int(request.form.get("user_id", "-1"))
|
||||
|
||||
match request.form.get("action"):
|
||||
case "approve":
|
||||
row = ctx.db.approve_user(uid=uid)
|
||||
ctx.db.enable_user(uid=uid)
|
||||
ctx.db.create_email(
|
||||
uid=uid,
|
||||
subject="Tentacles account approved!",
|
||||
body=render_template(
|
||||
"approval_email.html.j2",
|
||||
username=row.name,
|
||||
base_url=request.root_url,
|
||||
),
|
||||
)
|
||||
if row:
|
||||
flash(f"Approved {row.name}", category="success")
|
||||
|
||||
case "enable":
|
||||
row = ctx.db.enable_user(uid=uid)
|
||||
if row:
|
||||
flash(f"Enabled {row.name}", category="success")
|
||||
|
||||
case "disable":
|
||||
row = ctx.db.disable_user(uid=uid)
|
||||
if row:
|
||||
flash(f"Disabled {row.name}", category="success")
|
||||
|
||||
case "passwdchng":
|
||||
ctx.db.set_user_status(uid=uid, status="passwdchng")
|
||||
|
||||
case _:
|
||||
print(request.form)
|
||||
flash("Not supported yet", category="warning")
|
||||
return render_template("admin.html.j2"), 400
|
||||
|
||||
return redirect("/admin")
|
||||
|
||||
|
||||
@BLUEPRINT.route("/admin/users", methods=["GET"])
|
||||
def get_users():
|
||||
return redirect("/admin")
|
||||
|
||||
|
||||
@BLUEPRINT.route("/admin/printers")
|
||||
@requires_admin
|
||||
def printers():
|
||||
return render_template("printers.html.j2")
|
||||
|
||||
|
||||
@BLUEPRINT.route("/admin/printers/add", methods=["GET"])
|
||||
@requires_admin
|
||||
def add_printer():
|
||||
return render_template("add_printer.html.j2")
|
||||
|
||||
|
||||
@BLUEPRINT.route("/admin/printers", methods=["POST"])
|
||||
@requires_admin
|
||||
def handle_add_printer():
|
||||
try:
|
||||
assert request.form["name"]
|
||||
assert request.form["url"]
|
||||
assert request.form["api_key"]
|
||||
ctx.db.try_create_printer(
|
||||
name=request.form["name"],
|
||||
url=request.form["url"],
|
||||
api_key=request.form["api_key"],
|
||||
sid=0, # Disconnected
|
||||
)
|
||||
flash("Printer created")
|
||||
return redirect("/admin/printers")
|
||||
|
||||
except Exception as e:
|
||||
log.exception("Failed to create printer")
|
||||
flash(f"Unable to create printer", category="error")
|
||||
|
||||
return render_template("printers.html.j2")
|
||||
|
||||
|
||||
@BLUEPRINT.route("/admin/files", methods=["POST"])
|
||||
@requires_admin
|
||||
def manipulate_files():
|
||||
fid = int(request.form.get("file_id", "-1"))
|
||||
|
||||
match request.form.get("action"):
|
||||
case _:
|
||||
print(request.form)
|
||||
flash("Not supported yet", category="warning")
|
||||
return render_template("admin.html.j2"), 400
|
||||
|
||||
return redirect("/admin")
|
|
@ -12,6 +12,7 @@ from flask import (
|
|||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
send_file,
|
||||
)
|
||||
from tentacles.globals import ctx
|
||||
|
||||
|
@ -41,6 +42,15 @@ def manipulate_files():
|
|||
flash(resp.get("error"), category="error")
|
||||
return render_template("files.html.j2"), code
|
||||
|
||||
case "download":
|
||||
file = ctx.db.fetch_file(uid=ctx.uid, fid=int(request.form.get("file_id")))
|
||||
if file:
|
||||
return send_file(
|
||||
file.path, as_attachment=True, download_name=file.filename
|
||||
)
|
||||
else:
|
||||
flash("File not found", category="error")
|
||||
|
||||
case "delete":
|
||||
file = ctx.db.fetch_file(uid=ctx.uid, fid=int(request.form.get("file_id")))
|
||||
if any(
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import logging
|
||||
|
||||
from .util import is_logged_in, requires_admin
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
flash,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
)
|
||||
from tentacles.globals import ctx
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
BLUEPRINT = Blueprint("printer", __name__)
|
||||
|
||||
|
||||
@BLUEPRINT.route("/printers")
|
||||
@requires_admin
|
||||
def printers():
|
||||
return render_template("printers.html.j2")
|
||||
|
||||
|
||||
@BLUEPRINT.route("/printers/add", methods=["get", "post"])
|
||||
@requires_admin
|
||||
def add_printer():
|
||||
if not is_logged_in():
|
||||
return redirect("/")
|
||||
|
||||
elif request.method == "POST":
|
||||
try:
|
||||
assert request.form["name"]
|
||||
assert request.form["url"]
|
||||
assert request.form["api_key"]
|
||||
ctx.db.try_create_printer(
|
||||
name=request.form["name"],
|
||||
url=request.form["url"],
|
||||
api_key=request.form["api_key"],
|
||||
sid=0, # Disconnected
|
||||
)
|
||||
flash("Printer created")
|
||||
return redirect("/printers")
|
||||
|
||||
except Exception as e:
|
||||
log.exception("Failed to create printer")
|
||||
flash(f"Unable to create printer", category="error")
|
||||
|
||||
return render_template("add_printer.html.j2")
|
||||
|
||||
|
||||
@BLUEPRINT.route("/printers/delete")
|
||||
@requires_admin
|
||||
def delete_printer():
|
||||
return render_template("delete_printer.html.j2")
|
|
@ -120,6 +120,18 @@ def post_register():
|
|||
return render_template("register.html.j2")
|
||||
|
||||
|
||||
@BLUEPRINT.route("/user/verify", methods=["GET", "POST"])
|
||||
def verify():
|
||||
token = request.args.get("token", "")
|
||||
row = ctx.db.try_verify_user(token=token)
|
||||
if row:
|
||||
flash(
|
||||
f"Thanks for verifying your email {row.name}! You'll receive another email when your account is ready for use.",
|
||||
category="success",
|
||||
)
|
||||
return redirect("/user/login")
|
||||
|
||||
|
||||
@BLUEPRINT.route("/user/logout")
|
||||
def logout():
|
||||
# Invalidate the user's authorization
|
||||
|
|
|
@ -28,7 +28,7 @@ def qfn(name, f):
|
|||
# Force lazy values for convenience
|
||||
if isinstance(res, GeneratorType):
|
||||
res = list(res)
|
||||
log.log(logging.DEBUG - 1, "%s (%r) -> %r", name, kwargs, res)
|
||||
log.log(logging.TRACE, "%s (%r) -> %r", name, kwargs, res)
|
||||
return res
|
||||
|
||||
_helper.__name__ = f.__name__
|
||||
|
@ -158,15 +158,10 @@ class Db(Queries):
|
|||
digest.update(password.encode("utf-8"))
|
||||
res = super().try_login(username=username, hash=digest.hexdigest())
|
||||
if not res:
|
||||
return
|
||||
res = self.fetch_user_status(sid=res.status_id)
|
||||
raise LoginError(res.name)
|
||||
|
||||
uid, status = res
|
||||
if status > 0:
|
||||
return self.create_key(uid=uid, name="web session", ttl=ttl)
|
||||
|
||||
else:
|
||||
_, status = self.fetch_user_status(status)
|
||||
raise LoginError(status)
|
||||
return self.create_key(uid=res.id, name="web session", ttl=ttl)
|
||||
|
||||
def try_create_user(
|
||||
self, *, username: str, email: str, password: str, gid: int = 1, sid: int = -3
|
||||
|
|
|
@ -18,6 +18,7 @@ CREATE TABLE IF NOT EXISTS user_statuses (
|
|||
, UNIQUE(name)
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO user_statuses (id, name) VALUES (-4, 'passwdchng');
|
||||
INSERT OR IGNORE INTO user_statuses (id, name) VALUES (-3, 'unverified');
|
||||
INSERT OR IGNORE INTO user_statuses (id, name) VALUES (-2, 'unapproved');
|
||||
INSERT OR IGNORE INTO user_statuses (id, name) VALUES (-1, 'disabled');
|
||||
|
@ -30,6 +31,7 @@ CREATE TABLE IF NOT EXISTS users (
|
|||
, email TEXT
|
||||
, hash TEXT
|
||||
, status_id INTEGER
|
||||
, created_at TEXT DEFAULT (datetime('now'))
|
||||
, verification_token TEXT DEFAULT (lower(hex(randomblob(32))))
|
||||
, verified_at TEXT
|
||||
, approved_at TEXT
|
||||
|
@ -89,16 +91,29 @@ CREATE TABLE IF NOT EXISTS files (
|
|||
----------------------------------------------------------------------------------------------------
|
||||
-- A job is a request for a copy of a file to be run off
|
||||
-- For simplicity, jobs also serve as scheduling records
|
||||
|
||||
CREATE TABLE IF NOT EXISTS job_statuses (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
, name TEXT
|
||||
, UNIQUE(name)
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO job_statuses (id, name) VALUES (2, 'success');
|
||||
INSERT OR IGNORE INTO job_statuses (id, name) VALUES (1, 'running');
|
||||
INSERT OR IGNORE INTO job_statuses (id, name) VALUES (0, 'queued');
|
||||
INSERT OR IGNORE INTO job_statuses (id, name) VALUES (-1, 'cancelled');
|
||||
INSERT OR IGNORE INTO job_statuses (id, name) VALUES (-2, 'failed');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
, user_id INTEGER NOT NULL
|
||||
, file_id INTEGER NOT NULL
|
||||
, printer_id INTEGER
|
||||
, started_at TEXT
|
||||
, cancelled_at TEXT
|
||||
, finished_at TEXT
|
||||
, state TEXT
|
||||
, status_id INTEGER DEFAULT (0)
|
||||
, message TEXT
|
||||
, printer_id INTEGER
|
||||
, FOREIGN KEY(user_id) REFERENCES users(id)
|
||||
, FOREIGN KEY(file_id) REFERENCES files(id)
|
||||
, FOREIGN KEY(printer_id) REFERENCES printer(id)
|
||||
|
@ -141,8 +156,7 @@ WHERE
|
|||
|
||||
-- name: list-users
|
||||
SELECT
|
||||
id
|
||||
, email
|
||||
*
|
||||
FROM users
|
||||
;
|
||||
|
||||
|
@ -155,21 +169,60 @@ WHERE
|
|||
AND verified_at IS NULL
|
||||
;
|
||||
|
||||
-- name: verify-user!
|
||||
-- name: try-verify-user^
|
||||
UPDATE users
|
||||
SET
|
||||
verified_at = datetime('now')
|
||||
, verification_token = lower(hex(randomblob(32)))
|
||||
WHERE
|
||||
id = :uid
|
||||
verification_token = :token
|
||||
RETURNING
|
||||
id
|
||||
, name
|
||||
;
|
||||
|
||||
|
||||
-- name: set-user-status!
|
||||
-- name: enable-user^
|
||||
UPDATE users
|
||||
SET
|
||||
status_id = :sid
|
||||
enabled_at = datetime('now')
|
||||
WHERE
|
||||
id = :uid
|
||||
RETURNING
|
||||
id
|
||||
, name
|
||||
;
|
||||
|
||||
-- name: disable-user^
|
||||
UPDATE users
|
||||
SET
|
||||
enabled_at = NULL
|
||||
WHERE
|
||||
id = :uid
|
||||
RETURNING
|
||||
id
|
||||
, name
|
||||
;
|
||||
|
||||
-- name: approve-user^
|
||||
UPDATE users
|
||||
SET
|
||||
approved_at = datetime('now')
|
||||
WHERE
|
||||
id = :uid
|
||||
RETURNING
|
||||
id
|
||||
, name
|
||||
;
|
||||
|
||||
-- name: set-user-status^
|
||||
UPDATE users
|
||||
SET
|
||||
status_id = (SELECT id FROM user_statuses WHERE id = :status OR name = :status)
|
||||
WHERE
|
||||
id = :uid
|
||||
RETURNING
|
||||
id
|
||||
, name
|
||||
;
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
@ -207,8 +260,12 @@ SELECT
|
|||
, status_id
|
||||
FROM users
|
||||
WHERE
|
||||
(name = :username AND hash = :hash)
|
||||
OR (email = :username AND hash = :hash)
|
||||
((name = :username AND hash = :hash)
|
||||
OR (email = :username AND hash = :hash))
|
||||
AND ((verified_at IS NOT NULL
|
||||
AND approved_at IS NOT NULL
|
||||
AND enabled_at IS NOT NULL)
|
||||
OR group_id = 0)
|
||||
LIMIT 1
|
||||
;
|
||||
|
||||
|
@ -222,6 +279,17 @@ WHERE
|
|||
user_id = :uid
|
||||
;
|
||||
|
||||
-- name: list-nonweb-keys
|
||||
SELECT
|
||||
id
|
||||
, name
|
||||
, expiration
|
||||
FROM user_keys
|
||||
WHERE
|
||||
user_id = :uid
|
||||
AND name NOT LIKE '%web session%'
|
||||
;
|
||||
|
||||
-- name: fetch-key^
|
||||
SELECT
|
||||
*
|
||||
|
@ -232,12 +300,15 @@ WHERE
|
|||
|
||||
-- name: try-key^
|
||||
SELECT
|
||||
id
|
||||
k.id
|
||||
, user_id
|
||||
FROM user_keys
|
||||
FROM user_keys k
|
||||
INNER JOIN users u
|
||||
ON k.user_id = u.id
|
||||
WHERE
|
||||
(expiration IS NULL OR unixepoch(expiration) > unixepoch('now'))
|
||||
AND id = :kid
|
||||
AND k.id = :kid
|
||||
AND u.enabled_at IS NOT NULL -- and the user is not disabled!
|
||||
;
|
||||
|
||||
-- name: refresh-key
|
||||
|
@ -336,7 +407,9 @@ RETURNING
|
|||
-- name: list-files
|
||||
SELECT
|
||||
*
|
||||
FROM files
|
||||
, (SELECT COUNT(*) FROM jobs WHERE file_id = f.id AND status_id > 1) AS `print_successes`
|
||||
, (SELECT COUNT(*) FROM jobs WHERE file_id = f.id AND status_id < 0) AS `print_failures`
|
||||
FROM files f
|
||||
WHERE
|
||||
user_id = :uid
|
||||
;
|
||||
|
@ -385,6 +458,7 @@ WHERE
|
|||
-- name: list-jobs
|
||||
SELECT
|
||||
*
|
||||
, (SELECT name FROM job_statuses WHERE id = j.status_id) AS `status`
|
||||
FROM jobs
|
||||
WHERE
|
||||
(:uid IS NULL OR user_id = :uid)
|
||||
|
@ -393,6 +467,7 @@ WHERE
|
|||
-- name: list-jobs-by-file
|
||||
SELECT
|
||||
*
|
||||
, (SELECT name FROM job_statuses WHERE id = j.status_id) AS `status`
|
||||
FROM jobs
|
||||
WHERE
|
||||
file_id = :fid
|
||||
|
@ -422,7 +497,8 @@ LIMIT 1
|
|||
-- name: list-job-history
|
||||
SELECT
|
||||
*
|
||||
FROM jobs
|
||||
, (SELECT name FROM job_statuses WHERE id = j.status_id) AS `status`
|
||||
FROM jobs j
|
||||
WHERE
|
||||
finished_at IS NOT NULL
|
||||
AND (:uid IS NULL OR user_id = :uid)
|
||||
|
@ -496,7 +572,7 @@ WHERE
|
|||
UPDATE jobs
|
||||
SET
|
||||
finished_at = datetime('now')
|
||||
, state = :state
|
||||
, status_id = (SELECT id FROM job_statuses WHERE name = :state)
|
||||
, message = :message
|
||||
WHERE
|
||||
id = :jid
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{% extends "base.html.j2" %}
|
||||
{% block content %}
|
||||
<div class="row twelve columns mb-1">
|
||||
{% include "users_list.html.j2" %}
|
||||
</div>
|
||||
<div class="row twelve columns mb-1">
|
||||
{% include "files_admin_list.html.j2" %}
|
||||
</div>
|
||||
<div class="row twelve columns mb-1">
|
||||
{% include "printers_list.html.j2" %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="stylesheet" href="{{ base_url }}/static/css/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<nav class="container navbar">
|
||||
<span class="logo">
|
||||
<a class="row" href="/">
|
||||
<img src="{{ base_url }}/static/tentacles.svg" alt="Tentacles">
|
||||
<span class="name color-yellow">Tentacles</span>
|
||||
</a>
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
<div class="container content">
|
||||
<div class="row">
|
||||
<p>
|
||||
Hello {{ username }},
|
||||
</p>
|
||||
<p>
|
||||
Your account on Tentacles has been approved! You may now <a href="{{ base_url }}login">log in</a> and start printing!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<footer>
|
||||
|
||||
</footer>
|
||||
</html>
|
|
@ -33,7 +33,7 @@
|
|||
<li><a class="twelve columns button slide" href="/jobs">Jobs</a></li>
|
||||
<li><a class="twelve columns button slide" href="/files">Files</a></li>
|
||||
{% if ctx.is_admin %}
|
||||
<li><a class="twelve columns button slide" href="/printers">Printers</a></li>
|
||||
<li><a class="twelve columns button slide" href="/admin">Administration</a></li>
|
||||
{% endif %}
|
||||
<li><a class="twelve columns button slide" href="/user">Settings</a></li>
|
||||
<li><a class="twelve columns button slide" href="/user/logout">Log out</a></li>
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
{% import "macros.html.j2" as macros %}
|
||||
<h2>Files</h2>
|
||||
{% with files = ctx.db.list_files(uid=ctx.uid) %}
|
||||
{% if files %}
|
||||
{% for file in files %}
|
||||
<div class="file row u-flex">
|
||||
<div class="details six columns">
|
||||
<span class="file-name">{{ file.filename }}</span>
|
||||
<span class="file-owner">{{ ctx.db.fetch_user(uid=file.user_id).name }}</span>
|
||||
<span class="file-sucesses">{{ file.print_successes }}</span> successes
|
||||
<span class="file-failures">{{ file.print_failures }}</span> errors
|
||||
</div>
|
||||
<div class="controls u-flex u-ml-auto">
|
||||
{{ macros.delete_file(file.id, endpoint="/admin/files") }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
You don't have any files. Upload something!
|
||||
{% endif %}
|
||||
{% endwith %}
|
|
@ -6,9 +6,12 @@
|
|||
<div class="file row u-flex">
|
||||
<div class="details six columns">
|
||||
<span class="file-name">{{ file.filename }}</span>
|
||||
<span class="file-sucesses">{{ file.print_successes }}</span> successes
|
||||
<span class="file-failures">{{ file.print_failures }}</span> errors
|
||||
</div>
|
||||
<div class="controls u-flex u-ml-auto">
|
||||
{{ macros.start_job(file.id) }}
|
||||
{{ macros.download_file(file.id) }}
|
||||
{{ macros.delete_file(file.id) }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -36,15 +36,58 @@
|
|||
{{ 'queued' if (not job.finished_at and not job.printer_id and not job.cancelled_at) else
|
||||
'running' if (not job.finished_at and job.printer_id and not job.cancelled_at) else
|
||||
'cancelling' if (not job.finished_at and job.cancelled_at) else
|
||||
job.state }}
|
||||
job.status }}
|
||||
{% endmacro %}
|
||||
|
||||
{# #################################################################################################### #}
|
||||
{# File CRUD #}
|
||||
{% macro delete_file(id) %}
|
||||
<form class="inline" method="post" action="/files">
|
||||
{% macro delete_file(id, endpoint="/files") %}
|
||||
<form class="inline" method="post" action="{{ endpoint }}">
|
||||
<input type="hidden" name="action" value="delete" />
|
||||
<input type="hidden" name="file_id" value="{{ id }}" />
|
||||
<input id="submit" type="submit" value="Delete"/>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro download_file(id, endpoint="/files") %}
|
||||
<form class="inline" method="post" action="{{ endpoint }}">
|
||||
<input type="hidden" name="action" value="download" />
|
||||
<input type="hidden" name="file_id" value="{{ id }}" />
|
||||
<input id="submit" type="submit" value="Download"/>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
{# #################################################################################################### #}
|
||||
{# User CRUD #}
|
||||
|
||||
{% macro approve_user(id) %}
|
||||
<form class="inline" method="post" action="/admin/users">
|
||||
<input type="hidden" name="action" value="approve" />
|
||||
<input type="hidden" name="user_id" value="{{ id }}" />
|
||||
<input id="submit" type="submit" value="Approve"/>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro enable_user(id) %}
|
||||
<form class="inline" method="post" action="/admin/users">
|
||||
<input type="hidden" name="action" value="enable" />
|
||||
<input type="hidden" name="user_id" value="{{ id }}" />
|
||||
<input id="submit" type="submit" value="Enable"/>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro lock_user(id) %}
|
||||
<form class="inline" method="post" action="/admin/users">
|
||||
<input type="hidden" name="action" value="lock" />
|
||||
<input type="hidden" name="user_id" value="{{ id }}" />
|
||||
<input id="submit" type="submit" value="Lock"/>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro passwdchng_user(id) %}
|
||||
<form class="inline" method="post" action="/admin/users">
|
||||
<input type="hidden" name="action" value="passwdchng" />
|
||||
<input type="hidden" name="user_id" value="{{ id }}" />
|
||||
<input id="submit" type="submit" value="Password change"/>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
<h1>User settings</h1>
|
||||
<div class="row twelve columns keys">
|
||||
<h2>API keys</h2>
|
||||
{% with keys = ctx.db.list_keys(ctx.uid) %}
|
||||
{% with keys = ctx.db.list_nonweb_keys(ctx.uid) %}
|
||||
{% if keys %}
|
||||
{% for id, name, exp in keys %}
|
||||
<div class="row key u-flex">
|
||||
<div class="details six columns">
|
||||
<span class="key-name">{{ name or 'anonymous' }}</span>
|
||||
<span class="key-key">{{ id[:10] }}...</span>
|
||||
<span class="key-expiration u-ml-auto">{{ 'Expires in ' if exp else ''}}{{ exp - datetime.now() if exp else 'Never expires' }}</span>
|
||||
<span class="key-expiration u-ml-auto">{{ 'Expires in ' if exp else ''}}{{ datetime.fromisoformat(exp) - datetime.now() if exp else 'Never expires' }}</span>
|
||||
</div>
|
||||
<div class="controls u-flex u-ml-auto">
|
||||
<form class="inline" method="none" class="ml-auto">
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
{% import "macros.html.j2" as macros %}
|
||||
<h2>Users</h2>
|
||||
{% with users = ctx.db.list_users(uid=ctx.uid) %}
|
||||
{% for user in users %}
|
||||
<div class="user row u-flex">
|
||||
<div class="details six columns">
|
||||
<span class="user-name">{{ user.name }}</span>
|
||||
<span class="user-email">{{ user.email }}</span>
|
||||
<span class="user-group">{{ user.group }}</span>
|
||||
<span class="user-status">{{ user.status }}</span>
|
||||
</div>
|
||||
<div class="controls u-flex u-ml-auto">
|
||||
{% if not user.approved_at %}{{ macros.approve_user(user.id) }}{% endif %}
|
||||
{% if not user.enabled_at %}
|
||||
{{ macros.enable_user(user.id) }}
|
||||
{% else %}
|
||||
{{ macros.lock_user(user.id) }}
|
||||
{% endif %}
|
||||
{{ macros.passwdchng_user(user.id) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
|
@ -10,7 +10,6 @@ Mostly related to monitoring and managing Printer state.
|
|||
from contextlib import closing
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from threading import Event
|
||||
from typing import Callable
|
||||
from urllib import parse as urlparse
|
||||
|
||||
|
@ -42,8 +41,6 @@ class OctoRest(_OR):
|
|||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
SHUTDOWN = Event()
|
||||
|
||||
|
||||
def poll_printers(app: App, db: Db) -> None:
|
||||
"""Poll printers for their status."""
|
||||
|
@ -56,13 +53,14 @@ def poll_printers(app: App, db: Db) -> None:
|
|||
log.info(f"Printer {printer.id} {printer.status} -> {status}")
|
||||
db.update_printer_status(pid=printer.id, status=status)
|
||||
|
||||
printer_job = {}
|
||||
try:
|
||||
client = OctoRest(url=printer.url, apikey=printer.api_key)
|
||||
printer_job = client.job_info()
|
||||
printer_job: dict = client.job_info()
|
||||
try:
|
||||
printer_state = client.printer().get("state").get("flags", {})
|
||||
printer_state: dict = client.printer().get("state").get("flags", {})
|
||||
except HTTPError:
|
||||
printer_state = {"disconnected": True}
|
||||
printer_state: dict = {"disconnected": True}
|
||||
|
||||
if printer_state.get("error"):
|
||||
# If there's a mapped job, we manually fail it so that
|
||||
|
@ -251,19 +249,7 @@ def send_emails(app, db: Db):
|
|||
subject=message.subject,
|
||||
msg=message.body,
|
||||
)
|
||||
db.send_email(message.id)
|
||||
|
||||
|
||||
def once(f):
|
||||
val = uninitialized = object()
|
||||
|
||||
def _helper(*args, **kwargs):
|
||||
nonlocal val
|
||||
if val is uninitialized:
|
||||
val = f(*args, **kwargs)
|
||||
return val
|
||||
|
||||
return _helper
|
||||
db.send_email(eid=message.id)
|
||||
|
||||
|
||||
def toil(*fs):
|
||||
|
|
Loading…
Reference in a new issue