From d54aa2bc575a545916ebda349adf29c0655de5ea Mon Sep 17 00:00:00 2001 From: s3lph Date: Tue, 9 Apr 2024 22:35:37 +0200 Subject: [PATCH] feat: add option to log out users automatically after completing a purchase --- CHANGELOG.md | 13 +++++ matemat/db/facade.py | 38 ++++++++----- matemat/db/migrations.py | 8 +++ matemat/db/primitives/User.py | 10 +++- matemat/db/schemas.py | 81 +++++++++++++++++++++++++++ matemat/db/wrapper.py | 4 +- matemat/webserver/pagelets/admin.py | 8 ++- matemat/webserver/pagelets/buy.py | 4 ++ matemat/webserver/pagelets/moduser.py | 4 +- templates/admin_all.html | 3 + templates/admin_restricted.html | 3 + templates/moduser.html | 3 + 12 files changed, 158 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19cc617..eb5d524 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Matemat Changelog + +## Version 0.3.10 + +Add option to log out users automatically after completing a purchase + +### Changes + + +- Add option to log out users automatically after completing a purchase + + + + ## Version 0.3.9 diff --git a/matemat/db/facade.py b/matemat/db/facade.py index 522dfd4..9ed5b15 100644 --- a/matemat/db/facade.py +++ b/matemat/db/facade.py @@ -84,7 +84,7 @@ class MatematDatabase(object): users: List[User] = [] with self.db.transaction(exclusive=False) as c: for row in c.execute(''' - SELECT user_id, username, email, is_admin, is_member, balance, receipt_pref + SELECT user_id, username, email, is_admin, is_member, balance, receipt_pref, logout_after_purchase FROM users WHERE touchkey IS NOT NULL OR NOT :must_have_touchkey ORDER BY username COLLATE NOCASE ASC @@ -92,12 +92,13 @@ class MatematDatabase(object): 'must_have_touchkey': with_touchkey }): # Decompose each row and put the values into a User object - user_id, username, email, is_admin, is_member, balance, receipt_p = row + user_id, username, email, is_admin, is_member, balance, receipt_p, logout_after_purchase = row try: receipt_pref: ReceiptPreference = ReceiptPreference(receipt_p) except ValueError: raise DatabaseConsistencyError(f'{receipt_p} is not a valid ReceiptPreference') - users.append(User(user_id, username, balance, email, is_admin, is_member, receipt_pref)) + users.append(User(user_id, username, balance, email, is_admin, is_member, receipt_pref, + logout_after_purchase)) return users def get_user(self, uid: int) -> User: @@ -108,7 +109,7 @@ class MatematDatabase(object): with self.db.transaction(exclusive=False) as c: # Fetch all values to construct the user c.execute(''' - SELECT user_id, username, email, is_admin, is_member, balance, receipt_pref + SELECT user_id, username, email, is_admin, is_member, balance, receipt_pref, logout_after_purchase FROM users WHERE user_id = ? ''', @@ -117,19 +118,20 @@ class MatematDatabase(object): if row is None: raise ValueError(f'No user with user ID {uid} exists.') # Unpack the row and construct the user - user_id, username, email, is_admin, is_member, balance, receipt_p = row + user_id, username, email, is_admin, is_member, balance, receipt_p, logout_after_purchase = row try: receipt_pref: ReceiptPreference = ReceiptPreference(receipt_p) except ValueError: raise DatabaseConsistencyError(f'{receipt_p} is not a valid ReceiptPreference') - return User(user_id, username, balance, email, is_admin, is_member, receipt_pref) + return User(user_id, username, balance, email, is_admin, is_member, receipt_pref, logout_after_purchase) def create_user(self, username: str, password: str, email: Optional[str] = None, admin: bool = False, - member: bool = True) -> User: + member: bool = True, + logout_after_purchase: bool = False) -> User: """ Create a new user. :param username: The name of the new user. @@ -137,6 +139,7 @@ class MatematDatabase(object): :param email: The user's email address, defaults to None. :param admin: Whether the user is an administrator, defaults to False. :param member: Whether the user is a member, defaults to True. + :param logout_after_purchase: Whether the user should be logged out after completing a purchase. :return: A User object representing the created user. :raises ValueError: If a user with the same name already exists. """ @@ -150,8 +153,10 @@ class MatematDatabase(object): raise ValueError(f'A user with the name \'{username}\' already exists.') # Insert the user into the database. c.execute(''' - INSERT INTO users (username, email, password, balance, is_admin, is_member, lastchange, created) - VALUES (:username, :email, :pwhash, 0, :admin, :member, STRFTIME('%s', 'now'), STRFTIME('%s', 'now')) + INSERT INTO users (username, email, password, balance, is_admin, is_member, lastchange, created, + logout_after_purchase) + VALUES (:username, :email, :pwhash, 0, :admin, :member, STRFTIME('%s', 'now'), STRFTIME('%s', 'now'), + logout_after_purchase) ''', { 'username': username, 'email': email, @@ -179,14 +184,15 @@ class MatematDatabase(object): raise ValueError('Exactly one of password and touchkey must be provided') with self.db.transaction(exclusive=False) as c: c.execute(''' - SELECT user_id, username, email, password, touchkey, is_admin, is_member, balance, receipt_pref + SELECT user_id, username, email, password, touchkey, is_admin, is_member, balance, receipt_pref, + logout_after_purchase FROM users WHERE username = ? ''', [username]) row = c.fetchone() if row is None: raise AuthenticationError('User does not exist') - user_id, username, email, pwhash, tkhash, admin, member, balance, receipt_p = row + user_id, username, email, pwhash, tkhash, admin, member, balance, receipt_p, logout_after_purchase = row if password is not None and not compare_digest(crypt.crypt(password, pwhash), pwhash): raise AuthenticationError('Password mismatch') elif touchkey is not None \ @@ -199,7 +205,7 @@ class MatematDatabase(object): receipt_pref: ReceiptPreference = ReceiptPreference(receipt_p) except ValueError: raise DatabaseConsistencyError(f'{receipt_p} is not a valid ReceiptPreference') - return User(user_id, username, balance, email, admin, member, receipt_pref) + return User(user_id, username, balance, email, admin, member, receipt_pref, logout_after_purchase) def change_password(self, user: User, oldpass: str, newpass: str, verify_password: bool = True) -> None: """ @@ -280,6 +286,7 @@ class MatematDatabase(object): balance: int = kwargs['balance'] if 'balance' in kwargs else user.balance balance_reason: Optional[str] = kwargs['balance_reason'] if 'balance_reason' in kwargs else None receipt_pref: ReceiptPreference = kwargs['receipt_pref'] if 'receipt_pref' in kwargs else user.receipt_pref + logout_after_purchase: bool = kwargs['logout_after_purchase'] if 'logout_after_purchase' in kwargs else user.logout_after_purchase with self.db.transaction() as c: c.execute('SELECT balance FROM users WHERE user_id = :user_id', {'user_id': user.id}) row = c.fetchone() @@ -312,7 +319,8 @@ class MatematDatabase(object): is_admin = :is_admin, is_member = :is_member, receipt_pref = :receipt_pref, - lastchange = STRFTIME('%s', 'now') + lastchange = STRFTIME('%s', 'now'), + logout_after_purchase = :logout_after_purchase WHERE user_id = :user_id ''', { 'user_id': user.id, @@ -321,7 +329,8 @@ class MatematDatabase(object): 'balance': balance, 'is_admin': is_admin, 'is_member': is_member, - 'receipt_pref': receipt_pref.value + 'receipt_pref': receipt_pref.value, + 'logout_after_purchase': logout_after_purchase }) # Only update the actual user object after the changes in the database succeeded user.name = name @@ -329,6 +338,7 @@ class MatematDatabase(object): user.balance = balance user.is_admin = is_admin user.is_member = is_member + user.logout_after_purchase = user.logout_after_purchase user.receipt_pref = receipt_pref def delete_user(self, user: User) -> None: diff --git a/matemat/db/migrations.py b/matemat/db/migrations.py index 8617b12..bbdff55 100644 --- a/matemat/db/migrations.py +++ b/matemat/db/migrations.py @@ -276,3 +276,11 @@ def migrate_schema_5_to_6(c: sqlite3.Cursor): ALTER TABLE products ADD COLUMN custom_price INTEGER(1) DEFAULT 0; ''') + + +def migrate_schema_6_to_7(c: sqlite3.Cursor): + # Add custom_price column + c.execute(''' + ALTER TABLE users ADD COLUMN + logout_after_purchase INTEGER(1) DEFAULT 0; + ''') diff --git a/matemat/db/primitives/User.py b/matemat/db/primitives/User.py index ce203d7..6febc40 100644 --- a/matemat/db/primitives/User.py +++ b/matemat/db/primitives/User.py @@ -26,7 +26,8 @@ class User: email: Optional[str] = None, is_admin: bool = False, is_member: bool = False, - receipt_pref: ReceiptPreference = ReceiptPreference.NONE) -> None: + receipt_pref: ReceiptPreference = ReceiptPreference.NONE, + logout_after_purchase: bool = False) -> None: self.id: int = _id self.name: str = name self.balance: int = balance @@ -34,6 +35,7 @@ class User: self.is_admin: bool = is_admin self.is_member: bool = is_member self.receipt_pref: ReceiptPreference = receipt_pref + self.logout_after_purchase: bool = logout_after_purchase def __eq__(self, other) -> bool: if not isinstance(other, User): @@ -44,7 +46,9 @@ class User: self.email == other.email and \ self.is_admin == other.is_admin and \ self.is_member == other.is_member and \ - self.receipt_pref == other.receipt_pref + self.receipt_pref == other.receipt_pref and \ + self.logout_after_purchase == other.logout_after_purchase def __hash__(self) -> int: - return hash((self.id, self.name, self.balance, self.email, self.is_admin, self.is_member, self.receipt_pref)) + return hash((self.id, self.name, self.balance, self.email, self.is_admin, self.is_member, self.receipt_pref, + self.logout_after_purchase)) diff --git a/matemat/db/schemas.py b/matemat/db/schemas.py index 436c102..d5ec75e 100644 --- a/matemat/db/schemas.py +++ b/matemat/db/schemas.py @@ -413,3 +413,84 @@ SCHEMAS[6] = [ ON DELETE SET NULL ON UPDATE CASCADE ); '''] + + +SCHEMAS[7] = [ + ''' + CREATE TABLE users ( + user_id INTEGER PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + email TEXT DEFAULT NULL, + password TEXT NOT NULL, + touchkey TEXT DEFAULT NULL, + is_admin INTEGER(1) NOT NULL DEFAULT 0, + is_member INTEGER(1) NOT NULL DEFAULT 1, + balance INTEGER(8) NOT NULL DEFAULT 0, + lastchange INTEGER(8) NOT NULL DEFAULT 0, + receipt_pref INTEGER(1) NOT NULL DEFAULT 0, + created INTEGER(8) NOT NULL DEFAULT 0, + logout_after_purchase INTEGER(1) DEFAULT 0 + ); + ''', + ''' + CREATE TABLE products ( + product_id INTEGER PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + stock INTEGER(8) DEFAULT 0, + stockable INTEGER(1) DEFAULT 1, + price_member INTEGER(8) NOT NULL, + price_non_member INTEGER(8) NOT NULL, + custom_price INTEGER(1) DEFAULT 0 + ); + ''', + ''' + CREATE TABLE transactions ( -- "superclass" of the following 3 tables + ta_id INTEGER PRIMARY KEY, + user_id INTEGER DEFAULT NULL, + value INTEGER(8) NOT NULL, + old_balance INTEGER(8) NOT NULL, + date INTEGER(8) DEFAULT (STRFTIME('%s', 'now')), + FOREIGN KEY (user_id) REFERENCES users(user_id) + ON DELETE SET NULL ON UPDATE CASCADE + ); + ''', + ''' + CREATE TABLE consumptions ( -- transactions involving buying a product + ta_id INTEGER PRIMARY KEY, + product TEXT NOT NULL, + FOREIGN KEY (ta_id) REFERENCES transactions(ta_id) + ON DELETE CASCADE ON UPDATE CASCADE + ); + ''', + ''' + CREATE TABLE deposits ( -- transactions involving depositing cash + ta_id INTEGER PRIMARY KEY, + FOREIGN KEY (ta_id) REFERENCES transactions(ta_id) + ON DELETE CASCADE ON UPDATE CASCADE + ); + ''', + ''' + CREATE TABLE modifications ( -- transactions involving balance modification by an admin + ta_id INTEGER NOT NULL, + agent TEXT NOT NULL, + reason TEXT DEFAULT NULL, + PRIMARY KEY (ta_id), + FOREIGN KEY (ta_id) REFERENCES transactions(ta_id) + ON DELETE CASCADE ON UPDATE CASCADE + ); + ''', + ''' + CREATE TABLE receipts ( -- receipts sent to the users + receipt_id INTEGER PRIMARY KEY, + user_id INTEGER NOT NULL, + first_ta_id INTEGER DEFAULT NULL, + last_ta_id INTEGER DEFAULT NULL, + date INTEGER(8) DEFAULT (STRFTIME('%s', 'now')), + FOREIGN KEY (user_id) REFERENCES users(user_id) + ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (first_ta_id) REFERENCES transactions(ta_id) + ON DELETE SET NULL ON UPDATE CASCADE, + FOREIGN KEY (last_ta_id) REFERENCES transactions(ta_id) + ON DELETE SET NULL ON UPDATE CASCADE + ); + '''] diff --git a/matemat/db/wrapper.py b/matemat/db/wrapper.py index 8f4354e..1c8695b 100644 --- a/matemat/db/wrapper.py +++ b/matemat/db/wrapper.py @@ -40,7 +40,7 @@ class DatabaseTransaction(object): class DatabaseWrapper(object): - SCHEMA_VERSION = 6 + SCHEMA_VERSION = 7 def __init__(self, filename: str) -> None: self._filename: str = filename @@ -89,6 +89,8 @@ class DatabaseWrapper(object): migrate_schema_4_to_5(c) if from_version <= 5 and to_version >= 6: migrate_schema_5_to_6(c) + if from_version <= 6 and to_version >= 7: + migrate_schema_6_to_7(c) def connect(self) -> None: if self.is_connected(): diff --git a/matemat/webserver/pagelets/admin.py b/matemat/webserver/pagelets/admin.py index 6a73da5..49fc35e 100644 --- a/matemat/webserver/pagelets/admin.py +++ b/matemat/webserver/pagelets/admin.py @@ -75,6 +75,7 @@ def handle_change(args: FormsDict, files: FormsDict, user: User, db: MatematData return username = str(args.username) email = str(args.email) + logout_after_purchase = 'logout_after_purchase' in args # An empty e-mail field should be interpreted as NULL if len(email) == 0: email = None @@ -84,7 +85,8 @@ def handle_change(args: FormsDict, files: FormsDict, user: User, db: MatematData return # Attempt to update username, e-mail and receipt preference try: - db.change_user(user, agent=None, name=username, email=email, receipt_pref=receipt_pref) + db.change_user(user, agent=None, name=username, email=email, receipt_pref=receipt_pref, + logout_after_purchase=logout_after_purchase) except DatabaseConsistencyError: return @@ -176,8 +178,10 @@ def handle_admin_change(args: FormsDict, files: FormsDict, db: MatematDatabase): password = str(args.password) is_member = 'ismember' in args is_admin = 'isadmin' in args + logout_after_purchase = 'logout_after_purchase' in args # Create the user in the database - newuser: User = db.create_user(username, password, email, member=is_member, admin=is_admin) + newuser: User = db.create_user(username, password, email, member=is_member, admin=is_admin, + logout_after_purchase=logout_after_purchase) # If a default avatar is set, copy it to the user's avatar path diff --git a/matemat/webserver/pagelets/buy.py b/matemat/webserver/pagelets/buy.py index 796458d..a01bb45 100644 --- a/matemat/webserver/pagelets/buy.py +++ b/matemat/webserver/pagelets/buy.py @@ -16,6 +16,7 @@ def buy(): # If no user is logged in, redirect to the main page, as a purchase must always be bound to a user if not session.has(session_id, 'authenticated_user'): redirect('/') + authlevel: int = session.get(session_id, 'authentication_level') # Connect to the database with MatematDatabase(config['DatabaseFile']) as db: # Fetch the authenticated user from the database @@ -34,6 +35,9 @@ def buy(): stock_provider = c.get_stock_provider() if stock_provider.needs_update(): stock_provider.update_stock(product, -1) + # Logout user if configured, logged in via touchkey and no price entry input was shown + if user.logout_after_purchase and authlevel < 2 and not product.custom_price: + redirect('/logout') # Redirect to the main page (where this request should have come from) redirect(f'/?lastaction=buy&lastproduct={pid}&lastprice={price}') redirect('/') diff --git a/matemat/webserver/pagelets/moduser.py b/matemat/webserver/pagelets/moduser.py index dd212ab..3251e97 100644 --- a/matemat/webserver/pagelets/moduser.py +++ b/matemat/webserver/pagelets/moduser.py @@ -111,6 +111,7 @@ def handle_change(args: FormsDict, files: FormsDict, user: User, authuser: User, balance_reason = None is_member = 'ismember' in args is_admin = 'isadmin' in args + logout_after_purchase = 'logout_after_purchase' in args # An empty e-mail field should be interpreted as NULL if len(email) == 0: email = None @@ -121,7 +122,8 @@ def handle_change(args: FormsDict, files: FormsDict, user: User, authuser: User, 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, balance_reason=balance_reason, receipt_pref=receipt_pref) + balance=balance, balance_reason=balance_reason, receipt_pref=receipt_pref, + logout_after_purchase=logout_after_purchase) except DatabaseConsistencyError: return # If a new avatar was uploaded, process it diff --git a/templates/admin_all.html b/templates/admin_all.html index 93b688b..e21fbb9 100644 --- a/templates/admin_all.html +++ b/templates/admin_all.html @@ -23,6 +23,9 @@
+ +
+ diff --git a/templates/admin_restricted.html b/templates/admin_restricted.html index b5e64d3..fedfdfb 100644 --- a/templates/admin_restricted.html +++ b/templates/admin_restricted.html @@ -17,6 +17,9 @@
+ +
+ diff --git a/templates/moduser.html b/templates/moduser.html index b71d100..3bfa7b7 100644 --- a/templates/moduser.html +++ b/templates/moduser.html @@ -35,6 +35,9 @@
+ +
+ CHF