s3lph
67e2a813d5
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
178 lines
7.5 KiB
Python
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')
|