Merge branch '23-user-account-bootstrap' into 'staging'
Resolve "User account bootstrap" See merge request s3lph/matemat!32
This commit is contained in:
commit
fd90516125
7 changed files with 113 additions and 7 deletions
2
doc
2
doc
|
@ -1 +1 @@
|
||||||
Subproject commit c3cbf1fdabc81c3e73d831655fe4d9c2d3d8330c
|
Subproject commit 9449a6dc39843969d3b549f05848b5857c23cfa3
|
|
@ -62,9 +62,22 @@ class MatematDatabase(object):
|
||||||
"""
|
"""
|
||||||
return self.db.transaction(exclusive=exclusive)
|
return self.db.transaction(exclusive=exclusive)
|
||||||
|
|
||||||
def list_users(self) -> List[User]:
|
def has_admin_users(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check whether the instance has any admin users configured.
|
||||||
|
|
||||||
|
:return: True if there are admin users, false otherwise.
|
||||||
|
"""
|
||||||
|
with self.db.transaction(exclusive=False) as c:
|
||||||
|
c.execute('SELECT COUNT(user_id) FROM users WHERE is_admin = 1')
|
||||||
|
n, = c.fetchone()
|
||||||
|
return n > 0
|
||||||
|
|
||||||
|
def list_users(self, with_touchkey: bool = False) -> List[User]:
|
||||||
"""
|
"""
|
||||||
Return a list of users in the database.
|
Return a list of users in the database.
|
||||||
|
|
||||||
|
:param with_touchkey: If true, only lists those users that have a touchkey set. Defaults to false.
|
||||||
:return: A list of users.
|
:return: A list of users.
|
||||||
"""
|
"""
|
||||||
users: List[User] = []
|
users: List[User] = []
|
||||||
|
@ -72,7 +85,10 @@ class MatematDatabase(object):
|
||||||
for row in c.execute('''
|
for row in c.execute('''
|
||||||
SELECT user_id, username, email, is_admin, is_member, balance
|
SELECT user_id, username, email, is_admin, is_member, balance
|
||||||
FROM users
|
FROM users
|
||||||
'''):
|
WHERE touchkey IS NOT NULL OR NOT :must_have_touchkey
|
||||||
|
''', {
|
||||||
|
'must_have_touchkey': with_touchkey
|
||||||
|
}):
|
||||||
# Decompose each row and put the values into a User object
|
# Decompose each row and put the values into a User object
|
||||||
user_id, username, email, is_admin, is_member, balance = row
|
user_id, username, email, is_admin, is_member, balance = row
|
||||||
users.append(User(user_id, username, balance, email, is_admin, is_member))
|
users.append(User(user_id, username, balance, email, is_admin, is_member))
|
||||||
|
|
|
@ -4,6 +4,7 @@ import unittest
|
||||||
import crypt
|
import crypt
|
||||||
|
|
||||||
from matemat.db import MatematDatabase
|
from matemat.db import MatematDatabase
|
||||||
|
from matemat.db.primitives import User
|
||||||
from matemat.exceptions import AuthenticationError, DatabaseConsistencyError
|
from matemat.exceptions import AuthenticationError, DatabaseConsistencyError
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,10 +44,14 @@ class DatabaseTest(unittest.TestCase):
|
||||||
with self.db as db:
|
with self.db as db:
|
||||||
users = db.list_users()
|
users = db.list_users()
|
||||||
self.assertEqual(0, len(users))
|
self.assertEqual(0, len(users))
|
||||||
db.create_user('testuser', 'supersecurepassword', 'testuser@example.com', True, True)
|
users = db.list_users(with_touchkey=True)
|
||||||
|
self.assertEqual(0, len(users))
|
||||||
|
testuser: User = db.create_user('testuser', 'supersecurepassword', 'testuser@example.com', True, True)
|
||||||
|
db.change_touchkey(testuser, '', 'touchkey', verify_password=False)
|
||||||
db.create_user('anothertestuser', 'otherpassword', 'anothertestuser@example.com', False, True)
|
db.create_user('anothertestuser', 'otherpassword', 'anothertestuser@example.com', False, True)
|
||||||
db.create_user('yatu', 'igotapasswordtoo', 'yatu@example.com', False, False)
|
db.create_user('yatu', 'igotapasswordtoo', 'yatu@example.com', False, False)
|
||||||
users = db.list_users()
|
users = db.list_users()
|
||||||
|
users_with_touchkey = db.list_users(with_touchkey=True)
|
||||||
self.assertEqual(3, len(users))
|
self.assertEqual(3, len(users))
|
||||||
usercheck = {}
|
usercheck = {}
|
||||||
for user in users:
|
for user in users:
|
||||||
|
@ -64,6 +69,16 @@ class DatabaseTest(unittest.TestCase):
|
||||||
self.assertFalse(user.is_admin)
|
self.assertFalse(user.is_admin)
|
||||||
usercheck[user.id] = 1
|
usercheck[user.id] = 1
|
||||||
self.assertEqual(3, len(usercheck))
|
self.assertEqual(3, len(usercheck))
|
||||||
|
self.assertEqual(1, len(users_with_touchkey))
|
||||||
|
self.assertEqual('testuser', users_with_touchkey[0].name)
|
||||||
|
|
||||||
|
def test_has_admin_users(self):
|
||||||
|
with self.db as db:
|
||||||
|
self.assertFalse(db.has_admin_users())
|
||||||
|
testuser: User = db.create_user('testuser', 'supersecurepassword', 'testuser@example.com', True, True)
|
||||||
|
self.assertTrue(db.has_admin_users())
|
||||||
|
db.change_user(testuser, agent=testuser, is_admin=False)
|
||||||
|
self.assertFalse(db.has_admin_users())
|
||||||
|
|
||||||
def test_login(self) -> None:
|
def test_login(self) -> None:
|
||||||
with self.db as db:
|
with self.db as db:
|
||||||
|
|
|
@ -13,3 +13,4 @@ from .deposit import deposit
|
||||||
from .admin import admin
|
from .admin import admin
|
||||||
from .moduser import moduser
|
from .moduser import moduser
|
||||||
from .modproduct import modproduct
|
from .modproduct import modproduct
|
||||||
|
from .userbootstrap import userbootstrap
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from typing import Any, Dict, Union
|
from typing import Any, Dict, Union
|
||||||
|
|
||||||
from matemat.webserver import pagelet, RequestArguments, PageletResponse, TemplateResponse
|
from matemat.webserver import pagelet, RequestArguments, PageletResponse, TemplateResponse, RedirectResponse
|
||||||
from matemat.db import MatematDatabase
|
from matemat.db import MatematDatabase
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,7 +30,10 @@ def main_page(method: str,
|
||||||
authuser=user, products=products, authlevel=authlevel,
|
authuser=user, products=products, authlevel=authlevel,
|
||||||
setupname=config['InstanceName'])
|
setupname=config['InstanceName'])
|
||||||
else:
|
else:
|
||||||
|
# If there are no admin users registered, jump to the admin creation procedure
|
||||||
|
if not db.has_admin_users():
|
||||||
|
return RedirectResponse('/userbootstrap')
|
||||||
# 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()
|
users = db.list_users(with_touchkey=True)
|
||||||
return TemplateResponse('userlist.html',
|
return TemplateResponse('userlist.html',
|
||||||
users=users, setupname=config['InstanceName'])
|
users=users, setupname=config['InstanceName'])
|
||||||
|
|
43
matemat/webserver/pagelets/userbootstrap.py
Normal file
43
matemat/webserver/pagelets/userbootstrap.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
|
||||||
|
from typing import Any, Dict, Union
|
||||||
|
|
||||||
|
from matemat.db import MatematDatabase
|
||||||
|
from matemat.webserver import pagelet, RequestArguments, PageletResponse, RedirectResponse, TemplateResponse
|
||||||
|
from matemat.exceptions import HttpException
|
||||||
|
|
||||||
|
|
||||||
|
@pagelet('/userbootstrap')
|
||||||
|
def userbootstrap(method: str,
|
||||||
|
path: str,
|
||||||
|
args: RequestArguments,
|
||||||
|
session_vars: Dict[str, Any],
|
||||||
|
headers: Dict[str, str],
|
||||||
|
config: Dict[str, str]) \
|
||||||
|
-> Union[bytes, str, PageletResponse]:
|
||||||
|
"""
|
||||||
|
The page for creating a first admin user. To be used when the system is set up the first time, or when there are no
|
||||||
|
admin users left.
|
||||||
|
"""
|
||||||
|
with MatematDatabase(config['DatabaseFile']) as db:
|
||||||
|
# Redirect to main if there are still administrators registered
|
||||||
|
if db.has_admin_users():
|
||||||
|
return RedirectResponse('/')
|
||||||
|
# Process submission
|
||||||
|
if method == 'POST':
|
||||||
|
# Make sure all required values are present
|
||||||
|
if 'username' not in args or 'password' not in args or 'password2' not in args:
|
||||||
|
raise HttpException(400, 'Some arguments are missing')
|
||||||
|
username: str = str(args.username)
|
||||||
|
password: str = str(args.password)
|
||||||
|
password2: str = str(args.password2)
|
||||||
|
# The 2 passwords must match
|
||||||
|
if password != password2:
|
||||||
|
return RedirectResponse('/userbootstrap')
|
||||||
|
# Create the admin user
|
||||||
|
db.create_user(username, password, None, True, False)
|
||||||
|
# Redirect to the main page
|
||||||
|
return RedirectResponse('/')
|
||||||
|
# Requested via GET; show the user creation UI
|
||||||
|
else:
|
||||||
|
return TemplateResponse('userbootstrap.html',
|
||||||
|
setupname=config['InstanceName'])
|
28
templates/userbootstrap.html
Normal file
28
templates/userbootstrap.html
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
{# Show the setup name, as set in the config file, as page title followed by "Setup". Don't escape HTML entities. #}
|
||||||
|
<h1>{{ setupname|safe }} Setup</h1>
|
||||||
|
{{ super() }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
{# Show a user creation form #}
|
||||||
|
Please create an admin user account
|
||||||
|
<form method="post" action="/userbootstrap" accept-charset="UTF-8">
|
||||||
|
<label for="username">Username: </label>
|
||||||
|
<input id="username" type="text" name="username"/><br/>
|
||||||
|
|
||||||
|
<label for="password">Password: </label>
|
||||||
|
<input id="password" type="password" name="password"/><br/>
|
||||||
|
|
||||||
|
<label for="password2">Repeat: </label>
|
||||||
|
<input id="password2" type="password" name="password2"/><br/>
|
||||||
|
|
||||||
|
<input type="submit" value="Create user">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{ super() }}
|
||||||
|
|
||||||
|
{% endblock %}
|
Loading…
Reference in a new issue