AO3-DL/python/webuiAdmin.py
2025-12-25 18:47:33 -08:00

223 lines
7 KiB
Python
Executable file

#!/usr/bin/env python3
# This file is part of AOMO, "Archive Of My Own", a collection of Python and PHP
# scripts designed act as an improved local backup system
# for works published on https://archiveofourown.org
#
# Copyright (c) 2025 Cyberpro123, except where otherwise noted.
#
# This project is available under the GNU General Public License (GPL) v2.0,
# available at https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html
# or in the 'LICENSE.md' file that should be distributed alongside this one.
#
# Report issues to https://codeberg.org/Cyberpro123/AOMO
# Contact author at cyberpro123@posteo.com
from collections.abc import Callable
import curses
import datetime
import textwrap
import time
import sys
import bcrypt
import common
import sql
class MenuItem:
def __init__(
self,
prompt: str,
actionType: str,
action: Callable | str = "",
key: str | int = "",
value: str | bool = "",
):
self.prompt = str(prompt)
self.actionType = str(actionType)
self.action = action
self.key = str(key)[:1]
self.value = value
self.defaultValue = value
def __str__(self):
if self.key:
outStr = f"[{self.key}] "
else:
outStr = "[ ] "
outStr += self.prompt
if self.actionType in ("checkbox", "getTxt") and (not outStr.endswith(" ")):
outStr += " "
if self.actionType == "checkbox":
if self.value:
outStr += "[X]"
else:
outStr += "[ ]"
elif self.actionType == "getTxt":
if "password" in self.prompt.lower():
for i in self.value:
outStr += "*"
else:
outStr += self.value
return outStr
menus = {
"menu_main": [
MenuItem(prompt="Manage Users", actionType="chMn", action="menu_users", key=1),
MenuItem(prompt="Close Menu", actionType="close", key=9),
],
"menu_users": [
MenuItem(prompt="Add User", actionType="chMn", action="userAdd", key=1),
MenuItem(prompt="Change Password", actionType="chMn", action="chPass", key=2),
MenuItem(prompt="Back", actionType="chMn", action="menu_main", key=9),
],
"chPass": [],
}
header = ""
loaded = {"highestExistingUserID": None}
def getKeys(menu: list):
output = []
for i in menu:
output.append(i.key)
return output
def getMenuItemFromKey(key: str, menu: list) -> MenuItem:
for i in menu:
if i.key == key:
return i
def doMenu(screen):
global header
exitMenu = False
char = 0
row = 0
optionsStart = 0
menu = menus["menu_main"]
while not exitMenu:
screen.erase()
skipInput = False
height, width = screen.getmaxyx()
headerList = textwrap.wrap(header, width=width)
optionsStart = len(headerList)
optionsEnd = optionsStart + len(menu) - 1
if row < optionsStart:
row = optionsStart
common.logger.debug(f"Char: [{char}]")
for num, line in enumerate(headerList):
screen.addstr(num, 0, line)
for num, item in enumerate(menu):
screen.addstr(num + optionsStart, 0, str(item))
if (chr(char) in getKeys(menu)) or (
char in (curses.KEY_RIGHT, curses.KEY_ENTER, 10, 13)
):
# 10 = newline 13 = carriage return
if chr(char) in getKeys(menu):
item = getMenuItemFromKey(chr(char), menu)
else:
item = menu[row - optionsStart]
match item.actionType:
case "close":
sys.exit(0)
case "chMn":
for i in menu:
i.value = i.defaultValue
menu = menus[item.action]
case "checkbox":
item.value = not item.value
case "function":
header = item.action(menu)
for i in menu:
i.value = i.defaultValue
if item.actionType != "checkbox":
row = optionsStart
skipInput = True
del item
elif char in (curses.KEY_DOWN, 9): # 9 is tab
row += 1
elif char in (curses.KEY_UP, curses.KEY_BTAB):
row -= 1
elif chr(char).isprintable() and char != curses.KEY_LEFT:
item = menu[row - optionsStart]
if char == curses.KEY_BACKSPACE:
item.value = item.value[:-1]
else:
item.value = item.value + chr(char)
del item
skipInput = True
if char in (curses.KEY_HOME, curses.KEY_PPAGE) or row == optionsEnd + 1:
row = optionsStart
elif char in (curses.KEY_END, curses.KEY_NPAGE) or row == optionsStart - 1:
row = optionsEnd
screen.move(row, 1)
if skipInput:
char = 0
else:
char = screen.getch()
def addUser(menu: list) -> str:
id = 1 + getHighestExistingUserID()
username = menu[0].value
pass1 = menu[1].value
pass2 = menu[2].value
preHashed = menu[3].value
if pass1 != pass2:
return "ERROR: Passwords don't match. User not added."
if len(username) > 100:
return "ERROR: Username too long (max 100 characters)"
if len(pass1) > 72:
return "ERROR: Password too long (max 72 characters)"
if preHashed:
hashed = pass1
else:
hashed = bcrypt.hashpw(pass1.encode(), bcrypt.gensalt()).decode()
if not bcrypt.checkpw(pass1.encode(), hashed.encode()):
return "ERROR: Password hashing failed"
try:
sql.execute(
commands=[
(
"INSERT INTO users (id, username, password_hash) VALUES (?, ?, ?)",
(id, username, hashed),
)
],
config=common.config,
logger=common.logger,
)
except sql.UniqueConstraintFailed:
return "ERROR: User with that name already exists."
else:
loaded["highestExistingUserID"] += 1
return f"Successfully added user [{username}] as ID [{id}]"
def getHighestExistingUserID() -> int:
if not loaded["highestExistingUserID"]:
loaded["highestExistingUserID"] = sql.execute(
commands=[("SELECT MAX(id) FROM users", ())],
config=common.config,
logger=common.logger,
)[0][0][0]
if not loaded["highestExistingUserID"]:
loaded["highestExistingUserID"] = 0
return loaded["highestExistingUserID"]
menus["userAdd"] = [
MenuItem(prompt="Username: ", actionType="getTxt"),
MenuItem(prompt="Enter Password: ", actionType="getTxt"),
MenuItem(prompt="Confirm Password: ", actionType="getTxt"),
MenuItem(prompt="Password Pre-Hashed?", actionType="checkbox", value=False),
MenuItem(prompt="[Submit]", actionType="function", action=addUser),
MenuItem(prompt="Back", actionType="chMn", action="menu_users"),
]
if __name__ == "__main__":
common.init()
curses.wrapper(doMenu)