forked from s3lph/matemat
first, horribly broken, undocumented implementation of the matemat webapp using jinja2 templates
This commit is contained in:
parent
a89f2cd15d
commit
f699058cf0
37 changed files with 1217 additions and 119 deletions
|
@ -5,6 +5,11 @@ RUN useradd -d /home/matemat -m matemat
|
|||
RUN apt-get update -qy
|
||||
RUN apt-get install -y --no-install-recommends python3-dev python3-pip python3-coverage python3-setuptools build-essential
|
||||
RUN pip3 install wheel pycodestyle mypy
|
||||
ADD . /home/matemat
|
||||
RUN chown matemat:matemat -R /home/matemat
|
||||
RUN pip3 install -r /home/matemat/requirements.txt
|
||||
|
||||
WORKDIR /home/matemat
|
||||
USER matemat
|
||||
CMD python3 -m matemat
|
||||
EXPOSE 8080/tcp
|
||||
|
|
|
@ -13,4 +13,4 @@ if __name__ == '__main__':
|
|||
port = int(sys.argv[1])
|
||||
|
||||
# Start the web server
|
||||
MatematWebserver(port=port).start()
|
||||
MatematWebserver(port=port, webroot='./static').start()
|
||||
|
|
|
@ -73,14 +73,28 @@ 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
|
||||
SELECT user_id, username, email, is_admin, is_member, balance
|
||||
FROM users
|
||||
'''):
|
||||
# Decompose each row and put the values into a User object
|
||||
user_id, username, email, is_admin, is_member = row
|
||||
users.append(User(user_id, username, email, is_admin, is_member))
|
||||
user_id, username, email, is_admin, is_member, balance = row
|
||||
users.append(User(user_id, username, balance, email, is_admin, is_member))
|
||||
return users
|
||||
|
||||
def get_user(self, uid: int) -> User:
|
||||
"""
|
||||
Return a user identified by its user ID
|
||||
:param uid: The user's ID
|
||||
"""
|
||||
with self.db.transaction(exclusive=False) as c:
|
||||
c.execute('SELECT user_id, username, email, is_admin, is_member, balance FROM users WHERE user_id = ?',
|
||||
[uid])
|
||||
row = c.fetchone()
|
||||
if row is None:
|
||||
raise ValueError(f'No user with user ID {uid} exists.')
|
||||
user_id, username, email, is_admin, is_member, balance = row
|
||||
return User(user_id, username, balance, email, is_admin, is_member)
|
||||
|
||||
def create_user(self,
|
||||
username: str,
|
||||
password: str,
|
||||
|
@ -119,7 +133,7 @@ class MatematDatabase(object):
|
|||
# Fetch the new user's rowid.
|
||||
c.execute('SELECT last_insert_rowid()')
|
||||
user_id = int(c.fetchone()[0])
|
||||
return User(user_id, username, email, admin, member)
|
||||
return User(user_id, username, 0, email, admin, member)
|
||||
|
||||
def login(self, username: str, password: Optional[str] = None, touchkey: Optional[str] = None) -> User:
|
||||
"""
|
||||
|
@ -136,21 +150,21 @@ 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
|
||||
SELECT user_id, username, email, password, touchkey, is_admin, is_member, balance
|
||||
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 = row
|
||||
user_id, username, email, pwhash, tkhash, admin, member, balance = row
|
||||
if password is not None and crypt.crypt(password, pwhash) != pwhash:
|
||||
raise AuthenticationError('Password mismatch')
|
||||
elif touchkey is not None and tkhash is not None and crypt.crypt(touchkey, tkhash) != tkhash:
|
||||
raise AuthenticationError('Touchkey mismatch')
|
||||
elif touchkey is not None and tkhash is None:
|
||||
raise AuthenticationError('Touchkey not set')
|
||||
return User(user_id, username, email, admin, member)
|
||||
return User(user_id, username, balance, email, admin, member)
|
||||
|
||||
def change_password(self, user: User, oldpass: str, newpass: str, verify_password: bool = True) -> None:
|
||||
"""
|
||||
|
@ -221,14 +235,18 @@ class MatematDatabase(object):
|
|||
with self.db.transaction() as c:
|
||||
c.execute('''
|
||||
UPDATE users SET
|
||||
username = :username,
|
||||
email = :email,
|
||||
balance = :balance,
|
||||
is_admin = :is_admin,
|
||||
is_member = :is_member,
|
||||
lastchange = STRFTIME('%s', 'now')
|
||||
WHERE user_id = :user_id
|
||||
''', {
|
||||
'user_id': user.id,
|
||||
'username': user.name,
|
||||
'email': user.email,
|
||||
'balance': user.balance,
|
||||
'is_admin': user.is_admin,
|
||||
'is_member': user.is_member
|
||||
})
|
||||
|
@ -261,13 +279,30 @@ class MatematDatabase(object):
|
|||
products: List[Product] = []
|
||||
with self.db.transaction(exclusive=False) as c:
|
||||
for row in c.execute('''
|
||||
SELECT product_id, name, price_member, price_non_member
|
||||
SELECT product_id, name, price_member, price_non_member, stock
|
||||
FROM products
|
||||
'''):
|
||||
product_id, name, price_member, price_external = row
|
||||
products.append(Product(product_id, name, price_member, price_external))
|
||||
product_id, name, price_member, price_external, stock = row
|
||||
products.append(Product(product_id, name, price_member, price_external, stock))
|
||||
return products
|
||||
|
||||
def get_product(self, pid: int) -> Product:
|
||||
"""
|
||||
Return a product identified by its product ID
|
||||
:param pid: The products's ID
|
||||
"""
|
||||
with self.db.transaction(exclusive=False) as c:
|
||||
c.execute('''
|
||||
SELECT product_id, name, price_member, price_non_member, stock
|
||||
FROM products
|
||||
WHERE product_id = ?''',
|
||||
[pid])
|
||||
row = c.fetchone()
|
||||
if row is None:
|
||||
raise ValueError(f'No product with product ID {pid} exists.')
|
||||
product_id, name, price_member, price_non_member, stock = row
|
||||
return Product(product_id, name, price_member, price_non_member, stock)
|
||||
|
||||
def create_product(self, name: str, price_member: int, price_non_member: int) -> Product:
|
||||
"""
|
||||
Creates a new product.
|
||||
|
@ -292,7 +327,7 @@ class MatematDatabase(object):
|
|||
})
|
||||
c.execute('SELECT last_insert_rowid()')
|
||||
product_id = int(c.fetchone()[0])
|
||||
return Product(product_id, name, price_member, price_non_member)
|
||||
return Product(product_id, name, price_member, price_non_member, 0)
|
||||
|
||||
def change_product(self, product: Product) -> None:
|
||||
with self.db.transaction() as c:
|
||||
|
@ -301,13 +336,15 @@ class MatematDatabase(object):
|
|||
SET
|
||||
name = :name,
|
||||
price_member = :price_member,
|
||||
price_non_member = :price_non_member
|
||||
price_non_member = :price_non_member,
|
||||
stock = :stock
|
||||
WHERE product_id = :product_id
|
||||
''', {
|
||||
'product_id': product.id,
|
||||
'name': product.name,
|
||||
'price_member': product.price_member,
|
||||
'price_non_member': product.price_non_member
|
||||
'price_non_member': product.price_non_member,
|
||||
'stock': product.stock
|
||||
})
|
||||
affected = c.execute('SELECT changes()').fetchone()[0]
|
||||
if affected != 1:
|
||||
|
|
|
@ -47,7 +47,7 @@ class DatabaseWrapper(object):
|
|||
SCHEMA = '''
|
||||
CREATE TABLE users (
|
||||
user_id INTEGER PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
email TEXT DEFAULT NULL,
|
||||
password TEXT NOT NULL,
|
||||
touchkey TEXT DEFAULT NULL,
|
||||
|
|
|
@ -8,11 +8,13 @@ class Product(object):
|
|||
product_id: int,
|
||||
name: str,
|
||||
price_member: int,
|
||||
price_non_member: int) -> None:
|
||||
price_non_member: int,
|
||||
stock: int) -> None:
|
||||
self._product_id: int = product_id
|
||||
self._name: str = name
|
||||
self._price_member: int = price_member
|
||||
self._price_non_member: int = price_non_member
|
||||
self._stock: int = stock
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if other is None or not isinstance(other, Product):
|
||||
|
@ -49,3 +51,11 @@ class Product(object):
|
|||
@price_non_member.setter
|
||||
def price_non_member(self, price: int) -> None:
|
||||
self._price_non_member = price
|
||||
|
||||
@property
|
||||
def stock(self) -> int:
|
||||
return self._stock
|
||||
|
||||
@stock.setter
|
||||
def stock(self, stock: int) -> None:
|
||||
self._stock = stock
|
||||
|
|
|
@ -7,6 +7,7 @@ class User(object):
|
|||
def __init__(self,
|
||||
user_id: int,
|
||||
username: str,
|
||||
balance: int,
|
||||
email: Optional[str] = None,
|
||||
admin: bool = False,
|
||||
member: bool = True) -> None:
|
||||
|
@ -15,6 +16,7 @@ class User(object):
|
|||
self._email: Optional[str] = email
|
||||
self._admin: bool = admin
|
||||
self._member: bool = member
|
||||
self._balance: int = balance
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if other is None or not isinstance(other, User):
|
||||
|
@ -33,6 +35,10 @@ class User(object):
|
|||
def name(self) -> str:
|
||||
return self._username
|
||||
|
||||
@name.setter
|
||||
def name(self, value):
|
||||
self._username = value
|
||||
|
||||
@property
|
||||
def email(self) -> Optional[str]:
|
||||
return self._email
|
||||
|
@ -56,3 +62,11 @@ class User(object):
|
|||
@is_member.setter
|
||||
def is_member(self, member: bool) -> None:
|
||||
self._member = member
|
||||
|
||||
@property
|
||||
def balance(self) -> int:
|
||||
return self._balance
|
||||
|
||||
@balance.setter
|
||||
def balance(self, balance: int) -> None:
|
||||
self._balance = balance
|
||||
|
|
|
@ -278,7 +278,9 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||
except ValueError:
|
||||
self.send_response(400, 'Bad Request')
|
||||
self.end_headers()
|
||||
except BaseException:
|
||||
except BaseException as e:
|
||||
print(e)
|
||||
traceback.print_tb(e.__traceback__)
|
||||
# Generic error handling
|
||||
self.send_response(500, 'Internal Server Error')
|
||||
self.end_headers()
|
||||
|
|
|
@ -8,3 +8,8 @@ from .main import main_page
|
|||
from .login import login_page
|
||||
from .logout import logout
|
||||
from .touchkey import touchkey_page
|
||||
from .buy import buy
|
||||
from .deposit import deposit
|
||||
from .admin import admin
|
||||
from .moduser import moduser
|
||||
from .modproduct import modproduct
|
||||
|
|
119
matemat/webserver/pagelets/admin.py
Normal file
119
matemat/webserver/pagelets/admin.py
Normal file
|
@ -0,0 +1,119 @@
|
|||
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
import os
|
||||
|
||||
from matemat.webserver import pagelet, RequestArguments
|
||||
from matemat.db import MatematDatabase
|
||||
from matemat.primitives import User
|
||||
from matemat.exceptions import DatabaseConsistencyError
|
||||
|
||||
|
||||
@pagelet('/admin')
|
||||
def admin(method: str,
|
||||
path: str,
|
||||
args: RequestArguments,
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str]) \
|
||||
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||
env = Environment(loader=FileSystemLoader('templates'))
|
||||
if 'authentication_level' not in session_vars or 'authenticated_user' not in session_vars:
|
||||
headers['Location'] = '/login'
|
||||
return 301, None
|
||||
authlevel: int = session_vars['authentication_level']
|
||||
uid: int = session_vars['authenticated_user']
|
||||
if authlevel < 2:
|
||||
return 403, None
|
||||
|
||||
with MatematDatabase('test.db') as db:
|
||||
user = db.get_user(uid)
|
||||
if method == 'POST' and 'change' in args:
|
||||
handle_change(args, user, db)
|
||||
elif method == 'POST' and 'adminchange' in args and user.is_admin:
|
||||
handle_admin_change(args, db)
|
||||
|
||||
users = db.list_users()
|
||||
products = db.list_products()
|
||||
template = env.get_template('admin.html')
|
||||
return 200, template.render(user=user, authlevel=authlevel, users=users, products=products)
|
||||
|
||||
|
||||
def handle_change(args: RequestArguments, user: User, db: MatematDatabase) -> None:
|
||||
try:
|
||||
change = str(args.change)
|
||||
|
||||
if change == 'account':
|
||||
username = str(args.username)
|
||||
email = str(args.email)
|
||||
if len(email) == 0:
|
||||
email = None
|
||||
oldname = user.name
|
||||
oldmail = user.email
|
||||
try:
|
||||
user.name = username
|
||||
user.email = email
|
||||
db.change_user(user)
|
||||
except DatabaseConsistencyError:
|
||||
user.name = oldname
|
||||
user.email = oldmail
|
||||
|
||||
elif change == 'password':
|
||||
oldpass = str(args.oldpass)
|
||||
newpass = str(args.newpass)
|
||||
newpass2 = str(args.newpass2)
|
||||
if newpass != newpass2:
|
||||
raise ValueError('New passwords don\'t match')
|
||||
db.change_password(user, oldpass, newpass)
|
||||
|
||||
elif change == 'touchkey':
|
||||
oldpass = str(args.oldpass)
|
||||
touchkey = str(args.touchkey)
|
||||
if len(touchkey) == 0:
|
||||
touchkey = None
|
||||
db.change_touchkey(user, oldpass, touchkey)
|
||||
|
||||
elif change == 'avatar':
|
||||
avatar = bytes(args.avatar)
|
||||
os.makedirs('./static/img/thumbnails/users/', exist_ok=True)
|
||||
with open(f'./static/img/thumbnails/users/{user.id}.png', 'wb') as f:
|
||||
f.write(avatar)
|
||||
|
||||
except UnicodeDecodeError:
|
||||
raise ValueError('an argument not a string')
|
||||
|
||||
|
||||
def handle_admin_change(args: RequestArguments, db: MatematDatabase):
|
||||
try:
|
||||
change = str(args.adminchange)
|
||||
|
||||
if change == 'newuser':
|
||||
username = str(args.username)
|
||||
email = str(args.email)
|
||||
if len(email) == 0:
|
||||
email = None
|
||||
password = str(args.password)
|
||||
is_member = 'ismember' in args
|
||||
is_admin = 'isadmin' in args
|
||||
db.create_user(username, password, email, member=is_member, admin=is_admin)
|
||||
|
||||
elif change == 'newproduct':
|
||||
name = str(args.name)
|
||||
price_member = int(str(args.pricemember))
|
||||
price_non_member = int(str(args.pricenonmember))
|
||||
newproduct = db.create_product(name, price_member, price_non_member)
|
||||
if 'image' in args:
|
||||
image = bytes(args.image)
|
||||
os.makedirs('./static/img/thumbnails/products/', exist_ok=True)
|
||||
with open(f'./static/img/thumbnails/products/{newproduct.id}.png', 'wb') as f:
|
||||
f.write(image)
|
||||
|
||||
elif change == 'restock':
|
||||
productid = int(str(args.productid))
|
||||
amount = int(str(args.amount))
|
||||
product = db.get_product(productid)
|
||||
db.restock(product, amount)
|
||||
|
||||
except UnicodeDecodeError:
|
||||
raise ValueError('an argument not a string')
|
30
matemat/webserver/pagelets/buy.py
Normal file
30
matemat/webserver/pagelets/buy.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
from matemat.webserver import pagelet, RequestArguments
|
||||
from matemat.primitives import User
|
||||
from matemat.db import MatematDatabase
|
||||
|
||||
|
||||
@pagelet('/buy')
|
||||
def buy(method: str,
|
||||
path: str,
|
||||
args: RequestArguments,
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str]) \
|
||||
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||
if 'authenticated_user' not in session_vars:
|
||||
headers['Location'] = '/'
|
||||
return 301, None
|
||||
with MatematDatabase('test.db') as db:
|
||||
uid: int = session_vars['authenticated_user']
|
||||
user = db.get_user(uid)
|
||||
if 'n' in args:
|
||||
n = int(str(args.n))
|
||||
else:
|
||||
n = 1
|
||||
if 'pid' in args:
|
||||
pid = int(str(args.pid))
|
||||
product = db.get_product(pid)
|
||||
db.increment_consumption(user, product, n)
|
||||
headers['Location'] = '/'
|
||||
return 301, None
|
25
matemat/webserver/pagelets/deposit.py
Normal file
25
matemat/webserver/pagelets/deposit.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
from matemat.webserver import pagelet, RequestArguments
|
||||
from matemat.primitives import User
|
||||
from matemat.db import MatematDatabase
|
||||
|
||||
|
||||
@pagelet('/deposit')
|
||||
def deposit(method: str,
|
||||
path: str,
|
||||
args: RequestArguments,
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str]) \
|
||||
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||
if 'authenticated_user' not in session_vars:
|
||||
headers['Location'] = '/'
|
||||
return 301, None
|
||||
with MatematDatabase('test.db') as db:
|
||||
uid: int = session_vars['authenticated_user']
|
||||
user = db.get_user(uid)
|
||||
if 'n' in args:
|
||||
n = int(str(args.n))
|
||||
db.deposit(user, n)
|
||||
headers['Location'] = '/'
|
||||
return 301, None
|
|
@ -1,6 +1,8 @@
|
|||
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from matemat.exceptions import AuthenticationError
|
||||
from matemat.webserver import pagelet, RequestArguments
|
||||
from matemat.primitives import User
|
||||
|
@ -14,42 +16,22 @@ def login_page(method: str,
|
|||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str])\
|
||||
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||
if 'user' in session_vars:
|
||||
if 'authenticated_user' in session_vars:
|
||||
headers['Location'] = '/'
|
||||
return 301, None
|
||||
return 301, bytes()
|
||||
env = Environment(loader=FileSystemLoader('templates'))
|
||||
if method == 'GET':
|
||||
data = '''
|
||||
<DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Matemat</title>
|
||||
<style>
|
||||
body {{
|
||||
color: #f0f0f0;
|
||||
background: #000000;
|
||||
}};
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Matemat</h1>
|
||||
{msg}
|
||||
<form action="/login" method="post">
|
||||
Username: <input type="text" name="username"/><br/>
|
||||
Password: <input type="password" name="password" /><br/>
|
||||
<input type="submit" value="Login"/>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
return 200, data.format(msg=str(args.msg) if 'msg' in args else '')
|
||||
template = env.get_template('login.html')
|
||||
return 200, template.render()
|
||||
elif method == 'POST':
|
||||
with MatematDatabase('test.db') as db:
|
||||
try:
|
||||
user: User = db.login(str(args.username), str(args.password))
|
||||
except AuthenticationError:
|
||||
headers['Location'] = '/login?msg=Username%20or%20password%20wrong.%20Please%20try%20again.'
|
||||
headers['Location'] = '/login'
|
||||
return 301, bytes()
|
||||
session_vars['user'] = user
|
||||
session_vars['authenticated_user'] = user.id
|
||||
session_vars['authentication_level'] = 2
|
||||
headers['Location'] = '/'
|
||||
return 301, bytes()
|
||||
return 301, None
|
||||
return 405, None
|
||||
|
|
|
@ -11,7 +11,8 @@ def logout(method: str,
|
|||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str])\
|
||||
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||
if 'user' in session_vars:
|
||||
del session_vars['user']
|
||||
if 'authenticated_user' in session_vars:
|
||||
del session_vars['authenticated_user']
|
||||
session_vars['authentication_level'] = 0
|
||||
headers['Location'] = '/'
|
||||
return 301, None
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from matemat.webserver import pagelet, RequestArguments
|
||||
from matemat.primitives import User
|
||||
from matemat.db import MatematDatabase
|
||||
|
||||
|
||||
|
@ -13,45 +14,16 @@ def main_page(method: str,
|
|||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str])\
|
||||
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||
data = '''
|
||||
<DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Matemat</title>
|
||||
<style>
|
||||
body {{
|
||||
color: #f0f0f0;
|
||||
background: #000000;
|
||||
}};
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Matemat</h1>
|
||||
{user}
|
||||
<ul>
|
||||
{list}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
try:
|
||||
with MatematDatabase('test.db') as db:
|
||||
if 'user' in session_vars:
|
||||
user: User = session_vars['user']
|
||||
products = db.list_products()
|
||||
plist = '\n'.join([f'<li/> <b>{p.name}</b> ' +
|
||||
f'{p.price_member//100 if user.is_member else p.price_non_member//100}' +
|
||||
f'.{p.price_member%100 if user.is_member else p.price_non_member%100}'
|
||||
for p in products])
|
||||
uname = f'<b>{user.name}</b> <a href="/logout">(Logout)</a>'
|
||||
data = data.format(user=uname, list=plist)
|
||||
else:
|
||||
users = db.list_users()
|
||||
ulist = '\n'.join([f'<li/> <b><a href=/touchkey?username={u.name}>{u.name}</a></b>' for u in users])
|
||||
ulist = ulist + '<li/> <a href=/login>Password login</a>'
|
||||
data = data.format(user='', list=ulist)
|
||||
return 200, data
|
||||
except BaseException as e:
|
||||
import traceback
|
||||
traceback.print_tb(e.__traceback__)
|
||||
return 500, None
|
||||
env = Environment(loader=FileSystemLoader('templates'))
|
||||
with MatematDatabase('test.db') as db:
|
||||
if 'authenticated_user' in session_vars:
|
||||
uid: int = session_vars['authenticated_user']
|
||||
authlevel: int = session_vars['authentication_level']
|
||||
user = db.get_user(uid)
|
||||
products = db.list_products()
|
||||
template = env.get_template('productlist.html')
|
||||
return 200, template.render(user=user, products=products, authlevel=authlevel)
|
||||
else:
|
||||
users = db.list_users()
|
||||
template = env.get_template('userlist.html')
|
||||
return 200, template.render(users=users)
|
||||
|
|
84
matemat/webserver/pagelets/modproduct.py
Normal file
84
matemat/webserver/pagelets/modproduct.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
import os
|
||||
|
||||
from matemat.webserver import pagelet, RequestArguments
|
||||
from matemat.db import MatematDatabase
|
||||
from matemat.primitives import Product
|
||||
from matemat.exceptions import DatabaseConsistencyError
|
||||
|
||||
|
||||
@pagelet('/modproduct')
|
||||
def modproduct(method: str,
|
||||
path: str,
|
||||
args: RequestArguments,
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str]) \
|
||||
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||
env = Environment(loader=FileSystemLoader('templates'))
|
||||
if 'authentication_level' not in session_vars or 'authenticated_user' not in session_vars:
|
||||
headers['Location'] = '/login'
|
||||
return 301, None
|
||||
authlevel: int = session_vars['authentication_level']
|
||||
auth_uid: int = session_vars['authenticated_user']
|
||||
if authlevel < 2:
|
||||
return 403, None
|
||||
|
||||
with MatematDatabase('test.db') as db:
|
||||
authuser = db.get_user(auth_uid)
|
||||
if not authuser.is_admin:
|
||||
return 403, None
|
||||
if 'productid' not in args:
|
||||
return 400, None
|
||||
|
||||
modproduct_id = int(str(args.productid))
|
||||
product = db.get_product(modproduct_id)
|
||||
|
||||
if 'change' in args:
|
||||
handle_change(args, product, db)
|
||||
if str(args.change) == 'del':
|
||||
headers['Location'] = '/admin'
|
||||
return 301, None
|
||||
|
||||
template = env.get_template('modproduct.html')
|
||||
return 200, template.render(product=product, authlevel=authlevel)
|
||||
|
||||
|
||||
def handle_change(args: RequestArguments, product: Product, db: MatematDatabase) -> None:
|
||||
change = str(args.change)
|
||||
|
||||
if change == 'del':
|
||||
db.delete_product(product)
|
||||
try:
|
||||
os.remove(f'./static/img/thumbnails/products/{product.id}.png')
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
elif change == 'update':
|
||||
name = str(args.name)
|
||||
price_member = int(str(args.pricemember))
|
||||
price_non_member = int(str(args.pricenonmember))
|
||||
stock = int(str(args.stock))
|
||||
oldname = product.name
|
||||
oldprice_member = product.price_member
|
||||
oldprice_non_member = product.price_non_member
|
||||
oldstock = product.stock
|
||||
product.name = name
|
||||
product.price_member = price_member
|
||||
product.price_non_member = price_non_member
|
||||
product.stock = stock
|
||||
try:
|
||||
db.change_product(product)
|
||||
except DatabaseConsistencyError:
|
||||
product.name = oldname
|
||||
product.email = oldprice_member
|
||||
product.is_member = oldprice_non_member
|
||||
product.stock = oldstock
|
||||
if 'image' in args:
|
||||
image = bytes(args.image)
|
||||
if len(image) > 0:
|
||||
os.makedirs('./static/img/thumbnails/products/', exist_ok=True)
|
||||
with open(f'./static/img/thumbnails/products/{product.id}.png', 'wb') as f:
|
||||
f.write(image)
|
92
matemat/webserver/pagelets/moduser.py
Normal file
92
matemat/webserver/pagelets/moduser.py
Normal file
|
@ -0,0 +1,92 @@
|
|||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
import os
|
||||
|
||||
from matemat.webserver import pagelet, RequestArguments
|
||||
from matemat.db import MatematDatabase
|
||||
from matemat.primitives import User
|
||||
from matemat.exceptions import DatabaseConsistencyError
|
||||
|
||||
|
||||
@pagelet('/moduser')
|
||||
def moduser(method: str,
|
||||
path: str,
|
||||
args: RequestArguments,
|
||||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str]) \
|
||||
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||
env = Environment(loader=FileSystemLoader('templates'))
|
||||
if 'authentication_level' not in session_vars or 'authenticated_user' not in session_vars:
|
||||
headers['Location'] = '/login'
|
||||
return 301, None
|
||||
authlevel: int = session_vars['authentication_level']
|
||||
auth_uid: int = session_vars['authenticated_user']
|
||||
if authlevel < 2:
|
||||
return 403, None
|
||||
|
||||
with MatematDatabase('test.db') as db:
|
||||
authuser = db.get_user(auth_uid)
|
||||
if not authuser.is_admin:
|
||||
return 403, None
|
||||
if 'userid' not in args:
|
||||
return 400, None
|
||||
|
||||
moduser_id = int(str(args.userid))
|
||||
user = db.get_user(moduser_id)
|
||||
|
||||
if 'change' in args:
|
||||
handle_change(args, user, db)
|
||||
if str(args.change) == 'del':
|
||||
headers['Location'] = '/admin'
|
||||
return 301, None
|
||||
|
||||
template = env.get_template('moduser.html')
|
||||
return 200, template.render(user=user, authlevel=authlevel)
|
||||
|
||||
|
||||
def handle_change(args: RequestArguments, user: User, db: MatematDatabase) -> None:
|
||||
change = str(args.change)
|
||||
|
||||
if change == 'del':
|
||||
db.delete_user(user)
|
||||
try:
|
||||
os.remove(f'./static/img/thumbnails/users/{user.id}.png')
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
elif change == 'update':
|
||||
username = str(args.username)
|
||||
email = str(args.email)
|
||||
password = str(args.password)
|
||||
balance = int(str(args.balance))
|
||||
if len(email) == 0:
|
||||
email = None
|
||||
oldname = user.name
|
||||
oldmail = user.email
|
||||
oldmember = user.is_member
|
||||
oldadmin = user.is_admin
|
||||
oldbalance = user.balance
|
||||
user.name = username
|
||||
user.email = email
|
||||
user.is_member = 'ismember' in args
|
||||
user.is_admin = 'isadmin' in args
|
||||
user.balance = balance
|
||||
try:
|
||||
db.change_user(user)
|
||||
except DatabaseConsistencyError:
|
||||
user.name = oldname
|
||||
user.email = oldmail
|
||||
user.is_member = oldmember
|
||||
user.is_admin = oldadmin
|
||||
user.balance = oldbalance
|
||||
if len(password) > 0:
|
||||
db.change_password(user, '', password, verify_password=False)
|
||||
if 'avatar' in args:
|
||||
avatar = bytes(args.avatar)
|
||||
if len(avatar) > 0:
|
||||
os.makedirs('./static/img/thumbnails/products/', exist_ok=True)
|
||||
with open(f'./static/img/thumbnails/products/{user.id}.png', 'wb') as f:
|
||||
f.write(avatar)
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from matemat.exceptions import AuthenticationError
|
||||
from matemat.webserver import pagelet, RequestArguments
|
||||
from matemat.primitives import User
|
||||
|
@ -14,41 +16,22 @@ def touchkey_page(method: str,
|
|||
session_vars: Dict[str, Any],
|
||||
headers: Dict[str, str])\
|
||||
-> Tuple[int, Optional[Union[str, bytes]]]:
|
||||
if 'user' in session_vars:
|
||||
if 'authenticated_user' in session_vars:
|
||||
headers['Location'] = '/'
|
||||
return 301, bytes()
|
||||
env = Environment(loader=FileSystemLoader('templates'))
|
||||
if method == 'GET':
|
||||
data = '''
|
||||
<DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Matemat</title>
|
||||
<style>
|
||||
body {{
|
||||
color: #f0f0f0;
|
||||
background: #000000;
|
||||
}};
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Matemat</h1>
|
||||
<form action="/touchkey" method="post">
|
||||
<input type="hidden" name="username" value="{username}"/><br/>
|
||||
Touchkey: <input type="password" name="touchkey" /><br/>
|
||||
<input type="submit" value="Login"/>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
return 200, data.format(username=str(args.username) if 'username' in args else '')
|
||||
template = env.get_template('touchkey.html')
|
||||
return 200, template.render(username=str(args.username), uid=int(str(args.uid)))
|
||||
elif method == 'POST':
|
||||
with MatematDatabase('test.db') as db:
|
||||
try:
|
||||
user: User = db.login(str(args.username), touchkey=str(args.touchkey))
|
||||
except AuthenticationError:
|
||||
headers['Location'] = f'/touchkey?username={args["username"]}&msg=Please%20try%20again.'
|
||||
headers['Location'] = f'/touchkey?uid={str(args.uid)}&username={str(args.username)}&fail=1'
|
||||
return 301, bytes()
|
||||
session_vars['user'] = user
|
||||
session_vars['authenticated_user'] = user.id
|
||||
session_vars['authentication_level'] = 1
|
||||
headers['Location'] = '/'
|
||||
return 301, None
|
||||
return 405, None
|
||||
|
|
|
@ -46,8 +46,11 @@ def _parse_multipart(body: bytes, boundary: str) -> List[RequestArgument]:
|
|||
# Add header to hdr dict
|
||||
hk, hv = head.decode('utf-8').split(':')
|
||||
hdr[hk.strip()] = hv.strip()
|
||||
# At least Content-Type and Content-Disposition must be present
|
||||
if 'Content-Type' not in hdr or 'Content-Disposition' not in hdr:
|
||||
# No content type set - set broadest possible type
|
||||
if 'Content-Type' not in hdr:
|
||||
hdr['Content-Type'] = 'application/octet-stream'
|
||||
# At least Content-Disposition must be present
|
||||
if 'Content-Disposition' not in hdr:
|
||||
raise ValueError('Missing Content-Type or Content-Disposition header')
|
||||
# Extract Content-Disposition header value and its arguments
|
||||
cd, *cdargs = hdr['Content-Disposition'].split(';')
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
apsw
|
||||
jinja2
|
||||
|
|
BIN
static/img/thumbnails/products/1.png
Normal file
BIN
static/img/thumbnails/products/1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 118 KiB |
BIN
static/img/thumbnails/products/2.png
Normal file
BIN
static/img/thumbnails/products/2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 477 KiB |
BIN
static/img/thumbnails/users/1.png
Normal file
BIN
static/img/thumbnails/users/1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.5 KiB |
BIN
static/img/thumbnails/users/2.png
Normal file
BIN
static/img/thumbnails/users/2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
127
static/js/touchkey.js
Normal file
127
static/js/touchkey.js
Normal file
|
@ -0,0 +1,127 @@
|
|||
|
||||
HTMLCollection.prototype.forEach = Array.prototype.forEach;
|
||||
HTMLCollection.prototype.slice = Array.prototype.slice;
|
||||
|
||||
initTouchkey = (keepPattern, svgid, formid, formfieldid) => {
|
||||
|
||||
let svg = document.getElementById(svgid);
|
||||
let form;
|
||||
if (formid !== null) {
|
||||
form = document.getElementById(formid);
|
||||
}
|
||||
let formfield = document.getElementById(formfieldid);
|
||||
|
||||
let currentStroke = null;
|
||||
let strokeId = 0;
|
||||
let doneMap = {};
|
||||
let enteredKey = '';
|
||||
|
||||
let drawLine = (fromX, fromY, toX, toY) => {
|
||||
let line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
let id = 'l-' + (strokeId++);
|
||||
let idAttr = document.createAttribute('id');
|
||||
let classAttr = document.createAttribute('class');
|
||||
let x1attr = document.createAttribute('x1');
|
||||
let y1attr = document.createAttribute('y1');
|
||||
let x2attr = document.createAttribute('x2');
|
||||
let y2attr = document.createAttribute('y2');
|
||||
let styleAttr = document.createAttribute('style');
|
||||
idAttr.value = id;
|
||||
classAttr.value = 'l';
|
||||
x1attr.value = fromX;
|
||||
y1attr.value = fromY;
|
||||
x2attr.value = toX;
|
||||
y2attr.value = toY;
|
||||
styleAttr.value = 'stroke: grey; stroke-width: 5%; stroke-linecap: round';
|
||||
line.setAttributeNode(idAttr);
|
||||
line.setAttributeNode(classAttr);
|
||||
line.setAttributeNode(x1attr);
|
||||
line.setAttributeNode(y1attr);
|
||||
line.setAttributeNode(x2attr);
|
||||
line.setAttributeNode(y2attr);
|
||||
line.setAttributeNode(styleAttr);
|
||||
svg.appendChild(line);
|
||||
return id;
|
||||
};
|
||||
|
||||
let endPath = () => {
|
||||
svg.removeChild(svg.getElementById(currentStroke));
|
||||
currentStroke = null;
|
||||
};
|
||||
|
||||
let clearTouchkey = () => {
|
||||
doneMap = {};
|
||||
enteredKey = '';
|
||||
svg.getElementsByClassName('l').slice().reverse().forEach((line) => {
|
||||
svg.removeChild(line);
|
||||
});
|
||||
};
|
||||
|
||||
svg.onmousedown = (ev) => {
|
||||
clearTouchkey();
|
||||
let svgrect = svg.getBoundingClientRect();
|
||||
let minId = '';
|
||||
let minDist = Infinity;
|
||||
let minx = 0;
|
||||
let miny = 0;
|
||||
doneMap = {};
|
||||
document.getElementsByClassName('c').forEach((circle) => {
|
||||
let x = parseFloat(circle.getAttribute('cx')) / 100.0 * svgrect.width;
|
||||
let y = parseFloat(circle.getAttribute('cy')) / 100.0 * svgrect.height;
|
||||
let dist = Math.pow(ev.offsetX - x, 2) + Math.pow(ev.offsetY - y, 2);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
minId = circle.id;
|
||||
minx = x;
|
||||
miny = y;
|
||||
}
|
||||
});
|
||||
currentStroke = drawLine(minx, miny, ev.offsetX, ev.offsetY);
|
||||
doneMap[minId] = 1;
|
||||
enteredKey += minId;
|
||||
};
|
||||
|
||||
svg.onmouseup = (ev) => {
|
||||
endPath();
|
||||
formfield.value = enteredKey;
|
||||
if (keepPattern !== true) {
|
||||
clearTouchkey();
|
||||
}
|
||||
if (formid !== null) {
|
||||
form.submit();
|
||||
}
|
||||
};
|
||||
|
||||
svg.onmousemove = (ev) => {
|
||||
if (currentStroke != null) {
|
||||
let svgrect = svg.getBoundingClientRect();
|
||||
let minId = '';
|
||||
let minDist = Infinity;
|
||||
let minx = 0;
|
||||
let miny = 0;
|
||||
document.getElementsByClassName('c').forEach((circle) => {
|
||||
let x = parseFloat(circle.getAttribute('cx')) / 100.0 * svgrect.width;
|
||||
let y = parseFloat(circle.getAttribute('cy')) / 100.0 * svgrect.height;
|
||||
let dist = Math.pow(ev.offsetX - x, 2) + Math.pow(ev.offsetY - y, 2);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
minId = circle.id;
|
||||
minx = x;
|
||||
miny = y;
|
||||
}
|
||||
});
|
||||
if (minDist < 2000 && !(minId in doneMap)) {
|
||||
let line = svg.getElementById(currentStroke);
|
||||
line.setAttribute('x2', minx);
|
||||
line.setAttribute('y2', miny);
|
||||
currentStroke = drawLine(minx, miny, ev.offsetX, ev.offsetY);
|
||||
doneMap[minId] = 1;
|
||||
enteredKey += minId;
|
||||
}
|
||||
let line = svg.getElementById(currentStroke);
|
||||
line.setAttribute('x2', ev.offsetX);
|
||||
line.setAttribute('y2', ev.offsetY);
|
||||
}
|
||||
};
|
||||
|
||||
};
|
22
templates/admin.html
Normal file
22
templates/admin.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block header %}
|
||||
{% if user.is_admin %}
|
||||
<h1>Administration</h1>
|
||||
{% else %}
|
||||
<h1>Settings</h1>
|
||||
{% endif %}
|
||||
{{ super() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% include "admin_all.html" %}
|
||||
|
||||
{% if user.is_admin %}
|
||||
{% include "admin_restricted.html" %}
|
||||
{% endif %}
|
||||
|
||||
{{ super() }}
|
||||
|
||||
{% endblock %}
|
71
templates/admin_all.html
Normal file
71
templates/admin_all.html
Normal file
|
@ -0,0 +1,71 @@
|
|||
<section id="admin-myaccount">
|
||||
<h2>My Account</h2>
|
||||
|
||||
<form id="admin-myaccount-form" method="post" action="/admin?change=account">
|
||||
<label for="admin-myaccount-username">Username: </label>
|
||||
<input id="admin-myaccount-username" type="text" name="username" value="{{ user.name }}" /><br/>
|
||||
|
||||
<label for="admin-myaccount-email">E-Mail: </label>
|
||||
<input id="admin-myaccount-email" type="text" name="email" value="{% if user.email is not none %}{{ user.email }}{% endif %}" /><br/>
|
||||
|
||||
<label for="admin-myaccount-ismember">Member: </label>
|
||||
<input id="admin-myaccount-ismember" type="checkbox" disabled="disabled" {% if user.is_member %} checked="checked" {% endif %}/><br/>
|
||||
|
||||
<label for="admin-myaccount-isadmin">Admin: </label>
|
||||
<input id="admin-myaccount-isadmin" type="checkbox" disabled="disabled" {% if user.is_admin %} checked="checked" {% endif %}/><br/>
|
||||
|
||||
<input type="submit" value="Save changes" />
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="admin-avatar">
|
||||
<h2>Avatar</h2>
|
||||
|
||||
<form id="admin-avatar-form" method="post" action="/admin?change=avatar" enctype="multipart/form-data">
|
||||
<img src="/img/thumbnails/users/{{ user.id }}.png" alt="Avatar of {{ user.name }}" /><br/>
|
||||
|
||||
<label for="admin-avatar-avatar">Upload new file: </label>
|
||||
<input id="admin-avatar-avatar" type="file" name="avatar" accept="image/png" /><br/>
|
||||
|
||||
<input type="submit" value="Save changes" />
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="admin-password">
|
||||
<h2>Password</h2>
|
||||
|
||||
<form id="admin-password-form" method="post" action="/admin?change=password">
|
||||
<label for="admin-password-oldpass">Current password: </label>
|
||||
<input id="admin-password-oldpass" type="password" name="oldpass" /><br/>
|
||||
|
||||
<label for="admin-password-newpass">New password: </label>
|
||||
<input id="admin-password-newpass" type="password" name="newpass" /><br/>
|
||||
|
||||
<label for="admin-password-newpass2">Repeat password: </label>
|
||||
<input id="admin-password-newpass2" type="password" name="newpass2" /><br/>
|
||||
|
||||
<input type="submit" value="Save changes" />
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="admin-touchkey">
|
||||
<h2>Touchkey</h2>
|
||||
|
||||
<form id="admin-touchkey-form" method="post" action="/admin?change=touchkey">
|
||||
<label for="admin-touchkey-oldpass">Current password: </label>
|
||||
<input id="admin-touchkey-oldpass" type="password" name="oldpass" /><br/>
|
||||
|
||||
Draw a new touchkey (leave empty to disable):
|
||||
<br/>
|
||||
{% include "touchkey.svg" %}
|
||||
<br/>
|
||||
<input id="admin-touchkey-touchkey" type="hidden" name="touchkey" value="" />
|
||||
|
||||
<input type="submit" value="Save changes" />
|
||||
</form>
|
||||
|
||||
<script src="/js/touchkey.js" ></script>
|
||||
<script>
|
||||
initTouchkey(true, 'touchkey-svg', null, 'admin-touchkey-touchkey');
|
||||
</script>
|
||||
</section>
|
90
templates/admin_restricted.html
Normal file
90
templates/admin_restricted.html
Normal file
|
@ -0,0 +1,90 @@
|
|||
<section id="admin-restricted-newuser">
|
||||
<h2>Create New User</h2>
|
||||
|
||||
<form id="admin-newuser-form" method="post" action="/admin?adminchange=newuser">
|
||||
<label for="admin-newuser-username">Username: </label>
|
||||
<input id="admin-newuser-username" type="text" name="username" /><br/>
|
||||
|
||||
<label for="admin-newuser-email">E-Mail (optional): </label>
|
||||
<input id="admin-newuser-email" type="text" name="email" /><br/>
|
||||
|
||||
<label for="admin-newuser-password">Password: </label>
|
||||
<input id="admin-newuser-password" type="password" name="password" /><br/>
|
||||
|
||||
<label for="admin-newuser-ismember">Member: </label>
|
||||
<input id="admin-newuser-ismember" type="checkbox" name="ismember" /><br/>
|
||||
|
||||
<label for="admin-newuser-isadmin">Admin: </label>
|
||||
<input id="admin-newuser-isadmin" type="checkbox" name="isadmin" /><br/>
|
||||
|
||||
<input type="submit" value="Create User" />
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="admin-restricted-moduser">
|
||||
<h2>Modify User</h2>
|
||||
|
||||
<form id="admin-moduser-form" method="get" action="/moduser">
|
||||
<label for="admin-moduser-userid">Username: </label>
|
||||
<select id="admin-moduser-userid" name="userid">
|
||||
{% for user in users %}
|
||||
<option value="{{ user.id }}">{{ user.name }}</option>
|
||||
{% endfor %}
|
||||
</select><br/>
|
||||
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="admin-restricted-newproduct">
|
||||
<h2>Create New Product</h2>
|
||||
|
||||
<form id="admin-newproduct-form" method="post" action="/admin?adminchange=newproduct" enctype="multipart/form-data">
|
||||
<label for="admin-newproduct-name">Name: </label>
|
||||
<input id="admin-newproduct-name" type="text" name="name" /><br/>
|
||||
|
||||
<label for="admin-newproduct-price-member">Member price: </label>
|
||||
<input id="admin-newproduct-price-member" type="number" min="0" name="pricemember" /><br/>
|
||||
|
||||
<label for="admin-newproduct-price-non-member">Non-member price: </label>
|
||||
<input id="admin-newproduct-price-non-member" type="number" min="0" name="pricenonmember" /><br/>
|
||||
|
||||
<label for="admin-newproduct-image">Image: </label>
|
||||
<input id="admin-newproduct-image" type="file" accept="image/png" /><br/>
|
||||
|
||||
<input type="submit" value="Create Product" />
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="admin-restricted-restock">
|
||||
<h2>Restock Product</h2>
|
||||
|
||||
<form id="admin-restock-form" method="post" action="/admin?adminchange=restock">
|
||||
<label for="admin-restock-productid">Product: </label>
|
||||
<select id="admin-restock-productid" name="productid">
|
||||
{% for product in products %}
|
||||
<option value="{{ product.id }}">{{ product.name }} ({{ product.stock }})</option>
|
||||
{% endfor %}
|
||||
</select><br/>
|
||||
|
||||
<label for="admin-restock-amount">Amount: </label>
|
||||
<input id="admin-restock-amount" type="number" min="0" name="amount" /><br/>
|
||||
|
||||
<input type="submit" value="Restock" />
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="admin-restricted-modproduct">
|
||||
<h2>Modify Product</h2>
|
||||
|
||||
<form id="admin-modproduct-form" method="get" action="/modproduct">
|
||||
<label for="admin-modproduct-productid">Product: </label>
|
||||
<select id="admin-modproduct-productid" name="productid">
|
||||
{% for product in products %}
|
||||
<option value="{{ product.id }}">{{ product.name }}</option>
|
||||
{% endfor %}
|
||||
</select><br/>
|
||||
|
||||
<input type="submit" value="Go">
|
||||
</form>
|
||||
</section>
|
36
templates/base.html
Normal file
36
templates/base.html
Normal file
|
@ -0,0 +1,36 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{% block head %}
|
||||
<title>Matemat</title>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
{% block header %}
|
||||
|
||||
<a href="/">Home</a>
|
||||
{% if authlevel|default(0) > 1 %}
|
||||
{% if user.is_admin %}
|
||||
<a href="/admin">Administration</a>
|
||||
{% else %}
|
||||
<a href="/admin">Settings</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</header>
|
||||
<main>
|
||||
{% block main %}{% endblock %}
|
||||
</main>
|
||||
<footer>
|
||||
{% block footer %}
|
||||
<ul>
|
||||
<li> Matemat
|
||||
<li> © 2018 s3lph
|
||||
<li> MIT License
|
||||
<li> <a href="https://gitlab.com/s3lph/matemat">Source & Documentation</a>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
23
templates/login.html
Normal file
23
templates/login.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
|
||||
{% block header %}
|
||||
<h1>Welcome</h1>
|
||||
{{ super() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<form method="post" action="/login" id="loginform">
|
||||
<label for="login-username">Username: </label>
|
||||
<input id="login-username" type="text" name="username" /><br/>
|
||||
|
||||
<label for="login-password">Password: </label>
|
||||
<input id="login-password" type="password" name="password" /><br/>
|
||||
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
|
||||
{{ super() }}
|
||||
|
||||
{% endblock %}
|
45
templates/modproduct.html
Normal file
45
templates/modproduct.html
Normal file
|
@ -0,0 +1,45 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block header %}
|
||||
<h1>Administration</h1>
|
||||
{{ super() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<section id="modproduct">
|
||||
<h2>Modify {{ product.name }}</h2>
|
||||
|
||||
<form id="modproduct-form" method="post" action="/modproduct?change=update" enctype="multipart/form-data">
|
||||
<label for="modproduct-name">Name: </label>
|
||||
<input id="modproduct-name" type="text" name="name" value="{{ product.name }}" /><br/>
|
||||
|
||||
<label for="modproduct-price-member">Member price: </label>
|
||||
<input id="modproduct-price-member" type="text" name="pricemember" value="{{ product.price_member }}" /><br/>
|
||||
|
||||
<label for="modproduct-price-non-member">Non-member price: </label>
|
||||
<input id="modproduct-price-non-member" type="text" name="pricenonmember" value="{{ product.price_non_member }}" /><br/>
|
||||
|
||||
<label for="modproduct-balance">Stock: </label>
|
||||
<input id="modproduct-balance" name="stock" type="text" value="{{ product.stock }}" /><br/>
|
||||
|
||||
<label for="modproduct-image">
|
||||
<img height="150" src="/img/thumbnails/products/{{ product.id }}.png" alt="Image of {{ product.name }}" />
|
||||
</label><br/>
|
||||
<input id="modproduct-image" type="file" name="image" accept="image/png" /><br/>
|
||||
|
||||
<input id="modproduct-productid" type="hidden" name="productid" value="{{ product.id }}" /><br/>
|
||||
|
||||
<input type="submit" value="Save changes">
|
||||
</form>
|
||||
|
||||
<form id="modproduct-delproduct-form" method="post" action="/modproduct?change=del">
|
||||
<input id="modproduct-delproduct-userid" type="hidden" name="productid" value="{{ product.id }}" /><br/>
|
||||
<input type="submit" value="Delete product" />
|
||||
</form>
|
||||
|
||||
</section>
|
||||
|
||||
{{ super() }}
|
||||
|
||||
{% endblock %}
|
51
templates/moduser.html
Normal file
51
templates/moduser.html
Normal file
|
@ -0,0 +1,51 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block header %}
|
||||
<h1>Administration</h1>
|
||||
{{ super() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<section id="moduser-account">
|
||||
<h2>Modify {{ user.name }}</h2>
|
||||
|
||||
<form id="moduser-account-form" method="post" action="/moduser?change=update" enctype="multipart/form-data">
|
||||
<label for="moduser-account-username">Username: </label>
|
||||
<input id="moduser-account-username" type="text" name="username" value="{{ user.name }}" /><br/>
|
||||
|
||||
<label for="moduser-account-email">E-Mail: </label>
|
||||
<input id="moduser-account-email" type="text" name="email" value="{% if user.email is not none %}{{ user.email }}{% endif %}" /><br/>
|
||||
|
||||
<label for="moduser-account-password">Password: </label>
|
||||
<input id="moduser-account-password" type="password" name="password" /><br/>
|
||||
|
||||
<label for="moduser-account-ismember">Member: </label>
|
||||
<input id="moduser-account-ismember" name="ismember" type="checkbox" {% if user.is_member %} checked="checked" {% endif %}/><br/>
|
||||
|
||||
<label for="moduser-account-isadmin">Admin: </label>
|
||||
<input id="moduser-account-isadmin" name="isadmin" type="checkbox" {% if user.is_admin %} checked="checked" {% endif %}/><br/>
|
||||
|
||||
<label for="moduser-account-balance">Balance: </label>
|
||||
<input id="moduser-account-balance" name="balance" type="text" value="{{ user.balance }}" /><br/>
|
||||
|
||||
<label for="moduser-account-avatar">
|
||||
<img height="150" src="/img/thumbnails/users/{{ user.id }}.png" alt="Avatar of {{ user.name }}" />
|
||||
</label><br/>
|
||||
<input id="moduser-account-avatar" type="file" name="avatar" accept="image/png" /><br/>
|
||||
|
||||
<input id="moduser-account-userid" type="hidden" name="userid" value="{{ user.id }}" /><br/>
|
||||
|
||||
<input type="submit" value="Save changes">
|
||||
</form>
|
||||
|
||||
<form id="moduser-deluser-form" method="post" action="/moduser?change=del">
|
||||
<input id="moduser-deluser-userid" type="hidden" name="userid" value="{{ user.id }}" /><br/>
|
||||
<input type="submit" value="Delete user" />
|
||||
</form>
|
||||
|
||||
</section>
|
||||
|
||||
{{ super() }}
|
||||
|
||||
{% endblock %}
|
36
templates/productlist.html
Normal file
36
templates/productlist.html
Normal file
|
@ -0,0 +1,36 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block header %}
|
||||
<h1>Welcome, {{ user.name }}</h1>
|
||||
|
||||
{{ super() }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
Your balance: {{ user.balance }}
|
||||
|
||||
<a href="/deposit?n=100">Deposit CHF 1</a>
|
||||
<a href="/deposit?n=1000">Deposit CHF 10</a>
|
||||
|
||||
{% for product in products %}
|
||||
<div class="thumblist-item">
|
||||
<a href="/buy?pid={{ product.id }}">
|
||||
<span class="thumblist-title">{{ product.name }}</span>
|
||||
<span class="thumblist-detail">Price:
|
||||
{% if user.is_member %}
|
||||
{{ product.price_member }}
|
||||
{% else %}
|
||||
{{ product.price_non_member }}
|
||||
{% endif %}
|
||||
; Stock: {{ product.stock }}</span><br/>
|
||||
<img height="150" src="/img/thumbnails/products/{{ product.id }}.png" alt="Picture of {{ product.name }}" />
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<a href="/logout">Logout</a>
|
||||
|
||||
{{ super() }}
|
||||
|
||||
{% endblock %}
|
35
templates/touchkey.html
Normal file
35
templates/touchkey.html
Normal file
|
@ -0,0 +1,35 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<style>
|
||||
svg {
|
||||
width: 600px;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<h1>Welcome, {{ username }}</h1>
|
||||
{{ super() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include "touchkey.svg" %}
|
||||
|
||||
<form method="post" action="/touchkey" id="loginform">
|
||||
<input type="hidden" name="uid" value="{{ uid }}" />
|
||||
<input type="hidden" name="username" value="{{ username }}" />
|
||||
<input type="hidden" name="touchkey" value="" id="loginform-touchkey-value" />
|
||||
</form>
|
||||
<a href="/">Cancel</a>
|
||||
|
||||
<script src="/js/touchkey.js"></script>
|
||||
<script>
|
||||
initTouchkey(false, 'touchkey-svg', 'loginform', 'loginform-touchkey-value');
|
||||
</script>
|
||||
|
||||
{{ super() }}
|
||||
|
||||
{% endblock %}
|
21
templates/touchkey.svg
Normal file
21
templates/touchkey.svg
Normal file
|
@ -0,0 +1,21 @@
|
|||
<svg id="touchkey-svg" width="400" height="400">
|
||||
<circle class="c" id="0" cx="12.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||
<circle class="c" id="1" cx="37.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||
<circle class="c" id="2" cx="62.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||
<circle class="c" id="3" cx="87.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||
|
||||
<circle class="c" id="4" cx="12.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||
<circle class="c" id="5" cx="37.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||
<circle class="c" id="6" cx="62.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||
<circle class="c" id="7" cx="87.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||
|
||||
<circle class="c" id="8" cx="12.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||
<circle class="c" id="9" cx="37.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||
<circle class="c" id="a" cx="62.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||
<circle class="c" id="b" cx="87.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||
|
||||
<circle class="c" id="c" cx="12.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||
<circle class="c" id="d" cx="37.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||
<circle class="c" id="e" cx="62.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||
<circle class="c" id="f" cx="87.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
19
templates/userlist.html
Normal file
19
templates/userlist.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block header %}
|
||||
<h1>Welcome</h1>
|
||||
{{ super() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% for user in users %}
|
||||
<div class="thumblist-item">
|
||||
<a href="/touchkey?uid={{ user.id }}&username={{ user.name }}">
|
||||
<span class="thumblist-title">{{ user.name }}</span><br/>
|
||||
<img height="150" src="/img/thumbnails/users/{{ user.id }}.png" alt="Avatar of {{ user.name }}" />
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<a href="/login">Password-based login</a>
|
||||
{{ super() }}
|
||||
{% endblock %}
|
10
testing/Dockerfile
Normal file
10
testing/Dockerfile
Normal file
|
@ -0,0 +1,10 @@
|
|||
|
||||
FROM debian:buster
|
||||
|
||||
RUN useradd -d /home/matemat -m matemat
|
||||
RUN apt-get update -qy
|
||||
RUN apt-get install -y --no-install-recommends python3-dev python3-pip python3-coverage python3-setuptools build-essential
|
||||
RUN pip3 install wheel pycodestyle mypy
|
||||
|
||||
WORKDIR /home/matemat
|
||||
USER matemat
|
147
touchkey.html
Normal file
147
touchkey.html
Normal file
|
@ -0,0 +1,147 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
svg {
|
||||
width: 600px;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Welcome, {{username}}</h1>
|
||||
|
||||
<svg id="svg" width="400" height="400">
|
||||
<circle class="c" id="0" cx="12.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="1" cx="37.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="2" cx="62.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="3" cx="87.5%" cy="12.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
|
||||
<circle class="c" id="4" cx="12.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="5" cx="37.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="6" cx="62.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="7" cx="87.5%" cy="37.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
|
||||
<circle class="c" id="8" cx="12.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="9" cx="37.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="a" cx="62.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="b" cx="87.5%" cy="62.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
|
||||
<circle class="c" id="c" cx="12.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="d" cx="37.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="e" cx="62.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
<circle class="c" id="f" cx="87.5%" cy="87.5%" r="10%" stroke="grey" stroke-width="2%" fill="white" />
|
||||
</svg>
|
||||
|
||||
<script>
|
||||
|
||||
HTMLCollection.prototype.forEach = Array.prototype.forEach;
|
||||
HTMLCollection.prototype.slice = Array.prototype.slice;
|
||||
|
||||
mouseDown = false;
|
||||
currentStroke = null;
|
||||
strokeId = 0;
|
||||
doneMap = {};
|
||||
enteredKey = '';
|
||||
|
||||
svg = document.getElementById('svg');
|
||||
|
||||
drawLine = (fromX, fromY, toX, toY) => {
|
||||
var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
var id = 'l-' + (strokeId++);
|
||||
var idAttr = document.createAttribute('id');
|
||||
var classAttr = document.createAttribute('class');
|
||||
var x1attr = document.createAttribute('x1');
|
||||
var y1attr = document.createAttribute('y1');
|
||||
var x2attr = document.createAttribute('x2');
|
||||
var y2attr = document.createAttribute('y2');
|
||||
var styleAttr = document.createAttribute('style');
|
||||
idAttr.value = id;
|
||||
classAttr.value = 'l';
|
||||
x1attr.value = fromX;
|
||||
y1attr.value = fromY;
|
||||
x2attr.value = toX;
|
||||
y2attr.value = toY;
|
||||
styleAttr.value = 'stroke: grey; stroke-width: 5%; stroke-linecap: round';
|
||||
line.setAttributeNode(idAttr);
|
||||
line.setAttributeNode(classAttr);
|
||||
line.setAttributeNode(x1attr);
|
||||
line.setAttributeNode(y1attr);
|
||||
line.setAttributeNode(x2attr);
|
||||
line.setAttributeNode(y2attr);
|
||||
line.setAttributeNode(styleAttr);
|
||||
svg.appendChild(line);
|
||||
return id;
|
||||
};
|
||||
|
||||
svg.onmousedown = (ev) => {
|
||||
var svgrect = svg.getBoundingClientRect();
|
||||
var minId = '';
|
||||
var minDist = Infinity;
|
||||
var minx = 0;
|
||||
var miny = 0;
|
||||
doneMap = {};
|
||||
document.getElementsByClassName('c').forEach((circle) => {
|
||||
var x = parseFloat(circle.getAttribute('cx'))/100.0 * svgrect.width;
|
||||
var y = parseFloat(circle.getAttribute('cy'))/100.0 * svgrect.height;
|
||||
var dist = Math.pow(ev.offsetX - x, 2) + Math.pow(ev.offsetY - y, 2);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
minId = circle.id;
|
||||
minx = x;
|
||||
miny = y;
|
||||
}
|
||||
});
|
||||
var minNode = svg.getElementById(minId);
|
||||
currentStroke = drawLine(minx, miny, ev.offsetX, ev.offsetY);
|
||||
doneMap[minId] = 1;
|
||||
enteredKey += minId;
|
||||
};
|
||||
|
||||
svg.onmouseup = (ev) => {
|
||||
currentStroke = null;
|
||||
doneMap = {};
|
||||
console.log(enteredKey);
|
||||
enteredKey = '';
|
||||
svg.getElementsByClassName('l').slice().reverse().forEach((line) => {
|
||||
svg.removeChild(line);
|
||||
});
|
||||
};
|
||||
|
||||
svg.onmousemove = (ev) => {
|
||||
if (currentStroke != null) {
|
||||
var svgrect = svg.getBoundingClientRect();
|
||||
var minId = '';
|
||||
var minDist = Infinity;
|
||||
var minx = 0;
|
||||
var miny = 0;
|
||||
document.getElementsByClassName('c').forEach((circle) => {
|
||||
var x = parseFloat(circle.getAttribute('cx'))/100.0 * svgrect.width;
|
||||
var y = parseFloat(circle.getAttribute('cy'))/100.0 * svgrect.height;
|
||||
var dist = Math.pow(ev.offsetX - x, 2) + Math.pow(ev.offsetY - y, 2);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
minId = circle.id;
|
||||
minx = x;
|
||||
miny = y;
|
||||
}
|
||||
});
|
||||
if (minDist < 2000 && !(minId in doneMap)) {
|
||||
var line = svg.getElementById(currentStroke);
|
||||
line.setAttribute('x2', minx);
|
||||
line.setAttribute('y2', miny);
|
||||
currentStroke = drawLine(minx, miny, ev.offsetX, ev.offsetY);
|
||||
doneMap[minId] = 1;
|
||||
enteredKey += minId;
|
||||
}
|
||||
var line = svg.getElementById(currentStroke);
|
||||
line.setAttribute('x2', ev.offsetX);
|
||||
line.setAttribute('y2', ev.offsetY);
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue