matemat/matemat/webserver/pagelets/settings.py
s3lph 67e2a813d5
All checks were successful
/ test (push) Successful in 1m22s
/ codestyle (push) Successful in 1m6s
/ build_wheel (push) Successful in 2m0s
/ build_debian (push) Successful in 2m32s
feat: redesign ui using bootstrap
feat: split user settings and admin settings
fix: list user tokens in admin user settings
feat!: remove osk, osk should be provided by kiosk browser
2024-12-07 15:53:19 +01:00

178 lines
7.5 KiB
Python

import os
from datetime import datetime, UTC
from io import BytesIO
from shutil import copyfile
import magic
from PIL import Image
from bottle import get, post, abort, redirect, request, FormsDict
from matemat.db import MatematDatabase
from matemat.db.primitives import User, ReceiptPreference
from matemat.exceptions import AuthenticationError, DatabaseConsistencyError
from matemat.util.currency_format import parse_chf
from matemat.webserver import session, template
from matemat.webserver.config import get_app_config, get_stock_provider
from matemat.webserver.template import Notification
@get('/settings')
@post('/settings')
def settings():
"""
The settings panel, shows a user's own settings.
"""
config = get_app_config()
session_id: str = session.start()
# If no user is logged in, redirect to the login page
if not session.has(session_id, 'authentication_level') or not session.has(session_id, 'authenticated_user'):
redirect('/login')
authlevel: int = session.get(session_id, 'authentication_level')
uid: int = session.get(session_id, 'authenticated_user')
# Show a 403 Forbidden error page if no user is logged in (0) or a user logged in via touchkey or token (1)
if authlevel < 2:
abort(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 'change' in request.params:
handle_change(request.params, request.files, user, db)
user = db.get_user(uid)
tokens = db.list_tokens(uid)
# Render the "Settings" page
now = str(int(datetime.now(UTC).timestamp()))
return template.render('settings.html',
authuser=user, authlevel=authlevel, tokens=tokens,
receipt_preference_class=ReceiptPreference, now=now,
setupname=config['InstanceName'], config_smtp_enabled=config['SmtpSendReceipts'])
def handle_change(args: FormsDict, files: FormsDict, user: User, db: MatematDatabase) -> None:
"""
Write the changes requested by a user for its own account to the database.
:param args: The FormsDict object passed to the pagelet.
:param user: The user to edit.
:param db: The database facade where changes are written to.
"""
config = get_app_config()
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)
logout_after_purchase = 'logout_after_purchase' in args
# An empty e-mail field should be interpreted as NULL
if len(email) == 0:
email = None
try:
receipt_pref = ReceiptPreference(int(str(args.receipt_pref)))
except ValueError:
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,
logout_after_purchase=logout_after_purchase)
except DatabaseConsistencyError:
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
try:
db.change_password(user, oldpass, newpass)
except AuthenticationError:
raise ValueError('Old password doesn\'t match')
# 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 added a new token
elif change == 'addtoken':
if 'token' not in args:
return
token = str(args.token)
if len(token) < 6:
return
name = None if 'name' not in args or len(args.name) == 0 else str(args.name)
try:
tokobj = db.add_token(user, token, name)
Notification.success(f'Token {tokobj.name} created successfully')
except DatabaseConsistencyError:
Notification.error('Token already exists', decay=True)
elif change == 'deltoken':
try:
tokid = int(str(request.params.token))
token = db.get_token(tokid)
except Exception as e:
Notification.error('Token not found', decay=True)
return
if token.user_id != user.id:
Notification.error('Token not found', decay=True)
return
try:
db.delete_token(token)
except DatabaseConsistencyError:
Notification.error(f'Failed to delete token {token.name}', decay=True)
Notification.success(f'Token {token.name} removed', decay=True)
# The user requested an avatar change
elif change == 'avatar':
# The new avatar field must be present
if 'avatar' not in files:
return
# Read the raw image data from the request
avatar = files.avatar.file.read()
# 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)
if not filemagic.mime_type.startswith('image/'):
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)
try:
# Parse the image data
image: Image = Image.open(BytesIO(avatar))
# Resize the image to 150x150
image.thumbnail((150, 150), Image.LANCZOS)
# Write the image to the file
image.save(os.path.join(abspath, f'{user.id}.png'), 'PNG')
except OSError:
return
except UnicodeDecodeError:
raise ValueError('an argument not a string')