mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-13 18:28:14 +02:00
Initial commit
This commit is contained in:
commit
8aa223bc7b
40 files changed
+4320
No files matched your search
@@ -0,0 +1,170 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import secrets
|
||||
import tomllib
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import tomli_w
|
||||
|
||||
from Settings import GetDataDirectory
|
||||
|
||||
|
||||
Pbkdf2Iterations = 260_000
|
||||
|
||||
UserColourOptions = ["Red", "Orange", "Yellow", "Green", "Blue", "Purple"]
|
||||
DefaultUserColour = "Purple"
|
||||
|
||||
AllowedAvatarExtensions = {".png", ".jpg", ".jpeg", ".gif", ".webp"}
|
||||
|
||||
|
||||
def GetUsersPath():
|
||||
return GetDataDirectory() / "Users.toml"
|
||||
|
||||
|
||||
def GetAvatarDirectory() -> Path:
|
||||
return GetDataDirectory() / "Avatars"
|
||||
|
||||
|
||||
def HashPassword(Password: str) -> str:
|
||||
Salt = secrets.token_hex(16)
|
||||
DerivedKey = hashlib.pbkdf2_hmac(
|
||||
"sha256", Password.encode("utf-8"), bytes.fromhex(Salt), Pbkdf2Iterations
|
||||
)
|
||||
return f"pbkdf2_sha256${Pbkdf2Iterations}${Salt}${DerivedKey.hex()}"
|
||||
|
||||
|
||||
def VerifyPassword(Password: str, StoredHash: str) -> bool:
|
||||
try:
|
||||
Algorithm, IterationsText, Salt, HashHex = StoredHash.split("$")
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
if Algorithm != "pbkdf2_sha256":
|
||||
return False
|
||||
|
||||
DerivedKey = hashlib.pbkdf2_hmac(
|
||||
"sha256", Password.encode("utf-8"), bytes.fromhex(Salt), int(IterationsText)
|
||||
)
|
||||
return hmac.compare_digest(DerivedKey.hex(), HashHex)
|
||||
|
||||
|
||||
def LoadUsers() -> dict[str, dict[str, Any]]:
|
||||
UsersPath = GetUsersPath()
|
||||
|
||||
if not UsersPath.is_file():
|
||||
return {}
|
||||
|
||||
with UsersPath.open("rb") as UsersFile:
|
||||
Loaded = tomllib.load(UsersFile)
|
||||
|
||||
return Loaded.get("Users", {})
|
||||
|
||||
|
||||
def SaveUsers(Users: dict[str, dict[str, Any]]) -> None:
|
||||
GetDataDirectory().mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with GetUsersPath().open("wb") as UsersFile:
|
||||
tomli_w.dump({"Users": Users}, UsersFile)
|
||||
|
||||
|
||||
def AnyUsersExist() -> bool:
|
||||
return len(LoadUsers()) > 0
|
||||
|
||||
|
||||
def GetUser(Username: str) -> dict[str, Any] | None:
|
||||
return LoadUsers().get(Username)
|
||||
|
||||
|
||||
def CreateUser(Username: str, Password: str, IsAdmin: bool, Colour: str = DefaultUserColour) -> None:
|
||||
if not Username or not Username.strip():
|
||||
raise ValueError("Username cannot be empty.")
|
||||
|
||||
if not Password:
|
||||
raise ValueError("Password cannot be empty.")
|
||||
|
||||
if Colour not in UserColourOptions:
|
||||
Colour = DefaultUserColour
|
||||
|
||||
Users = LoadUsers()
|
||||
|
||||
if Username in Users:
|
||||
raise ValueError(f"User '{Username}' already exists.")
|
||||
|
||||
Users[Username] = {
|
||||
"PasswordHash": HashPassword(Password),
|
||||
"IsAdmin": IsAdmin,
|
||||
"Colour": Colour,
|
||||
}
|
||||
SaveUsers(Users)
|
||||
|
||||
|
||||
def UpdateUserColour(Username: str, Colour: str) -> None:
|
||||
if Colour not in UserColourOptions:
|
||||
raise ValueError(f"Invalid colour: {Colour}")
|
||||
|
||||
Users = LoadUsers()
|
||||
|
||||
if Username not in Users:
|
||||
raise ValueError("User not found.")
|
||||
|
||||
Users[Username]["Colour"] = Colour
|
||||
SaveUsers(Users)
|
||||
|
||||
|
||||
def VerifyLogin(Username: str, Password: str) -> dict[str, Any] | None:
|
||||
User = GetUser(Username)
|
||||
|
||||
if User is None:
|
||||
return None
|
||||
|
||||
if not VerifyPassword(Password, User["PasswordHash"]):
|
||||
return None
|
||||
|
||||
return {
|
||||
"Username": Username,
|
||||
"IsAdmin": User.get("IsAdmin", False),
|
||||
"Colour": User.get("Colour", DefaultUserColour),
|
||||
}
|
||||
|
||||
|
||||
def ListUsers() -> list[dict[str, Any]]:
|
||||
Users = LoadUsers()
|
||||
return [
|
||||
{
|
||||
"Username": Name,
|
||||
"IsAdmin": Data.get("IsAdmin", False),
|
||||
"Colour": Data.get("Colour", DefaultUserColour),
|
||||
}
|
||||
for Name, Data in Users.items()
|
||||
]
|
||||
|
||||
|
||||
def SaveAvatar(Username: str, FileBytes: bytes, Extension: str) -> None:
|
||||
Extension = Extension.lower()
|
||||
|
||||
if Extension not in AllowedAvatarExtensions:
|
||||
raise ValueError(f"Unsupported image type: {Extension}")
|
||||
|
||||
Directory = GetAvatarDirectory()
|
||||
Directory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Remove any previous avatar for this user under a different extension.
|
||||
for ExistingPath in Directory.glob(f"{Username}.*"):
|
||||
ExistingPath.unlink()
|
||||
|
||||
AvatarPath = Directory / f"{Username}{Extension}"
|
||||
AvatarPath.write_bytes(FileBytes)
|
||||
|
||||
|
||||
def GetAvatarPath(Username: str) -> Path | None:
|
||||
Directory = GetAvatarDirectory()
|
||||
|
||||
if not Directory.is_dir():
|
||||
return None
|
||||
|
||||
Matches = sorted(Directory.glob(f"{Username}.*"))
|
||||
return Matches[0] if Matches else None
|
||||
Reference in new issue
Block a user