From 9623d0d9836813cef464988ba0dda702983583d7 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Tue, 30 Sep 2025 18:06:53 -0600 Subject: [PATCH] Implement web-based user management / admin setup. --- cista/auth.py | 89 +++++++ frontend/src/App.vue | 2 + frontend/src/components/HeaderMain.vue | 5 +- .../src/components/UserManagementModal.vue | 250 ++++++++++++++++++ frontend/src/repositories/Client.ts | 50 ++++ frontend/src/repositories/User.ts | 31 +++ frontend/src/stores/main.ts | 2 +- 7 files changed, 427 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/UserManagementModal.vue diff --git a/cista/auth.py b/cista/auth.py index 4145754..7a272ec 100644 --- a/cista/auth.py +++ b/cista/auth.py @@ -10,6 +10,7 @@ from sanic import Blueprint, html, json, redirect from sanic.exceptions import BadRequest, Forbidden, Unauthorized from cista import config, session +from cista.util import pwgen _argon = argon2.PasswordHasher() _droppyhash = re.compile(r"^([a-f0-9]{64})\$([a-f0-9]{8})$") @@ -191,3 +192,91 @@ async def change_password(request): res = json({"message": "Password updated"}) session.create(res, username) return res + + +@bp.get("/users") +async def list_users(request): + verify(request, privileged=True) + users = [] + for name, user in config.config.users.items(): + users.append( + { + "username": name, + "privileged": user.privileged, + "lastSeen": user.lastSeen, + } + ) + return json({"users": users}) + + +@bp.post("/users") +async def create_user(request): + verify(request, privileged=True) + try: + if request.headers.content_type == "application/json": + username = request.json["username"] + password = request.json.get("password") + privileged = request.json.get("privileged", False) + else: + username = request.form["username"][0] + password = request.form.get("password", [None])[0] + privileged = request.form.get("privileged", ["false"])[0].lower() == "true" + if not username or not username.isidentifier(): + raise ValueError("Invalid username") + except (KeyError, ValueError) as e: + raise BadRequest(str(e)) from e + if username in config.config.users: + raise BadRequest("User already exists") + if not password: + password = pwgen.generate() + changes = {"privileged": privileged} + changes["hash"] = _argon.hash(_pwnorm(password)) + try: + config.update_user(username, changes) + except Exception as e: + raise BadRequest(str(e)) from e + return json({"message": f"User {username} created", "password": password}) + + +@bp.put("/users/") +async def update_user(request, username): + verify(request, privileged=True) + try: + if request.headers.content_type == "application/json": + changes = request.json + else: + changes = {} + if "password" in request.form: + changes["password"] = request.form["password"][0] + if "privileged" in request.form: + changes["privileged"] = request.form["privileged"][0].lower() == "true" + except KeyError as e: + raise BadRequest("Missing fields") from e + password_response = None + if "password" in changes: + if changes["password"] == "": + changes["password"] = pwgen.generate() + password_response = changes["password"] + changes["hash"] = _argon.hash(_pwnorm(changes["password"])) + del changes["password"] + if not changes: + return json({"message": "No changes"}) + try: + config.update_user(username, changes) + except Exception as e: + raise BadRequest(str(e)) from e + response = {"message": f"User {username} updated"} + if password_response: + response["password"] = password_response + return json(response) + + +@bp.put("/config/public") +async def update_public(request): + verify(request, privileged=True) + try: + public = request.json["public"] + except KeyError: + raise BadRequest("Missing public field") from None + config.update_config({"public": public}) + return json({"message": "Public setting updated"}) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 040aa20..40d7c8c 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,6 +1,7 @@