diff --git a/matemat/db/facade.py b/matemat/db/facade.py index 91365d3..30fafbf 100644 --- a/matemat/db/facade.py +++ b/matemat/db/facade.py @@ -232,7 +232,7 @@ class MatematDatabase(object): 'tkhash': tkhash }) - def change_user(self, user: User, agent: User, **kwargs)\ + def change_user(self, user: User, agent: Optional[User], **kwargs)\ -> None: """ Commit changes to the user in the database. If writing the requested changes succeeded, the values are updated @@ -240,7 +240,7 @@ class MatematDatabase(object): the ID field in the provided user object. :param user: The user object to update and to identify the requested user by. - :param agent: The user that is performing the change. + :param agent: The user that is performing the change. Must be present if the balance is changed. :param kwargs: The properties to change. :raises DatabaseConsistencyError: If the user represented by the object does not exist. """ @@ -257,6 +257,8 @@ class MatematDatabase(object): raise DatabaseConsistencyError(f'User with ID {user.id} does not exist') oldbalance: int = row[0] if balance != oldbalance: + if agent is None: + raise ValueError('agent must not be None for a balance change') c.execute(''' INSERT INTO transactions (user_id, value, old_balance) VALUES (:user_id, :value, :old_balance) diff --git a/matemat/webserver/pagelets/admin.py b/matemat/webserver/pagelets/admin.py index 4cc112c..b84db3c 100644 --- a/matemat/webserver/pagelets/admin.py +++ b/matemat/webserver/pagelets/admin.py @@ -1,4 +1,3 @@ - from typing import Any, Dict, Union import os @@ -18,71 +17,116 @@ def admin(method: str, headers: Dict[str, str], config: Dict[str, str]) \ -> Union[str, bytes, PageletResponse]: + """ + The admin panel, shows a user's own settings. Additionally, for administrators, settings to modify other users and + products are shown. + """ + # If no user is logged in, redirect to the login page if 'authentication_level' not in session_vars or 'authenticated_user' not in session_vars: return RedirectResponse('/login') authlevel: int = session_vars['authentication_level'] uid: int = session_vars['authenticated_user'] + # Show a 403 Forbidden error page if no user is logged in (0) or a user logged in via touchkey (1) if authlevel < 2: raise HttpException(403) + # Connect to the database with MatematDatabase(config['DatabaseFile']) as db: + # Fetch the authenticated user user = db.get_user(uid) + # If the POST request contains a "change" parameter, delegate the change handling to the function below if method == 'POST' and 'change' in args: handle_change(args, user, db, config) + # If the POST request contains an "adminchange" parameter, delegate the change handling to the function below elif method == 'POST' and 'adminchange' in args and user.is_admin: handle_admin_change(args, db, config) + # Fetch all existing users and products from the database users = db.list_users() products = db.list_products() + # Render the "Admin/Settings" page return TemplateResponse('admin.html', authuser=user, authlevel=authlevel, users=users, products=products, setupname=config['InstanceName']) def handle_change(args: RequestArguments, user: User, db: MatematDatabase, config: Dict[str, str]) -> None: + """ + Write the changes requested by a user for its own account to the database. + + :param args: The RequestArguments object passed to the pagelet. + :param user: The user to edit. + :param db: The database facade where changes are written to. + :param config: The dictionary of config file entries from the [Pagelets] section. + """ try: + # Read the type of change requested by the user, then switch over it change = str(args.change) + # The user requested a modification of its general account information (username, email) if change == 'account': + # Username and email must be set in the request arguments if 'username' not in args or 'email' not in args: return username = str(args.username) email = str(args.email) + # An empty e-mail field should be interpreted as NULL if len(email) == 0: email = None + # Attempt to update username and e-mail try: - db.change_user(user, name=username, email=email) + db.change_user(user, agent=None, name=username, email=email) except DatabaseConsistencyError: - pass + return + # The user requested a password change elif change == 'password': + # The old password and 2x the new password must be present if 'oldpass' not in args or 'newpass' not in args or 'newpass2' not in args: return + # Read the passwords from the request arguments oldpass = str(args.oldpass) newpass = str(args.newpass) newpass2 = str(args.newpass2) + # The two instances of the new password must match if newpass != newpass2: raise ValueError('New passwords don\'t match') + # Write the new password to the database db.change_password(user, oldpass, newpass) + # The user requested a touchkey change elif change == 'touchkey': + # The touchkey must be present if 'touchkey' not in args: return + # Read the touchkey from the request arguments touchkey = str(args.touchkey) + # An empty touchkey field should set the touchkey to NULL (disable touchkey login) if len(touchkey) == 0: touchkey = None + # Write the new touchkey to the database db.change_touchkey(user, '', touchkey, verify_password=False) + # The user requested an avatar change elif change == 'avatar': + # The new avatar field must be present if 'avatar' not in args: return + # Read the raw image data from the request avatar = bytes(args.avatar) + # Only process the image, if its size is more than zero. Zero size means no new image was uploaded + if len(avatar) == 0: + return + # Detect the MIME type filemagic: magic.FileMagic = magic.detect_from_content(avatar) + # Currently, only image/png is supported, don't process any other formats if filemagic.mime_type != 'image/png': # TODO: Optionally convert to png return + # Create the absolute path of the upload directory abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/users/') os.makedirs(abspath, exist_ok=True) + # Write the image to the file with open(os.path.join(abspath, f'{user.id}.png'), 'wb') as f: f.write(avatar) @@ -91,46 +135,76 @@ def handle_change(args: RequestArguments, user: User, db: MatematDatabase, confi def handle_admin_change(args: RequestArguments, db: MatematDatabase, config: Dict[str, str]): + """ + Write the changes requested by an admin for users of products. + + :param args: The RequestArguments object passed to the pagelet. + :param db: The database facade where changes are written to. + :param config: The dictionary of config file entries from the [Pagelets] section. + """ try: + # Read the type of change requested by the admin, then switch over it change = str(args.adminchange) + # The user requested to create a new user if change == 'newuser': + # Only create a new user if all required properties of the user are present in the request arguments if 'username' not in args or 'email' not in args or 'password' not in args: return + # Read the properties from the request arguments username = str(args.username) email = str(args.email) + # An empty e-mail field should be interpreted as NULL if len(email) == 0: email = None password = str(args.password) is_member = 'ismember' in args is_admin = 'isadmin' in args + # Create the user in the database db.create_user(username, password, email, member=is_member, admin=is_admin) + # The user requested to create a new product elif change == 'newproduct': + # Only create a new product if all required properties of the product are present in the request arguments if 'name' not in args or 'pricemember' not in args or 'pricenonmember' not in args: return + # Read the properties from the request arguments name = str(args.name) price_member = int(str(args.pricemember)) price_non_member = int(str(args.pricenonmember)) + # Create the user in the database newproduct = db.create_product(name, price_member, price_non_member) + # If a new product image was uploaded, process it if 'image' in args: + # Read the raw image data from the request image = bytes(args.image) + # Only process the image, if its size is more than zero. Zero size means no new image was uploaded + if len(image) == 0: + return + # Detect the MIME type filemagic: magic.FileMagic = magic.detect_from_content(image) + # Currently, only image/png is supported, don't process any other formats if filemagic.mime_type != 'image/png': # TODO: Optionally convert to png return - if len(image) > 0: - abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/products/') - os.makedirs(abspath, exist_ok=True) - with open(os.path.join(abspath, f'{newproduct.id}.png'), 'wb') as f: - f.write(image) + # Create the absolute path of the upload directory + abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/products/') + os.makedirs(abspath, exist_ok=True) + # Write the image to the file + with open(os.path.join(abspath, f'{newproduct.id}.png'), 'wb') as f: + f.write(image) + # The user requested to restock a product elif change == 'restock': + # Only restock a product if all required properties are present in the request arguments if 'productid' not in args or 'amount' not in args: return + # Read the properties from the request arguments productid = int(str(args.productid)) amount = int(str(args.amount)) + # Fetch the product to restock from the database product = db.get_product(productid) + # Write the new stock count to the database db.restock(product, amount) except UnicodeDecodeError: diff --git a/matemat/webserver/pagelets/buy.py b/matemat/webserver/pagelets/buy.py index 3b68cee..d08bafa 100644 --- a/matemat/webserver/pagelets/buy.py +++ b/matemat/webserver/pagelets/buy.py @@ -12,13 +12,22 @@ def buy(method: str, headers: Dict[str, str], config: Dict[str, str]) \ -> Union[str, bytes, PageletResponse]: + """ + The purchasing mechanism. Called by the user clicking an item on the product list. + """ + # If no user is logged in, redirect to the main page, as a purchase must always be bound to a user if 'authenticated_user' not in session_vars: return RedirectResponse('/') + # Connect to the database with MatematDatabase(config['DatabaseFile']) as db: + # Fetch the authenticated user from the database uid: int = session_vars['authenticated_user'] user = db.get_user(uid) + # Read the product from the database, identified by the product ID passed as request argument if 'pid' in args: pid = int(str(args.pid)) product = db.get_product(pid) + # Create a consumption entry for the (user, product) combination db.increment_consumption(user, product) - return RedirectResponse('/') + # Redirect to the main page (where this request should have come from) + return RedirectResponse('/') diff --git a/matemat/webserver/pagelets/deposit.py b/matemat/webserver/pagelets/deposit.py index 60e6cf0..9e38a0b 100644 --- a/matemat/webserver/pagelets/deposit.py +++ b/matemat/webserver/pagelets/deposit.py @@ -12,12 +12,21 @@ def deposit(method: str, headers: Dict[str, str], config: Dict[str, str]) \ -> Union[str, bytes, PageletResponse]: + """ + The cash depositing mechanism. Called by the user submitting a deposit from the product list. + """ + # If no user is logged in, redirect to the main page, as a deposit must always be bound to a user if 'authenticated_user' not in session_vars: return RedirectResponse('/') + # Connect to the database with MatematDatabase(config['DatabaseFile']) as db: + # Fetch the authenticated user from the database uid: int = session_vars['authenticated_user'] user = db.get_user(uid) if 'n' in args: + # Read the amount of cash to deposit from the request arguments n = int(str(args.n)) + # Write the deposit to the database db.deposit(user, n) - return RedirectResponse('/') + # Redirect to the main page (where this request should have come from) + return RedirectResponse('/') diff --git a/matemat/webserver/pagelets/login.py b/matemat/webserver/pagelets/login.py index d8c7f0a..be4ac4b 100644 --- a/matemat/webserver/pagelets/login.py +++ b/matemat/webserver/pagelets/login.py @@ -1,4 +1,3 @@ - from typing import Any, Dict, Union from matemat.exceptions import AuthenticationError, HttpException @@ -13,20 +12,34 @@ def login_page(method: str, args: RequestArguments, session_vars: Dict[str, Any], headers: Dict[str, str], - config: Dict[str, str])\ + config: Dict[str, str]) \ -> Union[bytes, str, PageletResponse]: + """ + The password login mechanism. If called via GET, render the UI template; if called via POST, attempt to log in with + the provided credentials (username and passsword). + """ + # If a user is already logged in, simply redirect to the main page, showing the product list if 'authenticated_user' in session_vars: return RedirectResponse('/') + # If requested via HTTP GET, render the login page showing the login UI if method == 'GET': return TemplateResponse('login.html', setupname=config['InstanceName']) + # If requested via HTTP POST, read the request arguments and attempt to log in with the provided credentials elif method == 'POST': + # Connect to the database with MatematDatabase(config['DatabaseFile']) as db: try: + # Read the request arguments and attempt to log in with them user: User = db.login(str(args.username), str(args.password)) except AuthenticationError: + # Reload the touchkey login page on failure return RedirectResponse('/login') - session_vars['authenticated_user'] = user.id - session_vars['authentication_level'] = 2 + # Set the user ID session variable + session_vars['authenticated_user'] = user.id + # Set the authlevel session variable (0 = none, 1 = touchkey, 2 = password login) + session_vars['authentication_level'] = 2 + # Redirect to the main page, showing the product list return RedirectResponse('/') + # If neither GET nor POST was used, show a 405 Method Not Allowed error page raise HttpException(405) diff --git a/matemat/webserver/pagelets/logout.py b/matemat/webserver/pagelets/logout.py index fb33437..5d8f289 100644 --- a/matemat/webserver/pagelets/logout.py +++ b/matemat/webserver/pagelets/logout.py @@ -1,4 +1,3 @@ - from typing import Any, Dict, Union from matemat.webserver import pagelet, RequestArguments, PageletResponse, RedirectResponse @@ -10,9 +9,15 @@ def logout(method: str, args: RequestArguments, session_vars: Dict[str, Any], headers: Dict[str, str], - config: Dict[str, str])\ + config: Dict[str, str]) \ -> Union[bytes, str, PageletResponse]: + """ + The logout mechanism, clearing the authentication values in the session storage. + """ + # Remove the authenticated user ID from the session storage, if any if 'authenticated_user' in session_vars: del session_vars['authenticated_user'] + # Reset the authlevel session variable (0 = none, 1 = touchkey, 2 = password login) session_vars['authentication_level'] = 0 + # Redirect to the main page, showing the user list return RedirectResponse('/') diff --git a/matemat/webserver/pagelets/main.py b/matemat/webserver/pagelets/main.py index 368ff83..81f2d7d 100644 --- a/matemat/webserver/pagelets/main.py +++ b/matemat/webserver/pagelets/main.py @@ -1,4 +1,3 @@ - from typing import Any, Dict, Union from matemat.webserver import pagelet, RequestArguments, PageletResponse, TemplateResponse @@ -11,18 +10,27 @@ def main_page(method: str, args: RequestArguments, session_vars: Dict[str, Any], headers: Dict[str, str], - config: Dict[str, str])\ + config: Dict[str, str]) \ -> Union[bytes, str, PageletResponse]: + """ + The main page, showing either the user list (if no user is logged in) or the product list (if a user is logged in). + """ with MatematDatabase(config['DatabaseFile']) as db: + # Check whether a user is logged in if 'authenticated_user' in session_vars: + # Fetch the user id and authentication level (touchkey vs password) from the session storage uid: int = session_vars['authenticated_user'] authlevel: int = session_vars['authentication_level'] + # Fetch the user object from the database (for name display, price calculation and admin check) user = db.get_user(uid) + # Fetch the list of products to display products = db.list_products() + # Prepare a response with a jinja2 template return TemplateResponse('productlist.html', authuser=user, products=products, authlevel=authlevel, setupname=config['InstanceName']) else: + # If no user is logged in, fetch the list of users and render the userlist template users = db.list_users() return TemplateResponse('userlist.html', users=users, setupname=config['InstanceName']) diff --git a/matemat/webserver/pagelets/modproduct.py b/matemat/webserver/pagelets/modproduct.py index e3416d9..ef6acbd 100644 --- a/matemat/webserver/pagelets/modproduct.py +++ b/matemat/webserver/pagelets/modproduct.py @@ -1,4 +1,3 @@ - from typing import Any, Dict, Union import os @@ -19,64 +18,102 @@ def modproduct(method: str, headers: Dict[str, str], config: Dict[str, str]) \ -> Union[str, bytes, PageletResponse]: + """ + The product modification page available from the admin panel. + """ + # If no user is logged in, redirect to the login page if 'authentication_level' not in session_vars or 'authenticated_user' not in session_vars: return RedirectResponse('/login') authlevel: int = session_vars['authentication_level'] auth_uid: int = session_vars['authenticated_user'] + # Show a 403 Forbidden error page if no user is logged in (0) or a user logged in via touchkey (1) if authlevel < 2: raise HttpException(403) + # Connect to the database with MatematDatabase(config['DatabaseFile']) as db: + # Fetch the authenticated user authuser = db.get_user(auth_uid) if not authuser.is_admin: + # Show a 403 Forbidden error page if the user is not an admin raise HttpException(403) if 'productid' not in args: + # Show a 400 Bad Request error page if no product to edit was specified + # (should never happen during normal operation) raise HttpException(400, '"productid" argument missing') + # Fetch the product to modify from the database modproduct_id = int(str(args.productid)) product = db.get_product(modproduct_id) + # If the request contains a "change" parameter, delegate the change handling to the function below if 'change' in args: handle_change(args, product, db, config) + # If the product was deleted, redirect back to the admin page, as there is nothing to edit any more if str(args.change) == 'del': return RedirectResponse('/admin') + # Render the "Modify Product" page return TemplateResponse('modproduct.html', authuser=authuser, product=product, authlevel=authlevel, setupname=config['InstanceName']) def handle_change(args: RequestArguments, product: Product, db: MatematDatabase, config: Dict[str, str]) -> None: + """ + Write the changes requested by an admin to the database. + + :param args: The RequestArguments object passed to the pagelet. + :param product: The product to edit. + :param db: The database facade where changes are written to. + :param config: The dictionary of config file entries from the [Pagelets] section. + """ + # Read the type of change requested by the admin, then switch over it change = str(args.change) + # Admin requested deletion of the product if change == 'del': + # Delete the product from the database db.delete_product(product) + # Delete the product image, if it exists try: abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/products/') os.remove(os.path.join(abspath, f'{product.id}.png')) except FileNotFoundError: pass + # Admin requested update of the product details elif change == 'update': + # Only write a change if all properties of the product are present in the request arguments if 'name' not in args or 'pricemember' not in args or 'pricenonmember' not in args or 'stock' not in args: return + # Read the properties from the request arguments name = str(args.name) price_member = parse_chf(str(args.pricemember)) price_non_member = parse_chf(str(args.pricenonmember)) stock = int(str(args.stock)) + # Attempt to write the changes to the database try: db.change_product(product, name=name, price_member=price_member, price_non_member=price_non_member, stock=stock) except DatabaseConsistencyError: - pass + return + # If a new product image was uploaded, process it if 'image' in args: + # Read the raw image data from the request image = bytes(args.image) + # Only process the image, if its size is more than zero. Zero size means no new image was uploaded + if len(image) == 0: + return + # Detect the MIME type filemagic: magic.FileMagic = magic.detect_from_content(image) + # Currently, only image/png is supported, don't process any other formats if filemagic.mime_type != 'image/png': # TODO: Optionally convert to png return - if len(image) > 0: - abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/products/') - os.makedirs(abspath, exist_ok=True) - with open(os.path.join(abspath, f'{product.id}.png'), 'wb') as f: - f.write(image) + # Create the absolute path of the upload directory + abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/products/') + os.makedirs(abspath, exist_ok=True) + # Write the image to the file + with open(os.path.join(abspath, f'{product.id}.png'), 'wb') as f: + f.write(image) diff --git a/matemat/webserver/pagelets/moduser.py b/matemat/webserver/pagelets/moduser.py index 043b3f4..0b535f1 100644 --- a/matemat/webserver/pagelets/moduser.py +++ b/matemat/webserver/pagelets/moduser.py @@ -1,4 +1,3 @@ - from typing import Any, Dict, Union import os @@ -19,69 +18,113 @@ def moduser(method: str, headers: Dict[str, str], config: Dict[str, str]) \ -> Union[str, bytes, PageletResponse]: + """ + The user modification page available from the admin panel. + """ + # If no user is logged in, redirect to the login page if 'authentication_level' not in session_vars or 'authenticated_user' not in session_vars: return RedirectResponse('/login') authlevel: int = session_vars['authentication_level'] auth_uid: int = session_vars['authenticated_user'] + # Show a 403 Forbidden error page if no user is logged in (0) or a user logged in via touchkey (1) if authlevel < 2: raise HttpException(403) + # Connect to the database with MatematDatabase(config['DatabaseFile']) as db: + # Fetch the authenticated user authuser = db.get_user(auth_uid) if not authuser.is_admin: + # Show a 403 Forbidden error page if the user is not an admin raise HttpException(403) if 'userid' not in args: + # Show a 400 Bad Request error page if no users to edit was specified + # (should never happen during normal operation) raise HttpException(400, '"userid" argument missing') + # Fetch the user to modify from the database moduser_id = int(str(args.userid)) user = db.get_user(moduser_id) + # If the request contains a "change" parameter, delegate the change handling to the function below if 'change' in args: - handle_change(args, user, db, config) + handle_change(args, user, authuser, db, config) + # If the user was deleted, redirect back to the admin page, as there is nothing to edit any more if str(args.change) == 'del': return RedirectResponse('/admin') + # Render the "Modify User" page return TemplateResponse('moduser.html', authuser=authuser, user=user, authlevel=authlevel, setupname=config['InstanceName']) -def handle_change(args: RequestArguments, user: User, db: MatematDatabase, config: Dict[str, str]) -> None: +def handle_change(args: RequestArguments, user: User, authuser: User, db: MatematDatabase, config: Dict[str, str]) \ + -> None: + """ + Write the changes requested by an admin to the database. + + :param args: The RequestArguments object passed to the pagelet. + :param user: The user to edit. + :param authuser: The user performing the modification. + :param db: The database facade where changes are written to. + :param config: The dictionary of config file entries from the [Pagelets] section. + """ + # Read the type of change requested by the admin, then switch over it change = str(args.change) + # Admin requested deletion of the user if change == 'del': + # Delete the user from the database db.delete_user(user) + # Delete the user's avatar, if it exists try: abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/users/') os.remove(os.path.join(abspath, f'{user.id}.png')) except FileNotFoundError: pass + # Admin requested update of the user's details elif change == 'update': + # Only write a change if all properties of the user are present in the request arguments if 'username' not in args or 'email' not in args or 'password' not in args or 'balance' not in args: return + # Read the properties from the request arguments username = str(args.username) email = str(args.email) password = str(args.password) balance = parse_chf(str(args.balance)) is_member = 'ismember' in args is_admin = 'isadmin' in args + # An empty e-mail field should be interpreted as NULL if len(email) == 0: email = None + # Attempt to write the changes to the database try: - db.change_user(user, name=username, email=email, is_member=is_member, is_admin=is_admin, balance=balance) + # If a password was entered, replace the password in the database + if len(password) > 0: + db.change_password(user, '', password, verify_password=False) + # Write the user detail changes + db.change_user(user, agent=authuser, name=username, email=email, is_member=is_member, is_admin=is_admin, + balance=balance) except DatabaseConsistencyError: - pass - if len(password) > 0: - db.change_password(user, '', password, verify_password=False) + return + # If a new avatar was uploaded, process it if 'avatar' in args: + # Read the raw image data from the request avatar = bytes(args.avatar) + # Only process the image, if its size is more than zero. Zero size means no new image was uploaded + if len(avatar) == 0: + return + # Detect the MIME type filemagic: magic.FileMagic = magic.detect_from_content(avatar) + # Currently, only image/png is supported, don't process any other formats if filemagic.mime_type != 'image/png': # TODO: Optionally convert to png return - if len(avatar) > 0: - abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/users/') - os.makedirs(abspath, exist_ok=True) - with open(os.path.join(abspath, f'{user.id}.png'), 'wb') as f: - f.write(avatar) + # Create the absolute path of the upload directory + abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/users/') + os.makedirs(abspath, exist_ok=True) + # Write the image to the file + with open(os.path.join(abspath, f'{user.id}.png'), 'wb') as f: + f.write(avatar) diff --git a/matemat/webserver/pagelets/touchkey.py b/matemat/webserver/pagelets/touchkey.py index 253e94b..69f5b8e 100644 --- a/matemat/webserver/pagelets/touchkey.py +++ b/matemat/webserver/pagelets/touchkey.py @@ -1,4 +1,3 @@ - from typing import Any, Dict, Union from matemat.exceptions import AuthenticationError, HttpException @@ -13,21 +12,35 @@ def touchkey_page(method: str, args: RequestArguments, session_vars: Dict[str, Any], headers: Dict[str, str], - config: Dict[str, str])\ + config: Dict[str, str]) \ -> Union[bytes, str, PageletResponse]: + """ + The touchkey login mechanism. If called via GET, render the UI template; if called via POST, attempt to log in with + the provided credentials (username and touchkey). + """ + # If a user is already logged in, simply redirect to the main page, showing the product list if 'authenticated_user' in session_vars: return RedirectResponse('/') + # If requested via HTTP GET, render the login page showing the touchkey UI if method == 'GET': return TemplateResponse('touchkey.html', username=str(args.username), uid=int(str(args.uid)), setupname=config['InstanceName']) + # If requested via HTTP POST, read the request arguments and attempt to log in with the provided credentials elif method == 'POST': + # Connect to the database with MatematDatabase(config['DatabaseFile']) as db: try: + # Read the request arguments and attempt to log in with them user: User = db.login(str(args.username), touchkey=str(args.touchkey)) except AuthenticationError: + # Reload the touchkey login page on failure return RedirectResponse(f'/touchkey?uid={str(args.uid)}&username={str(args.username)}') - session_vars['authenticated_user'] = user.id - session_vars['authentication_level'] = 1 + # Set the user ID session variable + session_vars['authenticated_user'] = user.id + # Set the authlevel session variable (0 = none, 1 = touchkey, 2 = password login) + session_vars['authentication_level'] = 1 + # Redirect to the main page, showing the product list return RedirectResponse('/') + # If neither GET nor POST was used, show a 405 Method Not Allowed error page raise HttpException(405) diff --git a/templates/admin.html b/templates/admin.html index 8a1e29c..7343c6d 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -1,22 +1,25 @@ {% extends "base.html" %} {% block header %} -{% if authuser.is_admin %} -

Administration

-{% else %} -

Settings

-{% endif %} -{{ super() }} + {# If the logged in user is an administrator, call the title "Administration", otherwise "Settings" #} + {% if authuser.is_admin %} +

Administration

+ {% else %} +

Settings

+ {% endif %} + {{ super() }} {% endblock %} {% block main %} -{% include "admin_all.html" %} + {# Always show the settings a user can edit for itself #} + {% include "admin_all.html" %} -{% if authuser.is_admin %} -{% include "admin_restricted.html" %} -{% endif %} + {# Only show the "restricted" section if the user is an admin #} + {% if authuser.is_admin %} + {% include "admin_restricted.html" %} + {% endif %} -{{ super() }} + {{ super() }} {% endblock %} diff --git a/templates/base.html b/templates/base.html index 8a48454..d41a1d0 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,39 +1,55 @@ + {% block head %} - {{ setupname|safe }} - + {# Show the setup name, as set in the config file, as tab title. Don't escape HTML entities. #} + {{ setupname|safe }} + {% endblock %} + +
{% block header %} - Home - {% if authlevel|default(0) > 1 %} - {% if authuser is defined %} - {% if authuser.is_admin %} - Administration - {% else %} - Settings + {# Always show a link to the home page, either a list of users or of products. #} + Home + {# Show a link to the settings, if a user logged in via password (authlevel 2). #} + {% if authlevel|default(0) > 1 %} + {% if authuser is defined %} + {# If a user is an admin, call the link "Administration". Otherwise, call it "Settings". #} + {% if authuser.is_admin %} + Administration + {% else %} + Settings + {% endif %} {% endif %} {% endif %} - {% endif %} {% endblock %}
+
- {% block main %}{% endblock %} + {% block main %} + {# Here be content. #} + {% endblock %}
+ + diff --git a/templates/login.html b/templates/login.html index 5210253..09f1fd6 100644 --- a/templates/login.html +++ b/templates/login.html @@ -1,23 +1,23 @@ {% extends "base.html" %} - {% block header %} -

Welcome

-{{ super() }} +

Welcome

+ {{ super() }} {% endblock %} {% block main %} -
- -
+ {# Show a username/password login form #} + + +
- -
+ +
- -
+ + -{{ super() }} + {{ super() }} {% endblock %} diff --git a/templates/productlist.html b/templates/productlist.html index c3479af..b763bd5 100644 --- a/templates/productlist.html +++ b/templates/productlist.html @@ -1,39 +1,42 @@ {% extends "base.html" %} {% block header %} -

Welcome, {{ authuser.name }}

- -{{ super() }} - + {# Show the username. #} +

Welcome, {{ authuser.name }}

+ {{ super() }} {% endblock %} {% block main %} -Your balance: {{ authuser.balance|chf }} -
-Deposit CHF 1
-Deposit CHF 10
+ {# Show the users current balance #} + Your balance: {{ authuser.balance|chf }} +
+ {# Links to deposit two common amounts of cash. TODO: Will be replaced by a nicer UI later (#20) #} + Deposit CHF 1
+ Deposit CHF 10
-{% for product in products %} -
- - {{ product.name }} - Price: - {% if authuser.is_member %} - {{ product.price_member|chf }} - {% else %} - {{ product.price_non_member|chf }} - {% endif %} - ; Stock: {{ product.stock }}
-
- Picture of {{ product.name }} + {% for product in products %} + {# Show an item per product, consisting of the name, image, price and stock, triggering a purchase on click #} + - -
-{% endfor %} -
-Logout + {% endfor %} +
+ {# Logout link #} + Logout -{{ super() }} + {{ super() }} {% endblock %} diff --git a/templates/userlist.html b/templates/userlist.html index 15bd1dd..cc42af8 100644 --- a/templates/userlist.html +++ b/templates/userlist.html @@ -1,22 +1,29 @@ {% extends "base.html" %} {% block header %} -

{{ setupname }}

-{{ super() }} + {# Show the setup name, as set in the config file, as page title. Don't escape HTML entities. #} +

{{ setupname|safe }}

+ {{ super() }} {% endblock %} {% block main %} -{% for user in users %} -
- - {{ user.name }}
-
-{% endfor %} -
-Password-based login -{{ super() }} + {% endfor %} + +
+ {# Link to the password login #} + Password login + + {{ super() }} + {% endblock %}