Get PrusaSlicer working; fix some auth problems

This commit is contained in:
Reid 'arrdem' McKenzie 2023-05-29 10:27:52 -06:00
parent ab0924d803
commit d40a20138b
6 changed files with 86 additions and 29 deletions

View file

@ -51,8 +51,20 @@ def create_j2_request_global(app):
def user_session(): def user_session():
if (session_id := request.cookies.get("sid", "")) and ( if (
uid := ctx.db.try_key(session_id) (
(session_id := request.cookies.get("sid", ""))
and (uid := ctx.db.try_key(session_id))
)
or (
request.authorization
and request.authorization.token
and (uid := ctx.db.try_key(request.authorization.token))
)
or (
(api_key := request.headers.get("x-api-key"))
and (uid := ctx.db.try_key(api_key))
)
): ):
ctx.sid = session_id ctx.sid = session_id
ctx.uid = uid ctx.uid = uid

View file

@ -7,29 +7,47 @@ import os
from typing import Optional from typing import Optional
from flask import Blueprint, current_app, request from flask import Blueprint, current_app, request
from tentacles.blueprints.util import (
requires_admin,
requires_auth,
)
from tentacles.globals import ctx from tentacles.globals import ctx
BLUEPRINT = Blueprint("api", __name__, url_prefix="/api") BLUEPRINT = Blueprint("api", __name__, url_prefix="/api")
def requires_admin(f):
def _helper(*args, **kwargs):
if not ctx.is_admin:
return {"error": "unauthorized"}, 401
else:
return f(*args, **kwargs)
_helper.__name__ = f.__name__
return _helper
def requires_auth(f):
def _helper(*args, **kwargs):
if not ctx.uid:
return {"error": "unauthorized"}, 401
else:
return f(*args, **kwargs)
_helper.__name__ = f.__name__
return _helper
#################################################################################################### ####################################################################################################
# Printers # Printers
# #
# The trick here is handling multipart uploads. # The trick here is handling multipart uploads.
@requires_admin
@BLUEPRINT.route("/printers", methods=["POST"]) @BLUEPRINT.route("/printers", methods=["POST"])
@requires_admin
def create_printer(): def create_printer():
pass pass
@requires_auth
@BLUEPRINT.route("/printers", methods=["GET"]) @BLUEPRINT.route("/printers", methods=["GET"])
@BLUEPRINT.route("/printers/", methods=["GET"]) @BLUEPRINT.route("/printers/", methods=["GET"])
@requires_auth
def list_printers(): def list_printers():
return { return {
"printers": [ "printers": [
@ -38,8 +56,8 @@ def list_printers():
}, 200 }, 200
@requires_admin
@BLUEPRINT.route("/printers", methods=["DELETE"]) @BLUEPRINT.route("/printers", methods=["DELETE"])
@requires_admin
def delete_printer(): def delete_printer():
pass pass
@ -48,9 +66,9 @@ def delete_printer():
# Files # Files
# #
# The trick here is handling multipart uploads. # The trick here is handling multipart uploads.
@requires_auth
@BLUEPRINT.route("/files", methods=["POST"]) @BLUEPRINT.route("/files", methods=["POST"])
@BLUEPRINT.route("/files/<location>", methods=["POST"]) @BLUEPRINT.route("/files/<location>", methods=["POST"])
@requires_auth
def create_file(location: Optional[str] = None): def create_file(location: Optional[str] = None):
# This is the important one, because it's the one that PrusaSlicer et all use to upload jobs. # This is the important one, because it's the one that PrusaSlicer et all use to upload jobs.
@ -68,20 +86,29 @@ def create_file(location: Optional[str] = None):
else: else:
digest = sha3_256() digest = sha3_256()
digest.update(
f"$USER${ctx.uid}$".encode()
) # Salt filenames per-user to avoid collisions
digest.update(file.filename.encode()) digest.update(file.filename.encode())
sanitized_filename = digest.hexdigest() + ".gcode" sanitized_filename = digest.hexdigest() + ".gcode"
sanitized_path = os.path.join( sanitized_path = os.path.join(
current_app.config["UPLOAD_FOLDER"], sanitized_filename current_app.config["UPLOAD_FOLDER"], sanitized_filename
) )
if os.path.exists(sanitized_path):
return {"error": "file exists already"}, 409
file.save(sanitized_path) file.save(sanitized_path)
ctx.db.create_file(ctx.uid, file.filename, sanitized_path) fid = ctx.db.create_file(ctx.uid, file.filename, sanitized_path)
if request.form.get("print", "").lower() == "true":
ctx.db.create_job(ctx.uid, fid)
return {"status": "ok"}, 202 return {"status": "ok"}, 202
@requires_auth
@BLUEPRINT.route("/files", methods=["GET"]) @BLUEPRINT.route("/files", methods=["GET"])
@BLUEPRINT.route("/files/", methods=["GET"]) @BLUEPRINT.route("/files/", methods=["GET"])
@requires_auth
def get_files(): def get_files():
return { return {
"files": [ "files": [
@ -97,22 +124,22 @@ def get_files():
}, 200 }, 200
@requires_auth
@BLUEPRINT.route("/files", methods=["DELETE"]) @BLUEPRINT.route("/files", methods=["DELETE"])
@requires_auth
def delete_file(): def delete_file():
pass pass
#################################################################################################### ####################################################################################################
# Jobs # Jobs
@requires_auth
@BLUEPRINT.route("/jobs", methods=["POST"]) @BLUEPRINT.route("/jobs", methods=["POST"])
@requires_auth
def create_job(): def create_job():
pass pass
@requires_auth
@BLUEPRINT.route("/jobs", methods=["GET"]) @BLUEPRINT.route("/jobs", methods=["GET"])
@requires_auth
def get_jobs(): def get_jobs():
return { return {
"jobs": [ "jobs": [
@ -128,15 +155,27 @@ def get_jobs():
}, 200 }, 200
@requires_auth
@BLUEPRINT.route("/jobs", methods=["DELETE"]) @BLUEPRINT.route("/jobs", methods=["DELETE"])
@requires_auth
def delete_job(): def delete_job():
pass pass
#################################################################################################### ####################################################################################################
# API tokens # API tokens
@requires_auth
@BLUEPRINT.route("/tokens", methods=["GET"]) @BLUEPRINT.route("/tokens", methods=["GET"])
@requires_auth
def get_tokens(): def get_tokens():
pass pass
@BLUEPRINT.route("/version", methods=["GET"])
@requires_auth
def get_version():
"""OctoPrint compatability endpoint."""
return {
"api": "0.1",
"server": "1.3.10",
"text": "OctoPrint 1.3.10 (Tentacles)",
}, 200

View file

@ -20,8 +20,8 @@ log = logging.getLogger(__name__)
BLUEPRINT = Blueprint("files", __name__) BLUEPRINT = Blueprint("files", __name__)
@requires_auth
@BLUEPRINT.route("/files", methods=["GET"]) @BLUEPRINT.route("/files", methods=["GET"])
@requires_auth
def list_files(): def list_files():
if request.method == "POST": if request.method == "POST":
flash("Not supported yet", category="warning") flash("Not supported yet", category="warning")
@ -29,8 +29,8 @@ def list_files():
return render_template("files.html.j2") return render_template("files.html.j2")
@requires_auth
@BLUEPRINT.route("/files", methods=["POST"]) @BLUEPRINT.route("/files", methods=["POST"])
@requires_auth
def manipulate_files(): def manipulate_files():
match request.form.get("action"): match request.form.get("action"):
case "upload": case "upload":
@ -43,11 +43,15 @@ def manipulate_files():
case "delete": case "delete":
file = ctx.db.fetch_file(ctx.uid, int(request.form.get("file_id"))) file = ctx.db.fetch_file(ctx.uid, int(request.form.get("file_id")))
if any(job.finished_at is None for job in ctx.db.list_jobs_by_file(file.id)): if any(
job.finished_at is None for job in ctx.db.list_jobs_by_file(file.id)
):
flash("File is in use", category="error") flash("File is in use", category="error")
return render_template("files.html.j2"), 400 return render_template("files.html.j2"), 400
if os.path.exists(file.path):
os.unlink(file.path) os.unlink(file.path)
ctx.db.delete_file(ctx.uid, file.id) ctx.db.delete_file(ctx.uid, file.id)
flash("File deleted", category="info") flash("File deleted", category="info")

View file

@ -18,14 +18,14 @@ log = logging.getLogger(__name__)
BLUEPRINT = Blueprint("jobs", __name__) BLUEPRINT = Blueprint("jobs", __name__)
@requires_auth
@BLUEPRINT.route("/jobs", methods=["GET"]) @BLUEPRINT.route("/jobs", methods=["GET"])
@requires_auth
def list_jobs(): def list_jobs():
return render_template("jobs.html.j2") return render_template("jobs.html.j2")
@requires_auth
@BLUEPRINT.route("/jobs", methods=["POST"]) @BLUEPRINT.route("/jobs", methods=["POST"])
@requires_auth
def manipulate_jobs(): def manipulate_jobs():
match request.form.get("action"): match request.form.get("action"):
case "enqueue": case "enqueue":

View file

@ -18,14 +18,14 @@ log = logging.getLogger(__name__)
BLUEPRINT = Blueprint("printer", __name__) BLUEPRINT = Blueprint("printer", __name__)
@requires_admin
@BLUEPRINT.route("/printers") @BLUEPRINT.route("/printers")
@requires_admin
def printers(): def printers():
return render_template("printers.html.j2") return render_template("printers.html.j2")
@requires_admin
@BLUEPRINT.route("/printers/add", methods=["get", "post"]) @BLUEPRINT.route("/printers/add", methods=["get", "post"])
@requires_admin
def add_printer(): def add_printer():
if not is_logged_in(): if not is_logged_in():
return redirect("/") return redirect("/")
@ -50,7 +50,7 @@ def add_printer():
return render_template("add_printer.html.j2") return render_template("add_printer.html.j2")
@requires_admin
@BLUEPRINT.route("/printers/delete") @BLUEPRINT.route("/printers/delete")
@requires_admin
def delete_printer(): def delete_printer():
return render_template("delete_printer.html.j2") return render_template("delete_printer.html.j2")

View file

@ -21,10 +21,11 @@ def requires_admin(f):
def _helper(*args, **kwargs): def _helper(*args, **kwargs):
if not ctx.is_admin: if not ctx.is_admin:
flash("Sorry, admins only", category="error") flash("Sorry, admins only", category="error")
redirect("/") return redirect("/")
else: else:
return f(*args, **kwargs) return f(*args, **kwargs)
_helper.__name__ = f.__name__
return _helper return _helper
@ -32,8 +33,9 @@ def requires_auth(f):
def _helper(*args, **kwargs): def _helper(*args, **kwargs):
if not ctx.uid: if not ctx.uid:
flash("Please log in first", category="error") flash("Please log in first", category="error")
redirect("/") return redirect("/")
else: else:
return f(*args, **kwargs) return f(*args, **kwargs)
_helper.__name__ = f.__name__
return _helper return _helper