diff --git a/lumi2/static/assets/default_user_icon.jpg b/lumi2/static/assets/default_user_icon.jpg new file mode 100644 index 0000000..f5bc265 Binary files /dev/null and b/lumi2/static/assets/default_user_icon.jpg differ diff --git a/lumi2/static/assets/default_user_icon.svg b/lumi2/static/assets/default_user_icon.svg new file mode 100644 index 0000000..4bc4ef2 --- /dev/null +++ b/lumi2/static/assets/default_user_icon.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + ? + + diff --git a/lumi2/usermodel.py b/lumi2/usermodel.py index 3553b78..0a6ea98 100644 --- a/lumi2/usermodel.py +++ b/lumi2/usermodel.py @@ -1,10 +1,12 @@ """Provides the application-internal class-based models for users and groups.""" from string import ascii_lowercase, ascii_uppercase, digits, whitespace -from base64 import b64decode +from base64 import b64encode, b64decode +import hashlib from binascii import Error as Base64DecodeError -from PIL.Image import Image +from PIL import Image +from flask import current_app class User: """Class model for a user. @@ -187,7 +189,7 @@ class User: @staticmethod - def is_valid_picture(input_image: Image) -> bool: + def is_valid_picture(input_image: Image.Image) -> bool: """Checks whether the input image is a valid Image object. TBD - unsure which formats and filesizes to allow here. @@ -208,18 +210,64 @@ class User: If input_image is not of type PIL.Image. """ - if not isinstance(input_image, Image): + if not isinstance(input_image, Image.Image): raise TypeError(f"Expected a PIL Image but got: '{type(input_image)}'.") - # TODO implement + # TODO implement some integrity checks + # TODO implement some filesize restrictions return True + @staticmethod + def generate_password_hash(password: str) -> str: + """Generates a base64-encoded SHA512 hash of the input string. + + Parameters + ---------- + password : str + The plaintext password for which to generate a hash. + + Returns + ------- + str + A base64-encoded SHA512 hash digest of the input string. + + Raises + ------ + TypeError + If the input is not of type string. + ValueError + If the input string is empty. + """ + + if not isinstance(password, str): + raise TypeError(f"Expected a string but got: '{type(password)}'.") + if not len(password): + raise ValueError("Input string cannot be empty.") + + hash_bytes = hashlib.sha512() + hash_bytes.update(bytes(password, "UTF-8")) + return b64encode(hash_bytes.digest()).decode("ASCII") + + + def _get_default_picture() -> Image.Image: + """Returns the default user picture as a PIL Image object. + + Returns + ------- + PIL.Image.Image + The default user profile picture. + """ + + image_path = f"{current_app.static_folder}/assets/default_user_icon.jpg" + return Image.open(image_path) + + def __init__( self, username: str, password_hash: str, email: str, - first_name: str, last_name: str, display_name: str, - picture: Image, + first_name: str, last_name: str, display_name = None, + picture = None, ): if not User.is_valid_username(username): @@ -234,16 +282,25 @@ class User: raise ValueError(f"Not a valid email address: '{email}'.") self.email = email - for name in [first_name, last_name, display_name]: + for name in [first_name, last_name]: if not User.is_valid_person_name(name): raise ValueError(f"Not a valid name: '{name}'.") self.first_name = first_name self.last_name = last_name - self.display_name = display_name - if not User.is_valid_picture(picture): - raise ValueError(f"Not a valid image: '{picture}'.") - self.picture = picture + if display_name is not None: + if not User.is_valid_person_name(display_name): + raise ValueError(f"Not a valid display name: '{display_name}'.") + self.display_name = display_name + else: + self.display_name = first_name + + if picture is not None: + if not User.is_valid_picture(picture): + raise ValueError(f"Not a valid image: '{picture}'.") + self.picture = picture + else: + self.picture = User._get_default_picture() def __eq__(self, other):