Merge branch 'staging' into 'master'

0.2.11: Add user signup (has to be enabled via config)

See merge request s3lph/matemat!80
This commit is contained in:
s3lph 2022-07-16 22:14:42 +00:00
commit 5cd245eee4
9 changed files with 158 additions and 8 deletions

View file

@ -1,5 +1,18 @@
# Matemat Changelog # Matemat Changelog
<!-- BEGIN RELEASE v0.2.11 -->
## Version 0.2.11
Feature release
### Changes
<!-- BEGIN CHANGES 0.2.11 -->
- Feature: Permit user signup
<!-- END CHANGES 0.2.11 -->
<!-- END RELEASE v0.2.11 -->
<!-- BEGIN RELEASE v0.2.10 --> <!-- BEGIN RELEASE v0.2.10 -->
## Version 0.2.10 ## Version 0.2.10

View file

@ -1,2 +1,2 @@
__version__ = '0.2.10' __version__ = '0.2.11'

View file

@ -7,6 +7,7 @@ A new pagelet function must be imported here to be automatically loaded when the
from .main import main_page from .main import main_page
from .login import login_page from .login import login_page
from .logout import logout from .logout import logout
from .signup import signup
from .touchkey import touchkey_page from .touchkey import touchkey_page
from .buy import buy from .buy import buy
from .deposit import deposit from .deposit import deposit

View file

@ -163,14 +163,14 @@ def handle_admin_change(args: FormsDict, files: FormsDict, db: MatematDatabase):
# The user requested to create a new user # The user requested to create a new user
if change == 'newuser': if change == 'newuser':
# Only create a new user if all required properties of the user are present in the request arguments # 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: if 'username' not in args or 'password' not in args:
return return
# Read the properties from the request arguments # Read the properties from the request arguments
username = str(args.username) username = str(args.username)
email = str(args.email) email = None
# An empty e-mail field should be interpreted as NULL if 'email' in args and len(args.email) > 0:
if len(email) == 0: # An empty e-mail field should be interpreted as NULL
email = None email = str(args.email)
password = str(args.password) password = str(args.password)
is_member = 'ismember' in args is_member = 'ismember' in args
is_admin = 'isadmin' in args is_admin = 'isadmin' in args

View file

@ -34,4 +34,5 @@ def main_page():
# If no user is logged in, fetch the list of users and render the userlist template # If no user is logged in, fetch the list of users and render the userlist template
users = db.list_users(with_touchkey=True) users = db.list_users(with_touchkey=True)
return template.render('userlist.html', return template.render('userlist.html',
users=users, setupname=config['InstanceName']) users=users, setupname=config['InstanceName'],
signup=(config.get('SignupEnabled', '0') == '1'))

View file

@ -0,0 +1,93 @@
import os
from shutil import copyfile
from io import BytesIO
import magic
from bottle import get, post, redirect, abort, request, FormsDict
from PIL import Image
from matemat.db import MatematDatabase
from matemat.db.primitives import User
from matemat.webserver import template, session
from matemat.webserver.config import get_app_config
def signup_user(args: FormsDict, files: FormsDict, db: MatematDatabase) -> User:
config = get_app_config()
if 'username' not in args or 'password' not in args or 'password2' not in args:
raise ValueError('Required fields missing')
username = str(args.username)
password = str(args.password)
password2 = str(args.password2)
if password != password2:
raise ValueError('Passwords do not match')
email = None
if 'email' in args and len(args.email) > 0:
email = str(args.email)
new_user = db.create_user(username, password, email=email, admin=False, member=False)
# If a touchkey was provided, set it
if 'touchkey' in args and len(args.touchkey) > 0:
touchkey = str(args.touchkey)
db.change_touchkey(new_user, password, touchkey, verify_password=False)
# Finally, set the avatar, if provided
if 'avatar' in files:
avatar = files.avatar.file.read()
if len(avatar) > 0:
filemagic: magic.FileMagic = magic.detect_from_content(avatar)
if filemagic.mime_type.startswith('image/'):
abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/users/')
os.makedirs(abspath, exist_ok=True)
# 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'{new_user.id}.png'), 'PNG')
else:
# If a default avatar is set, copy it to the user's avatar path
# Create the absolute path of the upload directory
abspath: str = os.path.join(os.path.abspath(config['UploadDir']), 'thumbnails/users/')
# Derive the individual paths
default: str = os.path.join(abspath, 'default.png')
userimg: str = os.path.join(abspath, f'{new_user.id}.png')
# Copy the default image, if it exists
if os.path.exists(default):
copyfile(default, userimg, follow_symlinks=True)
return new_user
@get('/signup')
@post('/signup')
def signup():
"""
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).
"""
config = get_app_config()
session_id: str = session.start()
# If signup is not enabled, redirect to the main page
if config.get('SignupEnabled', '0') != '1':
redirect('/')
# If a user is already logged in, simply redirect to the main page, showing the product list
if session.has(session_id, 'authenticated_user'):
redirect('/')
# If requested via HTTP POST, read the request arguments and attempt to create a new user
if request.method == 'POST':
# Connect to the database and create the user
with MatematDatabase(config['DatabaseFile']) as db:
try:
user = signup_user(request.params, request.files, db)
except ValueError as e:
redirect('/signup')
# Set the user ID session variable
session.put(session_id, 'authenticated_user', user.id)
# Set the authlevel session variable (0 = none, 1 = touchkey, 2 = password login)
session.put(session_id, 'authentication_level', 2)
# Redirect to the main page, showing the product list
redirect('/')
elif request.method != 'GET':
abort(405, 'Method not allowed')
return template.render('signup.html',
setupname=config['InstanceName'])

View file

@ -1,5 +1,5 @@
Package: matemat Package: matemat
Version: 0.2.10 Version: 0.2.11
Maintainer: s3lph <account-gitlab-ideynizv@kernelpanic.lol> Maintainer: s3lph <account-gitlab-ideynizv@kernelpanic.lol>
Section: web Section: web
Priority: optional Priority: optional

39
templates/signup.html Normal file
View file

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block header %}
<h1>Signup</h1>
{{ super() }}
{% endblock %}
{% block main %}
{# Show a username/password signup form #}
<form method="post" action="/signup" id="signupform" enctype="multipart/form-data" accept-charset="UTF-8">
<label for="signup-username"><b>Username</b>: </label>
<input id="signup-username" type="text" name="username" required="required"/><br/>
<label for="signup-password"><b>Choose a password</b>: </label>
<input id="signup-password" type="password" name="password" required="required"/><br/>
<label for="signup-password2"><b>Repeat password</b>: </label>
<input id="signup-password2" type="password" name="password2" required="required"/><br/>
<label for="signup-avatar">Upload a profile picture: </label>
<input id="signup-avatar" type="file" name="avatar" accept="image/*" /><br/>
<label for="signup-touchkey">Draw a touchkey (touchscreen login pattern)</label>
<br/>
<svg id="touchkey-svg" width="400" height="400"></svg>
<br/>
<input id="signup-touchkey" type="hidden" name="touchkey" value="" />
<input type="submit" value="Create account">
</form>
<script src="/static/js/touchkey.js" ></script>
<script>
initTouchkey(true, 'touchkey-svg', null, 'signup-touchkey');
</script>
{{ super() }}
{% endblock %}

View file

@ -23,6 +23,9 @@
<br/> <br/>
{# Link to the password login #} {# Link to the password login #}
<a href="/login">Password login</a> <a href="/login">Password login</a>
{% if signup %}
<a href="/signup">Create account</a>
{% endif %}
{{ super() }} {{ super() }}