More tentacles progress
This commit is contained in:
parent
1f7c21f26e
commit
e05cf362df
14 changed files with 442 additions and 139 deletions
|
@ -1,4 +1,9 @@
|
||||||
SECRET_KEY = "SgvzxsO5oPBGInmqsyyGQWAJXkS9"
|
SECRET_KEY = "SgvzxsO5oPBGInmqsyyGQWAJXkS9"
|
||||||
|
|
||||||
[db]
|
[db]
|
||||||
uri = "tentacles.sqlite3"
|
uri = "/home/arrdem/Documents/hobby/programming/source/projects/tentacles/tentacles.sqlite3"
|
||||||
|
|
||||||
|
[[users]]
|
||||||
|
email = "root@tirefireind.us"
|
||||||
|
group_id = 0
|
||||||
|
status_id = 1
|
||||||
|
|
|
@ -6,10 +6,10 @@
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import click
|
import click
|
||||||
from flask import Flask, request, session, current_app, Authorization
|
from flask import Flask, request, session, current_app
|
||||||
import tomllib
|
import tomllib
|
||||||
|
|
||||||
from tentacles.blueprints import ui, api
|
from tentacles.blueprints import user_ui, printer_ui, api
|
||||||
from tentacles.store import Store
|
from tentacles.store import Store
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,22 +19,28 @@ def cli():
|
||||||
|
|
||||||
|
|
||||||
def open_db():
|
def open_db():
|
||||||
current_app.db = Store(current_app.config.get("db", {}).get("uri"))
|
request.db = Store(current_app.config.get("db", {}).get("uri"))
|
||||||
current_app.db.connect()
|
request.db.connect()
|
||||||
|
|
||||||
|
|
||||||
def commit_db(resp):
|
def commit_db(resp):
|
||||||
current_app.db.commit()
|
request.db.close()
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
def create_j2_request_global():
|
||||||
|
current_app.jinja_env.globals["request"] = request
|
||||||
|
|
||||||
|
|
||||||
def user_session():
|
def user_session():
|
||||||
if (session_id := request.cookies.get("sid", "")) and (
|
if (session_id := request.cookies.get("sid", "")) and (
|
||||||
uid := current_app.db.try_key(session_id)
|
uid := request.db.try_key(session_id)
|
||||||
):
|
):
|
||||||
request.sid = session_id
|
request.sid = session_id
|
||||||
request.uid = uid
|
request.uid = uid
|
||||||
_, gid, name, _ = current_app.db.fetch_user(uid)
|
_id, gid, name, _email, _hash, _status, _verification = request.db.fetch_user(
|
||||||
|
uid
|
||||||
|
)
|
||||||
request.gid = gid
|
request.gid = gid
|
||||||
request.username = name
|
request.username = name
|
||||||
request.is_admin = gid == 0
|
request.is_admin = gid == 0
|
||||||
|
@ -58,11 +64,19 @@ def serve(hostname: str, port: int, config: Path):
|
||||||
|
|
||||||
print(app.config)
|
print(app.config)
|
||||||
|
|
||||||
app.before_first_request(open_db)
|
# Before first request
|
||||||
|
app.before_first_request(create_j2_request_global)
|
||||||
|
|
||||||
|
# Before request
|
||||||
|
app.before_request(open_db)
|
||||||
app.before_request(user_session)
|
app.before_request(user_session)
|
||||||
|
|
||||||
|
# After request
|
||||||
app.after_request(commit_db)
|
app.after_request(commit_db)
|
||||||
|
|
||||||
app.register_blueprint(ui.BLUEPRINT)
|
# Blueprints
|
||||||
|
app.register_blueprint(user_ui.BLUEPRINT)
|
||||||
|
app.register_blueprint(printer_ui.BLUEPRINT)
|
||||||
app.register_blueprint(api.BLUEPRINT)
|
app.register_blueprint(api.BLUEPRINT)
|
||||||
|
|
||||||
app.run(host=hostname, port=port)
|
app.run(host=hostname, port=port)
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""Blueprints for HTML serving 'ui'."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
from importlib.resources import files
|
||||||
|
|
||||||
|
from click import group
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Blueprint,
|
||||||
|
current_app,
|
||||||
|
request,
|
||||||
|
Request,
|
||||||
|
redirect,
|
||||||
|
render_template,
|
||||||
|
session,
|
||||||
|
url_for,
|
||||||
|
flash,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .util import is_logged_in
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
BLUEPRINT = Blueprint("printer", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@BLUEPRINT.route("/printers")
|
||||||
|
def printers():
|
||||||
|
return render_template("printers.html.j2")
|
||||||
|
|
||||||
|
|
||||||
|
@BLUEPRINT.route("/printers/add", methods=["get", "post"])
|
||||||
|
def add_printer():
|
||||||
|
if not is_logged_in(request):
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
|
elif request.method == "POST":
|
||||||
|
pass
|
||||||
|
|
||||||
|
return render_template("add_printer.html.j2")
|
||||||
|
|
||||||
|
|
||||||
|
@BLUEPRINT.route("/printers/delete")
|
||||||
|
def delete_printer():
|
||||||
|
return render_template("delete_printer.html.j2")
|
|
@ -1,95 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
"""Blueprints for HTML serving 'ui'."""
|
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
from importlib.resources import files
|
|
||||||
|
|
||||||
from flask import (
|
|
||||||
Blueprint,
|
|
||||||
current_app,
|
|
||||||
request,
|
|
||||||
Request,
|
|
||||||
redirect,
|
|
||||||
render_template,
|
|
||||||
session,
|
|
||||||
url_for,
|
|
||||||
flash,
|
|
||||||
)
|
|
||||||
|
|
||||||
BLUEPRINT = Blueprint("ui", __name__)
|
|
||||||
|
|
||||||
|
|
||||||
def is_logged_in(request: Request) -> bool:
|
|
||||||
return request.uid is not None
|
|
||||||
|
|
||||||
|
|
||||||
@BLUEPRINT.route("/")
|
|
||||||
def root():
|
|
||||||
return (
|
|
||||||
render_template(
|
|
||||||
"index.html.j2",
|
|
||||||
request=request,
|
|
||||||
),
|
|
||||||
200,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@BLUEPRINT.route("/login", methods=["GET", "POST"])
|
|
||||||
def login():
|
|
||||||
if is_logged_in(request):
|
|
||||||
return redirect("/")
|
|
||||||
|
|
||||||
elif request.method == "POST":
|
|
||||||
if sid := current_app.db.try_login(
|
|
||||||
username := request.form["username"],
|
|
||||||
request.form["password"],
|
|
||||||
timedelta(days=1),
|
|
||||||
):
|
|
||||||
resp = redirect("/")
|
|
||||||
resp.set_cookie("sid", sid)
|
|
||||||
flash(f"Welcome, {username}", category="success")
|
|
||||||
return resp
|
|
||||||
|
|
||||||
else:
|
|
||||||
flash("Incorrect username/password", category="error")
|
|
||||||
return render_template("login.html.j2")
|
|
||||||
|
|
||||||
else:
|
|
||||||
return render_template("login.html.j2")
|
|
||||||
|
|
||||||
|
|
||||||
@BLUEPRINT.route("/register", methods=["GET", "POST"])
|
|
||||||
def register():
|
|
||||||
if is_logged_in(request):
|
|
||||||
return redirect("/")
|
|
||||||
|
|
||||||
elif request.method == "POST":
|
|
||||||
try:
|
|
||||||
if uid := current_app.db.try_create_user(
|
|
||||||
request.form["username"],
|
|
||||||
request.form["email"],
|
|
||||||
request.form["password"],
|
|
||||||
):
|
|
||||||
flash(
|
|
||||||
"Please check your email for a verification request",
|
|
||||||
category="success",
|
|
||||||
)
|
|
||||||
return render_template("register.html.j2")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
flash("Unable to register that username", category="error")
|
|
||||||
return render_template("register.html.j2")
|
|
||||||
|
|
||||||
else:
|
|
||||||
return render_template("register.html.j2")
|
|
||||||
|
|
||||||
|
|
||||||
@BLUEPRINT.route("/logout")
|
|
||||||
def logout():
|
|
||||||
# Invalidate the user's authorization
|
|
||||||
current_app.db.delete_key(request.sid)
|
|
||||||
resp = redirect("/")
|
|
||||||
resp.set_cookie("sid", "")
|
|
||||||
return resp
|
|
123
projects/tentacles/src/python/tentacles/blueprints/user_ui.py
Normal file
123
projects/tentacles/src/python/tentacles/blueprints/user_ui.py
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""Blueprints for HTML serving 'ui'."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
from importlib.resources import files
|
||||||
|
|
||||||
|
from click import group
|
||||||
|
from flask import (
|
||||||
|
Blueprint,
|
||||||
|
current_app,
|
||||||
|
request,
|
||||||
|
Request,
|
||||||
|
redirect,
|
||||||
|
render_template,
|
||||||
|
session,
|
||||||
|
url_for,
|
||||||
|
flash,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .util import salt, is_logged_in
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
BLUEPRINT = Blueprint("user", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@BLUEPRINT.route("/")
|
||||||
|
def root():
|
||||||
|
return (
|
||||||
|
render_template(
|
||||||
|
"index.html.j2",
|
||||||
|
),
|
||||||
|
200,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@BLUEPRINT.route("/user/login", methods=["GET", "POST"])
|
||||||
|
def login():
|
||||||
|
if is_logged_in(request):
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
|
elif request.method == "POST":
|
||||||
|
if sid := request.db.try_login(
|
||||||
|
username := request.form["username"],
|
||||||
|
salt(request.form["password"]),
|
||||||
|
timedelta(days=1),
|
||||||
|
):
|
||||||
|
resp = redirect("/")
|
||||||
|
resp.set_cookie("sid", sid)
|
||||||
|
flash(f"Welcome, {username}", category="success")
|
||||||
|
return resp
|
||||||
|
|
||||||
|
else:
|
||||||
|
flash("Incorrect username/password", category="error")
|
||||||
|
return render_template("login.html.j2")
|
||||||
|
|
||||||
|
else:
|
||||||
|
return render_template("login.html.j2")
|
||||||
|
|
||||||
|
|
||||||
|
@BLUEPRINT.route("/user/register", methods=["GET", "POST"])
|
||||||
|
def register():
|
||||||
|
if is_logged_in(request):
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
|
elif request.method == "POST":
|
||||||
|
try:
|
||||||
|
username = request.form["username"]
|
||||||
|
email = request.form["email"]
|
||||||
|
group_id = None
|
||||||
|
status_id = None
|
||||||
|
|
||||||
|
for user_config in current_app.config.get("users", []):
|
||||||
|
if user_config["email"] == email:
|
||||||
|
if "group_id" in user_config:
|
||||||
|
group_id = user_config["group_id"]
|
||||||
|
|
||||||
|
if "status_id" in user_config:
|
||||||
|
status_id = user_config["status_id"]
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
if res := request.db.try_create_user(
|
||||||
|
username, email, salt(request.form["password"]), group_id, status_id
|
||||||
|
):
|
||||||
|
id, status = res
|
||||||
|
if status == -1:
|
||||||
|
flash(
|
||||||
|
"Please check your email for a verification request",
|
||||||
|
category="success",
|
||||||
|
)
|
||||||
|
return render_template("register.html.j2")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Error encountered while registering a user...")
|
||||||
|
|
||||||
|
flash("Unable to register that username", category="error")
|
||||||
|
return render_template("register.html.j2")
|
||||||
|
|
||||||
|
else:
|
||||||
|
return render_template("register.html.j2")
|
||||||
|
|
||||||
|
|
||||||
|
@BLUEPRINT.route("/user/logout")
|
||||||
|
def logout():
|
||||||
|
# Invalidate the user's authorization
|
||||||
|
request.db.delete_key(request.sid)
|
||||||
|
resp = redirect("/")
|
||||||
|
resp.set_cookie("sid", "")
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
@BLUEPRINT.route("/user", methods=["GET", "POST"])
|
||||||
|
def settings():
|
||||||
|
if is_logged_in(request):
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
|
elif request.method == "POST":
|
||||||
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
return render_template("user.html.j2")
|
29
projects/tentacles/src/python/tentacles/blueprints/util.py
Normal file
29
projects/tentacles/src/python/tentacles/blueprints/util.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
from importlib.resources import files
|
||||||
|
|
||||||
|
from click import group
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Blueprint,
|
||||||
|
current_app,
|
||||||
|
request,
|
||||||
|
Request,
|
||||||
|
redirect,
|
||||||
|
render_template,
|
||||||
|
session,
|
||||||
|
url_for,
|
||||||
|
flash,
|
||||||
|
)
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def is_logged_in(request: Request) -> bool:
|
||||||
|
return request.uid is not None
|
||||||
|
|
||||||
|
|
||||||
|
def salt(password: str) -> str:
|
||||||
|
return "$SALT$" + current_app.config["SECRET_KEY"] + password
|
|
@ -29,6 +29,7 @@ CREATE TABLE IF NOT EXISTS users (
|
||||||
, FOREIGN KEY(group_id) REFERENCES groups(id)
|
, FOREIGN KEY(group_id) REFERENCES groups(id)
|
||||||
, FOREIGN KEY(status_id) REFERENCES user_statuses(id)
|
, FOREIGN KEY(status_id) REFERENCES user_statuses(id)
|
||||||
, UNIQUE(name)
|
, UNIQUE(name)
|
||||||
|
, UNIQUE(email)
|
||||||
);
|
);
|
||||||
|
|
||||||
----------------------------------------------------------------------------------------------------
|
----------------------------------------------------------------------------------------------------
|
||||||
|
|
|
@ -10,31 +10,35 @@ $secondary_blue: #288BC2;
|
||||||
$secondary_green: #A5C426;
|
$secondary_green: #A5C426;
|
||||||
$secondary_light_grey: #CACBCA;
|
$secondary_light_grey: #CACBCA;
|
||||||
$secondary_dark_grey: #9A9A9A;
|
$secondary_dark_grey: #9A9A9A;
|
||||||
|
$secondary_red: red;
|
||||||
$clear: rgba(255, 255, 255, 255);
|
$clear: rgba(255, 255, 255, 255);
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Aaux Next';
|
font-family: 'Aaux Next';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local('Aaux Next'), url('https://fonts.cdnfonts.com/s/60597/AauxNextBlk.woff') format('woff');
|
src: local('Aaux Next'), url('/static/font/AauxNextBlk.otf') format('otf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Aaux Next';
|
font-family: 'Aaux Next';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local('Aaux Next'), url('https://fonts.cdnfonts.com/s/60597/aauxnextbdwebfont.woff') format('woff');
|
src: local('Aaux Next'), url('/static/font/aauxnextbdwebfont.otf') format('otf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Aaux Next';
|
font-family: 'Aaux Next';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local('Aaux Next'), url('https://fonts.cdnfonts.com/s/60597/aauxnextltwebfont.woff') format('woff');
|
src: local('Aaux Next'), url('/static/font/aauxnextltwebfont.otf') format('otf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Aaux Next';
|
font-family: 'Aaux Next';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local('Aaux Next'), url('https://fonts.cdnfonts.com/s/60597/aauxnextmdwebfont.woff') format('woff');
|
src: local('Aaux Next'), url('/static/font/aauxnextmdwebfont.otf') format('otf');
|
||||||
}
|
}
|
||||||
|
|
||||||
@import url(https://fonts.googleapis.com/css?family=Raleway);
|
@import url(https://fonts.googleapis.com/css?family=Raleway);
|
||||||
|
@ -55,6 +59,20 @@ html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 400px;
|
min-width: 400px;
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content, .footer {
|
||||||
|
padding-left: 10%;
|
||||||
|
padding-right: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
.flash, .panel {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -228,3 +246,29 @@ $navbar_padding: 10px;
|
||||||
padding-left: 10%;
|
padding-left: 10%;
|
||||||
padding-right: 10%;
|
padding-right: 10%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flashes {
|
||||||
|
.flash {
|
||||||
|
border: 10px solid $secondary_blue;
|
||||||
|
border-radius: 20px;
|
||||||
|
min-height: 40px;
|
||||||
|
p {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
border-color: $secondary_green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
border-color: $secondary_red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from datetime import timedelta, datetime
|
from collections import namedtuple
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from hashlib import sha3_256
|
from hashlib import sha3_256
|
||||||
|
from importlib.resources import files
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from textwrap import indent
|
from textwrap import indent
|
||||||
from importlib.resources import files
|
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,14 +45,28 @@ def one(it, *args, **kwargs):
|
||||||
return it
|
return it
|
||||||
|
|
||||||
|
|
||||||
|
class StoreError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LoginError(StoreError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Store(object):
|
class Store(object):
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
self._path = path
|
self._path = path
|
||||||
self._conn: sqlite3.Connection = None
|
self._conn: sqlite3.Connection = None
|
||||||
|
|
||||||
|
def _factory(self, cursor, row):
|
||||||
|
fields = [column[0] for column in cursor.description]
|
||||||
|
cls = namedtuple("Row", fields)
|
||||||
|
return cls._make(row)
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
if not self._conn:
|
if not self._conn:
|
||||||
self._conn = sqlite3.connect(self._path, isolation_level="IMMEDIATE")
|
self._conn = sqlite3.connect(self._path, isolation_level="IMMEDIATE")
|
||||||
|
self._conn.row_factory = self._factory
|
||||||
for hunk in PRELUDE.split("\n\n"):
|
for hunk in PRELUDE.split("\n\n"):
|
||||||
try:
|
try:
|
||||||
self._conn.executescript(hunk).fetchall()
|
self._conn.executescript(hunk).fetchall()
|
||||||
|
@ -71,16 +85,28 @@ class Store(object):
|
||||||
self._conn.close()
|
self._conn.close()
|
||||||
self._conn = None
|
self._conn = None
|
||||||
|
|
||||||
@fmap(one)
|
################################################################################
|
||||||
|
# Users
|
||||||
|
|
||||||
@requires_conn
|
@requires_conn
|
||||||
def try_create_user(self, username, email, password):
|
def try_create_user(self, username, email, password, group_id=10, status_id=-2):
|
||||||
"""Attempt to create a new user."""
|
"""Attempt to create a new user.
|
||||||
|
|
||||||
|
:param username: The name of the user to be created.
|
||||||
|
:param email: The email of the user to be created.
|
||||||
|
:param password: The (hopefully salted!) plain text password for the user. Will be hashed before storage.
|
||||||
|
:param group_id: The numeric ID of a group to assign the user to. Default 10 AKA normal user.
|
||||||
|
:param status_id: The numeric ID of the status to assign the user to. Default -2 AKA email verification required.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
digest = sha3_256()
|
digest = sha3_256()
|
||||||
digest.update(password.encode("utf-8"))
|
digest.update(password.encode("utf-8"))
|
||||||
|
digest = digest.hexdigest()
|
||||||
|
print(f"{username}: {digest!r}")
|
||||||
return self._conn.execute(
|
return self._conn.execute(
|
||||||
"INSERT INTO users (name, email, hash) VALUES (?, ?, ?) RETURNING (id)",
|
"INSERT INTO users (name, email, hash, group_id, status_id) VALUES (?, ?, ?, ?, ?) RETURNING id, status_id",
|
||||||
[username, email, digest.hexdigest()],
|
[username, email, digest, group_id, status_id],
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
||||||
@requires_conn
|
@requires_conn
|
||||||
|
@ -99,6 +125,15 @@ class Store(object):
|
||||||
def list_users(self):
|
def list_users(self):
|
||||||
return self._conn.execute("SELECT id, name FROM users").fetchall()
|
return self._conn.execute("SELECT id, name FROM users").fetchall()
|
||||||
|
|
||||||
|
@fmap(one)
|
||||||
|
@requires_conn
|
||||||
|
def fetch_user_status(self, user_status_id: int):
|
||||||
|
"""Fetch a user status by ID"""
|
||||||
|
|
||||||
|
return self._conn.execute(
|
||||||
|
"SELECT id, name FROM user_statuses WHERE id = ?", [user_status_id]
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Sessions / 'keys'
|
# Sessions / 'keys'
|
||||||
|
|
||||||
|
@ -120,14 +155,22 @@ class Store(object):
|
||||||
|
|
||||||
digest = sha3_256()
|
digest = sha3_256()
|
||||||
digest.update(password.encode("utf-8"))
|
digest.update(password.encode("utf-8"))
|
||||||
|
digest = digest.hexdigest()
|
||||||
|
print(f"{username}: {digest!r}")
|
||||||
res = self._conn.execute(
|
res = self._conn.execute(
|
||||||
"SELECT id FROM users WHERE name=? AND hash=? LIMIT 1",
|
"SELECT id, status_id FROM users WHERE (name=?1 AND hash=?2) OR (email=?1 AND hash=?2) LIMIT 1",
|
||||||
[username, digest.hexdigest()],
|
[username, digest],
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if not res:
|
if not res:
|
||||||
return
|
return
|
||||||
uid = res[0]
|
|
||||||
return self._create_session(uid, ttl)
|
uid, status = res
|
||||||
|
if status > 0:
|
||||||
|
return self._create_session(uid, ttl)
|
||||||
|
|
||||||
|
else:
|
||||||
|
_, status = self.fetch_user_status(status)
|
||||||
|
raise LoginError(status)
|
||||||
|
|
||||||
@requires_conn
|
@requires_conn
|
||||||
def create_key(self, kid: str, ttl) -> Optional[str]:
|
def create_key(self, kid: str, ttl) -> Optional[str]:
|
||||||
|
@ -183,16 +226,23 @@ class Store(object):
|
||||||
# Printers
|
# Printers
|
||||||
#
|
#
|
||||||
# Printers represent connections to OctoPrint instances controlling physical machines.
|
# Printers represent connections to OctoPrint instances controlling physical machines.
|
||||||
|
|
||||||
|
@fmap(one)
|
||||||
@requires_conn
|
@requires_conn
|
||||||
def create_printer(self):
|
def try_create_printer(self, url, api_key):
|
||||||
pass
|
self._conn.execute(
|
||||||
|
"INSERT INTO printers (url, api_key, status_id) VALUES (?, ?, 0) RETURNING id",
|
||||||
|
[url, api_key],
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
@requires_conn
|
@requires_conn
|
||||||
def list_printers(self):
|
def list_printers(self):
|
||||||
pass
|
return self._conn.execute(
|
||||||
|
"SELECT id, url, last_poll_date, s.name as status FROM printers p INNER JOIN printer_stauses s ON p.status_id = s.id"
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
@requires_conn
|
@requires_conn
|
||||||
def update_printer_status(self):
|
def update_printer_status(self, printer_id, status_id):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
{% extends "base.html.j2" %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>Add printer</h1>
|
||||||
|
<form method="post" id="form">
|
||||||
|
<p>Hostname: <input type="text" name="hostname" /></p>
|
||||||
|
<p>API key: <input type="text" name="key" /></p>
|
||||||
|
<p><input id="test" type="button" value="Test" enabled="false" /></p>
|
||||||
|
<p><input id="submit" type="submit" value="Add" onclick="maybeSubmit();" /></p>
|
||||||
|
<input type="hidden" name="tested" value="false" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.getElementById("input").disabled = True;
|
||||||
|
|
||||||
|
function testSettings() {
|
||||||
|
var formData = new FormData(document.getElementById("form"))
|
||||||
|
var req = new XMLHttpRequest();
|
||||||
|
req.open("POST", "/printer/test");
|
||||||
|
req.send(formData);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
function maybeSubmit() {
|
||||||
|
if (document.getElementById("tested").value == "true") {
|
||||||
|
document.getElementById("form").submit();
|
||||||
|
} else {
|
||||||
|
console.error("Form values have not been tested!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -24,28 +24,31 @@
|
||||||
|
|
||||||
<ul class="menu">
|
<ul class="menu">
|
||||||
{% if not request.uid %}
|
{% if not request.uid %}
|
||||||
<li><a href="/login">Log in</a></li>
|
<li><a href="/user/login">Log in</a></li>
|
||||||
<li><a href="/register">Register</a></li>
|
<li><a href="/user/register">Register</a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if request.is_admin %}
|
{% if request.is_admin %}
|
||||||
<li><a href="/printers">Settings</a></li>
|
<li><a href="/printers">Printers</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="/settings">Settings</a></li>
|
<li><a href="/user">Settings</a></li>
|
||||||
<li><a href="/logout">Log out</a></li>
|
<li><a href="/user/logout">Log out</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
<div class="content">
|
||||||
{% if messages %}
|
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||||
|
{% if messages %}
|
||||||
<div class="flashes">
|
<div class="flashes">
|
||||||
{% for category, message in messages %}
|
{% for category, message in messages %}
|
||||||
<div class="flash-{{ category }}">{{ message }}</div>
|
<div class="flash {{ category }}">
|
||||||
|
<center><p>{{ message }}</p></center>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<div class="content">
|
|
||||||
{% block content %}Oops, an empty page :/{% endblock %}
|
{% block content %}Oops, an empty page :/{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
|
|
|
@ -1,4 +1,49 @@
|
||||||
{% extends "base.html.j2" %}
|
{% extends "base.html.j2" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p>Hello, {% if request.uid %}{{ request.username }}{% else %}world{% endif %}!</p>
|
<div class="panel printers">
|
||||||
|
<h2>Printers</h2>
|
||||||
|
{% with printers = request.db.list_printers() %}
|
||||||
|
{% if printers %}
|
||||||
|
<ul>
|
||||||
|
{% for printer in printers %}
|
||||||
|
<li></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
No printers available. {% if request.is_admin %}<a href="/printers/add">Configure one!</a>{% else %}Ask the admin to configure one!{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel queue">
|
||||||
|
<h2>Queue</h2>
|
||||||
|
{% with jobs = request.db.list_jobs(uid=request.uid) %}
|
||||||
|
{% if jobs %}
|
||||||
|
<ul>
|
||||||
|
{% for job in jobs %}
|
||||||
|
<li></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
No pending tasks. {% if request.uid %}Start something!{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if request.uid %}
|
||||||
|
<div class="panel files">
|
||||||
|
<h2>Files</h2>
|
||||||
|
{% with files = request.db.list_files(uid=request.uid) %}
|
||||||
|
{% if files %}
|
||||||
|
<ul>
|
||||||
|
{% for file in files %}
|
||||||
|
<li></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
You don't have any files. Upload something!
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -5,6 +5,6 @@
|
||||||
<p>Username: <input type="text" name="username">
|
<p>Username: <input type="text" name="username">
|
||||||
<p>Email address: <input type="text" name="email">
|
<p>Email address: <input type="text" name="email">
|
||||||
<p>Password: <input type="password" name="password">
|
<p>Password: <input type="password" name="password">
|
||||||
<p><input type="submit" value=Register>
|
<p><input type="submit" value="Register">
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -26,7 +26,13 @@ def password_testy():
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def uid_testy(store, username_testy, password_testy):
|
def uid_testy(store, username_testy, password_testy):
|
||||||
return store.try_create_user(username_testy, password_testy)
|
uid, status = store.try_create_user(
|
||||||
|
username_testy,
|
||||||
|
username_testy,
|
||||||
|
password_testy,
|
||||||
|
status_id=1,
|
||||||
|
)
|
||||||
|
return uid
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
Loading…
Reference in a new issue